diff --git a/src/hooks/useExportActions.ts b/src/hooks/useExportActions.ts index b6ad6a822d52..992db447e753 100644 --- a/src/hooks/useExportActions.ts +++ b/src/hooks/useExportActions.ts @@ -126,7 +126,23 @@ function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsPa }); }; + const showEmptyReportDownloadErrorModal = () => { + showDecisionModal({ + title: translate('common.downloadFailedTitle'), + prompt: translate('common.downloadFailedEmptyReportDescription', {count: 1}), + secondOptionText: translate('common.buttonConfirm'), + }); + }; + + // A report without expenses has nothing to export, so the export is blocked the same way it is in the Search export flow. + const isEmptyReport = transactionIDs.length === 0 && (moneyRequestReport?.transactionCount ?? 0) === 0; + const beginExportWithTemplate = (templateName: string, templateType: string, transactionIDList: string[], exportName: string, policyID?: string) => { + if (isEmptyReport) { + showEmptyReportDownloadErrorModal(); + return; + } + if (isOffline) { showOfflineModal(); return; @@ -163,6 +179,10 @@ function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsPa if (!moneyRequestReport) { return; } + if (isEmptyReport) { + showEmptyReportDownloadErrorModal(); + return; + } if (isOffline) { showOfflineModal(); return; diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 5c55480d9bf3..4c3861f6ffe7 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -648,6 +648,20 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { .map((report) => report.reportID) .filter((reportID) => reportID !== undefined); + // Reports with no expenses have nothing to export, so every export flow blocks them instead of producing an empty file. + const emptyReports = useMemo( + () => + selectedReports.filter((selectedReport) => { + if (!selectedReport) { + return false; + } + const fullReport = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${selectedReport.reportID}`]; + return !!fullReport && (fullReport.transactionCount ?? 0) === 0; + }), + [selectedReports, currentSearchResults?.data], + ); + const hasOnlyEmptyReports = selectedReports.length > 0 && emptyReports.length === selectedReports.length; + const payableSelectedReports = useMemo(() => selectedReports.filter((report) => report.canPay), [selectedReports]); const payableSelectedReportIDs = useMemo(() => payableSelectedReports.map((report) => report.reportID).filter((reportID) => reportID !== undefined), [payableSelectedReports]); const payScopedReports = useMemo(() => (payableSelectedReports.length > 0 ? payableSelectedReports : selectedReports), [payableSelectedReports, selectedReports]); @@ -916,16 +930,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { const beginExportWithTemplate = useCallback( (templateName: string, templateType: string, policyID: string | undefined, exportName: string) => { - const emptyReports = - selectedReports?.filter((selectedReport) => { - if (!selectedReport) { - return false; - } - const fullReport = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${selectedReport.reportID}`]; - return !!fullReport && (fullReport.transactionCount ?? 0) === 0; - }) ?? []; - const hasOnlyEmptyReports = selectedReports.length > 0 && emptyReports.length === selectedReports.length; - if (hasOnlyEmptyReports) { setEmptyReportsCount(emptyReports.length); setIsDownloadErrorModalVisible(true); @@ -976,7 +980,8 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { clearSelectedTransactions(undefined, true); }, [ - selectedReports, + hasOnlyEmptyReports, + emptyReports.length, selectedTransactions, isOffline, areAllMatchingItemsSelected, @@ -1056,6 +1061,11 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { const handleCSVExport = useCallback( async (isBasicExport: boolean) => { + if (hasOnlyEmptyReports) { + setEmptyReportsCount(emptyReports.length); + setIsDownloadErrorModalVisible(true); + return; + } if (isOffline) { setIsOfflineModalVisible(true); return; @@ -1119,6 +1129,8 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { } }, [ + hasOnlyEmptyReports, + emptyReports.length, isOffline, areAllMatchingItemsSelected, queryJSON, diff --git a/tests/unit/hooks/useExportActionsTest.ts b/tests/unit/hooks/useExportActionsTest.ts index 6e94abde95fb..fead77c84605 100644 --- a/tests/unit/hooks/useExportActionsTest.ts +++ b/tests/unit/hooks/useExportActionsTest.ts @@ -102,6 +102,7 @@ describe('useExportActions - template export status modal', () => { beforeEach(() => { jest.clearAllMocks(); mockIsOffline = false; + mockReportTransactions = {transaction1: {...createRandomTransaction(1), reportID: REPORT_ID}}; }); it('queues the export with progress tracking', () => { @@ -137,6 +138,18 @@ describe('useExportActions - template export status modal', () => { expect(mockQueueExportSearchWithTemplate).not.toHaveBeenCalled(); expect(mockShowDecisionModal).toHaveBeenCalled(); }); + + it('does not queue the export and shows the empty report modal when the report has no expenses', () => { + mockReportTransactions = {}; + const {result} = renderHook(() => useExportActions({reportID: REPORT_ID})); + + act(() => { + result.current.beginExportWithTemplate('Test Template', 'csv', [], EXPORT_NAME, POLICY_ID); + }); + + expect(mockQueueExportSearchWithTemplate).not.toHaveBeenCalled(); + expect(mockShowDecisionModal).toHaveBeenCalledWith(expect.objectContaining({prompt: 'common.downloadFailedEmptyReportDescription:1'})); + }); }); describe('useExportActions - download labels', () => {