From 4c1f2532144c9905fbc9459b18d76d093c85e1b2 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Fri, 19 Jun 2026 13:32:45 -0300 Subject: [PATCH 01/15] Rename "Export current view" menu option to "Current view" --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index cafffa8acc4d..c483bf948c5d 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9722,7 +9722,7 @@ const translations = { }, export: { basicExport: 'Basic export', - currentView: 'Export current view', + currentView: 'Current view', reportLevelExport: 'All Data - report level', expenseLevelExport: 'All Data - expense level', exportInProgress: 'Export in progress', diff --git a/src/languages/es.ts b/src/languages/es.ts index 76ba22019233..abffb25c4a31 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -9819,7 +9819,7 @@ ${amount} para ${merchant} - ${date}`, expenseLevelExport: 'Todos los datos - a nivel de gasto', exportInProgress: 'Exportación en curso', conciergeWillSend: 'Concierge te enviará el archivo en breve.', - currentView: 'Exportar vista actual', + currentView: 'Vista actual', }, exportDownload: { preparingTitle: 'Preparando descarga...', From 81aca1bd22512a79a3fc9229da5b0a6e649198d5 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Fri, 19 Jun 2026 13:44:50 -0300 Subject: [PATCH 02/15] Standardize search export filenames to Expensify__.csv --- src/components/ExportDownloadStatusModal.tsx | 3 ++- .../SelectionToolbar/index.tsx | 5 +++-- src/hooks/useExportActions.ts | 7 +++--- src/hooks/useSearchBulkActions.ts | 10 +++++++-- src/hooks/useSelectedTransactionsActions.ts | 4 ++-- .../ExportSearchItemsToCSVParams.ts | 1 + .../ExportSearchWithTemplateParams.ts | 1 + src/libs/actions/Search.ts | 22 +++++++++++++++---- src/libs/fileDownload/DownloadUtils.ts | 6 ++++- src/libs/fileDownload/FileUtils.ts | 10 +++++++++ src/libs/fileDownload/index.android.ts | 16 ++++++++------ src/libs/fileDownload/index.ios.ts | 12 +++++----- src/libs/fileDownload/index.ts | 4 +++- src/libs/fileDownload/types.ts | 1 + tests/unit/FileUtilsTest.ts | 15 +++++++++++++ tests/unit/SearchActionsTest.ts | 3 +++ 16 files changed, 92 insertions(+), 28 deletions(-) diff --git a/src/components/ExportDownloadStatusModal.tsx b/src/components/ExportDownloadStatusModal.tsx index 67e0e130345b..623ab4d0981f 100644 --- a/src/components/ExportDownloadStatusModal.tsx +++ b/src/components/ExportDownloadStatusModal.tsx @@ -70,7 +70,8 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E const isCSV = fileName.endsWith('.csv'); const secureType = isCSV ? 'csvexport' : 'pdfreport'; const url = `${baseURL}secure?secureType=${secureType}&filename=${encodeURIComponent(fileName)}&downloadName=${encodeURIComponent(fileName)}&email=${encodeURIComponent(currentUserLogin)}`; - fileDownload(translate, addEncryptedAuthTokenToURL(url, encryptedAuthToken ?? '', true), fileName, '', isMobileSafari()); + // The backend already names the file `Expensify__`, so skip the timestamp suffix. + fileDownload(translate, addEncryptedAuthTokenToURL(url, encryptedAuthToken ?? '', true), fileName, '', isMobileSafari(), undefined, undefined, undefined, false, false); }; useEffect(() => { diff --git a/src/components/MoneyRequestReportView/SelectionToolbar/index.tsx b/src/components/MoneyRequestReportView/SelectionToolbar/index.tsx index 84e20254a316..790bc22603e6 100644 --- a/src/components/MoneyRequestReportView/SelectionToolbar/index.tsx +++ b/src/components/MoneyRequestReportView/SelectionToolbar/index.tsx @@ -81,7 +81,7 @@ function SelectionToolbar({reportID, transactions, reportActions}: SelectionTool const transactionsWithoutPendingDelete = transactions.filter((t) => !isTransactionPendingDelete(t)); - const beginExportWithTemplate = (templateName: string, templateType: string, transactionIDList: string[]) => { + const beginExportWithTemplate = (templateName: string, templateType: string, transactionIDList: string[], exportName: string) => { if (isOffline) { setOfflineModalVisible(true); return; @@ -98,6 +98,7 @@ function SelectionToolbar({reportID, transactions, reportActions}: SelectionTool reportIDList: [report.reportID], transactionIDList, policyID: policy?.id, + exportName, }); showConfirmModal({ @@ -153,7 +154,7 @@ function SelectionToolbar({reportID, transactions, reportActions}: SelectionTool onExportFailed: () => setIsDownloadErrorModalVisible(true), onExportOffline: () => setOfflineModalVisible(true), policy, - beginExportWithTemplate: (templateName, templateType, transactionIDList) => beginExportWithTemplate(templateName, templateType, transactionIDList), + beginExportWithTemplate: (templateName, templateType, transactionIDList, exportName) => beginExportWithTemplate(templateName, templateType, transactionIDList, exportName), onDeleteSelected, }); diff --git a/src/hooks/useExportActions.ts b/src/hooks/useExportActions.ts index 7dce009065f3..06e12970698e 100644 --- a/src/hooks/useExportActions.ts +++ b/src/hooks/useExportActions.ts @@ -36,7 +36,7 @@ type UseExportActionsParams = { type UseExportActionsReturn = { exportActionEntries: Record> & Pick>; secondaryExportActions: Array>; - beginExportWithTemplate: (templateName: string, templateType: string, transactionIDList: string[], policyID?: string) => void; + beginExportWithTemplate: (templateName: string, templateType: string, transactionIDList: string[], exportName: string, policyID?: string) => void; showOfflineModal: () => void; showDownloadErrorModal: () => void; }; @@ -110,7 +110,7 @@ function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsPa }); }; - const beginExportWithTemplate = (templateName: string, templateType: string, transactionIDList: string[], policyID?: string) => { + const beginExportWithTemplate = (templateName: string, templateType: string, transactionIDList: string[], exportName: string, policyID?: string) => { if (isOffline) { showOfflineModal(); return; @@ -134,6 +134,7 @@ function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsPa reportIDList: [moneyRequestReport.reportID], transactionIDList, policyID, + exportName, }); }; @@ -212,7 +213,7 @@ function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsPa value: template.templateName, description: template.description, sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.EXPORT_FILE, - onSelected: () => beginExportWithTemplate(template.templateName, template.type, transactionIDs, template.policyID), + onSelected: () => beginExportWithTemplate(template.templateName, template.type, transactionIDs, template.name, template.policyID), }; } diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 5377ab8f69cb..3698025c09e8 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -560,7 +560,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(selectedTransactionsKeys); const beginExportWithTemplate = useCallback( - (templateName: string, templateType: string, policyID: string | undefined) => { + (templateName: string, templateType: string, policyID: string | undefined, exportName: string) => { const emptyReports = selectedReports?.filter((selectedReport) => { if (!selectedReport) { @@ -592,6 +592,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { reportIDList: [], transactionIDList: [], policyID, + exportName, }, true, ); @@ -605,6 +606,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { reportIDList: isGroupExport ? [] : selectedTransactionReportIDs, transactionIDList: isGroupExport ? [] : selectedTransactionsKeys, policyID, + exportName, }, true, ); @@ -670,6 +672,8 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { return; } + const exportName = translate(isBasicExport ? 'export.basicExport' : 'export.currentView'); + if (areAllMatchingItemsSelected) { if (selectedTransactionsKeys.length === 0 || status == null || !hash) { return; @@ -683,6 +687,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { transactionIDList: selectedTransactionsKeys, isBasicExport: exportParameters.isBasicExport, exportColumnLabels: exportParameters.exportColumnLabels, + exportName, }); setActiveExportID(exportID); return; @@ -701,6 +706,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { transactionIDList: isGroupExport ? [] : selectedTransactionsKeys, isBasicExport: exportParameters.isBasicExport, exportColumnLabels: exportParameters.exportColumnLabels, + exportName, }, () => { didFail = true; @@ -1492,7 +1498,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { icon: isStandardTemplate ? expensifyIcons.Table : expensifyIcons.TablePencil, description: template.description, onSelected: () => { - beginExportWithTemplate(template.templateName, template.type, template.policyID); + beginExportWithTemplate(template.templateName, template.type, template.policyID, template.name); }, shouldCloseModalOnSelect: true, shouldCallAfterModalHide: true, diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 73ba73b70ac7..461dc1c56230 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -73,7 +73,7 @@ function useSelectedTransactionsActions({ onExportFailed?: () => void; onExportOffline?: () => void; policy?: Policy; - beginExportWithTemplate: (templateName: string, templateType: string, transactionIDList: string[], policyID?: string) => void; + beginExportWithTemplate: (templateName: string, templateType: string, transactionIDList: string[], exportName: string, policyID?: string) => void; isOnSearch?: boolean; onDeleteSelected?: (handleDeleteTransactions: () => void, handleDeleteTransactionsWithNavigation: (backToRoute?: Route) => void) => void | Promise; }) { @@ -411,7 +411,7 @@ function useSelectedTransactionsActions({ text: template.name, icon: isStandardTemplate ? expensifyIcons.Table : expensifyIcons.TablePencil, description: template.description, - onSelected: () => beginExportWithTemplate(template.templateName, template.type, selectedTransactionIDs, template.policyID), + onSelected: () => beginExportWithTemplate(template.templateName, template.type, selectedTransactionIDs, template.name, template.policyID), }); } return exportOptions; diff --git a/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts b/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts index 0d321bf01e48..b7309bf30f30 100644 --- a/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts +++ b/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts @@ -7,6 +7,7 @@ type ExportSearchItemsToCSVParams = { transactionIDList: string[]; isBasicExport: boolean; exportColumnLabels: string; + exportName: string; }; export default ExportSearchItemsToCSVParams; diff --git a/src/libs/API/parameters/ExportSearchWithTemplateParams.ts b/src/libs/API/parameters/ExportSearchWithTemplateParams.ts index 17b24ab4c3f1..4fd69ce08433 100644 --- a/src/libs/API/parameters/ExportSearchWithTemplateParams.ts +++ b/src/libs/API/parameters/ExportSearchWithTemplateParams.ts @@ -7,6 +7,7 @@ type ExportSearchWithTemplateParams = { reportIDList: string[]; transactionIDList: string[]; policyID: string | undefined; + exportName: string; }; export default ExportSearchWithTemplateParams; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 42f554d962b2..77fb3c856987 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -26,6 +26,7 @@ import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import {getCommandURL} from '@libs/ApiUtils'; import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import fileDownload from '@libs/fileDownload'; +import {getExportFileName} from '@libs/fileDownload/FileUtils'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; @@ -1200,7 +1201,7 @@ function rejectMoneyRequestsOnSearch( type Params = Record; function exportSearchItemsToCSV( - {query, jsonQuery, reportIDList, transactionIDList, isBasicExport, exportColumnLabels}: ExportSearchItemsToCSVParams, + {query, jsonQuery, reportIDList, transactionIDList, isBasicExport, exportColumnLabels, exportName}: ExportSearchItemsToCSVParams, onDownloadFailed: () => void, translate: LocalizedTranslate, ) { @@ -1247,10 +1248,21 @@ function exportSearchItemsToCSV( } } - return fileDownload(translate, getCommandURL({command: WRITE_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV}), 'Expensify.csv', '', false, formData, CONST.NETWORK.METHOD.POST, onDownloadFailed); + return fileDownload( + translate, + getCommandURL({command: WRITE_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV}), + getExportFileName(exportName, rand64()), + '', + false, + formData, + CONST.NETWORK.METHOD.POST, + onDownloadFailed, + false, + false, + ); } -function queueExportSearchItemsToCSV({query, jsonQuery, reportIDList, transactionIDList, isBasicExport, exportColumnLabels}: ExportSearchItemsToCSVParams): string { +function queueExportSearchItemsToCSV({query, jsonQuery, reportIDList, transactionIDList, isBasicExport, exportColumnLabels, exportName}: ExportSearchItemsToCSVParams): string { const exportID = rand64(); const onyxKey = `${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}${exportID}` as const; @@ -1276,6 +1288,7 @@ function queueExportSearchItemsToCSV({query, jsonQuery, reportIDList, transactio transactionIDList, isBasicExport, exportColumnLabels, + exportName, exportID, }) as QueueExportSearchItemsToCSVParams; @@ -1285,7 +1298,7 @@ function queueExportSearchItemsToCSV({query, jsonQuery, reportIDList, transactio } function queueExportSearchWithTemplate( - {templateName, templateType, jsonQuery, reportIDList, transactionIDList, policyID}: ExportSearchWithTemplateParams, + {templateName, templateType, jsonQuery, reportIDList, transactionIDList, policyID, exportName}: ExportSearchWithTemplateParams, shouldTrackExportProgress = false, ): string { const exportID = rand64(); @@ -1317,6 +1330,7 @@ function queueExportSearchWithTemplate( reportIDList, transactionIDList, policyID, + exportName, ...(shouldTrackExportProgress ? {exportID} : {}), }) as QueueExportSearchWithTemplateParams; diff --git a/src/libs/fileDownload/DownloadUtils.ts b/src/libs/fileDownload/DownloadUtils.ts index cb64cfdf03ed..5c71f7a3b95c 100644 --- a/src/libs/fileDownload/DownloadUtils.ts +++ b/src/libs/fileDownload/DownloadUtils.ts @@ -38,6 +38,9 @@ const fetchFileDownload: FileDownload = ( formData = undefined, requestType = 'get', onDownloadFailed?: () => void, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + shouldUnlink = false, + appendTimestamp = true, ) => { const resolvedUrl = tryResolveUrlFromApiRoot(url); @@ -72,7 +75,8 @@ const fetchFileDownload: FileDownload = ( .then((blob) => { // Create blob link to download const href = URL.createObjectURL(new Blob([blob])); - const completeFileName = appendTimeToFileName(fileName ?? getFileName(url)); + const resolvedFileName = fileName ?? getFileName(url); + const completeFileName = appendTimestamp ? appendTimeToFileName(resolvedFileName) : resolvedFileName; createDownloadLink(href, completeFileName); }) .catch(() => { diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts index 9a8523e4c731..88529d93f5a0 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -197,6 +197,15 @@ function cleanFileName(fileName: string): string { return fileName.replaceAll(/[^a-zA-Z0-9\-._]/g, '_'); } +/** + * Builds a standardized export filename: `Expensify__.`. + * The export name is sanitized so it is safe to use as a filename, and the unique id keeps + * filenames distinct without relying on a timestamp. + */ +function getExportFileName(exportName: string, uniqueID: string, extension = 'csv'): string { + return `Expensify_${cleanFileName(exportName)}_${uniqueID}.${extension}`; +} + function appendTimeToFileName(fileName: string): string { const file = splitExtensionFromFileName(fileName); @@ -926,6 +935,7 @@ export { getFileName, getFileType, cleanFileName, + getExportFileName, appendTimeToFileName, ANDROID_SAFE_FILE_NAME_LENGTH, truncateFileNameToSafeLengthOnAndroid, diff --git a/src/libs/fileDownload/index.android.ts b/src/libs/fileDownload/index.android.ts index 6a476d0ed9d9..ddd5e604ea34 100644 --- a/src/libs/fileDownload/index.android.ts +++ b/src/libs/fileDownload/index.android.ts @@ -36,14 +36,15 @@ function hasAndroidPermission(): Promise { /** * Handling the download */ -function handleDownload(translate: LocalizedTranslate, url: string, fileName?: string, successMessage?: string, shouldUnlink = true): Promise { +function handleDownload(translate: LocalizedTranslate, url: string, fileName?: string, successMessage?: string, shouldUnlink = true, appendTimestamp = true): Promise { return new Promise((resolve) => { const dirs = RNFetchBlob.fs.dirs; // Android files will download to Download directory const path = dirs.DownloadDir; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null, and since fileName can be an empty string we want to default to `FileUtils.getFileName(url)` - const attachmentName = appendTimeToFileName(fileName || getFileName(url)); + const resolvedFileName = fileName || getFileName(url); + const attachmentName = appendTimestamp ? appendTimeToFileName(resolvedFileName) : resolvedFileName; const isLocalFile = url.startsWith('file://'); @@ -97,7 +98,7 @@ function handleDownload(translate: LocalizedTranslate, url: string, fileName?: s }); } -const postDownloadFile = (translate: LocalizedTranslate, url: string, fileName?: string, formData?: FormData, onDownloadFailed?: () => void): Promise => { +const postDownloadFile = (translate: LocalizedTranslate, url: string, fileName?: string, formData?: FormData, onDownloadFailed?: () => void, appendTimestamp = true): Promise => { const fetchOptions: RequestInit = { method: 'POST', body: formData, @@ -115,7 +116,8 @@ const postDownloadFile = (translate: LocalizedTranslate, url: string, fileName?: return response.text(); }) .then((fileData) => { - const finalFileName = appendTimeToFileName(fileName ?? 'Expensify'); + const resolvedFileName = fileName ?? 'Expensify'; + const finalFileName = appendTimestamp ? appendTimeToFileName(resolvedFileName) : resolvedFileName; const downloadPath = `${RNFS.DownloadDirectoryPath}/${finalFileName}`; return RNFS.writeFile(downloadPath, fileData, 'utf8').then(() => downloadPath); }) @@ -145,15 +147,15 @@ const postDownloadFile = (translate: LocalizedTranslate, url: string, fileName?: /** * Checks permission and downloads the file for Android */ -const fileDownload: FileDownload = (translate, url, fileName, successMessage, _, formData, requestType, onDownloadFailed, shouldUnlink) => +const fileDownload: FileDownload = (translate, url, fileName, successMessage, _, formData, requestType, onDownloadFailed, shouldUnlink, appendTimestamp = true) => new Promise((resolve) => { hasAndroidPermission() .then((hasPermission) => { if (hasPermission) { if (requestType === CONST.NETWORK.METHOD.POST) { - return postDownloadFile(translate, url, fileName, formData, onDownloadFailed); + return postDownloadFile(translate, url, fileName, formData, onDownloadFailed, appendTimestamp); } - return handleDownload(translate, url, fileName, successMessage, shouldUnlink); + return handleDownload(translate, url, fileName, successMessage, shouldUnlink, appendTimestamp); } showPermissionErrorAlert(translate); }) diff --git a/src/libs/fileDownload/index.ios.ts b/src/libs/fileDownload/index.ios.ts index 1157c175ac53..1f2f568201d7 100644 --- a/src/libs/fileDownload/index.ios.ts +++ b/src/libs/fileDownload/index.ios.ts @@ -45,7 +45,7 @@ function downloadFile(fileUrl: string, fileName: string) { }).fetch('GET', fileUrl); } -const postDownloadFile = (translate: LocalizedTranslate, url: string, fileName?: string, formData?: FormData, onDownloadFailed?: () => void) => { +const postDownloadFile = (translate: LocalizedTranslate, url: string, fileName?: string, formData?: FormData, onDownloadFailed?: () => void, appendTimestamp = true) => { const fetchOptions: RequestInit = { method: 'POST', body: formData, @@ -63,7 +63,8 @@ const postDownloadFile = (translate: LocalizedTranslate, url: string, fileName?: return response.text(); }) .then((fileData) => { - const finalFileName = appendTimeToFileName(fileName ?? 'Expensify'); + const resolvedFileName = fileName ?? 'Expensify'; + const finalFileName = appendTimestamp ? appendTimeToFileName(resolvedFileName) : resolvedFileName; const expensifyDir = `${RNFS.DocumentDirectoryPath}/Expensify`; const localPath = `${expensifyDir}/${finalFileName}`; return RNFS.mkdir(expensifyDir).then(() => { @@ -125,12 +126,13 @@ function downloadVideo(fileUrl: string, fileName: string): Promise +const fileDownload: FileDownload = (translate, fileUrl, fileName, successMessage, _, formData, requestType, onDownloadFailed, shouldUnlink, appendTimestamp = true) => new Promise((resolve) => { let fileDownloadPromise; const fileType = getFileType(fileUrl); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null, and since fileName can be an empty string we want to default to `FileUtils.getFileName(url)` - const attachmentName = appendTimeToFileName(fileName || getFileName(fileUrl)); + const resolvedFileName = fileName || getFileName(fileUrl); + const attachmentName = appendTimestamp ? appendTimeToFileName(resolvedFileName) : resolvedFileName; switch (fileType) { case CONST.ATTACHMENT_FILE_TYPE.IMAGE: @@ -141,7 +143,7 @@ const fileDownload: FileDownload = (translate, fileUrl, fileName, successMessage break; default: if (requestType === CONST.NETWORK.METHOD.POST) { - fileDownloadPromise = postDownloadFile(translate, fileUrl, fileName, formData, onDownloadFailed); + fileDownloadPromise = postDownloadFile(translate, fileUrl, fileName, formData, onDownloadFailed, appendTimestamp); break; } diff --git a/src/libs/fileDownload/index.ts b/src/libs/fileDownload/index.ts index 3612d9455a1d..2969a6530806 100644 --- a/src/libs/fileDownload/index.ts +++ b/src/libs/fileDownload/index.ts @@ -13,6 +13,8 @@ const fileDownload: FileDownload = ( formData = undefined, requestType = 'get', onDownloadFailed?: () => void, -) => fetchFileDownload(translate, url, fileName, successMessage, shouldOpenExternalLink, formData, requestType, onDownloadFailed); + shouldUnlink = false, + appendTimestamp = true, +) => fetchFileDownload(translate, url, fileName, successMessage, shouldOpenExternalLink, formData, requestType, onDownloadFailed, shouldUnlink, appendTimestamp); export default fileDownload; diff --git a/src/libs/fileDownload/types.ts b/src/libs/fileDownload/types.ts index e8efbe131f31..038ef0f2e17e 100644 --- a/src/libs/fileDownload/types.ts +++ b/src/libs/fileDownload/types.ts @@ -12,6 +12,7 @@ type FileDownload = ( requestType?: RequestType, onDownloadFailed?: () => void, shouldUnlink?: boolean, + appendTimestamp?: boolean, ) => Promise; type ImageResolution = {width: number; height: number}; type GetImageResolution = (url: File | Asset) => Promise; diff --git a/tests/unit/FileUtilsTest.ts b/tests/unit/FileUtilsTest.ts index 1859b8fae032..1ed7893e154a 100644 --- a/tests/unit/FileUtilsTest.ts +++ b/tests/unit/FileUtilsTest.ts @@ -6,6 +6,7 @@ import { ANDROID_SAFE_FILE_NAME_LENGTH, appendTimeToFileName, canvasFallback, + getExportFileName, getFileValidationErrorText, getImageDimensionsAfterResize, splitExtensionFromFileName, @@ -98,6 +99,20 @@ describe('FileUtils', () => { }); }); + describe('getExportFileName', () => { + it('builds the Expensify__ filename and sanitizes the export name', () => { + expect(getExportFileName('Current view', '123abc')).toEqual('Expensify_Current_view_123abc.csv'); + }); + + it('replaces every illegal character in the export name with an underscore', () => { + expect(getExportFileName('All Data - expense level', 'abc')).toEqual('Expensify_All_Data_-_expense_level_abc.csv'); + }); + + it('honors a custom extension', () => { + expect(getExportFileName('Current view', 'abc', 'xlsx')).toEqual('Expensify_Current_view_abc.xlsx'); + }); + }); + describe('canvasFallback', () => { const mockCreateImageBitmap = jest.fn(); const mockCanvas = { diff --git a/tests/unit/SearchActionsTest.ts b/tests/unit/SearchActionsTest.ts index 7c5f3bb228c5..6a693b800bc1 100644 --- a/tests/unit/SearchActionsTest.ts +++ b/tests/unit/SearchActionsTest.ts @@ -41,6 +41,7 @@ describe('queueExportSearchItemsToCSV', () => { transactionIDList: [], isBasicExport: true, exportColumnLabels: '{}', + exportName: 'Basic export', }); expect(typeof exportID).toBe('string'); @@ -76,6 +77,7 @@ describe('queueExportSearchWithTemplate', () => { reportIDList: [], transactionIDList: [], policyID: 'policy123', + exportName: 'Test Template', }, true, ); @@ -108,6 +110,7 @@ describe('queueExportSearchWithTemplate', () => { reportIDList: [], transactionIDList: [], policyID: 'policy123', + exportName: 'Test Template', }); const finalParameters = mockWrite.mock.calls.at(-1)?.at(1); From 7c20177aa07f6f5750bbb165aa2a2728ded8c388 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Fri, 19 Jun 2026 16:00:29 -0300 Subject: [PATCH 03/15] Use sentence case for All Data export labels --- src/languages/en.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index c483bf948c5d..6686eb331465 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9723,8 +9723,8 @@ const translations = { export: { basicExport: 'Basic export', currentView: 'Current view', - reportLevelExport: 'All Data - report level', - expenseLevelExport: 'All Data - expense level', + reportLevelExport: 'All data - report level', + expenseLevelExport: 'All data - expense level', exportInProgress: 'Export in progress', conciergeWillSend: 'Concierge will send you the file shortly.', }, From 5a387d61302d7cc6e50e105314a9fbe28fa359b7 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Fri, 19 Jun 2026 16:29:53 -0300 Subject: [PATCH 04/15] Lowercase export filenames and revert All Data label casing --- src/languages/en.ts | 4 ++-- src/libs/fileDownload/FileUtils.ts | 8 ++++---- tests/unit/FileUtilsTest.ts | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 6686eb331465..c483bf948c5d 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9723,8 +9723,8 @@ const translations = { export: { basicExport: 'Basic export', currentView: 'Current view', - reportLevelExport: 'All data - report level', - expenseLevelExport: 'All data - expense level', + reportLevelExport: 'All Data - report level', + expenseLevelExport: 'All Data - expense level', exportInProgress: 'Export in progress', conciergeWillSend: 'Concierge will send you the file shortly.', }, diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts index 88529d93f5a0..185cc57d96fb 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -198,12 +198,12 @@ function cleanFileName(fileName: string): string { } /** - * Builds a standardized export filename: `Expensify__.`. - * The export name is sanitized so it is safe to use as a filename, and the unique id keeps - * filenames distinct without relying on a timestamp. + * Builds a standardized export filename: `expensify__.`. + * The export name is sanitized and the whole name is lowercased so it is safe and consistent + * to use as a filename, and the unique id keeps filenames distinct without relying on a timestamp. */ function getExportFileName(exportName: string, uniqueID: string, extension = 'csv'): string { - return `Expensify_${cleanFileName(exportName)}_${uniqueID}.${extension}`; + return `expensify_${cleanFileName(exportName)}_${uniqueID}.${extension}`.toLowerCase(); } function appendTimeToFileName(fileName: string): string { diff --git a/tests/unit/FileUtilsTest.ts b/tests/unit/FileUtilsTest.ts index 1ed7893e154a..bd81ad06bf69 100644 --- a/tests/unit/FileUtilsTest.ts +++ b/tests/unit/FileUtilsTest.ts @@ -100,16 +100,16 @@ describe('FileUtils', () => { }); describe('getExportFileName', () => { - it('builds the Expensify__ filename and sanitizes the export name', () => { - expect(getExportFileName('Current view', '123abc')).toEqual('Expensify_Current_view_123abc.csv'); + it('builds the expensify__ filename, sanitizes and lowercases the export name', () => { + expect(getExportFileName('Current view', '123abc')).toEqual('expensify_current_view_123abc.csv'); }); it('replaces every illegal character in the export name with an underscore', () => { - expect(getExportFileName('All Data - expense level', 'abc')).toEqual('Expensify_All_Data_-_expense_level_abc.csv'); + expect(getExportFileName('All Data - expense level', 'abc')).toEqual('expensify_all_data_-_expense_level_abc.csv'); }); it('honors a custom extension', () => { - expect(getExportFileName('Current view', 'abc', 'xlsx')).toEqual('Expensify_Current_view_abc.xlsx'); + expect(getExportFileName('Current view', 'abc', 'xlsx')).toEqual('expensify_current_view_abc.xlsx'); }); }); From 443c4326b49a343c158b142ca3c61b24c490d843 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Mon, 22 Jun 2026 10:52:10 -0300 Subject: [PATCH 05/15] translations --- src/languages/de.ts | 2 +- src/languages/fr.ts | 2 +- src/languages/it.ts | 2 +- src/languages/ja.ts | 2 +- src/languages/nl.ts | 2 +- src/languages/pl.ts | 2 +- src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index bc3747c7f540..715aa3ab75c3 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -9708,7 +9708,7 @@ Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`, expenseLevelExport: 'Alle Daten – Ausgabenebene', exportInProgress: 'Export wird ausgeführt', conciergeWillSend: 'Concierge wird dir die Datei in Kürze senden.', - currentView: 'Aktuelle Ansicht exportieren', + currentView: 'Aktuelle Ansicht', }, exportDownload: { preparingTitle: 'Preparing download...', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index d11270506d4d..a235a3746609 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -9740,7 +9740,7 @@ Voici un *reçu test* pour vous montrer comment ça fonctionne :`, expenseLevelExport: 'Toutes les données - niveau dépense', exportInProgress: 'Export en cours', conciergeWillSend: 'Concierge vous enverra le fichier sous peu.', - currentView: 'Exporter la vue actuelle', + currentView: 'Vue actuelle', }, exportDownload: { preparingTitle: 'Preparing download...', diff --git a/src/languages/it.ts b/src/languages/it.ts index c810e1ed480a..17f2e7ee5faa 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -9696,7 +9696,7 @@ Ecco una *ricevuta di prova* per mostrarti come funziona:`, expenseLevelExport: 'Tutti i dati - livello spesa', exportInProgress: 'Esportazione in corso', conciergeWillSend: 'Concierge ti invierà il file a breve.', - currentView: 'Esporta vista corrente', + currentView: 'Vista corrente', }, exportDownload: { preparingTitle: 'Preparing download...', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index f3749ef6ba99..f0473a928d13 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -9573,7 +9573,7 @@ ${reportName}`, expenseLevelExport: 'すべてのデータ - 経費レベル', exportInProgress: 'エクスポート処理中', conciergeWillSend: 'Conciergeがまもなくファイルを送信します。', - currentView: '現在のビューをエクスポート', + currentView: '現在のビュー', }, exportDownload: { preparingTitle: 'Preparing download...', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 16407d9f6acf..815543c250b8 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -9662,7 +9662,7 @@ Hier is een *proefbon* om je te laten zien hoe het werkt:`, expenseLevelExport: 'Alle gegevens - uitgaveniveau', exportInProgress: 'Export bezig', conciergeWillSend: 'Concierge stuurt je het bestand zo meteen.', - currentView: 'Huidige weergave exporteren', + currentView: 'Huidige weergave', }, exportDownload: { preparingTitle: 'Preparing download...', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 31a534f28c84..5b008f5a5480 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -9647,7 +9647,7 @@ Oto *paragon testowy*, żeby pokazać Ci, jak to działa:`, expenseLevelExport: 'Wszystkie dane – poziom wydatku', exportInProgress: 'Trwa eksport', conciergeWillSend: 'Concierge wkrótce wyśle Ci plik.', - currentView: 'Eksportuj bieżący widok', + currentView: 'Bieżący widok', }, exportDownload: { preparingTitle: 'Preparing download...', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 166a913104b7..73fdc9d62214 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -9651,7 +9651,7 @@ Aqui está um *comprovante de teste* para mostrar como funciona:`, expenseLevelExport: 'Todos os dados - nível de despesa', exportInProgress: 'Exportação em andamento', conciergeWillSend: 'O Concierge enviará o arquivo para você em breve.', - currentView: 'Exportar visão atual', + currentView: 'Visualização atual', }, exportDownload: { preparingTitle: 'Preparing download...', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index c1eead6cbb9f..e98d22c82840 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -9390,7 +9390,7 @@ ${reportName}`, expenseLevelExport: '所有数据 - 报销级别', exportInProgress: '导出进行中', conciergeWillSend: 'Concierge 将很快把文件发送给你。', - currentView: '导出当前视图', + currentView: '当前视图', }, exportDownload: { preparingTitle: 'Preparing download...', From def3a096d3a46cb8692770860b0bdb97e9ab1592 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Mon, 22 Jun 2026 10:52:48 -0300 Subject: [PATCH 06/15] fix submodule --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index aa5daf5ee204..9980e46fc98d 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit aa5daf5ee204593442c55b3e07446020d6ef03fe +Subproject commit 9980e46fc98d57b05cc73e61198727ed037ee9d8 From cac7344423ca469968318a171ed0fe12c2291ddb Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Mon, 22 Jun 2026 11:27:21 -0300 Subject: [PATCH 07/15] fix bad merge --- src/components/ExportDownloadStatusModal.tsx | 2 +- src/libs/actions/Search.ts | 2 +- tests/unit/ExportDownloadStatusModalTest.tsx | 28 ++++++++++++++++++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/components/ExportDownloadStatusModal.tsx b/src/components/ExportDownloadStatusModal.tsx index c27cfbe5f366..32a4800e3a14 100644 --- a/src/components/ExportDownloadStatusModal.tsx +++ b/src/components/ExportDownloadStatusModal.tsx @@ -75,7 +75,7 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E const isCSV = fileName.endsWith('.csv'); const secureType = isCSV ? CONST.SECURE_DOWNLOAD_TYPE.CSV_EXPORT : CONST.SECURE_DOWNLOAD_TYPE.PDF_REPORT; const url = buildSecureDownloadURL({baseURL, secureType, fileName, downloadName: fileName, email: currentUserLogin}); - fileDownload(translate, addEncryptedAuthTokenToURL(url, encryptedAuthToken ?? '', true), fileName, '', isMobileSafari(), undefined, undefined, undefined, false, false); + fileDownload(translate, addEncryptedAuthTokenToURL(url, encryptedAuthToken ?? '', true), fileName, '', isMobileSafari(), undefined, undefined, undefined, undefined, false); }; useEffect(() => { diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index c24e124e61cf..1df6923f0722 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -1305,7 +1305,7 @@ function exportSearchItemsToCSV( formData, CONST.NETWORK.METHOD.POST, onDownloadFailed, - false, + undefined, false, ); } diff --git a/tests/unit/ExportDownloadStatusModalTest.tsx b/tests/unit/ExportDownloadStatusModalTest.tsx index e90e30294755..bb860bbd1fe5 100644 --- a/tests/unit/ExportDownloadStatusModalTest.tsx +++ b/tests/unit/ExportDownloadStatusModalTest.tsx @@ -118,7 +118,19 @@ describe('ExportDownloadStatusModal', () => { await waitForBatchedUpdatesWithAct(); const expectedURLPart = `secure?secureType=csvexport&filename=${encodeURIComponent(CSV_FILE_NAME)}&downloadName=${encodeURIComponent(CSV_FILE_NAME)}`; - expect(mockFileDownload).toHaveBeenCalledWith(expect.anything(), expect.stringContaining(expectedURLPart), CSV_FILE_NAME, expect.anything(), expect.anything()); + // shouldUnlink (arg 9) is left undefined so the platform default cleans up the temp file; appendTimestamp (arg 10) is false so the OS-recorded download time isn't duplicated in the name. + expect(mockFileDownload).toHaveBeenCalledWith( + expect.anything(), + expect.stringContaining(expectedURLPart), + CSV_FILE_NAME, + expect.anything(), + expect.anything(), + undefined, + undefined, + undefined, + undefined, + false, + ); }); it('auto-downloads PDF on ready state transition with pdfreport secureType', async () => { @@ -128,7 +140,19 @@ describe('ExportDownloadStatusModal', () => { await waitForBatchedUpdatesWithAct(); const expectedURLPart = `secure?secureType=pdfreport&filename=${encodeURIComponent(PDF_FILE_NAME)}&downloadName=${encodeURIComponent(PDF_FILE_NAME)}`; - expect(mockFileDownload).toHaveBeenCalledWith(expect.anything(), expect.stringContaining(expectedURLPart), PDF_FILE_NAME, expect.anything(), expect.anything()); + // shouldUnlink (arg 9) is left undefined so the platform default cleans up the temp file; appendTimestamp (arg 10) is false so the OS-recorded download time isn't duplicated in the name. + expect(mockFileDownload).toHaveBeenCalledWith( + expect.anything(), + expect.stringContaining(expectedURLPart), + PDF_FILE_NAME, + expect.anything(), + expect.anything(), + undefined, + undefined, + undefined, + undefined, + false, + ); }); it('shows ready state with a Download button and no Close button', async () => { From c535ebced3b0a7cdb3c90dd4b409c3e5ba2b76a6 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Thu, 25 Jun 2026 08:27:32 -0300 Subject: [PATCH 08/15] unused comment --- src/libs/fileDownload/DownloadUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/fileDownload/DownloadUtils.ts b/src/libs/fileDownload/DownloadUtils.ts index 5c71f7a3b95c..781239d55bb8 100644 --- a/src/libs/fileDownload/DownloadUtils.ts +++ b/src/libs/fileDownload/DownloadUtils.ts @@ -37,7 +37,8 @@ const fetchFileDownload: FileDownload = ( shouldOpenExternalLink = false, formData = undefined, requestType = 'get', - onDownloadFailed?: () => void, + onDownloadFailed?: () => void, + // `shouldUnlink` is part of the cross-platform FileDownload signature but has no meaning on web, so it is intentionally unused here. // eslint-disable-next-line @typescript-eslint/no-unused-vars shouldUnlink = false, appendTimestamp = true, From a4ce766ff95e073eb5d2fd135b01c9202e2c5666 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Thu, 25 Jun 2026 08:28:33 -0300 Subject: [PATCH 09/15] update submodule --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 9980e46fc98d..f38ade5ee675 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 9980e46fc98d57b05cc73e61198727ed037ee9d8 +Subproject commit f38ade5ee67541f370a4c4b228256516a86807ad From f5b1f8a5bdcac4df3495c7ac7d43ce0db2bd348f Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Thu, 25 Jun 2026 14:28:15 -0300 Subject: [PATCH 10/15] prettier --- src/libs/fileDownload/DownloadUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/fileDownload/DownloadUtils.ts b/src/libs/fileDownload/DownloadUtils.ts index 781239d55bb8..ddeabfbd4468 100644 --- a/src/libs/fileDownload/DownloadUtils.ts +++ b/src/libs/fileDownload/DownloadUtils.ts @@ -37,7 +37,7 @@ const fetchFileDownload: FileDownload = ( shouldOpenExternalLink = false, formData = undefined, requestType = 'get', - onDownloadFailed?: () => void, + onDownloadFailed?: () => void, // `shouldUnlink` is part of the cross-platform FileDownload signature but has no meaning on web, so it is intentionally unused here. // eslint-disable-next-line @typescript-eslint/no-unused-vars shouldUnlink = false, From 73c4a829f2b54d4f78bdc5b1bbd775bf49faba9b Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Fri, 26 Jun 2026 11:32:59 -0300 Subject: [PATCH 11/15] missing file name on ios --- Mobile-Expensify | 2 +- src/libs/actions/Report/index.ts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 81df53c398be..f38ade5ee675 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 81df53c398befa32ba3c204e63949ba59cd2651a +Subproject commit f38ade5ee67541f370a4c4b228256516a86807ad diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 593da3386411..9ef0d6853949 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -69,6 +69,7 @@ import getEnvironment from '@libs/Environment/getEnvironment'; import type EnvironmentType from '@libs/Environment/getEnvironment/types'; import {getMicroSecondOnyxErrorWithTranslationKey, getMicroSecondTranslationErrorWithTranslationKey} from '@libs/ErrorUtils'; import fileDownload from '@libs/fileDownload'; +import {getExportFileName} from '@libs/fileDownload/FileUtils'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import HttpUtils from '@libs/HttpUtils'; import Log from '@libs/Log'; @@ -6104,7 +6105,18 @@ function exportReportToCSV({reportID, transactionIDList}: ExportReportCSVParams, } } - fileDownload(translate, ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_REPORT_TO_CSV}), 'Expensify.csv', '', false, formData, CONST.NETWORK.METHOD.POST, onDownloadFailed); + fileDownload( + translate, + ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_REPORT_TO_CSV}), + getExportFileName(translate('export.basicExport'), rand64()), + '', + false, + formData, + CONST.NETWORK.METHOD.POST, + onDownloadFailed, + undefined, + false, + ); } async function exportReportToPDF({reportID}: ExportReportPDFParams) { From dbfb9c720c6f215fba2ed01250f7dc37be1d1a62 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Fri, 26 Jun 2026 14:40:41 -0300 Subject: [PATCH 12/15] fix submodule --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index f38ade5ee675..bb08cca4d05c 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit f38ade5ee67541f370a4c4b228256516a86807ad +Subproject commit bb08cca4d05c87c406f99cc1ff9234d14f5e31e2 From c58f69dbb6c74fa1f74e91aff7e792ab5563bfd8 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Fri, 26 Jun 2026 15:29:08 -0300 Subject: [PATCH 13/15] Fix useExportActions test for new beginExportWithTemplate signature Pass exportName and policyID in their correct argument positions and assert exportName in the queueExportSearchWithTemplate call. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/unit/hooks/useExportActionsTest.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/unit/hooks/useExportActionsTest.ts b/tests/unit/hooks/useExportActionsTest.ts index a777797b7b68..f5c1731c2f32 100644 --- a/tests/unit/hooks/useExportActionsTest.ts +++ b/tests/unit/hooks/useExportActionsTest.ts @@ -9,6 +9,7 @@ const mockClearExportDownload = jest.mocked(clearExportDownload); const REPORT_ID = 'report1'; const POLICY_ID = 'policy1'; +const EXPORT_NAME = 'Test Template'; type ExportDownloadStatusModalProps = {exportID: string; onClose: () => void}; @@ -104,7 +105,7 @@ describe('useExportActions - template export status modal', () => { const {result} = renderHook(() => useExportActions({reportID: REPORT_ID})); act(() => { - result.current.beginExportWithTemplate('Test Template', 'csv', ['1', '2'], POLICY_ID); + result.current.beginExportWithTemplate('Test Template', 'csv', ['1', '2'], EXPORT_NAME, POLICY_ID); }); expect(mockQueueExportSearchWithTemplate).toHaveBeenCalledWith( @@ -115,6 +116,7 @@ describe('useExportActions - template export status modal', () => { reportIDList: [REPORT_ID], transactionIDList: ['1', '2'], policyID: POLICY_ID, + exportName: EXPORT_NAME, }, true, ); @@ -127,7 +129,7 @@ describe('useExportActions - template export status modal', () => { const {result} = renderHook(() => useExportActions({reportID: REPORT_ID})); act(() => { - result.current.beginExportWithTemplate('Test Template', 'csv', ['1'], POLICY_ID); + result.current.beginExportWithTemplate('Test Template', 'csv', ['1'], EXPORT_NAME, POLICY_ID); }); expect(mockQueueExportSearchWithTemplate).not.toHaveBeenCalled(); @@ -139,7 +141,7 @@ describe('useExportActions - template export status modal', () => { const {result} = renderHook(() => useExportActions({reportID: REPORT_ID})); act(() => { - result.current.beginExportWithTemplate('Test Template', 'csv', ['1'], POLICY_ID); + result.current.beginExportWithTemplate('Test Template', 'csv', ['1'], EXPORT_NAME, POLICY_ID); }); const modal: ReactElement | null = result.current.exportDownloadStatusModal; expect(modal?.props.exportID).toBe('mock-export-id'); From d21d2b21674064f926603da652c40b3307aac037 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Fri, 26 Jun 2026 16:00:04 -0300 Subject: [PATCH 14/15] rename group export to current view too --- src/hooks/useSearchBulkActions.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index c9a8a5ef1780..a79af490111b 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -1499,8 +1499,12 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { } } + const isGroupedSearch = !!getValidGroupBy(queryJSON?.groupBy); + exportOptions.push({ - text: translate('export.basicExport'), + // Group by exports dont have a basicExport, at the same time the backend expects isBasicExport to be false for grouped exports, so we just rename the option here + // Fixing here https://github.com/Expensify/Expensify/issues/652978 + text: translate(isGroupedSearch ? 'export.currentView' : 'export.basicExport'), icon: expensifyIcons.Table, onSelected: () => { handleBasicExport(); @@ -1509,7 +1513,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { shouldCallAfterModalHide: true, }); - const isGroupedSearch = !!getValidGroupBy(queryJSON?.groupBy); if (!isGroupedSearch) { exportOptions.push({ text: translate('export.currentView'), From e7a942987a183f212c88a42f7ba01a84f3df584b Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Fri, 26 Jun 2026 16:00:57 -0300 Subject: [PATCH 15/15] fix comment --- src/hooks/useSearchBulkActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index a79af490111b..c266b872756a 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -1502,7 +1502,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { const isGroupedSearch = !!getValidGroupBy(queryJSON?.groupBy); exportOptions.push({ - // Group by exports dont have a basicExport, at the same time the backend expects isBasicExport to be false for grouped exports, so we just rename the option here + // Group by exports dont have a basicExport, at the same time the backend expects isBasicExport to be true for grouped exports, so we just rename the option here // Fixing here https://github.com/Expensify/Expensify/issues/652978 text: translate(isGroupedSearch ? 'export.currentView' : 'export.basicExport'), icon: expensifyIcons.Table,