Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions src/components/ExportDownloadStatusModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {getOldDotURLFromEnvironment} from '@libs/Environment/Environment';
import fileDownload from '@libs/fileDownload';
import Navigation from '@libs/Navigation/Navigation';
import addTrailingForwardSlash from '@libs/UrlUtils';
import {clearExportDownload, sendExportFileFromConcierge} from '@userActions/Export';
import {sendExportFileFromConcierge} from '@userActions/Export';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
Expand Down Expand Up @@ -92,11 +92,6 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
}
};

const handleClose = () => {
clearExportDownload(exportID, displayedExport ?? undefined);
onClose();
};

const isNonDismissible = isPreparing;

const renderContent = () => {
Expand Down Expand Up @@ -153,7 +148,7 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
/>
<Button
text={translate('exportDownload.close')}
onPress={handleClose}
onPress={onClose}
style={[styles.w100, styles.mt3]}
/>
</>
Expand All @@ -167,7 +162,7 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
{!!failedBody && <Text style={styles.mb5}>{failedBody}</Text>}
<Button
text={translate('exportDownload.close')}
onPress={handleClose}
onPress={onClose}
style={styles.w100}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo
onRejectModalOpen: openRejectModal,
});

const {exportActionEntries} = useExportActions({
const {exportActionEntries, exportDownloadStatusModal} = useExportActions({
reportID,
policy,
onPDFModalOpen: openPDFDownload,
Expand Down Expand Up @@ -400,21 +400,24 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo
};

if (!applicableSecondaryActions.length) {
return null;
return exportDownloadStatusModal;
}

return (
<MoneyReportHeaderKYCDropdown
chatReportID={chatReport?.reportID}
iouReport={moneyRequestReport}
onPaymentSelect={onPaymentSelect}
onSuccessfulKYC={(type) => confirmPayment({paymentType: type})}
primaryAction={primaryAction}
applicableSecondaryActions={applicableSecondaryActions}
dropdownMenuRef={dropdownMenuRef}
onOptionsMenuHide={handleOptionsMenuHide}
ref={kycWallRef}
/>
<>
{exportDownloadStatusModal}
<MoneyReportHeaderKYCDropdown
chatReportID={chatReport?.reportID}
iouReport={moneyRequestReport}
onPaymentSelect={onPaymentSelect}
onSuccessfulKYC={(type) => confirmPayment({paymentType: type})}
primaryAction={primaryAction}
applicableSecondaryActions={applicableSecondaryActions}
dropdownMenuRef={dropdownMenuRef}
onOptionsMenuHide={handleOptionsMenuHide}
ref={kycWallRef}
/>
</>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn

const expensifyIcons = useMemoizedLazyExpensifyIcons(PAYMENT_ICONS);

const {beginExportWithTemplate, showOfflineModal, showDownloadErrorModal} = useExportActions({
const {beginExportWithTemplate, showOfflineModal, showDownloadErrorModal, exportDownloadStatusModal} = useExportActions({
reportID,
policy,
});
Expand Down Expand Up @@ -514,6 +514,7 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn
return (
<>
{bulkDuplicateHandler}
{exportDownloadStatusModal}
<MoneyReportHeaderKYCDropdown
chatReportID={chatReport?.reportID}
iouReport={moneyRequestReport}
Expand All @@ -538,6 +539,7 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn
return (
<>
{bulkDuplicateHandler}
{exportDownloadStatusModal}
<ButtonWithDropdownMenu
onPress={() => null}
options={selectedTransactionsOptions}
Expand Down
35 changes: 15 additions & 20 deletions src/components/MoneyRequestReportView/SelectionToolbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ProcessMoneyReportHoldMenu from '@components/ProcessMoneyReportHoldMenu';
import BulkDuplicateHandler from '@components/Search/BulkDuplicateHandler';
import {useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext';
import useConfirmModal from '@hooks/useConfirmModal';
import useExportDownloadStatusModal from '@hooks/useExportDownloadStatusModal';
import useFilterSelectedTransactions from '@hooks/useFilterSelectedTransactions';
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
Expand Down Expand Up @@ -74,6 +75,7 @@ function SelectionToolbar({reportID, transactions, reportActions}: SelectionTool

const isMobileSelectionModeEnabled = useMobileSelectionMode();
const {showConfirmModal} = useConfirmModal();
const {trackExport, exportDownloadStatusModal} = useExportDownloadStatusModal(() => clearSelectedTransactions(undefined, true));

const [offlineModalVisible, setOfflineModalVisible] = useState(false);
const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false);
Expand All @@ -91,26 +93,18 @@ function SelectionToolbar({reportID, transactions, reportActions}: SelectionTool
return;
}

queueExportSearchWithTemplate({
templateName,
templateType,
jsonQuery: '{}',
reportIDList: [report.reportID],
transactionIDList,
policyID: policy?.id,
});

showConfirmModal({
title: translate('export.exportInProgress'),
prompt: translate('export.conciergeWillSend'),
confirmText: translate('common.buttonConfirm'),
shouldShowCancelButton: false,
}).then((result) => {
if (result.action !== ModalActions.CONFIRM) {
return;
}
clearSelectedTransactions(undefined, true);
});
const exportID = queueExportSearchWithTemplate(
{
templateName,
templateType,
jsonQuery: '{}',
reportIDList: [report.reportID],
transactionIDList,
policyID: policy?.id,
},
true,
);
trackExport(exportID);
};

const onDeleteSelected = (handleDeleteTransactions: () => void, handleDeleteTransactionsWithNavigation: (backToRoute?: Route) => void) => {
Expand Down Expand Up @@ -235,6 +229,7 @@ function SelectionToolbar({reportID, transactions, reportActions}: SelectionTool

return (
<>
{exportDownloadStatusModal}
{isDuplicateOptionVisible && (
<BulkDuplicateHandler
selectedTransactionsKeys={selectedTransactionIDs}
Expand Down
46 changes: 19 additions & 27 deletions src/hooks/useExportActions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type React from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
import {ModalActions} from '@components/Modal/Global/ModalContext';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import {useSearchSelectionActions} from '@components/Search/SearchContext';
import {openOldDotLink} from '@libs/actions/Link';
Expand All @@ -15,10 +15,10 @@ import {getIntegrationIcon, isExported as isExportedUtils} from '@libs/ReportUti
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxTypes from '@src/types/onyx';
import useConfirmModal from './useConfirmModal';
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
import useDecisionModal from './useDecisionModal';
import useExportAgainModal from './useExportAgainModal';
import useExportDownloadStatusModal from './useExportDownloadStatusModal';
import {useMemoizedLazyExpensifyIcons} from './useLazyAsset';
import useLocalize from './useLocalize';
import useNetwork from './useNetwork';
Expand All @@ -39,6 +39,9 @@ type UseExportActionsReturn = {
beginExportWithTemplate: (templateName: string, templateType: string, transactionIDList: string[], policyID?: string) => void;
showOfflineModal: () => void;
showDownloadErrorModal: () => void;

/** The realtime export status modal for the in-progress template export (or null when none is active). Render it directly in the consumer. */
exportDownloadStatusModal: React.JSX.Element | null;
};

function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsParams): UseExportActionsReturn {
Expand All @@ -64,10 +67,10 @@ function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsPa
const exportTemplates = getExportTemplates(integrationsExportTemplates ?? [], csvExportLayouts ?? {}, translate, policy);
const isExported = isExportedUtils(reportActions, moneyRequestReport);

const {showConfirmModal} = useConfirmModal();
const {showDecisionModal} = useDecisionModal();
const {triggerExportOrConfirm} = useExportAgainModal(moneyRequestReport?.reportID, moneyRequestReport?.policyID);
const {clearSelectedTransactions} = useSearchSelectionActions();
const {trackExport, exportDownloadStatusModal} = useExportDownloadStatusModal(() => clearSelectedTransactions(undefined, true));

const expensifyIcons = useMemoizedLazyExpensifyIcons([
'Table',
Expand Down Expand Up @@ -101,15 +104,6 @@ function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsPa
});
};

const showExportProgressModal = () => {
return showConfirmModal({
title: translate('export.exportInProgress'),
prompt: translate('export.conciergeWillSend'),
confirmText: translate('common.buttonConfirm'),
shouldShowCancelButton: false,
});
};

const beginExportWithTemplate = (templateName: string, templateType: string, transactionIDList: string[], policyID?: string) => {
if (isOffline) {
showOfflineModal();
Expand All @@ -120,21 +114,18 @@ function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsPa
return;
}

showExportProgressModal().then((result) => {
if (result.action !== ModalActions.CONFIRM) {
return;
}
clearSelectedTransactions(undefined, true);
});

queueExportSearchWithTemplate({
templateName,
templateType,
jsonQuery: '{}',
reportIDList: [moneyRequestReport.reportID],
transactionIDList,
policyID,
});
const exportID = queueExportSearchWithTemplate(
{
templateName,
templateType,
jsonQuery: '{}',
reportIDList: [moneyRequestReport.reportID],
transactionIDList,
policyID,
},
true,
);
trackExport(exportID);
};

const exportSubmenuOptions: Record<string, DropdownOption<string>> = {
Expand Down Expand Up @@ -267,6 +258,7 @@ function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsPa
beginExportWithTemplate,
showOfflineModal,
showDownloadErrorModal,
exportDownloadStatusModal,
};
}

Expand Down
53 changes: 53 additions & 0 deletions src/hooks/useExportDownloadStatusModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, {useState} from 'react';
import ExportDownloadStatusModal from '@components/ExportDownloadStatusModal';
import {clearExportDownload} from '@libs/actions/Export';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import useLocalize from './useLocalize';
import useOnyx from './useOnyx';

type UseExportDownloadStatusModalReturn = {
/** Start tracking a queued export so the status modal renders for it */
trackExport: (exportID: string) => void;

/** The realtime export status modal for the in-progress export (or null when none is active). Render it directly in the consumer. */
exportDownloadStatusModal: React.JSX.Element | null;
};

/**
* Encapsulates the shared wiring for the queued CSV export status modal (ExportDownloadStatusModal): it tracks the
* active export, renders the modal, and handles close/cleanup (no-op while still preparing, unless handed off to
* Concierge). Used by every surface that triggers a tracked template export so the modal wiring lives in one place.
*
* @param onCleanup - Optional extra cleanup to run once the modal is dismissed (e.g. clearing the selection).
*/
function useExportDownloadStatusModal(onCleanup?: () => void): UseExportDownloadStatusModalReturn {
const {translate} = useLocalize();
const [activeExportID, setActiveExportID] = useState<string | undefined>(undefined);
const [activeExportDownload] = useOnyx(`${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}${activeExportID}`);

const handleExportModalClose = () => {
// Keep the modal open while the export is still preparing (unless it was handed off to Concierge).
if (activeExportDownload?.state === CONST.EXPORT_DOWNLOAD.STATE.PREPARING && !activeExportDownload?.shouldSendFromConcierge) {
return;
}
if (activeExportID) {
clearExportDownload(activeExportID, activeExportDownload);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Avoid sending duplicate clear requests

When the ready/failed modal is closed with its Close button, ExportDownloadStatusModal.handleClose already calls clearExportDownload(exportID, ...) before invoking this onClose prop. This handler then calls clearExportDownload again, so every newly wired report/selection export close sends two ClearExportDownload writes; keep the parent handler limited to hiding the modal/selection cleanup or otherwise make only one layer own the API clear.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved

}
setActiveExportID(undefined);
onCleanup?.();
};

const exportDownloadStatusModal = activeExportID ? (
<ExportDownloadStatusModal
exportID={activeExportID}
isVisible
onClose={handleExportModalClose}
failedBody={translate('exportDownload.csvFailedBody')}
/>
) : null;

return {trackExport: setActiveExportID, exportDownloadStatusModal};
}

export default useExportDownloadStatusModal;
5 changes: 3 additions & 2 deletions tests/unit/ExportDownloadStatusModalTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ describe('ExportDownloadStatusModal', () => {
expect(mockNavigate).toHaveBeenCalledWith(expect.stringContaining(conciergeReportID));
});

it('Close button calls clearExportDownload', async () => {
it('Close button calls onClose and delegates clearing to the parent', async () => {
const onClose = jest.fn();
await Onyx.set(`${ONYXKEYS.COLLECTION.EXPORT_DOWNLOAD}${EXPORT_ID}`, {state: 'ready', fileName: FILE_NAME});

Expand All @@ -176,7 +176,8 @@ describe('ExportDownloadStatusModal', () => {

fireEvent.press(screen.getByText('exportDownload.close'));

expect(mockClearExportDownload).toHaveBeenCalledWith(EXPORT_ID, expect.objectContaining({state: 'ready'}));
expect(onClose).toHaveBeenCalled();
// Clearing the export download is owned by the parent's onClose handler, so the modal must not clear it itself (avoids a duplicate write).
expect(mockClearExportDownload).not.toHaveBeenCalled();
});
});
Loading
Loading