diff --git a/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts b/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts index 9cb6a7aed8ec..2f758f4f701b 100644 --- a/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts +++ b/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts @@ -24,8 +24,8 @@ import {getPreservedNavigatorState} from './createSplitNavigator/usePreserveNavi // This timing is used to call the preload function after a tab change, when the initial tab screen has already been rendered. const TIMING_TO_CALL_PRELOAD = 1000; -// Currently, only the Account and Workspaces tabs are preloaded. The remaining tabs will be supported soon. -const TABS_TO_PRELOAD = [NAVIGATION_TABS.SETTINGS, NAVIGATION_TABS.WORKSPACES]; +// Currently, only the Workspaces, Account tabs are preloaded. The remaining tabs will be supported soon. +const TABS_TO_PRELOAD = [NAVIGATION_TABS.WORKSPACES, NAVIGATION_TABS.SETTINGS]; function preloadWorkspacesTab(navigation: PlatformStackNavigationProp) { const state = getWorkspacesTabStateFromSessionStorage() ?? navigation.getState(); diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 4df55d8b9efe..0b88694f54e0 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1,7 +1,8 @@ import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; import {InteractionManager, View} from 'react-native'; +import Animated from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import ConfirmModal from '@components/ConfirmModal'; import DecisionModal from '@components/DecisionModal'; @@ -10,14 +11,9 @@ import DragAndDropProvider from '@components/DragAndDrop/Provider'; import DropZoneUI from '@components/DropZone/DropZoneUI'; import type {PaymentMethodType} from '@components/KYCWall/types'; import type {PopoverMenuItem} from '@components/PopoverMenu'; -import ScreenWrapper from '@components/ScreenWrapper'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; -import Search from '@components/Search'; import {useSearchContext} from '@components/Search/SearchContext'; -import SearchPageFooter from '@components/Search/SearchPageFooter'; -import SearchFiltersBar from '@components/Search/SearchPageHeader/SearchFiltersBar'; import type {SearchHeaderOptionValue} from '@components/Search/SearchPageHeader/SearchPageHeader'; -import SearchPageHeader from '@components/Search/SearchPageHeader/SearchPageHeader'; import type {PaymentData, SearchParams} from '@components/Search/types'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; import useBulkPayOptions from '@hooks/useBulkPayOptions'; @@ -69,7 +65,7 @@ import { isInvoiceReport, isIOUReport as isIOUReportUtil, } from '@libs/ReportUtils'; -import {buildCannedSearchQuery, buildSearchQueryJSON} from '@libs/SearchQueryUtils'; +import {buildSearchQueryJSON} from '@libs/SearchQueryUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types'; import variables from '@styles/variables'; @@ -83,11 +79,13 @@ import type SCREENS from '@src/SCREENS'; import type {SearchResults, Transaction} from '@src/types/onyx'; import type {FileObject} from '@src/types/utils/Attachment'; import SearchPageNarrow from './SearchPageNarrow'; +import SearchPageWide from './SearchPageWide'; type SearchPageProps = PlatformStackScreenProps; function SearchPage({route}: SearchPageProps) { const {translate, localeCompare, formatPhoneNumber} = useLocalize(); + // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type for the decision modal // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); @@ -186,7 +184,14 @@ function SearchPage({route}: SearchPageProps) { (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({templateName, templateType, jsonQuery: JSON.stringify(queryJSON), reportIDList: [], transactionIDList: [], policyID}); + queueExportSearchWithTemplate({ + templateName, + templateType, + jsonQuery: JSON.stringify(queryJSON), + reportIDList: [], + transactionIDList: [], + policyID, + }); } else { // Otherwise, we will use the selected transactionIDs and reportIDs directly queueExportSearchWithTemplate({ @@ -614,22 +619,22 @@ function SearchPage({route}: SearchPageProps) { areAllMatchingItemsSelected, isOffline, selectedReports, + lastPaymentMethods, + selectedReportIDs, + selectedTransactionReportIDs, queryJSON, + selectedPolicyIDs, + policies, + integrationsExportTemplates, + csvExportLayouts, clearSelectedTransactions, - lastPaymentMethods, + beginExportWithTemplate, + bulkPayButtonOptions, + onBulkPaySelected, theme.icon, styles.colorMuted, styles.fontWeightNormal, styles.textWrap, - beginExportWithTemplate, - integrationsExportTemplates, - csvExportLayouts, - policies, - bulkPayButtonOptions, - onBulkPaySelected, - selectedPolicyIDs, - selectedReportIDs, - selectedTransactionReportIDs, expensifyIcons, ]); @@ -642,6 +647,7 @@ function SearchPage({route}: SearchPageProps) { // 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, allSnapshots, transactions, currentSearchResults); @@ -737,7 +743,7 @@ function SearchPage({route}: SearchPageProps) { clearSelectedTransactions(); }, [selectedTransactionsKeys, status, hash, selectedReports, queryJSON, selectAllMatchingItems, clearSelectedTransactions]); - const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery()})); + const isPossibleToShowDownloadExportModal = !shouldUseNarrowLayout && isDownloadExportModalVisible && !!createExportAll && !!setIsDownloadExportModalVisible; const {resetVideoPlayerData} = usePlaybackContext(); const [isSorting, setIsSorting] = useState(false); @@ -750,17 +756,8 @@ function SearchPage({route}: SearchPageProps) { } const metadata = searchResults?.search; - const shouldShowOfflineIndicator = !!searchResults?.data; const shouldShowFooter = !!metadata?.count || selectedTransactionsKeys.length > 0; - const offlineIndicatorStyle = useMemo(() => { - if (shouldShowFooter) { - return [styles.mtAuto, styles.pAbsolute, styles.h10, styles.b0]; - } - - return [styles.mtAuto]; - }, [shouldShowFooter, styles]); - // Handles video player cleanup: // 1. On mount: Resets player if navigating from report screen // 2. On unmount: Stops video when leaving this screen @@ -794,7 +791,9 @@ function SearchPage({route}: SearchPageProps) { if (typeof value === 'string') { searchInServer(value); } else { - search(value).then((jsonCode) => setSearchRequestResponseStatusCode(Number(jsonCode ?? 0))); + search(value).then((jsonCode) => { + setSearchRequestResponseStatusCode(Number(jsonCode ?? 0)); + }); } }, []); @@ -808,243 +807,170 @@ function SearchPage({route}: SearchPageProps) { return {count, total, currency}; }, [areAllMatchingItemsSelected, metadata?.count, metadata?.currency, metadata?.total, selectedTransactions, selectedTransactionsKeys.length]); - if (shouldUseNarrowLayout) { - return ( - <> - - {PDFValidationComponent} - { + setIsSorting(true); + }, []); + + const scrollHandler = useCallback( + (e: NativeSyntheticEvent) => { + if (!e.nativeEvent.contentOffset.y) { + return; + } + + saveScrollOffset(route, e.nativeEvent.contentOffset.y); + }, + [saveScrollOffset, route], + ); + + const handleDeleteExpensesCancel = useCallback(() => { + setIsDeleteExpensesConfirmModalVisible(false); + }, [setIsDeleteExpensesConfirmModalVisible]); + + const handleOfflineModalClose = useCallback(() => { + setIsOfflineModalVisible(false); + }, [setIsOfflineModalVisible]); + + const handleDownloadErrorModalClose = useCallback(() => { + 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]); + + return ( + <> + + {shouldUseNarrowLayout ? ( + + {PDFValidationComponent} + + + + + {ErrorModal} + + ) : ( + - - - - {ErrorModal} - - {!!isMobileSelectionModeEnabled && ( - + )} + + {(!shouldUseNarrowLayout || isMobileSelectionModeEnabled) && ( + + + + + + + {isPossibleToShowDownloadExportModal && ( { - setIsDeleteExpensesConfirmModalVisible(false); - }} - title={translate('iou.deleteExpense', {count: selectedTransactionsKeys.length})} - prompt={translate('iou.deleteConfirmation', {count: selectedTransactionsKeys.length})} - confirmText={translate('common.delete')} + isVisible={isDownloadExportModalVisible} + onConfirm={createExportAll} + onCancel={handleDownloadExportModalCancel} + title={translate('search.exportSearchResults.title')} + prompt={translate('search.exportSearchResults.description')} + confirmText={translate('search.exportSearchResults.title')} cancelText={translate('common.cancel')} - danger - /> - setIsOfflineModalVisible(false)} - secondOptionText={translate('common.buttonConfirm')} - isVisible={isOfflineModalVisible} - onClose={() => setIsOfflineModalVisible(false)} - /> - setIsDownloadErrorModalVisible(false)} - secondOptionText={translate('common.buttonConfirm')} - isVisible={isDownloadErrorModalVisible} - onClose={() => setIsDownloadErrorModalVisible(false)} - /> - { - setIsExportWithTemplateModalVisible(false); - clearSelectedTransactions(undefined, true); - }} - onCancel={() => setIsExportWithTemplateModalVisible(false)} - title={translate('export.exportInProgress')} - prompt={translate('export.conciergeWillSend')} - confirmText={translate('common.buttonConfirm')} - shouldShowCancelButton={false} /> - { - setIsDEWModalVisible(false); - openOldDotLink(CONST.OLDDOT_URLS.INBOX); - }} - onCancel={() => setIsDEWModalVisible(false)} - prompt={translate('customApprovalWorkflow.description')} - confirmText={translate('customApprovalWorkflow.goToExpensifyClassic')} - shouldShowCancelButton={false} - /> - - )} - - ); - } - - return ( - - - {!!queryJSON && ( - - - - {PDFValidationComponent} - - - { - if (!e.nativeEvent.contentOffset.y) { - return; - } - - saveScrollOffset(route, e.nativeEvent.contentOffset.y); - }} - onSortPressedCallback={() => { - setIsSorting(true); - }} - searchRequestResponseStatusCode={searchRequestResponseStatusCode} - /> - {shouldShowFooter && ( - - )} - - - - - - {ErrorModal} - - )} - { - setIsDeleteExpensesConfirmModalVisible(false); - }} - title={translate('iou.deleteExpense', {count: selectedTransactionsKeys.length})} - prompt={translate('iou.deleteConfirmation', {count: selectedTransactionsKeys.length})} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - /> - { - setIsDownloadExportModalVisible(false); - }} - title={translate('search.exportSearchResults.title')} - prompt={translate('search.exportSearchResults.description')} - confirmText={translate('search.exportSearchResults.title')} - cancelText={translate('common.cancel')} - /> - { - setIsExportWithTemplateModalVisible(false); - clearSelectedTransactions(undefined, true); - }} - onCancel={() => setIsExportWithTemplateModalVisible(false)} - title={translate('export.exportInProgress')} - prompt={translate('export.conciergeWillSend')} - confirmText={translate('common.buttonConfirm')} - shouldShowCancelButton={false} - /> - setIsOfflineModalVisible(false)} - secondOptionText={translate('common.buttonConfirm')} - isVisible={isOfflineModalVisible} - onClose={() => setIsOfflineModalVisible(false)} - /> - setIsDownloadErrorModalVisible(false)} - secondOptionText={translate('common.buttonConfirm')} - isVisible={isDownloadErrorModalVisible} - onClose={() => setIsDownloadErrorModalVisible(false)} - /> - { - setIsDEWModalVisible(false); - openOldDotLink(CONST.OLDDOT_URLS.INBOX); - }} - onCancel={() => setIsDEWModalVisible(false)} - prompt={translate('customApprovalWorkflow.description')} - confirmText={translate('customApprovalWorkflow.goToExpensifyClassic')} - shouldShowCancelButton={false} - /> - - + )} + + )} + ); } diff --git a/src/pages/Search/SearchPageWide.tsx b/src/pages/Search/SearchPageWide.tsx new file mode 100644 index 000000000000..6b1531ec90dc --- /dev/null +++ b/src/pages/Search/SearchPageWide.tsx @@ -0,0 +1,156 @@ +import React, {useMemo} from 'react'; +import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import DragAndDropConsumer from '@components/DragAndDrop/Consumer'; +import DropZoneUI from '@components/DropZone/DropZoneUI'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Search from '@components/Search'; +import SearchPageFooter from '@components/Search/SearchPageFooter'; +import SearchFiltersBar from '@components/Search/SearchPageHeader/SearchFiltersBar'; +import SearchPageHeader from '@components/Search/SearchPageHeader/SearchPageHeader'; +import type {SearchHeaderOptionValue} from '@components/Search/SearchPageHeader/SearchPageHeader'; +import type {BankAccountMenuItem, SearchParams, SearchQueryJSON} from '@components/Search/types'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; +import Navigation from '@navigation/Navigation'; +import ROUTES from '@src/ROUTES'; +import type {SearchResults} from '@src/types/onyx'; +import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; + +type SearchPageWideProps = { + queryJSON?: SearchQueryJSON; + searchResults: OnyxEntry; + searchRequestResponseStatusCode: number | null; + isMobileSelectionModeEnabled: boolean; + headerButtonsOptions: Array>; + footerData: { + count: number | undefined; + total: number | undefined; + currency: string | undefined; + }; + selectedPolicyIDs: Array; + selectedTransactionReportIDs: string[]; + selectedReportIDs: string[]; + latestBankItems?: BankAccountMenuItem[]; + onBulkPaySelected: (paymentMethod?: PaymentMethodType) => void; + handleSearchAction: (value: SearchParams | string) => void; + onSortPressedCallback: () => void; + scrollHandler: (event: NativeSyntheticEvent) => void; + initScanRequest: (e: DragEvent) => void; + PDFValidationComponent: React.ReactNode; + ErrorModal: React.ReactNode; + shouldShowFooter: boolean; +}; + +function SearchPageWide({ + queryJSON, + searchResults, + searchRequestResponseStatusCode, + isMobileSelectionModeEnabled, + headerButtonsOptions, + footerData, + selectedPolicyIDs, + selectedTransactionReportIDs, + selectedReportIDs, + latestBankItems, + onBulkPaySelected, + handleSearchAction, + onSortPressedCallback, + scrollHandler, + initScanRequest, + PDFValidationComponent, + ErrorModal, + shouldShowFooter, +}: SearchPageWideProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + const {translate} = useLocalize(); + + const offlineIndicatorStyle = useMemo(() => { + if (shouldShowFooter) { + return [styles.mtAuto, styles.pAbsolute, styles.h10, styles.b0]; + } + + return [styles.mtAuto]; + }, [shouldShowFooter, styles]); + + const expensifyIcons = useMemoizedLazyExpensifyIcons(['SmartScan'] as const); + const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery()})); + + return ( + + + + {!!queryJSON && ( + <> + {PDFValidationComponent} + + + + {shouldShowFooter && ( + + )} + + + + + )} + + + {ErrorModal} + + ); +} + +SearchPageWide.displayName = 'SearchPageWide'; + +export default SearchPageWide;