From f84ffac689c096779d4fdd7e22d9fa5054f0d57f Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:39:56 +0700 Subject: [PATCH 01/19] Add bulk export to accounting integration --- src/languages/de.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + src/libs/API/parameters/ReportExportParams.ts | 2 +- src/libs/actions/Report/index.ts | 56 ++++++++ src/libs/actions/Search.ts | 100 ++++++++++++++ src/pages/Search/SearchPage.tsx | 126 ++++++++++++++---- 14 files changed, 264 insertions(+), 30 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 0d7c52143b5cb..20faa6a45e2e5 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -8189,6 +8189,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.', + conciergeWillNotifyOnExportFailure: 'Concierge wird dir eine Nachricht senden, wenn Berichte nicht exportiert werden.', }, domain: { notVerified: 'Nicht verifiziert', diff --git a/src/languages/en.ts b/src/languages/en.ts index f797a1e62716d..389df38652bae 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -8106,6 +8106,7 @@ const translations = { expenseLevelExport: 'All Data - expense level', exportInProgress: 'Export in progress', conciergeWillSend: 'Concierge will send you the file shortly.', + conciergeWillNotifyOnExportFailure: "Concierge will send you a message if any reports don't export.", }, domain: { notVerified: 'Not verified', diff --git a/src/languages/es.ts b/src/languages/es.ts index 0bcd0578cdcb0..076e9df967292 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -8254,6 +8254,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.', + conciergeWillNotifyOnExportFailure: 'Concierge te enviará un mensaje si algún informe no se exporta.', }, openAppFailureModal: { title: 'Algo salió mal...', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 8227beb6b5bfd..78991cb95cd87 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -8203,6 +8203,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.', + conciergeWillNotifyOnExportFailure: 'Concierge vous enverra un message si certains rapports ne sont pas exportés.', }, domain: { notVerified: 'Non vérifié', diff --git a/src/languages/it.ts b/src/languages/it.ts index 6169ff2dfc3f0..f4ff1fe1b8e9c 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -8171,6 +8171,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.', + conciergeWillNotifyOnExportFailure: 'Concierge ti invierà un messaggio se alcuni report non vengono esportati.', }, domain: { notVerified: 'Non verificato', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 7497d8df7f674..12ce90c6d4c70 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -8103,6 +8103,7 @@ ${reportName} expenseLevelExport: 'すべてのデータ - 経費レベル', exportInProgress: 'エクスポート処理中', conciergeWillSend: 'Conciergeがまもなくファイルを送信します。', + conciergeWillNotifyOnExportFailure: 'レポートのエクスポートに失敗した場合、Concierge からメッセージが届きます。', }, domain: { notVerified: '未確認', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index f9423d3a515bb..b7066ae81e6ba 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -8158,6 +8158,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.', + conciergeWillNotifyOnExportFailure: 'Concierge stuurt je een bericht als rapporten niet worden geëxporteerd.', }, domain: { notVerified: 'Niet geverifieerd', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index ce9ca4863cd66..634d85dd35472 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -8138,6 +8138,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.', + conciergeWillNotifyOnExportFailure: 'Concierge wyśle Ci wiadomość, jeśli niektóre raporty nie zostaną wyeksportowane.', }, domain: { notVerified: 'Niezweryfikowane', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 0781ad77b079f..6c2baae731e4b 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -8139,6 +8139,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.', + conciergeWillNotifyOnExportFailure: 'O Concierge enviará uma mensagem se algum relatório não for exportado.', }, domain: { notVerified: 'Não verificado', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index e1a3b2f98c0f5..0db725bfced18 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7982,6 +7982,7 @@ ${reportName} expenseLevelExport: '所有数据 - 报销级别', exportInProgress: '导出进行中', conciergeWillSend: 'Concierge 将很快把文件发送给你。', + conciergeWillNotifyOnExportFailure: '如果有报告导出失败,Concierge 将向你发送消息。', }, domain: { notVerified: '未验证', diff --git a/src/libs/API/parameters/ReportExportParams.ts b/src/libs/API/parameters/ReportExportParams.ts index c6a4b7b58ee89..5b69aa1978a70 100644 --- a/src/libs/API/parameters/ReportExportParams.ts +++ b/src/libs/API/parameters/ReportExportParams.ts @@ -2,7 +2,7 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; type ReportExportParams = { - reportIDList: string; + reportIDList: string | string[]; connectionName: ValueOf; type: 'MANUAL'; /** diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index dcdd77356ed95..75bf6d7dc0950 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -5017,6 +5017,61 @@ function markAsManuallyExported(reportID: string, connectionName: ConnectionName API.write(WRITE_COMMANDS.MARK_AS_EXPORTED, params, {optimisticData, successData, failureData}); } +function markAsManuallyExportedMultipleReports(reportIDs: string[], connectionName: ConnectionName) { + const label = CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]; + const optimisticData: Array> = []; + const successData: Array> = []; + const failureData: Array> = []; + const reportData: Array<{reportID: string; label: string; optimisticReportActionID: string}> = []; + + // Process each report ID + for (const reportID of reportIDs) { + const action = buildOptimisticExportIntegrationAction(connectionName, true); + const optimisticReportActionID = action.reportActionID; + + reportData.push({ + reportID, + label, + optimisticReportActionID, + }); + + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [optimisticReportActionID]: action, + }, + }); + + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [optimisticReportActionID]: { + pendingAction: null, + }, + }, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [optimisticReportActionID]: { + errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + }); + } + + const params = { + markedManually: true, + data: JSON.stringify(reportData), + } satisfies MarkAsExportedParams; + + API.write(WRITE_COMMANDS.MARK_AS_EXPORTED, params, {optimisticData, successData, failureData}); +} + function exportReportToCSV({reportID, transactionIDList}: ExportReportCSVParams, onDownloadFailed: () => void, translate: LocalizedTranslate) { let reportIDParam = reportID; const allReportTransactions = getReportTransactions(reportID).filter((transaction) => transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); @@ -6683,6 +6738,7 @@ export { leaveGroupChat, leaveRoom, markAsManuallyExported, + markAsManuallyExportedMultipleReports, markCommentAsUnread, navigateToAndOpenChildReport, createChildReport, diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 6ddaf363d8714..84716edb6892d 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -713,6 +713,105 @@ function exportToIntegrationOnSearch(hash: number, reportID: string | undefined, API.write(WRITE_COMMANDS.REPORT_EXPORT, params, {optimisticData, failureData, successData}); } +function exportMultipleReportsToIntegration(hash: number, reportIDs: string[], connectionName: ConnectionName, currentSearchKey?: SearchKey) { + if (!reportIDs.length) { + return; + } + + const optimisticActions: Record = {}; + const successActions: Record = {}; + const optimisticReportActions: Record = {}; + + for (const reportID of reportIDs) { + const optimisticAction = buildOptimisticExportIntegrationAction(connectionName); + const successAction: OptimisticExportIntegrationAction = {...optimisticAction, pendingAction: null}; + const optimisticReportActionID = optimisticAction.reportActionID; + + optimisticActions[reportID] = optimisticAction; + successActions[reportID] = successAction; + optimisticReportActions[reportID] = optimisticReportActionID; + } + + const optimisticData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE_COLLECTION, + key: ONYXKEYS.COLLECTION.REPORT_METADATA, + value: Object.fromEntries(reportIDs.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {isActionLoading: true}])), + }, + ]; + + for (const reportID of reportIDs) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [optimisticReportActions[reportID]]: optimisticActions[reportID], + }, + }); + } + + const successData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE_COLLECTION, + key: ONYXKEYS.COLLECTION.REPORT_METADATA, + value: Object.fromEntries(reportIDs.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {isActionLoading: false}])), + }, + ]; + + for (const reportID of reportIDs) { + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [optimisticReportActions[reportID]]: successActions[reportID], + }, + }); + } + + if (currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT) { + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, + value: { + data: Object.fromEntries(reportIDs.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null])), + }, + }); + } + + const failureData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE_COLLECTION, + key: ONYXKEYS.COLLECTION.REPORT_METADATA, + value: Object.fromEntries(reportIDs.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {isActionLoading: false}])), + }, + ]; + + for (const reportID of reportIDs) { + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [optimisticReportActions[reportID]]: null, + }, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: {errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, + }); + } + + const params = { + reportIDList: reportIDs, + connectionName, + type: 'MANUAL', + optimisticReportActions: JSON.stringify(optimisticReportActions), + } satisfies ReportExportParams; + + API.write(WRITE_COMMANDS.REPORT_EXPORT, params, {optimisticData, failureData, successData}); +} + function payMoneyRequestOnSearch(hash: number, paymentData: PaymentData[], currentSearchKey?: SearchKey) { const optimisticData: Array> = [ { @@ -1366,6 +1465,7 @@ export { getLastPolicyPaymentMethod, getLastPolicyBankAccountID, exportToIntegrationOnSearch, + exportMultipleReportsToIntegration, getPayOption, isValidBulkPayOption, handleBulkPayItemSelected, diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index c362994c9ec84..194b23f86e176 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -4,6 +4,7 @@ import {InteractionManager, View} from 'react-native'; import Animated from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import ConfirmModal from '@components/ConfirmModal'; import DecisionModal from '@components/DecisionModal'; import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider'; import DragAndDropConsumer from '@components/DragAndDrop/Consumer'; @@ -37,10 +38,11 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {confirmReadyToOpenApp} from '@libs/actions/App'; import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction'; -import {moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter, searchInServer} from '@libs/actions/Report'; +import {markAsManuallyExportedMultipleReports, moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter, searchInServer} from '@libs/actions/Report'; import { approveMoneyRequestOnSearch, deleteMoneyRequestOnSearch, + exportMultipleReportsToIntegration, exportSearchItemsToCSV, getExportTemplates, getLastPolicyBankAccountID, @@ -65,10 +67,11 @@ import {getTransactionsAndReportsFromSearch} from '@libs/MergeTransactionUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; -import {getActiveAdminWorkspaces, hasDynamicExternalWorkflow, hasOnlyPersonalPolicies as hasOnlyPersonalPoliciesUtil, isPaidGroupPolicy} from '@libs/PolicyUtils'; -import {isMergeActionForSelectedTransactions} from '@libs/ReportSecondaryActionUtils'; +import {getActiveAdminWorkspaces, getConnectedIntegration, hasDynamicExternalWorkflow, hasOnlyPersonalPolicies as hasOnlyPersonalPoliciesUtil, isPaidGroupPolicy} from '@libs/PolicyUtils'; +import {getSecondaryExportReportActions, isMergeActionForSelectedTransactions} from '@libs/ReportSecondaryActionUtils'; import { generateReportID, + getIntegrationIcon, getPolicyExpenseChat, getReportOrDraftReport, isBusinessInvoiceRoom, @@ -132,6 +135,7 @@ function SearchPage({route}: SearchPageProps) { const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); + const [accountingExportModalVisible, setAccountingExportModalVisible] = useState(false); const [searchRequestResponseStatusCode, setSearchRequestResponseStatusCode] = useState(null); const {showConfirmModal} = useConfirmModal(); const {isBetaEnabled} = usePermissions(); @@ -163,6 +167,11 @@ function SearchPage({route}: SearchPageProps) { 'SmartScan', 'MoneyBag', 'ArrowSplit', + 'QBOSquare', + 'XeroSquare', + 'NetSuiteSquare', + 'IntacctSquare', + 'QBDSquare', ] as const); const lastNonEmptySearchResults = useRef(undefined); @@ -629,21 +638,10 @@ function SearchPage({route}: SearchPageProps) { const typeExpenseReport = queryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT; - // Gets the list of options for the export sub-menu // Gets the list of options for the export sub-menu const getExportOptions = () => { // We provide the basic and expense level export options by default - const exportOptions: PopoverMenuItem[] = [ - { - text: translate('export.basicExport'), - icon: expensifyIcons.Table, - onSelected: () => { - handleBasicExport(); - }, - shouldCloseModalOnSelect: true, - shouldCallAfterModalHide: true, - }, - ]; + const exportOptions: PopoverMenuItem[] = []; // Determine if only full reports are selected by comparing the reportIDs of the selected transactions and the reportIDs of the selected reports const areFullReportsSelected = selectedTransactionReportIDs.length === selectedReportIDs.length && selectedTransactionReportIDs.every((id) => selectedReportIDs.includes(id)); @@ -655,8 +653,76 @@ function SearchPage({route}: SearchPageProps) { // the selected expenses are the only expenses of their parent expense report include the report level export option. const includeReportLevelExport = ((typeExpenseReport || typeInvoice) && areFullReportsSelected) || (typeExpense && !typeExpenseReport && isAllOneTransactionReport); - // Collect a list of export templates available to the user from their account, policy, and custom integrations templates const policy = selectedPolicyIDs.length === 1 ? policies?.[`${ONYXKEYS.COLLECTION.POLICY}${selectedPolicyIDs.at(0)}`] : undefined; + const connectedIntegration = getConnectedIntegration(policy); + const isReportsTab = typeExpenseReport; + + // Helper function to check if a single report can be exported + const canReportBeExported = (report: (typeof selectedReports)[0]) => { + if (!report.reportID) { + return false; + } + + const reportPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; + const completeReport = getReportOrDraftReport(report.reportID); + + if (!completeReport) { + return false; + } + + const reportExportOptions = getSecondaryExportReportActions( + currentUserPersonalDetails?.accountID ?? 0, + currentUserPersonalDetails?.login ?? '', + completeReport, + bankAccountList, + reportPolicy, + ); + + return reportExportOptions.includes(CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION); + }; + + // Check if all selected reports can be exported using existing logic + const canExportAllReports = isReportsTab && selectedReportIDs.length > 0 && includeReportLevelExport && selectedReports.every(canReportBeExported); + + // Add accounting integration export options if conditions are met + if (canExportAllReports && connectedIntegration) { + const connectionNameFriendly = CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectedIntegration]; + const integrationIcon = getIntegrationIcon(connectedIntegration, expensifyIcons); + + exportOptions.push( + { + text: connectionNameFriendly, + icon: integrationIcon, + onSelected: () => { + // Show accounting integration confirmation modal + setAccountingExportModalVisible(true); + exportMultipleReportsToIntegration(hash, selectedReportIDs, connectedIntegration); + }, + shouldCloseModalOnSelect: true, + shouldCallAfterModalHide: true, + }, + { + text: translate('workspace.common.markAsExported'), + icon: integrationIcon, + onSelected: () => { + markAsManuallyExportedMultipleReports(selectedReportIDs, connectedIntegration); + }, + shouldCloseModalOnSelect: true, + shouldCallAfterModalHide: true, + }, + ); + } + + exportOptions.push({ + text: translate('export.basicExport'), + icon: expensifyIcons.Table, + onSelected: () => { + handleBasicExport(); + }, + shouldCloseModalOnSelect: true, + shouldCallAfterModalHide: true, + }); + const exportTemplates = getExportTemplates(integrationsExportTemplates ?? [], csvExportLayouts ?? {}, translate, policy, includeReportLevelExport); for (const template of exportTemplates) { exportOptions.push({ @@ -991,19 +1057,6 @@ function SearchPage({route}: SearchPageProps) { styles.colorMuted, styles.fontWeightNormal, styles.textWrap, - expensifyIcons.ArrowCollapse, - expensifyIcons.ArrowRight, - expensifyIcons.ArrowSplit, - expensifyIcons.DocumentMerge, - expensifyIcons.Exclamation, - expensifyIcons.Export, - expensifyIcons.MoneyBag, - expensifyIcons.Send, - expensifyIcons.Stopwatch, - expensifyIcons.Table, - expensifyIcons.ThumbsDown, - expensifyIcons.ThumbsUp, - expensifyIcons.Trashcan, dismissedHoldUseExplanation, dismissedRejectUseExplanation, areAllTransactionsFromSubmitter, @@ -1012,7 +1065,10 @@ function SearchPage({route}: SearchPageProps) { isDelegateAccessRestricted, showDelegateNoAccessModal, currentUserPersonalDetails.accountID, + currentUserPersonalDetails?.login, personalPolicyID, + bankAccountList, + expensifyIcons, ]); const saveFileAndInitMoneyRequest = (files: FileObject[]) => { @@ -1272,6 +1328,18 @@ function SearchPage({route}: SearchPageProps) { onConfirm={dismissModalAndUpdateUseHold} /> )} + + { + setAccountingExportModalVisible(false); + }} + onCancel={() => setAccountingExportModalVisible(false)} + title={translate('export.exportInProgress')} + prompt={translate('export.conciergeWillNotifyOnExportFailure')} + confirmText={translate('common.buttonConfirm')} + shouldShowCancelButton={false} + /> )} From 86564bc30e8e1588f64968facc1a7053cc3febde Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:53:21 +0700 Subject: [PATCH 02/19] Handle already exported reports --- src/pages/Search/SearchPage.tsx | 86 ++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 2719ef37ee79b..7bf2b1637a4fa 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -43,7 +43,6 @@ import {markAsManuallyExportedMultipleReports, moveIOUReportToPolicy, moveIOURep import { approveMoneyRequestOnSearch, bulkDeleteReports, - deleteMoneyRequestOnSearch, exportMultipleReportsToIntegration, exportSearchItemsToCSV, getExportTemplates, @@ -70,6 +69,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; import {getActiveAdminWorkspaces, getConnectedIntegration, hasDynamicExternalWorkflow, hasOnlyPersonalPolicies as hasOnlyPersonalPoliciesUtil, isPaidGroupPolicy} from '@libs/PolicyUtils'; +import {getFilteredReportActionsForReportView} from '@libs/ReportActionsUtils'; import {getSecondaryExportReportActions, isMergeActionForSelectedTransactions} from '@libs/ReportSecondaryActionUtils'; import { generateReportID, @@ -79,6 +79,7 @@ import { isBusinessInvoiceRoom, isCurrentUserSubmitter, isExpenseReport as isExpenseReportUtil, + isExported as isExportedUtils, isInvoiceReport, isIOUReport as isIOUReportUtil, isSelfDM, @@ -136,6 +137,7 @@ function SearchPage({route}: SearchPageProps) { const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES, {canBeMissing: true}); const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS, {canBeMissing: true}); const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); + const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {canBeMissing: true}); const {accountID} = useCurrentUserPersonalDetails(); const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); @@ -788,9 +790,43 @@ function SearchPage({route}: SearchPageProps) { text: connectionNameFriendly, icon: integrationIcon, onSelected: () => { - // Show accounting integration confirmation modal - setAccountingExportModalVisible(true); - exportMultipleReportsToIntegration(hash, selectedReportIDs, connectedIntegration); + let exportedReportName = ''; + const areAnyReportsExported = selectedReportIDs.some((reportID) => { + const unfilteredReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; + if (!unfilteredReportActions) { + return false; + } + + const reportActions = getFilteredReportActionsForReportView(Object.values(unfilteredReportActions)); + const isExported = isExportedUtils(reportActions); + if (isExported && !exportedReportName) { + exportedReportName = getReportOrDraftReport(reportID)?.reportName ?? ''; + } + return isExported; + }); + + if (areAnyReportsExported) { + showConfirmModal({ + title: translate('workspace.exportAgainModal.title'), + prompt: translate('workspace.exportAgainModal.description', { + connectionName: connectedIntegration, + reportName: exportedReportName, + }), + confirmText: translate('workspace.exportAgainModal.confirmText'), + cancelText: translate('workspace.exportAgainModal.cancelText'), + }).then((result) => { + if (result.action !== ModalActions.CONFIRM) { + return; + } + + if (hash) { + exportMultipleReportsToIntegration(hash, selectedReportIDs, connectedIntegration); + } + }); + } else if (hash) { + setAccountingExportModalVisible(true); + exportMultipleReportsToIntegration(hash, selectedReportIDs, connectedIntegration); + } }, shouldCloseModalOnSelect: true, shouldCallAfterModalHide: true, @@ -1120,49 +1156,51 @@ function SearchPage({route}: SearchPageProps) { return options; }, [ - searchResults, selectedTransactionsKeys, status, hash, selectedTransactions, + queryJSON?.type, + expensifyIcons, translate, - localeCompare, areAllMatchingItemsSelected, isOffline, selectedReports, - selectedTransactionReportIDs, lastPaymentMethods, selectedReportIDs, + personalPolicyID, + searchResults, allTransactions, - queryJSON?.type, + currentSearchResults?.data, + selectedTransactionReportIDs, selectedPolicyIDs, policies, integrationsExportTemplates, csvExportLayouts, - clearSelectedTransactions, + currentUserPersonalDetails.accountID, + currentUserPersonalDetails?.login, + bankAccountList, + allReportActions, + handleBasicExport, beginExportWithTemplate, + handleApproveWithDEWCheck, + allTransactionViolations, + isDelegateAccessRestricted, + dismissedRejectUseExplanation, + showDelegateNoAccessModal, + clearSelectedTransactions, bulkPayButtonOptions, onBulkPaySelected, - handleBasicExport, - handleApproveWithDEWCheck, - handleDeleteSelectedTransactions, + areAllTransactionsFromSubmitter, + dismissedHoldUseExplanation, + localeCompare, allReports, + handleDeleteSelectedTransactions, theme.icon, styles.colorMuted, styles.fontWeightNormal, styles.textWrap, - dismissedHoldUseExplanation, - dismissedRejectUseExplanation, - areAllTransactionsFromSubmitter, - allTransactionViolations, - currentSearchResults?.data, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - currentUserPersonalDetails.accountID, - currentUserPersonalDetails?.login, - personalPolicyID, - bankAccountList, - expensifyIcons, + showConfirmModal, ]); const saveFileAndInitMoneyRequest = (files: FileObject[]) => { From 855ea2cac3ee20f4755ebbc8a2001740765cd124 Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:16:57 +0700 Subject: [PATCH 03/19] Clear selected reports and handle draft report case --- src/pages/Search/SearchPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 7bf2b1637a4fa..8388b0770e71d 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -753,14 +753,13 @@ function SearchPage({route}: SearchPageProps) { const connectedIntegration = getConnectedIntegration(policy); const isReportsTab = typeExpenseReport; - // Helper function to check if a single report can be exported const canReportBeExported = (report: (typeof selectedReports)[0]) => { if (!report.reportID) { return false; } const reportPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; - const completeReport = getReportOrDraftReport(report.reportID); + const completeReport = getReportOrDraftReport(report.reportID) ?? currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]; if (!completeReport) { return false; @@ -777,7 +776,6 @@ function SearchPage({route}: SearchPageProps) { return reportExportOptions.includes(CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION); }; - // Check if all selected reports can be exported using existing logic const canExportAllReports = isReportsTab && selectedReportIDs.length > 0 && includeReportLevelExport && selectedReports.every(canReportBeExported); // Add accounting integration export options if conditions are met @@ -820,12 +818,14 @@ function SearchPage({route}: SearchPageProps) { } if (hash) { + clearSelectedTransactions(); exportMultipleReportsToIntegration(hash, selectedReportIDs, connectedIntegration); } }); } else if (hash) { setAccountingExportModalVisible(true); exportMultipleReportsToIntegration(hash, selectedReportIDs, connectedIntegration); + clearSelectedTransactions(); } }, shouldCloseModalOnSelect: true, From 21a52851ff0bede4f370825ebdd2c6384cf0a161 Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:38:11 +0700 Subject: [PATCH 04/19] Handle offline case when exporting to QBO --- src/pages/Search/SearchPage.tsx | 96 +++++++++++++++++---------------- 1 file changed, 51 insertions(+), 45 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 8388b0770e71d..835f8e69e77b7 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -778,65 +778,71 @@ function SearchPage({route}: SearchPageProps) { const canExportAllReports = isReportsTab && selectedReportIDs.length > 0 && includeReportLevelExport && selectedReports.every(canReportBeExported); - // Add accounting integration export options if conditions are met if (canExportAllReports && connectedIntegration) { const connectionNameFriendly = CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectedIntegration]; const integrationIcon = getIntegrationIcon(connectedIntegration, expensifyIcons); + const handleExportAction = (exportAction: () => void, shouldShowAccountingModal = false) => { + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + let exportedReportName = ''; + const areAnyReportsExported = selectedReportIDs.some((reportID) => { + const unfilteredReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; + if (!unfilteredReportActions) { + return false; + } + + const reportActions = getFilteredReportActionsForReportView(Object.values(unfilteredReportActions)); + const isExported = isExportedUtils(reportActions); + if (isExported && !exportedReportName) { + exportedReportName = getReportOrDraftReport(reportID)?.reportName ?? ''; + } + return isExported; + }); + + if (areAnyReportsExported) { + showConfirmModal({ + title: translate('workspace.exportAgainModal.title'), + prompt: translate('workspace.exportAgainModal.description', { + connectionName: connectedIntegration, + reportName: exportedReportName, + }), + confirmText: translate('workspace.exportAgainModal.confirmText'), + cancelText: translate('workspace.exportAgainModal.cancelText'), + }).then((result) => { + if (result.action !== ModalActions.CONFIRM) { + return; + } + + if (hash) { + clearSelectedTransactions(); + exportAction(); + } + }); + } else if (hash) { + if (shouldShowAccountingModal) { + setAccountingExportModalVisible(true); + } + exportAction(); + clearSelectedTransactions(); + } + }; + exportOptions.push( { text: connectionNameFriendly, icon: integrationIcon, - onSelected: () => { - let exportedReportName = ''; - const areAnyReportsExported = selectedReportIDs.some((reportID) => { - const unfilteredReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; - if (!unfilteredReportActions) { - return false; - } - - const reportActions = getFilteredReportActionsForReportView(Object.values(unfilteredReportActions)); - const isExported = isExportedUtils(reportActions); - if (isExported && !exportedReportName) { - exportedReportName = getReportOrDraftReport(reportID)?.reportName ?? ''; - } - return isExported; - }); - - if (areAnyReportsExported) { - showConfirmModal({ - title: translate('workspace.exportAgainModal.title'), - prompt: translate('workspace.exportAgainModal.description', { - connectionName: connectedIntegration, - reportName: exportedReportName, - }), - confirmText: translate('workspace.exportAgainModal.confirmText'), - cancelText: translate('workspace.exportAgainModal.cancelText'), - }).then((result) => { - if (result.action !== ModalActions.CONFIRM) { - return; - } - - if (hash) { - clearSelectedTransactions(); - exportMultipleReportsToIntegration(hash, selectedReportIDs, connectedIntegration); - } - }); - } else if (hash) { - setAccountingExportModalVisible(true); - exportMultipleReportsToIntegration(hash, selectedReportIDs, connectedIntegration); - clearSelectedTransactions(); - } - }, + onSelected: () => handleExportAction(() => exportMultipleReportsToIntegration(hash, selectedReportIDs, connectedIntegration), true), shouldCloseModalOnSelect: true, shouldCallAfterModalHide: true, }, { text: translate('workspace.common.markAsExported'), icon: integrationIcon, - onSelected: () => { - markAsManuallyExportedMultipleReports(selectedReportIDs, connectedIntegration); - }, + onSelected: () => handleExportAction(() => markAsManuallyExportedMultipleReports(selectedReportIDs, connectedIntegration)), shouldCloseModalOnSelect: true, shouldCallAfterModalHide: true, }, From 629231413671431b9112e89eb81b32912876d220 Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:46:44 +0700 Subject: [PATCH 05/19] update default account ID --- src/pages/Search/SearchPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 835f8e69e77b7..0d230f723e959 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -766,7 +766,7 @@ function SearchPage({route}: SearchPageProps) { } const reportExportOptions = getSecondaryExportReportActions( - currentUserPersonalDetails?.accountID ?? 0, + currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, currentUserPersonalDetails?.login ?? '', completeReport, bankAccountList, From 9d14f79facbd20bc535908cb2db521e0f733502b Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:45:00 +0700 Subject: [PATCH 06/19] Add in-progress modal shown after already exported modal --- src/libs/actions/Search.ts | 68 ++++++++++++++------------------- src/pages/Search/SearchPage.tsx | 4 ++ 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index e410cc25323e6..3c30edb21e86d 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -748,20 +748,8 @@ function exportMultipleReportsToIntegration(hash: number, reportIDs: string[], c return; } - const optimisticActions: Record = {}; - const successActions: Record = {}; const optimisticReportActions: Record = {}; - for (const reportID of reportIDs) { - const optimisticAction = buildOptimisticExportIntegrationAction(connectionName); - const successAction: OptimisticExportIntegrationAction = {...optimisticAction, pendingAction: null}; - const optimisticReportActionID = optimisticAction.reportActionID; - - optimisticActions[reportID] = optimisticAction; - successActions[reportID] = successAction; - optimisticReportActions[reportID] = optimisticReportActionID; - } - const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE_COLLECTION, @@ -770,16 +758,6 @@ function exportMultipleReportsToIntegration(hash: number, reportIDs: string[], c }, ]; - for (const reportID of reportIDs) { - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: { - [optimisticReportActions[reportID]]: optimisticActions[reportID], - }, - }); - } - const successData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE_COLLECTION, @@ -788,40 +766,42 @@ function exportMultipleReportsToIntegration(hash: number, reportIDs: string[], c }, ]; + const failureData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE_COLLECTION, + key: ONYXKEYS.COLLECTION.REPORT_METADATA, + value: Object.fromEntries(reportIDs.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {isActionLoading: false}])), + }, + ]; + for (const reportID of reportIDs) { - successData.push({ + const optimisticAction = buildOptimisticExportIntegrationAction(connectionName); + const successAction: OptimisticExportIntegrationAction = {...optimisticAction, pendingAction: null}; + const optimisticReportActionID = optimisticAction.reportActionID; + + optimisticReportActions[reportID] = optimisticReportActionID; + + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: { - [optimisticReportActions[reportID]]: successActions[reportID], + [optimisticReportActionID]: optimisticAction, }, }); - } - if (currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT) { successData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: { - data: Object.fromEntries(reportIDs.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null])), + [optimisticReportActionID]: successAction, }, }); - } - - const failureData: Array> = [ - { - onyxMethod: Onyx.METHOD.MERGE_COLLECTION, - key: ONYXKEYS.COLLECTION.REPORT_METADATA, - value: Object.fromEntries(reportIDs.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {isActionLoading: false}])), - }, - ]; - for (const reportID of reportIDs) { failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: { - [optimisticReportActions[reportID]]: null, + [optimisticReportActionID]: null, }, }); @@ -832,6 +812,16 @@ function exportMultipleReportsToIntegration(hash: number, reportIDs: string[], c }); } + if (currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT) { + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, + value: { + data: Object.fromEntries(reportIDs.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null])), + }, + }); + } + const params = { reportIDList: reportIDs, connectionName, diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 5cd69cb1cd2c4..fcdfac4044ac3 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -823,6 +823,10 @@ function SearchPage({route}: SearchPageProps) { return; } + if (shouldShowAccountingModal) { + setAccountingExportModalVisible(true); + } + if (hash) { clearSelectedTransactions(); exportAction(); From d085b819b52776280628a2524011574f1679a1fe Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:00:44 +0700 Subject: [PATCH 07/19] Add report contains export errors --- .../Search/ExpenseReportListItem.tsx | 38 +++++++++---------- src/languages/de.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + 11 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx b/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx index 14bf14256fbe0..6032e3fbb5698 100644 --- a/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx +++ b/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx @@ -26,6 +26,7 @@ import {isInvoiceReport, isOpenExpenseReport, isProcessingReport} from '@libs/Re import {isViolationDismissed, shouldShowViolation} from '@libs/TransactionUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import {isActionLoadingSelector} from '@src/selectors/ReportMetaData'; import type {Policy, Report} from '@src/types/onyx'; @@ -192,11 +193,8 @@ function ExpenseReportListItem({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const hasAnyVisibleViolations = reportItem?.hasVisibleViolations || hasSyncedMissingAttendeesViolation; - const getDescription = useMemo(() => { - if (!hasAnyVisibleViolations || !shouldShowViolationDescription) { - return; - } - return ( + const renderIndicator = useCallback( + (messageKey: TranslationPaths) => ( ({ width={12} height={12} /> - {translate('reportViolations.reportContainsExpensesWithViolations')} + {translate(messageKey)} - ); - }, [ - hasAnyVisibleViolations, - shouldShowViolationDescription, - styles.flexRow, - styles.alignItemsCenter, - styles.mt2, - styles.mr1, - styles.textMicro, - styles.textDanger, - expensifyIcons.DotIndicator, - theme.danger, - translate, - ]); + ), + [styles.flexRow, styles.alignItemsCenter, styles.mt2, styles.mr1, styles.textMicro, styles.textDanger, expensifyIcons.DotIndicator, theme.danger, translate], + ); + + const getDescription = useMemo(() => { + if (!hasAnyVisibleViolations || !shouldShowViolationDescription) { + if (parentReport?.errorFields?.export) { + return renderIndicator('reportViolations.reportContainsExportErrors'); + } + + return null; + } + return renderIndicator('reportViolations.reportContainsExpensesWithViolations'); + }, [hasAnyVisibleViolations, shouldShowViolationDescription, renderIndicator, parentReport?.errorFields?.export]); return ( `${fieldName} ist erforderlich`, reportContainsExpensesWithViolations: 'Der Bericht enthält Ausgaben mit Verstößen.', + reportContainsExportErrors: 'Unsere übliche Fehlermeldung bei fehlgeschlagenem Export', }, violationDismissal: { rter: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 0db4f838d836e..536bb2c66d85e 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7780,6 +7780,7 @@ const translations = { reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} is required`, reportContainsExpensesWithViolations: 'Report contains expenses with violations.', + reportContainsExportErrors: 'Our regular error message for failed export', }, violationDismissal: { rter: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 9d3942b982bd7..3fdb8b5fa1673 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7955,6 +7955,7 @@ ${amount} para ${merchant} - ${date}`, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}) => `${fieldName} es obligatorio`, reportContainsExpensesWithViolations: 'El informe contiene gastos con violaciones.', + reportContainsExportErrors: 'Nuestro mensaje de error habitual para exportaciones fallidas', }, violationDismissal: { rter: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 7d36c70c2c98e..71a122882e6a6 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7854,6 +7854,7 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} est obligatoire`, reportContainsExpensesWithViolations: 'La note de frais contient des dépenses avec des violations.', + reportContainsExportErrors: 'Notre message d’erreur habituel en cas d’échec de l’exportation', }, violationDismissal: { rter: { diff --git a/src/languages/it.ts b/src/languages/it.ts index d20c87eeebaf9..d0b1bce56daec 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7819,6 +7819,7 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} è obbligatorio`, reportContainsExpensesWithViolations: 'Il report contiene spese con violazioni.', + reportContainsExportErrors: 'Il nostro messaggio di errore standard per l’esportazione non riuscita', }, violationDismissal: { rter: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index a5d630455c954..6d07edda561f1 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7748,6 +7748,7 @@ ${reportName} reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} は必須です`, reportContainsExpensesWithViolations: 'レポートに違反のある経費が含まれています。', + reportContainsExportErrors: 'エクスポートに失敗した際の通常のエラーメッセージ', }, violationDismissal: { rter: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 346c8f03e9708..e806f4164685f 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7804,6 +7804,7 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} is verplicht`, reportContainsExpensesWithViolations: 'Rapport bevat onkosten met overtredingen.', + reportContainsExportErrors: 'Onze gebruikelijke foutmelding bij een mislukte export', }, violationDismissal: { rter: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index ef45e321e238e..8cd498a34add1 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7786,6 +7786,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `Pole ${fieldName} jest wymagane`, reportContainsExpensesWithViolations: 'Raport zawiera wydatki z naruszeniami.', + reportContainsExportErrors: 'Nasz standardowy komunikat błędu w przypadku nieudanego eksportu', }, violationDismissal: { rter: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 1e695c675f0ed..8c0eef357ed23 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7789,6 +7789,7 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} é obrigatório`, reportContainsExpensesWithViolations: 'O relatório contém despesas com violações.', + reportContainsExportErrors: 'Nossa mensagem de erro padrão para falha na exportação', }, violationDismissal: { rter: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 2c650ee6998fc..6077cdd96589a 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7630,6 +7630,7 @@ ${reportName} reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} 为必填项`, reportContainsExpensesWithViolations: '报表包含有违规的报销。', + reportContainsExportErrors: '导出失败时的常规错误消息', }, violationDismissal: { rter: { From effb7bd1f5fd66d5125fa1a67a25863d7a2cd7cc Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:22:50 +0700 Subject: [PATCH 08/19] update translation --- src/languages/de.ts | 2 +- src/languages/en.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 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index c44f592ed48dc..87ecb5df6a409 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7832,7 +7832,7 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} ist erforderlich`, reportContainsExpensesWithViolations: 'Der Bericht enthält Ausgaben mit Verstößen.', - reportContainsExportErrors: 'Unsere übliche Fehlermeldung bei fehlgeschlagenem Export', + reportContainsExportErrors: 'Unsere übliche Fehlermeldung bei fehlgeschlagenen Exporten', }, violationDismissal: { rter: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 536bb2c66d85e..2d9caf394c86a 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7780,7 +7780,7 @@ const translations = { reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} is required`, reportContainsExpensesWithViolations: 'Report contains expenses with violations.', - reportContainsExportErrors: 'Our regular error message for failed export', + reportContainsExportErrors: 'Our regular error message for failed to export', }, violationDismissal: { rter: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 71a122882e6a6..b809ec7a37d0f 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7854,7 +7854,7 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} est obligatoire`, reportContainsExpensesWithViolations: 'La note de frais contient des dépenses avec des violations.', - reportContainsExportErrors: 'Notre message d’erreur habituel en cas d’échec de l’exportation', + reportContainsExportErrors: 'Notre message d’erreur habituel pour les exportations échouées', }, violationDismissal: { rter: { diff --git a/src/languages/it.ts b/src/languages/it.ts index d0b1bce56daec..338f6831742dd 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7819,7 +7819,7 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} è obbligatorio`, reportContainsExpensesWithViolations: 'Il report contiene spese con violazioni.', - reportContainsExportErrors: 'Il nostro messaggio di errore standard per l’esportazione non riuscita', + reportContainsExportErrors: 'Il nostro messaggio di errore abituale per le esportazioni non riuscite', }, violationDismissal: { rter: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 6d07edda561f1..48e7721e138a5 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7748,7 +7748,7 @@ ${reportName} reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} は必須です`, reportContainsExpensesWithViolations: 'レポートに違反のある経費が含まれています。', - reportContainsExportErrors: 'エクスポートに失敗した際の通常のエラーメッセージ', + reportContainsExportErrors: 'エクスポートに失敗した場合の通常のエラーメッセージ', }, violationDismissal: { rter: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index e806f4164685f..3cf97393cd28c 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7804,7 +7804,7 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} is verplicht`, reportContainsExpensesWithViolations: 'Rapport bevat onkosten met overtredingen.', - reportContainsExportErrors: 'Onze gebruikelijke foutmelding bij een mislukte export', + reportContainsExportErrors: 'Onze gebruikelijke foutmelding bij mislukte exporten', }, violationDismissal: { rter: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 8cd498a34add1..8754f72b01369 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7786,7 +7786,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `Pole ${fieldName} jest wymagane`, reportContainsExpensesWithViolations: 'Raport zawiera wydatki z naruszeniami.', - reportContainsExportErrors: 'Nasz standardowy komunikat błędu w przypadku nieudanego eksportu', + reportContainsExportErrors: 'Nasz standardowy komunikat błędu dla nieudanych eksportów', }, violationDismissal: { rter: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 8c0eef357ed23..5d4f8f301f85e 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7789,7 +7789,7 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} é obrigatório`, reportContainsExpensesWithViolations: 'O relatório contém despesas com violações.', - reportContainsExportErrors: 'Nossa mensagem de erro padrão para falha na exportação', + reportContainsExportErrors: 'Nossa mensagem de erro padrão para exportações com falha', }, violationDismissal: { rter: { From dfc087f73cbe7d496e8d10dda70e0ae65980cffc Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:40:12 +0700 Subject: [PATCH 09/19] Revert showing export failed error message --- .../Search/ExpenseReportListItem.tsx | 38 ++++++++++--------- src/languages/de.ts | 1 - src/languages/en.ts | 1 - src/languages/es.ts | 1 - src/languages/fr.ts | 1 - src/languages/it.ts | 1 - src/languages/ja.ts | 1 - src/languages/nl.ts | 1 - src/languages/pl.ts | 1 - src/languages/pt-BR.ts | 1 - src/languages/zh-hans.ts | 1 - 11 files changed, 20 insertions(+), 28 deletions(-) diff --git a/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx b/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx index 6032e3fbb5698..14bf14256fbe0 100644 --- a/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx +++ b/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx @@ -26,7 +26,6 @@ import {isInvoiceReport, isOpenExpenseReport, isProcessingReport} from '@libs/Re import {isViolationDismissed, shouldShowViolation} from '@libs/TransactionUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import {isActionLoadingSelector} from '@src/selectors/ReportMetaData'; import type {Policy, Report} from '@src/types/onyx'; @@ -193,8 +192,11 @@ function ExpenseReportListItem({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const hasAnyVisibleViolations = reportItem?.hasVisibleViolations || hasSyncedMissingAttendeesViolation; - const renderIndicator = useCallback( - (messageKey: TranslationPaths) => ( + const getDescription = useMemo(() => { + if (!hasAnyVisibleViolations || !shouldShowViolationDescription) { + return; + } + return ( ({ width={12} height={12} /> - {translate(messageKey)} + {translate('reportViolations.reportContainsExpensesWithViolations')} - ), - [styles.flexRow, styles.alignItemsCenter, styles.mt2, styles.mr1, styles.textMicro, styles.textDanger, expensifyIcons.DotIndicator, theme.danger, translate], - ); - - const getDescription = useMemo(() => { - if (!hasAnyVisibleViolations || !shouldShowViolationDescription) { - if (parentReport?.errorFields?.export) { - return renderIndicator('reportViolations.reportContainsExportErrors'); - } - - return null; - } - return renderIndicator('reportViolations.reportContainsExpensesWithViolations'); - }, [hasAnyVisibleViolations, shouldShowViolationDescription, renderIndicator, parentReport?.errorFields?.export]); + ); + }, [ + hasAnyVisibleViolations, + shouldShowViolationDescription, + styles.flexRow, + styles.alignItemsCenter, + styles.mt2, + styles.mr1, + styles.textMicro, + styles.textDanger, + expensifyIcons.DotIndicator, + theme.danger, + translate, + ]); return ( `${fieldName} ist erforderlich`, reportContainsExpensesWithViolations: 'Der Bericht enthält Ausgaben mit Verstößen.', - reportContainsExportErrors: 'Unsere übliche Fehlermeldung bei fehlgeschlagenen Exporten', }, violationDismissal: { rter: { diff --git a/src/languages/en.ts b/src/languages/en.ts index e3c8576aa03d0..f50b436e0e1ee 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7734,7 +7734,6 @@ const translations = { reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} is required`, reportContainsExpensesWithViolations: 'Report contains expenses with violations.', - reportContainsExportErrors: 'Our regular error message for failed to export', }, violationDismissal: { rter: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 1f9cef4aa13fa..8a1a26607fc58 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7932,7 +7932,6 @@ ${amount} para ${merchant} - ${date}`, reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName) => `${fieldName} es obligatorio`, reportContainsExpensesWithViolations: 'El informe contiene gastos con violaciones.', - reportContainsExportErrors: 'Nuestro mensaje de error habitual para exportaciones fallidas', }, violationDismissal: { rter: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index feb91789446c3..1f071e73bc7b0 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7815,7 +7815,6 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} est obligatoire`, reportContainsExpensesWithViolations: 'La note de frais contient des dépenses avec des violations.', - reportContainsExportErrors: 'Notre message d’erreur habituel pour les exportations échouées', }, violationDismissal: { rter: { diff --git a/src/languages/it.ts b/src/languages/it.ts index fd2b00c8bc436..9d94382f20d96 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7781,7 +7781,6 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} è obbligatorio`, reportContainsExpensesWithViolations: 'Il report contiene spese con violazioni.', - reportContainsExportErrors: 'Il nostro messaggio di errore abituale per le esportazioni non riuscite', }, violationDismissal: { rter: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 1373b571fccde..e1d1af885cacd 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7708,7 +7708,6 @@ ${reportName} reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} は必須です`, reportContainsExpensesWithViolations: 'レポートに違反のある経費が含まれています。', - reportContainsExportErrors: 'エクスポートに失敗した場合の通常のエラーメッセージ', }, violationDismissal: { rter: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index e6609dc187d2e..6fc53d62470de 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7765,7 +7765,6 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} is verplicht`, reportContainsExpensesWithViolations: 'Rapport bevat onkosten met overtredingen.', - reportContainsExportErrors: 'Onze gebruikelijke foutmelding bij mislukte exporten', }, violationDismissal: { rter: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 8494fc8594931..ca7fd7f0b3b1f 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7747,7 +7747,6 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `Pole ${fieldName} jest wymagane`, reportContainsExpensesWithViolations: 'Raport zawiera wydatki z naruszeniami.', - reportContainsExportErrors: 'Nasz standardowy komunikat błędu dla nieudanych eksportów', }, violationDismissal: { rter: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 3df718c852339..b2fbfedd5c61f 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7750,7 +7750,6 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} é obrigatório`, reportContainsExpensesWithViolations: 'O relatório contém despesas com violações.', - reportContainsExportErrors: 'Nossa mensagem de erro padrão para exportações com falha', }, violationDismissal: { rter: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index c9467638b67b5..f6b2a39b4c40c 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7595,7 +7595,6 @@ ${reportName} reportViolations: { [CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} 为必填项`, reportContainsExpensesWithViolations: '报表包含有违规的报销。', - reportContainsExportErrors: '导出失败时的常规错误消息', }, violationDismissal: { rter: { From 2e75d3159406ce5304232b5ea79ffd80d7498093 Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:03:00 +0700 Subject: [PATCH 10/19] Remove in-progress modal and update report name display --- src/pages/Search/SearchPage.tsx | 50 +++++++++++++-------------------- 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 713488e5d2fb2..1b53b8ecf84c8 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -4,7 +4,6 @@ import {InteractionManager, View} from 'react-native'; import Animated from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; -import ConfirmModal from '@components/ConfirmModal'; import DecisionModal from '@components/DecisionModal'; import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider'; import DragAndDropConsumer from '@components/DragAndDrop/Consumer'; @@ -154,7 +153,6 @@ function SearchPage({route}: SearchPageProps) { const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); - const [accountingExportModalVisible, setAccountingExportModalVisible] = useState(false); const [searchRequestResponseStatusCode, setSearchRequestResponseStatusCode] = useState(null); const {showConfirmModal} = useConfirmModal(); const {isBetaEnabled} = usePermissions(); @@ -805,33 +803,42 @@ function SearchPage({route}: SearchPageProps) { const connectionNameFriendly = CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectedIntegration]; const integrationIcon = getIntegrationIcon(connectedIntegration, expensifyIcons); - const handleExportAction = (exportAction: () => void, shouldShowAccountingModal = false) => { + const handleExportAction = (exportAction: () => void) => { if (isOffline) { setIsOfflineModalVisible(true); return; } - let exportedReportName = ''; - const areAnyReportsExported = selectedReportIDs.some((reportID) => { + const exportedReportNames: string[] = []; + let areAnyReportsExported = false; + + for (const reportID of selectedReportIDs) { const unfilteredReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; + if (!unfilteredReportActions) { - return false; + continue; } const reportActions = getFilteredReportActionsForReportView(Object.values(unfilteredReportActions)); + const isExported = isExportedUtils(reportActions); - if (isExported && !exportedReportName) { - exportedReportName = getReportOrDraftReport(reportID)?.reportName ?? ''; + + if (isExported) { + areAnyReportsExported = true; + + const reportName = getReportOrDraftReport(reportID)?.reportName ?? ''; + if (reportName) { + exportedReportNames.push(reportName); + } } - return isExported; - }); + } if (areAnyReportsExported) { showConfirmModal({ title: translate('workspace.exportAgainModal.title'), prompt: translate('workspace.exportAgainModal.description', { connectionName: connectedIntegration, - reportName: exportedReportName, + reportName: exportedReportNames.join('\n'), }), confirmText: translate('workspace.exportAgainModal.confirmText'), cancelText: translate('workspace.exportAgainModal.cancelText'), @@ -840,19 +847,12 @@ function SearchPage({route}: SearchPageProps) { return; } - if (shouldShowAccountingModal) { - setAccountingExportModalVisible(true); - } - if (hash) { clearSelectedTransactions(); exportAction(); } }); } else if (hash) { - if (shouldShowAccountingModal) { - setAccountingExportModalVisible(true); - } exportAction(); clearSelectedTransactions(); } @@ -862,7 +862,7 @@ function SearchPage({route}: SearchPageProps) { { text: connectionNameFriendly, icon: integrationIcon, - onSelected: () => handleExportAction(() => exportMultipleReportsToIntegration(hash, selectedReportIDs, connectedIntegration), true), + onSelected: () => handleExportAction(() => exportMultipleReportsToIntegration(hash, selectedReportIDs, connectedIntegration)), shouldCloseModalOnSelect: true, shouldCallAfterModalHide: true, }, @@ -1501,18 +1501,6 @@ function SearchPage({route}: SearchPageProps) { onConfirm={dismissModalAndUpdateUseHold} /> )} - - { - setAccountingExportModalVisible(false); - }} - onCancel={() => setAccountingExportModalVisible(false)} - title={translate('export.exportInProgress')} - prompt={translate('export.conciergeWillNotifyOnExportFailure')} - confirmText={translate('common.buttonConfirm')} - shouldShowCancelButton={false} - /> )} Date: Wed, 18 Feb 2026 14:48:13 +0700 Subject: [PATCH 11/19] Remove unused translations and add prop for integration icon --- src/languages/de.ts | 1 - src/languages/en.ts | 1 - src/languages/es.ts | 1 - src/languages/fr.ts | 1 - src/languages/it.ts | 1 - src/languages/ja.ts | 1 - src/languages/nl.ts | 1 - src/languages/pl.ts | 1 - src/languages/pt-BR.ts | 1 - src/languages/zh-hans.ts | 1 - src/pages/Search/SearchPage.tsx | 4 ++++ 11 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 58cb4ef56b9e6..d17e23251c4e0 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -8342,7 +8342,6 @@ 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.', - conciergeWillNotifyOnExportFailure: 'Concierge wird dir eine Nachricht senden, wenn Berichte nicht exportiert werden.', }, domain: { notVerified: 'Nicht verifiziert', diff --git a/src/languages/en.ts b/src/languages/en.ts index 517cf90fa8548..493b8e4763ca7 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -8281,7 +8281,6 @@ const translations = { expenseLevelExport: 'All Data - expense level', exportInProgress: 'Export in progress', conciergeWillSend: 'Concierge will send you the file shortly.', - conciergeWillNotifyOnExportFailure: "Concierge will send you a message if any reports don't export.", }, domain: { notVerified: 'Not verified', diff --git a/src/languages/es.ts b/src/languages/es.ts index 7db2e26070d05..297e20e73d8a2 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -8486,7 +8486,6 @@ ${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.', - conciergeWillNotifyOnExportFailure: 'Concierge te enviará un mensaje si algún informe no se exporta.', }, openAppFailureModal: { title: 'Algo salió mal...', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 977c66551a5f9..6a2d2b204bfd7 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -8364,7 +8364,6 @@ 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.', - conciergeWillNotifyOnExportFailure: 'Concierge vous enverra un message si certains rapports ne sont pas exportés.', }, domain: { notVerified: 'Non vérifié', diff --git a/src/languages/it.ts b/src/languages/it.ts index bb36fec9c1ffa..ecb9d7e17519b 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -8328,7 +8328,6 @@ 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.', - conciergeWillNotifyOnExportFailure: 'Concierge ti invierà un messaggio se alcuni report non vengono esportati.', }, domain: { notVerified: 'Non verificato', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 67d5f9ff74497..f0d95b7ba1882 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -8249,7 +8249,6 @@ ${reportName} expenseLevelExport: 'すべてのデータ - 経費レベル', exportInProgress: 'エクスポート処理中', conciergeWillSend: 'Conciergeがまもなくファイルを送信します。', - conciergeWillNotifyOnExportFailure: 'レポートのエクスポートに失敗した場合、Concierge からメッセージが届きます。', }, domain: { notVerified: '未確認', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 67bd22db807e1..aa76a24398840 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -8312,7 +8312,6 @@ 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.', - conciergeWillNotifyOnExportFailure: 'Concierge stuurt je een bericht als rapporten niet worden geëxporteerd.', }, domain: { notVerified: 'Niet geverifieerd', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 5577138df7471..3d8c767b0bb77 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -8292,7 +8292,6 @@ 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.', - conciergeWillNotifyOnExportFailure: 'Concierge wyśle Ci wiadomość, jeśli niektóre raporty nie zostaną wyeksportowane.', }, domain: { notVerified: 'Niezweryfikowane', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 67fccd588f11c..71d3990b0fc88 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -8296,7 +8296,6 @@ 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.', - conciergeWillNotifyOnExportFailure: 'O Concierge enviará uma mensagem se algum relatório não for exportado.', }, domain: { notVerified: 'Não verificado', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 228214987b9db..1c6d0390a553e 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -8123,7 +8123,6 @@ ${reportName} expenseLevelExport: '所有数据 - 报销级别', exportInProgress: '导出进行中', conciergeWillSend: 'Concierge 将很快把文件发送给你。', - conciergeWillNotifyOnExportFailure: '如果有报告导出失败,Concierge 将向你发送消息。', }, domain: { notVerified: '未验证', diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index cee0ef214f1fa..6693021cf24de 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -872,6 +872,8 @@ function SearchPage({route}: SearchPageProps) { onSelected: () => handleExportAction(() => exportMultipleReportsToIntegration(hash, selectedReportIDs, connectedIntegration)), shouldCloseModalOnSelect: true, shouldCallAfterModalHide: true, + displayInDefaultIconColor: true, + additionalIconStyles: styles.integrationIcon, }, { text: translate('workspace.common.markAsExported'), @@ -879,6 +881,8 @@ function SearchPage({route}: SearchPageProps) { onSelected: () => handleExportAction(() => markAsManuallyExportedMultipleReports(selectedReportIDs, connectedIntegration)), shouldCloseModalOnSelect: true, shouldCallAfterModalHide: true, + displayInDefaultIconColor: true, + additionalIconStyles: styles.integrationIcon, }, ); } From e4b7ec1dad1e110658f9581a566281f0e9a519e5 Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Sat, 21 Feb 2026 14:38:24 +0700 Subject: [PATCH 12/19] Remove duplicate code and update optimistic data --- src/libs/actions/Report/index.ts | 56 +---------------- src/libs/actions/Search.ts | 103 +++---------------------------- src/pages/Search/SearchPage.tsx | 8 +-- 3 files changed, 14 insertions(+), 153 deletions(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index cd50da22112cb..88fd1a8788cfa 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -4957,61 +4957,10 @@ function exportToIntegration(reportID: string, connectionName: ConnectionName) { API.write(WRITE_COMMANDS.REPORT_EXPORT, params, {optimisticData, failureData}); } -function markAsManuallyExported(reportID: string, connectionName: ConnectionName) { - const action = buildOptimisticExportIntegrationAction(connectionName, true); +function markAsManuallyExported(reportIDOrIDs: string | string[], connectionName: ConnectionName) { const label = CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]; - const optimisticReportActionID = action.reportActionID; - - const optimisticData: Array> = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: { - [optimisticReportActionID]: action, - }, - }, - ]; - - const successData: Array> = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: { - [optimisticReportActionID]: { - pendingAction: null, - }, - }, - }, - ]; + const reportIDs = Array.isArray(reportIDOrIDs) ? reportIDOrIDs : [reportIDOrIDs]; - const failureData: Array> = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: { - [optimisticReportActionID]: { - errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), - }, - }, - }, - ]; - - const params = { - markedManually: true, - data: JSON.stringify([ - { - reportID, - label, - optimisticReportActionID, - }, - ]), - } satisfies MarkAsExportedParams; - - API.write(WRITE_COMMANDS.MARK_AS_EXPORTED, params, {optimisticData, successData, failureData}); -} - -function markAsManuallyExportedMultipleReports(reportIDs: string[], connectionName: ConnectionName) { - const label = CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]; const optimisticData: Array> = []; const successData: Array> = []; const failureData: Array> = []; @@ -6749,7 +6698,6 @@ export { leaveGroupChat, leaveRoom, markAsManuallyExported, - markAsManuallyExportedMultipleReports, markCommentAsUnread, navigateToAndOpenChildReport, createChildReport, diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index a64d0a89b6e57..2013546b1afbf 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -644,94 +644,12 @@ function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], curre API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, failureData, successData}); } -function exportToIntegrationOnSearch(hash: number, reportID: string | undefined, connectionName: ConnectionName, currentSearchKey?: SearchKey) { - if (!reportID) { - return; - } - const optimisticAction = buildOptimisticExportIntegrationAction(connectionName); - const successAction: OptimisticExportIntegrationAction = {...optimisticAction, pendingAction: null}; - const optimisticReportActionID = optimisticAction.reportActionID; - - const optimisticData: Array> = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: {isActionLoading: true}, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: { - [optimisticReportActionID]: optimisticAction, - }, - }, - ]; - - const successData: Array> = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: {isActionLoading: false}, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: { - [optimisticReportActionID]: successAction, - }, - }, - ]; - - // If we are on the 'Export' suggested search, remove the report from the view once the action is taken, don't wait for the view to be re-fetched via Search - if (currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT) { - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, - value: { - data: { - [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: null, - }, - }, - }); - } - - const failureData: Array> = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: {isActionLoading: false}, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: { - [optimisticReportActionID]: null, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: {errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, - }, - ]; - - const params = { - reportIDList: reportID, - connectionName, - type: 'MANUAL', - optimisticReportActions: JSON.stringify({ - [reportID]: optimisticReportActionID, - }), - } satisfies ReportExportParams; - - API.write(WRITE_COMMANDS.REPORT_EXPORT, params, {optimisticData, failureData, successData}); -} - -function exportMultipleReportsToIntegration(hash: number, reportIDs: string[], connectionName: ConnectionName, currentSearchKey?: SearchKey) { - if (!reportIDs.length) { +function exportToIntegrationOnSearch(hash: number, reportIDOrIDs: string | string[] | undefined, connectionName: ConnectionName, currentSearchKey?: SearchKey) { + if (!reportIDOrIDs || (Array.isArray(reportIDOrIDs) && !reportIDOrIDs.length)) { return; } + const reportIDs = Array.isArray(reportIDOrIDs) ? reportIDOrIDs : [reportIDOrIDs]; const optimisticReportActions: Record = {}; const optimisticData: Array> = [ @@ -742,15 +660,9 @@ function exportMultipleReportsToIntegration(hash: number, reportIDs: string[], c }, ]; - const successData: Array> = [ - { - onyxMethod: Onyx.METHOD.MERGE_COLLECTION, - key: ONYXKEYS.COLLECTION.REPORT_METADATA, - value: Object.fromEntries(reportIDs.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {isActionLoading: false}])), - }, - ]; + const successData: Array> = []; - const failureData: Array> = [ + const finallyData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE_COLLECTION, key: ONYXKEYS.COLLECTION.REPORT_METADATA, @@ -758,6 +670,8 @@ function exportMultipleReportsToIntegration(hash: number, reportIDs: string[], c }, ]; + const failureData: Array> = []; + for (const reportID of reportIDs) { const optimisticAction = buildOptimisticExportIntegrationAction(connectionName); const successAction: OptimisticExportIntegrationAction = {...optimisticAction, pendingAction: null}; @@ -813,7 +727,7 @@ function exportMultipleReportsToIntegration(hash: number, reportIDs: string[], c optimisticReportActions: JSON.stringify(optimisticReportActions), } satisfies ReportExportParams; - API.write(WRITE_COMMANDS.REPORT_EXPORT, params, {optimisticData, failureData, successData}); + API.write(WRITE_COMMANDS.REPORT_EXPORT, params, {optimisticData, successData, failureData, finallyData}); } function payMoneyRequestOnSearch(hash: number, paymentData: PaymentData[], currentSearchKey?: SearchKey) { @@ -1570,7 +1484,6 @@ export { getLastPolicyPaymentMethod, getLastPolicyBankAccountID, exportToIntegrationOnSearch, - exportMultipleReportsToIntegration, getPayOption, isValidBulkPayOption, handleBulkPayItemSelected, diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index e226c382da05c..5c30757f94d06 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -39,12 +39,12 @@ import useSelfDMReport from '@hooks/useSelfDMReport'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction'; -import {deleteAppReport, markAsManuallyExportedMultipleReports, moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter, searchInServer} from '@libs/actions/Report'; +import {deleteAppReport, markAsManuallyExported, moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter, searchInServer} from '@libs/actions/Report'; import { approveMoneyRequestOnSearch, bulkDeleteReports, - exportMultipleReportsToIntegration, exportSearchItemsToCSV, + exportToIntegrationOnSearch, getExportTemplates, getLastPolicyBankAccountID, getLastPolicyPaymentMethod, @@ -851,7 +851,7 @@ function SearchPage({route}: SearchPageProps) { { text: connectionNameFriendly, icon: integrationIcon, - onSelected: () => handleExportAction(() => exportMultipleReportsToIntegration(hash, selectedReportIDs, connectedIntegration)), + onSelected: () => handleExportAction(() => exportToIntegrationOnSearch(hash, selectedReportIDs, connectedIntegration)), shouldCloseModalOnSelect: true, shouldCallAfterModalHide: true, displayInDefaultIconColor: true, @@ -860,7 +860,7 @@ function SearchPage({route}: SearchPageProps) { { text: translate('workspace.common.markAsExported'), icon: integrationIcon, - onSelected: () => handleExportAction(() => markAsManuallyExportedMultipleReports(selectedReportIDs, connectedIntegration)), + onSelected: () => handleExportAction(() => markAsManuallyExported(selectedReportIDs, connectedIntegration)), shouldCloseModalOnSelect: true, shouldCallAfterModalHide: true, displayInDefaultIconColor: true, From a0ba4517fb65117c11a95f727855e3c5e0befa3d Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Sat, 21 Feb 2026 14:55:10 +0700 Subject: [PATCH 13/19] Use snapshot data to get report actions --- src/pages/Search/SearchPage.tsx | 33 ++++++--------------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 5c30757f94d06..9abcd2f7cdf2b 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -132,7 +132,6 @@ function SearchPage({route}: SearchPageProps) { const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: true}); const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const {accountID} = useCurrentUserPersonalDetails(); - const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {canBeMissing: true}); const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); @@ -397,6 +396,8 @@ function SearchPage({route}: SearchPageProps) { selectAllMatchingItems, clearSelectedTransactions, setIsDownloadErrorModalVisible, + selectedReportIDs, + selectedTransactionReportIDs, ]); const handleApproveWithDEWCheck = useCallback(async () => { @@ -769,7 +770,7 @@ function SearchPage({route}: SearchPageProps) { } const reportPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; - const completeReport = getReportOrDraftReport(report.reportID) ?? currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]; + const completeReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]; if (!completeReport) { return false; @@ -802,7 +803,7 @@ function SearchPage({route}: SearchPageProps) { let areAnyReportsExported = false; for (const reportID of selectedReportIDs) { - const unfilteredReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; + const unfilteredReportActions = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; if (!unfilteredReportActions) { continue; @@ -815,7 +816,7 @@ function SearchPage({route}: SearchPageProps) { if (isExported) { areAnyReportsExported = true; - const reportName = getReportOrDraftReport(reportID)?.reportName ?? ''; + const reportName = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.reportName ?? ''; if (reportName) { exportedReportNames.push(reportName); } @@ -1206,7 +1207,6 @@ function SearchPage({route}: SearchPageProps) { currentUserPersonalDetails.accountID, currentUserPersonalDetails?.login, bankAccountList, - allReportActions, handleBasicExport, beginExportWithTemplate, handleApproveWithDEWCheck, @@ -1226,29 +1226,8 @@ function SearchPage({route}: SearchPageProps) { styles.colorMuted, styles.fontWeightNormal, styles.textWrap, + styles.integrationIcon, showConfirmModal, - expensifyIcons.ArrowCollapse, - expensifyIcons.ArrowRight, - expensifyIcons.ArrowSplit, - expensifyIcons.DocumentMerge, - expensifyIcons.Exclamation, - expensifyIcons.Export, - expensifyIcons.MoneyBag, - expensifyIcons.Send, - expensifyIcons.Stopwatch, - expensifyIcons.Table, - expensifyIcons.ThumbsDown, - expensifyIcons.ThumbsUp, - expensifyIcons.Trashcan, - dismissedHoldUseExplanation, - dismissedRejectUseExplanation, - areAllTransactionsFromSubmitter, - allTransactionViolations, - currentSearchResults?.data, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - currentUserPersonalDetails.accountID, - personalPolicyID, ]); const {initScanRequest, PDFValidationComponent, ErrorModal} = useReceiptScanDrop(); From 5820af75f7ba58a3e2b6af45e9c25c77c85eac71 Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:08:03 +0700 Subject: [PATCH 14/19] Update logic to get exported reports --- src/pages/Search/SearchPage.tsx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 7dbe4356bb024..797bc7369cca8 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -803,23 +803,16 @@ function SearchPage({route}: SearchPageProps) { let areAnyReportsExported = false; for (const reportID of selectedReportIDs) { - const unfilteredReportActions = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - if (!unfilteredReportActions) { + if (!report?.isExportedToIntegration) { continue; } - const reportActions = getFilteredReportActionsForReportView(Object.values(unfilteredReportActions)); - - const isExported = isExportedUtils(reportActions); + areAnyReportsExported = true; - if (isExported) { - areAnyReportsExported = true; - - const reportName = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.reportName ?? ''; - if (reportName) { - exportedReportNames.push(reportName); - } + if (report.reportName) { + exportedReportNames.push(report.reportName); } } From 205976aaa03e0738ef5ccd3cd3004bdbf5c4c66f Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:09:09 +0700 Subject: [PATCH 15/19] Remove unused imports --- src/pages/Search/SearchPage.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 797bc7369cca8..9a149396e1683 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -67,7 +67,6 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; import {getConnectedIntegration, hasDynamicExternalWorkflow} from '@libs/PolicyUtils'; -import {getFilteredReportActionsForReportView} from '@libs/ReportActionsUtils'; import {getSecondaryExportReportActions, isMergeActionForSelectedTransactions} from '@libs/ReportSecondaryActionUtils'; import { getIntegrationIcon, @@ -75,7 +74,6 @@ import { isBusinessInvoiceRoom, isCurrentUserSubmitter, isExpenseReport as isExpenseReportUtil, - isExported as isExportedUtils, isInvoiceReport, isIOUReport as isIOUReportUtil, } from '@libs/ReportUtils'; From 3dde75bacf44f4a529fb4ca4dc8a8a0c6bdce5ef Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Tue, 24 Feb 2026 07:53:08 +0700 Subject: [PATCH 16/19] Use report ID as array param when exporting report --- src/components/MoneyReportHeader.tsx | 4 ++-- .../ReportActionItem/ExportWithDropdownMenu.tsx | 4 ++-- src/libs/actions/Report/index.ts | 3 +-- src/libs/actions/Search.ts | 13 ++++++++----- src/pages/inbox/report/ReportDetailsExportPage.tsx | 2 +- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index d5bf29943fb4d..83039417c896d 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -894,7 +894,7 @@ function MoneyReportHeader({ if (exportModalStatus === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) { exportToIntegration(moneyRequestReport?.reportID, connectedIntegration); } else if (exportModalStatus === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) { - markAsManuallyExported(moneyRequestReport?.reportID, connectedIntegration); + markAsManuallyExported([moneyRequestReport?.reportID ?? ''], connectedIntegration); } }, [connectedIntegration, exportModalStatus, moneyRequestReport?.reportID]); @@ -985,7 +985,7 @@ function MoneyReportHeader({ setExportModalStatus(CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED); return; } - markAsManuallyExported(moneyRequestReport?.reportID, connectedIntegration); + markAsManuallyExported([moneyRequestReport?.reportID ?? ''], connectedIntegration); }, }, }; diff --git a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx index 4fe5938493e4d..f597547efde08 100644 --- a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx +++ b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx @@ -96,7 +96,7 @@ function ExportWithDropdownMenu({ if (modalStatus === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) { exportToIntegration(reportID, connectionName); } else if (modalStatus === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) { - markAsManuallyExported(reportID, connectionName); + markAsManuallyExported([reportID], connectionName); } }, [connectionName, modalStatus, reportID]); @@ -125,7 +125,7 @@ function ExportWithDropdownMenu({ if (value === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) { exportToIntegration(reportID, connectionName); } else if (value === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) { - markAsManuallyExported(reportID, connectionName); + markAsManuallyExported([reportID], connectionName); } }} onOptionSelected={({value}) => savePreferredExportMethod(value)} diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 88fd1a8788cfa..beb8838647b02 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -4957,9 +4957,8 @@ function exportToIntegration(reportID: string, connectionName: ConnectionName) { API.write(WRITE_COMMANDS.REPORT_EXPORT, params, {optimisticData, failureData}); } -function markAsManuallyExported(reportIDOrIDs: string | string[], connectionName: ConnectionName) { +function markAsManuallyExported(reportIDs: string[], connectionName: ConnectionName) { const label = CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]; - const reportIDs = Array.isArray(reportIDOrIDs) ? reportIDOrIDs : [reportIDOrIDs]; const optimisticData: Array> = []; const successData: Array> = []; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 2013546b1afbf..084e096cab529 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -166,7 +166,11 @@ function handleActionButtonPress({ return; } - exportToIntegrationOnSearch(hash, item?.reportID, connectedIntegration, currentSearchKey); + if (!item?.reportID) { + return; + } + + exportToIntegrationOnSearch(hash, [item.reportID], connectedIntegration, currentSearchKey); return; } default: @@ -644,12 +648,10 @@ function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], curre API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, failureData, successData}); } -function exportToIntegrationOnSearch(hash: number, reportIDOrIDs: string | string[] | undefined, connectionName: ConnectionName, currentSearchKey?: SearchKey) { - if (!reportIDOrIDs || (Array.isArray(reportIDOrIDs) && !reportIDOrIDs.length)) { +function exportToIntegrationOnSearch(hash: number, reportIDs: string[], connectionName: ConnectionName, currentSearchKey?: SearchKey) { + if (!reportIDs.length) { return; } - - const reportIDs = Array.isArray(reportIDOrIDs) ? reportIDOrIDs : [reportIDOrIDs]; const optimisticReportActions: Record = {}; const optimisticData: Array> = [ @@ -710,6 +712,7 @@ function exportToIntegrationOnSearch(hash: number, reportIDOrIDs: string | strin }); } + // If we are on the 'Export' suggested search, remove the report from the view once the action is taken, don't wait for the view to be re-fetched via Search if (currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT) { successData.push({ onyxMethod: Onyx.METHOD.MERGE, diff --git a/src/pages/inbox/report/ReportDetailsExportPage.tsx b/src/pages/inbox/report/ReportDetailsExportPage.tsx index 57aa99d015ef1..e0d8e9606facd 100644 --- a/src/pages/inbox/report/ReportDetailsExportPage.tsx +++ b/src/pages/inbox/report/ReportDetailsExportPage.tsx @@ -51,7 +51,7 @@ function ReportDetailsExportPage({route}: ReportDetailsExportPageProps) { if (type === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) { exportToIntegration(reportID, connectionName); } else if (type === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) { - markAsManuallyExported(reportID, connectionName); + markAsManuallyExported([reportID], connectionName); } Navigation.dismissModal(); }, From f70e68e7c065f3590c88e4970c6f5836adb998eb Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Tue, 24 Feb 2026 07:59:45 +0700 Subject: [PATCH 17/19] Update default report ID --- src/components/MoneyReportHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 83039417c896d..cfaba97c1cd89 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -894,7 +894,7 @@ function MoneyReportHeader({ if (exportModalStatus === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) { exportToIntegration(moneyRequestReport?.reportID, connectedIntegration); } else if (exportModalStatus === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) { - markAsManuallyExported([moneyRequestReport?.reportID ?? ''], connectedIntegration); + markAsManuallyExported([moneyRequestReport?.reportID ?? CONST.DEFAULT_NUMBER_ID], connectedIntegration); } }, [connectedIntegration, exportModalStatus, moneyRequestReport?.reportID]); @@ -985,7 +985,7 @@ function MoneyReportHeader({ setExportModalStatus(CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED); return; } - markAsManuallyExported([moneyRequestReport?.reportID ?? ''], connectedIntegration); + markAsManuallyExported([moneyRequestReport?.reportID ?? CONST.DEFAULT_NUMBER_ID], connectedIntegration); }, }, }; From de48a87fefce1aaba1bbd994104dfeb1545afc24 Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:01:49 +0700 Subject: [PATCH 18/19] run prettier --- src/components/AvatarCropModal/AvatarCropModal.tsx | 2 +- src/components/HeaderWithBackButton/index.tsx | 2 +- src/components/Lightbox/index.tsx | 2 +- src/components/PDFThumbnail/index.native.tsx | 2 +- src/components/TransactionItemRow/ReceiptPreview/index.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index 033279932f39b..ac77934485a92 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -7,7 +7,6 @@ import ImageSize from 'react-native-image-size'; import {interpolate, useSharedValue} from 'react-native-reanimated'; import {scheduleOnUI} from 'react-native-worklets'; import ActivityIndicator from '@components/ActivityIndicator'; -import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import Button from '@components/Button'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Icon from '@components/Icon'; @@ -24,6 +23,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import cropOrRotateImage from '@libs/cropOrRotateImage'; import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; +import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; import ImageCropView from './ImageCropView'; diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 2d81005e4feb5..96c2c9c6b7f92 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -2,7 +2,6 @@ import React, {useMemo} from 'react'; import {Keyboard, StyleSheet, View} from 'react-native'; import type {SvgProps} from 'react-native-svg'; import ActivityIndicator from '@components/ActivityIndicator'; -import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import Avatar from '@components/Avatar'; import AvatarWithDisplayName from '@components/AvatarWithDisplayName'; import Header from '@components/Header'; @@ -21,6 +20,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useThrottledButtonState from '@hooks/useThrottledButtonState'; import getButtonState from '@libs/getButtonState'; import Navigation from '@libs/Navigation/Navigation'; +import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index 6ad732981b180..63cfeb4fbc799 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -3,7 +3,6 @@ import type {LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native'; import {PixelRatio, StyleSheet, View} from 'react-native'; import {useSharedValue} from 'react-native-reanimated'; import ActivityIndicator from '@components/ActivityIndicator'; -import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import AttachmentOfflineIndicator from '@components/AttachmentOfflineIndicator'; import {useAttachmentCarouselPagerActions, useAttachmentCarouselPagerState} from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import type {Attachment} from '@components/Attachments/types'; @@ -17,6 +16,7 @@ import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {isLocalFile} from '@libs/fileDownload/FileUtils'; +import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import CONST from '@src/CONST'; import type {Dimensions} from '@src/types/utils/Layout'; import NUMBER_OF_CONCURRENT_LIGHTBOXES from './numberOfConcurrentLightboxes'; diff --git a/src/components/PDFThumbnail/index.native.tsx b/src/components/PDFThumbnail/index.native.tsx index 09778fde31940..024bbad2a627c 100644 --- a/src/components/PDFThumbnail/index.native.tsx +++ b/src/components/PDFThumbnail/index.native.tsx @@ -2,9 +2,9 @@ import React, {useState} from 'react'; import {View} from 'react-native'; import Pdf from 'react-native-pdf'; import LoadingIndicator from '@components/LoadingIndicator'; -import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import useThemeStyles from '@hooks/useThemeStyles'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; +import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import PDFThumbnailError from './PDFThumbnailError'; import type PDFThumbnailProps from './types'; diff --git a/src/components/TransactionItemRow/ReceiptPreview/index.tsx b/src/components/TransactionItemRow/ReceiptPreview/index.tsx index f2ffbb0c8da2e..c102d9fdfc6ff 100644 --- a/src/components/TransactionItemRow/ReceiptPreview/index.tsx +++ b/src/components/TransactionItemRow/ReceiptPreview/index.tsx @@ -4,7 +4,6 @@ import type {LayoutChangeEvent} from 'react-native'; import {StyleSheet, View} from 'react-native'; import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'; import ActivityIndicator from '@components/ActivityIndicator'; -import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import DistanceEReceipt from '@components/DistanceEReceipt'; import EReceiptWithSizeCalculation from '@components/EReceiptWithSizeCalculation'; import type {ImageOnLoadEvent} from '@components/Image/types'; @@ -12,6 +11,7 @@ import useDebouncedState from '@hooks/useDebouncedState'; import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import {hasReceiptSource, isDistanceRequest, isManualDistanceRequest, isPerDiemRequest} from '@libs/TransactionUtils'; import variables from '@styles/variables'; import Image from '@src/components/Image'; From ee6ecf1b797e19a7b077bc65979412cb1a96c9fc Mon Sep 17 00:00:00 2001 From: huutech <20178761+huult@users.noreply.github.com> Date: Wed, 25 Feb 2026 07:32:34 +0700 Subject: [PATCH 19/19] update dependencies --- src/pages/Search/SearchPage.tsx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index a1a0fd73eae25..fc8bafeb24bdd 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -396,8 +396,6 @@ function SearchPage({route}: SearchPageProps) { translate, clearSelectedTransactions, setIsDownloadErrorModalVisible, - selectedReportIDs, - selectedTransactionReportIDs, showConfirmModal, hash, selectAllMatchingItems, @@ -1184,19 +1182,6 @@ function SearchPage({route}: SearchPageProps) { selectedTransactions, queryJSON?.type, expensifyIcons, - expensifyIcons.Export, - expensifyIcons.ArrowRight, - expensifyIcons.Table, - expensifyIcons.ThumbsUp, - expensifyIcons.ThumbsDown, - expensifyIcons.Send, - expensifyIcons.MoneyBag, - expensifyIcons.Stopwatch, - expensifyIcons.ArrowCollapse, - expensifyIcons.DocumentMerge, - expensifyIcons.ArrowSplit, - expensifyIcons.Trashcan, - expensifyIcons.Exclamation, translate, areAllMatchingItemsSelected, isOffline, @@ -1205,7 +1190,6 @@ function SearchPage({route}: SearchPageProps) { selectedReportIDs, personalPolicyID, searchResults, - allTransactions, currentSearchResults?.data, selectedTransactionReportIDs, selectedPolicyIDs, @@ -1229,8 +1213,6 @@ function SearchPage({route}: SearchPageProps) { dismissedHoldUseExplanation, localeCompare, allReports, - currentUserPersonalDetails.accountID, - localeCompare, firstTransaction, firstTransactionPolicy, handleDeleteSelectedTransactions,