Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
19549c6
fix: Amount is out of view when max number of digits
daledah Feb 4, 2026
6b153d5
resolve conflict
daledah Feb 6, 2026
ed93321
fix: comments
daledah Feb 9, 2026
ea527ff
Merge branch 'main' into fix/81243
daledah Feb 11, 2026
9179923
Merge branch 'main' into fix/81243
daledah Feb 19, 2026
4c248ac
Merge branch 'main' into fix/81243
daledah Feb 23, 2026
83c03c7
Merge branch 'main' into fix/81243
daledah Feb 25, 2026
57770a9
Merge branch 'main' into fix/81243
daledah Mar 2, 2026
b646417
add ManualSubmitToDestinationVisible metric
JakubKorytko Mar 3, 2026
0a78792
add reset logic to ReportScreen & SearchMoneyRequestReportPage
JakubKorytko Mar 3, 2026
0a1d64c
Update training session times for clarity
RachCHopkins Mar 4, 2026
63d0ab1
Merge branch 'main' into korytko/perf/add-e2e-expense-creation
JakubKorytko Mar 4, 2026
db788bb
create submitToDestinationVisible module
JakubKorytko Mar 4, 2026
2955c6a
fix codex follow-ups
JakubKorytko Mar 4, 2026
9c9ca1a
address chatgpt-codex comment
JakubKorytko Mar 4, 2026
06b241b
address staszekscp comment
JakubKorytko Mar 4, 2026
15387f4
Merge branch 'main' into fix/81243
daledah Mar 5, 2026
9bee95f
fix cover zoomed mode
daledah Mar 5, 2026
a94c382
Merge branch 'main' into korytko/perf/add-e2e-expense-creation
JakubKorytko Mar 5, 2026
5249a85
change onLayout placement in transaction list
JakubKorytko Mar 5, 2026
266b9c0
Merge branch 'main' into wt/w1
TMisiukiewicz Mar 6, 2026
91a9ab9
Merge branch 'main' into korytko/perf/add-e2e-expense-creation
JakubKorytko Mar 6, 2026
8726fdc
Add error message files to folder
stephanieelliott Mar 6, 2026
d178715
refactor: expense submit span - follow-up action attribution
JakubKorytko Mar 6, 2026
860e716
Add nested troubleshooting hubs for integration docs
stephanieelliott Mar 6, 2026
863267a
Merge branch 'main' into helpsite-netsuite-error-docs
stephanieelliott Mar 6, 2026
da9c880
Update section.html
stephanieelliott Mar 6, 2026
c402087
Fix ESLint, Prettier, and Jekyll build errors
MelvinBot Mar 6, 2026
29d711a
Fix: use proper empty array initialization in Liquid template
MelvinBot Mar 6, 2026
585c624
Fix tile rendering issue
stephanieelliott Mar 6, 2026
9856091
Fixed rendering issue
stephanieelliott Mar 6, 2026
7b7f089
Fix nested troubleshooting section lookup
stephanieelliott Mar 6, 2026
2feb0e1
Fix time zone abbreviation from CEST to CET
RachCHopkins Mar 8, 2026
163ab7e
Merge branch 'main' into fix/81243
daledah Mar 9, 2026
e0391dd
Merge branch 'main' into korytko/perf/add-e2e-expense-creation
JakubKorytko Mar 9, 2026
d03c01a
Add tryEndSpan to silence noisy logs on report navigation
TMisiukiewicz Mar 9, 2026
7509098
Merge branch 'main' into tomasz/fix-send-message-span-noise
TMisiukiewicz Mar 9, 2026
7e6fbe0
Merge remote-tracking branch 'origin/main' into fix-send-message-span…
TMisiukiewicz Mar 9, 2026
d4dcaa7
Merge remote-tracking branch 'origin/main' into fix-send-message-span…
TMisiukiewicz Mar 9, 2026
fe02765
Auto-close duplicate expense dropdown after action completes
MelvinBot Mar 9, 2026
b0c3c6d
address PR comments
JakubKorytko Mar 9, 2026
3062629
add useSubmitToDestinationVisible
JakubKorytko Mar 9, 2026
b148459
Fix: Skip auto-closing dropdown when shouldCloseModalOnSelect is true…
MelvinBot Mar 9, 2026
408d0ab
use hub.flatSections in section.html to avoid having to traverse sect…
chuckdries Mar 9, 2026
9838892
Merge pull request #84646 from Expensify/chuckdries/helpsite-netsuite…
chuckdries Mar 9, 2026
3ebea85
Merge pull request #84652 from Expensify/main
stephanieelliott Mar 9, 2026
91954ac
Merge pull request #81655 from daledah/fix/81243
Julesssss Mar 9, 2026
d8ac478
Update Mobile-Expensify submodule to 16295ce
OSBotify Mar 9, 2026
746c90b
Merge pull request #84441 from Expensify/helpsite-netsuite-error-docs
deetergp Mar 9, 2026
e5cd992
Merge pull request #84069 from software-mansion-labs/korytko/perf/add…
mountiny Mar 9, 2026
24f15f7
Merge pull request #84570 from callstack-internal/fix-send-message-sp…
mountiny Mar 9, 2026
aaa944b
Merge pull request #84592 from Expensify/claude-fixDuplicateExpenseMo…
chuckdries Mar 10, 2026
26ccc41
Merge pull request #84100 from Expensify/RachCHopkins-patch-4
stephanieelliott Mar 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 68 additions & 58 deletions .github/scripts/createDocsRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Section = {
href: string;
title: string;
articles?: Article[];
sections?: Section[];
};

