From e4bafc2a523497a2d90566e90254e592709a727b Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Mon, 12 Jan 2026 14:35:26 +0100 Subject: [PATCH 01/19] add PERSONAL_AND_WORKSPACE_CARD_LIST derived value --- src/ONYXKEYS.ts | 2 ++ .../ReportActionItem/MoneyRequestView.tsx | 6 ++---- .../Search/SearchAutocompleteList.tsx | 4 ++-- .../SearchPageHeader/SearchFiltersBar.tsx | 5 +---- .../SearchPageHeaderInput.tsx | 5 +---- .../Search/SearchRouter/SearchRouter.tsx | 7 ++----- .../SearchRouter/buildSubstitutionsMap.ts | 2 +- src/hooks/useAdvancedSearchFilters.ts | 10 +++------ src/hooks/useCardFeedsForDisplay.ts | 7 ++----- src/hooks/useFilterFormValues.tsx | 6 +----- src/hooks/useSearchTypeMenu.tsx | 5 +---- src/libs/CardFeedUtils.ts | 4 ++-- src/libs/CardUtils.ts | 18 +++++++++++++--- src/libs/SearchQueryUtils.ts | 10 ++++----- src/libs/SearchUIUtils.ts | 2 +- .../OnyxDerived/ONYX_DERIVED_VALUES.ts | 2 ++ .../configs/personalAndWorkspaceCardList.ts | 21 +++++++++++++++++++ src/pages/Search/AdvancedSearchFilters.tsx | 14 ++++++------- src/pages/Search/SearchTypeMenu.tsx | 5 +---- src/types/onyx/DerivedValues.ts | 15 ++++++++++++- src/types/onyx/index.ts | 8 ++++++- 21 files changed, 92 insertions(+), 66 deletions(-) create mode 100644 src/libs/actions/OnyxDerived/configs/personalAndWorkspaceCardList.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 47dbc171a2b7..41373820de67 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -982,6 +982,7 @@ const ONYXKEYS = { REPORT_ATTRIBUTES: 'reportAttributes', REPORT_TRANSACTIONS_AND_VIOLATIONS: 'reportTransactionsAndViolations', OUTSTANDING_REPORTS_BY_POLICY_ID: 'outstandingReportsByPolicyID', + PERSONAL_AND_WORKSPACE_CARD_LIST: 'personalAndWorkspaceCardList', }, /** Stores HybridApp specific state required to interoperate with OldDot */ @@ -1388,6 +1389,7 @@ type OnyxDerivedValuesMapping = { [ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: OnyxTypes.ReportAttributesDerivedValue; [ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS]: OnyxTypes.ReportTransactionsAndViolationsDerivedValue; [ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID]: OnyxTypes.OutstandingReportsByPolicyIDDerivedValue; + [ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST]: OnyxTypes.PersonalAndWorkspaceCardListDerivedValue; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index c2e31941a049..fdc68a0755b1 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -32,7 +32,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionViolations from '@hooks/useTransactionViolations'; import type {ViolationField} from '@hooks/useViolations'; import useViolations from '@hooks/useViolations'; -import {filterPersonalCards, getCompanyCardDescription, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; +import {getCompanyCardDescription} from '@libs/CardUtils'; import {getDecodedCategoryName, isCategoryMissing} from '@libs/CategoryUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; @@ -227,9 +227,7 @@ function MoneyRequestView({ const targetPolicyID = updatedTransaction?.reportID ? parentReport?.policyID : policyID; const allPolicyTags = usePolicyTags(); const policyTagList = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${targetPolicyID}`]; - const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true}); - const [companyCardList] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true}); - const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(companyCardList ?? CONST.EMPTY_OBJECT, cardList), [companyCardList, cardList]); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${getNonEmptyStringOnyxID(linkedTransactionID)}`, {canBeMissing: true}); const transactionViolations = useTransactionViolations(transaction?.transactionID); diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 9bf74be30480..de18e2bea6f3 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -106,7 +106,7 @@ type SearchAutocompleteListProps = { allFeeds: Record | undefined; /** All cards */ - allCards: CardList; + allCards: CardList | undefined; /** Reference to the outer element */ ref?: ForwardedRef; @@ -179,7 +179,7 @@ function SearchAutocompleteList({ personalDetails, reports, allFeeds, - allCards, + allCards = CONST.EMPTY_OBJECT, ref, }: SearchAutocompleteListProps) { const styles = useThemeStyles(); diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx index 4f5fbc652ced..66786ee81ff9 100644 --- a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx @@ -38,7 +38,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWorkspaceList from '@hooks/useWorkspaceList'; import {close} from '@libs/actions/Modal'; import {handleBulkPayItemSelected, updateAdvancedFilters} from '@libs/actions/Search'; -import {filterPersonalCards, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; @@ -105,9 +104,8 @@ function SearchFiltersBar({ const {selectedTransactions, selectAllMatchingItems, areAllMatchingItemsSelected, showSelectAllMatchingItems, shouldShowFiltersBarLoading} = useSearchContext(); const [email] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true, selector: emailSelector}); - const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true}); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); - const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true}); const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true}); const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: true}); const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext); @@ -138,7 +136,6 @@ function SearchFiltersBar({ })); }, [workspaces]); - const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]); const selectedTransactionsKeys = useMemo(() => Object.keys(selectedTransactions ?? {}), [selectedTransactions]); const hasMultipleOutputCurrency = useMemo(() => { const policies = Object.values(allPolicies ?? {}).filter((policy): policy is Policy => isPaidGroupPolicy(policy)); diff --git a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx index 4ecd440d4a51..d2f2b1048234 100644 --- a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx +++ b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx @@ -26,7 +26,6 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {navigateToAndOpenReport} from '@libs/actions/Report'; import {setSearchContext} from '@libs/actions/Search'; -import {filterPersonalCards, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import {getAllTaxRates} from '@libs/PolicyUtils'; @@ -62,9 +61,7 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false}); const taxRates = useMemo(() => getAllTaxRates(policies), [policies]); - const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true}); - const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true}); - const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true}); const {inputQuery: originalInputQuery} = queryJSON; const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: false}); diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 8f547047cb0e..3836c40b4149 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -1,6 +1,6 @@ import {findFocusedRoute} from '@react-navigation/native'; import {deepEqual} from 'fast-equals'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import type {TextInputProps} from 'react-native'; import {InteractionManager, View} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; @@ -26,7 +26,6 @@ import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useRootNavigationState from '@hooks/useRootNavigationState'; import useThemeStyles from '@hooks/useThemeStyles'; -import {filterPersonalCards, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; import {scrollToRight} from '@libs/InputUtils'; import Log from '@libs/Log'; import backHistory from '@libs/Navigation/helpers/backHistory'; @@ -105,9 +104,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla const personalDetails = usePersonalDetails(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); - const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true}); - const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true}); - const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true}); const {shouldUseNarrowLayout} = useResponsiveLayout(); const listRef = useRef(null); diff --git a/src/components/Search/SearchRouter/buildSubstitutionsMap.ts b/src/components/Search/SearchRouter/buildSubstitutionsMap.ts index 82d874cd706b..11502022c4d6 100644 --- a/src/components/Search/SearchRouter/buildSubstitutionsMap.ts +++ b/src/components/Search/SearchRouter/buildSubstitutionsMap.ts @@ -29,7 +29,7 @@ function buildSubstitutionsMap( personalDetails: PersonalDetailsList | undefined, reports: OnyxCollection, allTaxRates: Record, - cardList: CardList, + cardList: CardList | undefined, cardFeeds: OnyxCollection, policies: OnyxCollection, currentUserAccountID: number, diff --git a/src/hooks/useAdvancedSearchFilters.ts b/src/hooks/useAdvancedSearchFilters.ts index ac6c3a33c56a..4558bfc6ca62 100644 --- a/src/hooks/useAdvancedSearchFilters.ts +++ b/src/hooks/useAdvancedSearchFilters.ts @@ -1,7 +1,6 @@ import {emailSelector} from '@selectors/Session'; -import {useMemo} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; -import {filterPersonalCards, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; +import {filterCardsHiddenFromSearch} from '@libs/CardUtils'; import {getAllTaxRates, getTagNamesFromTagsLists, isPolicyFeatureEnabled} from '@libs/PolicyUtils'; import {getAllPolicyValues} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; @@ -206,10 +205,7 @@ function useAdvancedSearchFilters() { const {localeCompare} = useLocalize(); const [searchAdvancedFilters = getEmptyObject()] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true}); const policyID = searchAdvancedFilters.policyID; - const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: false}); - const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: false}); - const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList, true), [userCardList, workspaceCardFeeds]); - + const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true, selector: filterCardsHiddenFromSearch}); const [policies = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false}); const [allPolicyCategories = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES, { canBeMissing: false, @@ -252,7 +248,7 @@ function useAdvancedSearchFilters() { const shouldDisplayAttendeeFilter = isFeatureEnabledInPolicies(policies, CONST.POLICY.MORE_FEATURES.IS_ATTENDEE_TRACKING_ENABLED); const shouldDisplayCategoryFilter = shouldDisplayFilter(nonPersonalPolicyCategoryCount, areCategoriesEnabled, selectedPolicyCategories?.length > 0); const shouldDisplayTagFilter = shouldDisplayFilter(tagListsUnpacked.length, areTagsEnabled, !!selectedPolicyTagLists); - const shouldDisplayCardFilter = shouldDisplayFilter(Object.keys(allCards).length, areCardsEnabled); + const shouldDisplayCardFilter = shouldDisplayFilter(Object.keys(allCards ?? {}).length, areCardsEnabled); const shouldDisplayTaxFilter = shouldDisplayFilter(Object.keys(taxRates).length, areTaxEnabled); const shouldDisplayWorkspaceFilter = workspaces.some((section) => section.data.length > 1); const shouldDisplayGroupCurrencyFilter = !!searchAdvancedFilters.groupBy; diff --git a/src/hooks/useCardFeedsForDisplay.ts b/src/hooks/useCardFeedsForDisplay.ts index ec83214640f2..8dc136ac4438 100644 --- a/src/hooks/useCardFeedsForDisplay.ts +++ b/src/hooks/useCardFeedsForDisplay.ts @@ -1,9 +1,8 @@ import {useMemo} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; import {getCardFeedsForDisplay, getCardFeedsForDisplayPerPolicy} from '@libs/CardFeedUtils'; -import {filterPersonalCards, isCustomFeed, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; +import {isCustomFeed} from '@libs/CardUtils'; import {isPaidGroupPolicy} from '@libs/PolicyUtils'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {CompanyCardFeed, Policy} from '@src/types/onyx'; import useLocalize from './useLocalize'; @@ -58,9 +57,7 @@ const useCardFeedsForDisplay = () => { return commercialFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0); }, [eligiblePoliciesIDs, activePolicyID, cardFeedsByPolicy, localeCompare]); - const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true}); - const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true}); - const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const expensifyCards = getCardFeedsForDisplay({}, allCards); const defaultExpensifyCard = Object.values(expensifyCards)?.at(0); diff --git a/src/hooks/useFilterFormValues.tsx b/src/hooks/useFilterFormValues.tsx index f26fa41f8d2c..4627b95edc36 100644 --- a/src/hooks/useFilterFormValues.tsx +++ b/src/hooks/useFilterFormValues.tsx @@ -1,9 +1,7 @@ import {usePersonalDetails} from '@components/OnyxListItemProvider'; import type {SearchQueryJSON} from '@components/Search/types'; -import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; import {getAllTaxRates} from '@libs/PolicyUtils'; import {buildFilterFormValuesFromQuery} from '@libs/SearchQueryUtils'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {SearchAdvancedFiltersForm} from '@src/types/form'; import type {CurrencyList} from '@src/types/onyx'; @@ -13,16 +11,14 @@ import useOnyx from './useOnyx'; const useFilterFormValues = (queryJSON?: SearchQueryJSON) => { const personalDetails = usePersonalDetails(); - const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); const [policyTagsLists] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {canBeMissing: true}); const [policyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES, {canBeMissing: true}); - const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true}); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: true}); const taxRates = getAllTaxRates(policies); - const allCards = mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList); const formValues = queryJSON ? buildFilterFormValuesFromQuery(queryJSON, policyCategories, policyTagsLists, currencyList, personalDetails, allCards, allReports, taxRates) diff --git a/src/hooks/useSearchTypeMenu.tsx b/src/hooks/useSearchTypeMenu.tsx index d4bcf3326b20..89cdd92b3541 100644 --- a/src/hooks/useSearchTypeMenu.tsx +++ b/src/hooks/useSearchTypeMenu.tsx @@ -7,7 +7,6 @@ import {useSearchContext} from '@components/Search/SearchContext'; import type {SearchQueryJSON} from '@components/Search/types'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import {setSearchContext} from '@libs/actions/Search'; -import {filterPersonalCards, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getAllTaxRates} from '@libs/PolicyUtils'; import {buildSearchQueryJSON, buildUserReadableQueryString, shouldSkipSuggestedSearchNavigation as shouldSkipSuggestedSearchNavigationForQuery} from '@libs/SearchQueryUtils'; @@ -46,8 +45,7 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) { const personalDetails = usePersonalDetails(); const [reports = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); const taxRates = getAllTaxRates(allPolicies); - const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true}); - const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true}); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true}); const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: false}); const expensifyIcons = useMemoizedLazyExpensifyIcons([ @@ -65,7 +63,6 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) { const [isPopoverVisible, setIsPopoverVisible] = useState(false); - const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]); const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true}); const flattenedMenuItems = useMemo(() => typeMenuSections.flatMap((section) => section.menuItems), [typeMenuSections]); diff --git a/src/libs/CardFeedUtils.ts b/src/libs/CardFeedUtils.ts index 6aa9a7d1fad7..b4020cb7e544 100644 --- a/src/libs/CardFeedUtils.ts +++ b/src/libs/CardFeedUtils.ts @@ -427,7 +427,7 @@ const generateSelectedCards = ( * * The `allCards` parameter is only used to determine if we should add the "Expensify Card" feeds. */ -function getCardFeedsForDisplay(allCardFeeds: OnyxCollection, allCards: CardList): CardFeedsForDisplay { +function getCardFeedsForDisplay(allCardFeeds: OnyxCollection, allCards: CardList | undefined): CardFeedsForDisplay { const cardFeedsForDisplay = {} as CardFeedsForDisplay; for (const [domainKey, cardFeeds] of Object.entries(allCardFeeds ?? {})) { @@ -454,7 +454,7 @@ function getCardFeedsForDisplay(allCardFeeds: OnyxCollection, allCard } } - for (const card of Object.values(allCards)) { + for (const card of Object.values(allCards ?? {})) { if (card.bank !== CONST.EXPENSIFY_CARD.BANK || !card.fundID) { continue; } diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 6bbefbe67f06..eb851f560814 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -145,10 +145,10 @@ function isCardClosed(card: Card) { return card?.state === CONST.EXPENSIFY_CARD.STATE.CLOSED; } -function mergeCardListWithWorkspaceFeeds(workspaceFeeds: Record, cardList: CardList | undefined, shouldExcludeCardHiddenFromSearch = false) { +function mergeCardListWithWorkspaceFeeds(workspaceFeeds: Record, cardList: CardList | undefined) { const feedCards: CardList = {}; for (const card of Object.values(cardList ?? {})) { - if (!isCard(card) || (shouldExcludeCardHiddenFromSearch && isCardHiddenFromSearch(card))) { + if (!isCard(card)) { continue; } @@ -157,7 +157,7 @@ function mergeCardListWithWorkspaceFeeds(workspaceFeeds: Record, currencyList: OnyxTypes.CurrencyList, personalDetails: OnyxTypes.PersonalDetailsList | undefined, - cardList: OnyxTypes.CardList, + cardList: OnyxTypes.CardList | undefined, reports: OnyxCollection, taxRates: Record, ) { @@ -793,7 +793,7 @@ function buildFilterFormValuesFromQuery( ); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID) { - filtersForm[key as typeof filterKey] = filterValues.filter((card) => cardList[card]); + filtersForm[key as typeof filterKey] = filterValues.filter((card) => cardList?.[card]); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.FEED) { filtersForm[key as typeof filterKey] = filterValues.filter((feed) => feed); @@ -1017,7 +1017,7 @@ function getFilterDisplayValue( filterValue: string, personalDetails: OnyxTypes.PersonalDetailsList | undefined, reports: OnyxCollection, - cardList: OnyxTypes.CardList, + cardList: OnyxTypes.CardList | undefined, cardFeeds: OnyxCollection, policies: OnyxCollection, currentUserAccountID: number, @@ -1066,7 +1066,7 @@ function getDisplayQueryFiltersForKey( personalDetails: OnyxTypes.PersonalDetailsList | undefined, reports: OnyxCollection, taxRates: Record, - cardList: OnyxTypes.CardList, + cardList: OnyxTypes.CardList | undefined, cardFeeds: OnyxCollection, policies: OnyxCollection, currentUserAccountID: number, @@ -1197,7 +1197,7 @@ function buildUserReadableQueryString( PersonalDetails: OnyxTypes.PersonalDetailsList | undefined, reports: OnyxCollection, taxRates: Record, - cardList: OnyxTypes.CardList, + cardList: OnyxTypes.CardList | undefined, cardFeeds: OnyxCollection, policies: OnyxCollection, currentUserAccountID: number, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 01415c68c99a..34270ff542db 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -3072,7 +3072,7 @@ function getGroupCurrencyOptions(currencyList: OnyxTypes.CurrencyList) { ); } -function getFeedOptions(allCardFeeds: OnyxCollection, allCards: OnyxTypes.CardList) { +function getFeedOptions(allCardFeeds: OnyxCollection, allCards: OnyxTypes.CardList | undefined) { return Object.values(getCardFeedsForDisplay(allCardFeeds, allCards)).map>((cardFeed) => ({ text: cardFeed.name, value: cardFeed.id, diff --git a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts index 0f9c467e518d..1ee7356380e9 100644 --- a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts +++ b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts @@ -1,5 +1,6 @@ import type {ValueOf} from 'type-fest'; import ONYXKEYS from '@src/ONYXKEYS'; +import personalAndWorkspaceCardListConfig from './configs/personalAndWorkspaceCardList'; import outstandingReportsByPolicyIDConfig from './configs/outstandingReportsByPolicyID'; import reportAttributesConfig from './configs/reportAttributes'; import reportTransactionsAndViolationsConfig from './configs/reportTransactionsAndViolations'; @@ -13,6 +14,7 @@ const ONYX_DERIVED_VALUES = { [ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: reportAttributesConfig, [ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS]: reportTransactionsAndViolationsConfig, [ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID]: outstandingReportsByPolicyIDConfig, + [ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST]: personalAndWorkspaceCardListConfig, } as const satisfies { // eslint-disable-next-line @typescript-eslint/no-explicit-any [Key in ValueOf]: OnyxDerivedValueConfig; diff --git a/src/libs/actions/OnyxDerived/configs/personalAndWorkspaceCardList.ts b/src/libs/actions/OnyxDerived/configs/personalAndWorkspaceCardList.ts new file mode 100644 index 000000000000..8e717e32ab04 --- /dev/null +++ b/src/libs/actions/OnyxDerived/configs/personalAndWorkspaceCardList.ts @@ -0,0 +1,21 @@ +import {filterPersonalCards, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; +import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {CardList} from '@src/types/onyx'; + +const personalAndWorkspaceCardListConfig = createOnyxDerivedValueConfig({ + key: ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, + dependencies: [ONYXKEYS.CARD_LIST, ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST], + compute: ([cardList, workspaceCardFeeds], {areAllConnectionsSet, currentValue}) => { + if (!areAllConnectionsSet) { + return currentValue ?? (CONST.EMPTY_OBJECT as CardList); + } + + const personalCards = filterPersonalCards(cardList); + + return mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, personalCards); + }, +}); + +export default personalAndWorkspaceCardListConfig; diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index 11f0638320dd..81cd4b8c13e0 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -22,7 +22,7 @@ import type {WorkspaceListItem} from '@hooks/useWorkspaceList'; import useWorkspaceList from '@hooks/useWorkspaceList'; import {saveSearch} from '@libs/actions/Search'; import {createCardFeedKey, getCardFeedKey, getCardFeedNamesWithType, getWorkspaceCardFeedKey} from '@libs/CardFeedUtils'; -import {filterPersonalCards, getCardDescription, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; +import {filterCardsHiddenFromSearch, getCardDescription} from '@libs/CardUtils'; import {convertToDisplayStringWithoutCurrency} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import {createDisplayName} from '@libs/PersonalDetailsUtils'; @@ -255,10 +255,10 @@ function getFilterWorkspaceDisplayTitle(filters: SearchAdvancedFiltersForm, poli .join(', '); } -function getFilterCardDisplayTitle(filters: Partial, cards: CardList, translate: LocaleContextProps['translate']) { +function getFilterCardDisplayTitle(filters: Partial, cards: CardList | undefined, translate: LocaleContextProps['translate']) { const cardIdsFilter = filters[CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID] ?? []; const feedFilter = filters[CONST.SEARCH.SYNTAX_FILTER_KEYS.FEED] ?? []; - const workspaceCardFeeds = Object.entries(cards).reduce>((workspaceCardsFeed, [cardID, card]) => { + const workspaceCardFeeds = Object.entries(cards ?? {}).reduce>((workspaceCardsFeed, [cardID, card]) => { const feedKey = `${createCardFeedKey(card.fundID, card.bank)}`; const workspaceFeedKey = getWorkspaceCardFeedKey(feedKey); /* eslint-disable no-param-reassign */ @@ -273,7 +273,7 @@ function getFilterCardDisplayTitle(filters: Partial, translate, }); - const cardNames = Object.values(cards) + const cardNames = Object.values(cards ?? {}) .filter((card) => cardIdsFilter.includes(card.cardID.toString()) && !feedFilter.includes(createCardFeedKey(card.fundID, card.bank))) .map((card) => getCardDescription(card)); @@ -542,9 +542,7 @@ function AdvancedSearchFilters() { const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false}); const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true}); const [searchAdvancedFilters = getEmptyObject()] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true}); - const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: false}); - const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: false}); - const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList, true), [userCardList, workspaceCardFeeds]); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true, selector: filterCardsHiddenFromSearch}); const personalDetails = usePersonalDetails(); const [policies = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false}); @@ -561,7 +559,7 @@ function AdvancedSearchFilters() { searchTerm: '', localeCompare, }); - + const {currentType, typeFiltersKeys} = useAdvancedSearchFilters(); const queryString = useMemo(() => buildQueryStringFromFilterFormValues(searchAdvancedFilters), [searchAdvancedFilters]); diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index eeba8905da81..3f46385b4239 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -23,7 +23,6 @@ import useSingleExecution from '@hooks/useSingleExecution'; import useSuggestedSearchDefaultNavigation from '@hooks/useSuggestedSearchDefaultNavigation'; import useThemeStyles from '@hooks/useThemeStyles'; import {setSearchContext} from '@libs/actions/Search'; -import {filterPersonalCards, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getAllTaxRates} from '@libs/PolicyUtils'; import {buildSearchQueryJSON, buildUserReadableQueryString, shouldSkipSuggestedSearchNavigation as shouldSkipSuggestedSearchNavigationForQuery} from '@libs/SearchQueryUtils'; @@ -74,9 +73,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const personalDetails = usePersonalDetails(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); - const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true}); - const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true}); - const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true}); const taxRates = getAllTaxRates(allPolicies); const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: false}); diff --git a/src/types/onyx/DerivedValues.ts b/src/types/onyx/DerivedValues.ts index d39cc88e76e2..e2c1cff3452d 100644 --- a/src/types/onyx/DerivedValues.ts +++ b/src/types/onyx/DerivedValues.ts @@ -1,6 +1,7 @@ import type {OnyxCollection} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; +import type {CardList} from './Card'; import type {Errors} from './OnyxCommon'; import type Report from './Report'; import type Transaction from './Transaction'; @@ -70,5 +71,17 @@ type ReportTransactionsAndViolationsDerivedValue = Record>; +/** + * The derived value for merged personal and workspace card feeds. + */ +type PersonalAndWorkspaceCardListDerivedValue = CardList; + export default ReportAttributesDerivedValue; -export type {ReportAttributes, ReportAttributesDerivedValue, ReportTransactionsAndViolationsDerivedValue, ReportTransactionsAndViolations, OutstandingReportsByPolicyIDDerivedValue}; +export type { + ReportAttributes, + ReportAttributesDerivedValue, + ReportTransactionsAndViolationsDerivedValue, + ReportTransactionsAndViolations, + OutstandingReportsByPolicyIDDerivedValue, + PersonalAndWorkspaceCardListDerivedValue, +}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 81e89f0df9e3..eeab88f36870 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -28,7 +28,12 @@ import type Credentials from './Credentials'; import type Currency from './Currency'; import type {CurrencyList} from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; -import type {OutstandingReportsByPolicyIDDerivedValue, ReportAttributesDerivedValue, ReportTransactionsAndViolationsDerivedValue} from './DerivedValues'; +import type { + OutstandingReportsByPolicyIDDerivedValue, + PersonalAndWorkspaceCardListDerivedValue, + ReportAttributesDerivedValue, + ReportTransactionsAndViolationsDerivedValue, +} from './DerivedValues'; import type DismissedProductTraining from './DismissedProductTraining'; import type DismissedReferralBanners from './DismissedReferralBanners'; import type Domain from './Domain'; @@ -307,6 +312,7 @@ export type { LastSearchParams, ReportTransactionsAndViolationsDerivedValue, OutstandingReportsByPolicyIDDerivedValue, + PersonalAndWorkspaceCardListDerivedValue, ScheduleCallDraft, ValidateUserAndGetAccessiblePolicies, VacationDelegate, From 8d50afd3efe810df91a3091e3c38f32628f97c3e Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Mon, 12 Jan 2026 14:46:52 +0100 Subject: [PATCH 02/19] move defaultExpensifyCard to useSearchTypeMenuSections --- src/hooks/useCardFeedsForDisplay.ts | 8 ++------ src/hooks/useSearchTypeMenuSections.ts | 7 ++++++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/hooks/useCardFeedsForDisplay.ts b/src/hooks/useCardFeedsForDisplay.ts index 8dc136ac4438..bbfed5d0e811 100644 --- a/src/hooks/useCardFeedsForDisplay.ts +++ b/src/hooks/useCardFeedsForDisplay.ts @@ -1,6 +1,6 @@ import {useMemo} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; -import {getCardFeedsForDisplay, getCardFeedsForDisplayPerPolicy} from '@libs/CardFeedUtils'; +import {getCardFeedsForDisplayPerPolicy} from '@libs/CardFeedUtils'; import {isCustomFeed} from '@libs/CardUtils'; import {isPaidGroupPolicy} from '@libs/PolicyUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -57,11 +57,7 @@ const useCardFeedsForDisplay = () => { return commercialFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0); }, [eligiblePoliciesIDs, activePolicyID, cardFeedsByPolicy, localeCompare]); - const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); - const expensifyCards = getCardFeedsForDisplay({}, allCards); - const defaultExpensifyCard = Object.values(expensifyCards)?.at(0); - - return {defaultCardFeed, cardFeedsByPolicy, defaultExpensifyCard}; + return {defaultCardFeed, cardFeedsByPolicy}; }; export default useCardFeedsForDisplay; diff --git a/src/hooks/useSearchTypeMenuSections.ts b/src/hooks/useSearchTypeMenuSections.ts index d79512545e7d..05d17e9d5e59 100644 --- a/src/hooks/useSearchTypeMenuSections.ts +++ b/src/hooks/useSearchTypeMenuSections.ts @@ -1,6 +1,7 @@ import {createPoliciesSelector} from '@selectors/Policy'; import {useCallback, useEffect, useMemo, useState} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {getCardFeedsForDisplay} from '@libs/CardFeedUtils'; import {areAllGroupPoliciesExpenseChatDisabled} from '@libs/PolicyUtils'; import {createTypeMenuSections} from '@libs/SearchUIUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -43,7 +44,11 @@ const currentUserLoginAndAccountIDSelector = (session: OnyxEntry) => ({ * currently focused search, based on the hash */ const useSearchTypeMenuSections = () => { - const {defaultCardFeed, cardFeedsByPolicy, defaultExpensifyCard} = useCardFeedsForDisplay(); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const expensifyCards = useMemo(() => getCardFeedsForDisplay({}, allCards), [allCards]); + const defaultExpensifyCard = useMemo(() => Object.values(expensifyCards)?.at(0), [expensifyCards]); + + const {defaultCardFeed, cardFeedsByPolicy} = useCardFeedsForDisplay(); const icons = useMemoizedLazyExpensifyIcons(['Document', 'Pencil', 'ThumbsUp']); const {isOffline} = useNetwork(); From e62e95a3a562bf4bb20a4e6f80f32356faf6134a Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Mon, 12 Jan 2026 15:27:11 +0100 Subject: [PATCH 03/19] restore useFilterFormValues --- src/hooks/useFilterFormValues.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/hooks/useFilterFormValues.tsx b/src/hooks/useFilterFormValues.tsx index 4627b95edc36..4f62982f8201 100644 --- a/src/hooks/useFilterFormValues.tsx +++ b/src/hooks/useFilterFormValues.tsx @@ -1,7 +1,10 @@ +import {useMemo} from 'react'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; import type {SearchQueryJSON} from '@components/Search/types'; +import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; import {getAllTaxRates} from '@libs/PolicyUtils'; import {buildFilterFormValuesFromQuery} from '@libs/SearchQueryUtils'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {SearchAdvancedFiltersForm} from '@src/types/form'; import type {CurrencyList} from '@src/types/onyx'; @@ -11,14 +14,16 @@ import useOnyx from './useOnyx'; const useFilterFormValues = (queryJSON?: SearchQueryJSON) => { const personalDetails = usePersonalDetails(); + const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); const [policyTagsLists] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {canBeMissing: true}); const [policyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES, {canBeMissing: true}); - const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true}); const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: true}); const taxRates = getAllTaxRates(policies); + const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [workspaceCardFeeds, userCardList]); const formValues = queryJSON ? buildFilterFormValuesFromQuery(queryJSON, policyCategories, policyTagsLists, currencyList, personalDetails, allCards, allReports, taxRates) From f6cc44d3f4a9c48e6a59d59a51581a3425ec15eb Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Mon, 12 Jan 2026 15:39:54 +0100 Subject: [PATCH 04/19] add isCard guard to filterCardsHiddenFromSearch --- src/libs/CardUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index eb851f560814..34637cf87df2 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -168,11 +168,11 @@ function mergeCardListWithWorkspaceFeeds(workspaceFeeds: Record Date: Mon, 12 Jan 2026 15:41:28 +0100 Subject: [PATCH 05/19] prettier --- src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts | 2 +- src/pages/Search/AdvancedSearchFilters.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts index 1ee7356380e9..f9d3d88da25a 100644 --- a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts +++ b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts @@ -1,7 +1,7 @@ import type {ValueOf} from 'type-fest'; import ONYXKEYS from '@src/ONYXKEYS'; -import personalAndWorkspaceCardListConfig from './configs/personalAndWorkspaceCardList'; import outstandingReportsByPolicyIDConfig from './configs/outstandingReportsByPolicyID'; +import personalAndWorkspaceCardListConfig from './configs/personalAndWorkspaceCardList'; import reportAttributesConfig from './configs/reportAttributes'; import reportTransactionsAndViolationsConfig from './configs/reportTransactionsAndViolations'; import type {OnyxDerivedValueConfig} from './types'; diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index 81cd4b8c13e0..6124d6e79068 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -559,7 +559,7 @@ function AdvancedSearchFilters() { searchTerm: '', localeCompare, }); - + const {currentType, typeFiltersKeys} = useAdvancedSearchFilters(); const queryString = useMemo(() => buildQueryStringFromFilterFormValues(searchAdvancedFilters), [searchAdvancedFilters]); From a292163981383b70562264c9c20a0d62370f07cd Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Tue, 13 Jan 2026 16:02:33 +0100 Subject: [PATCH 06/19] move updateAdvancedFilters useEffect to useFilterFormValues --- src/hooks/useFilterFormValues.tsx | 7 ++++++- src/pages/Search/SearchPage.tsx | 9 --------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/hooks/useFilterFormValues.tsx b/src/hooks/useFilterFormValues.tsx index 4f62982f8201..66ffd42e94c1 100644 --- a/src/hooks/useFilterFormValues.tsx +++ b/src/hooks/useFilterFormValues.tsx @@ -1,6 +1,7 @@ -import {useMemo} from 'react'; +import {useEffect, useMemo} from 'react'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; import type {SearchQueryJSON} from '@components/Search/types'; +import {updateAdvancedFilters} from '@libs/actions/Search'; import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; import {getAllTaxRates} from '@libs/PolicyUtils'; import {buildFilterFormValuesFromQuery} from '@libs/SearchQueryUtils'; @@ -29,6 +30,10 @@ const useFilterFormValues = (queryJSON?: SearchQueryJSON) => { ? buildFilterFormValuesFromQuery(queryJSON, policyCategories, policyTagsLists, currencyList, personalDetails, allCards, allReports, taxRates) : getEmptyObject>(); + useEffect(() => { + updateAdvancedFilters(formValues, true); + }, [formValues]); + return formValues; }; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 475882508eb9..819ccc883277 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -24,7 +24,6 @@ import useBulkPayOptions from '@hooks/useBulkPayOptions'; import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useFilesValidation from '@hooks/useFilesValidation'; -import useFilterFormValues from '@hooks/useFilterFormValues'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; @@ -56,7 +55,6 @@ import { search, submitMoneyRequestOnSearch, unholdMoneyRequestOnSearch, - updateAdvancedFilters, } from '@libs/actions/Search'; import {setTransactionReport} from '@libs/actions/Transaction'; import {setNameValuePair} from '@libs/actions/User'; @@ -219,13 +217,6 @@ function SearchPage({route}: SearchPageProps) { onlyShowPayElsewhere, }); - const formValues = useFilterFormValues(queryJSON); - - // Sync the advanced filters form with the current query when it changes - useEffect(() => { - updateAdvancedFilters(formValues, true); - }, [formValues]); - useEffect(() => { confirmReadyToOpenApp(); }, []); From bcde84eaf2b85a48a8ef8306e54f9e4fe8db2ae5 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Wed, 14 Jan 2026 08:45:52 +0100 Subject: [PATCH 07/19] rename to NON_PERSONAL_AND_WORKSPACE_CARD_LIST --- src/ONYXKEYS.ts | 4 ++-- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- .../Search/SearchPageHeader/SearchFiltersBar.tsx | 2 +- .../SearchPageHeader/SearchPageHeaderInput.tsx | 2 +- src/components/Search/SearchRouter/SearchRouter.tsx | 2 +- src/hooks/useAdvancedSearchFilters.ts | 2 +- src/hooks/useSearchTypeMenu.tsx | 2 +- src/hooks/useSearchTypeMenuSections.ts | 2 +- src/libs/CardUtils.ts | 4 ++-- src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts | 4 ++-- ...ardList.ts => nonPersonalAndWorkspaceCardList.ts} | 12 +++++------- src/pages/Search/AdvancedSearchFilters.tsx | 2 +- src/pages/Search/SearchTypeMenu.tsx | 2 +- src/types/onyx/DerivedValues.ts | 6 +++--- src/types/onyx/index.ts | 4 ++-- 15 files changed, 25 insertions(+), 27 deletions(-) rename src/libs/actions/OnyxDerived/configs/{personalAndWorkspaceCardList.ts => nonPersonalAndWorkspaceCardList.ts} (60%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 41373820de67..d7a72879891b 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -982,7 +982,7 @@ const ONYXKEYS = { REPORT_ATTRIBUTES: 'reportAttributes', REPORT_TRANSACTIONS_AND_VIOLATIONS: 'reportTransactionsAndViolations', OUTSTANDING_REPORTS_BY_POLICY_ID: 'outstandingReportsByPolicyID', - PERSONAL_AND_WORKSPACE_CARD_LIST: 'personalAndWorkspaceCardList', + NON_PERSONAL_AND_WORKSPACE_CARD_LIST: 'nonPersonalAndWorkspaceCardList', }, /** Stores HybridApp specific state required to interoperate with OldDot */ @@ -1389,7 +1389,7 @@ type OnyxDerivedValuesMapping = { [ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: OnyxTypes.ReportAttributesDerivedValue; [ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS]: OnyxTypes.ReportTransactionsAndViolationsDerivedValue; [ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID]: OnyxTypes.OutstandingReportsByPolicyIDDerivedValue; - [ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST]: OnyxTypes.PersonalAndWorkspaceCardListDerivedValue; + [ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST]: OnyxTypes.NonPersonalAndWorkspaceCardListDerivedValue; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index fdc68a0755b1..a60dec9fbd03 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -227,7 +227,7 @@ function MoneyRequestView({ const targetPolicyID = updatedTransaction?.reportID ? parentReport?.policyID : policyID; const allPolicyTags = usePolicyTags(); const policyTagList = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${targetPolicyID}`]; - const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${getNonEmptyStringOnyxID(linkedTransactionID)}`, {canBeMissing: true}); const transactionViolations = useTransactionViolations(transaction?.transactionID); diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx index 66786ee81ff9..fd4855cf65fb 100644 --- a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx @@ -104,7 +104,7 @@ function SearchFiltersBar({ const {selectedTransactions, selectAllMatchingItems, areAllMatchingItemsSelected, showSelectAllMatchingItems, shouldShowFiltersBarLoading} = useSearchContext(); const [email] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true, selector: emailSelector}); - const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true}); const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: true}); diff --git a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx index d2f2b1048234..1b230933ee1e 100644 --- a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx +++ b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx @@ -61,7 +61,7 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false}); const taxRates = useMemo(() => getAllTaxRates(policies), [policies]); - const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true}); const {inputQuery: originalInputQuery} = queryJSON; const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: false}); diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 3836c40b4149..7596afa702f9 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -104,7 +104,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla const personalDetails = usePersonalDetails(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); - const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true}); const {shouldUseNarrowLayout} = useResponsiveLayout(); const listRef = useRef(null); diff --git a/src/hooks/useAdvancedSearchFilters.ts b/src/hooks/useAdvancedSearchFilters.ts index 4558bfc6ca62..03573551ac06 100644 --- a/src/hooks/useAdvancedSearchFilters.ts +++ b/src/hooks/useAdvancedSearchFilters.ts @@ -205,7 +205,7 @@ function useAdvancedSearchFilters() { const {localeCompare} = useLocalize(); const [searchAdvancedFilters = getEmptyObject()] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true}); const policyID = searchAdvancedFilters.policyID; - const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true, selector: filterCardsHiddenFromSearch}); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true, selector: filterCardsHiddenFromSearch}); const [policies = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false}); const [allPolicyCategories = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES, { canBeMissing: false, diff --git a/src/hooks/useSearchTypeMenu.tsx b/src/hooks/useSearchTypeMenu.tsx index 89cdd92b3541..12eaba4299a2 100644 --- a/src/hooks/useSearchTypeMenu.tsx +++ b/src/hooks/useSearchTypeMenu.tsx @@ -45,7 +45,7 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) { const personalDetails = usePersonalDetails(); const [reports = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); const taxRates = getAllTaxRates(allPolicies); - const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true}); const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: false}); const expensifyIcons = useMemoizedLazyExpensifyIcons([ diff --git a/src/hooks/useSearchTypeMenuSections.ts b/src/hooks/useSearchTypeMenuSections.ts index 05d17e9d5e59..20be89a52c02 100644 --- a/src/hooks/useSearchTypeMenuSections.ts +++ b/src/hooks/useSearchTypeMenuSections.ts @@ -44,7 +44,7 @@ const currentUserLoginAndAccountIDSelector = (session: OnyxEntry) => ({ * currently focused search, based on the hash */ const useSearchTypeMenuSections = () => { - const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const expensifyCards = useMemo(() => getCardFeedsForDisplay({}, allCards), [allCards]); const defaultExpensifyCard = useMemo(() => Object.values(expensifyCards)?.at(0), [expensifyCards]); diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 34637cf87df2..20496e29a082 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -145,10 +145,10 @@ function isCardClosed(card: Card) { return card?.state === CONST.EXPENSIFY_CARD.STATE.CLOSED; } -function mergeCardListWithWorkspaceFeeds(workspaceFeeds: Record, cardList: CardList | undefined) { +function mergeCardListWithWorkspaceFeeds(workspaceFeeds: Record, cardList: CardList | undefined, shouldFilterOutPersonalCards = false) { const feedCards: CardList = {}; for (const card of Object.values(cardList ?? {})) { - if (!isCard(card)) { + if (!isCard(card) || (shouldFilterOutPersonalCards && !isPersonalCard(card))) { continue; } diff --git a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts index f9d3d88da25a..a67c1790f6b2 100644 --- a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts +++ b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts @@ -1,7 +1,7 @@ import type {ValueOf} from 'type-fest'; import ONYXKEYS from '@src/ONYXKEYS'; +import nonPersonalAndWorkspaceCardListConfig from './configs/nonPersonalAndWorkspaceCardList'; import outstandingReportsByPolicyIDConfig from './configs/outstandingReportsByPolicyID'; -import personalAndWorkspaceCardListConfig from './configs/personalAndWorkspaceCardList'; import reportAttributesConfig from './configs/reportAttributes'; import reportTransactionsAndViolationsConfig from './configs/reportTransactionsAndViolations'; import type {OnyxDerivedValueConfig} from './types'; @@ -14,7 +14,7 @@ const ONYX_DERIVED_VALUES = { [ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: reportAttributesConfig, [ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS]: reportTransactionsAndViolationsConfig, [ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID]: outstandingReportsByPolicyIDConfig, - [ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST]: personalAndWorkspaceCardListConfig, + [ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST]: nonPersonalAndWorkspaceCardListConfig, } as const satisfies { // eslint-disable-next-line @typescript-eslint/no-explicit-any [Key in ValueOf]: OnyxDerivedValueConfig; diff --git a/src/libs/actions/OnyxDerived/configs/personalAndWorkspaceCardList.ts b/src/libs/actions/OnyxDerived/configs/nonPersonalAndWorkspaceCardList.ts similarity index 60% rename from src/libs/actions/OnyxDerived/configs/personalAndWorkspaceCardList.ts rename to src/libs/actions/OnyxDerived/configs/nonPersonalAndWorkspaceCardList.ts index 8e717e32ab04..863c272d21a4 100644 --- a/src/libs/actions/OnyxDerived/configs/personalAndWorkspaceCardList.ts +++ b/src/libs/actions/OnyxDerived/configs/nonPersonalAndWorkspaceCardList.ts @@ -1,21 +1,19 @@ -import {filterPersonalCards, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; +import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {CardList} from '@src/types/onyx'; -const personalAndWorkspaceCardListConfig = createOnyxDerivedValueConfig({ - key: ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, +const nonPersonalAndWorkspaceCardListConfig = createOnyxDerivedValueConfig({ + key: ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, dependencies: [ONYXKEYS.CARD_LIST, ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST], compute: ([cardList, workspaceCardFeeds], {areAllConnectionsSet, currentValue}) => { if (!areAllConnectionsSet) { return currentValue ?? (CONST.EMPTY_OBJECT as CardList); } - const personalCards = filterPersonalCards(cardList); - - return mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, personalCards); + return mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, cardList, true); }, }); -export default personalAndWorkspaceCardListConfig; +export default nonPersonalAndWorkspaceCardListConfig; diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index 6124d6e79068..37fe7a74f397 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -542,7 +542,7 @@ function AdvancedSearchFilters() { const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false}); const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true}); const [searchAdvancedFilters = getEmptyObject()] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true}); - const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true, selector: filterCardsHiddenFromSearch}); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true, selector: filterCardsHiddenFromSearch}); const personalDetails = usePersonalDetails(); const [policies = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false}); diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 3f46385b4239..68ebd50be491 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -73,7 +73,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const personalDetails = usePersonalDetails(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); - const [allCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true}); const taxRates = getAllTaxRates(allPolicies); const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: false}); diff --git a/src/types/onyx/DerivedValues.ts b/src/types/onyx/DerivedValues.ts index e2c1cff3452d..41f4cae1f9e8 100644 --- a/src/types/onyx/DerivedValues.ts +++ b/src/types/onyx/DerivedValues.ts @@ -72,9 +72,9 @@ type ReportTransactionsAndViolationsDerivedValue = Record>; /** - * The derived value for merged personal and workspace card feeds. + * The derived value for merged non-personal and workspace card feeds. */ -type PersonalAndWorkspaceCardListDerivedValue = CardList; +type NonPersonalAndWorkspaceCardListDerivedValue = CardList; export default ReportAttributesDerivedValue; export type { @@ -83,5 +83,5 @@ export type { ReportTransactionsAndViolationsDerivedValue, ReportTransactionsAndViolations, OutstandingReportsByPolicyIDDerivedValue, - PersonalAndWorkspaceCardListDerivedValue, + NonPersonalAndWorkspaceCardListDerivedValue, }; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index eeab88f36870..0b3980a19058 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -30,7 +30,7 @@ import type {CurrencyList} from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type { OutstandingReportsByPolicyIDDerivedValue, - PersonalAndWorkspaceCardListDerivedValue, + NonPersonalAndWorkspaceCardListDerivedValue, ReportAttributesDerivedValue, ReportTransactionsAndViolationsDerivedValue, } from './DerivedValues'; @@ -312,7 +312,7 @@ export type { LastSearchParams, ReportTransactionsAndViolationsDerivedValue, OutstandingReportsByPolicyIDDerivedValue, - PersonalAndWorkspaceCardListDerivedValue, + NonPersonalAndWorkspaceCardListDerivedValue, ScheduleCallDraft, ValidateUserAndGetAccessiblePolicies, VacationDelegate, From 9f17e504f4129e93ded1dc864b2b6bdf83dd1d5f Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Wed, 14 Jan 2026 08:46:10 +0100 Subject: [PATCH 08/19] consolidate defaultExpensifyCard useMemo --- src/hooks/useSearchTypeMenuSections.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/hooks/useSearchTypeMenuSections.ts b/src/hooks/useSearchTypeMenuSections.ts index 20be89a52c02..59b44dd4610b 100644 --- a/src/hooks/useSearchTypeMenuSections.ts +++ b/src/hooks/useSearchTypeMenuSections.ts @@ -45,8 +45,11 @@ const currentUserLoginAndAccountIDSelector = (session: OnyxEntry) => ({ */ const useSearchTypeMenuSections = () => { const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); - const expensifyCards = useMemo(() => getCardFeedsForDisplay({}, allCards), [allCards]); - const defaultExpensifyCard = useMemo(() => Object.values(expensifyCards)?.at(0), [expensifyCards]); + + const defaultExpensifyCard = useMemo(() => { + const cards = getCardFeedsForDisplay({}, allCards); + return Object.values(cards)?.at(0); + }, [allCards]); const {defaultCardFeed, cardFeedsByPolicy} = useCardFeedsForDisplay(); From c72f71a57d1d6ec5c698bded7b7d58ca54ce4bf6 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Wed, 14 Jan 2026 08:51:03 +0100 Subject: [PATCH 09/19] prettier --- src/types/onyx/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 0b3980a19058..c2ec422dfa1b 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -29,8 +29,8 @@ import type Currency from './Currency'; import type {CurrencyList} from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type { - OutstandingReportsByPolicyIDDerivedValue, NonPersonalAndWorkspaceCardListDerivedValue, + OutstandingReportsByPolicyIDDerivedValue, ReportAttributesDerivedValue, ReportTransactionsAndViolationsDerivedValue, } from './DerivedValues'; From 73ff1e759a77f981d645e91e418468bfb5fbe5d0 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Wed, 14 Jan 2026 13:41:48 +0100 Subject: [PATCH 10/19] revert moving useEffect to useFilterFormValues --- src/hooks/useFilterFormValues.tsx | 9 ++------- src/pages/Search/SearchPage.tsx | 9 +++++++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/hooks/useFilterFormValues.tsx b/src/hooks/useFilterFormValues.tsx index 66ffd42e94c1..77e7b1b09736 100644 --- a/src/hooks/useFilterFormValues.tsx +++ b/src/hooks/useFilterFormValues.tsx @@ -1,7 +1,6 @@ -import {useEffect, useMemo} from 'react'; +import {useMemo} from 'react'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; import type {SearchQueryJSON} from '@components/Search/types'; -import {updateAdvancedFilters} from '@libs/actions/Search'; import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; import {getAllTaxRates} from '@libs/PolicyUtils'; import {buildFilterFormValuesFromQuery} from '@libs/SearchQueryUtils'; @@ -23,17 +22,13 @@ const useFilterFormValues = (queryJSON?: SearchQueryJSON) => { const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true}); const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: true}); - const taxRates = getAllTaxRates(policies); + const taxRates = useMemo(() => getAllTaxRates(policies), [policies]); const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [workspaceCardFeeds, userCardList]); const formValues = queryJSON ? buildFilterFormValuesFromQuery(queryJSON, policyCategories, policyTagsLists, currencyList, personalDetails, allCards, allReports, taxRates) : getEmptyObject>(); - useEffect(() => { - updateAdvancedFilters(formValues, true); - }, [formValues]); - return formValues; }; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 819ccc883277..475882508eb9 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -24,6 +24,7 @@ import useBulkPayOptions from '@hooks/useBulkPayOptions'; import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useFilesValidation from '@hooks/useFilesValidation'; +import useFilterFormValues from '@hooks/useFilterFormValues'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; @@ -55,6 +56,7 @@ import { search, submitMoneyRequestOnSearch, unholdMoneyRequestOnSearch, + updateAdvancedFilters, } from '@libs/actions/Search'; import {setTransactionReport} from '@libs/actions/Transaction'; import {setNameValuePair} from '@libs/actions/User'; @@ -217,6 +219,13 @@ function SearchPage({route}: SearchPageProps) { onlyShowPayElsewhere, }); + const formValues = useFilterFormValues(queryJSON); + + // Sync the advanced filters form with the current query when it changes + useEffect(() => { + updateAdvancedFilters(formValues, true); + }, [formValues]); + useEffect(() => { confirmReadyToOpenApp(); }, []); From d3108e310046672008c909bfb011ee250c04227c Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Fri, 16 Jan 2026 09:58:43 +0100 Subject: [PATCH 11/19] add defaultExpensifyCardSelector --- src/hooks/useSearchTypeMenuSections.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/hooks/useSearchTypeMenuSections.ts b/src/hooks/useSearchTypeMenuSections.ts index 59b44dd4610b..af8fb65f054a 100644 --- a/src/hooks/useSearchTypeMenuSections.ts +++ b/src/hooks/useSearchTypeMenuSections.ts @@ -5,7 +5,7 @@ import {getCardFeedsForDisplay} from '@libs/CardFeedUtils'; import {areAllGroupPoliciesExpenseChatDisabled} from '@libs/PolicyUtils'; import {createTypeMenuSections} from '@libs/SearchUIUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, Session} from '@src/types/onyx'; +import type {NonPersonalAndWorkspaceCardListDerivedValue, Policy, Session} from '@src/types/onyx'; import useCardFeedsForDisplay from './useCardFeedsForDisplay'; import useCreateEmptyReportConfirmation from './useCreateEmptyReportConfirmation'; import {useMemoizedLazyExpensifyIcons} from './useLazyAsset'; @@ -39,17 +39,18 @@ const currentUserLoginAndAccountIDSelector = (session: OnyxEntry) => ({ email: session?.email, accountID: session?.accountID, }); + +const defaultExpensifyCardSelector = (allCards: OnyxEntry) => { + const cards = getCardFeedsForDisplay({}, allCards); + return Object.values(cards)?.at(0); +}; + /** * Get a list of all search groupings, along with their search items. Also returns the * currently focused search, based on the hash */ const useSearchTypeMenuSections = () => { - const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); - - const defaultExpensifyCard = useMemo(() => { - const cards = getCardFeedsForDisplay({}, allCards); - return Object.values(cards)?.at(0); - }, [allCards]); + const [defaultExpensifyCard] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true, selector: defaultExpensifyCardSelector}); const {defaultCardFeed, cardFeedsByPolicy} = useCardFeedsForDisplay(); From 0df829e8b167f4d0f7ef0cd1b8e71d2274c565c9 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Tue, 20 Jan 2026 13:54:47 +0100 Subject: [PATCH 12/19] add useMemo comment in useFilterFormValues --- src/hooks/useFilterFormValues.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hooks/useFilterFormValues.tsx b/src/hooks/useFilterFormValues.tsx index 77e7b1b09736..9db20da94886 100644 --- a/src/hooks/useFilterFormValues.tsx +++ b/src/hooks/useFilterFormValues.tsx @@ -22,6 +22,7 @@ const useFilterFormValues = (queryJSON?: SearchQueryJSON) => { const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true}); const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: true}); + // Helps to avoid unnecessary recalculations when user open report details screen. React Compiler does not provide same result. const taxRates = useMemo(() => getAllTaxRates(policies), [policies]); const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [workspaceCardFeeds, userCardList]); From 8405e4a37145785371ecb2da3c6dfc8308663abf Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Mon, 26 Jan 2026 08:57:04 +0100 Subject: [PATCH 13/19] update naming --- src/components/ReportActionItem/MoneyRequestView.tsx | 4 ++-- .../Search/SearchPageHeader/SearchFiltersBar.tsx | 6 +++--- .../SearchPageHeader/SearchPageHeaderInput.tsx | 12 ++++++------ src/components/Search/SearchRouter/SearchRouter.tsx | 4 ++-- src/hooks/useAdvancedSearchFilters.ts | 4 ++-- src/hooks/useSearchTypeMenu.tsx | 6 +++--- src/pages/Search/AdvancedSearchFilters.tsx | 4 ++-- src/pages/Search/SearchTypeMenu.tsx | 6 +++--- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 715400bcd5e8..af56d443d3b1 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -229,7 +229,7 @@ function MoneyRequestView({ const targetPolicyID = updatedTransaction?.reportID ? parentReport?.policyID : policyID; const allPolicyTags = usePolicyTags(); const policyTagList = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${targetPolicyID}`]; - const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${getNonEmptyStringOnyxID(linkedTransactionID)}`, {canBeMissing: true}); const transactionViolations = useTransactionViolations(transaction?.transactionID); @@ -298,7 +298,7 @@ function MoneyRequestView({ const transactionOriginalAmount = transaction && getOriginalAmountForDisplay(transaction, isExpenseReport(moneyRequestReport)); const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency); const isManagedCardTransaction = isCardTransactionTransactionUtils(transaction); - const cardProgramName = getCompanyCardDescription(transaction?.cardName, transaction?.cardID, allCards); + const cardProgramName = getCompanyCardDescription(transaction?.cardName, transaction?.cardID, nonPersonalAndWorkspaceCards); const shouldShowCard = isManagedCardTransaction && cardProgramName; const taxRates = policy?.taxRates; diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx index 8ba95b828ea8..28f5dc8355f5 100644 --- a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx @@ -105,7 +105,7 @@ function SearchFiltersBar({ const {currencyList, getCurrencySymbol} = useCurrencyList(); const [email] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true, selector: emailSelector}); - const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true}); const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext); @@ -196,10 +196,10 @@ function SearchFiltersBar({ const [feedOptions, feed] = useMemo(() => { const feedFilterValues = flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.FEED)?.filters?.map((filter) => filter.value); - const options = getFeedOptions(allFeeds, allCards); + const options = getFeedOptions(allFeeds, nonPersonalAndWorkspaceCards); const value = feedFilterValues ? options.filter((option) => feedFilterValues.includes(option.value)) : []; return [options, value]; - }, [flatFilters, allFeeds, allCards]); + }, [flatFilters, allFeeds, nonPersonalAndWorkspaceCards]); const [statusOptions, status] = useMemo(() => { const options = type ? getStatusOptions(translate, type.value) : []; diff --git a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx index b6336f2d8ac1..47afcb5bec51 100644 --- a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx +++ b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx @@ -61,11 +61,11 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false}); const taxRates = useMemo(() => getAllTaxRates(policies), [policies]); - const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true}); const {inputQuery: originalInputQuery} = queryJSON; const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: false}); - const queryText = buildUserReadableQueryString(queryJSON, personalDetails, reports, taxRates, allCards, allFeeds, policies, currentUserAccountID, true); + const queryText = buildUserReadableQueryString(queryJSON, personalDetails, reports, taxRates, nonPersonalAndWorkspaceCards, allFeeds, policies, currentUserAccountID, true); const [searchContext] = useOnyx(ONYXKEYS.SEARCH_CONTEXT, {canBeMissing: true}); const shouldShowQuery = searchContext?.shouldShowSearchQuery ?? false; @@ -112,9 +112,9 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo }, [queryText, shouldShowQuery]); useEffect(() => { - const substitutionsMap = buildSubstitutionsMap(originalInputQuery, personalDetails, reports, taxRates, allCards, allFeeds, policies, currentUserAccountID); + const substitutionsMap = buildSubstitutionsMap(originalInputQuery, personalDetails, reports, taxRates, nonPersonalAndWorkspaceCards, allFeeds, policies, currentUserAccountID); setAutocompleteSubstitutions(substitutionsMap); - }, [allFeeds, allCards, originalInputQuery, personalDetails, reports, taxRates, policies, currentUserAccountID]); + }, [allFeeds, nonPersonalAndWorkspaceCards, originalInputQuery, personalDetails, reports, taxRates, policies, currentUserAccountID]); useEffect(() => { if (searchRouterListVisible) { @@ -397,7 +397,7 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo ref={listRef} personalDetails={personalDetails} reports={reports} - allCards={allCards} + allCards={nonPersonalAndWorkspaceCards} allFeeds={allFeeds} /> @@ -471,7 +471,7 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo shouldSubscribeToArrowKeyEvents={isAutocompleteListVisible} personalDetails={personalDetails} reports={reports} - allCards={allCards} + allCards={nonPersonalAndWorkspaceCards} allFeeds={allFeeds} /> diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 149df70f2a40..8fd42a0af91d 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -107,7 +107,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla const personalDetails = usePersonalDetails(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); - const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true}); const {shouldUseNarrowLayout} = useResponsiveLayout(); const listRef = useRef(null); @@ -558,7 +558,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla personalDetails={personalDetails} reports={reports} allFeeds={allFeeds} - allCards={allCards} + allCards={nonPersonalAndWorkspaceCards} /> )} {!shouldShowList && ( diff --git a/src/hooks/useAdvancedSearchFilters.ts b/src/hooks/useAdvancedSearchFilters.ts index 03573551ac06..f31622c52bad 100644 --- a/src/hooks/useAdvancedSearchFilters.ts +++ b/src/hooks/useAdvancedSearchFilters.ts @@ -205,7 +205,7 @@ function useAdvancedSearchFilters() { const {localeCompare} = useLocalize(); const [searchAdvancedFilters = getEmptyObject()] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true}); const policyID = searchAdvancedFilters.policyID; - const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true, selector: filterCardsHiddenFromSearch}); + const [searchCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true, selector: filterCardsHiddenFromSearch}); const [policies = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false}); const [allPolicyCategories = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES, { canBeMissing: false, @@ -248,7 +248,7 @@ function useAdvancedSearchFilters() { const shouldDisplayAttendeeFilter = isFeatureEnabledInPolicies(policies, CONST.POLICY.MORE_FEATURES.IS_ATTENDEE_TRACKING_ENABLED); const shouldDisplayCategoryFilter = shouldDisplayFilter(nonPersonalPolicyCategoryCount, areCategoriesEnabled, selectedPolicyCategories?.length > 0); const shouldDisplayTagFilter = shouldDisplayFilter(tagListsUnpacked.length, areTagsEnabled, !!selectedPolicyTagLists); - const shouldDisplayCardFilter = shouldDisplayFilter(Object.keys(allCards ?? {}).length, areCardsEnabled); + const shouldDisplayCardFilter = shouldDisplayFilter(Object.keys(searchCards ?? {}).length, areCardsEnabled); const shouldDisplayTaxFilter = shouldDisplayFilter(Object.keys(taxRates).length, areTaxEnabled); const shouldDisplayWorkspaceFilter = workspaces.some((section) => section.data.length > 1); const shouldDisplayGroupCurrencyFilter = !!searchAdvancedFilters.groupBy; diff --git a/src/hooks/useSearchTypeMenu.tsx b/src/hooks/useSearchTypeMenu.tsx index 12eaba4299a2..32c456969e27 100644 --- a/src/hooks/useSearchTypeMenu.tsx +++ b/src/hooks/useSearchTypeMenu.tsx @@ -45,7 +45,7 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) { const personalDetails = usePersonalDetails(); const [reports = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); const taxRates = getAllTaxRates(allPolicies); - const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true}); const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: false}); const expensifyIcons = useMemoizedLazyExpensifyIcons([ @@ -106,7 +106,7 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) { if (savedSearchTitle === item.query) { const jsonQuery = buildSearchQueryJSON(item.query) ?? ({} as SearchQueryJSON); - savedSearchTitle = buildUserReadableQueryString(jsonQuery, personalDetails, reports, taxRates, allCards, allFeeds, allPolicies, currentUserAccountID); + savedSearchTitle = buildUserReadableQueryString(jsonQuery, personalDetails, reports, taxRates, nonPersonalAndWorkspaceCards, allFeeds, allPolicies, currentUserAccountID); } const isItemFocused = Number(key) === hash; @@ -145,7 +145,7 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) { savedSearchesMenuItems: menuItems, isSavedSearchActive: savedSearchFocused, }; - }, [savedSearches, hash, getOverflowMenu, expensifyIcons.Bookmark, personalDetails, reports, taxRates, allCards, allFeeds, allPolicies, currentUserAccountID]); + }, [savedSearches, hash, getOverflowMenu, expensifyIcons.Bookmark, personalDetails, reports, taxRates, nonPersonalAndWorkspaceCards, allFeeds, allPolicies, currentUserAccountID]); const activeItemIndex = useMemo(() => { // If we have a suggested search, then none of the menu items are active diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index 4209e3d5b09c..9a3aabc47e17 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -540,7 +540,7 @@ function AdvancedSearchFilters() { const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false}); const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true}); const [searchAdvancedFilters = getEmptyObject()] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true}); - const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true, selector: filterCardsHiddenFromSearch}); + const [searchCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true, selector: filterCardsHiddenFromSearch}); const personalDetails = usePersonalDetails(); const [policies = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false}); @@ -592,7 +592,7 @@ function AdvancedSearchFilters() { const onPress = singleExecution(waitForNavigate(() => Navigation.navigate(baseFilterConfig[key].route))); let filterTitle; if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID) { - filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters, allCards, translate); + filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters, searchCards, translate); } else if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) { filterTitle = baseFilterConfig[key].getTitle(searchAdvancedFilters, taxRates); } else if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPENSE_TYPE) { diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 68ebd50be491..efe9d8a0e5bf 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -73,7 +73,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const personalDetails = usePersonalDetails(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); - const [allCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true}); const taxRates = getAllTaxRates(allPolicies); const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: false}); @@ -98,7 +98,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { let title = item.name; if (title === item.query) { const jsonQuery = buildSearchQueryJSON(item.query) ?? ({} as SearchQueryJSON); - title = buildUserReadableQueryString(jsonQuery, personalDetails, reports, taxRates, allCards, allFeeds, allPolicies, currentUserAccountID); + title = buildUserReadableQueryString(jsonQuery, personalDetails, reports, taxRates, nonPersonalAndWorkspaceCards, allFeeds, allPolicies, currentUserAccountID); } const isItemFocused = Number(key) === hash; @@ -143,7 +143,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { personalDetails, reports, taxRates, - allCards, + nonPersonalAndWorkspaceCards, allFeeds, currentUserAccountID, allPolicies, From 8548a795b5f015c35673e0cb4fe529b1193826d9 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Mon, 26 Jan 2026 09:24:16 +0100 Subject: [PATCH 14/19] add defaultExpensifyCardSelector test --- src/hooks/useSearchTypeMenuSections.ts | 2 + tests/unit/selectors/CardTest.ts | 74 ++++++++++++++++ tests/unit/useSearchTypeMenuSectionsTest.ts | 2 +- tests/utils/collections/card.ts | 97 +++++++++++++++++++++ 4 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 tests/unit/selectors/CardTest.ts create mode 100644 tests/utils/collections/card.ts diff --git a/src/hooks/useSearchTypeMenuSections.ts b/src/hooks/useSearchTypeMenuSections.ts index af8fb65f054a..7bdb68d44dce 100644 --- a/src/hooks/useSearchTypeMenuSections.ts +++ b/src/hooks/useSearchTypeMenuSections.ts @@ -45,6 +45,8 @@ const defaultExpensifyCardSelector = (allCards: OnyxEntry { + it('Should return undefined if allCards is undefined or empty', () => { + expect(defaultExpensifyCardSelector(undefined)).toBeUndefined(); + expect(defaultExpensifyCardSelector({})).toBeUndefined(); + }); + + it('Should return undefined if cards do not have Expensify Card bank', () => { + const allCards: CardList = { + '1': createRandomCompanyCard(1, {bank: 'vcf'}), + '2': createRandomCompanyCard(2, {bank: 'stripe'}), + }; + + expect(defaultExpensifyCardSelector(allCards)).toBeUndefined(); + }); + + it('Should return undefined if Expensify Card does not have fundID', () => { + const allCards: CardList = { + '1': createRandomExpensifyCard(1, {fundID: undefined}), + '2': createRandomExpensifyCard(2, {fundID: ''}), + }; + + expect(defaultExpensifyCardSelector(allCards)).toBeUndefined(); + }); + + it('Should return the first Expensify Card feed when multiple Expensify Cards exist', () => { + const allCards: CardList = { + '1': createRandomExpensifyCard(1, {fundID: '5555'}), + '2': createRandomExpensifyCard(2, {fundID: '6666'}), + }; + const result = defaultExpensifyCardSelector(allCards); + expect(result).toEqual({ + id: '5555_Expensify Card', + feed: CONST.EXPENSIFY_CARD.BANK, + fundID: '5555', + name: CONST.EXPENSIFY_CARD.BANK, + }); + }); + + it('Should return the first Expensify Card feed when mixed cards exist (some Expensify, some not)', () => { + const allCards: CardList = { + '1': createRandomCompanyCard(1, {bank: 'vcf'}), + '2': createRandomExpensifyCard(2, {fundID: '5555'}), + '3': createRandomExpensifyCard(3, {fundID: '6666'}), + }; + + const result = defaultExpensifyCardSelector(allCards); + expect(result).toEqual({ + id: '5555_Expensify Card', + feed: CONST.EXPENSIFY_CARD.BANK, + fundID: '5555', + name: CONST.EXPENSIFY_CARD.BANK, + }); + }); + + it('Should ignore Expensify Cards without fundID when other Expensify Cards with fundID exist', () => { + const allCards: CardList = { + '1': createRandomExpensifyCard(1, {fundID: undefined}), + '2': createRandomExpensifyCard(2, {fundID: '5555'}), + }; + const result = defaultExpensifyCardSelector(allCards); + expect(result).toEqual({ + id: '5555_Expensify Card', + feed: CONST.EXPENSIFY_CARD.BANK, + fundID: '5555', + name: CONST.EXPENSIFY_CARD.BANK, + }); + }); +}); diff --git a/tests/unit/useSearchTypeMenuSectionsTest.ts b/tests/unit/useSearchTypeMenuSectionsTest.ts index 0047d6188cd5..6cb4ba3c4f53 100644 --- a/tests/unit/useSearchTypeMenuSectionsTest.ts +++ b/tests/unit/useSearchTypeMenuSectionsTest.ts @@ -16,7 +16,7 @@ jest.mock('@userActions/Report', () => ({ createNewReport: jest.fn(() => ({reportID: 'mock-report-id'})), })); -jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}, defaultExpensifyCard: null}))); +jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}}))); jest.mock('@hooks/useCreateEmptyReportConfirmation', () => jest.fn(() => ({openCreateReportConfirmation: jest.fn(), CreateReportConfirmationModal: null}))); jest.mock('@hooks/useNetwork', () => jest.fn(() => ({isOffline: false}))); jest.mock('@hooks/usePermissions', () => jest.fn(() => ({isBetaEnabled: jest.fn(() => false)}))); diff --git a/tests/utils/collections/card.ts b/tests/utils/collections/card.ts new file mode 100644 index 000000000000..01cc89285ced --- /dev/null +++ b/tests/utils/collections/card.ts @@ -0,0 +1,97 @@ +import {rand, randAmount, randNumber, randPastDate, randWord} from '@ngneat/falso'; +import {format} from 'date-fns'; +import type {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; +import type {Card} from '@src/types/onyx'; + +export default function createRandomCard( + index: number, + options?: { + bank?: string; + fundID?: string; + state?: ValueOf; + fraud?: ValueOf; + accountID?: number; + domainName?: string; + }, +): Card { + const cardID = index > 0 ? index : randNumber(); + const bank = options?.bank ?? rand([CONST.EXPENSIFY_CARD.BANK, 'vcf', 'stripe', 'oauth.chase.com', 'oauth.capitalone.com']); + const state = options?.state ?? rand(Object.values(CONST.EXPENSIFY_CARD.STATE)); + const fraud = options?.fraud ?? rand(Object.values(CONST.EXPENSIFY_CARD.FRAUD_TYPES)); + const accountID = options?.accountID ?? randNumber(); + const domainName = options?.domainName ?? `expensify-policy${randNumber()}.exfy`; + + // Only generate fundID if not explicitly set (including undefined) and it's an Expensify Card + let fundID: string | undefined; + if (options && 'fundID' in options) { + fundID = options.fundID; + } else if (bank === CONST.EXPENSIFY_CARD.BANK) { + fundID = randNumber().toString(); + } else { + fundID = undefined; + } + + return { + cardID, + state, + bank, + domainName, + lastUpdated: format(randPastDate(), CONST.DATE.FNS_DB_FORMAT_STRING), + fraud, + availableSpend: randAmount(), + unapprovedSpend: -randAmount(), + totalSpend: -randAmount(), + lastFourPAN: randNumber().toString(), + cardName: `${randWord()}...${randNumber()}`, + fundID: bank === CONST.EXPENSIFY_CARD.BANK ? fundID : undefined, + accountID, + isLoading: false, + isLoadingLastUpdated: false, + lastScrape: format(randPastDate(), CONST.DATE.FNS_DB_FORMAT_STRING), + lastScrapeResult: randNumber(), + scrapeMinDate: format(randPastDate(), CONST.DATE.FNS_DB_FORMAT_STRING), + errors: {}, + errorFields: {}, + }; +} + +/** + * Creates a random Expensify Card (with bank === 'Expensify Card') + */ +function createRandomExpensifyCard( + index: number, + options?: { + fundID?: string; + state?: ValueOf; + fraud?: ValueOf; + accountID?: number; + domainName?: string; + }, +): Card { + return createRandomCard(index, { + ...options, + bank: CONST.EXPENSIFY_CARD.BANK, + }); +} + +/** + * Creates a random company card (non-Expensify card) + */ +function createRandomCompanyCard( + index: number, + options?: { + bank?: string; + accountID?: number; + domainName?: string; + }, +): Card { + const banks = ['vcf', 'stripe', 'oauth.chase.com', 'oauth.capitalone.com', 'oauth.citibank.com']; + return createRandomCard(index, { + ...options, + bank: options?.bank ?? rand(banks), + fundID: undefined, // Company cards don't have fundID + }); +} + +export {createRandomExpensifyCard, createRandomCompanyCard}; From d589ead6d5a10b336df86b532bc9868aae6ce667 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Mon, 26 Jan 2026 10:47:42 +0100 Subject: [PATCH 15/19] Create Card selectors --- src/components/EReceipt.tsx | 3 +- .../ReportActionItem/IssueCardMessage.tsx | 2 +- src/hooks/useAdvancedSearchFilters.ts | 2 +- src/hooks/useSearchTypeMenuSections.ts | 11 +----- src/libs/CardUtils.ts | 20 ---------- src/pages/Search/AdvancedSearchFilters.tsx | 3 +- .../SearchFiltersCardPage.tsx | 2 +- src/pages/settings/InitialSettingsPage.tsx | 3 +- .../Wallet/ActivatePhysicalCardPageBase.tsx | 2 +- .../ExpensifyCardContextProvider.tsx | 2 +- .../Wallet/ExpensifyCardPage/index.tsx | 3 +- .../settings/Wallet/ReportCardLostPage.tsx | 2 +- .../Wallet/ReportVirtualCardFraudPage.tsx | 2 +- ...eportVirtualCardFraudVerifyAccountPage.tsx | 2 +- .../settings/Wallet/WalletPage/index.tsx | 3 +- src/selectors/Card.ts | 39 +++++++++++++++++++ 16 files changed, 59 insertions(+), 42 deletions(-) create mode 100644 src/selectors/Card.ts diff --git a/src/components/EReceipt.tsx b/src/components/EReceipt.tsx index d4cde4306b30..9dd830e67aae 100644 --- a/src/components/EReceipt.tsx +++ b/src/components/EReceipt.tsx @@ -8,7 +8,8 @@ import useOnyx from '@hooks/useOnyx'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {filterPersonalCards, getCardDescription, getCompanyCardDescription} from '@libs/CardUtils'; +import {filterPersonalCards} from '@selectors/Card'; +import {getCardDescription, getCompanyCardDescription} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getTransactionDetails} from '@libs/ReportUtils'; diff --git a/src/components/ReportActionItem/IssueCardMessage.tsx b/src/components/ReportActionItem/IssueCardMessage.tsx index 9adb3619602b..2696b1d53924 100644 --- a/src/components/ReportActionItem/IssueCardMessage.tsx +++ b/src/components/ReportActionItem/IssueCardMessage.tsx @@ -8,7 +8,7 @@ import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromR import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {filterPersonalCards} from '@libs/CardUtils'; +import {filterPersonalCards} from '@selectors/Card'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportsSplitNavigatorParamList} from '@libs/Navigation/types'; diff --git a/src/hooks/useAdvancedSearchFilters.ts b/src/hooks/useAdvancedSearchFilters.ts index f31622c52bad..2d0f11094b7f 100644 --- a/src/hooks/useAdvancedSearchFilters.ts +++ b/src/hooks/useAdvancedSearchFilters.ts @@ -1,6 +1,6 @@ import {emailSelector} from '@selectors/Session'; import type {OnyxCollection} from 'react-native-onyx'; -import {filterCardsHiddenFromSearch} from '@libs/CardUtils'; +import {filterCardsHiddenFromSearch} from '@selectors/Card'; import {getAllTaxRates, getTagNamesFromTagsLists, isPolicyFeatureEnabled} from '@libs/PolicyUtils'; import {getAllPolicyValues} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; diff --git a/src/hooks/useSearchTypeMenuSections.ts b/src/hooks/useSearchTypeMenuSections.ts index 7bdb68d44dce..770126114a2f 100644 --- a/src/hooks/useSearchTypeMenuSections.ts +++ b/src/hooks/useSearchTypeMenuSections.ts @@ -1,11 +1,11 @@ +import {defaultExpensifyCardSelector} from '@selectors/Card'; import {createPoliciesSelector} from '@selectors/Policy'; import {useCallback, useEffect, useMemo, useState} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {getCardFeedsForDisplay} from '@libs/CardFeedUtils'; import {areAllGroupPoliciesExpenseChatDisabled} from '@libs/PolicyUtils'; import {createTypeMenuSections} from '@libs/SearchUIUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {NonPersonalAndWorkspaceCardListDerivedValue, Policy, Session} from '@src/types/onyx'; +import type {Policy, Session} from '@src/types/onyx'; import useCardFeedsForDisplay from './useCardFeedsForDisplay'; import useCreateEmptyReportConfirmation from './useCreateEmptyReportConfirmation'; import {useMemoizedLazyExpensifyIcons} from './useLazyAsset'; @@ -40,13 +40,6 @@ const currentUserLoginAndAccountIDSelector = (session: OnyxEntry) => ({ accountID: session?.accountID, }); -const defaultExpensifyCardSelector = (allCards: OnyxEntry) => { - const cards = getCardFeedsForDisplay({}, allCards); - return Object.values(cards)?.at(0); -}; - -export {defaultExpensifyCardSelector}; - /** * Get a list of all search groupings, along with their search items. Also returns the * currently focused search, based on the hash diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index e6161dcbf106..29837be7d323 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -163,17 +163,6 @@ function mergeCardListWithWorkspaceFeeds(workspaceFeeds: Record isPersonalCard(card)); -} - type SplitMaskedCardNumberResult = { firstDigits?: string; lastDigits?: string; @@ -948,13 +930,11 @@ export { getCompanyCardFeed, getCompanyCardFeedWithDomainID, getEligibleBankAccountsForUkEuCard, - filterPersonalCards, isPersonalCard, COMPANY_CARD_FEED_ICON_NAMES, COMPANY_CARD_BANK_ICON_NAMES, splitMaskedCardNumber, isCardAlreadyAssigned, - filterCardsHiddenFromSearch, }; export type {CompanyCardFeedIcons, CompanyCardBankIcons}; diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index 9a3aabc47e17..fb536e716e9e 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -23,7 +23,8 @@ import type {WorkspaceListItem} from '@hooks/useWorkspaceList'; import useWorkspaceList from '@hooks/useWorkspaceList'; import {saveSearch} from '@libs/actions/Search'; import {createCardFeedKey, getCardFeedKey, getCardFeedNamesWithType, getWorkspaceCardFeedKey} from '@libs/CardFeedUtils'; -import {filterCardsHiddenFromSearch, getCardDescription} from '@libs/CardUtils'; +import {filterCardsHiddenFromSearch} from '@selectors/Card'; +import {getCardDescription} from '@libs/CardUtils'; import {convertToDisplayStringWithoutCurrency} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import {createDisplayName} from '@libs/PersonalDetailsUtils'; diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx index 0305b0ac4bb2..f39e0007eff2 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx @@ -16,7 +16,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {updateAdvancedFilters} from '@libs/actions/Search'; import type {CardFilterItem} from '@libs/CardFeedUtils'; import {buildCardFeedsData, buildCardsData, generateSelectedCards, getDomainFeedData, getSelectedCardsFromFeeds} from '@libs/CardFeedUtils'; -import {filterPersonalCards} from '@libs/CardUtils'; +import {filterPersonalCards} from '@selectors/Card'; import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 6165b60e050f..8bd07e07ff18 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -34,7 +34,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {resetExitSurveyForm} from '@libs/actions/ExitSurvey'; import {closeReactNativeApp} from '@libs/actions/HybridApp'; import {hasPartiallySetupBankAccount} from '@libs/BankAccountUtils'; -import {checkIfFeedConnectionIsBroken, filterPersonalCards, hasPendingExpensifyCardAction} from '@libs/CardUtils'; +import {filterPersonalCards} from '@selectors/Card'; +import {checkIfFeedConnectionIsBroken, hasPendingExpensifyCardAction} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import useIsSidebarRouteActive from '@libs/Navigation/helpers/useIsSidebarRouteActive'; import Navigation from '@libs/Navigation/Navigation'; diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPageBase.tsx b/src/pages/settings/Wallet/ActivatePhysicalCardPageBase.tsx index 5a37c60b378f..15757001b2a0 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPageBase.tsx +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPageBase.tsx @@ -13,7 +13,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {activatePhysicalExpensifyCard, clearCardListErrors} from '@libs/actions/Card'; -import {filterPersonalCards} from '@libs/CardUtils'; +import {filterPersonalCards} from '@selectors/Card'; import {getLatestErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardContextProvider.tsx b/src/pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardContextProvider.tsx index 0568ca3781ea..258465e5cbf7 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardContextProvider.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardContextProvider.tsx @@ -1,7 +1,7 @@ import type {PropsWithChildren} from 'react'; import React, {createContext, useEffect, useMemo, useState} from 'react'; import useOnyx from '@hooks/useOnyx'; -import {filterPersonalCards} from '@libs/CardUtils'; +import {filterPersonalCards} from '@selectors/Card'; import ONYXKEYS from '@src/ONYXKEYS'; import type {CardList, ExpensifyCardDetails} from '@src/types/onyx/Card'; import type {Errors} from '@src/types/onyx/OnyxCommon'; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx b/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx index df4fbbbfbc0d..498d0c9584f2 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx @@ -23,7 +23,8 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {resetValidateActionCodeSent} from '@libs/actions/User'; -import {filterPersonalCards, formatCardExpiration, getDomainCards, maskCard, maskPin} from '@libs/CardUtils'; +import {filterPersonalCards} from '@selectors/Card'; +import {formatCardExpiration, getDomainCards, maskCard, maskPin} from '@libs/CardUtils'; import {convertToDisplayString, getCurrencyKeyByCountryCode} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; diff --git a/src/pages/settings/Wallet/ReportCardLostPage.tsx b/src/pages/settings/Wallet/ReportCardLostPage.tsx index 7655eefe8c6b..6059964ae7c6 100644 --- a/src/pages/settings/Wallet/ReportCardLostPage.tsx +++ b/src/pages/settings/Wallet/ReportCardLostPage.tsx @@ -12,7 +12,7 @@ import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useThemeStyles from '@hooks/useThemeStyles'; import type {ReplacementReason} from '@libs/actions/Card'; import {setErrors} from '@libs/actions/FormActions'; -import {filterPersonalCards} from '@libs/CardUtils'; +import {filterPersonalCards} from '@selectors/Card'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; diff --git a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx index ced6c8ca7051..ad08396cdb66 100644 --- a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx +++ b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx @@ -8,7 +8,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {filterPersonalCards} from '@libs/CardUtils'; +import {filterPersonalCards} from '@selectors/Card'; import {getLatestErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; diff --git a/src/pages/settings/Wallet/ReportVirtualCardFraudVerifyAccountPage.tsx b/src/pages/settings/Wallet/ReportVirtualCardFraudVerifyAccountPage.tsx index caf22c52115a..9164ddba391f 100644 --- a/src/pages/settings/Wallet/ReportVirtualCardFraudVerifyAccountPage.tsx +++ b/src/pages/settings/Wallet/ReportVirtualCardFraudVerifyAccountPage.tsx @@ -5,7 +5,7 @@ import useOnyx from '@hooks/useOnyx'; import usePrevious from '@hooks/usePrevious'; import {clearCardListErrors, reportVirtualExpensifyCardFraud} from '@libs/actions/Card'; import {requestValidateCodeAction, resetValidateActionCodeSent} from '@libs/actions/User'; -import {filterPersonalCards} from '@libs/CardUtils'; +import {filterPersonalCards} from '@selectors/Card'; import {getLatestErrorFieldForAnyField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index cb87a2b8ee83..02f6f54b349b 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -30,7 +30,8 @@ import type {FormattedSelectedPaymentMethod} from '@hooks/usePaymentMethodState/ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {filterPersonalCards, maskCardNumber} from '@libs/CardUtils'; +import {filterPersonalCards} from '@selectors/Card'; +import {maskCardNumber} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import {formatPaymentMethods, getPaymentMethodDescription} from '@libs/PaymentUtils'; diff --git a/src/selectors/Card.ts b/src/selectors/Card.ts new file mode 100644 index 000000000000..41305871287b --- /dev/null +++ b/src/selectors/Card.ts @@ -0,0 +1,39 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import {filterObject} from '@libs/ObjectUtils'; +import {isCard, isCardHiddenFromSearch, isPersonalCard} from '@libs/CardUtils'; +import {getCardFeedsForDisplay} from '@libs/CardFeedUtils'; +import type {CardList, NonPersonalAndWorkspaceCardListDerivedValue} from '@src/types/onyx'; + +/** + * Filter out cards that are hidden from search. + * Cards are hidden if they are not virtual and have a state that is in HIDDEN_FROM_SEARCH_STATES. + */ +const filterCardsHiddenFromSearch = (cardList: OnyxEntry): CardList => { + const filteredCardList: CardList = {}; + for (const card of Object.values(cardList ?? {})) { + if (!isCard(card) || isCardHiddenFromSearch(card)) { + continue; + } + filteredCardList[card.cardID] = card; + } + return filteredCardList; +}; + +/** + * Filter out personal cards from the card list. + * Personal cards have fundID === '0' or no fundID. + * This selector keeps non-personal cards (fundID !== '0'). + */ +const filterPersonalCards = (cards: OnyxEntry): CardList => { + return filterObject(cards ?? {}, (key, card) => isPersonalCard(card)); +}; + +/** + * Selects the Expensify Card feed from the card list and returns the first one. + */ +const defaultExpensifyCardSelector = (allCards: OnyxEntry) => { + const cards = getCardFeedsForDisplay({}, allCards); + return Object.values(cards)?.at(0); +}; + +export {filterCardsHiddenFromSearch, filterPersonalCards, defaultExpensifyCardSelector}; From ef0b68488b923d16269dcc3a113fc9ef445ebdfd Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Mon, 26 Jan 2026 10:48:06 +0100 Subject: [PATCH 16/19] add nonPersonalAndWorkspaceCardList unit tests --- tests/unit/OnyxDerivedTest.tsx | 104 +++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/tests/unit/OnyxDerivedTest.tsx b/tests/unit/OnyxDerivedTest.tsx index 2ea3148a11f2..902480ecd1ee 100644 --- a/tests/unit/OnyxDerivedTest.tsx +++ b/tests/unit/OnyxDerivedTest.tsx @@ -15,6 +15,7 @@ import type {Report} from '@src/types/onyx'; import type {ReportActions} from '@src/types/onyx/ReportAction'; import {createRandomReport} from '../utils/collections/reports'; import createRandomTransaction from '../utils/collections/transaction'; +import {createRandomCompanyCard, createRandomExpensifyCard} from '../utils/collections/card'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; const renderLocaleContextProvider = () => { @@ -416,4 +417,107 @@ describe('OnyxDerived', () => { }); }); }); + + describe('nonPersonalAndWorkspaceCardList', () => { + it('returns empty object when dependencies are not set', async () => { + await waitForBatchedUpdates(); + const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); + expect(derivedCardList).toEqual({}); + }); + + + it('merges cardList and workspaceCardFeeds when dependencies are set', async () => { + // Non-personal cards (fundID !== '0') from cardList are kept, workspace cards are always included + const nonPersonalCard1 = createRandomExpensifyCard(1, {fundID: '123'}); + const nonPersonalCard2 = createRandomExpensifyCard(2, {fundID: '456'}); + const workspaceCard3 = createRandomCompanyCard(3, {bank: 'vcf'}); + + await Onyx.set(ONYXKEYS.CARD_LIST, { + '1': nonPersonalCard1, + '2': nonPersonalCard2, + }); + await Onyx.set(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}workspace_123`, { + '3': workspaceCard3, + }); + await waitForBatchedUpdates(); + const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); + + expect(derivedCardList).toMatchObject({ + '1': expect.objectContaining({cardID: 1}), + '2': expect.objectContaining({cardID: 2}), + '3': expect.objectContaining({cardID: 3}), + }); + }); + + it('filters out personal cards from cardList when dependencies are set', async () => { + const nonPersonalCard = createRandomExpensifyCard(1, {fundID: '123'}); + const personalCard = createRandomExpensifyCard(2, {fundID: '0'}); + const workspaceCard = createRandomCompanyCard(3, {bank: 'vcf'}); + + await Onyx.set(ONYXKEYS.CARD_LIST, { + '1': nonPersonalCard, + '2': personalCard, + }); + await Onyx.set(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}workspace_123`, { + '3': workspaceCard, + }); + await waitForBatchedUpdates(); + const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); + + expect(derivedCardList?.['1']).toBeDefined(); + expect(derivedCardList?.['1']).toMatchObject({cardID: 1}); + expect(derivedCardList?.['2']).toBeUndefined(); + expect(derivedCardList?.['3']).toBeDefined(); + expect(derivedCardList?.['3']).toMatchObject({cardID: 3}); + }); + + it('handles empty cardList when workspaceCardFeeds are set', async () => { + const workspaceCard = createRandomCompanyCard(1, {bank: 'vcf'}); + + await Onyx.set(ONYXKEYS.CARD_LIST, {}); + await Onyx.set(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}workspace_123`, { + '1': workspaceCard, + }); + await waitForBatchedUpdates(); + const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); + + expect(derivedCardList).toMatchObject({ + '1': expect.objectContaining({cardID: 1}), + }); + }); + + it('handles empty workspaceCardFeeds when cardList is set', async () => { + const nonPersonalCard = createRandomExpensifyCard(1, {fundID: '123'}); + + await Onyx.set(ONYXKEYS.CARD_LIST, { + '1': nonPersonalCard, + }); + await waitForBatchedUpdates(); + const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); + + expect(derivedCardList).toMatchObject({ + '1': expect.objectContaining({cardID: 1}), + }); + }); + + it('includes cards from multiple workspace feeds when dependencies are set', async () => { + const card1 = createRandomCompanyCard(1, {bank: 'vcf'}); + const card2 = createRandomCompanyCard(2, {bank: 'stripe'}); + + await Onyx.set(ONYXKEYS.CARD_LIST, {}); + await Onyx.set(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}workspace_123`, { + '1': card1, + }); + await Onyx.set(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}workspace_456`, { + '2': card2, + }); + await waitForBatchedUpdates(); + const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); + + expect(derivedCardList).toMatchObject({ + '1': expect.objectContaining({cardID: 1}), + '2': expect.objectContaining({cardID: 2}), + }); + }); + }); }); From 9b68bf96014a93dec22dcf021438be49e77f4f9b Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Mon, 26 Jan 2026 10:48:36 +0100 Subject: [PATCH 17/19] add Card selector tests --- tests/unit/selectors/CardTest.ts | 121 ++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 3 deletions(-) diff --git a/tests/unit/selectors/CardTest.ts b/tests/unit/selectors/CardTest.ts index fe628f375679..a95d70e90e01 100644 --- a/tests/unit/selectors/CardTest.ts +++ b/tests/unit/selectors/CardTest.ts @@ -1,8 +1,123 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import {defaultExpensifyCardSelector} from '@hooks/useSearchTypeMenuSections'; +import type {ValueOf} from 'type-fest'; +import {defaultExpensifyCardSelector, filterCardsHiddenFromSearch} from '@selectors/Card'; import CONST from '@src/CONST'; -import type {CardList} from '@src/types/onyx'; -import {createRandomCompanyCard, createRandomExpensifyCard} from '../../utils/collections/card'; +import type {Card, CardList} from '@src/types/onyx'; +import createRandomCard, {createRandomCompanyCard, createRandomExpensifyCard} from '../../utils/collections/card'; + +describe('filterCardsHiddenFromSearch', () => { + it('returns empty object when cardList is undefined or empty', () => { + expect(filterCardsHiddenFromSearch(undefined)).toEqual({}); + expect(filterCardsHiddenFromSearch({})).toEqual({}); + }); + + it('keeps cards that are not hidden from search', () => { + const card1 = createRandomExpensifyCard(1, {state: CONST.EXPENSIFY_CARD.STATE.OPEN}); + const card2 = createRandomCard(2, {state: CONST.EXPENSIFY_CARD.STATE.OPEN, bank: 'vcf'}); + + const cardList: CardList = { + '1': card1, + '2': card2, + }; + + const result = filterCardsHiddenFromSearch(cardList); + + expect(result).toMatchObject({ + '1': expect.objectContaining({cardID: 1}), + '2': expect.objectContaining({cardID: 2}), + }); + }); + + it('filters out non-virtual cards with state 2 (hidden from search)', () => { + const visibleCard = createRandomExpensifyCard(1, {state: CONST.EXPENSIFY_CARD.STATE.OPEN}); + const hiddenCard = createRandomExpensifyCard(2, {state: 2 as ValueOf}); + + const cardList: CardList = { + '1': visibleCard, + '2': hiddenCard, + }; + + const result = filterCardsHiddenFromSearch(cardList); + + expect(result?.['1']).toBeDefined(); + expect(result?.['2']).toBeUndefined(); + }); + + it('filters out non-virtual cards with state 4 (hidden from search)', () => { + const visibleCard = createRandomExpensifyCard(1, {state: CONST.EXPENSIFY_CARD.STATE.OPEN}); + const hiddenCard = createRandomExpensifyCard(2, {state: 4 as ValueOf}); + + const cardList: CardList = { + '1': visibleCard, + '2': hiddenCard, + }; + + const result = filterCardsHiddenFromSearch(cardList); + + expect(result?.['1']).toBeDefined(); + expect(result?.['2']).toBeUndefined(); + }); + + it('keeps virtual cards even if they have hidden state', () => { + const virtualCardWithHiddenState = { + ...createRandomExpensifyCard(1, {state: 2 as ValueOf}), + nameValuePairs: {isVirtual: true}, + } as Card; + + const cardList: CardList = { + '1': virtualCardWithHiddenState, + }; + + const result = filterCardsHiddenFromSearch(cardList); + + expect(result?.['1']).toBeDefined(); + }); + + it('filters out invalid card objects (missing cardID or bank)', () => { + const validCard = createRandomExpensifyCard(1); + const invalidCard1 = {cardID: 2} as Card; + const invalidCard2 = {bank: 'vcf'} as Card; + + const cardList: CardList = { + '1': validCard, + '2': invalidCard1, + '3': invalidCard2, + }; + + const result = filterCardsHiddenFromSearch(cardList); + + expect(result?.['1']).toBeDefined(); + expect(result?.['2']).toBeUndefined(); + expect(result?.['3']).toBeUndefined(); + }); + + it('handles mixed visible and hidden cards', () => { + const visibleCard1 = createRandomExpensifyCard(1, {state: CONST.EXPENSIFY_CARD.STATE.OPEN}); + const visibleCard2 = createRandomCard(2, {state: CONST.EXPENSIFY_CARD.STATE.OPEN, bank: 'vcf', fundID: undefined}); + const hiddenCard1 = createRandomExpensifyCard(3, {state: 2 as ValueOf}); + const hiddenCard2 = createRandomExpensifyCard(4, {state: 4 as ValueOf}); + const virtualHiddenCard: Card = { + ...createRandomExpensifyCard(5, {state: 2 as ValueOf}), + nameValuePairs: {isVirtual: true} as Card['nameValuePairs'], + }; + + const cardList: CardList = { + '1': visibleCard1, + '2': visibleCard2, + '3': hiddenCard1, + '4': hiddenCard2, + '5': virtualHiddenCard, + }; + + const result = filterCardsHiddenFromSearch(cardList); + + expect(result?.['1']).toBeDefined(); + expect(result?.['2']).toBeDefined(); + expect(result?.['3']).toBeUndefined(); + expect(result?.['4']).toBeUndefined(); + expect(result?.['5']).toBeDefined(); // Virtual cards are kept even with hidden state + }); +}); describe('defaultExpensifyCardSelector', () => { it('Should return undefined if allCards is undefined or empty', () => { From fea489c0f2d3a691c2d2e0cc2dc4492cfc632699 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Mon, 26 Jan 2026 10:58:34 +0100 Subject: [PATCH 18/19] prettier and typecheck fix --- src/components/EReceipt.tsx | 2 +- src/components/ReportActionItem/IssueCardMessage.tsx | 2 +- src/hooks/useAdvancedSearchFilters.ts | 2 +- src/pages/Search/AdvancedSearchFilters.tsx | 2 +- .../SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx | 2 +- src/pages/settings/InitialSettingsPage.tsx | 2 +- src/pages/settings/Wallet/ActivatePhysicalCardPageBase.tsx | 2 +- .../Wallet/ExpensifyCardPage/ExpensifyCardContextProvider.tsx | 2 +- src/pages/settings/Wallet/ExpensifyCardPage/index.tsx | 2 +- src/pages/settings/Wallet/PaymentMethodList.tsx | 2 +- src/pages/settings/Wallet/ReportCardLostPage.tsx | 2 +- src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx | 2 +- .../Wallet/ReportVirtualCardFraudVerifyAccountPage.tsx | 2 +- src/pages/settings/Wallet/WalletPage/index.tsx | 2 +- src/selectors/Card.ts | 4 ++-- tests/unit/OnyxDerivedTest.tsx | 3 +-- tests/unit/selectors/CardTest.ts | 2 +- 17 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/components/EReceipt.tsx b/src/components/EReceipt.tsx index 9dd830e67aae..a9599a455f6c 100644 --- a/src/components/EReceipt.tsx +++ b/src/components/EReceipt.tsx @@ -1,3 +1,4 @@ +import {filterPersonalCards} from '@selectors/Card'; import React, {useEffect, useRef} from 'react'; import {View} from 'react-native'; import useCurrencyList from '@hooks/useCurrencyList'; @@ -8,7 +9,6 @@ import useOnyx from '@hooks/useOnyx'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {filterPersonalCards} from '@selectors/Card'; import {getCardDescription, getCompanyCardDescription} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; diff --git a/src/components/ReportActionItem/IssueCardMessage.tsx b/src/components/ReportActionItem/IssueCardMessage.tsx index 2696b1d53924..b15f9345f77f 100644 --- a/src/components/ReportActionItem/IssueCardMessage.tsx +++ b/src/components/ReportActionItem/IssueCardMessage.tsx @@ -1,4 +1,5 @@ import {useRoute} from '@react-navigation/native'; +import {filterPersonalCards} from '@selectors/Card'; import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import Button from '@components/Button'; @@ -8,7 +9,6 @@ import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromR import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {filterPersonalCards} from '@selectors/Card'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportsSplitNavigatorParamList} from '@libs/Navigation/types'; diff --git a/src/hooks/useAdvancedSearchFilters.ts b/src/hooks/useAdvancedSearchFilters.ts index 2d0f11094b7f..8b57183d4f98 100644 --- a/src/hooks/useAdvancedSearchFilters.ts +++ b/src/hooks/useAdvancedSearchFilters.ts @@ -1,6 +1,6 @@ +import {filterCardsHiddenFromSearch} from '@selectors/Card'; import {emailSelector} from '@selectors/Session'; import type {OnyxCollection} from 'react-native-onyx'; -import {filterCardsHiddenFromSearch} from '@selectors/Card'; import {getAllTaxRates, getTagNamesFromTagsLists, isPolicyFeatureEnabled} from '@libs/PolicyUtils'; import {getAllPolicyValues} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index fb536e716e9e..f5ed1825ca94 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -1,3 +1,4 @@ +import {filterCardsHiddenFromSearch} from '@selectors/Card'; import {emailSelector} from '@selectors/Session'; import React, {useMemo} from 'react'; import {View} from 'react-native'; @@ -23,7 +24,6 @@ import type {WorkspaceListItem} from '@hooks/useWorkspaceList'; import useWorkspaceList from '@hooks/useWorkspaceList'; import {saveSearch} from '@libs/actions/Search'; import {createCardFeedKey, getCardFeedKey, getCardFeedNamesWithType, getWorkspaceCardFeedKey} from '@libs/CardFeedUtils'; -import {filterCardsHiddenFromSearch} from '@selectors/Card'; import {getCardDescription} from '@libs/CardUtils'; import {convertToDisplayStringWithoutCurrency} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx index f39e0007eff2..6962976b52d8 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx @@ -1,3 +1,4 @@ +import {filterPersonalCards} from '@selectors/Card'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -16,7 +17,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {updateAdvancedFilters} from '@libs/actions/Search'; import type {CardFilterItem} from '@libs/CardFeedUtils'; import {buildCardFeedsData, buildCardsData, generateSelectedCards, getDomainFeedData, getSelectedCardsFromFeeds} from '@libs/CardFeedUtils'; -import {filterPersonalCards} from '@selectors/Card'; import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 8bd07e07ff18..55fc0b0994cb 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -1,4 +1,5 @@ import {findFocusedRoute, useNavigationState, useRoute} from '@react-navigation/native'; +import {filterPersonalCards} from '@selectors/Card'; import {differenceInDays} from 'date-fns'; import React, {useContext, useEffect, useLayoutEffect, useRef, useState} from 'react'; // eslint-disable-next-line no-restricted-imports @@ -34,7 +35,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {resetExitSurveyForm} from '@libs/actions/ExitSurvey'; import {closeReactNativeApp} from '@libs/actions/HybridApp'; import {hasPartiallySetupBankAccount} from '@libs/BankAccountUtils'; -import {filterPersonalCards} from '@selectors/Card'; import {checkIfFeedConnectionIsBroken, hasPendingExpensifyCardAction} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import useIsSidebarRouteActive from '@libs/Navigation/helpers/useIsSidebarRouteActive'; diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPageBase.tsx b/src/pages/settings/Wallet/ActivatePhysicalCardPageBase.tsx index 15757001b2a0..9a97e935cd69 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPageBase.tsx +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPageBase.tsx @@ -1,3 +1,4 @@ +import {filterPersonalCards} from '@selectors/Card'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import Button from '@components/Button'; @@ -13,7 +14,6 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {activatePhysicalExpensifyCard, clearCardListErrors} from '@libs/actions/Card'; -import {filterPersonalCards} from '@selectors/Card'; import {getLatestErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardContextProvider.tsx b/src/pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardContextProvider.tsx index 258465e5cbf7..a1b089abcc80 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardContextProvider.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardContextProvider.tsx @@ -1,7 +1,7 @@ +import {filterPersonalCards} from '@selectors/Card'; import type {PropsWithChildren} from 'react'; import React, {createContext, useEffect, useMemo, useState} from 'react'; import useOnyx from '@hooks/useOnyx'; -import {filterPersonalCards} from '@selectors/Card'; import ONYXKEYS from '@src/ONYXKEYS'; import type {CardList, ExpensifyCardDetails} from '@src/types/onyx/Card'; import type {Errors} from '@src/types/onyx/OnyxCommon'; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx b/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx index 498d0c9584f2..f83432493727 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx @@ -1,4 +1,5 @@ import {useFocusEffect} from '@react-navigation/native'; +import {filterPersonalCards} from '@selectors/Card'; import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -23,7 +24,6 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {resetValidateActionCodeSent} from '@libs/actions/User'; -import {filterPersonalCards} from '@selectors/Card'; import {formatCardExpiration, getDomainCards, maskCard, maskPin} from '@libs/CardUtils'; import {convertToDisplayString, getCurrencyKeyByCountryCode} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index b85b203c3945..99b9f0bac20d 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -1,4 +1,5 @@ import {isUserValidatedSelector} from '@selectors/Account'; +import {filterPersonalCards} from '@selectors/Card'; import {FlashList} from '@shopify/flash-list'; import lodashSortBy from 'lodash/sortBy'; import type {ReactElement} from 'react'; @@ -20,7 +21,6 @@ import useOnyx from '@hooks/useOnyx'; import useThemeIllustrations from '@hooks/useThemeIllustrations'; import useThemeStyles from '@hooks/useThemeStyles'; import { - filterPersonalCards, getAssignedCardSortKey, getCardFeedIcon, getPlaidInstitutionIconUrl, diff --git a/src/pages/settings/Wallet/ReportCardLostPage.tsx b/src/pages/settings/Wallet/ReportCardLostPage.tsx index 6059964ae7c6..d3589a900fae 100644 --- a/src/pages/settings/Wallet/ReportCardLostPage.tsx +++ b/src/pages/settings/Wallet/ReportCardLostPage.tsx @@ -1,3 +1,4 @@ +import {filterPersonalCards} from '@selectors/Card'; import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; @@ -12,7 +13,6 @@ import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useThemeStyles from '@hooks/useThemeStyles'; import type {ReplacementReason} from '@libs/actions/Card'; import {setErrors} from '@libs/actions/FormActions'; -import {filterPersonalCards} from '@selectors/Card'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; diff --git a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx index ad08396cdb66..196b871df965 100644 --- a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx +++ b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx @@ -1,3 +1,4 @@ +import {filterPersonalCards} from '@selectors/Card'; import React, {useCallback, useEffect} from 'react'; import {View} from 'react-native'; import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; @@ -8,7 +9,6 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {filterPersonalCards} from '@selectors/Card'; import {getLatestErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; diff --git a/src/pages/settings/Wallet/ReportVirtualCardFraudVerifyAccountPage.tsx b/src/pages/settings/Wallet/ReportVirtualCardFraudVerifyAccountPage.tsx index 9164ddba391f..072a7fd046b6 100644 --- a/src/pages/settings/Wallet/ReportVirtualCardFraudVerifyAccountPage.tsx +++ b/src/pages/settings/Wallet/ReportVirtualCardFraudVerifyAccountPage.tsx @@ -1,3 +1,4 @@ +import {filterPersonalCards} from '@selectors/Card'; import React, {useEffect} from 'react'; import ValidateCodeActionContent from '@components/ValidateCodeActionModal/ValidateCodeActionContent'; import useLocalize from '@hooks/useLocalize'; @@ -5,7 +6,6 @@ import useOnyx from '@hooks/useOnyx'; import usePrevious from '@hooks/usePrevious'; import {clearCardListErrors, reportVirtualExpensifyCardFraud} from '@libs/actions/Card'; import {requestValidateCodeAction, resetValidateActionCodeSent} from '@libs/actions/User'; -import {filterPersonalCards} from '@selectors/Card'; import {getLatestErrorFieldForAnyField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 02f6f54b349b..202994673777 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -1,3 +1,4 @@ +import {filterPersonalCards} from '@selectors/Card'; import debounce from 'lodash/debounce'; import isEmpty from 'lodash/isEmpty'; import type {ForwardedRef, RefObject} from 'react'; @@ -30,7 +31,6 @@ import type {FormattedSelectedPaymentMethod} from '@hooks/usePaymentMethodState/ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {filterPersonalCards} from '@selectors/Card'; import {maskCardNumber} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; diff --git a/src/selectors/Card.ts b/src/selectors/Card.ts index 41305871287b..06852c1017cf 100644 --- a/src/selectors/Card.ts +++ b/src/selectors/Card.ts @@ -1,7 +1,7 @@ import type {OnyxEntry} from 'react-native-onyx'; -import {filterObject} from '@libs/ObjectUtils'; -import {isCard, isCardHiddenFromSearch, isPersonalCard} from '@libs/CardUtils'; import {getCardFeedsForDisplay} from '@libs/CardFeedUtils'; +import {isCard, isCardHiddenFromSearch, isPersonalCard} from '@libs/CardUtils'; +import {filterObject} from '@libs/ObjectUtils'; import type {CardList, NonPersonalAndWorkspaceCardListDerivedValue} from '@src/types/onyx'; /** diff --git a/tests/unit/OnyxDerivedTest.tsx b/tests/unit/OnyxDerivedTest.tsx index 902480ecd1ee..6da116dc797d 100644 --- a/tests/unit/OnyxDerivedTest.tsx +++ b/tests/unit/OnyxDerivedTest.tsx @@ -13,9 +13,9 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Report} from '@src/types/onyx'; import type {ReportActions} from '@src/types/onyx/ReportAction'; +import {createRandomCompanyCard, createRandomExpensifyCard} from '../utils/collections/card'; import {createRandomReport} from '../utils/collections/reports'; import createRandomTransaction from '../utils/collections/transaction'; -import {createRandomCompanyCard, createRandomExpensifyCard} from '../utils/collections/card'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; const renderLocaleContextProvider = () => { @@ -425,7 +425,6 @@ describe('OnyxDerived', () => { expect(derivedCardList).toEqual({}); }); - it('merges cardList and workspaceCardFeeds when dependencies are set', async () => { // Non-personal cards (fundID !== '0') from cardList are kept, workspace cards are always included const nonPersonalCard1 = createRandomExpensifyCard(1, {fundID: '123'}); diff --git a/tests/unit/selectors/CardTest.ts b/tests/unit/selectors/CardTest.ts index a95d70e90e01..c3fb613e39d8 100644 --- a/tests/unit/selectors/CardTest.ts +++ b/tests/unit/selectors/CardTest.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import type {ValueOf} from 'type-fest'; import {defaultExpensifyCardSelector, filterCardsHiddenFromSearch} from '@selectors/Card'; +import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import type {Card, CardList} from '@src/types/onyx'; import createRandomCard, {createRandomCompanyCard, createRandomExpensifyCard} from '../../utils/collections/card'; From 06209448faeb3efe0fc87e23ee655283a89138ba Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Mon, 26 Jan 2026 13:41:46 +0100 Subject: [PATCH 19/19] fix flaky tests --- .../nonPersonalAndWorkspaceCardList.ts | 4 +--- tests/unit/OnyxDerivedTest.tsx | 23 +++++++++++++------ tests/unit/selectors/CardTest.ts | 2 +- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/OnyxDerived/configs/nonPersonalAndWorkspaceCardList.ts b/src/libs/actions/OnyxDerived/configs/nonPersonalAndWorkspaceCardList.ts index 863c272d21a4..0bd8118e2293 100644 --- a/src/libs/actions/OnyxDerived/configs/nonPersonalAndWorkspaceCardList.ts +++ b/src/libs/actions/OnyxDerived/configs/nonPersonalAndWorkspaceCardList.ts @@ -4,7 +4,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {CardList} from '@src/types/onyx'; -const nonPersonalAndWorkspaceCardListConfig = createOnyxDerivedValueConfig({ +export default createOnyxDerivedValueConfig({ key: ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, dependencies: [ONYXKEYS.CARD_LIST, ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST], compute: ([cardList, workspaceCardFeeds], {areAllConnectionsSet, currentValue}) => { @@ -15,5 +15,3 @@ const nonPersonalAndWorkspaceCardListConfig = createOnyxDerivedValueConfig({ return mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, cardList, true); }, }); - -export default nonPersonalAndWorkspaceCardListConfig; diff --git a/tests/unit/OnyxDerivedTest.tsx b/tests/unit/OnyxDerivedTest.tsx index 6da116dc797d..235078030fe8 100644 --- a/tests/unit/OnyxDerivedTest.tsx +++ b/tests/unit/OnyxDerivedTest.tsx @@ -26,17 +26,22 @@ const renderLocaleContextProvider = () => { ); }; -describe('OnyxDerived', () => { - beforeAll(() => { - Onyx.init({keys: ONYXKEYS}); - initOnyxDerivedValues(); - }); +const onyxDerivedTestSetup = () => { + Onyx.clear(); + Onyx.init({keys: ONYXKEYS}); + initOnyxDerivedValues(); +}; - beforeEach(async () => { - await Onyx.clear(); +describe('OnyxDerived', () => { + beforeEach(() => { + Onyx.clear(); }); describe('reportAttributes', () => { + beforeAll(() => { + onyxDerivedTestSetup(); + }); + const mockReport: Report = { reportID: `test_1`, reportName: 'Test Report', @@ -419,6 +424,10 @@ describe('OnyxDerived', () => { }); describe('nonPersonalAndWorkspaceCardList', () => { + beforeAll(async () => { + onyxDerivedTestSetup(); + }); + it('returns empty object when dependencies are not set', async () => { await waitForBatchedUpdates(); const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); diff --git a/tests/unit/selectors/CardTest.ts b/tests/unit/selectors/CardTest.ts index c3fb613e39d8..395ce3b10da5 100644 --- a/tests/unit/selectors/CardTest.ts +++ b/tests/unit/selectors/CardTest.ts @@ -74,7 +74,7 @@ describe('filterCardsHiddenFromSearch', () => { }); it('filters out invalid card objects (missing cardID or bank)', () => { - const validCard = createRandomExpensifyCard(1); + const validCard = createRandomExpensifyCard(1, {state: CONST.EXPENSIFY_CARD.STATE.OPEN}); const invalidCard1 = {cardID: 2} as Card; const invalidCard2 = {bank: 'vcf'} as Card;