diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index c1c3eee042f7..08214f0d5b1f 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'; @@ -13,6 +12,7 @@ import DropZoneUI from '@components/DropZone/DropZoneUI'; import HoldOrRejectEducationalModal from '@components/HoldOrRejectEducationalModal'; import HoldSubmitterEducationalModal from '@components/HoldSubmitterEducationalModal'; import type {PaymentMethodType} from '@components/KYCWall/types'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; import {useSearchContext} from '@components/Search/SearchContext'; @@ -21,6 +21,7 @@ import type {PaymentData, SearchParams} from '@components/Search/types'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; import useAllTransactions from '@hooks/useAllTransactions'; import useBulkPayOptions from '@hooks/useBulkPayOptions'; +import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useFilesValidation from '@hooks/useFilesValidation'; import useFilterFormValues from '@hooks/useFilterFormValues'; @@ -129,11 +130,8 @@ function SearchPage({route}: SearchPageProps) { const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); - const [isDeleteExpensesConfirmModalVisible, setIsDeleteExpensesConfirmModalVisible] = useState(false); - const [isDownloadExportModalVisible, setIsDownloadExportModalVisible] = useState(false); - const [isExportWithTemplateModalVisible, setIsExportWithTemplateModalVisible] = useState(false); const [searchRequestResponseStatusCode, setSearchRequestResponseStatusCode] = useState(null); - const [isDEWModalVisible, setIsDEWModalVisible] = useState(false); + const {showConfirmModal} = useConfirmModal(); const {isBetaEnabled} = usePermissions(); const isDEWBetaEnabled = isBetaEnabled(CONST.BETAS.NEW_DOT_DEW); const [isHoldEducationalModalVisible, setIsHoldEducationalModalVisible] = useState(false); @@ -242,7 +240,7 @@ function SearchPage({route}: SearchPageProps) { const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {}); const beginExportWithTemplate = useCallback( - (templateName: string, templateType: string, policyID: string | undefined) => { + async (templateName: string, templateType: string, policyID: string | undefined) => { // If the user has selected a large number of items, we'll use the queryJSON to search for the reportIDs and transactionIDs necessary for the export if (areAllMatchingItemsSelected) { queueExportSearchWithTemplate({ @@ -265,9 +263,18 @@ function SearchPage({route}: SearchPageProps) { }); } - setIsExportWithTemplateModalVisible(true); + const result = await showConfirmModal({ + title: translate('export.exportInProgress'), + prompt: translate('export.conciergeWillSend'), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + }); + if (result.action !== ModalActions.CONFIRM) { + return; + } + clearSelectedTransactions(undefined, true); }, - [queryJSON, selectedTransactionsKeys, areAllMatchingItemsSelected, selectedTransactionReportIDs], + [queryJSON, selectedTransactionsKeys, areAllMatchingItemsSelected, selectedTransactionReportIDs, showConfirmModal, translate, clearSelectedTransactions], ); const policyIDsWithVBBA = useMemo(() => { @@ -276,6 +283,165 @@ function SearchPage({route}: SearchPageProps) { .map((policy) => policy.id); }, [policies]); + const handleBasicExport = useCallback(async () => { + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + if (status === null || status === undefined) { + return; + } + + if (areAllMatchingItemsSelected) { + const result = await showConfirmModal({ + title: translate('search.exportSearchResults.title'), + prompt: translate('search.exportSearchResults.description'), + confirmText: translate('search.exportSearchResults.title'), + cancelText: translate('common.cancel'), + }); + if (result.action !== ModalActions.CONFIRM) { + return; + } + if (selectedTransactionsKeys.length === 0 || status == null || !hash) { + return; + } + const reportIDList = selectedReports?.map((report) => report?.reportID).filter((reportID) => reportID !== undefined) ?? []; + queueExportSearchItemsToCSV({ + query: status, + jsonQuery: JSON.stringify(queryJSON), + reportIDList, + transactionIDList: selectedTransactionsKeys, + }); + selectAllMatchingItems(false); + clearSelectedTransactions(); + return; + } + + exportSearchItemsToCSV( + { + query: status, + jsonQuery: JSON.stringify(queryJSON), + reportIDList: selectedReports?.map((report) => report?.reportID).filter((reportID) => reportID !== undefined) ?? [], + transactionIDList: selectedTransactionsKeys, + }, + () => { + setIsDownloadErrorModalVisible(true); + }, + translate, + ); + clearSelectedTransactions(undefined, true); + }, [ + isOffline, + areAllMatchingItemsSelected, + showConfirmModal, + translate, + selectedTransactionsKeys, + status, + hash, + selectedReports, + queryJSON, + selectAllMatchingItems, + clearSelectedTransactions, + setIsDownloadErrorModalVisible, + ]); + + const handleApproveWithDEWCheck = useCallback(async () => { + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + if (!hash) { + return; + } + + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + + // Check if any of the selected items have DEW enabled + const selectedPolicyIDList = selectedReports.length + ? selectedReports.map((report) => report.policyID) + : Object.values(selectedTransactions).map((transaction) => transaction.policyID); + const hasDEWPolicy = selectedPolicyIDList.some((policyID) => { + const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; + return hasDynamicExternalWorkflow(policy); + }); + + if (hasDEWPolicy && !isDEWBetaEnabled) { + const result = await showConfirmModal({ + title: translate('customApprovalWorkflow.title'), + prompt: translate('customApprovalWorkflow.description'), + confirmText: translate('customApprovalWorkflow.goToExpensifyClassic'), + shouldShowCancelButton: false, + }); + if (result.action !== ModalActions.CONFIRM) { + return; + } + openOldDotLink(CONST.OLDDOT_URLS.INBOX); + return; + } + + const reportIDList = !selectedReports.length + ? Object.values(selectedTransactions).map((transaction) => transaction.reportID) + : (selectedReports?.filter((report) => !!report).map((report) => report.reportID) ?? []); + approveMoneyRequestOnSearch( + hash, + reportIDList.filter((reportID) => reportID !== undefined), + ); + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + clearSelectedTransactions(); + }); + }, [ + isOffline, + isDelegateAccessRestricted, + showDelegateNoAccessModal, + selectedReports, + selectedTransactions, + policies, + isDEWBetaEnabled, + showConfirmModal, + translate, + hash, + clearSelectedTransactions, + ]); + + const handleDeleteSelectedTransactions = useCallback(async () => { + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + if (!hash) { + return; + } + + // Use InteractionManager to ensure this runs after the dropdown modal closes + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(async () => { + const result = await showConfirmModal({ + title: translate('iou.deleteExpense', {count: selectedTransactionsKeys.length}), + prompt: translate('iou.deleteConfirmation', {count: selectedTransactionsKeys.length}), + confirmText: translate('common.delete'), + cancelText: translate('common.cancel'), + danger: true, + }); + if (result.action !== ModalActions.CONFIRM) { + return; + } + // Translations copy for delete modal depends on amount of selected items, + // We need to wait for modal to fully disappear before clearing them to avoid translation flicker between singular vs plural + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + deleteMoneyRequestOnSearch(hash, selectedTransactionsKeys); + clearSelectedTransactions(); + }); + }); + }, [isOffline, showConfirmModal, translate, selectedTransactionsKeys, hash, clearSelectedTransactions]); + const onBulkPaySelected = useCallback( (paymentMethod?: PaymentMethodType, additionalData?: Record) => { if (!hash) { @@ -441,6 +607,7 @@ function SearchPage({route}: SearchPageProps) { const options: Array> = []; const isAnyTransactionOnHold = Object.values(selectedTransactions).some((transaction) => transaction.isHeld); + // 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 @@ -449,29 +616,7 @@ function SearchPage({route}: SearchPageProps) { text: translate('export.basicExport'), icon: expensifyIcons.Table, onSelected: () => { - if (isOffline) { - setIsOfflineModalVisible(true); - return; - } - - if (areAllMatchingItemsSelected) { - setIsDownloadExportModalVisible(true); - return; - } - - exportSearchItemsToCSV( - { - query: status, - jsonQuery: JSON.stringify(queryJSON), - reportIDList: selectedReports?.map((report) => report?.reportID).filter((reportID) => reportID !== undefined) ?? [], - transactionIDList: selectedTransactionsKeys, - }, - () => { - setIsDownloadErrorModalVisible(true); - }, - translate, - ); - clearSelectedTransactions(undefined, true); + handleBasicExport(); }, shouldCloseModalOnSelect: true, shouldCallAfterModalHide: true, @@ -542,41 +687,7 @@ function SearchPage({route}: SearchPageProps) { value: CONST.SEARCH.BULK_ACTION_TYPES.APPROVE, shouldCloseModalOnSelect: true, onSelected: () => { - if (isOffline) { - setIsOfflineModalVisible(true); - return; - } - - if (isDelegateAccessRestricted) { - showDelegateNoAccessModal(); - return; - } - - // Check if any of the selected items have DEW enabled - const selectedPolicyIDList = selectedReports.length - ? selectedReports.map((report) => report.policyID) - : Object.values(selectedTransactions).map((transaction) => transaction.policyID); - const hasDEWPolicy = selectedPolicyIDList.some((policyID) => { - const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; - return hasDynamicExternalWorkflow(policy); - }); - - if (hasDEWPolicy && !isDEWBetaEnabled) { - setIsDEWModalVisible(true); - return; - } - - const reportIDList = !selectedReports.length - ? Object.values(selectedTransactions).map((transaction) => transaction.reportID) - : (selectedReports?.filter((report) => !!report).map((report) => report.reportID) ?? []); - approveMoneyRequestOnSearch( - hash, - reportIDList.filter((reportID) => reportID !== undefined), - ); - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - clearSelectedTransactions(); - }); + handleApproveWithDEWCheck(); }, }); } @@ -815,16 +926,7 @@ function SearchPage({route}: SearchPageProps) { value: CONST.SEARCH.BULK_ACTION_TYPES.DELETE, shouldCloseModalOnSelect: true, onSelected: () => { - if (isOffline) { - setIsOfflineModalVisible(true); - return; - } - - // Use InteractionManager to ensure this runs after the dropdown modal closes - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - setIsDeleteExpensesConfirmModalVisible(true); - }); + handleDeleteSelectedTransactions(); }, }); } @@ -863,7 +965,7 @@ function SearchPage({route}: SearchPageProps) { lastPaymentMethods, selectedReportIDs, allTransactions, - queryJSON, + queryJSON?.type, selectedPolicyIDs, policies, integrationsExportTemplates, @@ -872,10 +974,12 @@ function SearchPage({route}: SearchPageProps) { beginExportWithTemplate, bulkPayButtonOptions, onBulkPaySelected, + handleBasicExport, + handleApproveWithDEWCheck, + handleDeleteSelectedTransactions, allReports, theme.icon, styles.colorMuted, - isDEWBetaEnabled, styles.fontWeightNormal, styles.textWrap, expensifyIcons.ArrowCollapse, @@ -898,27 +1002,10 @@ function SearchPage({route}: SearchPageProps) { currentSearchResults?.data, isDelegateAccessRestricted, showDelegateNoAccessModal, - currentUserPersonalDetails?.accountID, + currentUserPersonalDetails.accountID, personalPolicyID, ]); - const handleDeleteExpenses = () => { - if (selectedTransactionsKeys.length === 0 || !hash) { - return; - } - - setIsDeleteExpensesConfirmModalVisible(false); - - // Translations copy for delete modal depends on amount of selected items, - // We need to wait for modal to fully disappear before clearing them to avoid translation flicker between singular vs plural - - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - deleteMoneyRequestOnSearch(hash, selectedTransactionsKeys); - clearSelectedTransactions(); - }); - }; - const saveFileAndInitMoneyRequest = (files: FileObject[]) => { const initialTransaction = initMoneyRequest({ isFromGlobalCreate: true, @@ -992,24 +1079,6 @@ function SearchPage({route}: SearchPageProps) { validateFiles(files, Array.from(e.dataTransfer?.items ?? [])); }; - const createExportAll = useCallback(() => { - if (selectedTransactionsKeys.length === 0 || status == null || !hash) { - return []; - } - - setIsDownloadExportModalVisible(false); - const reportIDList = selectedReports?.map((report) => report?.reportID).filter((reportID) => reportID !== undefined) ?? []; - queueExportSearchItemsToCSV({ - query: status, - jsonQuery: JSON.stringify(queryJSON), - reportIDList, - transactionIDList: selectedTransactionsKeys, - }); - selectAllMatchingItems(false); - clearSelectedTransactions(); - }, [selectedTransactionsKeys, status, hash, selectedReports, queryJSON, selectAllMatchingItems, clearSelectedTransactions]); - - const isPossibleToShowDownloadExportModal = !shouldUseNarrowLayout && isDownloadExportModalVisible && !!createExportAll && !!setIsDownloadExportModalVisible; const {resetVideoPlayerData} = usePlaybackContext(); const metadata = searchResults?.search; @@ -1079,10 +1148,6 @@ function SearchPage({route}: SearchPageProps) { [saveScrollOffset, route], ); - const handleDeleteExpensesCancel = useCallback(() => { - setIsDeleteExpensesConfirmModalVisible(false); - }, [setIsDeleteExpensesConfirmModalVisible]); - const handleOfflineModalClose = useCallback(() => { setIsOfflineModalVisible(false); }, [setIsOfflineModalVisible]); @@ -1091,28 +1156,6 @@ function SearchPage({route}: SearchPageProps) { setIsDownloadErrorModalVisible(false); }, [setIsDownloadErrorModalVisible]); - const handleExportWithTemplateConfirm = useCallback(() => { - setIsExportWithTemplateModalVisible(false); - clearSelectedTransactions(undefined, true); - }, [setIsExportWithTemplateModalVisible, clearSelectedTransactions]); - - const handleExportWithTemplateCancel = useCallback(() => { - setIsExportWithTemplateModalVisible(false); - }, [setIsExportWithTemplateModalVisible]); - - const handleDEWModalConfirm = useCallback(() => { - setIsDEWModalVisible(false); - openOldDotLink(CONST.OLDDOT_URLS.INBOX); - }, [setIsDEWModalVisible]); - - const handleDEWModalCancel = useCallback(() => { - setIsDEWModalVisible(false); - }, [setIsDEWModalVisible]); - - const handleDownloadExportModalCancel = useCallback(() => { - setIsDownloadExportModalVisible?.(false); - }, [setIsDownloadExportModalVisible]); - const dismissModalAndUpdateUseHold = useCallback(() => { setIsHoldEducationalModalVisible(false); setNameValuePair(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, true, false, !isOffline); @@ -1189,16 +1232,6 @@ function SearchPage({route}: SearchPageProps) { {(!shouldUseNarrowLayout || isMobileSelectionModeEnabled) && ( - - - - {isPossibleToShowDownloadExportModal && ( - - )} {!!rejectModalAction && ( (''); @@ -440,7 +440,12 @@ function SplitExpensePage({route}: SplitExpensePageProps) { const onSelectRow = useCallback( (item: SplitListItemType) => { if (!item.isEditable) { - setCannotBeEditedModalVisible(true); + showConfirmModal({ + title: translate('iou.splitExpenseCannotBeEditedModalTitle'), + prompt: translate('iou.splitExpenseCannotBeEditedModalDescription'), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + }); return; } Keyboard.dismiss(); @@ -449,7 +454,7 @@ function SplitExpensePage({route}: SplitExpensePageProps) { initDraftSplitExpenseDataForEdit(draftTransaction, item.transactionID, item.reportID ?? reportID); }); }, - [draftTransaction, reportID], + [draftTransaction, reportID, showConfirmModal, translate], ); return ( @@ -541,15 +546,6 @@ function SplitExpensePage({route}: SplitExpensePageProps) { )} - setCannotBeEditedModalVisible(false)} - onCancel={() => setCannotBeEditedModalVisible(false)} - confirmText={translate('common.buttonConfirm')} - isVisible={cannotBeEditedModalVisible} - shouldShowCancelButton={false} - /> );