diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index cb051e4ed727..2f75a0c77e4d 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -6,7 +6,7 @@ import type {Parameters} from 'storybook/internal/types'; import EnvironmentProvider from '@components/EnvironmentContextProvider'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; import ScreenWrapperStatusContext from '@components/ScreenWrapper/ScreenWrapperStatusContext'; -import {SearchContextProvider} from '@components/Search/SearchContext'; +import {SearchContextProvider} from '@components/Search/SearchContextProvider'; import colors from '@styles/theme/colors'; import ComposeProviders from '@src/components/ComposeProviders'; import HTMLEngineProvider from '@src/components/HTMLEngineProvider'; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 8074d99f1c17..2513cdb928e2 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -24,7 +24,7 @@ import MoneyReportHeaderActions from './MoneyReportHeaderActions'; import MoneyReportHeaderModals from './MoneyReportHeaderModals'; import MoneyReportHeaderMoreContent from './MoneyReportHeaderMoreContent'; import {PaymentAnimationsProvider} from './PaymentAnimationsContext'; -import {useSearchActionsContext} from './Search/SearchContext'; +import {useSearchSelectionActions} from './Search/SearchContext'; type MoneyReportHeaderProps = { /** The reportID of the report currently being looked at */ @@ -52,7 +52,7 @@ function MoneyReportHeader({reportID, shouldDisplayBackButton = false, onBackBut } function MoneyReportHeaderContent({reportID: reportIDProp, shouldDisplayBackButton = false, onBackButtonPress}: MoneyReportHeaderProps) { - const {clearSelectedTransactions} = useSearchActionsContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDProp}`); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(moneyRequestReport?.policyID)}`); diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx index 5d5ce1c301db..1bc747dc34d4 100644 --- a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx +++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx @@ -14,7 +14,7 @@ import {useMoneyReportHeaderModals} from '@components/MoneyReportHeaderModalsCon import NavigationDeferredMount from '@components/NavigationDeferredMount'; import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext'; import type {PopoverMenuItem} from '@components/PopoverMenu'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchResultsContext} from '@components/Search/SearchContext'; import type {PaymentActionParams} from '@components/SettlementButton/types'; import useActiveAdminPolicies from '@hooks/useActiveAdminPolicies'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; @@ -132,7 +132,8 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo const {isDelegateAccessRestricted} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); - const {currentSearchQueryJSON, currentSearchKey, currentSearchResults} = useSearchStateContext(); + const {currentSearchQueryJSON, currentSearchKey} = useSearchQueryContext(); + const {currentSearchResults} = useSearchResultsContext(); const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.hash, true); const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport); diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx index 1877649c6cc0..a97c7ec65438 100644 --- a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx +++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx @@ -16,7 +16,7 @@ import {useMoneyReportHeaderModals} from '@components/MoneyReportHeaderModalsCon import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import BulkDuplicateHandler from '@components/Search/BulkDuplicateHandler'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchResultsContext, useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import type {PaymentActionParams} from '@components/SettlementButton/types'; import useActiveAdminPolicies from '@hooks/useActiveAdminPolicies'; import useConfirmModal from '@hooks/useConfirmModal'; @@ -88,8 +88,10 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn const lastWorkspaceNumber = useLastWorkspaceNumber(); const {convertToDisplayString} = useCurrencyListActions(); - const {selectedTransactionIDs, currentSearchQueryJSON, currentSearchKey, currentSearchResults} = useSearchStateContext(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {selectedTransactionIDs} = useSearchSelectionContext(); + const {currentSearchQueryJSON, currentSearchKey} = useSearchQueryContext(); + const {currentSearchResults} = useSearchResultsContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.hash, true); const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportID)}`); diff --git a/src/components/MoneyReportHeaderActions/index.tsx b/src/components/MoneyReportHeaderActions/index.tsx index 61f296cc977a..e74a6b646a3b 100644 --- a/src/components/MoneyReportHeaderActions/index.tsx +++ b/src/components/MoneyReportHeaderActions/index.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import type {ButtonWithDropdownMenuRef} from '@components/ButtonWithDropdownMenu/types'; import MoneyReportHeaderPrimaryAction from '@components/MoneyReportHeaderPrimaryAction'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import useExportAgainModal from '@hooks/useExportAgainModal'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -44,8 +44,8 @@ function MoneyReportHeaderActions({reportID, primaryAction, isReportInSearch, ba const {triggerExportOrConfirm} = useExportAgainModal(moneyRequestReport?.reportID, moneyRequestReport?.policyID); - const {selectedTransactionIDs} = useSearchStateContext(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {selectedTransactionIDs} = useSearchSelectionContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const hasSelectedTransactions = !!selectedTransactionIDs.length; const isTransactionThread = !!transactionThreadReportID; diff --git a/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx b/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx index e4bbcf07daba..b36bd7fac5f6 100644 --- a/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx +++ b/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx @@ -3,7 +3,7 @@ import React from 'react'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import {useMoneyReportHeaderModals} from '@components/MoneyReportHeaderModalsContext'; import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchResultsContext} from '@components/Search/SearchContext'; import AnimatedSettlementButton from '@components/SettlementButton/AnimatedSettlementButton'; import type {PaymentActionParams} from '@components/SettlementButton/types'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; @@ -107,7 +107,8 @@ function PayPrimaryAction({reportID, chatReportID}: PayPrimaryActionProps) { const totalAmount = getTotalAmountForIOUReportPreviewButton(moneyRequestReport, policy, CONST.REPORT.PRIMARY_ACTIONS.PAY, nonPendingDeleteTransactions, convertToDisplayString); const isAnyTransactionOnHold = hasHeldExpensesReportUtils(transactions); - const {currentSearchQueryJSON, currentSearchKey, currentSearchResults} = useSearchStateContext(); + const {currentSearchQueryJSON, currentSearchKey} = useSearchQueryContext(); + const {currentSearchResults} = useSearchResultsContext(); const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.hash, true); const {openHoldMenu} = useMoneyReportHeaderModals(); diff --git a/src/components/MoneyReportHeaderPrimaryAction/SubmitPrimaryAction.tsx b/src/components/MoneyReportHeaderPrimaryAction/SubmitPrimaryAction.tsx index 4c8e37f0714b..f788022b2b1e 100644 --- a/src/components/MoneyReportHeaderPrimaryAction/SubmitPrimaryAction.tsx +++ b/src/components/MoneyReportHeaderPrimaryAction/SubmitPrimaryAction.tsx @@ -2,7 +2,7 @@ import {delegateEmailSelector} from '@selectors/Account'; import React from 'react'; import AnimatedSubmitButton from '@components/AnimatedSubmitButton'; import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchResultsContext} from '@components/Search/SearchContext'; import useConfirmModal from '@hooks/useConfirmModal'; import useConfirmPendingRTERAndProceed from '@hooks/useConfirmPendingRTERAndProceed'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -71,7 +71,8 @@ function SubmitPrimaryAction({reportID}: SubmitPrimaryActionProps) { ); const shouldBlockSubmit = isBlockSubmitDueToStrictPolicyRules || isBlockSubmitDueToPreventSelfApproval; - const {currentSearchQueryJSON, currentSearchKey, currentSearchResults} = useSearchStateContext(); + const {currentSearchQueryJSON, currentSearchKey} = useSearchQueryContext(); + const {currentSearchResults} = useSearchResultsContext(); const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.hash, true); const handleSubmit = () => { diff --git a/src/components/MoneyRequestHeaderSecondaryActions.tsx b/src/components/MoneyRequestHeaderSecondaryActions.tsx index 27d9ae23d1d8..f044658be043 100644 --- a/src/components/MoneyRequestHeaderSecondaryActions.tsx +++ b/src/components/MoneyRequestHeaderSecondaryActions.tsx @@ -74,7 +74,7 @@ import HoldOrRejectEducationalModal from './HoldOrRejectEducationalModal'; import HoldSubmitterEducationalModal from './HoldSubmitterEducationalModal'; import {ModalActions} from './Modal/Global/ModalContext'; import {usePersonalDetails} from './OnyxListItemProvider'; -import {useSearchActionsContext, useSearchStateContext} from './Search/SearchContext'; +import {useSearchQueryContext, useSearchSelectionActions} from './Search/SearchContext'; import {useWideRHPState} from './WideRHPContextProvider'; type MoneyRequestHeaderSecondaryActionsProps = { @@ -162,8 +162,8 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money const {showConfirmModal} = useConfirmModal(); const {isDelegateAccessRestricted} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); - const {currentSearchHash} = useSearchStateContext(); - const {removeTransaction} = useSearchActionsContext(); + const {currentSearchHash} = useSearchQueryContext(); + const {removeTransaction} = useSearchSelectionActions(); const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transaction?.transactionID ? [transaction.transactionID] : []); const isReportInSearch = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT || route.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT; const {getCurrencyDecimals} = useCurrencyListActions(); diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx index f3739c106841..92c1950b6782 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import PrevNextButtons from '@components/PrevNextButtons'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchResultsContext} from '@components/Search/SearchContext'; import Text from '@components/Text'; import useFilterPendingDeleteReports from '@hooks/useFilterPendingDeleteReports'; import useOnyx from '@hooks/useOnyx'; @@ -222,7 +222,7 @@ function MoneyRequestReportNavigation({reportID, shouldDisplayNarrowVersion}: Mo const [snapshotGuard = EMPTY_GUARD] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, {selector: snapshotGuardSelector}); // Fast-path hooks (always called to satisfy rules of hooks) - const {sortedReportIDs} = useSearchStateContext(); + const {sortedReportIDs} = useSearchResultsContext(); const [lastSearchQuery] = useOnyx(ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY); const searchLoadingSelector = (data: OnyxEntry) => !!data?.search?.isLoading; const [isSearchLoading = false] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${lastSearchQuery?.queryJSON?.hash}`, { diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 42b6cc141d14..f80690f2a592 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -14,7 +14,7 @@ import Modal from '@components/Modal'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScrollView from '@components/ScrollView'; import DropdownButton from '@components/Search/FilterDropdowns/DropdownButton'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import type {SearchCustomColumnIds, SortOrder} from '@components/Search/types'; import SelectionList from '@components/SelectionList'; import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; @@ -206,8 +206,8 @@ function MoneyRequestReportTransactionList({ return hasPendingDeletionTransaction || transactions.some(getTransactionPendingAction); }, [hasPendingDeletionTransaction, transactions]); - const {selectedTransactionIDs} = useSearchStateContext(); - const {setSelectedTransactions, clearSelectedTransactions} = useSearchActionsContext(); + const {selectedTransactionIDs} = useSearchSelectionContext(); + const {setSelectedTransactions, clearSelectedTransactions} = useSearchSelectionActions(); useHandleSelectionMode(selectedTransactionIDs); const isMobileSelectionModeEnabled = useMobileSelectionMode(); diff --git a/src/components/MoneyRequestReportView/SelectionToolbar.tsx b/src/components/MoneyRequestReportView/SelectionToolbar.tsx index 1ad7fab2808a..7ac91276ceeb 100644 --- a/src/components/MoneyRequestReportView/SelectionToolbar.tsx +++ b/src/components/MoneyRequestReportView/SelectionToolbar.tsx @@ -13,7 +13,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {PressableWithFeedback} from '@components/Pressable'; import ProcessMoneyReportHoldMenu from '@components/ProcessMoneyReportHoldMenu'; import BulkDuplicateHandler from '@components/Search/BulkDuplicateHandler'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import Text from '@components/Text'; import useConfirmModal from '@hooks/useConfirmModal'; import useFilterSelectedTransactions from '@hooks/useFilterSelectedTransactions'; @@ -70,8 +70,8 @@ function SelectionToolbar({reportID, transactions, reportActions}: SelectionTool const {isDelegateAccessRestricted} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); - const {selectedTransactionIDs} = useSearchStateContext(); - const {setSelectedTransactions, clearSelectedTransactions} = useSearchActionsContext(); + const {selectedTransactionIDs} = useSearchSelectionContext(); + const {setSelectedTransactions, clearSelectedTransactions} = useSearchSelectionActions(); useFilterSelectedTransactions(transactions, reportID); @@ -355,8 +355,8 @@ function SelectionToolbar({reportID, transactions, reportActions}: SelectionTool } function SelectionToolbarGate({reportID, transactions, reportActions}: SelectionToolbarProps) { - const {selectedTransactionIDs, currentSelectedTransactionReportID} = useSearchStateContext(); - const {clearSelectedTransactions, setCurrentSelectedTransactionReportID} = useSearchActionsContext(); + const {selectedTransactionIDs, currentSelectedTransactionReportID} = useSearchSelectionContext(); + const {clearSelectedTransactions, setCurrentSelectedTransactionReportID} = useSearchSelectionActions(); const isMobileSelectionModeEnabled = useMobileSelectionMode(); useFocusEffect(() => { diff --git a/src/components/Navigation/SearchSidebar.tsx b/src/components/Navigation/SearchSidebar.tsx index afeceaa23957..d66d3aa1e8b4 100644 --- a/src/components/Navigation/SearchSidebar.tsx +++ b/src/components/Navigation/SearchSidebar.tsx @@ -1,7 +1,7 @@ import type {ParamListBase} from '@react-navigation/native'; import React, {useEffect} from 'react'; import {View} from 'react-native'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchResultsActions, useSearchResultsContext} from '@components/Search/SearchContext'; import useLoadingBarVisibility from '@hooks/useLoadingBarVisibility'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -24,8 +24,9 @@ function SearchSidebar({state}: SearchSidebarProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const route = state.routes.at(-1); - const {lastSearchType, currentSearchResults, currentSearchQueryJSON} = useSearchStateContext(); - const {setLastSearchType} = useSearchActionsContext(); + const {lastSearchType, currentSearchResults} = useSearchResultsContext(); + const {currentSearchQueryJSON} = useSearchQueryContext(); + const {setLastSearchType} = useSearchResultsActions(); const searchType = currentSearchResults?.search?.type; const isSearchLoading = currentSearchResults?.search?.isLoading; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 378c4289c4c0..e5a7737a465a 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -11,7 +11,7 @@ import {ModalActions} from '@components/Modal/Global/ModalContext'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails, usePolicyCategories, usePolicyTags} from '@components/OnyxListItemProvider'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchResultsContext} from '@components/Search/SearchContext'; import Switch from '@components/Switch'; import Text from '@components/Text'; import UserPills from '@components/UserPills'; @@ -188,7 +188,7 @@ function MoneyRequestView({ const {showConfirmModal} = useConfirmModal(); const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH); - const {currentSearchResults} = useSearchStateContext(); + const {currentSearchResults} = useSearchResultsContext(); const reportAttributes = useReportAttributes(); // When this component is used when merging from the search page, we might not have the parent report stored in the main collection diff --git a/src/components/Search/FilterDropdowns/SortByPopup.tsx b/src/components/Search/FilterDropdowns/SortByPopup.tsx index 2af4920ab2be..9d9992ca05b5 100644 --- a/src/components/Search/FilterDropdowns/SortByPopup.tsx +++ b/src/components/Search/FilterDropdowns/SortByPopup.tsx @@ -4,7 +4,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ListFilterWrapper from '@components/Search/FilterComponents/ListFilterViewWrapper'; import type {SingleSelectItem} from '@components/Search/FilterComponents/SingleSelect'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchResultsContext, useSearchSelectionActions} from '@components/Search/SearchContext'; import type {SearchColumnType, SearchGroupBy, SearchQueryJSON} from '@components/Search/types'; import SelectionList from '@components/SelectionList'; import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; @@ -40,8 +40,8 @@ function SortByPopup({searchResults, queryJSON, groupBy, onSort, onSortOrderPres const {translate} = useLocalize(); const styles = useThemeStyles(); const {accountID} = useCurrentUserPersonalDetails(); - const {shouldUseLiveData} = useSearchStateContext(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {shouldUseLiveData} = useSearchResultsContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const [visibleColumns] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {selector: columnsSelector}); diff --git a/src/components/Search/FilterDropdowns/SortOrderPopup.tsx b/src/components/Search/FilterDropdowns/SortOrderPopup.tsx index d6187af4d22e..33e66e4a01f6 100644 --- a/src/components/Search/FilterDropdowns/SortOrderPopup.tsx +++ b/src/components/Search/FilterDropdowns/SortOrderPopup.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {useSearchActionsContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions} from '@components/Search/SearchContext'; import type {SearchQueryJSON, SortOrder} from '@components/Search/types'; import useLocalize from '@hooks/useLocalize'; import {close} from '@libs/actions/Modal'; @@ -17,7 +17,7 @@ type SortOrderPopupProps = { function SortOrderPopup({queryJSON, onSort, onBackButtonPress, closeOverlay}: SortOrderPopupProps) { const {translate} = useLocalize(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const onSortChange = (sortOrder: SortOrder) => { clearSelectedTransactions(); diff --git a/src/components/Search/SearchBulkActionsButton.tsx b/src/components/Search/SearchBulkActionsButton.tsx index 99fc669a0e4a..1696549a4374 100644 --- a/src/components/Search/SearchBulkActionsButton.tsx +++ b/src/components/Search/SearchBulkActionsButton.tsx @@ -28,7 +28,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import BulkDuplicateHandler from './BulkDuplicateHandler'; import BulkDuplicateReportHandler from './BulkDuplicateReportHandler'; -import {useSearchActionsContext, useSearchStateContext} from './SearchContext'; +import {useSearchSelectionActions, useSearchSelectionContext} from './SearchContext'; import type {BulkPaySelectionData, SearchQueryJSON} from './types'; type SearchBulkActionsButtonProps = { @@ -41,8 +41,8 @@ function SearchBulkActionsButton({queryJSON}: SearchBulkActionsButtonProps) { // We need isSmallScreenWidth (not just shouldUseNarrowLayout) because DecisionModal requires it for correct modal type // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); - const {selectedTransactions, selectedReports, areAllMatchingItemsSelected, shouldShowSelectAllMatchingItems} = useSearchStateContext(); - const {selectAllMatchingItems} = useSearchActionsContext(); + const {selectedTransactions, selectedReports, areAllMatchingItemsSelected, shouldShowSelectAllMatchingItems} = useSearchSelectionContext(); + const {selectAllMatchingItems} = useSearchSelectionActions(); const kycWallRef = useContext(KYCWallContext); const {isAccountLocked} = useLockedAccountState(); const {showLockedAccountModal} = useLockedAccountActions(); diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index b347e2ba1c4f..0ede75ae13be 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -1,433 +1,53 @@ -import {useNavigation} from '@react-navigation/native'; -import type {NavigationState} from '@react-navigation/routers'; -import React, {useContext, useEffect, useRef, useState} from 'react'; -// We need direct access to useOnyx from react-native-onyx to avoid circular dependencies in SearchContext -// eslint-disable-next-line no-restricted-imports -import {useOnyx} from 'react-native-onyx'; -import useCardFeedsForDisplay from '@hooks/useCardFeedsForDisplay'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import usePreviousDefined from '@hooks/usePreviousDefined'; -import useRootNavigationState from '@hooks/useRootNavigationState'; -import useTodos from '@hooks/useTodos'; -import {getDeepestFocusedScreen} from '@libs/Navigation/Navigation'; -import {isMoneyRequestReport} from '@libs/ReportUtils'; -import {buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils'; -import type {SearchKey, SearchTypeMenuItem} from '@libs/SearchUIUtils'; -import {getSuggestedSearches, isTodoSearch, isTransactionListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils'; -import {hasValidModifiedAmount} from '@libs/TransactionUtils'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import SCREENS from '@src/SCREENS'; -import type {SearchResultsInfo} from '@src/types/onyx/SearchResults'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {ReportActionListItemType, TaskListItemType, TransactionGroupListItemType, TransactionListItemType} from './SearchList/ListItem/types'; -import type {SearchActionsContextValue, SearchContextData, SearchStateContextValue, SelectedReports, SelectedTransactions} from './types'; - -type SearchContextProps = { - children: React.ReactNode; -}; - -// Default search info when building from live data -// Used for to-do searches where we build SearchResults from live Onyx data instead of API snapshots -const defaultSearchInfo: SearchResultsInfo = { - offset: 0, - type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, - hasMoreResults: false, - hasResults: true, - isLoading: false, - count: 0, - total: 0, - currency: '', -}; - -const defaultSearchContextData: SearchContextData = { - currentSearchKey: undefined, - currentSearchQueryJSON: undefined, - currentSearchResults: undefined, - currentSelectedTransactionReportID: undefined, - selectedTransactions: {}, - selectedTransactionIDs: [], - selectedReports: [], - isOnSearch: false, - shouldTurnOffSelectionMode: false, - shouldResetSearchQuery: false, - hasSelectedTransactions: false, - currentSearchHash: -1, - currentSimilarSearchHash: -1, - suggestedSearches: {} as Record, - sortedReportIDs: CONST.EMPTY_ARRAY, -}; - -const defaultSearchStateContext: SearchStateContextValue = { - ...defaultSearchContextData, - lastSearchType: undefined, - areAllMatchingItemsSelected: false, - shouldShowSelectAllMatchingItems: false, - shouldShowFiltersBarLoading: false, - currentSearchResults: undefined, - shouldUseLiveData: false, -}; - -const defaultSearchActionsContext: SearchActionsContextValue = { - setLastSearchType: () => {}, - setCurrentSelectedTransactionReportID: () => {}, - setSelectedTransactions: () => {}, - setSelectedReports: () => {}, - removeTransaction: () => {}, - clearSelectedTransactions: () => {}, - setShouldShowFiltersBarLoading: () => {}, - setShouldShowSelectAllMatchingItems: () => {}, - selectAllMatchingItems: () => {}, - setShouldResetSearchQuery: () => {}, - setSortedReportIDs: () => {}, -}; - -function deriveSelectedReports( - transactionIDs: SelectedTransactions, - data: TransactionListItemType[] | TransactionGroupListItemType[] | ReportActionListItemType[] | TaskListItemType[], -): SelectedReports[] { - if (data.length && data.every(isTransactionReportGroupListItemType)) { - return data - .filter((item) => { - if (!isMoneyRequestReport(item)) { - return false; - } - if (item.transactions.length === 0) { - return !!item.keyForList && transactionIDs[item.keyForList]?.isSelected; - } - return item.transactions.every(({keyForList}) => transactionIDs[keyForList]?.isSelected); - }) - .map( - ({ - reportID, - action = CONST.SEARCH.ACTION_TYPES.VIEW, - total = CONST.DEFAULT_NUMBER_ID, - policyID, - allActions = [action], - currency, - chatReportID, - managerID, - ownerAccountID, - parentReportActionID, - parentReportID, - type, - }) => ({ - reportID, - action, - total, - policyID, - allActions, - currency, - chatReportID, - managerID, - ownerAccountID, - parentReportActionID, - parentReportID, - type, - }), - ); - } - if (data.length && data.every(isTransactionListItemType)) { - return data - .filter(({keyForList}) => !!keyForList && transactionIDs[keyForList]?.isSelected) - .map((item) => { - const total = hasValidModifiedAmount(item) ? Number(item.modifiedAmount) : (item.amount ?? CONST.DEFAULT_NUMBER_ID); - const action = item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW; - - return { - reportID: item.reportID, - action, - total, - policyID: item.policyID, - allActions: item.allActions ?? [action], - currency: item.currency, - chatReportID: item.report?.chatReportID, - managerID: item.report?.managerID, - ownerAccountID: item.report?.ownerAccountID, - parentReportActionID: item.report?.parentReportActionID, - parentReportID: item.report?.parentReportID, - type: item.report?.type, - }; - }); - } - return []; +import {useContext} from 'react'; +import { + SearchQueryActionsContext, + SearchQueryContext, + SearchResultsActionsContext, + SearchResultsContext, + SearchSelectionActionsContext, + SearchSelectionContext, +} from './SearchContextDefinitions'; + +// Lightweight public surface for search contexts. +// `useOnyx` imports the context instances from here; pulling in the providers (and their useOnyx +// users like useCardFeedsForDisplay) would create a cycle that breaks jest mock resolution in tests +// like PureReportActionItemTest. Providers live in `SearchContextProvider.tsx`. + +function useSearchQueryContext() { + return useContext(SearchQueryContext); } -const SearchStateContext = React.createContext(defaultSearchStateContext); -const SearchActionsContext = React.createContext(defaultSearchActionsContext); - -function selectSearchQueryParam(state: NavigationState | undefined) { - const focused = getDeepestFocusedScreen(state); - return focused?.name === SCREENS.SEARCH.ROOT ? (focused.params?.q as string | undefined) : undefined; -} - -function selectSearchRawQueryParam(state: NavigationState | undefined) { - const focused = getDeepestFocusedScreen(state); - return focused?.name === SCREENS.SEARCH.ROOT ? (focused.params?.rawQuery as string | undefined) : undefined; +function useSearchQueryActions() { + return useContext(SearchQueryActionsContext); } -function SearchContextProvider({children}: SearchContextProps) { - const navigation = useNavigation(); - // Extract only the primitive values we need from the focused screen to avoid - // re-renders from new object references returned by getDeepestFocusedScreen. - const queryParam = useRootNavigationState((state) => selectSearchQueryParam(state ?? navigation.getState())); - const rawQueryParam = useRootNavigationState((state) => selectSearchRawQueryParam(state ?? navigation.getState())); - const definedQueryParam = usePreviousDefined(queryParam) ?? buildSearchQueryString(); - const currentSearchQueryJSON = buildSearchQueryJSON(definedQueryParam, rawQueryParam); - - const areTransactionsEmpty = useRef(true); - const [lastSearchType, setLastSearchType] = useState(); - const [areAllMatchingItemsSelected, selectAllMatchingItems] = useState(false); - const [shouldShowFiltersBarLoading, setShouldShowFiltersBarLoading] = useState(false); - const [shouldShowSelectAllMatchingItems, setShouldShowSelectAllMatchingItems] = useState(false); - const [searchContextData, setSearchContextData] = useState({...defaultSearchContextData}); - - const currentSearchHash = currentSearchQueryJSON?.hash ?? -1; - const currentRecentSearchHash = currentSearchQueryJSON?.recentSearchHash ?? -1; - const currentSimilarSearchHash = currentSearchQueryJSON?.similarSearchHash ?? -1; - - const todoSearchResultsData = useTodos(); - const [snapshotSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`); - - const {defaultCardFeed} = useCardFeedsForDisplay(); - const {accountID} = useCurrentUserPersonalDetails(); - const defaultCardFeedID = defaultCardFeed?.id; - const suggestedSearches = getSuggestedSearches(accountID, defaultCardFeedID); - - const currentSearchKey = Object.values(suggestedSearches).find((search) => search.similarSearchHash === currentSimilarSearchHash)?.key; - - const shouldUseLiveData = !!currentSearchKey && isTodoSearch(currentRecentSearchHash, suggestedSearches); - - // If viewing a to-do search, use live data from useTodos, otherwise return the snapshot data - // We do this so that we can show the counters for the to-do search results without visiting the specific to-do page, e.g. show `Approve [3]` while viewing the `Submit` to-do search. - let currentSearchResults; - if (shouldUseLiveData) { - const liveData = todoSearchResultsData[currentSearchKey as keyof typeof todoSearchResultsData]; - const searchInfo: SearchResultsInfo = { - ...(snapshotSearchResults?.search ?? defaultSearchInfo), - count: liveData.metadata.count, - total: liveData.metadata.total, - currency: liveData.metadata.currency, - }; - const hasResults = Object.keys(liveData.data).length > 0; - // For to-do searches, always return a valid SearchResults object (even with empty data) - // This ensures we show the empty state instead of loading/blocking views - currentSearchResults = { - search: {...searchInfo, isLoading: false, hasResults}, - data: liveData.data, - }; - } else { - currentSearchResults = snapshotSearchResults ?? undefined; - } - - const setSelectedTransactions: SearchActionsContextValue['setSelectedTransactions'] = (transactionIDs, data) => { - if (transactionIDs instanceof Array) { - if (!transactionIDs.length && areTransactionsEmpty.current) { - areTransactionsEmpty.current = true; - return; - } - areTransactionsEmpty.current = false; - setSearchContextData((prevState) => ({ - ...prevState, - selectedTransactionIDs: transactionIDs, - })); - return; - } - - // When the caller provides `data`, derive `selectedReports` in the same commit so the - // two state slices can't diverge for a render. Used by callers (e.g. the refresh-selection - // effect) that already have `filteredData` in scope and react to it changing. - if (data) { - setSearchContextData((prevState) => ({ - ...prevState, - selectedTransactions: transactionIDs, - selectedReports: deriveSelectedReports(transactionIDs, data), - shouldTurnOffSelectionMode: false, - })); - return; - } - - setSearchContextData((prevState) => ({ - ...prevState, - selectedTransactions: transactionIDs, - shouldTurnOffSelectionMode: false, - })); - }; - - const setSelectedReports: SearchActionsContextValue['setSelectedReports'] = (reports) => { - setSearchContextData((prevState) => { - if (prevState.selectedReports.length === 0 && reports.length === 0) { - return prevState; - } - return { - ...prevState, - selectedReports: reports, - }; - }); - }; - - const currentSearchHashRef = useRef(currentSearchHash); - useEffect(() => { - currentSearchHashRef.current = currentSearchHash; - }, [currentSearchHash]); - - const setCurrentSelectedTransactionReportID: SearchActionsContextValue['setCurrentSelectedTransactionReportID'] = (reportID) => { - setSearchContextData((prevState) => { - if (reportID === prevState.currentSelectedTransactionReportID) { - return prevState; - } - - return { - ...prevState, - currentSelectedTransactionReportID: reportID, - }; - }); - }; - - const clearSelectedTransactions: SearchActionsContextValue['clearSelectedTransactions'] = (searchHashOrClearIDsFlag, shouldTurnOffSelectionMode = false) => { - if (typeof searchHashOrClearIDsFlag === 'boolean') { - setSelectedTransactions([]); - return; - } - - if (searchHashOrClearIDsFlag === currentSearchHashRef.current) { - return; - } - - setSearchContextData((prevState) => { - if (prevState.selectedReports.length === 0 && isEmptyObject(prevState.selectedTransactions) && !prevState.shouldTurnOffSelectionMode) { - return prevState; - } - return { - ...prevState, - shouldTurnOffSelectionMode, - selectedTransactions: {}, - selectedReports: [], - }; - }); - - setShouldShowSelectAllMatchingItems(false); - selectAllMatchingItems(false); - }; - - const removeTransaction: SearchActionsContextValue['removeTransaction'] = (transactionID) => { - if (!transactionID) { - return; - } - - setSearchContextData((prevState) => { - const hasSelectedTransactions = !isEmptyObject(prevState.selectedTransactions); - const hasSelectedIDs = prevState.selectedTransactionIDs.length > 0; - - if (!hasSelectedTransactions && !hasSelectedIDs) { - return prevState; - } - - const newState = {...prevState}; - if (hasSelectedTransactions) { - const newSelectedTransactions = Object.entries(prevState.selectedTransactions).reduce((acc, [key, value]) => { - if (key === transactionID) { - return acc; - } - acc[key] = value; - return acc; - }, {} as SelectedTransactions); - newState.selectedTransactions = newSelectedTransactions; - } - if (hasSelectedIDs) { - newState.selectedTransactionIDs = prevState.selectedTransactionIDs.filter((ID) => transactionID !== ID); - } - return newState; - }); - }; - - const setShouldResetSearchQuery = (shouldReset: boolean) => { - setSearchContextData((prevState) => ({ - ...prevState, - shouldResetSearchQuery: shouldReset, - })); - }; - - const setSortedReportIDs = (newIDs: ReadonlyArray) => { - setSearchContextData((prev) => { - // ensure that we don't save the same report IDs unless they are really different - const hasChanged = prev.sortedReportIDs.length !== newIDs.length || prev.sortedReportIDs.some((id, i) => id !== newIDs.at(i)); - - return hasChanged ? {...prev, sortedReportIDs: newIDs} : prev; - }); - }; - - const searchStateContextValue: SearchStateContextValue = { - ...searchContextData, - suggestedSearches, - currentSearchKey, - currentSearchHash, - currentSimilarSearchHash, - currentSearchResults, - shouldUseLiveData, - shouldShowFiltersBarLoading, - lastSearchType, - shouldShowSelectAllMatchingItems, - areAllMatchingItemsSelected, - hasSelectedTransactions: searchContextData.selectedTransactionIDs.length > 0 || Object.values(searchContextData.selectedTransactions).some((t) => t.isSelected), - currentSearchQueryJSON, - }; - - const searchActionsContextValue: SearchActionsContextValue = { - removeTransaction, - setSelectedTransactions, - setSelectedReports, - setCurrentSelectedTransactionReportID, - clearSelectedTransactions, - setShouldShowFiltersBarLoading, - setLastSearchType, - setShouldShowSelectAllMatchingItems, - selectAllMatchingItems, - setShouldResetSearchQuery, - setSortedReportIDs, - }; - - return ( - - {children} - - ); +function useSearchResultsContext() { + return useContext(SearchResultsContext); } -/** - * Note: `selectedTransactionIDs` and `selectedTransactions` are two separate properties. - * Setting or clearing one of them does not influence the other. - * IDs should be used if transaction details are not required. - */ -function useSearchStateContext() { - return useContext(SearchStateContext); +function useSearchResultsActions() { + return useContext(SearchResultsActionsContext); } -function useSearchActionsContext() { - return useContext(SearchActionsContext); +function useSearchSelectionContext() { + return useContext(SearchSelectionContext); } -/** - * Derives `selectedReports` from the current selection + visible rows and syncs it into context. - * Used by the Search component so `toggleTransaction` can stay independent of `filteredData`. - * - * `data` is read via a ref so this effect only fires when `selectedTransactions` changes. - * Without that, a `data` change (e.g. Onyx push) would fire this effect with a stale - * `selectedTransactions` from closure and clobber any atomic update made in the same commit. - */ -function useSyncSelectedReports(data: TransactionListItemType[] | TransactionGroupListItemType[] | ReportActionListItemType[] | TaskListItemType[]) { - const {selectedTransactions} = useSearchStateContext(); - const {setSelectedReports} = useSearchActionsContext(); - - const dataRef = useRef(data); - useEffect(() => { - dataRef.current = data; - }); - - useEffect(() => { - setSelectedReports(deriveSelectedReports(selectedTransactions, dataRef.current)); - }, [selectedTransactions, setSelectedReports]); +function useSearchSelectionActions() { + return useContext(SearchSelectionActionsContext); } -export {SearchContextProvider, useSearchStateContext, useSearchActionsContext, useSyncSelectedReports, SearchStateContext, SearchActionsContext}; +export { + SearchQueryContext, + SearchQueryActionsContext, + SearchResultsContext, + SearchResultsActionsContext, + SearchSelectionContext, + SearchSelectionActionsContext, + useSearchQueryContext, + useSearchQueryActions, + useSearchResultsContext, + useSearchResultsActions, + useSearchSelectionContext, + useSearchSelectionActions, +}; diff --git a/src/components/Search/SearchContextDefinitions.ts b/src/components/Search/SearchContextDefinitions.ts new file mode 100644 index 000000000000..e6c6cbeac396 --- /dev/null +++ b/src/components/Search/SearchContextDefinitions.ts @@ -0,0 +1,65 @@ +import React from 'react'; +import type {SearchKey, SearchTypeMenuItem} from '@libs/SearchUIUtils'; +import CONST from '@src/CONST'; +import type {SearchQueryActionsValue, SearchQueryContextValue, SearchResultsActionsValue, SearchResultsContextValue, SearchSelectionActionsValue, SearchSelectionContextValue} from './types'; + +// This file holds the bare React.createContext() calls so they can be imported by `@hooks/useOnyx` +// without triggering the SearchQueryProvider -> useCardFeedsForDisplay -> @hooks/useOnyx -> +// SearchQueryProvider circular dependency that caused TDZ errors under React Refresh. + +const defaultSearchQueryContext: SearchQueryContextValue = { + currentSearchHash: -1, + currentSimilarSearchHash: -1, + currentSearchKey: undefined, + currentSearchQueryJSON: undefined, + suggestedSearches: {} as Record, + shouldResetSearchQuery: false, +}; + +const defaultSearchQueryActions: SearchQueryActionsValue = { + setShouldResetSearchQuery: () => {}, +}; + +const defaultSearchResultsContext: SearchResultsContextValue = { + currentSearchResults: undefined, + shouldUseLiveData: false, + sortedReportIDs: CONST.EMPTY_ARRAY, + shouldShowFiltersBarLoading: false, + lastSearchType: undefined, +}; + +const defaultSearchResultsActions: SearchResultsActionsValue = { + setSortedReportIDs: () => {}, + setShouldShowFiltersBarLoading: () => {}, + setLastSearchType: () => {}, +}; + +const defaultSearchSelectionContext: SearchSelectionContextValue = { + currentSelectedTransactionReportID: undefined, + selectedTransactions: {}, + selectedTransactionIDs: [], + selectedReports: [], + shouldTurnOffSelectionMode: false, + hasSelectedTransactions: false, + shouldShowSelectAllMatchingItems: false, + areAllMatchingItemsSelected: false, +}; + +const defaultSearchSelectionActions: SearchSelectionActionsValue = { + setSelectedTransactions: () => {}, + setSelectedReports: () => {}, + setCurrentSelectedTransactionReportID: () => {}, + clearSelectedTransactions: () => {}, + removeTransaction: () => {}, + setShouldShowSelectAllMatchingItems: () => {}, + selectAllMatchingItems: () => {}, +}; + +const SearchQueryContext = React.createContext(defaultSearchQueryContext); +const SearchQueryActionsContext = React.createContext(defaultSearchQueryActions); +const SearchResultsContext = React.createContext(defaultSearchResultsContext); +const SearchResultsActionsContext = React.createContext(defaultSearchResultsActions); +const SearchSelectionContext = React.createContext(defaultSearchSelectionContext); +const SearchSelectionActionsContext = React.createContext(defaultSearchSelectionActions); + +export {SearchQueryContext, SearchQueryActionsContext, SearchResultsContext, SearchResultsActionsContext, SearchSelectionContext, SearchSelectionActionsContext}; diff --git a/src/components/Search/SearchContextProvider.tsx b/src/components/Search/SearchContextProvider.tsx new file mode 100644 index 000000000000..800c960d8192 --- /dev/null +++ b/src/components/Search/SearchContextProvider.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import SearchQueryProvider from './SearchQueryProvider'; +import SearchResultsProvider from './SearchResultsProvider'; +import {SearchSelectionProvider, useSyncSelectedReports} from './SearchSelectionProvider'; + +type SearchContextProps = { + children: React.ReactNode; +}; + +function SearchContextProvider({children}: SearchContextProps) { + return ( + + + {children} + + + ); +} + +export {SearchContextProvider, useSyncSelectedReports}; diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx index 35a9777f6b1b..182cb0ef0401 100644 --- a/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx +++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx @@ -7,7 +7,7 @@ import {View} from 'react-native'; import {useOnyx as originalUseOnyx} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import Icon from '@components/Icon'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchResultsContext} from '@components/Search/SearchContext'; import BaseListItem from '@components/SelectionList/ListItem/BaseListItem'; import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; @@ -66,7 +66,8 @@ function ExpenseReportListItem({ const theme = useTheme(); const {translate} = useLocalize(); const {isLargeScreenWidth} = useResponsiveLayout(); - const {currentSearchHash, currentSearchKey, currentSearchResults} = useSearchStateContext(); + const {currentSearchHash, currentSearchKey} = useSearchQueryContext(); + const {currentSearchResults} = useSearchResultsContext(); const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportItem.reportID}`, {selector: isActionLoadingSelector}); const expensifyIcons = useMemoizedLazyExpensifyIcons(['DotIndicator']); const currentUserDetails = useCurrentUserPersonalDetails(); diff --git a/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx b/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx index 67e36be9bbc7..ee2ae2cd6640 100644 --- a/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx +++ b/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx @@ -6,7 +6,7 @@ import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/ import Icon from '@components/Icon'; import {PressableWithFeedback} from '@components/Pressable'; import ReportSearchHeader from '@components/ReportSearchHeader'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchResultsContext} from '@components/Search/SearchContext'; import type {ListItem} from '@components/SelectionList/types'; import useConfirmModal from '@hooks/useConfirmModal'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -214,7 +214,8 @@ function ReportListItemHeader({ const StyleUtils = useStyleUtils(); const styles = useThemeStyles(); const theme = useTheme(); - const {currentSearchHash, currentSearchKey, currentSearchResults: snapshot} = useSearchStateContext(); + const {currentSearchHash, currentSearchKey} = useSearchQueryContext(); + const {currentSearchResults: snapshot} = useSearchResultsContext(); const {isLargeScreenWidth} = useResponsiveLayout(); const thereIsFromAndTo = !!reportItem?.from && !!reportItem?.to; const showUserInfo = (reportItem.type === CONST.REPORT.TYPE.IOU && thereIsFromAndTo) || (reportItem.type === CONST.REPORT.TYPE.EXPENSE && !!reportItem?.from); diff --git a/src/components/Search/SearchList/ListItem/TransactionGroupListItem.tsx b/src/components/Search/SearchList/ListItem/TransactionGroupListItem.tsx index 6d1b9035cb10..72d8fd1657da 100644 --- a/src/components/Search/SearchList/ListItem/TransactionGroupListItem.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionGroupListItem.tsx @@ -9,7 +9,7 @@ import AnimatedCollapsible from '@components/AnimatedCollapsible'; import {getButtonRole} from '@components/Button/utils'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {PressableWithFeedback} from '@components/Pressable'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchSelectionContext} from '@components/Search/SearchContext'; import type {SearchGroupBy} from '@components/Search/types'; import type {ListItem} from '@components/SelectionList/types'; import useActionLoadingReportIDs from '@hooks/useActionLoadingReportIDs'; @@ -92,7 +92,7 @@ function TransactionGroupListItem({ const theme = useTheme(); const styles = useThemeStyles(); const {translate, formatPhoneNumber} = useLocalize(); - const {selectedTransactions} = useSearchStateContext(); + const {selectedTransactions} = useSearchSelectionContext(); const {isLargeScreenWidth} = useResponsiveLayout(); const currentUserDetails = useCurrentUserPersonalDetails(); const isScreenFocused = useIsFocused(); diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx index 4db7dace8a09..01c62be798fd 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx @@ -8,7 +8,7 @@ import type {OnyxEntry} from 'react-native-onyx'; // eslint-disable-next-line no-restricted-imports import {useOnyx as originalUseOnyx} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchResultsContext} from '@components/Search/SearchContext'; import type {TransactionListItemProps, TransactionListItemType} from '@components/Search/SearchList/ListItem/types'; import type {ListItem} from '@components/SelectionList/types'; import {useEditingCellState} from '@components/TransactionItemRow/EditableCell'; @@ -66,7 +66,8 @@ function TransactionListItem({ const isDeletedTransaction = isDeletedTransactionUtil(transactionItem); const {isLargeScreenWidth} = useResponsiveLayout(); - const {currentSearchHash, currentSearchKey, currentSearchResults} = useSearchStateContext(); + const {currentSearchHash, currentSearchKey} = useSearchQueryContext(); + const {currentSearchResults} = useSearchResultsContext(); const snapshotReport = (currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`] ?? {}) as Report; const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${transactionItem.reportID}`, {selector: isActionLoadingSelector}); diff --git a/src/components/Search/SearchPageHeader/SearchActionsBarWide.tsx b/src/components/Search/SearchPageHeader/SearchActionsBarWide.tsx index 2e59b322e194..bff3075b0f44 100644 --- a/src/components/Search/SearchPageHeader/SearchActionsBarWide.tsx +++ b/src/components/Search/SearchPageHeader/SearchActionsBarWide.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import SearchBulkActionsButton from '@components/Search/SearchBulkActionsButton'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchSelectionContext} from '@components/Search/SearchContext'; import type {SearchQueryJSON} from '@components/Search/types'; import useThemeStyles from '@hooks/useThemeStyles'; import type {SearchResults} from '@src/types/onyx'; @@ -22,7 +22,7 @@ type SearchActionsBarWideProps = { function SearchActionsBarWide({queryJSON, searchResults, handleSearch, onSort}: SearchActionsBarWideProps) { const styles = useThemeStyles(); - const {selectedTransactions} = useSearchStateContext(); + const {selectedTransactions} = useSearchSelectionContext(); const hasSelectedItems = Object.keys(selectedTransactions ?? {}).length > 0; return ( diff --git a/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx index 9b56ad05b8b2..c5cfaa3a8fd6 100644 --- a/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/useSearchFiltersBar.tsx @@ -6,7 +6,7 @@ import CommonPopup from '@components/Search/FilterDropdowns/CommonPopup'; import type {PopoverComponentProps} from '@components/Search/FilterDropdowns/DropdownButton'; import ReportFieldPopup from '@components/Search/FilterDropdowns/ReportFieldPopup'; import useUpdateFilterQuery from '@components/Search/hooks/useUpdateFilterQuery'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchResultsContext} from '@components/Search/SearchContext'; import type {ReportFieldKey, SearchFilterKey, SearchQueryJSON} from '@components/Search/types'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useLocalize from '@hooks/useLocalize'; @@ -114,7 +114,7 @@ function useSearchFiltersBar(queryJSON: SearchQueryJSON): UseSearchFiltersBarRes const {translate, localeCompare} = useLocalize(); const {isOffline} = useNetwork(); const {convertToDisplayStringWithoutCurrency} = useCurrencyListActions(); - const {shouldShowFiltersBarLoading, currentSearchResults} = useSearchStateContext(); + const {shouldShowFiltersBarLoading, currentSearchResults} = useSearchResultsContext(); const updateFilterForm = useUpdateFilterQuery(queryJSON, false); const filters = mapFiltersFormToLabelValueList( searchAdvancedFiltersForm, diff --git a/src/components/Search/SearchQueryProvider.tsx b/src/components/Search/SearchQueryProvider.tsx new file mode 100644 index 000000000000..c4ce93687c46 --- /dev/null +++ b/src/components/Search/SearchQueryProvider.tsx @@ -0,0 +1,69 @@ +import {useNavigation} from '@react-navigation/native'; +import type {NavigationState} from '@react-navigation/routers'; +import React, {useState} from 'react'; +import useCardFeedsForDisplay from '@hooks/useCardFeedsForDisplay'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import usePreviousDefined from '@hooks/usePreviousDefined'; +import useRootNavigationState from '@hooks/useRootNavigationState'; +import {getDeepestFocusedScreen} from '@libs/Navigation/Navigation'; +import {buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils'; +import {getSuggestedSearches} from '@libs/SearchUIUtils'; +import SCREENS from '@src/SCREENS'; +import {SearchQueryActionsContext, SearchQueryContext} from './SearchContextDefinitions'; +import type {SearchQueryActionsValue, SearchQueryContextValue} from './types'; + +type SearchQueryProviderProps = { + children: React.ReactNode; +}; + +function selectSearchQueryParam(state: NavigationState | undefined) { + const focused = getDeepestFocusedScreen(state); + return focused?.name === SCREENS.SEARCH.ROOT ? (focused.params?.q as string | undefined) : undefined; +} + +function selectSearchRawQueryParam(state: NavigationState | undefined) { + const focused = getDeepestFocusedScreen(state); + return focused?.name === SCREENS.SEARCH.ROOT ? (focused.params?.rawQuery as string | undefined) : undefined; +} + +function SearchQueryProvider({children}: SearchQueryProviderProps) { + const navigation = useNavigation(); + // Extract only the primitive values we need from the focused screen to avoid + // re-renders from new object references returned by getDeepestFocusedScreen. + const queryParam = useRootNavigationState((state) => selectSearchQueryParam(state ?? navigation.getState())); + const rawQueryParam = useRootNavigationState((state) => selectSearchRawQueryParam(state ?? navigation.getState())); + const definedQueryParam = usePreviousDefined(queryParam) ?? buildSearchQueryString(); + const currentSearchQueryJSON = buildSearchQueryJSON(definedQueryParam, rawQueryParam); + + const {defaultCardFeed} = useCardFeedsForDisplay(); + const {accountID} = useCurrentUserPersonalDetails(); + const defaultCardFeedID = defaultCardFeed?.id; + const suggestedSearches = getSuggestedSearches(accountID, defaultCardFeedID); + + const currentSearchHash = currentSearchQueryJSON?.hash ?? -1; + const currentSimilarSearchHash = currentSearchQueryJSON?.similarSearchHash ?? -1; + const currentSearchKey = Object.values(suggestedSearches).find((search) => search.similarSearchHash === currentSimilarSearchHash)?.key; + + const [shouldResetSearchQuery, setShouldResetSearchQuery] = useState(false); + + const queryValue: SearchQueryContextValue = { + currentSearchHash, + currentSimilarSearchHash, + currentSearchKey, + currentSearchQueryJSON, + suggestedSearches, + shouldResetSearchQuery, + }; + + const queryActionsValue: SearchQueryActionsValue = { + setShouldResetSearchQuery, + }; + + return ( + + {children} + + ); +} + +export default SearchQueryProvider; diff --git a/src/components/Search/SearchResultsProvider.tsx b/src/components/Search/SearchResultsProvider.tsx new file mode 100644 index 000000000000..2e6467f67539 --- /dev/null +++ b/src/components/Search/SearchResultsProvider.tsx @@ -0,0 +1,99 @@ +import React, {useState} from 'react'; +// This provider is the source of the snapshot data that `@hooks/useOnyx` later routes consumers onto, +// so going through that wrapper here would be self-referential. The wrapper also short-circuits its own +// logic for snapshot keys (see the `!key.startsWith(ONYXKEYS.COLLECTION.SNAPSHOT)` guard in useOnyx.ts), +// so it would add nothing for this read. Use the raw react-native-onyx hook directly. +// eslint-disable-next-line no-restricted-imports +import {useOnyx} from 'react-native-onyx'; +import useTodos from '@hooks/useTodos'; +import {isTodoSearch} from '@libs/SearchUIUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {SearchResultsInfo} from '@src/types/onyx/SearchResults'; +import {useSearchQueryContext} from './SearchContext'; +import {SearchResultsActionsContext, SearchResultsContext} from './SearchContextDefinitions'; +import type {SearchResultsActionsValue, SearchResultsContextValue} from './types'; + +type SearchResultsProviderProps = { + children: React.ReactNode; +}; + +// Default search info when building from live data +// Used for to-do searches where we build SearchResults from live Onyx data instead of API snapshots +const defaultSearchInfo: SearchResultsInfo = { + offset: 0, + type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, + status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hasMoreResults: false, + hasResults: true, + isLoading: false, + count: 0, + total: 0, + currency: '', +}; + +function SearchResultsProvider({children}: SearchResultsProviderProps) { + const {currentSearchHash, currentSearchKey, currentSearchQueryJSON, suggestedSearches} = useSearchQueryContext(); + const currentRecentSearchHash = currentSearchQueryJSON?.recentSearchHash ?? -1; + + const todoSearchResultsData = useTodos(); + const [snapshotSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`); + + const shouldUseLiveData = !!currentSearchKey && isTodoSearch(currentRecentSearchHash, suggestedSearches); + + // If viewing a to-do search, use live data from useTodos, otherwise return the snapshot data + // We do this so that we can show the counters for the to-do search results without visiting the specific to-do page, e.g. show `Approve [3]` while viewing the `Submit` to-do search. + let currentSearchResults; + if (shouldUseLiveData) { + const liveData = todoSearchResultsData[currentSearchKey as keyof typeof todoSearchResultsData]; + const searchInfo: SearchResultsInfo = { + ...(snapshotSearchResults?.search ?? defaultSearchInfo), + count: liveData.metadata.count, + total: liveData.metadata.total, + currency: liveData.metadata.currency, + }; + const hasResults = Object.keys(liveData.data).length > 0; + // For to-do searches, always return a valid SearchResults object (even with empty data) + // This ensures we show the empty state instead of loading/blocking views + currentSearchResults = { + search: {...searchInfo, isLoading: false, hasResults}, + data: liveData.data, + }; + } else { + currentSearchResults = snapshotSearchResults ?? undefined; + } + + const [sortedReportIDs, setSortedReportIDsState] = useState>(CONST.EMPTY_ARRAY); + const [shouldShowFiltersBarLoading, setShouldShowFiltersBarLoading] = useState(false); + const [lastSearchType, setLastSearchType] = useState(); + + const setSortedReportIDs: SearchResultsActionsValue['setSortedReportIDs'] = (newIDs) => { + setSortedReportIDsState((prev) => { + // ensure that we don't save the same report IDs unless they are really different + const hasChanged = prev.length !== newIDs.length || prev.some((id, i) => id !== newIDs.at(i)); + return hasChanged ? newIDs : prev; + }); + }; + + const resultsValue: SearchResultsContextValue = { + currentSearchResults, + shouldUseLiveData, + sortedReportIDs, + shouldShowFiltersBarLoading, + lastSearchType, + }; + + const resultsActionsValue: SearchResultsActionsValue = { + setSortedReportIDs, + setShouldShowFiltersBarLoading, + setLastSearchType, + }; + + return ( + + {children} + + ); +} + +export default SearchResultsProvider; diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 53ac52205793..6afc831ee50c 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -10,7 +10,7 @@ import {usePersonalDetails} from '@components/OnyxListItemProvider'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import DeferredAutocompleteList from '@components/Search/DeferredSearchAutocompleteList'; import type {GetAdditionalSectionsCallback} from '@components/Search/SearchAutocompleteList'; -import {useSearchActionsContext} from '@components/Search/SearchContext'; +import {useSearchQueryActions} from '@components/Search/SearchContext'; import SearchInputSelectionWrapper from '@components/Search/SearchInputSelectionWrapper'; import type {SearchQueryItem} from '@components/Search/SearchList/ListItem/SearchQueryListItem'; import {isSearchQueryItem} from '@components/Search/SearchList/ListItem/SearchQueryListItem'; @@ -64,7 +64,7 @@ type SearchRouterProps = { function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDisplayed, ref}: SearchRouterProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const {setShouldResetSearchQuery} = useSearchActionsContext(); + const {setShouldResetSearchQuery} = useSearchQueryActions(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const currentUserAccountID = currentUserPersonalDetails.accountID; const [isSearchingForReports] = useOnyx(ONYXKEYS.RAM_ONLY_IS_SEARCHING_FOR_REPORTS); diff --git a/src/components/Search/SearchSelectionProvider.tsx b/src/components/Search/SearchSelectionProvider.tsx new file mode 100644 index 000000000000..57940d280e09 --- /dev/null +++ b/src/components/Search/SearchSelectionProvider.tsx @@ -0,0 +1,284 @@ +import React, {useEffect, useRef, useState} from 'react'; +import {isMoneyRequestReport} from '@libs/ReportUtils'; +import {isTransactionListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils'; +import {hasValidModifiedAmount} from '@libs/TransactionUtils'; +import CONST from '@src/CONST'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {useSearchQueryContext, useSearchSelectionActions, useSearchSelectionContext} from './SearchContext'; +import {SearchSelectionActionsContext, SearchSelectionContext} from './SearchContextDefinitions'; +import type {ReportActionListItemType, TaskListItemType, TransactionGroupListItemType, TransactionListItemType} from './SearchList/ListItem/types'; +import type {SearchSelectionActionsValue, SearchSelectionContextValue, SelectedReports, SelectedTransactions} from './types'; + +type SearchSelectionProviderProps = { + children: React.ReactNode; +}; + +type SelectionState = { + selectedTransactions: SelectedTransactions; + selectedTransactionIDs: string[]; + selectedReports: SelectedReports[]; + currentSelectedTransactionReportID: string | undefined; + shouldTurnOffSelectionMode: boolean; +}; + +const defaultSelectionState: SelectionState = { + selectedTransactions: {}, + selectedTransactionIDs: [], + selectedReports: [], + currentSelectedTransactionReportID: undefined, + shouldTurnOffSelectionMode: false, +}; + +function deriveSelectedReports( + transactionIDs: SelectedTransactions, + data: TransactionListItemType[] | TransactionGroupListItemType[] | ReportActionListItemType[] | TaskListItemType[], +): SelectedReports[] { + if (data.length && data.every(isTransactionReportGroupListItemType)) { + return data + .filter((item) => { + if (!isMoneyRequestReport(item)) { + return false; + } + if (item.transactions.length === 0) { + return !!item.keyForList && transactionIDs[item.keyForList]?.isSelected; + } + return item.transactions.every(({keyForList}) => transactionIDs[keyForList]?.isSelected); + }) + .map( + ({ + reportID, + action = CONST.SEARCH.ACTION_TYPES.VIEW, + total = CONST.DEFAULT_NUMBER_ID, + policyID, + allActions = [action], + currency, + chatReportID, + managerID, + ownerAccountID, + parentReportActionID, + parentReportID, + type, + }) => ({ + reportID, + action, + total, + policyID, + allActions, + currency, + chatReportID, + managerID, + ownerAccountID, + parentReportActionID, + parentReportID, + type, + }), + ); + } + if (data.length && data.every(isTransactionListItemType)) { + return data + .filter(({keyForList}) => !!keyForList && transactionIDs[keyForList]?.isSelected) + .map((item) => { + const total = hasValidModifiedAmount(item) ? Number(item.modifiedAmount) : (item.amount ?? CONST.DEFAULT_NUMBER_ID); + const action = item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW; + + return { + reportID: item.reportID, + action, + total, + policyID: item.policyID, + allActions: item.allActions ?? [action], + currency: item.currency, + chatReportID: item.report?.chatReportID, + managerID: item.report?.managerID, + ownerAccountID: item.report?.ownerAccountID, + parentReportActionID: item.report?.parentReportActionID, + parentReportID: item.report?.parentReportID, + type: item.report?.type, + }; + }); + } + return []; +} + +function SearchSelectionProvider({children}: SearchSelectionProviderProps) { + const {currentSearchHash} = useSearchQueryContext(); + + const areTransactionsEmpty = useRef(true); + const [areAllMatchingItemsSelected, selectAllMatchingItems] = useState(false); + const [shouldShowSelectAllMatchingItems, setShouldShowSelectAllMatchingItems] = useState(false); + const [selectionState, setSelectionState] = useState(defaultSelectionState); + + const currentSearchHashRef = useRef(currentSearchHash); + + useEffect(() => { + currentSearchHashRef.current = currentSearchHash; + }, [currentSearchHash]); + + const setSelectedTransactions: SearchSelectionActionsValue['setSelectedTransactions'] = (transactionIDs, data) => { + if (transactionIDs instanceof Array) { + if (!transactionIDs.length && areTransactionsEmpty.current) { + areTransactionsEmpty.current = true; + return; + } + areTransactionsEmpty.current = false; + setSelectionState((prevState) => ({ + ...prevState, + selectedTransactionIDs: transactionIDs, + })); + return; + } + + // When the caller provides `data`, derive `selectedReports` in the same commit so the + // two state slices can't diverge for a render. Used by callers (e.g. the refresh-selection + // effect) that already have `filteredData` in scope and react to it changing. + if (data) { + setSelectionState((prevState) => ({ + ...prevState, + selectedTransactions: transactionIDs, + selectedReports: deriveSelectedReports(transactionIDs, data), + shouldTurnOffSelectionMode: false, + })); + return; + } + + setSelectionState((prevState) => ({ + ...prevState, + selectedTransactions: transactionIDs, + shouldTurnOffSelectionMode: false, + })); + }; + + const setSelectedReports: SearchSelectionActionsValue['setSelectedReports'] = (reports) => { + setSelectionState((prevState) => { + if (prevState.selectedReports.length === 0 && reports.length === 0) { + return prevState; + } + return { + ...prevState, + selectedReports: reports, + }; + }); + }; + + const setCurrentSelectedTransactionReportID: SearchSelectionActionsValue['setCurrentSelectedTransactionReportID'] = (reportID) => { + setSelectionState((prevState) => { + if (reportID === prevState.currentSelectedTransactionReportID) { + return prevState; + } + return { + ...prevState, + currentSelectedTransactionReportID: reportID, + }; + }); + }; + + const clearSelectedTransactions: SearchSelectionActionsValue['clearSelectedTransactions'] = (searchHashOrClearIDsFlag, shouldTurnOffSelectionMode = false) => { + if (typeof searchHashOrClearIDsFlag === 'boolean') { + setSelectedTransactions([]); + return; + } + + if (searchHashOrClearIDsFlag === currentSearchHashRef.current) { + return; + } + + setSelectionState((prevState) => { + if (prevState.selectedReports.length === 0 && isEmptyObject(prevState.selectedTransactions) && !prevState.shouldTurnOffSelectionMode) { + return prevState; + } + return { + ...prevState, + shouldTurnOffSelectionMode, + selectedTransactions: {}, + selectedReports: [], + }; + }); + + setShouldShowSelectAllMatchingItems(false); + selectAllMatchingItems(false); + }; + + const removeTransaction: SearchSelectionActionsValue['removeTransaction'] = (transactionID) => { + if (!transactionID) { + return; + } + + setSelectionState((prevState) => { + const hasSelectedTransactions = !isEmptyObject(prevState.selectedTransactions); + const hasSelectedIDs = prevState.selectedTransactionIDs.length > 0; + + if (!hasSelectedTransactions && !hasSelectedIDs) { + return prevState; + } + + const newState = {...prevState}; + if (hasSelectedTransactions) { + const newSelectedTransactions = Object.entries(prevState.selectedTransactions).reduce((acc, [key, value]) => { + if (key === transactionID) { + return acc; + } + acc[key] = value; + return acc; + }, {} as SelectedTransactions); + newState.selectedTransactions = newSelectedTransactions; + } + if (hasSelectedIDs) { + newState.selectedTransactionIDs = prevState.selectedTransactionIDs.filter((ID) => transactionID !== ID); + } + return newState; + }); + }; + + const hasSelectedTransactions = selectionState.selectedTransactionIDs.length > 0 || Object.values(selectionState.selectedTransactions).some((t) => t.isSelected); + + const selectionValue: SearchSelectionContextValue = { + ...selectionState, + hasSelectedTransactions, + shouldShowSelectAllMatchingItems, + areAllMatchingItemsSelected, + }; + + const selectionActionsValue: SearchSelectionActionsValue = { + setSelectedTransactions, + setSelectedReports, + setCurrentSelectedTransactionReportID, + clearSelectedTransactions, + removeTransaction, + setShouldShowSelectAllMatchingItems, + selectAllMatchingItems, + }; + + return ( + + {children} + + ); +} + +/** + * Derives `selectedReports` from the current selection + visible rows and syncs it into context. + * Used by the Search component so `toggleTransaction` can stay independent of `filteredData`. + * + * Note: `selectedTransactionIDs` and `selectedTransactions` are two separate properties. + * Setting or clearing one of them does not influence the other. + * IDs should be used if transaction details are not required. + * + * `data` is read via a ref so this effect only fires when `selectedTransactions` changes. + * Without that, a `data` change (e.g. Onyx push) would fire this effect with a stale + * `selectedTransactions` from closure and clobber any atomic update made in the same commit. + */ +function useSyncSelectedReports(data: TransactionListItemType[] | TransactionGroupListItemType[] | ReportActionListItemType[] | TaskListItemType[]) { + const {selectedTransactions} = useSearchSelectionContext(); + const {setSelectedReports} = useSearchSelectionActions(); + + const dataRef = useRef(data); + useEffect(() => { + dataRef.current = data; + }); + + useEffect(() => { + setSelectedReports(deriveSelectedReports(selectedTransactions, dataRef.current)); + }, [selectedTransactions, setSelectedReports]); +} + +export {SearchSelectionProvider, useSyncSelectedReports}; diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 486fce3eebef..84ca7b28d048 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -88,7 +88,8 @@ import useOptimisticSearchTracking from './hooks/useOptimisticSearchTracking'; import useStableOptimisticSortedData from './hooks/useStableOptimisticSortedData'; import SearchChartView from './SearchChartView'; import SearchChartWrapper from './SearchChartWrapper'; -import {useSearchActionsContext, useSearchStateContext, useSyncSelectedReports} from './SearchContext'; +import {useSearchQueryActions, useSearchQueryContext, useSearchResultsActions, useSearchResultsContext, useSearchSelectionActions, useSearchSelectionContext} from './SearchContext'; +import {useSyncSelectedReports} from './SearchContextProvider'; import SearchList from './SearchList'; import type {ReportActionListItemType, SearchListItem, TransactionGroupListItemType, TransactionListItemType, TransactionReportGroupListItemType} from './SearchList/ListItem/types'; import {SearchScopeProvider} from './SearchScopeProvider'; @@ -242,20 +243,13 @@ function Search({ const isFocused = useIsFocused(); const {markReportIDAsExpense, markReportIDAsMultiTransactionExpense, unmarkReportIDAsMultiTransactionExpense} = useWideRHPActions(); - const { - currentSearchHash, - currentSearchKey, - selectedTransactions, - shouldTurnOffSelectionMode, - lastSearchType, - areAllMatchingItemsSelected, - shouldResetSearchQuery, - shouldUseLiveData, - suggestedSearches, - } = useSearchStateContext(); + const {currentSearchHash, currentSearchKey, shouldResetSearchQuery, suggestedSearches} = useSearchQueryContext(); + const {lastSearchType, shouldUseLiveData} = useSearchResultsContext(); + const {selectedTransactions, shouldTurnOffSelectionMode, areAllMatchingItemsSelected} = useSearchSelectionContext(); - const {setSelectedTransactions, clearSelectedTransactions, setShouldShowFiltersBarLoading, setShouldShowSelectAllMatchingItems, selectAllMatchingItems, setShouldResetSearchQuery} = - useSearchActionsContext(); + const {setShouldResetSearchQuery} = useSearchQueryActions(); + const {setShouldShowFiltersBarLoading} = useSearchResultsActions(); + const {setSelectedTransactions, clearSelectedTransactions, setShouldShowSelectAllMatchingItems, selectAllMatchingItems} = useSearchSelectionActions(); const [offset, setOffset] = useState(0); const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); @@ -951,8 +945,6 @@ function Search({ isRefreshingSelection.current = false; }, [selectedTransactions]); - // Keeps `selectedReports` in sync with the current selection + visible rows. - // Hoisted out of `toggleTransaction` so the callback doesn't churn on every search re-derivation. useSyncSelectedReports(filteredData); const areItemsGrouped = !!validGroupBy || isExpenseReportType; diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 68ff107e21c6..3e575dc16b92 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -166,36 +166,47 @@ type SearchCustomColumnIds = | ValueOf | ValueOf; -type SearchContextData = { +type SearchQueryContextValue = { currentSearchHash: number; currentSimilarSearchHash: number; currentSearchKey: SearchKey | undefined; currentSearchQueryJSON: Readonly | undefined; - currentSearchResults: SearchResults | undefined; - currentSelectedTransactionReportID: string | undefined; - selectedTransactions: SelectedTransactions; - selectedTransactionIDs: string[]; - selectedReports: SelectedReports[]; suggestedSearches: Record; - isOnSearch: boolean; - shouldTurnOffSelectionMode: boolean; shouldResetSearchQuery: boolean; - sortedReportIDs: ReadonlyArray; - /** True when at least one transaction is selected. */ - hasSelectedTransactions: boolean; }; -type SearchStateContextValue = SearchContextData & { +type SearchQueryActionsValue = { + setShouldResetSearchQuery: (shouldReset: boolean) => void; +}; + +type SearchResultsContextValue = { currentSearchResults: SearchResults | undefined; /** Whether we're on a main to-do search and should use live Onyx data instead of snapshots */ shouldUseLiveData: boolean; + sortedReportIDs: ReadonlyArray; shouldShowFiltersBarLoading: boolean; lastSearchType: string | undefined; +}; + +type SearchResultsActionsValue = { + setSortedReportIDs: (ids: ReadonlyArray) => void; + setShouldShowFiltersBarLoading: (shouldShow: boolean) => void; + setLastSearchType: (type: string | undefined) => void; +}; + +type SearchSelectionContextValue = { + currentSelectedTransactionReportID: string | undefined; + selectedTransactions: SelectedTransactions; + selectedTransactionIDs: string[]; + selectedReports: SelectedReports[]; + shouldTurnOffSelectionMode: boolean; + /** True when at least one transaction is selected. */ + hasSelectedTransactions: boolean; shouldShowSelectAllMatchingItems: boolean; areAllMatchingItemsSelected: boolean; }; -type SearchActionsContextValue = { +type SearchSelectionActionsValue = { /** * If you want to set `selectedTransactionIDs`, pass an array as the first argument, object/record otherwise. * The optional `data` argument lets callers atomically update `selectedReports` in the same commit @@ -213,14 +224,16 @@ type SearchActionsContextValue = { (clearIDs: true, unused?: undefined): void; }; removeTransaction: (transactionID: string | undefined) => void; - setShouldShowFiltersBarLoading: (shouldShow: boolean) => void; - setLastSearchType: (type: string | undefined) => void; setShouldShowSelectAllMatchingItems: (shouldShow: boolean) => void; selectAllMatchingItems: (on: boolean) => void; - setShouldResetSearchQuery: (shouldReset: boolean) => void; - setSortedReportIDs: (ids: ReadonlyArray) => void; }; +/** Composed value of all three Search state contexts. Kept as a union for callers that need the full bag shape (e.g. test fixtures, action `searchContext` payloads). */ +type SearchStateContextValue = SearchQueryContextValue & SearchResultsContextValue & SearchSelectionContextValue; + +/** Composed value of all three Search actions contexts. See `SearchStateContextValue`. */ +type SearchActionsContextValue = SearchQueryActionsValue & SearchResultsActionsValue & SearchSelectionActionsValue; + type ASTNode = { operator: ValueOf; left: SyntaxFilterKey | ASTNode; @@ -408,7 +421,12 @@ export type { SortOrder, SearchStateContextValue, SearchActionsContextValue, - SearchContextData, + SearchQueryContextValue, + SearchQueryActionsValue, + SearchResultsContextValue, + SearchResultsActionsValue, + SearchSelectionContextValue, + SearchSelectionActionsValue, ASTNode, QueryFilter, QueryFilters, diff --git a/src/hooks/useAllTransactions.ts b/src/hooks/useAllTransactions.ts index 2c385ee1c603..d32b8b584425 100644 --- a/src/hooks/useAllTransactions.ts +++ b/src/hooks/useAllTransactions.ts @@ -1,6 +1,6 @@ import {useMemo} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchResultsContext} from '@components/Search/SearchContext'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Transaction} from '@src/types/onyx'; import useOnyx from './useOnyx'; @@ -9,7 +9,7 @@ import useOnyx from './useOnyx'; * Hook that returns all transactions, filtered by current search results if a search data is available */ function useAllTransactions() { - const {currentSearchResults} = useSearchStateContext(); + const {currentSearchResults} = useSearchResultsContext(); const [allTransactionsCollection] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); const allTransactions = useMemo(() => { diff --git a/src/hooks/useBulkDuplicateAction.ts b/src/hooks/useBulkDuplicateAction.ts index 0cdce2380610..41ebc218e472 100644 --- a/src/hooks/useBulkDuplicateAction.ts +++ b/src/hooks/useBulkDuplicateAction.ts @@ -1,7 +1,7 @@ import {hasSeenTourSelector} from '@selectors/Onboarding'; import {validTransactionDraftsSelector} from '@selectors/TransactionDraft'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {useSearchActionsContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions} from '@components/Search/SearchContext'; import {bulkDuplicateExpenses} from '@libs/actions/IOU/Duplicate'; import {getPolicyExpenseChat} from '@libs/ReportUtils'; import CONST from '@src/CONST'; @@ -27,7 +27,7 @@ type UseBulkDuplicateActionParams = { */ function useBulkDuplicateAction({selectedTransactionsKeys, allTransactions, allReports, searchData, onAfterDuplicate}: UseBulkDuplicateActionParams) { const {accountID, login: currentUserLogin} = useCurrentUserPersonalDetails(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const defaultExpensePolicy = useDefaultExpensePolicy(); const {isBetaEnabled} = usePermissions(); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); diff --git a/src/hooks/useBulkDuplicateReportAction.ts b/src/hooks/useBulkDuplicateReportAction.ts index adc84d88a318..46c88a3203b9 100644 --- a/src/hooks/useBulkDuplicateReportAction.ts +++ b/src/hooks/useBulkDuplicateReportAction.ts @@ -1,7 +1,7 @@ import {hasSeenTourSelector} from '@selectors/Onboarding'; import {validTransactionDraftsSelector} from '@selectors/TransactionDraft'; import type {OnyxCollection} from 'react-native-onyx'; -import {useSearchActionsContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions} from '@components/Search/SearchContext'; import type {SelectedReports} from '@components/Search/types'; import {bulkDuplicateReports} from '@libs/actions/IOU/Duplicate'; import {getPolicyExpenseChat} from '@libs/ReportUtils'; @@ -22,7 +22,7 @@ type UseBulkDuplicateReportActionParams = { function useBulkDuplicateReportAction({selectedReports, allReports, searchData}: UseBulkDuplicateReportActionParams) { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const defaultExpensePolicy = useDefaultExpensePolicy(); const {isBetaEnabled} = usePermissions(); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); diff --git a/src/hooks/useDeleteSavedSearch.tsx b/src/hooks/useDeleteSavedSearch.tsx index 4980ad6c5ed8..06d0037cc146 100644 --- a/src/hooks/useDeleteSavedSearch.tsx +++ b/src/hooks/useDeleteSavedSearch.tsx @@ -1,6 +1,6 @@ import {useCallback} from 'react'; import {ModalActions} from '@components/Modal/Global/ModalContext'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext} from '@components/Search/SearchContext'; import {deleteSavedSearch} from '@libs/actions/Search'; import Navigation from '@libs/Navigation/Navigation'; import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; @@ -10,7 +10,7 @@ import useLocalize from './useLocalize'; export default function useDeleteSavedSearch() { const {translate} = useLocalize(); - const {currentSearchHash} = useSearchStateContext(); + const {currentSearchHash} = useSearchQueryContext(); const {showConfirmModal} = useConfirmModal(); const handleDeleteSavedSearch = useCallback( diff --git a/src/hooks/useDeleteTransactions.ts b/src/hooks/useDeleteTransactions.ts index f0e0e488f468..c931889cf5b5 100644 --- a/src/hooks/useDeleteTransactions.ts +++ b/src/hooks/useDeleteTransactions.ts @@ -1,7 +1,7 @@ import passthroughPolicyTagListSelector from '@selectors/PolicyTagList'; import {useCallback} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchResultsContext} from '@components/Search/SearchContext'; import {deleteMoneyRequest} from '@libs/actions/IOU/DeleteMoneyRequest'; import {getIOUActionForTransactions} from '@libs/actions/IOU/Duplicate'; import {getIOURequestPolicyID} from '@libs/actions/IOU/MoneyRequest'; @@ -60,7 +60,8 @@ function redistributeRemainingPerDiemSplitExpenses(splitExpenses: SplitExpense[] * All data must be provided through function parameters */ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransactionsParams) { - const {currentSearchResults, currentSearchQueryJSON} = useSearchStateContext(); + const {currentSearchResults} = useSearchResultsContext(); + const {currentSearchQueryJSON} = useSearchQueryContext(); const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${getNonEmptyStringOnyxID(report?.policyID)}`); diff --git a/src/hooks/useExpenseActions.ts b/src/hooks/useExpenseActions.ts index 8e33c74effd1..03b5c910cb22 100644 --- a/src/hooks/useExpenseActions.ts +++ b/src/hooks/useExpenseActions.ts @@ -8,7 +8,7 @@ import type {ValueOf} from 'type-fest'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import type {SecondaryActionEntry} from '@components/MoneyReportHeaderActions/types'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchSelectionActions} from '@components/Search/SearchContext'; import {duplicateReport as duplicateReportAction, duplicateExpenseTransaction as duplicateTransactionAction} from '@libs/actions/IOU/Duplicate'; import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction'; import {deleteAppReport} from '@libs/actions/Report'; @@ -89,8 +89,8 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic const {getCurrencyDecimals} = useCurrencyListActions(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {login: currentUserLogin, accountID, email} = currentUserPersonalDetails; - const {currentSearchHash} = useSearchStateContext(); - const {removeTransaction} = useSearchActionsContext(); + const {currentSearchHash} = useSearchQueryContext(); + const {removeTransaction} = useSearchSelectionActions(); // Report data const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); diff --git a/src/hooks/useExportActions.ts b/src/hooks/useExportActions.ts index bada20e5d1ef..a0e0a2dc0e6f 100644 --- a/src/hooks/useExportActions.ts +++ b/src/hooks/useExportActions.ts @@ -3,7 +3,7 @@ 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 {useSearchActionsContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions} from '@components/Search/SearchContext'; import {openOldDotLink} from '@libs/actions/Link'; import {exportReportToCSV, exportReportToPDF, exportToIntegration, markAsManuallyExported} from '@libs/actions/Report'; import {getExportTemplates, queueExportSearchWithTemplate} from '@libs/actions/Search'; @@ -67,7 +67,7 @@ function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsPa const {showConfirmModal} = useConfirmModal(); const {showDecisionModal} = useDecisionModal(); const {triggerExportOrConfirm} = useExportAgainModal(moneyRequestReport?.reportID, moneyRequestReport?.policyID); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const expensifyIcons = useMemoizedLazyExpensifyIcons([ 'Table', diff --git a/src/hooks/useExportedToFilterOptions.ts b/src/hooks/useExportedToFilterOptions.ts index 4c878d37cf50..3e3ed1eb6ecc 100644 --- a/src/hooks/useExportedToFilterOptions.ts +++ b/src/hooks/useExportedToFilterOptions.ts @@ -1,5 +1,5 @@ import type {OnyxCollection} from 'react-native-onyx'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext} from '@components/Search/SearchContext'; import {getStandardExportTemplateDisplayName} from '@libs/AccountingUtils'; import {getExportTemplates} from '@libs/actions/Search'; import {getConnectedIntegrationNamesForPolicies} from '@libs/PolicyUtils'; @@ -40,7 +40,7 @@ function exportedToPoliciesSelector(policies: OnyxCollection): OnyxColle * When currentSearchQueryJSON has policyID, options are scoped to those workspaces so form hydration and autocomplete stay consistent. */ export default function useExportedToFilterOptions(): UseExportedToFilterDataResult { - const {currentSearchQueryJSON} = useSearchStateContext(); + const {currentSearchQueryJSON} = useSearchQueryContext(); const policyIDs = currentSearchQueryJSON?.policyID; const {translate} = useLocalize(); diff --git a/src/hooks/useFilterSelectedTransactions.ts b/src/hooks/useFilterSelectedTransactions.ts index 6330a0c3d1c6..6fa54bda4fa9 100644 --- a/src/hooks/useFilterSelectedTransactions.ts +++ b/src/hooks/useFilterSelectedTransactions.ts @@ -1,5 +1,5 @@ import {useEffect, useMemo} from 'react'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import type {Transaction} from '@src/types/onyx'; /** @@ -10,8 +10,8 @@ import type {Transaction} from '@src/types/onyx'; * @param reportID - The report that owns the current transaction list */ function useFilterSelectedTransactions(transactions: Transaction[], reportID?: string) { - const {selectedTransactionIDs, currentSelectedTransactionReportID} = useSearchStateContext(); - const {setSelectedTransactions} = useSearchActionsContext(); + const {selectedTransactionIDs, currentSelectedTransactionReportID} = useSearchSelectionContext(); + const {setSelectedTransactions} = useSearchSelectionActions(); const transactionIDs = useMemo(() => transactions.map((transaction) => transaction.transactionID), [transactions]); const filteredSelectedTransactionIDs = useMemo(() => selectedTransactionIDs.filter((id) => transactionIDs.includes(id)), [selectedTransactionIDs, transactionIDs]); diff --git a/src/hooks/useLifecycleActions.tsx b/src/hooks/useLifecycleActions.tsx index 8b9d96054818..d53d292ee079 100644 --- a/src/hooks/useLifecycleActions.tsx +++ b/src/hooks/useLifecycleActions.tsx @@ -4,7 +4,7 @@ import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/ import type {ActionHandledType} from '@components/Modal/Global/HoldMenuModalWrapper'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import type {SecondaryActionEntry} from '@components/MoneyReportHeaderActions/types'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchResultsContext, useSearchSelectionActions} from '@components/Search/SearchContext'; import Text from '@components/Text'; import {search} from '@libs/actions/Search'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; @@ -93,8 +93,9 @@ function useLifecycleActions({reportID, startApprovedAnimation, startAnimation, const {isDelegateAccessRestricted} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); - const {currentSearchQueryJSON, currentSearchKey, currentSearchResults} = useSearchStateContext(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {currentSearchQueryJSON, currentSearchKey} = useSearchQueryContext(); + const {currentSearchResults} = useSearchResultsContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.hash, true); const expensifyIcons = useMemoizedLazyExpensifyIcons(['Send', 'ThumbsUp', 'CircularArrowBackwards', 'Clear', 'MoneyBag']); diff --git a/src/hooks/useMergeTransactions.ts b/src/hooks/useMergeTransactions.ts index 440fadbf44f6..091e41e0b287 100644 --- a/src/hooks/useMergeTransactions.ts +++ b/src/hooks/useMergeTransactions.ts @@ -1,5 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchResultsContext} from '@components/Search/SearchContext'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getReportIDForExpense, getTransactionFromMergeTransaction} from '@libs/MergeTransactionUtils'; import {isExpenseUnreported} from '@libs/TransactionUtils'; @@ -56,7 +56,8 @@ function getTransaction( } function useMergeTransactions({mergeTransaction}: UseMergeTransactionsProps): UseMergeTransactionsReturn { - const {currentSearchHash, currentSearchResults} = useSearchStateContext(); + const {currentSearchHash} = useSearchQueryContext(); + const {currentSearchResults} = useSearchResultsContext(); const {policyForMovingExpensesID} = usePolicyForMovingExpenses(); const [onyxTargetTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(mergeTransaction?.targetTransactionID)}`); diff --git a/src/hooks/useOnyx.ts b/src/hooks/useOnyx.ts index d3aafc04753b..9437389ba355 100644 --- a/src/hooks/useOnyx.ts +++ b/src/hooks/useOnyx.ts @@ -3,7 +3,7 @@ import type {DependencyList} from 'react'; // eslint-disable-next-line no-restricted-imports import {useOnyx as originalUseOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry, OnyxKey, OnyxValue, UseOnyxOptions, UseOnyxResult} from 'react-native-onyx'; -import {SearchStateContext} from '@components/Search/SearchContext'; +import {SearchQueryContext, SearchResultsContext} from '@components/Search/SearchContext'; import {useIsOnSearch} from '@components/Search/SearchScopeProvider'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -54,7 +54,8 @@ const useOnyx: OriginalUseOnyx = ) { - const {setSortedReportIDs} = useSearchActionsContext(); + const {setSortedReportIDs} = useSearchResultsActions(); useEffect(() => { // Only expense-report searches produce report-level IDs suitable for navigation arrows. diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 7c08badccd66..da2bfecd441c 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -9,7 +9,7 @@ import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/ import type {PaymentMethodType} from '@components/KYCWall/types'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import type {PopoverMenuItem} from '@components/PopoverMenu'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchResultsContext, useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import type {BulkPaySelectionData, PaymentData, SearchQueryJSON} from '@components/Search/types'; import {unholdRequest} from '@libs/actions/IOU/Hold'; import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction'; @@ -219,8 +219,10 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { const {isOffline} = useNetwork(); const {isDelegateAccessRestricted} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); - const {selectedTransactions, selectedReports, areAllMatchingItemsSelected, currentSearchResults, currentSearchKey} = useSearchStateContext(); - const {clearSelectedTransactions, selectAllMatchingItems} = useSearchActionsContext(); + const {selectedTransactions, selectedReports, areAllMatchingItemsSelected} = useSearchSelectionContext(); + const {currentSearchResults} = useSearchResultsContext(); + const {currentSearchKey} = useSearchQueryContext(); + const {clearSelectedTransactions, selectAllMatchingItems} = useSearchSelectionActions(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {accountID} = currentUserPersonalDetails; const allTransactions = useAllTransactions(); diff --git a/src/hooks/useSearchBulkEditPolicyID.ts b/src/hooks/useSearchBulkEditPolicyID.ts index 8c112083cdca..462ce7fa07a9 100644 --- a/src/hooks/useSearchBulkEditPolicyID.ts +++ b/src/hooks/useSearchBulkEditPolicyID.ts @@ -1,4 +1,4 @@ -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchResultsContext} from '@components/Search/SearchContext'; import {getSearchBulkEditPolicyID} from '@libs/SearchUIUtils'; import {withSnapshotReports, withSnapshotTransactions} from '@pages/Search/SearchEditMultiple/SearchEditMultipleUtils'; import CONST from '@src/CONST'; @@ -11,7 +11,7 @@ import useOnyx from './useOnyx'; * snapshot (e.g. after a hard refresh) are still resolved correctly. */ function useSearchBulkEditPolicyID(): string | undefined { - const {currentSearchResults} = useSearchStateContext(); + const {currentSearchResults} = useSearchResultsContext(); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`); const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); diff --git a/src/hooks/useSearchLoadingState.ts b/src/hooks/useSearchLoadingState.ts index f44a2958c832..b326ac828a72 100644 --- a/src/hooks/useSearchLoadingState.ts +++ b/src/hooks/useSearchLoadingState.ts @@ -1,4 +1,4 @@ -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchResultsContext} from '@components/Search/SearchContext'; import type {SearchQueryJSON} from '@components/Search/types'; import {getValidGroupBy} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; @@ -14,7 +14,7 @@ import useOnyx from './useOnyx'; */ function useSearchLoadingState(queryJSON: SearchQueryJSON | undefined, searchResults: SearchResults | undefined): boolean { const {isOffline} = useNetwork(); - const {shouldUseLiveData} = useSearchStateContext(); + const {shouldUseLiveData} = useSearchResultsContext(); const [, cardFeedsResult] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER); if (shouldUseLiveData || isOffline || !queryJSON) { diff --git a/src/hooks/useSearchPageSetup.ts b/src/hooks/useSearchPageSetup.ts index 38416754e129..ac809e7cc68e 100644 --- a/src/hooks/useSearchPageSetup.ts +++ b/src/hooks/useSearchPageSetup.ts @@ -1,6 +1,6 @@ import {useFocusEffect} from '@react-navigation/native'; import {useEffect} from 'react'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchResultsContext, useSearchSelectionActions} from '@components/Search/SearchContext'; import type {SearchQueryJSON} from '@components/Search/types'; import {saveLastSearchParams} from '@libs/actions/ReportNavigation'; import {openSearch, search} from '@libs/actions/Search'; @@ -25,8 +25,9 @@ let lastSavedSearchHash: number | undefined; function useSearchPageSetup(queryJSON: Readonly | undefined) { const {isOffline} = useNetwork(); const prevIsOffline = usePrevious(isOffline); - const {clearSelectedTransactions} = useSearchActionsContext(); - const {shouldUseLiveData, currentSearchResults, currentSearchKey} = useSearchStateContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); + const {shouldUseLiveData, currentSearchResults} = useSearchResultsContext(); + const {currentSearchKey} = useSearchQueryContext(); const hash = queryJSON?.hash; const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, hash, true); diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 7cb4ea374e48..8b6c10f6fad3 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -3,7 +3,7 @@ import {DeviceEventEmitter} from 'react-native'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import type {PopoverMenuItem} from '@components/PopoverMenu'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import {initBulkEditDraftTransaction} from '@libs/actions/IOU/BulkEdit'; import {unholdRequest} from '@libs/actions/IOU/Hold'; import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction'; @@ -78,8 +78,9 @@ function useSelectedTransactionsActions({ const {isOffline} = useNetworkWithOfflineStatus(); const {isDelegateAccessRestricted} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); - const {selectedTransactionIDs, currentSearchHash, selectedTransactions: selectedTransactionsMeta} = useSearchStateContext(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {selectedTransactionIDs, selectedTransactions: selectedTransactionsMeta} = useSearchSelectionContext(); + const {currentSearchHash} = useSearchQueryContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const allTransactions = useAllTransactions(); const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); diff --git a/src/hooks/useSelectionModeReportActions.ts b/src/hooks/useSelectionModeReportActions.ts index 2776db79c454..d071166295d0 100644 --- a/src/hooks/useSelectionModeReportActions.ts +++ b/src/hooks/useSelectionModeReportActions.ts @@ -11,7 +11,7 @@ import {KYCWallContext} from '@components/KYCWall/KYCWallContext'; import {useLockedAccountActions, useLockedAccountState} from '@components/LockedAccountModalProvider'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import type {ActionHandledType} from '@components/ProcessMoneyReportHoldMenu'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchResultsContext, useSearchSelectionActions} from '@components/Search/SearchContext'; import type {PaymentActionParams} from '@components/SettlementButton/types'; import {payInvoice, payMoneyRequest} from '@libs/actions/IOU/PayMoneyRequest'; import {approveMoneyRequest, canApproveIOU, canIOUBePaid as canIOUBePaidAction, submitReport} from '@libs/actions/IOU/ReportWorkflow'; @@ -95,8 +95,9 @@ function useSelectionModeReportActions({ const {showLockedAccountModal} = useLockedAccountActions(); const kycWallRef = useContext(KYCWallContext); - const {currentSearchQueryJSON, currentSearchKey, currentSearchResults} = useSearchStateContext(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {currentSearchQueryJSON, currentSearchKey} = useSearchQueryContext(); + const {currentSearchResults} = useSearchResultsContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.hash, true); const [session] = useOnyx(ONYXKEYS.SESSION); diff --git a/src/hooks/useTransactionInlineEdit.ts b/src/hooks/useTransactionInlineEdit.ts index 9c2b0c17a190..0f087e3356cb 100644 --- a/src/hooks/useTransactionInlineEdit.ts +++ b/src/hooks/useTransactionInlineEdit.ts @@ -7,7 +7,7 @@ import {useCallback, useRef} from 'react'; // eslint-disable-next-line no-restricted-imports -- Need original useOnyx to avoid reading partial Search snapshot policy data. import {useOnyx as originalUseOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchSelectionContext} from '@components/Search/SearchContext'; import type {TransactionInlineEditParams} from '@libs/actions/TransactionInlineEdit'; import { editTransactionAmountInline, @@ -122,7 +122,7 @@ function useTransactionInlineEdit({transactionID, hash, linkedReportAction}: Use const originalTransactionID = transaction?.comment?.originalTransactionID; const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(originalTransactionID)}`); - const {hasSelectedTransactions} = useSearchStateContext(); + const {hasSelectedTransactions} = useSearchSelectionContext(); const isPerDiem = isPerDiemRequest(transaction); const {shouldSelectPolicy} = usePolicyForMovingExpenses(isPerDiem); diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 91efe6d46387..721f8814fc15 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -13,7 +13,7 @@ import OpenAppFailureModal from '@components/OpenAppFailureModal'; import OptionsListContextProvider from '@components/OptionListContextProvider'; import PriorityModeController from '@components/PriorityModeController'; import {ProductTrainingContextProvider} from '@components/ProductTrainingContext'; -import {SearchContextProvider} from '@components/Search/SearchContext'; +import {SearchContextProvider} from '@components/Search/SearchContextProvider'; import {SearchRouterContextProvider} from '@components/Search/SearchRouter/SearchRouterContext'; import SearchRouterModal from '@components/Search/SearchRouter/SearchRouterModal'; import SupportalPermissionDeniedModal from '@components/SupportalPermissionDeniedModal'; diff --git a/src/pages/NewReportWorkspaceSelectionPage.tsx b/src/pages/NewReportWorkspaceSelectionPage.tsx index 02459541a373..b5a003a22322 100644 --- a/src/pages/NewReportWorkspaceSelectionPage.tsx +++ b/src/pages/NewReportWorkspaceSelectionPage.tsx @@ -5,7 +5,7 @@ import {View} from 'react-native'; import ActivityIndicator from '@components/ActivityIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/ListItem/UserListItem'; import type {ListItem} from '@components/SelectionList/types'; @@ -53,8 +53,8 @@ function NewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelectionPag const {isMovingExpenses, backTo} = route.params ?? {}; const {isOffline} = useNetwork(); const icons = useMemoizedLazyExpensifyIcons(['FallbackWorkspaceAvatar']); - const {selectedTransactions, selectedTransactionIDs} = useSearchStateContext(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {selectedTransactions, selectedTransactionIDs} = useSearchSelectionContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const styles = useThemeStyles(); const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const {translate, localeCompare} = useLocalize(); diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 2b8647a8ecee..9fbdc35dc0c6 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -22,7 +22,7 @@ import ReportActionAvatars from '@components/ReportActionAvatars'; import RoomHeaderAvatars from '@components/RoomHeaderAvatars'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; -import {useSearchActionsContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions} from '@components/Search/SearchContext'; import {SUPER_WIDE_RIGHT_MODALS} from '@components/WideRHPContextProvider/WIDE_RIGHT_MODALS'; import useActivePolicy from '@hooks/useActivePolicy'; import useAncestors from '@hooks/useAncestors'; @@ -197,7 +197,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata, reportLoading const {reportActions} = usePaginatedReportActions(report.reportID); const [reportActionsForOriginalReportID] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`); - const {removeTransaction} = useSearchActionsContext(); + const {removeTransaction} = useSearchSelectionActions(); const transactionThreadReportID = useMemo(() => getOneTransactionThreadReportID(report, chatReport, reportActions ?? [], isOffline), [reportActions, isOffline, report, chatReport]); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth diff --git a/src/pages/Search/SearchAddApproverPage.tsx b/src/pages/Search/SearchAddApproverPage.tsx index a6d63201e227..f9c2b0e7d96a 100644 --- a/src/pages/Search/SearchAddApproverPage.tsx +++ b/src/pages/Search/SearchAddApproverPage.tsx @@ -6,7 +6,7 @@ import type {SelectionListApprover} from '@components/ApproverSelectionList'; import Badge from '@components/Badge'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -33,8 +33,8 @@ function SearchAddApproverPage() { const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const [allReportNextSteps] = useOnyx(ONYXKEYS.COLLECTION.NEXT_STEP); - const {clearSelectedTransactions} = useSearchActionsContext(); - const {selectedReports} = useSearchStateContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); + const {selectedReports} = useSearchSelectionContext(); const [isSaving, setIsSaving] = useState(false); const currentUserDetails = useCurrentUserPersonalDetails(); diff --git a/src/pages/Search/SearchChangeApproverPage.tsx b/src/pages/Search/SearchChangeApproverPage.tsx index 4e459496c190..5e5164386dc8 100644 --- a/src/pages/Search/SearchChangeApproverPage.tsx +++ b/src/pages/Search/SearchChangeApproverPage.tsx @@ -6,7 +6,7 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import RenderHTML from '@components/RenderHTML'; import ScreenWrapper from '@components/ScreenWrapper'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import SelectionList from '@components/SelectionList'; import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; import type {ListItem} from '@components/SelectionList/types'; @@ -76,8 +76,8 @@ function SearchChangeApproverPage() { const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [allReportNextSteps] = useOnyx(ONYXKEYS.COLLECTION.NEXT_STEP); - const {clearSelectedTransactions} = useSearchActionsContext(); - const {selectedReports} = useSearchStateContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); + const {selectedReports} = useSearchSelectionContext(); const [hasLoadedApp] = useOnyx(ONYXKEYS.HAS_LOADED_APP); const [isLoadingBulkChangeApproverPage = true] = useOnyx(ONYXKEYS.IS_LOADING_BULK_CHANGE_APPROVER_PAGE); const {isOffline} = useNetwork(); diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultipleAmountPage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultipleAmountPage.tsx index 4601b679eeb5..4155be944afd 100644 --- a/src/pages/Search/SearchEditMultiple/SearchEditMultipleAmountPage.tsx +++ b/src/pages/Search/SearchEditMultiple/SearchEditMultipleAmountPage.tsx @@ -2,7 +2,7 @@ import {useFocusEffect} from '@react-navigation/native'; import React, {useMemo, useRef, useState} from 'react'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchResultsContext} from '@components/Search/SearchContext'; import isTextInputFocused from '@components/TextInput/BaseTextInput/isTextInputFocused'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useLocalize from '@hooks/useLocalize'; @@ -22,7 +22,7 @@ type CurrentMoney = {amount: string; currency: string}; function SearchEditMultipleAmountPage() { const {translate} = useLocalize(); - const {currentSearchResults} = useSearchStateContext(); + const {currentSearchResults} = useSearchResultsContext(); const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx index 667fcaac1c06..217c5bdf6f99 100644 --- a/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx +++ b/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx @@ -6,7 +6,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchResultsContext, useSearchSelectionActions} from '@components/Search/SearchContext'; import Text from '@components/Text'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -41,8 +41,9 @@ function SearchEditMultiplePage() { const {translate} = useLocalize(); const {convertToDisplayStringWithoutCurrency} = useCurrencyListActions(); const styles = useThemeStyles(); - const {currentSearchHash, currentSearchResults} = useSearchStateContext(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {currentSearchHash} = useSearchQueryContext(); + const {currentSearchResults} = useSearchResultsContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const {login: currentUserLogin, accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const delegateAccountID = useDelegateAccountID(); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); diff --git a/src/pages/Search/SearchHoldReasonPage.tsx b/src/pages/Search/SearchHoldReasonPage.tsx index a314e5045f24..1b9ef4e64c9d 100644 --- a/src/pages/Search/SearchHoldReasonPage.tsx +++ b/src/pages/Search/SearchHoldReasonPage.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useEffect} from 'react'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import useAncestors from '@hooks/useAncestors'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; @@ -25,8 +25,8 @@ type SearchHoldReasonPageProps = function SearchHoldReasonPage({route}: SearchHoldReasonPageProps) { const {translate} = useLocalize(); const {backTo = '', reportID} = route.params ?? {}; - const {selectedTransactionIDs, selectedTransactions} = useSearchStateContext(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {selectedTransactionIDs, selectedTransactions} = useSearchSelectionContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const {accountID: currentUserAccountID, login: currentUserLogin} = useCurrentUserPersonalDetails(); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const {isOffline} = useNetwork(); diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index 6bb002e2480c..92104d1d8c1f 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -8,7 +8,7 @@ import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView import DragAndDropProvider from '@components/DragAndDrop/Provider'; import MoneyRequestReportView from '@components/MoneyRequestReportView/MoneyRequestReportView'; import ScreenWrapper from '@components/ScreenWrapper'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchResultsContext} from '@components/Search/SearchContext'; import useShowSuperWideRHPVersion from '@components/WideRHPContextProvider/useShowSuperWideRHPVersion'; import WideRHPOverlayWrapper from '@components/WideRHPOverlayWrapper'; import useActionListContextValue from '@hooks/useActionListContextValue'; @@ -66,7 +66,7 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); const reportIDFromRoute = getNonEmptyStringOnyxID(route.params?.reportID); - const {currentSearchResults: snapshot} = useSearchStateContext(); + const {currentSearchResults: snapshot} = useSearchResultsContext(); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`); const [deleteTransactionNavigateBackUrl] = useOnyx(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL); diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 1f8516295875..4899045f29df 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1,6 +1,6 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import Animated from 'react-native-reanimated'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchResultsActions, useSearchResultsContext, useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import type {SearchParams} from '@components/Search/types'; import {usePlaybackActionsContext} from '@components/VideoPlayerContexts/PlaybackContext'; import useConfirmReadyToOpenApp from '@hooks/useConfirmReadyToOpenApp'; @@ -34,8 +34,11 @@ function SearchPage({route}: SearchPageProps) { useDocumentTitle(translate('common.spend')); const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); - const {selectedTransactions, lastSearchType, areAllMatchingItemsSelected, currentSearchKey, currentSearchResults, currentSearchQueryJSON, shouldUseLiveData} = useSearchStateContext(); - const {clearSelectedTransactions, setLastSearchType} = useSearchActionsContext(); + const {selectedTransactions, areAllMatchingItemsSelected} = useSearchSelectionContext(); + const {lastSearchType, currentSearchResults, shouldUseLiveData} = useSearchResultsContext(); + const {currentSearchKey, currentSearchQueryJSON} = useSearchQueryContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); + const {setLastSearchType} = useSearchResultsActions(); const isMobileSelectionModeEnabled = useMobileSelectionMode(clearSelectedTransactions); const [hasFilterBars = false] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {selector: hasFilterBarsSelector}); diff --git a/src/pages/Search/SearchPageNarrow/index.tsx b/src/pages/Search/SearchPageNarrow/index.tsx index cecf92411c9a..f68499759e64 100644 --- a/src/pages/Search/SearchPageNarrow/index.tsx +++ b/src/pages/Search/SearchPageNarrow/index.tsx @@ -13,7 +13,7 @@ import ReceiptScanDropZone from '@components/ReceiptScanDropZone'; import ScreenWrapper from '@components/ScreenWrapper'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; import Search from '@components/Search'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchResultsContext, useSearchSelectionActions} from '@components/Search/SearchContext'; import SearchLoadingSkeleton from '@components/Search/SearchLoadingSkeleton'; import SearchPageFooter from '@components/Search/SearchPageFooter'; import SearchPageHeaderNarrow from '@components/Search/SearchPageHeader/SearchPageHeaderNarrow'; @@ -90,8 +90,8 @@ function SearchPageNarrow({ const {windowHeight} = useWindowDimensions(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const {clearSelectedTransactions} = useSearchActionsContext(); - const {shouldUseLiveData} = useSearchStateContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); + const {shouldUseLiveData} = useSearchResultsContext(); const [searchRouterListVisible, setSearchRouterListVisible] = useState(false); const {isOffline} = useNetwork(); diff --git a/src/pages/Search/SearchPageWide.tsx b/src/pages/Search/SearchPageWide.tsx index dca1e2b1a485..48d66fc7ef64 100644 --- a/src/pages/Search/SearchPageWide.tsx +++ b/src/pages/Search/SearchPageWide.tsx @@ -7,7 +7,7 @@ import ReceiptScanDropZone from '@components/ReceiptScanDropZone'; import ScreenWrapper from '@components/ScreenWrapper'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; import Search from '@components/Search'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchResultsContext} from '@components/Search/SearchContext'; import SearchLoadingSkeleton from '@components/Search/SearchLoadingSkeleton'; import SearchPageFooter from '@components/Search/SearchPageFooter'; import SearchActionsBarWide from '@components/Search/SearchPageHeader/SearchActionsBarWide'; @@ -62,7 +62,7 @@ function SearchPageWide({ const shouldShowLoadingSkeleton = useSearchLoadingState(queryJSON, searchResults); const styles = useThemeStyles(); const {isOffline} = useNetwork(); - const {shouldUseLiveData} = useSearchStateContext(); + const {shouldUseLiveData} = useSearchResultsContext(); const {saveScrollOffset} = useContext(ScrollOffsetContext); const receiptDropTargetRef = useRef(null); diff --git a/src/pages/Search/SearchRejectReasonPage.tsx b/src/pages/Search/SearchRejectReasonPage.tsx index ae747afe80fe..7a65a22d1297 100644 --- a/src/pages/Search/SearchRejectReasonPage.tsx +++ b/src/pages/Search/SearchRejectReasonPage.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useEffect, useMemo} from 'react'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -22,8 +22,9 @@ type SearchRejectReasonPageProps = | PlatformStackScreenProps; function SearchRejectReasonPage({route}: SearchRejectReasonPageProps) { - const {selectedTransactionIDs, selectedTransactions, currentSearchHash} = useSearchStateContext(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {selectedTransactionIDs, selectedTransactions} = useSearchSelectionContext(); + const {currentSearchHash} = useSearchQueryContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const {reportID} = route.params ?? {}; const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); diff --git a/src/pages/Search/SearchSavePage.tsx b/src/pages/Search/SearchSavePage.tsx index cb1990a16312..ffa015a7e19f 100644 --- a/src/pages/Search/SearchSavePage.tsx +++ b/src/pages/Search/SearchSavePage.tsx @@ -11,7 +11,7 @@ import useFilterReportValue from '@components/Search/hooks/useFilterReportValue' import useFilterTaxRateValue from '@components/Search/hooks/useFilterTaxRateValue'; import useFilterUserValue from '@components/Search/hooks/useFilterUserValue'; import useFilterWorkspaceValue from '@components/Search/hooks/useFilterWorkspaceValue'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext} from '@components/Search/SearchContext'; import type {SearchQueryJSON} from '@components/Search/types'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; @@ -143,7 +143,7 @@ function SearchSavePage() { const [searchAdvancedFiltersForm = getEmptyObject>()] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); const [name, setName] = useState(''); - const {currentSearchQueryJSON} = useSearchStateContext(); + const {currentSearchQueryJSON} = useSearchQueryContext(); const onSaveSearch = () => { if (!currentSearchQueryJSON) { diff --git a/src/pages/Search/SearchTransactionsChangeReport.tsx b/src/pages/Search/SearchTransactionsChangeReport.tsx index 1b2e0c472f71..6a7afde29cc2 100644 --- a/src/pages/Search/SearchTransactionsChangeReport.tsx +++ b/src/pages/Search/SearchTransactionsChangeReport.tsx @@ -3,7 +3,7 @@ import React, {useMemo} from 'react'; import {InteractionManager} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import {usePersonalDetails, useSession} from '@components/OnyxListItemProvider'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import type {ListItem} from '@components/SelectionList/types'; import useConditionalCreateEmptyReportConfirmation from '@hooks/useConditionalCreateEmptyReportConfirmation'; import useHasPerDiemTransactions from '@hooks/useHasPerDiemTransactions'; @@ -28,8 +28,8 @@ type TransactionGroupListItem = ListItem & { }; function SearchTransactionsChangeReport() { - const {selectedTransactions} = useSearchStateContext(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {selectedTransactions} = useSearchSelectionContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const selectedTransactionsKeys = useMemo(() => Object.keys(selectedTransactions), [selectedTransactions]); const transactions = useMemo( () => diff --git a/src/pages/Search/SearchTypeMenuWide.tsx b/src/pages/Search/SearchTypeMenuWide.tsx index 0eefc65cf3a1..cb8f3c507414 100644 --- a/src/pages/Search/SearchTypeMenuWide.tsx +++ b/src/pages/Search/SearchTypeMenuWide.tsx @@ -5,7 +5,7 @@ import {View} from 'react-native'; import type {NativeScrollEvent, NativeSyntheticEvent, ScrollView as RNScrollView} from 'react-native'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; import ScrollView from '@components/ScrollView'; -import {useSearchActionsContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions} from '@components/Search/SearchContext'; import type {SearchQueryJSON} from '@components/Search/types'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -122,7 +122,7 @@ function SearchTypeMenuWide({queryJSON}: SearchTypeMenuProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {singleExecution} = useSingleExecution(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const {typeMenuSections, activeItemIndex} = useSearchTypeMenuSections({hash, similarSearchHash, sortBy, sortOrder, type}); const [isSearchDataLoaded, isSearchDataLoadedResult] = useOnyx(ONYXKEYS.IS_SEARCH_PAGE_DATA_LOADED); const [reportCounts = CONST.EMPTY_TODOS_REPORT_COUNTS] = useOnyx(ONYXKEYS.DERIVED.TODOS, {selector: todosReportCountsSelector}); diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx index 343cd796f3db..9370aedbcf13 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -8,7 +8,7 @@ import {scheduleOnRN} from 'react-native-worklets'; import {Actions, useActionSheetAwareScrollViewActions} from '@components/ActionSheetAwareScrollView'; import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext} from '@components/Search/SearchContext'; import useAncestors from '@hooks/useAncestors'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDeleteTransactions from '@hooks/useDeleteTransactions'; @@ -339,7 +339,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro const [selfDMReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); - const {currentSearchHash} = useSearchStateContext(); + const {currentSearchHash} = useSearchQueryContext(); const {deleteTransactions} = useDeleteTransactions({ report, reportActions: reportActionRef.current ? [reportActionRef.current] : [], diff --git a/src/pages/inbox/report/ReportActionCompose/useSidePanelContext.ts b/src/pages/inbox/report/ReportActionCompose/useSidePanelContext.ts index 5c94d31bdf26..9bdc7c8205a4 100644 --- a/src/pages/inbox/report/ReportActionCompose/useSidePanelContext.ts +++ b/src/pages/inbox/report/ReportActionCompose/useSidePanelContext.ts @@ -1,5 +1,5 @@ import {useMemo} from 'react'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext, useSearchSelectionContext} from '@components/Search/SearchContext'; import {useCurrentReportIDState} from '@hooks/useCurrentReportID'; import useIsInSidePanel from '@hooks/useIsInSidePanel'; import useOnyx from '@hooks/useOnyx'; @@ -12,7 +12,8 @@ function useSidePanelContext(reportID: string): OnyxTypes.SidePanelContext | und const isInSidePanel = useIsInSidePanel(); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const {currentReportID, currentRHPReportID} = useCurrentReportIDState(); - const {currentSearchQueryJSON, selectedTransactionIDs, selectedTransactions, selectedReports} = useSearchStateContext(); + const {currentSearchQueryJSON} = useSearchQueryContext(); + const {selectedTransactionIDs, selectedTransactions, selectedReports} = useSearchSelectionContext(); return useMemo(() => { if (conciergeReportID !== reportID || !isInSidePanel) { diff --git a/src/pages/iou/RejectReasonPage.tsx b/src/pages/iou/RejectReasonPage.tsx index 60a4f2316d8a..806b9783db74 100644 --- a/src/pages/iou/RejectReasonPage.tsx +++ b/src/pages/iou/RejectReasonPage.tsx @@ -2,7 +2,7 @@ import {getReportPolicyID} from '@selectors/Report'; import React, {useCallback, useEffect} from 'react'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; -import {useSearchActionsContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions} from '@components/Search/SearchContext'; import {useWideRHPState} from '@components/WideRHPContextProvider'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; @@ -29,7 +29,7 @@ function RejectReasonPage({route}: RejectReasonPageProps) { const {translate} = useLocalize(); const {transactionID, reportID, backTo} = route.params; - const {removeTransaction} = useSearchActionsContext(); + const {removeTransaction} = useSearchSelectionActions(); const [reportPolicyID] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportID)}`, {selector: getReportPolicyID}); const policy = usePolicy(reportPolicyID); const {superWideRHPRouteKeys} = useWideRHPState(); diff --git a/src/pages/iou/SplitExpenseCreateDateRagePage.tsx b/src/pages/iou/SplitExpenseCreateDateRagePage.tsx index 756d6e19681b..5a641d01e5ad 100644 --- a/src/pages/iou/SplitExpenseCreateDateRagePage.tsx +++ b/src/pages/iou/SplitExpenseCreateDateRagePage.tsx @@ -8,7 +8,7 @@ import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchResultsContext} from '@components/Search/SearchContext'; import useAllTransactions from '@hooks/useAllTransactions'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; @@ -33,7 +33,7 @@ type SplitExpenseCreateDateRagePageProps = PlatformStackScreenProps(''); - const {currentSearchResults, currentSearchHash, currentSearchQueryJSON} = useSearchStateContext(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {currentSearchResults} = useSearchResultsContext(); + const {currentSearchHash, currentSearchQueryJSON} = useSearchQueryContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const {convertToDisplayString, getCurrencySymbol} = useCurrencyListActions(); diff --git a/src/pages/iou/request/step/IOURequestEditReport.tsx b/src/pages/iou/request/step/IOURequestEditReport.tsx index 09a28958d9bc..7de0c8893d3a 100644 --- a/src/pages/iou/request/step/IOURequestEditReport.tsx +++ b/src/pages/iou/request/step/IOURequestEditReport.tsx @@ -1,7 +1,7 @@ import React, {useMemo} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {usePersonalDetails, useSession} from '@components/OnyxListItemProvider'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import type {ListItem} from '@components/SelectionList/types'; import useConditionalCreateEmptyReportConfirmation from '@hooks/useConditionalCreateEmptyReportConfirmation'; import useHasPerDiemTransactions from '@hooks/useHasPerDiemTransactions'; @@ -33,9 +33,9 @@ type IOURequestEditReportProps = WithWritableReportOrNotFoundProps) => [ { diff --git a/src/pages/iou/request/step/IOURequestStepTag.tsx b/src/pages/iou/request/step/IOURequestStepTag.tsx index 52520ddb7de3..4eb0a99ef9da 100644 --- a/src/pages/iou/request/step/IOURequestStepTag.tsx +++ b/src/pages/iou/request/step/IOURequestStepTag.tsx @@ -2,7 +2,7 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; import Button from '@components/Button'; import FixedFooter from '@components/FixedFooter'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchQueryContext} from '@components/Search/SearchContext'; import TagPicker from '@components/TagPicker'; import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -69,7 +69,7 @@ function IOURequestStepTag({ const styles = useThemeStyles(); const illustrations = useMemoizedLazyIllustrations(['EmptyStateExpenses']); - const {currentSearchHash} = useSearchStateContext(); + const {currentSearchHash} = useSearchQueryContext(); const {translate} = useLocalize(); useRestartOnReceiptFailure(transaction, reportIDFromRoute, iouType, action); diff --git a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx index d8b50ef06a5e..b17563904927 100644 --- a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx +++ b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx @@ -6,7 +6,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; -import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext'; import WorkspaceConfirmationForm from '@components/WorkspaceConfirmationForm'; import type {WorkspaceConfirmationSubmitFunctionParams} from '@components/WorkspaceConfirmationForm'; import useActivePolicy from '@hooks/useActivePolicy'; @@ -80,8 +80,8 @@ function IOURequestStepUpgrade({ const createReportForCurrentUser = useCreateNewReport(); // Hooks for bulk move functionality - const {selectedTransactions} = useSearchStateContext(); - const {clearSelectedTransactions} = useSearchActionsContext(); + const {selectedTransactions} = useSearchSelectionContext(); + const {clearSelectedTransactions} = useSearchSelectionActions(); const selectedTransactionsKeys = useMemo(() => Object.keys(selectedTransactions), [selectedTransactions]); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [allPolicyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES); diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index bd9932fa0249..d58a7a7f5055 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -9,7 +9,7 @@ import {ModalActions} from '@components/Modal/Global/ModalContext'; import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; -import {useSearchActionsContext} from '@components/Search/SearchContext'; +import {useSearchQueryActions} from '@components/Search/SearchContext'; import Section from '@components/Section'; import SectionSubtitleHTML from '@components/SectionSubtitleHTML'; import SentryDebugToolMenu from '@components/SentryDebugToolMenu'; @@ -70,7 +70,7 @@ function TroubleshootPage() { const {showConfirmModal} = useConfirmModal(); const isLoadingTryNewDot = isLoadingOnyxValue(tryNewDotMetadata); const shouldOpenSurveyReasonPage = tryNewDot?.classicRedirect?.dismissed === false; - const {setShouldResetSearchQuery} = useSearchActionsContext(); + const {setShouldResetSearchQuery} = useSearchQueryActions(); const showResetAndRefreshModal = async () => { const result = await showConfirmModal({ title: translate('common.areYouSure'), diff --git a/tests/ui/CategoryListItemHeaderTest.tsx b/tests/ui/CategoryListItemHeaderTest.tsx index 8bf8d877c81b..537756832a41 100644 --- a/tests/ui/CategoryListItemHeaderTest.tsx +++ b/tests/ui/CategoryListItemHeaderTest.tsx @@ -5,13 +5,13 @@ import ComposeProviders from '@components/ComposeProviders'; import {CurrencyListContextProvider} from '@components/CurrencyListContextProvider'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; -import {SearchActionsContext, SearchStateContext} from '@components/Search/SearchContext'; import CategoryListItemHeader from '@components/Search/SearchList/ListItem/CategoryListItemHeader'; import type {TransactionCategoryGroupListItemType} from '@components/Search/SearchList/ListItem/types'; import type {SearchActionsContextValue, SearchColumnType, SearchStateContextValue} from '@components/Search/types'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import MockSearchContextProvider from '../utils/MockSearchContextProvider'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; jest.mock('@components/ConfirmedRoute.tsx'); @@ -31,7 +31,6 @@ const mockSearchStateContext = { selectedReports: [], selectedTransactionIDs: [], selectedTransactions: {}, - isOnSearch: false, shouldTurnOffSelectionMode: false, shouldResetSearchQuery: false, lastSearchType: undefined, @@ -88,21 +87,22 @@ const renderCategoryListItemHeader = ( ) => { return render( - - - - - + + + , ); }; diff --git a/tests/ui/MerchantListItemHeaderTest.tsx b/tests/ui/MerchantListItemHeaderTest.tsx index 57da4ce7ae5d..5570ef4e80dd 100644 --- a/tests/ui/MerchantListItemHeaderTest.tsx +++ b/tests/ui/MerchantListItemHeaderTest.tsx @@ -5,13 +5,13 @@ import ComposeProviders from '@components/ComposeProviders'; import {CurrencyListContextProvider} from '@components/CurrencyListContextProvider'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; -import {SearchActionsContext, SearchStateContext} from '@components/Search/SearchContext'; import MerchantListItemHeader from '@components/Search/SearchList/ListItem/MerchantListItemHeader'; import type {TransactionMerchantGroupListItemType} from '@components/Search/SearchList/ListItem/types'; import type {SearchActionsContextValue, SearchColumnType, SearchStateContextValue} from '@components/Search/types'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import MockSearchContextProvider from '../utils/MockSearchContextProvider'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; jest.mock('@components/ConfirmedRoute.tsx'); @@ -31,7 +31,6 @@ const mockSearchStateContext = { selectedReports: [], selectedTransactionIDs: [], selectedTransactions: {}, - isOnSearch: false, shouldTurnOffSelectionMode: false, shouldResetSearchQuery: false, lastSearchType: undefined, @@ -88,21 +87,22 @@ const renderMerchantListItemHeader = ( ) => { return render( - - - - - + + + , ); }; diff --git a/tests/ui/MoneyRequestReportActionsListRejectModalTest.tsx b/tests/ui/MoneyRequestReportActionsListRejectModalTest.tsx index df6fe51d33d4..f748410b4bc8 100644 --- a/tests/ui/MoneyRequestReportActionsListRejectModalTest.tsx +++ b/tests/ui/MoneyRequestReportActionsListRejectModalTest.tsx @@ -8,7 +8,7 @@ import {LocaleContextProvider} from '@components/LocaleContextProvider'; import MoneyRequestReportActionsList from '@components/MoneyRequestReportView/MoneyRequestReportActionsList'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; import ScreenWrapper from '@components/ScreenWrapper'; -import {SearchContextProvider} from '@components/Search/SearchContext'; +import {SearchContextProvider} from '@components/Search/SearchContextProvider'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, Report, ReportAction, Session, Transaction} from '@src/types/onyx'; diff --git a/tests/ui/MonthListItemHeaderTest.tsx b/tests/ui/MonthListItemHeaderTest.tsx index 10861dd4bf98..2dfd53a02079 100644 --- a/tests/ui/MonthListItemHeaderTest.tsx +++ b/tests/ui/MonthListItemHeaderTest.tsx @@ -5,13 +5,13 @@ import ComposeProviders from '@components/ComposeProviders'; import {CurrencyListContextProvider} from '@components/CurrencyListContextProvider'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; -import {SearchActionsContext, SearchStateContext} from '@components/Search/SearchContext'; import MonthListItemHeader from '@components/Search/SearchList/ListItem/MonthListItemHeader'; import type {TransactionMonthGroupListItemType} from '@components/Search/SearchList/ListItem/types'; import type {SearchActionsContextValue, SearchColumnType, SearchStateContextValue} from '@components/Search/types'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import MockSearchContextProvider from '../utils/MockSearchContextProvider'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; jest.mock('@components/ConfirmedRoute.tsx'); @@ -31,7 +31,6 @@ const mockSearchStateContext = { selectedReports: [], selectedTransactionIDs: [], selectedTransactions: {}, - isOnSearch: false, shouldTurnOffSelectionMode: false, shouldResetSearchQuery: false, lastSearchType: undefined, @@ -90,21 +89,22 @@ const renderMonthListItemHeader = ( ) => { return render( - - - - - + + + , ); }; diff --git a/tests/ui/PureReportActionItemTest.tsx b/tests/ui/PureReportActionItemTest.tsx index 242b61379145..dbb6ec2bcb10 100644 --- a/tests/ui/PureReportActionItemTest.tsx +++ b/tests/ui/PureReportActionItemTest.tsx @@ -14,9 +14,12 @@ import {openLink} from '@libs/actions/Link'; import {setHasRadio} from '@libs/NetworkState'; import Parser from '@libs/Parser'; import {getIOUActionForReportID} from '@libs/ReportActionsUtils'; +import type * as UrlType from '@libs/Url'; import PureReportActionItem from '@pages/inbox/report/PureReportActionItem'; import ReportActionItemMessage from '@pages/inbox/report/ReportActionItemMessage'; import colors from '@styles/theme/colors'; +import type CONFIGType from '@src/CONFIG'; +import type CONSTType from '@src/CONST'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import * as ReportActionUtils from '@src/libs/ReportActionsUtils'; @@ -30,13 +33,46 @@ import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatch jest.mock('@react-navigation/native'); -type LinkModuleMock = {openLink: typeof openLink} & Record; - +// Stub `@libs/actions/Link` without spreading `requireActual` — its transitive Navigation/ReportUtils +// chain races with the lightweight `useOnyx → SearchContext` import path and can hand consumers +// (e.g. ReportActionItemMessageWithExplain) the real `openLink` reference before the mock factory +// finishes wiring up. Reimplement only the URL helpers AnchorRenderer needs for internal-link detection. jest.mock('@libs/actions/Link', () => { - const actual = jest.requireActual('@libs/actions/Link'); + const Url = jest.requireActual('@libs/Url'); + const CONSTreal = jest.requireActual<{default: typeof CONSTType}>('@src/CONST').default; + const CONFIGreal = jest.requireActual<{default: typeof CONFIGType}>('@src/CONFIG').default; return { - ...actual, openLink: jest.fn(), + openOldDotLink: jest.fn(), + openExternalLink: jest.fn(), + openTravelDotLink: jest.fn(), + openReportFromDeepLink: jest.fn(), + getTravelDotLink: jest.fn(), + buildOldDotURL: jest.fn(), + buildTravelDotURL: jest.fn(), + getInternalNewExpensifyPath: (href: string) => { + if (!href) { + return ''; + } + const attrPath = Url.getPathFromURL(href); + return (Url.hasSameExpensifyOrigin(href, CONSTreal.NEW_EXPENSIFY_URL) || + Url.hasSameExpensifyOrigin(href, CONSTreal.STAGING_NEW_EXPENSIFY_URL) || + href.startsWith(CONSTreal.DEV_NEW_EXPENSIFY_URL)) && + !CONSTreal.PATHS_TO_TREAT_AS_EXTERNAL.find((p) => attrPath.startsWith(p)) + ? attrPath + : ''; + }, + getInternalExpensifyPath: (href: string) => { + if (!href) { + return ''; + } + const attrPath = Url.getPathFromURL(href); + const hasExpensifyOrigin = Url.hasSameExpensifyOrigin(href, CONFIGreal.EXPENSIFY.EXPENSIFY_URL) || Url.hasSameExpensifyOrigin(href, CONFIGreal.EXPENSIFY.STAGING_API_ROOT); + if (!hasExpensifyOrigin || attrPath.startsWith(CONFIGreal.EXPENSIFY.CONCIERGE_URL_PATHNAME) || attrPath.startsWith(CONFIGreal.EXPENSIFY.DEVPORTAL_URL_PATHNAME)) { + return ''; + } + return attrPath; + }, }; }); diff --git a/tests/ui/ReportListItemHeaderTest.tsx b/tests/ui/ReportListItemHeaderTest.tsx index d1bfa5f50ee5..173d487dce1c 100644 --- a/tests/ui/ReportListItemHeaderTest.tsx +++ b/tests/ui/ReportListItemHeaderTest.tsx @@ -5,7 +5,6 @@ import type {ValueOf} from 'type-fest'; import ComposeProviders from '@components/ComposeProviders'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; -import {SearchActionsContext, SearchStateContext} from '@components/Search/SearchContext'; import ReportListItemHeader from '@components/Search/SearchList/ListItem/ReportListItemHeader'; import type {TransactionReportGroupListItemType} from '@components/Search/SearchList/ListItem/types'; import type {SearchActionsContextValue, SearchStateContextValue} from '@components/Search/types'; @@ -13,6 +12,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails} from '@src/types/onyx'; import createRandomPolicy from '../utils/collections/policies'; +import MockSearchContextProvider from '../utils/MockSearchContextProvider'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; jest.mock('@components/ConfirmedRoute.tsx'); @@ -25,7 +25,6 @@ const mockSearchStateContext = { selectedReports: [], selectedTransactionIDs: [], selectedTransactions: {}, - isOnSearch: false, shouldTurnOffSelectionMode: false, currentSearchKey: undefined, currentSearchQueryJSON: undefined, @@ -36,18 +35,26 @@ const mockSearchStateContext = { shouldUseLiveData: false, currentSimilarSearchHash: -1, suggestedSearches: {} as SearchStateContextValue['suggestedSearches'], -} satisfies Partial; + lastSearchType: undefined, + areAllMatchingItemsSelected: false, + shouldResetSearchQuery: false, + sortedReportIDs: [], + hasSelectedTransactions: false, +} satisfies SearchStateContextValue; const mockSearchActionsContext = { clearSelectedTransactions: jest.fn(), setLastSearchType: jest.fn(), setCurrentSelectedTransactionReportID: jest.fn(), setSelectedTransactions: jest.fn(), + setSelectedReports: jest.fn(), setShouldShowFiltersBarLoading: jest.fn(), setShouldShowSelectAllMatchingItems: jest.fn(), selectAllMatchingItems: jest.fn(), setShouldResetSearchQuery: jest.fn(), -} satisfies Partial; + removeTransaction: jest.fn(), + setSortedReportIDs: jest.fn(), +} satisfies SearchActionsContextValue; const mockPersonalDetails: Record = { john: { @@ -108,19 +115,18 @@ const createReportListItem = ( const renderReportListItemHeader = (reportItem: TransactionReportGroupListItemType) => { return render( - {/* @ts-expect-error - Disable TypeScript errors to simplify the test */} - - {/* @ts-expect-error - Disable TypeScript errors to simplify the test */} - - - - + + + , ); }; diff --git a/tests/ui/SearchPageTest.tsx b/tests/ui/SearchPageTest.tsx index 0bdf8ca02c61..2088fb0329df 100644 --- a/tests/ui/SearchPageTest.tsx +++ b/tests/ui/SearchPageTest.tsx @@ -8,7 +8,7 @@ import ComposeProviders from '@components/ComposeProviders'; import FullScreenBlockingViewContextProvider from '@components/FullScreenBlockingViewContextProvider'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; -import {SearchContextProvider} from '@components/Search/SearchContext'; +import {SearchContextProvider} from '@components/Search/SearchContextProvider'; import {PlaybackContextProvider} from '@components/VideoPlayerContexts/PlaybackContext'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import createRootStackNavigator from '@libs/Navigation/AppNavigator/createRootStackNavigator'; diff --git a/tests/ui/WeekListItemHeaderTest.tsx b/tests/ui/WeekListItemHeaderTest.tsx index 45aa50514735..61d49be98cfc 100644 --- a/tests/ui/WeekListItemHeaderTest.tsx +++ b/tests/ui/WeekListItemHeaderTest.tsx @@ -5,13 +5,13 @@ import ComposeProviders from '@components/ComposeProviders'; import {CurrencyListContextProvider} from '@components/CurrencyListContextProvider'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; -import {SearchActionsContext, SearchStateContext} from '@components/Search/SearchContext'; import type {TransactionWeekGroupListItemType} from '@components/Search/SearchList/ListItem/types'; import WeekListItemHeader from '@components/Search/SearchList/ListItem/WeekListItemHeader'; import type {SearchActionsContextValue, SearchColumnType, SearchStateContextValue} from '@components/Search/types'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import MockSearchContextProvider from '../utils/MockSearchContextProvider'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; jest.mock('@components/ConfirmedRoute.tsx'); @@ -30,7 +30,6 @@ const mockSearchStateContext = { selectedReports: [], selectedTransactionIDs: [], selectedTransactions: {}, - isOnSearch: false, shouldTurnOffSelectionMode: false, shouldResetSearchQuery: false, lastSearchType: undefined, @@ -87,21 +86,22 @@ const renderWeekListItemHeader = ( ) => { return render( - - - - - + + + , ); }; diff --git a/tests/ui/YearListItemHeaderTest.tsx b/tests/ui/YearListItemHeaderTest.tsx index ad76a74f5ee9..01002a3a493e 100644 --- a/tests/ui/YearListItemHeaderTest.tsx +++ b/tests/ui/YearListItemHeaderTest.tsx @@ -5,13 +5,13 @@ import ComposeProviders from '@components/ComposeProviders'; import {CurrencyListContextProvider} from '@components/CurrencyListContextProvider'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; -import {SearchActionsContext, SearchStateContext} from '@components/Search/SearchContext'; import type {TransactionYearGroupListItemType} from '@components/Search/SearchList/ListItem/types'; import YearListItemHeader from '@components/Search/SearchList/ListItem/YearListItemHeader'; import type {SearchActionsContextValue, SearchColumnType, SearchStateContextValue} from '@components/Search/types'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import MockSearchContextProvider from '../utils/MockSearchContextProvider'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; jest.mock('@components/ConfirmedRoute.tsx'); @@ -31,7 +31,6 @@ const mockSearchStateContext = { selectedReports: [], selectedTransactionIDs: [], selectedTransactions: {}, - isOnSearch: false, shouldTurnOffSelectionMode: false, shouldResetSearchQuery: false, lastSearchType: undefined, @@ -89,21 +88,22 @@ const renderYearListItemHeader = ( ) => { return render( - - - - - + + + , ); }; diff --git a/tests/unit/Search/useSyncSelectedReportsTest.tsx b/tests/unit/Search/useSyncSelectedReportsTest.tsx index 3926bbd3ac23..caba72c67da7 100644 --- a/tests/unit/Search/useSyncSelectedReportsTest.tsx +++ b/tests/unit/Search/useSyncSelectedReportsTest.tsx @@ -1,35 +1,24 @@ import {act, render} from '@testing-library/react-native'; import React, {useEffect, useMemo, useState} from 'react'; -import {SearchActionsContext, SearchStateContext, useSyncSelectedReports} from '@components/Search/SearchContext'; +import {SearchSelectionActionsContext, SearchSelectionContext} from '@components/Search/SearchContext'; +import {useSyncSelectedReports} from '@components/Search/SearchContextProvider'; import type {TransactionListItemType, TransactionReportGroupListItemType} from '@components/Search/SearchList/ListItem/types'; -import type {SearchActionsContextValue, SearchStateContextValue, SelectedReports, SelectedTransactions} from '@components/Search/types'; +import type {SearchSelectionActionsValue, SearchSelectionContextValue, SelectedReports, SelectedTransactions} from '@components/Search/types'; import CONST from '@src/CONST'; type HookData = TransactionListItemType[] | TransactionReportGroupListItemType[]; const createSetSelectedReportsMock = () => jest.fn(); -const baseStateContext = { - currentSearchKey: undefined, - currentSearchQueryJSON: undefined, - currentSearchResults: undefined, +const baseSelectionContext = { currentSelectedTransactionReportID: undefined, selectedTransactionIDs: [], selectedReports: [], - isOnSearch: false, shouldTurnOffSelectionMode: false, - shouldResetSearchQuery: false, hasSelectedTransactions: false, - currentSearchHash: -1, - currentSimilarSearchHash: -1, - suggestedSearches: {} as SearchStateContextValue['suggestedSearches'], - sortedReportIDs: CONST.EMPTY_ARRAY, - lastSearchType: undefined, areAllMatchingItemsSelected: false, shouldShowSelectAllMatchingItems: false, - shouldShowFiltersBarLoading: false, - shouldUseLiveData: false, -} satisfies Omit; +} satisfies Omit; function buildTransactionItem(overrides: Partial & {keyForList: string; transactionID: string}) { return { @@ -78,7 +67,7 @@ function renderHarness({ }: { initialSelected: SelectedTransactions; initialData: HookData; - setSelectedReports: SearchActionsContextValue['setSelectedReports']; + setSelectedReports: SearchSelectionActionsValue['setSelectedReports']; }): HarnessHandle { const handle: HarnessHandle = { setSelected: () => {}, @@ -102,31 +91,27 @@ function renderHarness({ onReady({setSelected, setData}); }, [onReady]); - const stateValue = useMemo(() => ({...baseStateContext, selectedTransactions: selected}), [selected]); + const selectionValue = useMemo(() => ({...baseSelectionContext, selectedTransactions: selected}), [selected]); - const actionsValue = useMemo( + const selectionActionsValue = useMemo( () => ({ - setLastSearchType: () => {}, setCurrentSelectedTransactionReportID: () => {}, setSelectedTransactions: () => {}, setSelectedReports, removeTransaction: () => {}, clearSelectedTransactions: () => {}, - setShouldShowFiltersBarLoading: () => {}, setShouldShowSelectAllMatchingItems: () => {}, selectAllMatchingItems: () => {}, - setShouldResetSearchQuery: () => {}, - setSortedReportIDs: () => {}, }), [], ); return ( - - + + - - + + ); } diff --git a/tests/unit/TransactionGroupListItemTest.tsx b/tests/unit/TransactionGroupListItemTest.tsx index 2d725911a41c..3cc1c51f20a8 100644 --- a/tests/unit/TransactionGroupListItemTest.tsx +++ b/tests/unit/TransactionGroupListItemTest.tsx @@ -7,7 +7,7 @@ import ComposeProviders from '@components/ComposeProviders'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; import ScreenWrapper from '@components/ScreenWrapper'; -import {SearchContextProvider} from '@components/Search/SearchContext'; +import {SearchContextProvider} from '@components/Search/SearchContextProvider'; import type {TransactionGroupListItemProps, TransactionListItemType, TransactionReportGroupListItemType} from '@components/Search/SearchList/ListItem/types'; import TransactionGroupListItem from '@src/components/Search/SearchList/ListItem/TransactionGroupListItem'; import CONST from '@src/CONST'; diff --git a/tests/unit/components/MoneyRequestReportNavigation.test.tsx b/tests/unit/components/MoneyRequestReportNavigation.test.tsx index 1b2475d95681..867f24f26acb 100644 --- a/tests/unit/components/MoneyRequestReportNavigation.test.tsx +++ b/tests/unit/components/MoneyRequestReportNavigation.test.tsx @@ -1,5 +1,5 @@ import {renderHook} from '@testing-library/react-native'; -import {useSearchStateContext} from '@components/Search/SearchContext'; +import {useSearchResultsContext} from '@components/Search/SearchContext'; import useFilterPendingDeleteReports from '@hooks/useFilterPendingDeleteReports'; import CONST from '@src/CONST'; @@ -12,7 +12,7 @@ import CONST from '@src/CONST'; let mockSortedReportIDs: ReadonlyArray = CONST.EMPTY_ARRAY; jest.mock('@components/Search/SearchContext', () => ({ - useSearchStateContext: () => ({sortedReportIDs: mockSortedReportIDs}), + useSearchResultsContext: () => ({sortedReportIDs: mockSortedReportIDs}), })); const mockUseOnyx = jest.fn(); @@ -49,7 +49,7 @@ describe('MoneyRequestReportNavigation', () => { // Simulate what the wrapper does: reads context + filter const {result} = renderHook(() => { - const {sortedReportIDs} = useSearchStateContext(); + const {sortedReportIDs} = useSearchResultsContext(); const allReports = useFilterPendingDeleteReports(sortedReportIDs); return {sortedReportIDs, allReports}; }); @@ -66,7 +66,7 @@ describe('MoneyRequestReportNavigation', () => { mockUseOnyx.mockReturnValue([undefined]); const {result} = renderHook(() => { - const {sortedReportIDs} = useSearchStateContext(); + const {sortedReportIDs} = useSearchResultsContext(); const allReports = useFilterPendingDeleteReports(sortedReportIDs); const isSearchLoading = false; return allReports.length > 0 && !isSearchLoading ? 'fast' : 'full'; @@ -80,7 +80,7 @@ describe('MoneyRequestReportNavigation', () => { mockSortedReportIDs = CONST.EMPTY_ARRAY; const {result} = renderHook(() => { - const {sortedReportIDs} = useSearchStateContext(); + const {sortedReportIDs} = useSearchResultsContext(); const allReports = useFilterPendingDeleteReports(sortedReportIDs); return allReports.length > 0 ? 'fast' : 'full'; }); @@ -93,7 +93,7 @@ describe('MoneyRequestReportNavigation', () => { const isSearchLoading = true; const {result} = renderHook(() => { - const {sortedReportIDs} = useSearchStateContext(); + const {sortedReportIDs} = useSearchResultsContext(); const allReports = useFilterPendingDeleteReports(sortedReportIDs); return allReports.length > 0 && !isSearchLoading ? 'fast' : 'full'; }); diff --git a/tests/unit/hooks/useAllTransactions.test.ts b/tests/unit/hooks/useAllTransactions.test.ts index 2d3c0d27dafd..aeb6324b795d 100644 --- a/tests/unit/hooks/useAllTransactions.test.ts +++ b/tests/unit/hooks/useAllTransactions.test.ts @@ -9,7 +9,7 @@ import createRandomTransaction from '../../utils/collections/transaction'; let mockCurrentSearchResults: SearchResults | undefined; jest.mock('@components/Search/SearchContext', () => ({ - useSearchStateContext: () => ({ + useSearchResultsContext: () => ({ currentSearchResults: mockCurrentSearchResults, }), })); diff --git a/tests/unit/hooks/useBulkDuplicateReportActionTest.ts b/tests/unit/hooks/useBulkDuplicateReportActionTest.ts index e92aa9c48e13..75d0bb05b292 100644 --- a/tests/unit/hooks/useBulkDuplicateReportActionTest.ts +++ b/tests/unit/hooks/useBulkDuplicateReportActionTest.ts @@ -43,13 +43,15 @@ jest.mock('@hooks/useDefaultExpensePolicy', () => ({ const mockClearSelectedTransactions = jest.fn(); jest.mock('@components/Search/SearchContext', () => ({ - useSearchStateContext: () => ({ + useSearchSelectionContext: () => ({ selectedTransactions: {}, selectedReports: [], areAllMatchingItemsSelected: false, + }), + useSearchResultsContext: () => ({ currentSearchResults: undefined, }), - useSearchActionsContext: () => ({ + useSearchSelectionActions: () => ({ clearSelectedTransactions: mockClearSelectedTransactions, selectAllMatchingItems: jest.fn(), }), diff --git a/tests/unit/hooks/useFilterSelectedTransactionsTest.ts b/tests/unit/hooks/useFilterSelectedTransactionsTest.ts index 3e9fc9cb3773..352f4f4d42c9 100644 --- a/tests/unit/hooks/useFilterSelectedTransactionsTest.ts +++ b/tests/unit/hooks/useFilterSelectedTransactionsTest.ts @@ -8,12 +8,11 @@ let mockSelectedTransactionIDs: string[] = []; const mockSetSelectedTransactions = jest.fn(); jest.mock('@components/Search/SearchContext', () => ({ - useSearchStateContext: () => ({ + useSearchSelectionContext: () => ({ selectedTransactionIDs: mockSelectedTransactionIDs, selectedTransactions: {}, - currentSearchHash: 12345, }), - useSearchActionsContext: () => ({ + useSearchSelectionActions: () => ({ setSelectedTransactions: mockSetSelectedTransactions, clearSelectedTransactions: jest.fn(), }), diff --git a/tests/unit/hooks/useSaveSortedReportIDs.test.ts b/tests/unit/hooks/useSaveSortedReportIDs.test.ts index 46f0b72744c1..42dab4d89379 100644 --- a/tests/unit/hooks/useSaveSortedReportIDs.test.ts +++ b/tests/unit/hooks/useSaveSortedReportIDs.test.ts @@ -6,7 +6,7 @@ import CONST from '@src/CONST'; const mockSetSortedReportIDs = jest.fn(); jest.mock('@components/Search/SearchContext', () => ({ - useSearchActionsContext: () => ({setSortedReportIDs: mockSetSortedReportIDs}), + useSearchResultsActions: () => ({setSortedReportIDs: mockSetSortedReportIDs}), })); describe('useSaveSortedReportIDs', () => { diff --git a/tests/unit/hooks/useSearchBulkActionsDownloadPDFTest.ts b/tests/unit/hooks/useSearchBulkActionsDownloadPDFTest.ts index 4036c3edfb57..01523a336ebf 100644 --- a/tests/unit/hooks/useSearchBulkActionsDownloadPDFTest.ts +++ b/tests/unit/hooks/useSearchBulkActionsDownloadPDFTest.ts @@ -28,13 +28,18 @@ let mockSelectedTransactions: SelectedTransactions = {}; let mockSelectedReports: SelectedReports[] = []; jest.mock('@components/Search/SearchContext', () => ({ - useSearchStateContext: () => ({ + useSearchSelectionContext: () => ({ selectedTransactions: mockSelectedTransactions, selectedReports: mockSelectedReports, areAllMatchingItemsSelected: false, + }), + useSearchResultsContext: () => ({ currentSearchResults: undefined, }), - useSearchActionsContext: () => ({ + useSearchQueryContext: () => ({ + currentSearchKey: undefined, + }), + useSearchSelectionActions: () => ({ clearSelectedTransactions: mockClearSelectedTransactions, selectAllMatchingItems: jest.fn(), }), diff --git a/tests/unit/hooks/useSearchBulkActionsDuplicateTest.ts b/tests/unit/hooks/useSearchBulkActionsDuplicateTest.ts index e6bb1d9bdd99..d541587e20db 100644 --- a/tests/unit/hooks/useSearchBulkActionsDuplicateTest.ts +++ b/tests/unit/hooks/useSearchBulkActionsDuplicateTest.ts @@ -132,13 +132,18 @@ let mockSelectedReports: SelectedReports[] = []; let mockAreAllMatchingItemsSelected = false; jest.mock('@components/Search/SearchContext', () => ({ - useSearchStateContext: () => ({ + useSearchSelectionContext: () => ({ selectedTransactions: mockSelectedTransactions, selectedReports: mockSelectedReports, areAllMatchingItemsSelected: mockAreAllMatchingItemsSelected, + }), + useSearchResultsContext: () => ({ currentSearchResults: undefined, }), - useSearchActionsContext: () => ({ + useSearchQueryContext: () => ({ + currentSearchKey: undefined, + }), + useSearchSelectionActions: () => ({ clearSelectedTransactions: mockClearSelectedTransactions, selectAllMatchingItems: mockSelectAllMatchingItems, }), diff --git a/tests/unit/hooks/useSelectedTransactionsActions.test.ts b/tests/unit/hooks/useSelectedTransactionsActions.test.ts index becea1f113a8..3ffdf3d643da 100644 --- a/tests/unit/hooks/useSelectedTransactionsActions.test.ts +++ b/tests/unit/hooks/useSelectedTransactionsActions.test.ts @@ -71,12 +71,13 @@ const mockSelectedTransactions: SelectedTransactions = {}; const mockCurrentSearchHash = 12345; jest.mock('@components/Search/SearchContext', () => ({ - useSearchStateContext: () => ({ + useSearchQueryContext: () => ({currentSearchHash: mockCurrentSearchHash}), + useSearchResultsContext: () => ({currentSearchResults: undefined}), + useSearchSelectionContext: () => ({ selectedTransactionIDs: mockSelectedTransactionIDs, - currentSearchHash: mockCurrentSearchHash, selectedTransactions: mockSelectedTransactions, }), - useSearchActionsContext: () => ({ + useSearchSelectionActions: () => ({ clearSelectedTransactions: mockClearSelectedTransactions, }), })); diff --git a/tests/unit/hooks/useSelectionModeReportActions.test.ts b/tests/unit/hooks/useSelectionModeReportActions.test.ts index 942648ff7391..f229db78fb16 100644 --- a/tests/unit/hooks/useSelectionModeReportActions.test.ts +++ b/tests/unit/hooks/useSelectionModeReportActions.test.ts @@ -138,13 +138,17 @@ jest.mock('@components/KYCWall/KYCWallContext', () => ({ jest.mock('@components/Search/SearchContext', () => ({ __esModule: true, - useSearchStateContext: jest.fn(() => ({ + useSearchQueryContext: jest.fn(() => ({ currentSearchQueryJSON: null, currentSearchKey: '', + })), + useSearchResultsContext: jest.fn(() => ({ currentSearchResults: null, + })), + useSearchSelectionContext: jest.fn(() => ({ selectedTransactionIDs: [], })), - useSearchActionsContext: jest.fn(() => ({ + useSearchSelectionActions: jest.fn(() => ({ clearSelectedTransactions: mockClearSelectedTransactions, setSelectedTransactions: jest.fn(), })), diff --git a/tests/unit/hooks/useSidePanelContext.test.ts b/tests/unit/hooks/useSidePanelContext.test.ts index 4609bbf9c69a..a32b85431408 100644 --- a/tests/unit/hooks/useSidePanelContext.test.ts +++ b/tests/unit/hooks/useSidePanelContext.test.ts @@ -35,7 +35,12 @@ jest.mock('@hooks/useCurrentReportID', () => ({ })); jest.mock('@components/Search/SearchContext', () => ({ - useSearchStateContext: () => mockSearchState, + useSearchQueryContext: () => ({currentSearchQueryJSON: mockSearchState.currentSearchQueryJSON}), + useSearchSelectionContext: () => ({ + selectedTransactionIDs: mockSearchState.selectedTransactionIDs, + selectedTransactions: mockSearchState.selectedTransactions, + selectedReports: mockSearchState.selectedReports, + }), })); function resetMocks() { diff --git a/tests/utils/MockSearchContextProvider.tsx b/tests/utils/MockSearchContextProvider.tsx new file mode 100644 index 000000000000..e96b1c7d7e68 --- /dev/null +++ b/tests/utils/MockSearchContextProvider.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { + SearchQueryActionsContext, + SearchQueryContext, + SearchResultsActionsContext, + SearchResultsContext, + SearchSelectionActionsContext, + SearchSelectionContext, +} from '@components/Search/SearchContext'; +import type { + SearchActionsContextValue, + SearchQueryActionsValue, + SearchQueryContextValue, + SearchResultsActionsValue, + SearchResultsContextValue, + SearchSelectionActionsValue, + SearchSelectionContextValue, + SearchStateContextValue, +} from '@components/Search/types'; + +type MockSearchContextProviderProps = { + state: SearchStateContextValue; + actions: SearchActionsContextValue; + children: React.ReactNode; +}; + +function splitState(value: SearchStateContextValue): { + query: SearchQueryContextValue; + results: SearchResultsContextValue; + selection: SearchSelectionContextValue; +} { + return { + query: { + currentSearchHash: value.currentSearchHash, + currentSimilarSearchHash: value.currentSimilarSearchHash, + currentSearchKey: value.currentSearchKey, + currentSearchQueryJSON: value.currentSearchQueryJSON, + suggestedSearches: value.suggestedSearches, + shouldResetSearchQuery: value.shouldResetSearchQuery, + }, + results: { + currentSearchResults: value.currentSearchResults, + shouldUseLiveData: value.shouldUseLiveData, + sortedReportIDs: value.sortedReportIDs, + shouldShowFiltersBarLoading: value.shouldShowFiltersBarLoading, + lastSearchType: value.lastSearchType, + }, + selection: { + selectedTransactions: value.selectedTransactions, + selectedTransactionIDs: value.selectedTransactionIDs, + selectedReports: value.selectedReports, + currentSelectedTransactionReportID: value.currentSelectedTransactionReportID, + shouldTurnOffSelectionMode: value.shouldTurnOffSelectionMode, + hasSelectedTransactions: value.hasSelectedTransactions, + shouldShowSelectAllMatchingItems: value.shouldShowSelectAllMatchingItems, + areAllMatchingItemsSelected: value.areAllMatchingItemsSelected, + }, + }; +} + +function splitActions(value: SearchActionsContextValue): { + query: SearchQueryActionsValue; + results: SearchResultsActionsValue; + selection: SearchSelectionActionsValue; +} { + return { + query: {setShouldResetSearchQuery: value.setShouldResetSearchQuery}, + results: { + setSortedReportIDs: value.setSortedReportIDs, + setShouldShowFiltersBarLoading: value.setShouldShowFiltersBarLoading, + setLastSearchType: value.setLastSearchType, + }, + selection: { + setSelectedTransactions: value.setSelectedTransactions, + setSelectedReports: value.setSelectedReports, + setCurrentSelectedTransactionReportID: value.setCurrentSelectedTransactionReportID, + clearSelectedTransactions: value.clearSelectedTransactions, + removeTransaction: value.removeTransaction, + setShouldShowSelectAllMatchingItems: value.setShouldShowSelectAllMatchingItems, + selectAllMatchingItems: value.selectAllMatchingItems, + }, + }; +} + +function MockSearchContextProvider({state, actions, children}: MockSearchContextProviderProps) { + const stateSlices = splitState(state); + const actionsSlices = splitActions(actions); + return ( + + + + + + {children} + + + + + + ); +} + +export default MockSearchContextProvider;