diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 2e08aed3250f..e590c50cd5cc 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -163,7 +163,7 @@ import type {PopoverMenuItem} from './PopoverMenu'; import {PressableWithFeedback} from './Pressable'; import type {ActionHandledType} from './ProcessMoneyReportHoldMenu'; import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu'; -import {useSearchContext} from './Search/SearchContext'; +import {useSearchActionsContext, useSearchStateContext} from './Search/SearchContext'; import AnimatedSettlementButton from './SettlementButton/AnimatedSettlementButton'; import Text from './Text'; @@ -429,7 +429,8 @@ function MoneyReportHeader({ typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.HOLD | typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT | typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT_BULK > | null>(null); - const {selectedTransactionIDs, removeTransaction, clearSelectedTransactions, currentSearchQueryJSON, currentSearchKey, currentSearchHash, currentSearchResults} = useSearchContext(); + const {selectedTransactionIDs, currentSearchQueryJSON, currentSearchKey, currentSearchHash, currentSearchResults} = useSearchStateContext(); + const {removeTransaction, clearSelectedTransactions} = useSearchActionsContext(); const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.hash, true); const [network] = useOnyx(ONYXKEYS.NETWORK); diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 6c707cc27414..cf5262a08d7b 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -84,7 +84,7 @@ import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusB import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; import MoneyRequestReportTransactionsNavigation from './MoneyRequestReportView/MoneyRequestReportTransactionsNavigation'; import {usePersonalDetails} from './OnyxListItemProvider'; -import {useSearchContext} from './Search/SearchContext'; +import {useSearchActionsContext, useSearchStateContext} from './Search/SearchContext'; import {useWideRHPState} from './WideRHPContextProvider'; type MoneyRequestHeaderProps = { @@ -158,7 +158,8 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const isOnHold = isOnHoldTransactionUtils(transaction); const isDuplicate = isDuplicateTransactionUtils(transaction, email ?? '', accountID, report, policy, transactionViolations); const reportID = report?.reportID; - const {removeTransaction, currentSearchHash} = useSearchContext(); + const {currentSearchHash} = useSearchStateContext(); + const {removeTransaction} = useSearchActionsContext(); const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction); const [cardList] = useOnyx(ONYXKEYS.CARD_LIST); const {deleteTransactions} = useDeleteTransactions({report: parentReport, reportActions: parentReportAction ? [parentReportAction] : [], policy}); diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index 92330bb39e53..7b041e708a12 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -20,7 +20,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; import {PressableWithFeedback} from '@components/Pressable'; import ScrollView from '@components/ScrollView'; -import {useSearchContext} from '@components/Search/SearchContext'; +import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; import Text from '@components/Text'; import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -197,7 +197,8 @@ function MoneyRequestReportActionsList({ const [enableScrollToEnd, setEnableScrollToEnd] = useState(false); const [lastActionEventId, setLastActionEventId] = useState(''); - const {selectedTransactionIDs, setSelectedTransactions, clearSelectedTransactions} = useSearchContext(); + const {selectedTransactionIDs} = useSearchStateContext(); + const {setSelectedTransactions, clearSelectedTransactions} = useSearchActionsContext(); useFilterSelectedTransactions(transactions); diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index ee3b436d8e54..f584a0309385 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -8,7 +8,7 @@ import Checkbox from '@components/Checkbox'; import MenuItem from '@components/MenuItem'; import Modal from '@components/Modal'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {useSearchContext} from '@components/Search/SearchContext'; +import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; import type {SortOrder} from '@components/Search/types'; import Text from '@components/Text'; import {useWideRHPActions} from '@components/WideRHPContextProvider'; @@ -193,7 +193,8 @@ function MoneyRequestReportTransactionList({ return hasPendingDeletionTransaction || transactions.some(getTransactionPendingAction); }, [hasPendingDeletionTransaction, transactions]); - const {selectedTransactionIDs, setSelectedTransactions, clearSelectedTransactions} = useSearchContext(); + const {selectedTransactionIDs} = useSearchStateContext(); + const {setSelectedTransactions, clearSelectedTransactions} = useSearchActionsContext(); useHandleSelectionMode(selectedTransactionIDs); const isMobileSelectionModeEnabled = useMobileSelectionMode(); diff --git a/src/components/Navigation/SearchSidebar.tsx b/src/components/Navigation/SearchSidebar.tsx index 21570acb8c5c..8b3f14f7e499 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 {useSearchContext} from '@components/Search/SearchContext'; +import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -27,7 +27,8 @@ function SearchSidebar({state}: SearchSidebarProps) { const route = state.routes.at(-1); const params = route?.params as SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.ROOT] | undefined; - const {lastSearchType, setLastSearchType, currentSearchResults} = useSearchContext(); + const {lastSearchType, currentSearchResults} = useSearchStateContext(); + const {setLastSearchType} = useSearchActionsContext(); const queryJSON = params?.q ? buildSearchQueryJSON(params.q, params.rawQuery) : undefined; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 4a2b4545cee0..14ba2034efa0 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -10,7 +10,7 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePolicyCategories, usePolicyTags} from '@components/OnyxListItemProvider'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; -import {useSearchContext} from '@components/Search/SearchContext'; +import {useSearchStateContext} from '@components/Search/SearchContext'; import Switch from '@components/Switch'; import Text from '@components/Text'; import ViolationMessages from '@components/ViolationMessages'; @@ -177,8 +177,7 @@ function MoneyRequestView({ const {getReportRHPActiveRoute} = useActiveRoute(); const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH); - const {currentSearchResults} = useSearchContext(); - + const {currentSearchResults} = useSearchStateContext(); 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/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 38348701ccc5..c392aed7c72b 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useContext, useMemo, useRef, useState} from 'react'; +import React, {useContext, useLayoutEffect, 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'; @@ -14,7 +14,7 @@ import type {SearchResults} from '@src/types/onyx'; import type {SearchResultsInfo} from '@src/types/onyx/SearchResults'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {SearchContextData, SearchContextProps, SearchQueryJSON, SelectedTransactions} from './types'; +import type {SearchActionsContextValue, SearchContextData, SearchQueryJSON, SearchStateContextValue, SelectedTransactions} from './types'; // 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 @@ -44,14 +44,17 @@ const defaultSearchContextData: SearchContextData = { shouldResetSearchQuery: false, }; -const defaultSearchContext: SearchContextProps = { +const defaultSearchStateContext: SearchStateContextValue = { ...defaultSearchContextData, lastSearchType: undefined, areAllMatchingItemsSelected: false, - showSelectAllMatchingItems: false, + shouldShowSelectAllMatchingItems: false, shouldShowFiltersBarLoading: false, currentSearchResults: undefined, shouldUseLiveData: false, +}; + +const defaultSearchActionsContext: SearchActionsContextValue = { setLastSearchType: () => {}, setCurrentSearchHashAndKey: () => {}, setCurrentSearchQueryJSON: () => {}, @@ -59,15 +62,16 @@ const defaultSearchContext: SearchContextProps = { removeTransaction: () => {}, clearSelectedTransactions: () => {}, setShouldShowFiltersBarLoading: () => {}, - shouldShowSelectAllMatchingItems: () => {}, + setShouldShowSelectAllMatchingItems: () => {}, selectAllMatchingItems: () => {}, setShouldResetSearchQuery: () => {}, }; -const SearchContext = React.createContext(defaultSearchContext); +const SearchStateContext = React.createContext(defaultSearchStateContext); +const SearchActionsContext = React.createContext(defaultSearchActionsContext); function SearchContextProvider({children}: ChildrenProps) { - const [showSelectAllMatchingItems, shouldShowSelectAllMatchingItems] = useState(false); + const [shouldShowSelectAllMatchingItems, setShouldShowSelectAllMatchingItems] = useState(false); const [areAllMatchingItemsSelected, selectAllMatchingItems] = useState(false); const [shouldShowFiltersBarLoading, setShouldShowFiltersBarLoading] = useState(false); const [lastSearchType, setLastSearchType] = useState(undefined); @@ -76,20 +80,22 @@ function SearchContextProvider({children}: ChildrenProps) { // Use a ref to access searchContextData in callbacks without causing callback reference changes const searchContextDataRef = useRef(searchContextData); - searchContextDataRef.current = searchContextData; + + useLayoutEffect(() => { + searchContextDataRef.current = searchContextData; + }, [searchContextData]); const [snapshotSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchContextData.currentSearchHash}`); const todoSearchResultsData = useTodos(); - const currentSearchKey = searchContextData.currentSearchKey; const currentRecentSearchHash = searchContextData.currentRecentSearchHash; const {accountID} = useCurrentUserPersonalDetails(); - const suggestedSearches = useMemo(() => getSuggestedSearches(accountID), [accountID]); + const suggestedSearches = getSuggestedSearches(accountID); 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. - const currentSearchResults = useMemo((): SearchResults | undefined => { + function getCurrentSearchResults(): SearchResults | undefined { if (shouldUseLiveData) { const liveData = todoSearchResultsData[currentSearchKey as keyof typeof todoSearchResultsData]; const searchInfo: SearchResultsInfo = { @@ -108,9 +114,11 @@ function SearchContextProvider({children}: ChildrenProps) { } return snapshotSearchResults ?? undefined; - }, [shouldUseLiveData, currentSearchKey, todoSearchResultsData, snapshotSearchResults]); + } + + const currentSearchResults = getCurrentSearchResults(); - const setCurrentSearchHashAndKey = useCallback((searchHash: number, recentHash: number, searchKey: SearchKey | undefined) => { + const setCurrentSearchHashAndKey = (searchHash: number, recentHash: number, searchKey: SearchKey | undefined) => { setSearchContextData((prevState) => { if (searchHash === prevState.currentSearchHash && recentHash === prevState.currentRecentSearchHash && searchKey === prevState.currentSearchKey) { return prevState; @@ -123,9 +131,9 @@ function SearchContextProvider({children}: ChildrenProps) { currentSearchKey: searchKey, }; }); - }, []); + }; - const setCurrentSearchQueryJSON = useCallback((searchQueryJSON: SearchQueryJSON | undefined) => { + const setCurrentSearchQueryJSON = (searchQueryJSON: SearchQueryJSON | undefined) => { setSearchContextData((prevState) => { if (searchQueryJSON === prevState.currentSearchQueryJSON) { return prevState; @@ -136,9 +144,9 @@ function SearchContextProvider({children}: ChildrenProps) { currentSearchQueryJSON: searchQueryJSON, }; }); - }, []); + }; - const setSelectedTransactions: SearchContextProps['setSelectedTransactions'] = useCallback((selectedTransactions, data = []) => { + const setSelectedTransactions: SearchActionsContextValue['setSelectedTransactions'] = (selectedTransactions, data = []) => { if (selectedTransactions instanceof Array) { if (!selectedTransactions.length && areTransactionsEmpty.current) { areTransactionsEmpty.current = true; @@ -152,7 +160,7 @@ function SearchContextProvider({children}: ChildrenProps) { } // When selecting transactions, we also need to manage the reports to which these transactions belong. This is done to ensure proper exporting to CSV. - let selectedReports: SearchContextProps['selectedReports'] = []; + let selectedReports: SearchStateContextValue['selectedReports'] = []; if (data.length && data.every(isTransactionReportGroupListItemType)) { selectedReports = data @@ -199,116 +207,100 @@ function SearchContextProvider({children}: ChildrenProps) { shouldTurnOffSelectionMode: false, selectedReports, })); - }, []); + }; - const clearSelectedTransactions: SearchContextProps['clearSelectedTransactions'] = useCallback( - (searchHashOrClearIDsFlag, shouldTurnOffSelectionMode = false) => { - if (typeof searchHashOrClearIDsFlag === 'boolean') { - setSelectedTransactions([]); - return; - } + const clearSelectedTransactions: SearchActionsContextValue['clearSelectedTransactions'] = (searchHashOrClearIDsFlag, shouldTurnOffSelectionMode = false) => { + if (typeof searchHashOrClearIDsFlag === 'boolean') { + setSelectedTransactions([]); + return; + } - const data = searchContextDataRef.current; + const data = searchContextDataRef.current; - if (searchHashOrClearIDsFlag === data.currentSearchHash) { - return; - } + if (searchHashOrClearIDsFlag === data.currentSearchHash) { + return; + } - if (data.selectedReports.length === 0 && isEmptyObject(data.selectedTransactions) && !data.shouldTurnOffSelectionMode) { - return; - } - setSearchContextData((prevState) => ({ - ...prevState, - shouldTurnOffSelectionMode, - selectedTransactions: {}, - selectedReports: [], - })); + if (data.selectedReports.length === 0 && isEmptyObject(data.selectedTransactions) && !data.shouldTurnOffSelectionMode) { + return; + } + setSearchContextData((prevState) => ({ + ...prevState, + shouldTurnOffSelectionMode, + selectedTransactions: {}, + selectedReports: [], + })); - // Unselect all transactions and hide the "select all matching items" option - shouldShowSelectAllMatchingItems(false); - selectAllMatchingItems(false); - }, - [setSelectedTransactions], - ); + // Unselect all transactions and hide the "select all matching items" option + setShouldShowSelectAllMatchingItems(false); + selectAllMatchingItems(false); + }; - const removeTransaction: SearchContextProps['removeTransaction'] = useCallback( - (transactionID) => { - if (!transactionID) { - return; - } - const selectedTransactionIDs = searchContextData.selectedTransactionIDs; + const {selectedTransactionIDs, selectedTransactions} = searchContextData; - if (!isEmptyObject(searchContextData.selectedTransactions)) { - const newSelectedTransactions = Object.entries(searchContextData.selectedTransactions).reduce((acc, [key, value]) => { - if (key === transactionID) { - return acc; - } - acc[key] = value; + const removeTransaction: SearchActionsContextValue['removeTransaction'] = (transactionID) => { + if (!transactionID) { + return; + } + + if (!isEmptyObject(selectedTransactions)) { + const newSelectedTransactions = Object.entries(selectedTransactions).reduce((acc, [key, value]) => { + if (key === transactionID) { return acc; - }, {} as SelectedTransactions); + } + acc[key] = value; + return acc; + }, {} as SelectedTransactions); - setSearchContextData((prevState) => ({ - ...prevState, - selectedTransactions: newSelectedTransactions, - })); - } + setSearchContextData((prevState) => ({ + ...prevState, + selectedTransactions: newSelectedTransactions, + })); + } - if (selectedTransactionIDs.length > 0) { - setSearchContextData((prevState) => ({ - ...prevState, - selectedTransactionIDs: selectedTransactionIDs.filter((ID) => transactionID !== ID), - })); - } - }, - [searchContextData.selectedTransactionIDs, searchContextData.selectedTransactions], - ); + if (selectedTransactionIDs.length > 0) { + setSearchContextData((prevState) => ({ + ...prevState, + selectedTransactionIDs: selectedTransactionIDs.filter((ID) => transactionID !== ID), + })); + } + }; - const setShouldResetSearchQuery = useCallback((shouldReset: boolean) => { + const setShouldResetSearchQuery = (shouldReset: boolean) => { setSearchContextData((prevState) => ({ ...prevState, shouldResetSearchQuery: shouldReset, })); - }, []); - - const searchContext = useMemo( - () => ({ - ...searchContextData, - currentSearchResults, - shouldUseLiveData, - removeTransaction, - setCurrentSearchHashAndKey, - setCurrentSearchQueryJSON, - setSelectedTransactions, - clearSelectedTransactions, - shouldShowFiltersBarLoading, - setShouldShowFiltersBarLoading, - lastSearchType, - setLastSearchType, - showSelectAllMatchingItems, - shouldShowSelectAllMatchingItems, - areAllMatchingItemsSelected, - selectAllMatchingItems, - setShouldResetSearchQuery, - }), - [ - searchContextData, - currentSearchResults, - shouldUseLiveData, - removeTransaction, - setCurrentSearchHashAndKey, - setCurrentSearchQueryJSON, - setSelectedTransactions, - clearSelectedTransactions, - shouldShowFiltersBarLoading, - lastSearchType, - shouldShowSelectAllMatchingItems, - showSelectAllMatchingItems, - areAllMatchingItemsSelected, - setShouldResetSearchQuery, - ], + }; + + const searchStateContextValue: SearchStateContextValue = { + ...searchContextData, + currentSearchResults, + shouldUseLiveData, + shouldShowFiltersBarLoading, + lastSearchType, + shouldShowSelectAllMatchingItems, + areAllMatchingItemsSelected, + }; + + const searchActionsContextValue: SearchActionsContextValue = { + removeTransaction, + setCurrentSearchHashAndKey, + setCurrentSearchQueryJSON, + setSelectedTransactions, + clearSelectedTransactions, + setShouldShowFiltersBarLoading, + setLastSearchType, + setShouldShowSelectAllMatchingItems, + selectAllMatchingItems, + setShouldResetSearchQuery, + }; + + return ( + + {children} + ); - - return {children}; } /** @@ -316,8 +308,12 @@ function SearchContextProvider({children}: ChildrenProps) { * Setting or clearing one of them does not influence the other. * IDs should be used if transaction details are not required. */ -function useSearchContext() { - return useContext(SearchContext); +function useSearchStateContext() { + return useContext(SearchStateContext); +} + +function useSearchActionsContext() { + return useContext(SearchActionsContext); } -export {SearchContextProvider, useSearchContext, SearchContext}; +export {SearchContextProvider, useSearchStateContext, useSearchActionsContext, SearchStateContext, SearchActionsContext}; diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx index 2b92fec0def6..b188ec04c850 100644 --- a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx @@ -20,7 +20,7 @@ import type {MultiSelectItem} from '@components/Search/FilterDropdowns/MultiSele import MultiSelectPopup from '@components/Search/FilterDropdowns/MultiSelectPopup'; import SingleSelectPopup from '@components/Search/FilterDropdowns/SingleSelectPopup'; import UserSelectPopup from '@components/Search/FilterDropdowns/UserSelectPopup'; -import {useSearchContext} from '@components/Search/SearchContext'; +import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; import type {BankAccountMenuItem, SearchDateFilterKeys, SearchQueryJSON, SingularSearchStatus} from '@components/Search/types'; import SearchFiltersSkeleton from '@components/Skeletons/SearchFiltersSkeleton'; import useAdvancedSearchFilters from '@hooks/useAdvancedSearchFilters'; @@ -161,7 +161,8 @@ function SearchFiltersBar({ const personalDetails = usePersonalDetails(); const filterFormValues = useFilterFormValues(queryJSON); const {shouldUseNarrowLayout, isLargeScreenWidth} = useResponsiveLayout(); - const {selectedTransactions, selectAllMatchingItems, areAllMatchingItemsSelected, showSelectAllMatchingItems, shouldShowFiltersBarLoading, currentSearchResults} = useSearchContext(); + const {selectedTransactions, areAllMatchingItemsSelected, shouldShowSelectAllMatchingItems, shouldShowFiltersBarLoading, currentSearchResults} = useSearchStateContext(); + const {selectAllMatchingItems} = useSearchActionsContext(); const {currencyList} = useCurrencyListState(); const {getCurrencySymbol} = useCurrencyListActions(); @@ -820,7 +821,7 @@ function SearchFiltersBar({ }} sentryLabel={CONST.SENTRY_LABEL.SEARCH.BULK_ACTIONS_DROPDOWN} /> - {!areAllMatchingItemsSelected && showSelectAllMatchingItems && ( + {!areAllMatchingItemsSelected && shouldShowSelectAllMatchingItems && (