type Hub = {
Expand Down Expand Up @@ -60,16 +61,14 @@ function toTitleCase(str: string): string {
}

/**
* @param filename - The name of the file (path used for href)
* @param filename - The name of the file
* @param order - Optional order from front matter
* @param titleOverride - Optional display title (e.g. subfolder name: "Export-Errors" -> "Export Errors")
*/
function getArticleObj(filename: string, order?: number, titleOverride?: string): Article {
function getArticleObj(filename: string, order?: number): Article {
const href = filename.replace('.md', '');
const title = titleOverride ? toTitleCase(titleOverride.replaceAll('-', ' ')) : toTitleCase(href.replaceAll('-', ' '));
return {
href,
title,
title: toTitleCase(href.replaceAll('-', ' ')),
order,
};
}
Expand Down Expand Up @@ -97,76 +96,87 @@ function pushOrCreateEntry<TKey extends HubEntriesKey>(hubs: Hub[], hub: string,
}

function getOrderFromArticleFrontMatter(path: string): number | undefined {
const frontmatter = fs.readFileSync(path, 'utf8').split('---').at(1);
if (!frontmatter) {
return;
try {
const frontmatter = fs.readFileSync(path, 'utf8').split('---').at(1);
if (!frontmatter) {
return undefined;
}
const frontmatterObject = yaml.load(frontmatter) as Record<string, unknown>;
return frontmatterObject.order as number | undefined;
} catch {
return undefined;
}
const frontmatterObject = yaml.load(frontmatter) as Record<string, unknown>;
return frontmatterObject.order as number | undefined;
}

/**
* Build a section from a directory path, with optional parent path for nested href
*/
function buildSection(platformName: string, hub: string, sectionPath: string, parentHref: string): Section {
const sectionName = sectionPath.split('/').pop() ?? sectionPath;
const fullPath = `${docsDir}/articles/${platformName}/${hub}/${sectionPath}`;
const articles: Article[] = [];
const childSections: Section[] = [];
const href = parentHref ? `${parentHref}/${sectionName}` : sectionName;

for (const entry of fs.readdirSync(fullPath)) {
const entryPath = `${fullPath}/${entry}`;
if (entry.endsWith('.md')) {
const order = getOrderFromArticleFrontMatter(entryPath);
articles.push(getArticleObj(entry, order));
} else if (fs.statSync(entryPath).isDirectory()) {
childSections.push(buildSection(platformName, hub, `${sectionPath}/${entry}`, href));
}
}

const section: Section = {
href,
title: toTitleCase(sectionName.replaceAll('-', ' ')),
...(articles.length > 0 && {articles}),
...(childSections.length > 0 && {sections: childSections}),
};
return section;
}

/**
* Flatten sections for lookup by full path (e.g. netsuite/troubleshooting/connection-errors)
*/
function flattenSections(sections: Section[]): Section[] {
const result: Section[] = [];
for (const s of sections) {
result.push(s);
if (s.sections?.length) {
result.push(...flattenSections(s.sections));
}
}
return result;
}

/**
* Add articles and sections to hubs
* @param hubs - The hubs inside docs/articles/ for a platform
* @param platformName - Expensify Classic or New Expensify
* @param routeHubs - The hubs insude docs/data/_routes.yml for a platform
* @param routeHubs - The hubs inside docs/data/_routes.yml for a platform
*/
function createHubsWithArticles(hubs: string[], platformName: ValueOf<typeof platformNames>, routeHubs: Hub[]) {
for (const hub of hubs) {
// Iterate through each directory in articles
for (const fileOrFolder of fs.readdirSync(`${docsDir}/articles/${platformName}/${hub}`)) {
// If the directory content is a markdown file, then it is an article
const basePath = `${docsDir}/articles/${platformName}/${hub}`;

for (const fileOrFolder of fs.readdirSync(basePath)) {
if (fileOrFolder.endsWith('.md')) {
const articleObj = getArticleObj(fileOrFolder);
pushOrCreateEntry(routeHubs, hub, 'articles', articleObj);
continue;
}

// For readability, we will use the term section to refer to subfolders
const section = fileOrFolder;
const articles: Article[] = [];

// Section can contain .md files directly and/or subfolders (and nested subfolders) that contain .md files
const sectionPath = `${docsDir}/articles/${platformName}/${hub}/${section}`;
for (const entry of fs.readdirSync(sectionPath)) {
const entryPath = `${sectionPath}/${entry}`;
if (entry.endsWith('.md') && fs.statSync(entryPath).isFile()) {
const order = getOrderFromArticleFrontMatter(entryPath);
articles.push(getArticleObj(entry, order));
continue;
}
if (fs.statSync(entryPath).isDirectory()) {
// One level: section/SubFolder/file.md -> href "SubFolder/file", display title = "Troubleshoot SubFolder"
for (const file of fs.readdirSync(entryPath)) {
const filePath = `${entryPath}/${file}`;
if (file.endsWith('.md') && fs.statSync(filePath).isFile()) {
const order = getOrderFromArticleFrontMatter(filePath);
articles.push(getArticleObj(`${entry}/${file}`, order, `Troubleshoot ${entry}`));
continue;
}
if (fs.statSync(filePath).isDirectory()) {
// Two levels: section/SubFolder/NestedFolder/file.md -> href "SubFolder/NestedFolder/file", display title = "Troubleshoot NestedFolder" (e.g. "Troubleshoot Export Errors")
for (const nestedFile of fs.readdirSync(filePath)) {
if (!nestedFile.endsWith('.md')) {
continue;
}
const nestedPath = `${filePath}/${nestedFile}`;
if (!fs.statSync(nestedPath).isFile()) {
continue;
}
const order = getOrderFromArticleFrontMatter(nestedPath);
articles.push(getArticleObj(`${entry}/${file}/${nestedFile}`, order, `Troubleshoot ${file}`));
}
}
}
}
}
const sectionPath = fileOrFolder;
const section = buildSection(platformName, hub, sectionPath, '');
pushOrCreateEntry(routeHubs, hub, 'sections', section);
}

pushOrCreateEntry(routeHubs, hub, 'sections', {
href: section,
title: toTitleCase(section.replaceAll('-', ' ')),
articles,
});
// Add flat section list for nested section page lookup
const hubObj = routeHubs.find((obj) => obj.href === hub);
if (hubObj?.sections?.length) {
(hubObj as Hub & {flatSections?: Section[]}).flatSections = flattenSections(hubObj.sections);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Mobile-Expensify
120 changes: 15 additions & 105 deletions docs/_includes/section.html
Original file line number Diff line number Diff line change
@@ -1,120 +1,30 @@
{% assign urlArray = page.url | replace: '/', ' ' | split: " " %}

{% assign activePlatform = urlArray[0] %}
{% assign urlParts = page.url | split: '/' %}
{% assign activePlatform = urlParts[1] %}
{% assign platform = site.data.routes.platforms | where: "href", activePlatform | first %}

{% assign activeHub = urlArray[2] %}
{% assign activeHub = urlParts[3] %}
{% assign hub = platform.hubs | where: "href", activeHub | first %}

{% assign activeSection = urlArray[3] | remove: ".html" %}
{% assign section = hub.sections | where: "href", activeSection | first %}
{% assign hubPrefix = "/" | append: activePlatform | append: "/hubs/" | append: activeHub | append: "/" %}
{% assign activeSectionPath = page.url | replace: ".html", "" | replace: hubPrefix, "" %}

{% assign section = hub.flatSections | where: "href", activeSectionPath | first %}

<h1 class="title">
{{ section.title }}
</h1>

<section>
{% assign sortedArticles = section.articles | sort: 'order', 'last' | default: 999 %}

{% comment %} Render direct articles (no nested path) {% endcomment %}
<div class="cards-group">
{% if section.sections %}
{% for subsection in section.sections %}
{% include section-card.html platform=activePlatform hub=hub.href section=subsection.href title=subsection.title %}
{% endfor %}
{% endif %}
{% assign sortedArticles = section.articles | default: empty %}
{% for article in sortedArticles %}
{% unless article.href contains '/' %}
{% assign article_href = section.href | append: '/' | append: article.href %}
{% include article-card.html hub=hub.href href=article_href title=article.title platform=activePlatform %}
{% endunless %}
{% assign article_href = section.href | append: '/' | append: article.href %}
{% include article-card.html hub=hub.href href=article_href title=article.title platform=activePlatform %}
{% endfor %}
</div>

{% comment %} Collect unique subfolder names from nested articles {% endcomment %}
{% assign subfolders = "" %}
{% for article in sortedArticles %}
{% if article.href contains '/' %}
{% assign parts = article.href | split: '/' %}
{% assign subfolder = parts[0] %}
{% assign alreadyAdded = false %}
{% if subfolders != "" %}
{% assign existingSubfolders = subfolders | split: '|' %}
{% for existing in existingSubfolders %}
{% if existing == subfolder %}
{% assign alreadyAdded = true %}
{% endif %}
{% endfor %}
{% endif %}
{% unless alreadyAdded %}
{% if subfolders == "" %}
{% assign subfolders = subfolder %}
{% else %}
{% assign subfolders = subfolders | append: '|' | append: subfolder %}
{% endif %}
{% endunless %}
{% endif %}
{% endfor %}

{% comment %} Render nested articles grouped under subfolder headings {% endcomment %}
{% if subfolders != "" %}
{% assign subfolderArray = subfolders | split: '|' %}
{% for subfolder in subfolderArray %}
<h3 class="title">{{ subfolder | replace: '-', ' ' }}</h3>

{% comment %} Render articles directly under this subfolder (2-segment paths like Troubleshooting/Overview) {% endcomment %}
<div class="cards-group">
{% for article in sortedArticles %}
{% if article.href contains '/' %}
{% assign parts = article.href | split: '/' %}
{% if parts[0] == subfolder and parts.size == 2 %}
{% assign article_href = section.href | append: '/' | append: article.href %}
{% include article-card.html hub=hub.href href=article_href title=article.title platform=activePlatform %}
{% endif %}
{% endif %}
{% endfor %}
</div>

{% comment %} Collect unique sub-subfolders for 3-segment paths (e.g., Troubleshooting/Export-Errors/Overview) {% endcomment %}
{% assign subSubfolders = "" %}
{% for article in sortedArticles %}
{% if article.href contains '/' %}
{% assign parts = article.href | split: '/' %}
{% if parts[0] == subfolder and parts.size >= 3 %}
{% assign subSubfolder = parts[1] %}
{% assign ssAlreadyAdded = false %}
{% if subSubfolders != "" %}
{% assign ssExisting = subSubfolders | split: '|' %}
{% for existing in ssExisting %}
{% if existing == subSubfolder %}
{% assign ssAlreadyAdded = true %}
{% endif %}
{% endfor %}
{% endif %}
{% unless ssAlreadyAdded %}
{% if subSubfolders == "" %}
{% assign subSubfolders = subSubfolder %}
{% else %}
{% assign subSubfolders = subSubfolders | append: '|' | append: subSubfolder %}
{% endif %}
{% endunless %}
{% endif %}
{% endif %}
{% endfor %}

{% comment %} Render sub-subfolder groups {% endcomment %}
{% if subSubfolders != "" %}
{% assign subSubfolderArray = subSubfolders | split: '|' %}
{% for subSubfolder in subSubfolderArray %}
<h4 class="title">{{ subSubfolder | replace: '-', ' ' }}</h4>
<div class="cards-group">
{% for article in sortedArticles %}
{% if article.href contains '/' %}
{% assign parts = article.href | split: '/' %}
{% if parts[0] == subfolder and parts[1] == subSubfolder %}
{% assign article_href = section.href | append: '/' | append: article.href %}
{% include article-card.html hub=hub.href href=article_href title=article.title platform=activePlatform %}
{% endif %}
{% endif %}
{% endfor %}
</div>
{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
</section>
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
title: NS0029 Export Error in NetSuite Integration
description: Learn what the NS0029 export error means when exporting reports to NetSuite and what to do to resolve the connection issue.
keywords: NS0029, NetSuite export error, unable to export report NetSuite, NetSuite connection issue, Expensify NetSuite integration, report export failed NetSuite, Workspace Admin
internalScope: Audience is Workspace Admins using the NetSuite integration. Covers resolving the NS0029 export error caused by NetSuite connection issues. Does not cover general NetSuite integration setup or configuration.
---

# NS0029 Export Error in NetSuite Integration

If you see the error:

NS0029 Export Error: Unable to export this report due to an error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readability suggestion: The error message text on this line is rendered as plain body text, making it hard to distinguish from the surrounding explanatory content. Consider formatting error messages with a blockquote (>) or a code block to make them visually distinct. For example:

> NS0029 Export Error: Unable to export this report due to an error.

This pattern applies to all ~80 troubleshooting articles in this PR. A consistent formatting choice here improves scannability for members who are looking for a specific error string.

This means there is a connection issue between the Workspace and NetSuite.

The error appears when you attempt to export a report to NetSuite and the export fails.

---

## Why the NS0029 Export Error Happens in NetSuite

The NS0029 error typically indicates that Expensify is unable to successfully communicate with NetSuite at the time of export.

Common causes include:

- An expired or disconnected NetSuite integration.
- Authentication issues between Expensify and NetSuite.
- Permission changes in NetSuite.
- Temporary connectivity issues.
- A change to the NetSuite role or token used for the integration.

Because this error is tied to the NetSuite connection, it cannot be resolved directly from the report itself.

This is a connection or authentication issue, not a report data issue.

---

## How to Fix the NS0029 Export Error

Follow the steps below to resolve the issue.

### Retry the Export

1. Open the report.
2. Retry exporting to NetSuite.

If the issue was temporary, the export may complete successfully.

### Contact Concierge for Assistance

If the error persists:

1. Do not delete the report.
2. Reach out to **Concierge**.
3. Include:
- The report ID.
- Confirmation that you’re seeing the **NS0029 Export Error**.
- The approximate time the export was attempted.

Concierge will review the NetSuite connection, check authentication and permission settings, and help restore the integration so you can export the report successfully.

---

# FAQ

## Can I Retry the Export After Seeing the NS0029 Export Error?

Yes. If the issue was temporary, retrying the export may resolve it. If it continues to fail, the integration likely needs review.

## Does the NS0029 Export Error Affect Other Reports?

Yes. If the issue is related to your NetSuite connection or authentication, other report exports may also fail until the connection is fixed.

## Should I Reconnect NetSuite Myself?

Do not disconnect the NetSuite integration unless directed by Concierge. Reconnecting without guidance may require reconfiguration.
Loading
Loading