From 2ab5950fbfa6b2ad596a7820a9b2435ad5c9a5df Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 13 Jan 2026 12:52:37 -0700 Subject: [PATCH 01/28] use live data on todos --- src/hooks/useTodos.ts | 196 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 3 deletions(-) diff --git a/src/hooks/useTodos.ts b/src/hooks/useTodos.ts index 485bea8bbf2c5..22f652f8aa3d9 100644 --- a/src/hooks/useTodos.ts +++ b/src/hooks/useTodos.ts @@ -1,21 +1,125 @@ import {useMemo} from 'react'; +// eslint-disable-next-line no-restricted-imports +import {useOnyx} from 'react-native-onyx'; +import {useSearchContext} from '@components/Search/SearchContext'; import {isApproveAction, isExportAction, isPrimaryPayAction, isSubmitAction} from '@libs/ReportPrimaryActionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report, Transaction} from '@src/types/onyx'; +import type {Report, SearchResults, Transaction} from '@src/types/onyx'; +import type {SearchResultsInfo} from '@src/types/onyx/SearchResults'; import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; -import useOnyx from './useOnyx'; +type TodosResult = { + reportsToSubmit: Report[]; + reportsToApprove: Report[]; + reportsToPay: Report[]; + reportsToExport: Report[]; +}; + +type TodoSearchResultsData = SearchResults['data']; + +/** + * Builds a SearchResults-compatible data object from the given reports and related data. + * This allows the search UI to use live Onyx data instead of snapshot data when viewing to-do results. + */ +function buildSearchResultsData( + reports: Report[], + allTransactions: Record | undefined, + allPolicies: Record | undefined, + allReportActions: Record> | undefined, + allReportNameValuePairs: Record | undefined, + personalDetails: Record | undefined, + transactionViolations: Record | undefined, +): TodoSearchResultsData { + const data: Record = {}; + + // Add reports + for (const report of reports) { + if (!report?.reportID) { + continue; + } + data[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`] = report; + + // Add the policy for this report + if (report.policyID && allPolicies) { + const policyKey = `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`; + if (allPolicies[policyKey] && !data[policyKey]) { + data[policyKey] = allPolicies[policyKey]; + } + } + + // Add report name value pairs + if (report.chatReportID && allReportNameValuePairs) { + const nvpKey = `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.chatReportID}`; + if (allReportNameValuePairs[nvpKey] && !data[nvpKey]) { + data[nvpKey] = allReportNameValuePairs[nvpKey]; + } + } + + // Add report actions + if (allReportActions) { + const actionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`; + if (allReportActions[actionsKey] && !data[actionsKey]) { + data[actionsKey] = allReportActions[actionsKey]; + } + } + } + + // Add transactions for these reports + if (allTransactions) { + const reportIDs = new Set(reports.map((r) => r.reportID)); + for (const [transactionKey, transaction] of Object.entries(allTransactions)) { + if (transaction?.reportID && reportIDs.has(transaction.reportID)) { + data[transactionKey] = transaction; + + // Add transaction violations + if (transactionViolations) { + const violationsKey = `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`; + if (transactionViolations[violationsKey]) { + data[violationsKey] = transactionViolations[violationsKey]; + } + } + } + } + } + + // Add personal details + if (personalDetails) { + data[ONYXKEYS.PERSONAL_DETAILS_LIST] = personalDetails; + } + + return data as TodoSearchResultsData; +} + +/** + * Hook that provides to-do data (reports needing action) and optionally builds it in SearchResults format. + * + * When the user is viewing a to-do search result, this hook provides live Onyx data in the same format + * as the search snapshot, allowing the UI to display real-time updates instead of stale snapshot data. + */ export default function useTodos() { const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false}); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false}); const [allReportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {canBeMissing: false}); const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false}); const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {canBeMissing: false}); + const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); + const [personalDetailsList] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); const {login = '', accountID} = useCurrentUserPersonalDetails(); - return useMemo(() => { + // Get search context to determine if we're viewing a to-do search + const {currentSearchKey, currentSearchResults} = useSearchContext(); + + // Determine if the current search is a to-do action search based on the search key + const isTodoSearch = + currentSearchKey === CONST.SEARCH.SEARCH_KEYS.SUBMIT || + currentSearchKey === CONST.SEARCH.SEARCH_KEYS.APPROVE || + currentSearchKey === CONST.SEARCH.SEARCH_KEYS.PAY || + currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT; + + // Compute the categorized to-do reports + const todos = useMemo((): TodosResult => { const reportsToSubmit: Report[] = []; const reportsToApprove: Report[] = []; const reportsToPay: Report[] = []; @@ -61,4 +165,90 @@ export default function useTodos() { return {reportsToSubmit, reportsToApprove, reportsToPay, reportsToExport}; }, [allReports, allTransactions, allPolicies, allReportNameValuePairs, allReportActions, accountID, login, bankAccountList]); + + const {reportsToSubmit, reportsToApprove, reportsToPay, reportsToExport} = todos; + + // Build SearchResults-formatted data for the current to-do search key + const todoSearchResultsData = useMemo((): TodoSearchResultsData | undefined => { + if (!isTodoSearch) { + return undefined; + } + + let relevantReports: Report[] = []; + switch (currentSearchKey) { + case CONST.SEARCH.SEARCH_KEYS.SUBMIT: + relevantReports = reportsToSubmit; + break; + case CONST.SEARCH.SEARCH_KEYS.APPROVE: + relevantReports = reportsToApprove; + break; + case CONST.SEARCH.SEARCH_KEYS.PAY: + relevantReports = reportsToPay; + break; + case CONST.SEARCH.SEARCH_KEYS.EXPORT: + relevantReports = reportsToExport; + break; + default: + return undefined; + } + + return buildSearchResultsData( + relevantReports, + allTransactions as Record | undefined, + allPolicies as Record | undefined, + allReportActions as Record> | undefined, + allReportNameValuePairs as Record | undefined, + personalDetailsList as Record | undefined, + allTransactionViolations as Record | undefined, + ); + }, [ + isTodoSearch, + currentSearchKey, + reportsToSubmit, + reportsToApprove, + reportsToPay, + reportsToExport, + allTransactions, + allPolicies, + allReportActions, + allReportNameValuePairs, + personalDetailsList, + allTransactionViolations, + ]); + + // Default search info when building from live data + const defaultSearchInfo: SearchResultsInfo = useMemo( + () => ({ + offset: 0, + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hasMoreResults: false, + hasResults: true, + isLoading: false, + }), + [], + ); + + // Return either the live to-do data or the snapshot data + const searchResultsData = useMemo((): SearchResults | undefined => { + // If viewing a to-do search and we have live data, use it + if (isTodoSearch && todoSearchResultsData) { + // Merge with snapshot search metadata but use live data + const searchInfo: SearchResultsInfo = (currentSearchResults as SearchResults | undefined)?.search ?? defaultSearchInfo; + return { + search: searchInfo, + data: todoSearchResultsData, + }; + } + + // Otherwise return the snapshot data + return (currentSearchResults as SearchResults | undefined) ?? undefined; + }, [isTodoSearch, todoSearchResultsData, currentSearchResults, defaultSearchInfo]); + + return { + ...todos, + isTodoSearch, + currentSearchKey, + searchResultsData, + }; } From a037d4ab7d466c5c3ab4a313e75815a9c76d3634 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 13 Jan 2026 13:27:31 -0700 Subject: [PATCH 02/28] update logic --- src/components/Search/SearchContext.tsx | 48 ++++++++- src/hooks/useTodos.ts | 130 +++++------------------- src/pages/Search/SearchPage.tsx | 4 +- 3 files changed, 72 insertions(+), 110 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 4e227884e8921..931918bafe262 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -1,14 +1,28 @@ import React, {useCallback, useContext, useMemo, useRef, useState} from 'react'; -import useOnyx from '@hooks/useOnyx'; +// eslint-disable-next-line no-restricted-imports +import {useOnyx} from 'react-native-onyx'; +import useTodos from '@hooks/useTodos'; 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 {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'; +// Default search info when building from live data +const defaultSearchInfo: SearchResultsInfo = { + offset: 0, + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + status: CONST.SEARCH.STATUS.EXPENSE.ALL, + hasMoreResults: false, + hasResults: true, + isLoading: false, +}; + const defaultSearchContextData: SearchContextData = { currentSearchHash: -1, currentSearchKey: undefined, @@ -51,7 +65,37 @@ function SearchContextProvider({children}: ChildrenProps) { const [searchContextData, setSearchContextData] = useState(defaultSearchContextData); const areTransactionsEmpty = useRef(true); - const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchContextData.currentSearchHash}`, {canBeMissing: true}); + // Snapshot data from Onyx + const [snapshotSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchContextData.currentSearchHash}`, {canBeMissing: true}); + + // Get pre-built to-do search results data from useTodos + const {todoSearchResultsData} = useTodos(); + + // Determine if the current search is a to-do action search based on the search key + const currentSearchKey = searchContextData.currentSearchKey; + const isTodoSearch = + currentSearchKey === CONST.SEARCH.SEARCH_KEYS.SUBMIT || + currentSearchKey === CONST.SEARCH.SEARCH_KEYS.APPROVE || + currentSearchKey === CONST.SEARCH.SEARCH_KEYS.PAY || + currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT; + + // Build the current search results - use live data for to-do searches, snapshot otherwise + const currentSearchResults = useMemo((): SearchResults | undefined => { + // If viewing a to-do search, use live data from useTodos + if (isTodoSearch && currentSearchKey) { + const liveData = todoSearchResultsData[currentSearchKey]; + if (liveData) { + const searchInfo: SearchResultsInfo = snapshotSearchResults?.search ?? defaultSearchInfo; + return { + search: searchInfo, + data: liveData, + }; + } + } + + // Otherwise return the snapshot data + return snapshotSearchResults ?? undefined; + }, [isTodoSearch, currentSearchKey, todoSearchResultsData, snapshotSearchResults]); const setCurrentSearchHashAndKey = useCallback((searchHash: number, searchKey: SearchKey | undefined) => { setSearchContextData((prevState) => { diff --git a/src/hooks/useTodos.ts b/src/hooks/useTodos.ts index 22f652f8aa3d9..b9523462b4afd 100644 --- a/src/hooks/useTodos.ts +++ b/src/hooks/useTodos.ts @@ -1,21 +1,12 @@ import {useMemo} from 'react'; // eslint-disable-next-line no-restricted-imports import {useOnyx} from 'react-native-onyx'; -import {useSearchContext} from '@components/Search/SearchContext'; import {isApproveAction, isExportAction, isPrimaryPayAction, isSubmitAction} from '@libs/ReportPrimaryActionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Report, SearchResults, Transaction} from '@src/types/onyx'; -import type {SearchResultsInfo} from '@src/types/onyx/SearchResults'; import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; -type TodosResult = { - reportsToSubmit: Report[]; - reportsToApprove: Report[]; - reportsToPay: Report[]; - reportsToExport: Report[]; -}; - type TodoSearchResultsData = SearchResults['data']; /** @@ -91,12 +82,6 @@ function buildSearchResultsData( return data as TodoSearchResultsData; } -/** - * Hook that provides to-do data (reports needing action) and optionally builds it in SearchResults format. - * - * When the user is viewing a to-do search result, this hook provides live Onyx data in the same format - * as the search snapshot, allowing the UI to display real-time updates instead of stale snapshot data. - */ export default function useTodos() { const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false}); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false}); @@ -108,18 +93,7 @@ export default function useTodos() { const [personalDetailsList] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); const {login = '', accountID} = useCurrentUserPersonalDetails(); - // Get search context to determine if we're viewing a to-do search - const {currentSearchKey, currentSearchResults} = useSearchContext(); - - // Determine if the current search is a to-do action search based on the search key - const isTodoSearch = - currentSearchKey === CONST.SEARCH.SEARCH_KEYS.SUBMIT || - currentSearchKey === CONST.SEARCH.SEARCH_KEYS.APPROVE || - currentSearchKey === CONST.SEARCH.SEARCH_KEYS.PAY || - currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT; - - // Compute the categorized to-do reports - const todos = useMemo((): TodosResult => { + const todos = useMemo(() => { const reportsToSubmit: Report[] = []; const reportsToApprove: Report[] = []; const reportsToPay: Report[] = []; @@ -168,87 +142,33 @@ export default function useTodos() { const {reportsToSubmit, reportsToApprove, reportsToPay, reportsToExport} = todos; - // Build SearchResults-formatted data for the current to-do search key - const todoSearchResultsData = useMemo((): TodoSearchResultsData | undefined => { - if (!isTodoSearch) { - return undefined; - } - - let relevantReports: Report[] = []; - switch (currentSearchKey) { - case CONST.SEARCH.SEARCH_KEYS.SUBMIT: - relevantReports = reportsToSubmit; - break; - case CONST.SEARCH.SEARCH_KEYS.APPROVE: - relevantReports = reportsToApprove; - break; - case CONST.SEARCH.SEARCH_KEYS.PAY: - relevantReports = reportsToPay; - break; - case CONST.SEARCH.SEARCH_KEYS.EXPORT: - relevantReports = reportsToExport; - break; - default: + // Build SearchResults-formatted data for each to-do category + const todoSearchResultsData = useMemo(() => { + const buildData = (reports: Report[]): TodoSearchResultsData | undefined => { + if (reports.length === 0) { return undefined; - } - - return buildSearchResultsData( - relevantReports, - allTransactions as Record | undefined, - allPolicies as Record | undefined, - allReportActions as Record> | undefined, - allReportNameValuePairs as Record | undefined, - personalDetailsList as Record | undefined, - allTransactionViolations as Record | undefined, - ); - }, [ - isTodoSearch, - currentSearchKey, - reportsToSubmit, - reportsToApprove, - reportsToPay, - reportsToExport, - allTransactions, - allPolicies, - allReportActions, - allReportNameValuePairs, - personalDetailsList, - allTransactionViolations, - ]); - - // Default search info when building from live data - const defaultSearchInfo: SearchResultsInfo = useMemo( - () => ({ - offset: 0, - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: CONST.SEARCH.STATUS.EXPENSE.ALL, - hasMoreResults: false, - hasResults: true, - isLoading: false, - }), - [], - ); - - // Return either the live to-do data or the snapshot data - const searchResultsData = useMemo((): SearchResults | undefined => { - // If viewing a to-do search and we have live data, use it - if (isTodoSearch && todoSearchResultsData) { - // Merge with snapshot search metadata but use live data - const searchInfo: SearchResultsInfo = (currentSearchResults as SearchResults | undefined)?.search ?? defaultSearchInfo; - return { - search: searchInfo, - data: todoSearchResultsData, - }; - } - - // Otherwise return the snapshot data - return (currentSearchResults as SearchResults | undefined) ?? undefined; - }, [isTodoSearch, todoSearchResultsData, currentSearchResults, defaultSearchInfo]); + } + return buildSearchResultsData( + reports, + allTransactions as Record | undefined, + allPolicies as Record | undefined, + allReportActions as Record> | undefined, + allReportNameValuePairs as Record | undefined, + personalDetailsList as Record | undefined, + allTransactionViolations as Record | undefined, + ); + }; + + return { + [CONST.SEARCH.SEARCH_KEYS.SUBMIT]: buildData(reportsToSubmit), + [CONST.SEARCH.SEARCH_KEYS.APPROVE]: buildData(reportsToApprove), + [CONST.SEARCH.SEARCH_KEYS.PAY]: buildData(reportsToPay), + [CONST.SEARCH.SEARCH_KEYS.EXPORT]: buildData(reportsToExport), + }; + }, [reportsToSubmit, reportsToApprove, reportsToPay, reportsToExport, allTransactions, allPolicies, allReportActions, allReportNameValuePairs, personalDetailsList, allTransactionViolations]); return { ...todos, - isTodoSearch, - currentSearchKey, - searchResultsData, + todoSearchResultsData, }; } diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 475882508eb97..323602df742e3 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -108,7 +108,7 @@ function SearchPage({route}: SearchPageProps) { const theme = useTheme(); const {isOffline} = useNetwork(); const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); - const {selectedTransactions, clearSelectedTransactions, selectedReports, lastSearchType, setLastSearchType, areAllMatchingItemsSelected, selectAllMatchingItems} = useSearchContext(); + const {selectedTransactions, clearSelectedTransactions, selectedReports, lastSearchType, setLastSearchType, areAllMatchingItemsSelected, selectAllMatchingItems, currentSearchResults} = useSearchContext(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const isMobileSelectionModeEnabled = useMobileSelectionMode(); const allTransactions = useAllTransactions(); @@ -163,8 +163,6 @@ function SearchPage({route}: SearchPageProps) { 'ArrowSplit', ] as const); - // eslint-disable-next-line rulesdir/no-default-id-values - const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${queryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID}`, {canBeMissing: true}); const lastNonEmptySearchResults = useRef(undefined); const selectedTransactionReportIDs = useMemo( () => [ From b1036dd64a4af5aa9cd2074ee4417b3023f4c29b Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 13 Jan 2026 13:46:20 -0700 Subject: [PATCH 03/28] fix ts --- src/pages/Search/SearchPage.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 323602df742e3..0fdda1247b3ff 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -108,7 +108,9 @@ function SearchPage({route}: SearchPageProps) { const theme = useTheme(); const {isOffline} = useNetwork(); const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); - const {selectedTransactions, clearSelectedTransactions, selectedReports, lastSearchType, setLastSearchType, areAllMatchingItemsSelected, selectAllMatchingItems, currentSearchResults} = useSearchContext(); + const searchContext = useSearchContext(); + const {selectedTransactions, clearSelectedTransactions, selectedReports, lastSearchType, setLastSearchType, areAllMatchingItemsSelected, selectAllMatchingItems} = searchContext; + const currentSearchResults = searchContext.currentSearchResults as SearchResults | undefined; const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const isMobileSelectionModeEnabled = useMobileSelectionMode(); const allTransactions = useAllTransactions(); @@ -195,10 +197,12 @@ function SearchPage({route}: SearchPageProps) { const totalFormattedAmount = getTotalFormattedAmount(selectedReports, selectedTransactions, selectedBulkCurrency); const onlyShowPayElsewhere = useMemo(() => { - const selectedPolicy = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${selectedPolicyIDs.at(0)}`]; + const firstPolicyID = selectedPolicyIDs.at(0); + const selectedPolicy = firstPolicyID ? currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${firstPolicyID}`] : undefined; return (selectedTransactionReportIDs ?? selectedReportIDs).some((reportID) => { const report = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - const chatReport = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`]; + const chatReportID = report?.chatReportID; + const chatReport = chatReportID ? currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] : undefined; return ( report && !canIOUBePaid(report, chatReport, selectedPolicy, bankAccountList, undefined, false) && From adeb4feccf377392121de24d7937e96a11b2abd5 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 13 Jan 2026 16:15:36 -0700 Subject: [PATCH 04/28] rm comments --- src/components/Search/SearchContext.tsx | 12 +++--------- src/hooks/useTodos.ts | 20 ++++++++++++-------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 931918bafe262..1adfd02e851ad 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -16,7 +16,7 @@ import type {SearchContextData, SearchContextProps, SearchQueryJSON, SelectedTra // Default search info when building from live data const defaultSearchInfo: SearchResultsInfo = { offset: 0, - type: CONST.SEARCH.DATA_TYPES.EXPENSE, + type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, status: CONST.SEARCH.STATUS.EXPENSE.ALL, hasMoreResults: false, hasResults: true, @@ -65,23 +65,18 @@ function SearchContextProvider({children}: ChildrenProps) { const [searchContextData, setSearchContextData] = useState(defaultSearchContextData); const areTransactionsEmpty = useRef(true); - // Snapshot data from Onyx const [snapshotSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchContextData.currentSearchHash}`, {canBeMissing: true}); - - // Get pre-built to-do search results data from useTodos const {todoSearchResultsData} = useTodos(); - // Determine if the current search is a to-do action search based on the search key const currentSearchKey = searchContextData.currentSearchKey; - const isTodoSearch = + const isTodoSearch = currentSearchKey === CONST.SEARCH.SEARCH_KEYS.SUBMIT || currentSearchKey === CONST.SEARCH.SEARCH_KEYS.APPROVE || currentSearchKey === CONST.SEARCH.SEARCH_KEYS.PAY || currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT; - // Build the current search results - use live data for to-do searches, snapshot otherwise + // If viewing a to-do search, use live data from useTodos, otherwise return the snapshot data const currentSearchResults = useMemo((): SearchResults | undefined => { - // If viewing a to-do search, use live data from useTodos if (isTodoSearch && currentSearchKey) { const liveData = todoSearchResultsData[currentSearchKey]; if (liveData) { @@ -93,7 +88,6 @@ function SearchContextProvider({children}: ChildrenProps) { } } - // Otherwise return the snapshot data return snapshotSearchResults ?? undefined; }, [isTodoSearch, currentSearchKey, todoSearchResultsData, snapshotSearchResults]); diff --git a/src/hooks/useTodos.ts b/src/hooks/useTodos.ts index b9523462b4afd..6226fc670bff2 100644 --- a/src/hooks/useTodos.ts +++ b/src/hooks/useTodos.ts @@ -24,14 +24,12 @@ function buildSearchResultsData( ): TodoSearchResultsData { const data: Record = {}; - // Add reports for (const report of reports) { if (!report?.reportID) { continue; } data[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`] = report; - // Add the policy for this report if (report.policyID && allPolicies) { const policyKey = `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`; if (allPolicies[policyKey] && !data[policyKey]) { @@ -39,7 +37,6 @@ function buildSearchResultsData( } } - // Add report name value pairs if (report.chatReportID && allReportNameValuePairs) { const nvpKey = `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.chatReportID}`; if (allReportNameValuePairs[nvpKey] && !data[nvpKey]) { @@ -47,7 +44,6 @@ function buildSearchResultsData( } } - // Add report actions if (allReportActions) { const actionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`; if (allReportActions[actionsKey] && !data[actionsKey]) { @@ -56,14 +52,12 @@ function buildSearchResultsData( } } - // Add transactions for these reports if (allTransactions) { const reportIDs = new Set(reports.map((r) => r.reportID)); for (const [transactionKey, transaction] of Object.entries(allTransactions)) { if (transaction?.reportID && reportIDs.has(transaction.reportID)) { data[transactionKey] = transaction; - // Add transaction violations if (transactionViolations) { const violationsKey = `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`; if (transactionViolations[violationsKey]) { @@ -74,7 +68,6 @@ function buildSearchResultsData( } } - // Add personal details if (personalDetails) { data[ONYXKEYS.PERSONAL_DETAILS_LIST] = personalDetails; } @@ -165,7 +158,18 @@ export default function useTodos() { [CONST.SEARCH.SEARCH_KEYS.PAY]: buildData(reportsToPay), [CONST.SEARCH.SEARCH_KEYS.EXPORT]: buildData(reportsToExport), }; - }, [reportsToSubmit, reportsToApprove, reportsToPay, reportsToExport, allTransactions, allPolicies, allReportActions, allReportNameValuePairs, personalDetailsList, allTransactionViolations]); + }, [ + reportsToSubmit, + reportsToApprove, + reportsToPay, + reportsToExport, + allTransactions, + allPolicies, + allReportActions, + allReportNameValuePairs, + personalDetailsList, + allTransactionViolations, + ]); return { ...todos, From 247b2a31549475e990ec9dfe716d7bd9df7a4a84 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 13 Jan 2026 16:17:06 -0700 Subject: [PATCH 05/28] add comment --- src/components/Search/SearchContext.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 1adfd02e851ad..3db142a64fc08 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -76,6 +76,7 @@ function SearchContextProvider({children}: ChildrenProps) { currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT; // 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 => { if (isTodoSearch && currentSearchKey) { const liveData = todoSearchResultsData[currentSearchKey]; From 3886fba7d6a3b8d56e10d8a3be71d8458ec801e5 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 13 Jan 2026 16:18:10 -0700 Subject: [PATCH 06/28] simplify hook call --- src/pages/Search/SearchPage.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 0fdda1247b3ff..cf4c501ab3777 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -108,9 +108,7 @@ function SearchPage({route}: SearchPageProps) { const theme = useTheme(); const {isOffline} = useNetwork(); const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); - const searchContext = useSearchContext(); - const {selectedTransactions, clearSelectedTransactions, selectedReports, lastSearchType, setLastSearchType, areAllMatchingItemsSelected, selectAllMatchingItems} = searchContext; - const currentSearchResults = searchContext.currentSearchResults as SearchResults | undefined; + const {selectedTransactions, clearSelectedTransactions, selectedReports, lastSearchType, setLastSearchType, areAllMatchingItemsSelected, selectAllMatchingItems, currentSearchResults} = useSearchContext(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const isMobileSelectionModeEnabled = useMobileSelectionMode(); const allTransactions = useAllTransactions(); From 577e433d42dd093829452b953570257134da5080 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 13 Jan 2026 17:03:05 -0700 Subject: [PATCH 07/28] fix prettier --- src/components/Search/SearchContext.tsx | 2 +- src/pages/Search/SearchPage.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 3db142a64fc08..dc803d1e0f272 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -69,7 +69,7 @@ function SearchContextProvider({children}: ChildrenProps) { const {todoSearchResultsData} = useTodos(); const currentSearchKey = searchContextData.currentSearchKey; - const isTodoSearch = + const isTodoSearch = currentSearchKey === CONST.SEARCH.SEARCH_KEYS.SUBMIT || currentSearchKey === CONST.SEARCH.SEARCH_KEYS.APPROVE || currentSearchKey === CONST.SEARCH.SEARCH_KEYS.PAY || diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index cf4c501ab3777..c8402ee428618 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -108,7 +108,8 @@ function SearchPage({route}: SearchPageProps) { const theme = useTheme(); const {isOffline} = useNetwork(); const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); - const {selectedTransactions, clearSelectedTransactions, selectedReports, lastSearchType, setLastSearchType, areAllMatchingItemsSelected, selectAllMatchingItems, currentSearchResults} = useSearchContext(); + const {selectedTransactions, clearSelectedTransactions, selectedReports, lastSearchType, setLastSearchType, areAllMatchingItemsSelected, selectAllMatchingItems, currentSearchResults} = + useSearchContext(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const isMobileSelectionModeEnabled = useMobileSelectionMode(); const allTransactions = useAllTransactions(); From 1bd41a0c06f89e0680fc3a17cd1bfd653a8392ee Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 13 Jan 2026 17:08:03 -0700 Subject: [PATCH 08/28] update useOnyx wrapper --- src/components/Search/SearchContext.tsx | 12 ++++-------- src/hooks/useOnyx.ts | 7 +++++-- src/libs/SearchUIUtils.ts | 5 +++++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index dc803d1e0f272..47d22ae34d526 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -3,7 +3,7 @@ import React, {useCallback, useContext, useMemo, useRef, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import useTodos from '@hooks/useTodos'; import {isMoneyRequestReport} from '@libs/ReportUtils'; -import {isTransactionListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils'; +import {isTransactionListItemType, isTransactionReportGroupListItemType, isTodoSearch} from '@libs/SearchUIUtils'; import type {SearchKey} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -69,16 +69,12 @@ function SearchContextProvider({children}: ChildrenProps) { const {todoSearchResultsData} = useTodos(); const currentSearchKey = searchContextData.currentSearchKey; - const isTodoSearch = - currentSearchKey === CONST.SEARCH.SEARCH_KEYS.SUBMIT || - currentSearchKey === CONST.SEARCH.SEARCH_KEYS.APPROVE || - currentSearchKey === CONST.SEARCH.SEARCH_KEYS.PAY || - currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT; + const shouldUseLiveData = currentSearchKey && isTodoSearch(currentSearchKey); // 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 => { - if (isTodoSearch && currentSearchKey) { + if (shouldUseLiveData) { const liveData = todoSearchResultsData[currentSearchKey]; if (liveData) { const searchInfo: SearchResultsInfo = snapshotSearchResults?.search ?? defaultSearchInfo; @@ -90,7 +86,7 @@ function SearchContextProvider({children}: ChildrenProps) { } return snapshotSearchResults ?? undefined; - }, [isTodoSearch, currentSearchKey, todoSearchResultsData, snapshotSearchResults]); + }, [shouldUseLiveData, currentSearchKey, todoSearchResultsData, snapshotSearchResults]); const setCurrentSearchHashAndKey = useCallback((searchHash: number, searchKey: SearchKey | undefined) => { setSearchContextData((prevState) => { diff --git a/src/hooks/useOnyx.ts b/src/hooks/useOnyx.ts index db42d64b636d3..11edeea504f6b 100644 --- a/src/hooks/useOnyx.ts +++ b/src/hooks/useOnyx.ts @@ -5,6 +5,7 @@ import {useOnyx as originalUseOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry, OnyxKey, OnyxValue, UseOnyxOptions, UseOnyxResult} from 'react-native-onyx'; import {SearchContext} from '@components/Search/SearchContext'; import {useIsOnSearch} from '@components/Search/SearchScopeProvider'; +import {isTodoSearch} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {SearchResults} from '@src/types/onyx'; @@ -52,16 +53,18 @@ const useOnyx: OriginalUseOnyx = > | undefined; const {selector: selectorProp, ...optionsWithoutSelector} = useOnyxOptions ?? {}; // Determine if we should use snapshot data based on search state and key - const shouldUseSnapshot = isOnSearch && !!currentSearchHash && isSnapshotCompatibleKey; + const shouldUseSnapshot = isOnSearch && !!currentSearchHash && isSnapshotCompatibleKey && !shouldUseLiveData; // Create selector function that handles both regular and snapshot data const selector = useMemo(() => { diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 01415c68c99af..2f0e1fecb35e2 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2774,6 +2774,10 @@ function isCorrectSearchUserName(displayName?: string) { return displayName && displayName.toUpperCase() !== CONST.REPORT.OWNER_EMAIL_FAKE; } +function isTodoSearch(currentSearchKey: SearchKey) { + return currentSearchKey === CONST.SEARCH.SEARCH_KEYS.SUBMIT || currentSearchKey === CONST.SEARCH.SEARCH_KEYS.APPROVE || currentSearchKey === CONST.SEARCH.SEARCH_KEYS.PAY || currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT; +} + // eslint-disable-next-line @typescript-eslint/max-params function createTypeMenuSections( icons: Record<'Document' | 'Pencil' | 'ThumbsUp', IconAsset>, @@ -3554,5 +3558,6 @@ export { getTableMinWidth, getCustomColumns, getCustomColumnDefault, + isTodoSearch, }; export type {SavedSearchMenuItem, SearchTypeMenuSection, SearchTypeMenuItem, SearchDateModifier, SearchDateModifierLower, SearchKey, ArchivedReportsIDSet}; From 48f09fd507733a9c4f03da5c44d146570d5ba122 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 13 Jan 2026 17:17:33 -0700 Subject: [PATCH 09/28] fix prettier --- src/components/Search/SearchContext.tsx | 2 +- src/libs/SearchUIUtils.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 47d22ae34d526..65c7452659431 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -3,7 +3,7 @@ import React, {useCallback, useContext, useMemo, useRef, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import useTodos from '@hooks/useTodos'; import {isMoneyRequestReport} from '@libs/ReportUtils'; -import {isTransactionListItemType, isTransactionReportGroupListItemType, isTodoSearch} from '@libs/SearchUIUtils'; +import {isTodoSearch, isTransactionListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils'; import type {SearchKey} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 2f0e1fecb35e2..3fbb3500ad0e7 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2775,7 +2775,12 @@ function isCorrectSearchUserName(displayName?: string) { } function isTodoSearch(currentSearchKey: SearchKey) { - return currentSearchKey === CONST.SEARCH.SEARCH_KEYS.SUBMIT || currentSearchKey === CONST.SEARCH.SEARCH_KEYS.APPROVE || currentSearchKey === CONST.SEARCH.SEARCH_KEYS.PAY || currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT; + return ( + currentSearchKey === CONST.SEARCH.SEARCH_KEYS.SUBMIT || + currentSearchKey === CONST.SEARCH.SEARCH_KEYS.APPROVE || + currentSearchKey === CONST.SEARCH.SEARCH_KEYS.PAY || + currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT + ); } // eslint-disable-next-line @typescript-eslint/max-params From 15ab577071d47bcad1af033fba992482d5385d3d Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 14 Jan 2026 12:51:20 -0700 Subject: [PATCH 10/28] disable loading/empty views for todos --- src/components/Search/index.tsx | 5 +++-- src/libs/SearchUIUtils.ts | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 913fd2d1b6687..7bea19ad74396 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -64,6 +64,7 @@ import { isTransactionWithdrawalIDGroupListItemType, shouldShowEmptyState, shouldShowYear as shouldShowYearUtil, + isTodoSearch, } from '@libs/SearchUIUtils'; import {cancelSpan, endSpan, startSpan} from '@libs/telemetry/activeSpans'; import {getOriginalTransactionWithSplitInfo, isOnHold, isTransactionPendingDelete} from '@libs/TransactionUtils'; @@ -359,7 +360,7 @@ function Search({ // There's a race condition in Onyx which makes it return data from the previous Search, so in addition to checking that the data is loaded // we also need to check that the searchResults matches the type and status of the current search - const isDataLoaded = isSearchDataLoaded(searchResults, queryJSON); + const isDataLoaded = isTodoSearch(searchKey) || isSearchDataLoaded(searchResults, queryJSON); const hasErrors = Object.keys(searchResults?.errors ?? {}).length > 0 && !isOffline; @@ -1032,7 +1033,7 @@ function Search({ } const visibleDataLength = filteredData.filter((item) => item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline).length; - if (shouldShowEmptyState(isDataLoaded, visibleDataLength, searchResults?.search?.type)) { + if (shouldShowEmptyState(isDataLoaded, visibleDataLength, searchResults?.search?.type, isTodoSearch(searchKey))) { cancelSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB); return ( diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 0984a0156fb8e..834a3781d1061 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2781,8 +2781,8 @@ function isCorrectSearchUserName(displayName?: string) { return displayName && displayName.toUpperCase() !== CONST.REPORT.OWNER_EMAIL_FAKE; } -function isTodoSearch(currentSearchKey: SearchKey) { - return ( +function isTodoSearch(currentSearchKey: SearchKey | undefined) { + return !!currentSearchKey && ( currentSearchKey === CONST.SEARCH.SEARCH_KEYS.SUBMIT || currentSearchKey === CONST.SEARCH.SEARCH_KEYS.APPROVE || currentSearchKey === CONST.SEARCH.SEARCH_KEYS.PAY || @@ -3005,8 +3005,8 @@ function createBaseSavedSearchMenuItem(item: SaveSearchItem, key: string, index: /** * Whether to show the empty state or not */ -function shouldShowEmptyState(isDataLoaded: boolean, dataLength: number, type: SearchDataTypes) { - return !isDataLoaded || dataLength === 0 || !Object.values(CONST.SEARCH.DATA_TYPES).includes(type); +function shouldShowEmptyState(isDataLoaded: boolean, dataLength: number, type: SearchDataTypes, isTodoSearchFlag = false) { + return !isDataLoaded || dataLength === 0 || (!isTodoSearchFlag && !Object.values(CONST.SEARCH.DATA_TYPES).includes(type)); } function isSearchDataLoaded(searchResults: SearchResults | undefined, queryJSON: SearchQueryJSON | undefined) { From 4296ca398e153e70a195982710c802ff7a48de40 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 14 Jan 2026 13:01:50 -0700 Subject: [PATCH 11/28] fix loading issue --- src/components/Search/index.tsx | 8 ++++---- src/libs/SearchUIUtils.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 7bea19ad74396..c2be052a6fe38 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -268,7 +268,7 @@ function Search({ const suggestedSearches = useMemo(() => getSuggestedSearches(accountID, defaultCardFeed?.id), [defaultCardFeed?.id, accountID]); const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.similarSearchHash === similarSearchHash)?.key, [suggestedSearches, similarSearchHash]); - + const searchDataType = useMemo(() => isTodoSearch(searchKey) ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type, [searchKey, searchResults?.search?.type]); const shouldCalculateTotals = useSearchShouldCalculateTotals(searchKey, similarSearchHash, offset === 0); const previousReportActions = usePrevious(reportActions); @@ -848,8 +848,8 @@ function Search({ if (!searchResults?.data) { return []; } - return getColumnsToShow(accountID, searchResults?.data, visibleColumns, false, searchResults?.search?.type, validGroupBy); - }, [accountID, searchResults?.data, searchResults?.search?.type, visibleColumns, validGroupBy]); + return getColumnsToShow(accountID, searchResults?.data, visibleColumns, false, searchDataType, validGroupBy); + }, [accountID, searchResults?.data, searchDataType, visibleColumns, validGroupBy]); const opacity = useSharedValue(1); const animatedStyle = useAnimatedStyle(() => ({ @@ -1033,7 +1033,7 @@ function Search({ } const visibleDataLength = filteredData.filter((item) => item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline).length; - if (shouldShowEmptyState(isDataLoaded, visibleDataLength, searchResults?.search?.type, isTodoSearch(searchKey))) { + if (shouldShowEmptyState(isDataLoaded, visibleDataLength, searchDataType)) { cancelSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB); return ( diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 834a3781d1061..b65dee0da23c0 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -3005,8 +3005,8 @@ function createBaseSavedSearchMenuItem(item: SaveSearchItem, key: string, index: /** * Whether to show the empty state or not */ -function shouldShowEmptyState(isDataLoaded: boolean, dataLength: number, type: SearchDataTypes, isTodoSearchFlag = false) { - return !isDataLoaded || dataLength === 0 || (!isTodoSearchFlag && !Object.values(CONST.SEARCH.DATA_TYPES).includes(type)); +function shouldShowEmptyState(isDataLoaded: boolean, dataLength: number, type: SearchDataTypes | undefined) { + return !isDataLoaded || dataLength === 0 || !type || !Object.values(CONST.SEARCH.DATA_TYPES).includes(type); } function isSearchDataLoaded(searchResults: SearchResults | undefined, queryJSON: SearchQueryJSON | undefined) { From 03e6325ff2992de9d28271400f6b261d92fc57a9 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 14 Jan 2026 13:20:21 -0700 Subject: [PATCH 12/28] fix loading/empty states --- src/components/Search/SearchContext.tsx | 15 ++++++++------- src/components/Search/index.tsx | 2 ++ src/hooks/useTodos.ts | 5 +++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index eb50c170289e5..93b4a4d09969c 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -76,13 +76,14 @@ function SearchContextProvider({children}: ChildrenProps) { const currentSearchResults = useMemo((): SearchResults | undefined => { if (shouldUseLiveData) { const liveData = todoSearchResultsData[currentSearchKey]; - if (liveData) { - const searchInfo: SearchResultsInfo = snapshotSearchResults?.search ?? defaultSearchInfo; - return { - search: searchInfo, - data: liveData, - }; - } + const searchInfo: SearchResultsInfo = snapshotSearchResults?.search ?? defaultSearchInfo; + const hasResults = Object.keys(liveData).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 + return { + search: {...searchInfo, isLoading: false, hasResults}, + data: liveData, + }; } return snapshotSearchResults ?? undefined; diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index c2be052a6fe38..5b7057a1fa4ba 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -364,7 +364,9 @@ function Search({ const hasErrors = Object.keys(searchResults?.errors ?? {}).length > 0 && !isOffline; + // For to-do searches, we never show loading state since the data is always available locally from Onyx const shouldShowLoadingState = + !isTodoSearch(searchKey) && !isOffline && (!isDataLoaded || (!!searchResults?.search.isLoading && Array.isArray(searchResults?.data) && searchResults?.data.length === 0) || (hasErrors && !searchRequestResponseStatusCode)); const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0; diff --git a/src/hooks/useTodos.ts b/src/hooks/useTodos.ts index 6226fc670bff2..b9fc10ab64086 100644 --- a/src/hooks/useTodos.ts +++ b/src/hooks/useTodos.ts @@ -137,9 +137,10 @@ export default function useTodos() { // Build SearchResults-formatted data for each to-do category const todoSearchResultsData = useMemo(() => { - const buildData = (reports: Report[]): TodoSearchResultsData | undefined => { + const buildData = (reports: Report[]): TodoSearchResultsData => { if (reports.length === 0) { - return undefined; + // Return empty object like the Search API would when there's no data + return {} as TodoSearchResultsData; } return buildSearchResultsData( reports, From dacd377a27eb0a7f0c587c6ce11e34b8496618df Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 14 Jan 2026 13:22:06 -0700 Subject: [PATCH 13/28] fix prettier --- src/components/Search/index.tsx | 4 ++-- src/libs/SearchUIUtils.ts | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 5b7057a1fa4ba..ae64ecc8f0d84 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -56,6 +56,7 @@ import { isSearchDataLoaded, isSearchResultsEmpty as isSearchResultsEmptyUtil, isTaskListItemType, + isTodoSearch, isTransactionCardGroupListItemType, isTransactionGroupListItemType, isTransactionListItemType, @@ -64,7 +65,6 @@ import { isTransactionWithdrawalIDGroupListItemType, shouldShowEmptyState, shouldShowYear as shouldShowYearUtil, - isTodoSearch, } from '@libs/SearchUIUtils'; import {cancelSpan, endSpan, startSpan} from '@libs/telemetry/activeSpans'; import {getOriginalTransactionWithSplitInfo, isOnHold, isTransactionPendingDelete} from '@libs/TransactionUtils'; @@ -268,7 +268,7 @@ function Search({ const suggestedSearches = useMemo(() => getSuggestedSearches(accountID, defaultCardFeed?.id), [defaultCardFeed?.id, accountID]); const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.similarSearchHash === similarSearchHash)?.key, [suggestedSearches, similarSearchHash]); - const searchDataType = useMemo(() => isTodoSearch(searchKey) ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type, [searchKey, searchResults?.search?.type]); + const searchDataType = useMemo(() => (isTodoSearch(searchKey) ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type), [searchKey, searchResults?.search?.type]); const shouldCalculateTotals = useSearchShouldCalculateTotals(searchKey, similarSearchHash, offset === 0); const previousReportActions = usePrevious(reportActions); diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index b65dee0da23c0..6b9208fe53d96 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2782,11 +2782,12 @@ function isCorrectSearchUserName(displayName?: string) { } function isTodoSearch(currentSearchKey: SearchKey | undefined) { - return !!currentSearchKey && ( - currentSearchKey === CONST.SEARCH.SEARCH_KEYS.SUBMIT || - currentSearchKey === CONST.SEARCH.SEARCH_KEYS.APPROVE || - currentSearchKey === CONST.SEARCH.SEARCH_KEYS.PAY || - currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT + return ( + !!currentSearchKey && + (currentSearchKey === CONST.SEARCH.SEARCH_KEYS.SUBMIT || + currentSearchKey === CONST.SEARCH.SEARCH_KEYS.APPROVE || + currentSearchKey === CONST.SEARCH.SEARCH_KEYS.PAY || + currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT) ); } From be13d6e5f976d66c7f59ad4a7fd2e61fe004b688 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 14 Jan 2026 13:37:24 -0700 Subject: [PATCH 14/28] fix tests --- tests/unit/ReportUtilsTest.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index b14f5486bf310..e6ee3680eeb25 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -211,6 +211,7 @@ jest.mock('@libs/PolicyUtils', () => ({ ...jest.requireActual('@libs/PolicyUtils'), isPolicyAdmin: jest.fn().mockImplementation((policy?: Policy) => policy?.role === 'admin'), isPaidGroupPolicy: jest.fn().mockImplementation((policy?: Policy) => policy?.type === 'corporate' || policy?.type === 'team'), + isPolicyOwner: jest.fn().mockImplementation((policy?: Policy, currentUserAccountID?: number) => !!currentUserAccountID && policy?.ownerAccountID === currentUserAccountID), })); const mockedPolicyUtils = PolicyUtils as jest.Mocked; From e6f1e058b643319809f5828d07bef5985176da9a Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 15 Jan 2026 16:09:32 -0700 Subject: [PATCH 15/28] address comments --- src/components/Search/SearchContext.tsx | 2 ++ src/hooks/useTodos.ts | 30 ++++++++++++++----------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 72d39c8bbe542..c0a6bc4105511 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -1,4 +1,5 @@ import React, {useCallback, useContext, useMemo, 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 useTodos from '@hooks/useTodos'; @@ -14,6 +15,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {SearchContextData, SearchContextProps, SearchQueryJSON, 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 const defaultSearchInfo: SearchResultsInfo = { offset: 0, type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, diff --git a/src/hooks/useTodos.ts b/src/hooks/useTodos.ts index b9fc10ab64086..91c6d9b7b24d2 100644 --- a/src/hooks/useTodos.ts +++ b/src/hooks/useTodos.ts @@ -1,4 +1,5 @@ import {useMemo} from 'react'; +// We need direct access to useOnyx from react-native-onyx to avoid using snapshots for live to-do data // eslint-disable-next-line no-restricted-imports import {useOnyx} from 'react-native-onyx'; import {isApproveAction, isExportAction, isPrimaryPayAction, isSubmitAction} from '@libs/ReportPrimaryActionUtils'; @@ -15,7 +16,7 @@ type TodoSearchResultsData = SearchResults['data']; */ function buildSearchResultsData( reports: Report[], - allTransactions: Record | undefined, + transactionsByReportID: Record, allPolicies: Record | undefined, allReportActions: Record> | undefined, allReportNameValuePairs: Record | undefined, @@ -50,12 +51,12 @@ function buildSearchResultsData( data[actionsKey] = allReportActions[actionsKey]; } } - } - if (allTransactions) { - const reportIDs = new Set(reports.map((r) => r.reportID)); - for (const [transactionKey, transaction] of Object.entries(allTransactions)) { - if (transaction?.reportID && reportIDs.has(transaction.reportID)) { + // Add transactions for this report using the pre-computed mapping + const reportTransactions = transactionsByReportID[report.reportID]; + if (reportTransactions) { + for (const transaction of reportTransactions) { + const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`; data[transactionKey] = transaction; if (transactionViolations) { @@ -91,13 +92,13 @@ export default function useTodos() { const reportsToApprove: Report[] = []; const reportsToPay: Report[] = []; const reportsToExport: Report[] = []; + const transactionsByReportID: Record = {}; const reports = allReports ? Object.values(allReports) : []; if (reports.length === 0) { - return {reportsToSubmit, reportsToApprove, reportsToPay, reportsToExport}; + return {reportsToSubmit, reportsToApprove, reportsToPay, reportsToExport, transactionsByReportID}; } - const transactionsByReportID: Record = {}; if (allTransactions) { for (const transaction of Object.values(allTransactions)) { if (!transaction?.reportID) { @@ -130,10 +131,10 @@ export default function useTodos() { } } - return {reportsToSubmit, reportsToApprove, reportsToPay, reportsToExport}; + return {reportsToSubmit, reportsToApprove, reportsToPay, reportsToExport, transactionsByReportID}; }, [allReports, allTransactions, allPolicies, allReportNameValuePairs, allReportActions, accountID, login, bankAccountList]); - const {reportsToSubmit, reportsToApprove, reportsToPay, reportsToExport} = todos; + const {reportsToSubmit, reportsToApprove, reportsToPay, reportsToExport, transactionsByReportID} = todos; // Build SearchResults-formatted data for each to-do category const todoSearchResultsData = useMemo(() => { @@ -144,7 +145,7 @@ export default function useTodos() { } return buildSearchResultsData( reports, - allTransactions as Record | undefined, + transactionsByReportID, allPolicies as Record | undefined, allReportActions as Record> | undefined, allReportNameValuePairs as Record | undefined, @@ -164,7 +165,7 @@ export default function useTodos() { reportsToApprove, reportsToPay, reportsToExport, - allTransactions, + transactionsByReportID, allPolicies, allReportActions, allReportNameValuePairs, @@ -173,7 +174,10 @@ export default function useTodos() { ]); return { - ...todos, + reportsToSubmit, + reportsToApprove, + reportsToPay, + reportsToExport, todoSearchResultsData, }; } From 1bc7a43fe6d1d6f78ded1b3bf66441ae0928658c Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 15 Jan 2026 16:12:33 -0700 Subject: [PATCH 16/28] add chat reports --- src/hooks/useTodos.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/hooks/useTodos.ts b/src/hooks/useTodos.ts index 91c6d9b7b24d2..6443a61918014 100644 --- a/src/hooks/useTodos.ts +++ b/src/hooks/useTodos.ts @@ -17,6 +17,7 @@ type TodoSearchResultsData = SearchResults['data']; function buildSearchResultsData( reports: Report[], transactionsByReportID: Record, + allReports: Record | undefined, allPolicies: Record | undefined, allReportActions: Record> | undefined, allReportNameValuePairs: Record | undefined, @@ -38,10 +39,21 @@ function buildSearchResultsData( } } - if (report.chatReportID && allReportNameValuePairs) { - const nvpKey = `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.chatReportID}`; - if (allReportNameValuePairs[nvpKey] && !data[nvpKey]) { - data[nvpKey] = allReportNameValuePairs[nvpKey]; + if (report.chatReportID) { + // Add the chat report itself (needed by getChatReport > canIOUBePaid for pay eligibility checks) + if (allReports) { + const chatReportKey = `${ONYXKEYS.COLLECTION.REPORT}${report.chatReportID}`; + if (allReports[chatReportKey] && !data[chatReportKey]) { + data[chatReportKey] = allReports[chatReportKey]; + } + } + + // Add the report name value pairs for the chat report + if (allReportNameValuePairs) { + const nvpKey = `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.chatReportID}`; + if (allReportNameValuePairs[nvpKey] && !data[nvpKey]) { + data[nvpKey] = allReportNameValuePairs[nvpKey]; + } } } @@ -146,6 +158,7 @@ export default function useTodos() { return buildSearchResultsData( reports, transactionsByReportID, + allReports as Record | undefined, allPolicies as Record | undefined, allReportActions as Record> | undefined, allReportNameValuePairs as Record | undefined, @@ -166,6 +179,7 @@ export default function useTodos() { reportsToPay, reportsToExport, transactionsByReportID, + allReports, allPolicies, allReportActions, allReportNameValuePairs, From 78b90a92205fee6815edf792aeb5149f1ed51751 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 20 Jan 2026 12:19:59 -0700 Subject: [PATCH 17/28] update types --- src/hooks/useTodos.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/hooks/useTodos.ts b/src/hooks/useTodos.ts index eaf0ccb0e93f7..e14cb3b8b597d 100644 --- a/src/hooks/useTodos.ts +++ b/src/hooks/useTodos.ts @@ -5,7 +5,7 @@ import {useOnyx} from 'react-native-onyx'; import {isApproveAction, isExportAction, isPrimaryPayAction, isSubmitAction} from '@libs/ReportPrimaryActionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report, SearchResults, Transaction} from '@src/types/onyx'; +import type {PersonalDetailsList, Policy, Report, ReportActions, ReportNameValuePairs, SearchResults, Transaction, TransactionViolations} from '@src/types/onyx'; import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; type TodoSearchResultsData = SearchResults['data']; @@ -18,11 +18,11 @@ function buildSearchResultsData( reports: Report[], transactionsByReportID: Record, allReports: Record | undefined, - allPolicies: Record | undefined, - allReportActions: Record> | undefined, - allReportNameValuePairs: Record | undefined, - personalDetails: Record | undefined, - transactionViolations: Record | undefined, + allPolicies: Record | undefined, + allReportActions: Record | undefined, + allReportNameValuePairs: Record | undefined, + personalDetails: PersonalDetailsList | undefined, + transactionViolations: Record | undefined, ): TodoSearchResultsData { const data: Record = {}; @@ -159,11 +159,11 @@ export default function useTodos() { reports, transactionsByReportID, allReports as Record | undefined, - allPolicies as Record | undefined, - allReportActions as Record> | undefined, - allReportNameValuePairs as Record | undefined, - personalDetailsList as Record | undefined, - allTransactionViolations as Record | undefined, + allPolicies as Record | undefined, + allReportActions as Record | undefined, + allReportNameValuePairs as Record | undefined, + personalDetailsList, + allTransactionViolations as Record | undefined, ); }; From 6e732e8880fcf0105e6827bb5dffd716c49e033b Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 20 Jan 2026 14:10:44 -0700 Subject: [PATCH 18/28] compute live metadata --- src/components/Search/SearchContext.tsx | 12 +++++- src/hooks/useTodos.ts | 50 +++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index c0a6bc4105511..73da3a4687f03 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -23,6 +23,9 @@ const defaultSearchInfo: SearchResultsInfo = { hasMoreResults: false, hasResults: true, isLoading: false, + count: 0, + total: 0, + currency: '', }; const defaultSearchContextData: SearchContextData = { @@ -78,13 +81,18 @@ function SearchContextProvider({children}: ChildrenProps) { const currentSearchResults = useMemo((): SearchResults | undefined => { if (shouldUseLiveData) { const liveData = todoSearchResultsData[currentSearchKey]; - const searchInfo: SearchResultsInfo = snapshotSearchResults?.search ?? defaultSearchInfo; + const searchInfo: SearchResultsInfo = { + ...snapshotSearchResults?.search ?? defaultSearchInfo, + count: liveData.metadata.count, + total: liveData.metadata.total, + currency: liveData.metadata.currency, + }; const hasResults = Object.keys(liveData).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 return { search: {...searchInfo, isLoading: false, hasResults}, - data: liveData, + data: liveData.data, }; } diff --git a/src/hooks/useTodos.ts b/src/hooks/useTodos.ts index e14cb3b8b597d..506dda0b5c7ae 100644 --- a/src/hooks/useTodos.ts +++ b/src/hooks/useTodos.ts @@ -10,6 +10,43 @@ import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; type TodoSearchResultsData = SearchResults['data']; +type TodoMetadata = { + /** Total number of transactions across all reports */ + count: number; + /** Sum of all report totals (in cents) */ + total: number; + /** Currency of the first report, used as reference currency */ + currency: string | undefined; +}; + +function computeMetadata(reports: Report[], transactionsByReportID: Record): TodoMetadata { + let count = 0; + let total = 0; + let currency: string | undefined; + + for (const report of reports) { + if (!report?.reportID) { + continue; + } + + const reportTransactions = transactionsByReportID[report.reportID]; + if (reportTransactions) { + count += reportTransactions.length; + } + + // Expense reports have a negative total, so we need to subtract it from the total + if (report.total) { + total -= report.total; + } + + if (currency === undefined && report.currency) { + currency = report.currency; + } + } + + return {count, total, currency}; +} + /** * Builds a SearchResults-compatible data object from the given reports and related data. * This allows the search UI to use live Onyx data instead of snapshot data when viewing to-do results. @@ -150,12 +187,17 @@ export default function useTodos() { // Build SearchResults-formatted data for each to-do category const todoSearchResultsData = useMemo(() => { - const buildData = (reports: Report[]): TodoSearchResultsData => { + const buildData = (reports: Report[]): {data: TodoSearchResultsData; metadata: TodoMetadata} => { if (reports.length === 0) { // Return empty object like the Search API would when there's no data - return {} as TodoSearchResultsData; + return { + data: {} as TodoSearchResultsData, + metadata: {count: 0, total: 0, currency: undefined}, + }; } - return buildSearchResultsData( + + const metadata = computeMetadata(reports, transactionsByReportID); + const data = buildSearchResultsData( reports, transactionsByReportID, allReports as Record | undefined, @@ -165,6 +207,8 @@ export default function useTodos() { personalDetailsList, allTransactionViolations as Record | undefined, ); + + return {data, metadata}; }; return { From afba4254295de74d8858cd2f5952d5895962e0d0 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 20 Jan 2026 14:13:10 -0700 Subject: [PATCH 19/28] fix prettier --- src/components/Search/SearchContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 73da3a4687f03..17372770c9c25 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -82,7 +82,7 @@ function SearchContextProvider({children}: ChildrenProps) { if (shouldUseLiveData) { const liveData = todoSearchResultsData[currentSearchKey]; const searchInfo: SearchResultsInfo = { - ...snapshotSearchResults?.search ?? defaultSearchInfo, + ...(snapshotSearchResults?.search ?? defaultSearchInfo), count: liveData.metadata.count, total: liveData.metadata.total, currency: liveData.metadata.currency, From bd99c27314c812cf0861a7597a7f83347749da3d Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 20 Jan 2026 14:43:08 -0700 Subject: [PATCH 20/28] update has data --- src/components/Search/SearchContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 17372770c9c25..844eab2d08dc9 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -87,7 +87,7 @@ function SearchContextProvider({children}: ChildrenProps) { total: liveData.metadata.total, currency: liveData.metadata.currency, }; - const hasResults = Object.keys(liveData).length > 0; + 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 return { From 6104632d8db977ac2d6fa5e1f8f628d77e1f3cea Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 20 Jan 2026 14:55:10 -0700 Subject: [PATCH 21/28] use snapshot metadata --- src/components/Search/SearchContext.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 844eab2d08dc9..4729cceb44959 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -83,9 +83,9 @@ function SearchContextProvider({children}: ChildrenProps) { const liveData = todoSearchResultsData[currentSearchKey]; const searchInfo: SearchResultsInfo = { ...(snapshotSearchResults?.search ?? defaultSearchInfo), - count: liveData.metadata.count, - total: liveData.metadata.total, - currency: liveData.metadata.currency, + count: liveData.metadata.count ?? snapshotSearchResults?.search?.count, + total: liveData.metadata.total ?? snapshotSearchResults?.search?.total, + currency: liveData.metadata.currency ?? snapshotSearchResults?.search?.currency, }; const hasResults = Object.keys(liveData.data).length > 0; // For to-do searches, always return a valid SearchResults object (even with empty data) From f372cbe57ce6607a0f7f4dc583c345f371d3617a Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 20 Jan 2026 15:21:57 -0700 Subject: [PATCH 22/28] revert changes to footer --- src/components/Search/SearchContext.tsx | 7 ++-- src/hooks/useTodos.ts | 47 ++----------------------- 2 files changed, 5 insertions(+), 49 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 4729cceb44959..c8099d30e624c 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -83,16 +83,13 @@ function SearchContextProvider({children}: ChildrenProps) { const liveData = todoSearchResultsData[currentSearchKey]; const searchInfo: SearchResultsInfo = { ...(snapshotSearchResults?.search ?? defaultSearchInfo), - count: liveData.metadata.count ?? snapshotSearchResults?.search?.count, - total: liveData.metadata.total ?? snapshotSearchResults?.search?.total, - currency: liveData.metadata.currency ?? snapshotSearchResults?.search?.currency, }; - const hasResults = Object.keys(liveData.data).length > 0; + const hasResults = Object.keys(liveData).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 return { search: {...searchInfo, isLoading: false, hasResults}, - data: liveData.data, + data: liveData, }; } diff --git a/src/hooks/useTodos.ts b/src/hooks/useTodos.ts index 506dda0b5c7ae..1a252b8ac4d76 100644 --- a/src/hooks/useTodos.ts +++ b/src/hooks/useTodos.ts @@ -10,43 +10,6 @@ import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; type TodoSearchResultsData = SearchResults['data']; -type TodoMetadata = { - /** Total number of transactions across all reports */ - count: number; - /** Sum of all report totals (in cents) */ - total: number; - /** Currency of the first report, used as reference currency */ - currency: string | undefined; -}; - -function computeMetadata(reports: Report[], transactionsByReportID: Record): TodoMetadata { - let count = 0; - let total = 0; - let currency: string | undefined; - - for (const report of reports) { - if (!report?.reportID) { - continue; - } - - const reportTransactions = transactionsByReportID[report.reportID]; - if (reportTransactions) { - count += reportTransactions.length; - } - - // Expense reports have a negative total, so we need to subtract it from the total - if (report.total) { - total -= report.total; - } - - if (currency === undefined && report.currency) { - currency = report.currency; - } - } - - return {count, total, currency}; -} - /** * Builds a SearchResults-compatible data object from the given reports and related data. * This allows the search UI to use live Onyx data instead of snapshot data when viewing to-do results. @@ -187,16 +150,12 @@ export default function useTodos() { // Build SearchResults-formatted data for each to-do category const todoSearchResultsData = useMemo(() => { - const buildData = (reports: Report[]): {data: TodoSearchResultsData; metadata: TodoMetadata} => { + const buildData = (reports: Report[]): TodoSearchResultsData => { if (reports.length === 0) { // Return empty object like the Search API would when there's no data - return { - data: {} as TodoSearchResultsData, - metadata: {count: 0, total: 0, currency: undefined}, - }; + return {} as TodoSearchResultsData; } - const metadata = computeMetadata(reports, transactionsByReportID); const data = buildSearchResultsData( reports, transactionsByReportID, @@ -208,7 +167,7 @@ export default function useTodos() { allTransactionViolations as Record | undefined, ); - return {data, metadata}; + return data; }; return { From aac08f70e9aba46eaf77bbda28216fc1cc16847d Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 21 Jan 2026 10:40:15 -0700 Subject: [PATCH 23/28] update footer --- src/components/Search/SearchContext.tsx | 7 ++-- src/hooks/useTodos.ts | 48 +++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index c8099d30e624c..185aa24d4bc69 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -83,13 +83,16 @@ function SearchContextProvider({children}: ChildrenProps) { const liveData = todoSearchResultsData[currentSearchKey]; const searchInfo: SearchResultsInfo = { ...(snapshotSearchResults?.search ?? defaultSearchInfo), + count: snapshotSearchResults?.search.count ?? liveData.metadata.count, + total: snapshotSearchResults?.search.total ?? liveData.metadata.total, + currency: snapshotSearchResults?.search.currency ?? liveData.metadata.currency, }; - const hasResults = Object.keys(liveData).length > 0; + 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 return { search: {...searchInfo, isLoading: false, hasResults}, - data: liveData, + data: liveData.data, }; } diff --git a/src/hooks/useTodos.ts b/src/hooks/useTodos.ts index 1a252b8ac4d76..f17ed5f050d7c 100644 --- a/src/hooks/useTodos.ts +++ b/src/hooks/useTodos.ts @@ -10,6 +10,44 @@ import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; type TodoSearchResultsData = SearchResults['data']; +type TodoMetadata = { + /** Total number of transactions across all reports */ + count: number; + /** Sum of all report totals (in cents) */ + total: number; + /** Currency of the first report, used as reference currency */ + currency: string | undefined; +}; + +function computeMetadata(reports: Report[], transactionsByReportID: Record): TodoMetadata { + let count = 0; + let total = 0; + let currency: string | undefined; + + for (const report of reports) { + if (!report?.reportID) { + continue; + } + + const reportTransactions = transactionsByReportID[report.reportID]; + if (reportTransactions) { + count += reportTransactions.length; + + for (const transaction of reportTransactions) { + if (transaction.groupAmount) { + total -= transaction.groupAmount; + } + + if (currency === undefined && transaction.groupCurrency) { + currency = transaction.groupCurrency; + } + } + } + } + + return {count, total, currency}; +} + /** * Builds a SearchResults-compatible data object from the given reports and related data. * This allows the search UI to use live Onyx data instead of snapshot data when viewing to-do results. @@ -150,12 +188,16 @@ export default function useTodos() { // Build SearchResults-formatted data for each to-do category const todoSearchResultsData = useMemo(() => { - const buildData = (reports: Report[]): TodoSearchResultsData => { + const buildData = (reports: Report[]): {data: TodoSearchResultsData; metadata: TodoMetadata} => { if (reports.length === 0) { // Return empty object like the Search API would when there's no data - return {} as TodoSearchResultsData; + return { + data: {} as TodoSearchResultsData, + metadata: {count: 0, total: 0, currency: undefined}, + }; } + const metadata = computeMetadata(reports, transactionsByReportID); const data = buildSearchResultsData( reports, transactionsByReportID, @@ -167,7 +209,7 @@ export default function useTodos() { allTransactionViolations as Record | undefined, ); - return data; + return {data, metadata}; }; return { From 37261f47bccbcc36d15e93ed1dca16482d69093d Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 21 Jan 2026 12:44:27 -0700 Subject: [PATCH 24/28] only use live data for base queries --- src/components/Search/SearchContext.tsx | 13 ++++++++++--- src/components/Search/index.tsx | 8 ++++---- src/components/Search/types.ts | 2 ++ src/hooks/useOnyx.ts | 5 ++--- src/libs/SearchUIUtils.ts | 12 ++++-------- 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 185aa24d4bc69..e232a44036f5f 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -2,9 +2,10 @@ import React, {useCallback, useContext, useMemo, 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 useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useTodos from '@hooks/useTodos'; import {isMoneyRequestReport} from '@libs/ReportUtils'; -import {isTodoSearch, isTransactionListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils'; +import {getSuggestedSearches, isTodoSearch, isTransactionListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils'; import type {SearchKey} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -48,6 +49,7 @@ const defaultSearchContext: SearchContextProps = { showSelectAllMatchingItems: false, shouldShowFiltersBarLoading: false, currentSearchResults: undefined, + shouldUseLiveData: false, setLastSearchType: () => {}, setCurrentSearchHashAndKey: () => {}, setCurrentSearchQueryJSON: () => {}, @@ -74,13 +76,16 @@ function SearchContextProvider({children}: ChildrenProps) { const {todoSearchResultsData} = useTodos(); const currentSearchKey = searchContextData.currentSearchKey; - const shouldUseLiveData = currentSearchKey && isTodoSearch(currentSearchKey); + const currentSearchHash = searchContextData.currentSearchHash; + const {accountID} = useCurrentUserPersonalDetails(); + const suggestedSearches = useMemo(() => getSuggestedSearches(accountID), [accountID]); + const shouldUseLiveData = !!currentSearchKey && isTodoSearch(currentSearchHash, 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 => { if (shouldUseLiveData) { - const liveData = todoSearchResultsData[currentSearchKey]; + const liveData = todoSearchResultsData[currentSearchKey as keyof typeof todoSearchResultsData]; const searchInfo: SearchResultsInfo = { ...(snapshotSearchResults?.search ?? defaultSearchInfo), count: snapshotSearchResults?.search.count ?? liveData.metadata.count, @@ -253,6 +258,7 @@ function SearchContextProvider({children}: ChildrenProps) { () => ({ ...searchContextData, currentSearchResults, + shouldUseLiveData, removeTransaction, setCurrentSearchHashAndKey, setCurrentSearchQueryJSON, @@ -271,6 +277,7 @@ function SearchContextProvider({children}: ChildrenProps) { [ searchContextData, currentSearchResults, + shouldUseLiveData, removeTransaction, setCurrentSearchHashAndKey, setCurrentSearchQueryJSON, diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 97300e154138f..ff3612831140e 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -238,6 +238,7 @@ function Search({ selectAllMatchingItems, shouldResetSearchQuery, setShouldResetSearchQuery, + shouldUseLiveData, } = useSearchContext(); const [offset, setOffset] = useState(0); @@ -268,9 +269,8 @@ function Search({ const {defaultCardFeed} = useCardFeedsForDisplay(); const suggestedSearches = useMemo(() => getSuggestedSearches(accountID, defaultCardFeed?.id), [defaultCardFeed?.id, accountID]); - const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.similarSearchHash === similarSearchHash)?.key, [suggestedSearches, similarSearchHash]); - const searchDataType = useMemo(() => (isTodoSearch(searchKey) ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type), [searchKey, searchResults?.search?.type]); + const searchDataType = useMemo(() => (shouldUseLiveData ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type), [shouldUseLiveData, searchResults?.search?.type]); const shouldCalculateTotals = useSearchShouldCalculateTotals(searchKey, similarSearchHash, offset === 0); const previousReportActions = usePrevious(reportActions); @@ -362,13 +362,13 @@ function Search({ // There's a race condition in Onyx which makes it return data from the previous Search, so in addition to checking that the data is loaded // we also need to check that the searchResults matches the type and status of the current search - const isDataLoaded = isTodoSearch(searchKey) || isSearchDataLoaded(searchResults, queryJSON); + const isDataLoaded = shouldUseLiveData || isSearchDataLoaded(searchResults, queryJSON); const hasErrors = Object.keys(searchResults?.errors ?? {}).length > 0 && !isOffline; // For to-do searches, we never show loading state since the data is always available locally from Onyx const shouldShowLoadingState = - !isTodoSearch(searchKey) && + !shouldUseLiveData && !isOffline && (!isDataLoaded || (!!searchResults?.search.isLoading && Array.isArray(searchResults?.data) && searchResults?.data.length === 0) || (hasErrors && !searchRequestResponseStatusCode)); const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0; diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index b4272937bfeda..29a47ad886b73 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -136,6 +136,8 @@ type SearchContextData = { type SearchContextProps = SearchContextData & { currentSearchResults: SearchResults | undefined; + /** Whether we're on a main to-do search and should use live Onyx data instead of snapshots */ + shouldUseLiveData: boolean; 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/hooks/useOnyx.ts b/src/hooks/useOnyx.ts index 11edeea504f6b..3834c1af47ea3 100644 --- a/src/hooks/useOnyx.ts +++ b/src/hooks/useOnyx.ts @@ -5,7 +5,6 @@ import {useOnyx as originalUseOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry, OnyxKey, OnyxValue, UseOnyxOptions, UseOnyxResult} from 'react-native-onyx'; import {SearchContext} from '@components/Search/SearchContext'; import {useIsOnSearch} from '@components/Search/SearchScopeProvider'; -import {isTodoSearch} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {SearchResults} from '@src/types/onyx'; @@ -55,9 +54,9 @@ const useOnyx: OriginalUseOnyx = > | undefined; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 0ac037779519f..1c82744af9bf3 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2760,14 +2760,10 @@ function isCorrectSearchUserName(displayName?: string) { return displayName && displayName.toUpperCase() !== CONST.REPORT.OWNER_EMAIL_FAKE; } -function isTodoSearch(currentSearchKey: SearchKey | undefined) { - return ( - !!currentSearchKey && - (currentSearchKey === CONST.SEARCH.SEARCH_KEYS.SUBMIT || - currentSearchKey === CONST.SEARCH.SEARCH_KEYS.APPROVE || - currentSearchKey === CONST.SEARCH.SEARCH_KEYS.PAY || - currentSearchKey === CONST.SEARCH.SEARCH_KEYS.EXPORT) - ); +function isTodoSearch(hash: number, suggestedSearches: Record) { + const TODO_KEYS: SearchKey[] = [CONST.SEARCH.SEARCH_KEYS.SUBMIT, CONST.SEARCH.SEARCH_KEYS.APPROVE, CONST.SEARCH.SEARCH_KEYS.PAY, CONST.SEARCH.SEARCH_KEYS.EXPORT]; + const matchedSearchKey = Object.values(suggestedSearches).find((search) => search.hash === hash)?.key; + return !!matchedSearchKey && TODO_KEYS.includes(matchedSearchKey); } // eslint-disable-next-line @typescript-eslint/max-params From 3e6352bb490540654da892693374d34af1f32d4f Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 21 Jan 2026 12:57:41 -0700 Subject: [PATCH 25/28] fix tests --- src/components/Search/index.tsx | 1 - tests/unit/TransactionGroupListItemTest.tsx | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index ff3612831140e..a1b05abdc1702 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -50,7 +50,6 @@ import { isSearchDataLoaded, isSearchResultsEmpty as isSearchResultsEmptyUtil, isTaskListItemType, - isTodoSearch, isTransactionCardGroupListItemType, isTransactionGroupListItemType, isTransactionListItemType, diff --git a/tests/unit/TransactionGroupListItemTest.tsx b/tests/unit/TransactionGroupListItemTest.tsx index 0a3dc1ebb35eb..71b9fae209316 100644 --- a/tests/unit/TransactionGroupListItemTest.tsx +++ b/tests/unit/TransactionGroupListItemTest.tsx @@ -22,6 +22,7 @@ jest.mock('@libs/SearchUIUtils', () => ({ getSections: jest.fn(() => []), isCorrectSearchUserName: jest.fn(() => true), getTableMinWidth: jest.fn(() => 0), + getSuggestedSearches: jest.fn(() => ({})), })); const mockTransaction: TransactionListItemType = { From d5adab1664e3ac8a826047ef6bb28a2eb54cc2dd Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 21 Jan 2026 17:07:21 -0700 Subject: [PATCH 26/28] update hook to trigger search on update --- src/components/Search/SearchContext.tsx | 6 +++--- src/components/Search/index.tsx | 1 + src/hooks/useSearchHighlightAndScroll.ts | 6 +++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index e232a44036f5f..1e421079f4113 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -88,9 +88,9 @@ function SearchContextProvider({children}: ChildrenProps) { const liveData = todoSearchResultsData[currentSearchKey as keyof typeof todoSearchResultsData]; const searchInfo: SearchResultsInfo = { ...(snapshotSearchResults?.search ?? defaultSearchInfo), - count: snapshotSearchResults?.search.count ?? liveData.metadata.count, - total: snapshotSearchResults?.search.total ?? liveData.metadata.total, - currency: snapshotSearchResults?.search.currency ?? liveData.metadata.currency, + 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) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index a1b05abdc1702..3934881d95e5d 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -357,6 +357,7 @@ function Search({ shouldCalculateTotals, reportActions, previousReportActions, + shouldUseLiveData, }); // There's a race condition in Onyx which makes it return data from the previous Search, so in addition to checking that the data is loaded diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts index 87d08877018a1..a66f5e43ef60e 100644 --- a/src/hooks/useSearchHighlightAndScroll.ts +++ b/src/hooks/useSearchHighlightAndScroll.ts @@ -23,6 +23,7 @@ type UseSearchHighlightAndScroll = { searchKey: SearchKey | undefined; offset: number; shouldCalculateTotals: boolean; + shouldUseLiveData: boolean; }; /** @@ -38,6 +39,7 @@ function useSearchHighlightAndScroll({ searchKey, offset, shouldCalculateTotals, + shouldUseLiveData, }: UseSearchHighlightAndScroll) { const isFocused = useIsFocused(); const {isOffline} = useNetwork(); @@ -147,12 +149,14 @@ function useSearchHighlightAndScroll({ ]); useEffect(() => { + // For live data, isLoading is always false, so we also need to reset when searchResultsData changes + // For snapshot data, we wait for isLoading to become false after the API call completes if (searchResults?.search?.isLoading) { return; } searchTriggeredRef.current = false; - }, [searchResults?.search?.isLoading]); + }, [searchResults?.search?.isLoading, shouldUseLiveData, searchResultsData]); // Initialize the set with existing IDs only once useEffect(() => { From cf8824df5f246ee02a3b7012bb5d92d5f6faf05f Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 21 Jan 2026 17:33:51 -0700 Subject: [PATCH 27/28] do not include chat reports --- src/hooks/useTodos.ts | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/hooks/useTodos.ts b/src/hooks/useTodos.ts index f17ed5f050d7c..40d1912b3a4c5 100644 --- a/src/hooks/useTodos.ts +++ b/src/hooks/useTodos.ts @@ -55,7 +55,6 @@ function computeMetadata(reports: Report[], transactionsByReportID: Record, - allReports: Record | undefined, allPolicies: Record | undefined, allReportActions: Record | undefined, allReportNameValuePairs: Record | undefined, @@ -77,21 +76,12 @@ function buildSearchResultsData( } } - if (report.chatReportID) { - // Add the chat report itself (needed by getChatReport > canIOUBePaid for pay eligibility checks) - if (allReports) { - const chatReportKey = `${ONYXKEYS.COLLECTION.REPORT}${report.chatReportID}`; - if (allReports[chatReportKey] && !data[chatReportKey]) { - data[chatReportKey] = allReports[chatReportKey]; - } - } - - // Add the report name value pairs for the chat report - if (allReportNameValuePairs) { - const nvpKey = `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.chatReportID}`; - if (allReportNameValuePairs[nvpKey] && !data[nvpKey]) { - data[nvpKey] = allReportNameValuePairs[nvpKey]; - } + // Add the report name value pairs for the chat report (needed for pay eligibility checks) + // Note: We don't add the chat report itself to match API behavior and avoid affecting shouldShowYear calculations + if (report.chatReportID && allReportNameValuePairs) { + const nvpKey = `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.chatReportID}`; + if (allReportNameValuePairs[nvpKey] && !data[nvpKey]) { + data[nvpKey] = allReportNameValuePairs[nvpKey]; } } @@ -201,7 +191,6 @@ export default function useTodos() { const data = buildSearchResultsData( reports, transactionsByReportID, - allReports as Record | undefined, allPolicies as Record | undefined, allReportActions as Record | undefined, allReportNameValuePairs as Record | undefined, @@ -224,7 +213,6 @@ export default function useTodos() { reportsToPay, reportsToExport, transactionsByReportID, - allReports, allPolicies, allReportActions, allReportNameValuePairs, From 3f79b847351051e34ee260197efd500594d30f3d Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 21 Jan 2026 17:34:41 -0700 Subject: [PATCH 28/28] fix ts --- tests/unit/useSearchHighlightAndScrollTest.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/useSearchHighlightAndScrollTest.ts b/tests/unit/useSearchHighlightAndScrollTest.ts index 8bf7650b979fe..30ac57245929a 100644 --- a/tests/unit/useSearchHighlightAndScrollTest.ts +++ b/tests/unit/useSearchHighlightAndScrollTest.ts @@ -25,6 +25,7 @@ afterEach(() => { describe('useSearchHighlightAndScroll', () => { const baseProps: UseSearchHighlightAndScroll = { + shouldUseLiveData: false, searchResults: { data: { personalDetailsList: {},