From a15c5d3fb19e334a9612aaf5ad6b28bf9cb3efd9 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Fri, 20 Mar 2026 09:40:16 -0400 Subject: [PATCH 1/5] add new key and set it --- src/ONYXKEYS.ts | 4 ++++ src/libs/actions/Search.ts | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 10ff7122d9984..b1e5163f5efda 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -395,6 +395,9 @@ const ONYXKEYS = { /** Set when we are loading fresh subscription/billing data from the server */ IS_LOADING_SUBSCRIPTION_DATA: 'isLoadingSubscriptionData', + /** Set when OpenSearchPage has been fetched for the first time */ + IS_SEARCH_PAGE_DATA_LOADED: 'isSearchPageDataLoaded', + /** Is the app loaded? */ HAS_LOADED_APP: 'hasLoadedApp', @@ -1359,6 +1362,7 @@ type OnyxValuesMapping = { [ONYXKEYS.IS_LOADING_SHARE_BANK_ACCOUNTS]: boolean; [ONYXKEYS.IS_LOADING_POLICY_CODING_RULES_PREVIEW]: boolean; [ONYXKEYS.IS_LOADING_SUBSCRIPTION_DATA]: boolean; + [ONYXKEYS.IS_SEARCH_PAGE_DATA_LOADED]: boolean; [ONYXKEYS.IS_LOADING_REPORT_DATA]: boolean; [ONYXKEYS.IS_TEST_TOOLS_MODAL_OPEN]: boolean; [ONYXKEYS.IS_LOADING_APP]: boolean; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 41269099bcc40..9fb960e5b7f11 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -441,7 +441,15 @@ function deleteSavedSearch(hash: number) { } function openSearchPage({includePartiallySetupBankAccounts}: OpenSearchPageParams) { - API.read(READ_COMMANDS.OPEN_SEARCH_PAGE, {includePartiallySetupBankAccounts}); + const successData: Array> = [ + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.IS_SEARCH_PAGE_DATA_LOADED, + value: true, + }, + ]; + + API.read(READ_COMMANDS.OPEN_SEARCH_PAGE, {includePartiallySetupBankAccounts}, {successData}); } // Tracks in-flight search requests by hash+offset to prevent duplicate API calls From 908e3eed7c23fd432b88a4879141a4b4c98921c7 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Fri, 20 Mar 2026 09:41:25 -0400 Subject: [PATCH 2/5] use the onyx key --- src/pages/Search/SearchTypeMenu.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 1646efb6d64fc..488b2e91fc460 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -35,7 +35,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { const styles = useThemeStyles(); const {singleExecution} = useSingleExecution(); const {translate} = useLocalize(); - const {typeMenuSections, shouldShowSuggestedSearchSkeleton, activeItemIndex} = useSearchTypeMenuSections({hash, similarSearchHash}); + const {typeMenuSections, activeItemIndex} = useSearchTypeMenuSections({hash, similarSearchHash}); const expensifyIcons = useMemoizedLazyExpensifyIcons([ 'Basket', 'CalendarSolid', @@ -51,6 +51,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { 'Folder', ] as const); const {clearSelectedTransactions} = useSearchActionsContext(); + const [isSearchDataLoaded] = useOnyx(ONYXKEYS.IS_SEARCH_PAGE_DATA_LOADED); const [reportCounts = CONST.EMPTY_TODOS_REPORT_COUNTS] = useOnyx(ONYXKEYS.DERIVED.TODOS, {selector: todosReportCountsSelector}); const route = useRoute(); @@ -93,7 +94,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { ref={scrollViewRef} showsVerticalScrollIndicator={false} > - {shouldShowSuggestedSearchSkeleton ? ( + {isSearchDataLoaded ? ( From ef5ea7124528980bec420b779e5921c5815fb739 Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Fri, 20 Mar 2026 09:42:21 -0400 Subject: [PATCH 3/5] check offline too --- src/pages/Search/SearchTypeMenu.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 488b2e91fc460..8ce0814ab7e61 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -10,6 +10,7 @@ import type {SearchQueryJSON} from '@components/Search/types'; import Text from '@components/Text'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useSearchTypeMenuSections from '@hooks/useSearchTypeMenuSections'; import useSingleExecution from '@hooks/useSingleExecution'; @@ -33,8 +34,9 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { const {hash, similarSearchHash} = queryJSON ?? {}; const styles = useThemeStyles(); - const {singleExecution} = useSingleExecution(); + const {isOffline} = useNetwork(); const {translate} = useLocalize(); + const {singleExecution} = useSingleExecution(); const {typeMenuSections, activeItemIndex} = useSearchTypeMenuSections({hash, similarSearchHash}); const expensifyIcons = useMemoizedLazyExpensifyIcons([ 'Basket', @@ -88,13 +90,15 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: searchQuery})); }); + const areSuggestedSearchesLoading = !isSearchDataLoaded && !isOffline; + return ( - {isSearchDataLoaded ? ( + {areSuggestedSearchesLoading ? ( From 9e8ca63a9aba15b9faa6812e0044ffb943da378d Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Fri, 20 Mar 2026 09:54:58 -0400 Subject: [PATCH 4/5] add is loading onyx check --- src/hooks/useSearchTypeMenuSections.ts | 5 ----- src/pages/Search/SearchTypeMenu.tsx | 5 +++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/hooks/useSearchTypeMenuSections.ts b/src/hooks/useSearchTypeMenuSections.ts index 4bcf26e586cd9..2dd1d2154c237 100644 --- a/src/hooks/useSearchTypeMenuSections.ts +++ b/src/hooks/useSearchTypeMenuSections.ts @@ -95,10 +95,6 @@ const useSearchTypeMenuSections = (queryParams?: UseSearchTypeMenuSectionsParams openCreateReportConfirmation(); }, [pendingReportCreation, openCreateReportConfirmation]); - const isSuggestedSearchDataReady = useMemo(() => { - return Object.values(allPolicies ?? {}).some((policy) => policy?.employeeList !== undefined && policy?.exporter !== undefined); - }, [allPolicies]); - const typeMenuSections = useMemo( () => createTypeMenuSections({ @@ -149,7 +145,6 @@ const useSearchTypeMenuSections = (queryParams?: UseSearchTypeMenuSectionsParams return { typeMenuSections, - shouldShowSuggestedSearchSkeleton: !isSuggestedSearchDataReady && !isOffline, activeItemIndex, }; }; diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 8ce0814ab7e61..d4b3645339019 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -22,6 +22,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import todosReportCountsSelector from '@src/selectors/Todos'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import SavedSearchList from './SavedSearchList'; import SearchTypeMenuItem from './SearchTypeMenuItem'; import SuggestedSearchSkeleton from './SuggestedSearchSkeleton'; @@ -53,7 +54,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { 'Folder', ] as const); const {clearSelectedTransactions} = useSearchActionsContext(); - const [isSearchDataLoaded] = useOnyx(ONYXKEYS.IS_SEARCH_PAGE_DATA_LOADED); + const [isSearchDataLoaded, isSearchDataLoadedResult] = useOnyx(ONYXKEYS.IS_SEARCH_PAGE_DATA_LOADED); const [reportCounts = CONST.EMPTY_TODOS_REPORT_COUNTS] = useOnyx(ONYXKEYS.DERIVED.TODOS, {selector: todosReportCountsSelector}); const route = useRoute(); @@ -90,7 +91,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: searchQuery})); }); - const areSuggestedSearchesLoading = !isSearchDataLoaded && !isOffline; + const areSuggestedSearchesLoading = !isOffline && !isSearchDataLoaded && !isLoadingOnyxValue(isSearchDataLoadedResult); return ( Date: Fri, 20 Mar 2026 10:43:31 -0400 Subject: [PATCH 5/5] remove old tests --- tests/unit/useSearchTypeMenuSectionsTest.ts | 114 -------------------- 1 file changed, 114 deletions(-) delete mode 100644 tests/unit/useSearchTypeMenuSectionsTest.ts diff --git a/tests/unit/useSearchTypeMenuSectionsTest.ts b/tests/unit/useSearchTypeMenuSectionsTest.ts deleted file mode 100644 index 29ad606f70541..0000000000000 --- a/tests/unit/useSearchTypeMenuSectionsTest.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import {renderHook} from '@testing-library/react-native'; -import useNetwork from '@hooks/useNetwork'; -import useSearchTypeMenuSections from '@hooks/useSearchTypeMenuSections'; -import ONYXKEYS from '@src/ONYXKEYS'; - -jest.mock('@libs/ReportUtils', () => ({ - getPersonalDetailsForAccountID: jest.fn(), - hasEmptyReportsForPolicy: jest.fn(() => false), - hasViolations: jest.fn(() => false), -})); - -jest.mock('@userActions/Report', () => ({ - createNewReport: jest.fn(() => ({reportID: 'mock-report-id'})), -})); - -jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}}))); -jest.mock('@hooks/useCreateEmptyReportConfirmation', () => jest.fn(() => ({openCreateReportConfirmation: jest.fn()}))); -jest.mock('@hooks/useNetwork', () => jest.fn(() => ({isOffline: false}))); -jest.mock('@hooks/usePermissions', () => jest.fn(() => ({isBetaEnabled: jest.fn(() => false)}))); - -const onyxData: Record = {}; - -const mockUseOnyx = jest.fn( - ( - key: string, - options?: { - selector?: (value: unknown) => unknown; - }, - ) => { - const value = onyxData[key]; - const selectedValue = options?.selector ? options.selector(value as never) : value; - return [selectedValue]; - }, -); - -jest.mock('@hooks/useOnyx', () => ({ - __esModule: true, - default: (key: string, options?: {selector?: (value: unknown) => unknown}) => mockUseOnyx(key, options), -})); - -const mockUseMappedPolicies = jest.fn(() => [onyxData[ONYXKEYS.COLLECTION.POLICY], {}]); - -jest.mock('@hooks/useMappedPolicies', () => ({ - __esModule: true, - default: () => mockUseMappedPolicies(), -})); - -describe('useSearchTypeMenuSections', () => { - beforeEach(() => { - onyxData[ONYXKEYS.COLLECTION.POLICY] = {}; - onyxData[ONYXKEYS.SESSION] = {email: 'test@example.com', accountID: 1}; - onyxData[ONYXKEYS.SAVED_SEARCHES] = {}; - onyxData[ONYXKEYS.COLLECTION.REPORT] = {}; - - mockUseOnyx.mockClear(); - }); - - it('shows suggested search skeleton when policies are missing employeeList', () => { - onyxData[ONYXKEYS.COLLECTION.POLICY] = { - policy1: { - id: 'policy1', - employeeList: undefined, - exporter: 'test@gmail.com', - }, - }; - - const {result} = renderHook(() => useSearchTypeMenuSections()); - - expect(result.current.shouldShowSuggestedSearchSkeleton).toBe(true); - }); - - it('shows suggested search skeleton when policies are missing exporter', () => { - onyxData[ONYXKEYS.COLLECTION.POLICY] = { - policy1: { - id: 'policy1', - employeeList: {'test@gmail.com': {accountID: 10000}}, - exporter: undefined, - }, - }; - - const {result} = renderHook(() => useSearchTypeMenuSections()); - - expect(result.current.shouldShowSuggestedSearchSkeleton).toBe(true); - }); - - it('hides suggested search skeleton when at least one policy has required data', () => { - onyxData[ONYXKEYS.COLLECTION.POLICY] = { - policy1: { - id: 'policy1', - employeeList: {'test@gmail.com': {accountID: 10000}}, - exporter: '', - }, - policy2: { - id: 'policy2', - employeeList: undefined, - exporter: undefined, - }, - }; - - const {result} = renderHook(() => useSearchTypeMenuSections()); - - expect(result.current.shouldShowSuggestedSearchSkeleton).toBe(false); - }); - - it('does not show suggested search skeleton when offline', () => { - (useNetwork as jest.Mock).mockReturnValue({ - isOffline: true, - }); - const {result} = renderHook(() => useSearchTypeMenuSections()); - - expect(result.current.shouldShowSuggestedSearchSkeleton).toBe(false); - }); -});