diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 649ec251a738..45362c5568d7 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -399,9 +399,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} = useSearchContext(); + const {selectedTransactionIDs, removeTransaction, clearSelectedTransactions, currentSearchQueryJSON, currentSearchKey, currentSearchHash, currentSearchResults} = useSearchContext(); const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.similarSearchHash, true); - const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON?.hash}`, {canBeMissing: true}); const [network] = useOnyx(ONYXKEYS.NETWORK, {canBeMissing: true}); diff --git a/src/components/Navigation/SearchSidebar.tsx b/src/components/Navigation/SearchSidebar.tsx index bf9bd4d44505..f7e3bcde883b 100644 --- a/src/components/Navigation/SearchSidebar.tsx +++ b/src/components/Navigation/SearchSidebar.tsx @@ -1,19 +1,15 @@ import type {ParamListBase} from '@react-navigation/native'; -import {searchResultsSelector} from '@selectors/Snapshot'; import React, {useEffect, useMemo} from 'react'; import {View} from 'react-native'; import {useSearchContext} from '@components/Search/SearchContext'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import type {PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; import {buildSearchQueryJSON} from '@libs/SearchQueryUtils'; import SearchTypeMenu from '@pages/Search/SearchTypeMenu'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; import NavigationTabBar from './NavigationTabBar'; import NAVIGATION_TABS from './NavigationTabBar/NAVIGATION_TABS'; @@ -31,7 +27,7 @@ function SearchSidebar({state}: SearchSidebarProps) { const route = state.routes.at(-1); const params = route?.params as SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.ROOT] | undefined; - const {lastSearchType, setLastSearchType} = useSearchContext(); + const {lastSearchType, setLastSearchType, currentSearchResults} = useSearchContext(); const queryJSON = useMemo(() => { if (!params?.q) { @@ -41,21 +37,15 @@ function SearchSidebar({state}: SearchSidebarProps) { return buildSearchQueryJSON(params.q, params.rawQuery); }, [params?.q, params?.rawQuery]); - const currentSearchResultsKey = queryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; - const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchResultsKey}`, { - canBeMissing: true, - selector: searchResultsSelector, - }); - useEffect(() => { - if (!currentSearchResults?.type) { + if (!currentSearchResults?.search?.type) { return; } - setLastSearchType(currentSearchResults.type); - }, [lastSearchType, queryJSON, setLastSearchType, currentSearchResults?.type]); + setLastSearchType(currentSearchResults.search.type); + }, [lastSearchType, queryJSON, setLastSearchType, currentSearchResults?.search?.type]); - const shouldShowLoadingState = route?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT ? false : !isOffline && !!currentSearchResults?.isLoading; + const shouldShowLoadingState = route?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT ? false : !isOffline && !!currentSearchResults?.search?.isLoading; if (shouldUseNarrowLayout) { return null; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index ad4dbf6317d2..a28f245497e6 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -172,9 +172,7 @@ function MoneyRequestView({ const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false}); - const searchContext = useSearchContext(); - const searchHash = searchContext?.currentSearchHash ?? CONST.DEFAULT_NUMBER_ID; - const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`, {canBeMissing: true}); + const {currentSearchResults} = useSearchContext(); // When this component is used when merging from the search page, we might not have the parent report stored in the main collection let [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`, {canBeMissing: true}); diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 35c0c48a8837..4e227884e892 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -1,8 +1,10 @@ import React, {useCallback, useContext, useMemo, useRef, useState} from 'react'; +import useOnyx from '@hooks/useOnyx'; import {isMoneyRequestReport} from '@libs/ReportUtils'; import {isTransactionListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils'; import type {SearchKey} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {SearchContextData, SearchContextProps, SearchQueryJSON, SelectedTransactions} from './types'; @@ -11,6 +13,7 @@ const defaultSearchContextData: SearchContextData = { currentSearchHash: -1, currentSearchKey: undefined, currentSearchQueryJSON: undefined, + currentSearchResults: undefined, selectedTransactions: {}, selectedTransactionIDs: [], selectedReports: [], @@ -25,6 +28,7 @@ const defaultSearchContext: SearchContextProps = { areAllMatchingItemsSelected: false, showSelectAllMatchingItems: false, shouldShowFiltersBarLoading: false, + currentSearchResults: undefined, setLastSearchType: () => {}, setCurrentSearchHashAndKey: () => {}, setCurrentSearchQueryJSON: () => {}, @@ -47,6 +51,8 @@ function SearchContextProvider({children}: ChildrenProps) { const [searchContextData, setSearchContextData] = useState(defaultSearchContextData); const areTransactionsEmpty = useRef(true); + const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchContextData.currentSearchHash}`, {canBeMissing: true}); + const setCurrentSearchHashAndKey = useCallback((searchHash: number, searchKey: SearchKey | undefined) => { setSearchContextData((prevState) => { if (searchHash === prevState.currentSearchHash && searchKey === prevState.currentSearchKey) { @@ -200,6 +206,7 @@ function SearchContextProvider({children}: ChildrenProps) { const searchContext = useMemo( () => ({ ...searchContextData, + currentSearchResults, removeTransaction, setCurrentSearchHashAndKey, setCurrentSearchQueryJSON, @@ -217,6 +224,7 @@ function SearchContextProvider({children}: ChildrenProps) { }), [ searchContextData, + currentSearchResults, removeTransaction, setCurrentSearchHashAndKey, setCurrentSearchQueryJSON, diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx index 4f5fbc652ced..9aaf3d633d5a 100644 --- a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx @@ -1,6 +1,5 @@ import {isUserValidatedSelector} from '@selectors/Account'; import {emailSelector} from '@selectors/Session'; -import {searchResultsErrorSelector} from '@selectors/Snapshot'; import React, {useCallback, useContext, useMemo, useRef} from 'react'; import type {ReactNode} from 'react'; import {FlatList, View} from 'react-native'; @@ -90,7 +89,7 @@ function SearchFiltersBar({ const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isUserValidatedSelector, canBeMissing: true}); const [searchAdvancedFiltersForm = getEmptyObject>()] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true}); // type, groupBy and status values are not guaranteed to respect the ts type as they come from user input - const {hash, type: unsafeType, groupBy: unsafeGroupBy, status: unsafeStatus, flatFilters} = queryJSON; + const {type: unsafeType, groupBy: unsafeGroupBy, status: unsafeStatus, flatFilters} = queryJSON; const [selectedIOUReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${currentSelectedReportID}`, {canBeMissing: true}); const isCurrentSelectedExpenseReport = isExpenseReport(currentSelectedReportID); const theme = useTheme(); @@ -102,7 +101,7 @@ function SearchFiltersBar({ const personalDetails = usePersonalDetails(); const filterFormValues = useFilterFormValues(queryJSON); const {shouldUseNarrowLayout, isLargeScreenWidth} = useResponsiveLayout(); - const {selectedTransactions, selectAllMatchingItems, areAllMatchingItemsSelected, showSelectAllMatchingItems, shouldShowFiltersBarLoading} = useSearchContext(); + const {selectedTransactions, selectAllMatchingItems, areAllMatchingItemsSelected, showSelectAllMatchingItems, shouldShowFiltersBarLoading, currentSearchResults} = useSearchContext(); const [email] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true, selector: emailSelector}); const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true}); @@ -111,7 +110,6 @@ function SearchFiltersBar({ const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true}); const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: true}); const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext); - const [searchResultsErrors] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, {canBeMissing: true, selector: searchResultsErrorSelector}); const expensifyIcons = useMemoizedLazyExpensifyIcons(['Filter', 'Columns']); const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); @@ -156,7 +154,7 @@ function SearchFiltersBar({ return workspaceOptions.filter((option) => normalizedIDs.includes(option.value)); }, [searchAdvancedFiltersForm.policyID, queryJSON.policyID, workspaceOptions]); - const hasErrors = Object.keys(searchResultsErrors ?? {}).length > 0 && !isOffline; + const hasErrors = Object.keys(currentSearchResults?.errors ?? {}).length > 0 && !isOffline; const shouldShowSelectedDropdown = headerButtonsOptions.length > 0 && (!shouldUseNarrowLayout || isMobileSelectionModeEnabled); const [typeOptions, type] = useMemo(() => { diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index cc1fddf94507..e18b9707d7e3 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -3,7 +3,7 @@ import type {PaymentMethod} from '@components/KYCWall/types'; import type {ReportActionListItemType, TaskListItemType, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionListWithSections/types'; import type {SearchKey} from '@libs/SearchUIUtils'; import type CONST from '@src/CONST'; -import type {ReportAction} from '@src/types/onyx'; +import type {ReportAction, SearchResults} from '@src/types/onyx'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -125,6 +125,7 @@ type SearchContextData = { currentSearchHash: number; currentSearchKey: SearchKey | undefined; currentSearchQueryJSON: SearchQueryJSON | undefined; + currentSearchResults: SearchResults | undefined; selectedTransactions: SelectedTransactions; selectedTransactionIDs: string[]; selectedReports: SelectedReports[]; @@ -134,6 +135,7 @@ type SearchContextData = { }; type SearchContextProps = SearchContextData & { + currentSearchResults: SearchResults | undefined; setCurrentSearchHashAndKey: (hash: number, key: SearchKey | undefined) => void; setCurrentSearchQueryJSON: (searchQueryJSON: SearchQueryJSON | undefined) => void; /** If you want to set `selectedTransactionIDs`, pass an array as the first argument, object/record otherwise */ diff --git a/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx b/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx index 0242a3aad707..5d7dbe67bb81 100644 --- a/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx +++ b/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx @@ -41,22 +41,26 @@ function ExpenseReportListItem({ const theme = useTheme(); const {translate} = useLocalize(); const {isLargeScreenWidth} = useResponsiveLayout(); - const {currentSearchHash, currentSearchKey} = useSearchContext(); + const {currentSearchHash, currentSearchKey, currentSearchResults} = useSearchContext(); const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true}); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true}); - const [snapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`, {canBeMissing: true}); const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportItem.reportID}`, {canBeMissing: true, selector: isActionLoadingSelector}); const expensifyIcons = useMemoizedLazyExpensifyIcons(['DotIndicator']); - const snapshotData = snapshot?.data; + const searchData = currentSearchResults?.data; const snapshotReport = useMemo(() => { - return (snapshotData?.[`${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`] ?? {}) as Report; - }, [snapshotData, reportItem.reportID]); + return (searchData?.[`${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`] ?? {}) as Report; + }, [searchData, reportItem.reportID]); const snapshotPolicy = useMemo(() => { - return (snapshotData?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as Policy; - }, [snapshotData, reportItem.policyID]); + return (searchData?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as Policy; + }, [searchData, reportItem.policyID]); + + const reportActions = useMemo(() => { + const actionsData = searchData?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportItem.reportID}`]; + return actionsData ? Object.values(actionsData) : []; + }, [searchData, reportItem.reportID]); const isDisabledCheckbox = useMemo(() => { const isEmpty = reportItem.transactions.length === 0; @@ -183,10 +187,10 @@ function ExpenseReportListItem({ {(hovered) => ( {}, onButtonPress = () => {}, isActionLoading, @@ -182,10 +182,7 @@ function ExpenseReportListItemRow({ ), [CONST.SEARCH.TABLE_COLUMNS.EXPORTED_TO]: ( - + ), [CONST.SEARCH.TABLE_COLUMNS.ACTION]: ( diff --git a/src/components/SelectionListWithSections/Search/ExportedIconCell.tsx b/src/components/SelectionListWithSections/Search/ExportedIconCell.tsx index 7e9a49c8f5e0..559ec617a5c2 100644 --- a/src/components/SelectionListWithSections/Search/ExportedIconCell.tsx +++ b/src/components/SelectionListWithSections/Search/ExportedIconCell.tsx @@ -3,38 +3,21 @@ import {View} from 'react-native'; import Avatar from '@components/Avatar'; import Icon from '@components/Icon'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useOnyx from '@hooks/useOnyx'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {getOriginalMessage, isExportedToIntegrationAction} from '@libs/ReportActionsUtils'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportAction} from '@src/types/onyx'; type ExportedIconCellProps = { - reportID?: string; - hash?: number; + reportActions?: ReportAction[]; }; -function ExportedIconCell({reportID, hash}: ExportedIconCellProps) { +function ExportedIconCell({reportActions}: ExportedIconCellProps) { const theme = useTheme(); const styles = useThemeStyles(); - // We need to subscribe directly to the snapshot to get the report actions because this can be rendered in either a group - // list (which has a separate hash than the current top-level search query) or in the top-level search query. - // This selector is specific to this edge-case (and thus is not in the selectors folder) and should be used in other places where the snapshot needs to be accessed - // eslint-disable-next-line rulesdir/no-inline-useOnyx-selector - const reportActions = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, { - canBeMissing: true, - selector: (snapshot) => { - return Object.entries(snapshot?.data ?? {}) - .filter(([key]) => key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`) - .map(([, value]) => Object.values(value ?? {}) as ReportAction[]) - .flat(); - }, - }); - - const actions = Object.values(reportActions[0] ?? {}); + const actions = reportActions ?? []; const icons = useMemoizedLazyExpensifyIcons(['NetSuiteSquare', 'XeroSquare', 'IntacctSquare', 'QBOSquare', 'Table', 'ZenefitsSquare', 'BillComSquare', 'CertiniaSquare']); let isExportedToCsv = false; diff --git a/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx b/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx index e013f656fe85..9119f45c9a21 100644 --- a/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx +++ b/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx @@ -211,13 +211,12 @@ function ReportListItemHeader({ const StyleUtils = useStyleUtils(); const styles = useThemeStyles(); const theme = useTheme(); - const {currentSearchHash, currentSearchKey} = useSearchContext(); + const {currentSearchHash, currentSearchKey, currentSearchResults: snapshot} = useSearchContext(); const {isLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true}); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true}); const thereIsFromAndTo = !!reportItem?.from && !!reportItem?.to; const showUserInfo = (reportItem.type === CONST.REPORT.TYPE.IOU && thereIsFromAndTo) || (reportItem.type === CONST.REPORT.TYPE.EXPENSE && !!reportItem?.from); - const [snapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`, {canBeMissing: true}); const snapshotReport = useMemo(() => { return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`] ?? {}) as Report; }, [snapshot, reportItem.reportID]); diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx index 58fadb2469f4..afbe31305f33 100644 --- a/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx @@ -188,9 +188,10 @@ function TransactionGroupListExpanded({ )} {visibleTransactions.map((transaction, index) => { const shouldShowBottomBorder = !isLastTransaction(index) && !isLargeScreenWidth; + const exportedReportActions = Object.values(transactionsSnapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transaction?.reportID}`] ?? {}); + const transactionRow = ( ({ shouldShowBottomBorder={shouldShowBottomBorder} onArrowRightPress={() => openReportInRHP(transaction)} shouldShowArrowRightOnNarrowLayout + reportActions={exportedReportActions} /> ); return ( diff --git a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx index 7ed0619ac150..7568e399d04e 100644 --- a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx @@ -55,11 +55,10 @@ function TransactionListItem({ const theme = useTheme(); const {isLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); - const {currentSearchHash, currentSearchKey} = useSearchContext(); - const [snapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`, {canBeMissing: true}); + const {currentSearchHash, currentSearchKey, currentSearchResults} = useSearchContext(); const snapshotReport = useMemo(() => { - return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`] ?? {}) as Report; - }, [snapshot, transactionItem.reportID]); + return (currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`] ?? {}) as Report; + }, [currentSearchResults, transactionItem.reportID]); const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${transactionItem.reportID}`, {canBeMissing: true, selector: isActionLoadingSelector}); @@ -76,8 +75,14 @@ function TransactionListItem({ selector: (policy) => policy?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`], }); const snapshotPolicy = useMemo(() => { - return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? {}) as Policy; - }, [snapshot, policyID]); + return (currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] ?? {}) as Policy; + }, [currentSearchResults, transactionItem.policyID]); + + const exportedReportActions = useMemo(() => { + const actionsData = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionItem.reportID}`]; + return actionsData ? Object.values(actionsData) : []; + }, [currentSearchResults, transactionItem.reportID]); + // Fetch policy categories directly from Onyx since they are not included in the search snapshot const [policyCategories] = originalUseOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${getNonEmptyStringOnyxID(policyID)}`, {canBeMissing: true}); const [lastPaymentMethod] = useOnyx(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, {canBeMissing: true}); @@ -247,7 +252,6 @@ function TransactionListItem({ /> )} ({ onArrowRightPress={onPress} isHover={hovered} customCardNames={customCardNames} + reportActions={exportedReportActions} /> )} diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 3ba3302ec51c..a04076a51874 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -44,7 +44,7 @@ import { } from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; -import type {PersonalDetails, Policy, Report, TransactionViolation} from '@src/types/onyx'; +import type {PersonalDetails, Policy, Report, ReportAction, TransactionViolation} from '@src/types/onyx'; import type {SearchTransactionAction} from '@src/types/onyx/SearchResults'; import CategoryCell from './DataCells/CategoryCell'; import ChatBubbleCell from './DataCells/ChatBubbleCell'; @@ -101,7 +101,6 @@ type TransactionWithOptionalSearchFields = TransactionWithOptionalHighlight & { }; type TransactionItemRowProps = { - hash?: number; transactionItem: TransactionWithOptionalSearchFields; report?: Report; shouldUseNarrowLayout: boolean; @@ -133,6 +132,7 @@ type TransactionItemRowProps = { isHover?: boolean; shouldShowArrowRightOnNarrowLayout?: boolean; customCardNames?: Record; + reportActions?: ReportAction[]; }; function getMerchantName(transactionItem: TransactionWithOptionalSearchFields, translate: (key: TranslationPaths) => string) { @@ -149,7 +149,6 @@ function getMerchantName(transactionItem: TransactionWithOptionalSearchFields, t } function TransactionItemRow({ - hash, transactionItem, report, shouldUseNarrowLayout, @@ -181,6 +180,7 @@ function TransactionItemRow({ isHover = false, shouldShowArrowRightOnNarrowLayout, customCardNames, + reportActions, }: TransactionItemRowProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -556,10 +556,7 @@ function TransactionItemRow({ ), [CONST.SEARCH.TABLE_COLUMNS.EXPORTED_TO]: ( - + ), }), @@ -591,7 +588,7 @@ function TransactionItemRow({ isAmountColumnWide, isTaxAmountColumnWide, isLargeScreenWidth, - hash, + reportActions, ], ); const shouldRenderChatBubbleCell = useMemo(() => { diff --git a/src/hooks/useAllTransactions.ts b/src/hooks/useAllTransactions.ts index f907458ba3a3..34a2fb0037b5 100644 --- a/src/hooks/useAllTransactions.ts +++ b/src/hooks/useAllTransactions.ts @@ -1,7 +1,6 @@ import {useMemo} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {useSearchContext} from '@components/Search/SearchContext'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Transaction} from '@src/types/onyx'; import useOnyx from './useOnyx'; @@ -10,9 +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 searchContext = useSearchContext(); - const searchHash = searchContext?.currentSearchHash ?? CONST.DEFAULT_NUMBER_ID; - const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`, {canBeMissing: true}); + const {currentSearchResults} = useSearchContext(); const [allTransactionsCollection] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false}); const allTransactions = useMemo(() => { diff --git a/src/hooks/useMergeTransactions.ts b/src/hooks/useMergeTransactions.ts index d33eb28b3f80..b82f1afcb742 100644 --- a/src/hooks/useMergeTransactions.ts +++ b/src/hooks/useMergeTransactions.ts @@ -2,7 +2,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import {useSearchContext} from '@components/Search/SearchContext'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getTransactionFromMergeTransaction} from '@libs/MergeTransactionUtils'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {MergeTransaction, Report, SearchResults, Transaction} from '@src/types/onyx'; import useOnyx from './useOnyx'; @@ -37,9 +36,7 @@ function getTransaction( } function useMergeTransactions({mergeTransaction}: UseMergeTransactionsProps): UseMergeTransactionsReturn { - const searchContext = useSearchContext(); - const searchHash = searchContext?.currentSearchHash ?? CONST.DEFAULT_NUMBER_ID; - const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`, {canBeMissing: true}); + const {currentSearchHash, currentSearchResults} = useSearchContext(); const [onyxTargetTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(mergeTransaction?.targetTransactionID)}`, { canBeMissing: true, @@ -59,7 +56,7 @@ function useMergeTransactions({mergeTransaction}: UseMergeTransactionsProps): Us }); // If we're on search and main collection reports are not available, get them from the search snapshot - if (searchHash && currentSearchResults?.data) { + if (currentSearchHash && currentSearchResults?.data) { targetTransactionReport = targetTransactionReport ?? currentSearchResults?.data[`${ONYXKEYS.COLLECTION.REPORT}${targetTransaction?.reportID}`]; sourceTransactionReport = sourceTransactionReport ?? currentSearchResults?.data[`${ONYXKEYS.COLLECTION.REPORT}${sourceTransaction?.reportID}`]; } diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index edc5da00af3b..ac5c2255a715 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -60,8 +60,7 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); const reportIDFromRoute = getNonEmptyStringOnyxID(route.params?.reportID); - const {currentSearchHash} = useSearchContext(); - const [snapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`, {canBeMissing: true}); + const {currentSearchResults: snapshot} = useSearchContext(); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`, {allowStaleData: true, canBeMissing: true}); diff --git a/src/pages/Search/SearchPageNarrow.tsx b/src/pages/Search/SearchPageNarrow.tsx index 5efc0be59900..b76f44407b9b 100644 --- a/src/pages/Search/SearchPageNarrow.tsx +++ b/src/pages/Search/SearchPageNarrow.tsx @@ -23,7 +23,6 @@ import type {BankAccountMenuItem, SearchParams, SearchQueryJSON} from '@componen import useHandleBackButton from '@hooks/useHandleBackButton'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useScrollEventEmitter from '@hooks/useScrollEventEmitter'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -36,8 +35,6 @@ import {isSearchDataLoaded} from '@libs/SearchUIUtils'; import variables from '@styles/variables'; import {searchInServer} from '@userActions/Report'; import {search} from '@userActions/Search'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {SearchResults} from '@src/types/onyx'; import type {SearchResultsInfo} from '@src/types/onyx/SearchResults'; @@ -83,8 +80,6 @@ function SearchPageNarrow({ const {clearSelectedTransactions, selectedTransactions} = useSearchContext(); const [searchRouterListVisible, setSearchRouterListVisible] = useState(false); const {isOffline} = useNetwork(); - const currentSearchResultsKey = queryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; - const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchResultsKey}`, {canBeMissing: true}); // Controls the visibility of the educational tooltip based on user scrolling. // Hides the tooltip when the user is scrolling and displays it once scrolling stops. const triggerScrollEvent = useScrollEventEmitter(); @@ -183,7 +178,7 @@ function SearchPageNarrow({ const shouldShowFooter = !!metadata?.count || Object.keys(selectedTransactions).length > 0; const isDataLoaded = isSearchDataLoaded(searchResults, queryJSON); - const shouldShowLoadingState = !isOffline && (!isDataLoaded || !!currentSearchResults?.search?.isLoading); + const shouldShowLoadingState = !isOffline && (!isDataLoaded || !!metadata?.isLoading); return ( >(() => getTransactionDetails(splitExpenseDraftTransaction) ?? {}, [splitExpenseDraftTransaction]); - - const searchHash = searchContext?.currentSearchHash ?? CONST.DEFAULT_NUMBER_ID; - const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`, {canBeMissing: true}); const allTransactions = useAllTransactions(); const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transactionID)}`]; diff --git a/src/pages/iou/SplitExpensePage.tsx b/src/pages/iou/SplitExpensePage.tsx index bbecb865ee2e..b3dd0d4b4841 100644 --- a/src/pages/iou/SplitExpensePage.tsx +++ b/src/pages/iou/SplitExpensePage.tsx @@ -70,7 +70,6 @@ function SplitExpensePage({route}: SplitExpensePageProps) { const {showConfirmModal} = useConfirmModal(); const [errorMessage, setErrorMessage] = React.useState(''); - const searchContext = useSearchContext(); const [selectedTab] = useOnyx(`${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.SPLIT_EXPENSE_TAB_TYPE}`, {canBeMissing: true}); @@ -81,8 +80,6 @@ function SplitExpensePage({route}: SplitExpensePageProps) { const expenseReport = transactionReport?.type === CONST.REPORT.TYPE.EXPENSE ? transactionReport : parentTransactionReport; const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${getNonEmptyStringOnyxID(expenseReport?.policyID)}`, {canBeMissing: true}); const [expenseReportPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(expenseReport?.policyID)}`, {canBeMissing: true}); - const searchHash = searchContext?.currentSearchHash ?? CONST.DEFAULT_NUMBER_ID; - const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`, {canBeMissing: true}); const allTransactions = useAllTransactions(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); @@ -92,14 +89,14 @@ function SplitExpensePage({route}: SplitExpensePageProps) { const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false}); const [allReportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {canBeMissing: true}); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportID)}`, {canBeMissing: true}); - const currentReport = report ?? currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportID)}`]; + const currentReport = report ?? searchContext?.currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportID)}`]; const [policyRecentlyUsedCurrencies] = useOnyx(ONYXKEYS.RECENTLY_USED_CURRENCIES, {canBeMissing: true}); const [policyRecentlyUsedCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${getIOURequestPolicyID(transaction, currentReport)}`, {canBeMissing: true}); const policy = usePolicy(currentReport?.policyID); const currentPolicy = Object.keys(policy?.employeeList ?? {}).length ? policy - : currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(currentReport?.policyID)}`]; + : searchContext?.currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(currentReport?.policyID)}`]; const isSplitAvailable = report && transaction && isSplitAction(currentReport, [transaction], originalTransaction, currentUserPersonalDetails.login ?? '', currentPolicy); diff --git a/tests/unit/hooks/useAllTransactions.test.ts b/tests/unit/hooks/useAllTransactions.test.ts index c278c43f9d79..0b4b323d6e79 100644 --- a/tests/unit/hooks/useAllTransactions.test.ts +++ b/tests/unit/hooks/useAllTransactions.test.ts @@ -6,11 +6,11 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {SearchResults} from '@src/types/onyx'; import createRandomTransaction from '../../utils/collections/transaction'; -const mockCurrentSearchHash = 12345; +let mockCurrentSearchResults: SearchResults | undefined; jest.mock('@components/Search/SearchContext', () => ({ useSearchContext: () => ({ - currentSearchHash: mockCurrentSearchHash, + currentSearchResults: mockCurrentSearchResults, }), })); @@ -24,6 +24,7 @@ describe('useAllTransactions', () => { beforeEach(() => { jest.clearAllMocks(); Onyx.clear(); + mockCurrentSearchResults = undefined; }); it('should return all transactions from collection when no search results', async () => { @@ -52,7 +53,7 @@ describe('useAllTransactions', () => { transaction1.transactionID = 'txn1'; await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}txn1`, transaction1); - await Onyx.merge(`${ONYXKEYS.COLLECTION.SNAPSHOT}${mockCurrentSearchHash}`, { + mockCurrentSearchResults = { search: { offset: 0, type: CONST.SEARCH.DATA_TYPES.EXPENSE, @@ -62,7 +63,7 @@ describe('useAllTransactions', () => { isLoading: false, }, data: undefined, - }); + } as unknown as SearchResults; const {result} = renderHook(() => useAllTransactions()); @@ -81,7 +82,7 @@ describe('useAllTransactions', () => { searchTransaction.transactionID = 'searchTxn1'; collectionTransaction.transactionID = 'collectionTxn1'; - await Onyx.merge(`${ONYXKEYS.COLLECTION.SNAPSHOT}${mockCurrentSearchHash}`, { + mockCurrentSearchResults = { search: { offset: 0, type: CONST.SEARCH.DATA_TYPES.EXPENSE, @@ -93,7 +94,7 @@ describe('useAllTransactions', () => { data: { [`${ONYXKEYS.COLLECTION.TRANSACTION}searchTxn1`]: searchTransaction, }, - } as unknown as SearchResults); + } as unknown as SearchResults; await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}collectionTxn1`, collectionTransaction); const {result} = renderHook(() => useAllTransactions()); @@ -117,7 +118,7 @@ describe('useAllTransactions', () => { searchTransaction.amount = 1000; collectionTransaction.amount = 2000; - await Onyx.merge(`${ONYXKEYS.COLLECTION.SNAPSHOT}${mockCurrentSearchHash}`, { + mockCurrentSearchResults = { search: { offset: 0, type: CONST.SEARCH.DATA_TYPES.EXPENSE, @@ -129,7 +130,7 @@ describe('useAllTransactions', () => { data: { [`${ONYXKEYS.COLLECTION.TRANSACTION}txn1`]: searchTransaction, }, - } as unknown as SearchResults); + } as unknown as SearchResults; await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}txn1`, collectionTransaction); const {result} = renderHook(() => useAllTransactions()); @@ -146,7 +147,7 @@ describe('useAllTransactions', () => { const transaction = createRandomTransaction(1); transaction.transactionID = 'txn1'; - await Onyx.merge(`${ONYXKEYS.COLLECTION.SNAPSHOT}${mockCurrentSearchHash}`, { + mockCurrentSearchResults = { search: { offset: 0, type: CONST.SEARCH.DATA_TYPES.EXPENSE, @@ -159,7 +160,7 @@ describe('useAllTransactions', () => { [`${ONYXKEYS.COLLECTION.TRANSACTION}txn1`]: transaction, [`${ONYXKEYS.COLLECTION.REPORT}report1`]: {reportID: 'report1'}, }, - } as unknown as SearchResults); + } as unknown as SearchResults; const {result} = renderHook(() => useAllTransactions()); @@ -175,7 +176,7 @@ describe('useAllTransactions', () => { }); it('should handle empty collection and empty search results', async () => { - await Onyx.merge(`${ONYXKEYS.COLLECTION.SNAPSHOT}${mockCurrentSearchHash}`, { + mockCurrentSearchResults = { search: { offset: 0, type: CONST.SEARCH.DATA_TYPES.EXPENSE, @@ -185,7 +186,7 @@ describe('useAllTransactions', () => { isLoading: false, }, data: {}, - }); + } as unknown as SearchResults; const {result} = renderHook(() => useAllTransactions()); @@ -200,7 +201,7 @@ describe('useAllTransactions', () => { const transaction = createRandomTransaction(1); transaction.transactionID = 'txn1'; - await Onyx.merge(`${ONYXKEYS.COLLECTION.SNAPSHOT}${mockCurrentSearchHash}`, { + mockCurrentSearchResults = { search: { offset: 0, type: CONST.SEARCH.DATA_TYPES.EXPENSE, @@ -214,7 +215,7 @@ describe('useAllTransactions', () => { [`${ONYXKEYS.COLLECTION.TRANSACTION}txn2`]: null, [`${ONYXKEYS.COLLECTION.TRANSACTION}txn3`]: undefined, }, - } as unknown as SearchResults); + } as unknown as SearchResults; const {result} = renderHook(() => useAllTransactions()); @@ -256,11 +257,16 @@ describe('useAllTransactions', () => { }); }); - it('should update when search results change', async () => { + it('should return search transactions when search results are available', async () => { + // This test verifies that search results transactions are returned + // Note: Since the mock is static, we can't test reactive updates to search results + // Reactive updates are tested via the collection transactions test const transaction1 = createRandomTransaction(1); + const transaction2 = createRandomTransaction(2); transaction1.transactionID = 'txn1'; + transaction2.transactionID = 'txn2'; - await Onyx.merge(`${ONYXKEYS.COLLECTION.SNAPSHOT}${mockCurrentSearchHash}`, { + mockCurrentSearchResults = { search: { offset: 0, type: CONST.SEARCH.DATA_TYPES.EXPENSE, @@ -271,36 +277,14 @@ describe('useAllTransactions', () => { }, data: { [`${ONYXKEYS.COLLECTION.TRANSACTION}txn1`]: transaction1, + [`${ONYXKEYS.COLLECTION.TRANSACTION}txn2`]: transaction2, }, - } as unknown as SearchResults); + } as unknown as SearchResults; const {result} = renderHook(() => useAllTransactions()); await waitFor(() => { expect(result.current).toBeDefined(); - expect(Object.keys(result.current ?? {}).length).toBe(1); - }); - - // Update search results - const transaction2 = createRandomTransaction(2); - transaction2.transactionID = 'txn2'; - - await Onyx.merge(`${ONYXKEYS.COLLECTION.SNAPSHOT}${mockCurrentSearchHash}`, { - search: { - offset: 0, - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, - hasMoreResults: false, - hasResults: true, - isLoading: false, - }, - data: { - [`${ONYXKEYS.COLLECTION.TRANSACTION}txn1`]: transaction1, - [`${ONYXKEYS.COLLECTION.TRANSACTION}txn2`]: transaction2, - }, - } as unknown as SearchResults); - - await waitFor(() => { expect(Object.keys(result.current ?? {}).length).toBe(2); }); @@ -310,30 +294,12 @@ describe('useAllTransactions', () => { }); }); - it('should use default search hash when context hash is not available', async () => { + it('should return collection transactions when search results are undefined', async () => { const transaction = createRandomTransaction(1); transaction.transactionID = 'txn1'; - const searchHash = CONST.DEFAULT_NUMBER_ID; - - // Mock context without search hash - jest.doMock('@components/Search/SearchContext', () => ({ - useSearchContext: () => ({ - currentSearchHash: undefined, - }), - })); + // Keep mockCurrentSearchResults as undefined (set in beforeEach) await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}txn1`, transaction); - await Onyx.merge(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`, { - search: { - offset: 0, - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, - hasMoreResults: false, - hasResults: false, - isLoading: false, - }, - data: {}, - }); const {result} = renderHook(() => useAllTransactions());