diff --git a/src/CONST.ts b/src/CONST.ts index ad0e5f4ab11a9..fdef9f9337599 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1995,6 +1995,7 @@ const CONST = { HEIGHT: 416, }, DESKTOP_HEADER_PADDING: 12, + SEARCH_ITEM_LIMIT: 15, CATEGORY_SHORTCUT_BAR_HEIGHT: 32, SMALL_EMOJI_PICKER_SIZE: { WIDTH: '100%', diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx new file mode 100644 index 0000000000000..e0a356c7f5237 --- /dev/null +++ b/src/components/SearchBar.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; +import {MagnifyingGlass} from './Icon/Expensicons'; +import Text from './Text'; +import TextInput from './TextInput'; + +type SearchBarProps = { + label: string; + icon?: IconAsset; + inputValue: string; + onChangeText?: (text: string) => void; + onSubmitEditing?: (text: string) => void; + style?: StyleProp; + shouldShowEmptyState?: boolean; +}; + +function SearchBar({label, style, icon = MagnifyingGlass, inputValue, onChangeText, onSubmitEditing, shouldShowEmptyState}: SearchBarProps) { + const styles = useThemeStyles(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {translate} = useLocalize(); + + return ( + <> + + onSubmitEditing?.(inputValue)} + shouldShowClearButton + shouldHideClearButton={!inputValue?.length} + /> + + {!!shouldShowEmptyState && inputValue.length !== 0 && ( + + {translate('common.noResultsFoundMatching', {searchString: inputValue})} + + )} + + ); +} + +SearchBar.displayName = 'SearchBar'; +export default SearchBar; diff --git a/src/hooks/useSearchResults.ts b/src/hooks/useSearchResults.ts new file mode 100644 index 0000000000000..8798f2562abba --- /dev/null +++ b/src/hooks/useSearchResults.ts @@ -0,0 +1,34 @@ +import {useEffect, useState, useTransition} from 'react'; +import CONST from '@src/CONST'; +import usePrevious from './usePrevious'; + +/** + * This hook filters (and optionally sorts) a dataset based on a search parameter. + * It utilizes `useTransition` to allow the searchQuery to change rapidly, while more expensive renders that occur using + * the result of the filtering and sorting are deprioritized, allowing them to happen in the background. + */ +function useSearchResults(data: TValue[], filterData: (datum: TValue, searchInput: string) => boolean, sortData: (data: TValue[]) => TValue[] = (d) => d) { + const [inputValue, setInputValue] = useState(''); + const [result, setResult] = useState(data); + const [, startTransition] = useTransition(); + const prevData = usePrevious(data); + useEffect(() => { + startTransition(() => { + const normalizedSearchQuery = inputValue.trim().toLowerCase(); + const filtered = normalizedSearchQuery.length ? data.filter((item) => filterData(item, normalizedSearchQuery)) : data; + const sorted = sortData(filtered); + setResult(sorted); + }); + }, [data, filterData, inputValue, sortData]); + + useEffect(() => { + if (prevData.length <= CONST.SEARCH_ITEM_LIMIT || data.length > CONST.SEARCH_ITEM_LIMIT) { + return; + } + setInputValue(''); + }, [data, prevData]); + + return [inputValue, setInputValue, result] as const; +} + +export default useSearchResults; diff --git a/src/languages/en.ts b/src/languages/en.ts index 716fbaef9e2b5..e4e85f634a301 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -367,6 +367,7 @@ const translations = { send: 'Send', na: 'N/A', noResultsFound: 'No results found', + noResultsFoundMatching: ({searchString}: {searchString: string}) => `No results found matching "${searchString}"`, recentDestinations: 'Recent destinations', timePrefix: "It's", conjunctionFor: 'for', @@ -3011,6 +3012,7 @@ const translations = { other: 'Delete rates', }), deletePerDiemRate: 'Delete per diem rate', + findPerDiemRate: 'Find per diem rate', areYouSureDelete: () => ({ one: 'Are you sure you want to delete this rate?', other: 'Are you sure you want to delete these rates?', @@ -3803,6 +3805,7 @@ const translations = { }, }, assignCard: 'Assign card', + findCard: 'Find card', cardNumber: 'Card number', commercialFeed: 'Commercial feed', feedName: ({feedName}: CompanyCardFeedNameParams) => `${feedName} cards`, @@ -3837,6 +3840,7 @@ const translations = { disclaimer: 'The Expensify Visa® Commercial Card is issued by The Bancorp Bank, N.A., Member FDIC, pursuant to a license from Visa U.S.A. Inc. and may not be used at all merchants that accept Visa cards. Apple® and the Apple logo® are trademarks of Apple Inc., registered in the U.S. and other countries. App Store is a service mark of Apple Inc. Google Play and the Google Play logo are trademarks of Google LLC.', issueCard: 'Issue card', + findCard: 'Find card', newCard: 'New card', name: 'Name', lastFour: 'Last 4', @@ -3927,6 +3931,7 @@ const translations = { addCategory: 'Add category', editCategory: 'Edit category', editCategories: 'Edit categories', + findCategory: 'Find category', categoryRequiredError: 'Category name is required', existingCategoryError: 'A category with this name already exists', invalidCategoryName: 'Invalid category name', @@ -4101,6 +4106,7 @@ const translations = { addField: 'Add field', delete: 'Delete field', deleteFields: 'Delete fields', + findReportField: 'Find report field', deleteConfirmation: 'Are you sure you want to delete this report field?', deleteFieldsConfirmation: 'Are you sure you want to delete these report fields?', emptyReportFields: { @@ -4157,6 +4163,7 @@ const translations = { addTag: 'Add tag', editTag: 'Edit tag', editTags: 'Edit tags', + findTag: 'Find tag', subtitle: 'Tags add more detailed ways to classify costs.', emptyTags: { title: "You haven't created any tags", @@ -4189,6 +4196,7 @@ const translations = { value: 'Value', taxReclaimableOn: 'Tax reclaimable on', taxRate: 'Tax rate', + findTaxRate: 'Find tax rate', error: { taxRateAlreadyExists: 'This tax name is already in use', taxCodeAlreadyExists: 'This tax code is already in use', @@ -4255,6 +4263,7 @@ const translations = { one: 'Remove member', other: 'Remove members', }), + findMember: 'Find member', removeWorkspaceMemberButtonTitle: 'Remove from workspace', removeGroupMemberButtonTitle: 'Remove from group', removeRoomMemberButtonTitle: 'Remove from chat', @@ -4609,6 +4618,7 @@ const translations = { centrallyManage: 'Centrally manage rates, track in miles or kilometers, and set a default category.', rate: 'Rate', addRate: 'Add rate', + findRate: 'Find rate', trackTax: 'Track tax', deleteRates: () => ({ one: 'Delete rate', diff --git a/src/languages/es.ts b/src/languages/es.ts index 690c078d368da..f17849221fcbc 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -358,6 +358,7 @@ const translations = { send: 'Enviar', na: 'N/A', noResultsFound: 'No se han encontrado resultados', + noResultsFoundMatching: ({searchString}: {searchString: string}) => `No se encontraron resultados que coincidan con "${searchString}"`, recentDestinations: 'Destinos recientes', timePrefix: 'Son las', conjunctionFor: 'para', @@ -3037,6 +3038,7 @@ const translations = { other: 'Eliminar tasas', }), deletePerDiemRate: 'Eliminar tasa per diem', + findPerDiemRate: 'Encontrar tasa per diem', areYouSureDelete: () => ({ one: '¿Estás seguro de que quieres eliminar esta tasa?', other: '¿Estás seguro de que quieres eliminar estas tasas?', @@ -3845,6 +3847,7 @@ const translations = { }, }, assignCard: 'Asignar tarjeta', + findCard: 'Encontrar tarjeta', cardNumber: 'Número de la tarjeta', commercialFeed: 'Fuente comercial', feedName: ({feedName}: CompanyCardFeedNameParams) => `Tarjetas ${feedName}`, @@ -3879,6 +3882,7 @@ const translations = { disclaimer: 'La tarjeta comercial Expensify Visa® es emitida por The Bancorp Bank, N.A., miembro de la FDIC, en virtud de una licencia de Visa U.S.A. Inc. y no puede utilizarse en todos los comercios que aceptan tarjetas Visa. Apple® y el logotipo de Apple® son marcas comerciales de Apple Inc. registradas en EE.UU. y otros países. App Store es una marca de servicio de Apple Inc. Google Play y el logotipo de Google Play son marcas comerciales de Google LLC.', issueCard: 'Emitir tarjeta', + findCard: 'Encontrar tarjeta', newCard: 'Nueva tarjeta', name: 'Nombre', lastFour: '4 últimos', @@ -3972,6 +3976,7 @@ const translations = { addCategory: 'Añadir categoría', editCategory: 'Editar categoría', editCategories: 'Editar categorías', + findCategory: 'Encontrar categoría', categoryRequiredError: 'Lo nombre de la categoría es obligatorio', existingCategoryError: 'Ya existe una categoría con este nombre', invalidCategoryName: 'Lo nombre de la categoría es invalido', @@ -4149,6 +4154,7 @@ const translations = { addField: 'Añadir campo', delete: 'Eliminar campo', deleteFields: 'Eliminar campos', + findReportField: 'Encontrar campo del informe', deleteConfirmation: '¿Está seguro de que desea eliminar este campo del informe?', deleteFieldsConfirmation: '¿Está seguro de que desea eliminar estos campos del informe?', emptyReportFields: { @@ -4205,6 +4211,7 @@ const translations = { addTag: 'Añadir etiqueta', editTag: 'Editar etiqueta', editTags: 'Editar etiquetas', + findTag: 'Encontrar etiquetas', subtitle: 'Las etiquetas añaden formas más detalladas de clasificar los costos.', emptyTags: { title: 'No has creado ninguna etiqueta', @@ -4236,6 +4243,7 @@ const translations = { customTaxName: 'Nombre del impuesto', value: 'Valor', taxRate: 'Tasa de impuesto', + findTaxRate: 'Encontrar tasa de impuesto', taxReclaimableOn: 'Impuesto recuperable en', error: { taxRateAlreadyExists: 'Ya existe un impuesto con este nombre', @@ -4303,6 +4311,7 @@ const translations = { one: 'Eliminar miembro', other: 'Eliminar miembros', }), + findMember: 'Encontrar miembro', removeWorkspaceMemberButtonTitle: 'Eliminar del espacio de trabajo', removeGroupMemberButtonTitle: 'Eliminar del grupo', removeRoomMemberButtonTitle: 'Eliminar del chat', @@ -4658,6 +4667,7 @@ const translations = { centrallyManage: 'Gestiona centralizadamente las tasas, elige si contabilizar en millas o kilómetros, y define una categoría por defecto', rate: 'Tasa', addRate: 'Agregar tasa', + findRate: 'Encontrar tasa', trackTax: 'Impuesto de seguimiento', deleteRates: () => ({ one: 'Eliminar tasa', diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index f997e33cfe550..f6aa51fcb8cca 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -255,19 +255,27 @@ function getEligibleBankAccountsForCard(bankAccountsList: OnyxEntry bankAccount?.accountData?.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS && bankAccount?.accountData?.allowDebit); } -function sortCardsByCardholderName(cardsList: OnyxEntry, personalDetails: OnyxEntry, policyMembersAccountIDs: number[]): Card[] { +function getCardsByCardholderName(cardsList: OnyxEntry, policyMembersAccountIDs: number[]): Card[] { const {cardList, ...cards} = cardsList ?? {}; - return Object.values(cards) - .filter((card: Card) => card.accountID && policyMembersAccountIDs.includes(card.accountID)) - .sort((cardA: Card, cardB: Card) => { - const userA = cardA.accountID ? personalDetails?.[cardA.accountID] ?? {} : {}; - const userB = cardB.accountID ? personalDetails?.[cardB.accountID] ?? {} : {}; + return Object.values(cards).filter((card: Card) => card.accountID && policyMembersAccountIDs.includes(card.accountID)); +} - const aName = getDisplayNameOrDefault(userA); - const bName = getDisplayNameOrDefault(userB); +function sortCardsByCardholderName(cards: Card[], personalDetails: OnyxEntry): Card[] { + return cards.sort((cardA: Card, cardB: Card) => { + const userA = cardA.accountID ? personalDetails?.[cardA.accountID] ?? {} : {}; + const userB = cardB.accountID ? personalDetails?.[cardB.accountID] ?? {} : {}; + const aName = getDisplayNameOrDefault(userA); + const bName = getDisplayNameOrDefault(userB); + return localeCompare(aName, bName); + }); +} - return localeCompare(aName, bName); - }); +function filterCardsByPersonalDetails(card: Card, searchQuery: string, personalDetails?: PersonalDetailsList) { + const cardTitle = card.nameValuePairs?.cardTitle?.toLowerCase() ?? ''; + const lastFourPAN = card?.lastFourPAN?.toLowerCase() ?? ''; + const accountLogin = personalDetails?.[card.accountID ?? CONST.DEFAULT_NUMBER_ID]?.login?.toLowerCase() ?? ''; + const accountName = personalDetails?.[card.accountID ?? CONST.DEFAULT_NUMBER_ID]?.displayName?.toLowerCase() ?? ''; + return cardTitle.includes(searchQuery) || lastFourPAN.includes(searchQuery) || accountLogin.includes(searchQuery) || accountName.includes(searchQuery); } function getCardFeedIcon(cardFeed: CompanyCardFeed | typeof CONST.EXPENSIFY_CARD.BANK, illustrations: IllustrationsType): IconAsset { @@ -636,5 +644,7 @@ export { isExpensifyCardFullySetUp, filterInactiveCards, getFundIdFromSettingsKey, + getCardsByCardholderName, + filterCardsByPersonalDetails, getCompanyCardDescription, }; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index f0b6f4a0a16b0..4bd342da6a07b 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -15,6 +15,7 @@ import DecisionModal from '@components/DecisionModal'; import {Download, FallbackAvatar, MakeAdmin, Plus, RemoveMembers, Table, User, UserEye} from '@components/Icon/Expensicons'; import {ReceiptWrangler} from '@components/Icon/Illustrations'; import MessagesRow from '@components/MessagesRow'; +import SearchBar from '@components/SearchBar'; import TableListItem from '@components/SelectionList/TableListItem'; import type {ListItem, SelectionListHandle} from '@components/SelectionList/types'; import SelectionListWithModal from '@components/SelectionListWithModal'; @@ -29,6 +30,7 @@ import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; +import useSearchResults from '@hooks/useSearchResults'; import useThemeStyles from '@hooks/useThemeStyles'; import useThreeDotsAnchorPosition from '@hooks/useThreeDotsAnchorPosition'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; @@ -406,8 +408,8 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const policyOwner = policy?.owner; const currentUserLogin = currentUserPersonalDetails.login; const invitedPrimaryToSecondaryLogins = invertObject(policy?.primaryLoginsInvited ?? {}); - const getUsers = useCallback((): MemberOption[] => { - let result: MemberOption[] = []; + const data: MemberOption[] = useMemo(() => { + const result: MemberOption[] = []; Object.entries(policy?.employeeList ?? {}).forEach(([email, policyEmployee]) => { const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); @@ -469,7 +471,6 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson invitedSecondaryLogin: details?.login ? invitedPrimaryToSecondaryLogins[details.login] ?? '' : '', }); }); - result = sortAlphabetically(result, 'text'); return result; }, [ isOffline, @@ -490,7 +491,12 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson isPolicyAdmin, ]); - const data = useMemo(() => getUsers(), [getUsers]); + const filterMember = useCallback( + (memberOption: MemberOption, searchQuery: string) => !!memberOption.text?.toLowerCase().includes(searchQuery) || !!memberOption.alternateText?.toLowerCase().includes(searchQuery), + [], + ); + const sortMembers = useCallback((memberOptions: MemberOption[]) => sortAlphabetically(memberOptions, 'text'), []); + const [inputValue, setInputValue, filteredData] = useSearchResults(data, filterMember, sortMembers); useEffect(() => { if (!isFocused) { @@ -716,6 +722,15 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson {() => ( <> {shouldUseNarrowLayout && {getHeaderButtons()}} + {shouldUseNarrowLayout ? {getHeaderContent()} : getHeaderContent()} + {data.length > CONST.SEARCH_ITEM_LIMIT && ( + + )} setIsOfflineModalVisible(false)} @@ -754,33 +769,33 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson isVisible={isDownloadFailureModalVisible} onClose={() => setIsDownloadFailureModalVisible(false)} /> - - item && toggleUser(item?.accountID)} - shouldUseUserSkeletonView - disableKeyboardShortcuts={removeMembersConfirmModalVisible} - headerMessage={getHeaderMessage()} - headerContent={!shouldUseNarrowLayout && getHeaderContent()} - onSelectRow={openMemberDetails} - shouldSingleExecuteRowSelect={!isPolicyAdmin} - onCheckboxPress={(item) => toggleUser(item.accountID)} - onSelectAll={() => toggleAllUsers(data)} - onDismissError={dismissError} - showLoadingPlaceholder={isLoading} - shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} - textInputRef={textInputRef} - customListHeader={getCustomListHeader()} - listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} - listHeaderContent={shouldUseNarrowLayout ? {getHeaderContent()} : null} - showScrollIndicator={false} - addBottomSafeAreaPadding - /> - + {!!filteredData.length && ( + + item && toggleUser(item?.accountID)} + shouldUseUserSkeletonView + disableKeyboardShortcuts={removeMembersConfirmModalVisible} + headerMessage={getHeaderMessage()} + onSelectRow={openMemberDetails} + shouldSingleExecuteRowSelect={!isPolicyAdmin} + onCheckboxPress={(item) => toggleUser(item.accountID)} + onSelectAll={() => toggleAllUsers(filteredData)} + onDismissError={dismissError} + showLoadingPlaceholder={isLoading} + shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} + textInputRef={textInputRef} + customListHeader={getCustomListHeader()} + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} + showScrollIndicator={false} + addBottomSafeAreaPadding + /> + + )} )} diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index ed2d4c0e10c08..23ccdbccde48b 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -14,6 +14,7 @@ import * as Illustrations from '@components/Icon/Illustrations'; import LottieAnimations from '@components/LottieAnimations'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; +import SearchBar from '@components/SearchBar'; import TableListItem from '@components/SelectionList/TableListItem'; import type {ListItem} from '@components/SelectionList/types'; import SelectionListWithModal from '@components/SelectionListWithModal'; @@ -32,6 +33,7 @@ import useNetwork from '@hooks/useNetwork'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; +import useSearchResults from '@hooks/useSearchResults'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useThreeDotsAnchorPosition from '@hooks/useThreeDotsAnchorPosition'; @@ -119,7 +121,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { ); const categoryList = useMemo(() => { - const categories = lodashSortBy(Object.values(policyCategories ?? {}), 'name', localeCompare) as PolicyCategory[]; + const categories = Object.values(policyCategories ?? {}); return categories.reduce((acc, value) => { const isDisabled = value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; @@ -148,7 +150,15 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { }, []); }, [policyCategories, isOffline, selectedCategories, canSelectMultiple, translate, updateWorkspaceRequiresCategory]); - useAutoTurnSelectionModeOffWhenHasNoActiveOption(categoryList); + const filterCategory = useCallback((categoryOption: PolicyOption, searchInput: string) => { + return !!categoryOption.text?.toLowerCase().includes(searchInput) || !!categoryOption.alternateText?.toLowerCase().includes(searchInput); + }, []); + const sortCategories = useCallback((data: PolicyOption[]) => { + return lodashSortBy(data, 'text', localeCompare) as PolicyOption[]; + }, []); + const [inputValue, setInputValue, filteredCategoryList] = useSearchResults(categoryList, filterCategory, sortCategories); + + useAutoTurnSelectionModeOffWhenHasNoActiveOption(filteredCategoryList); const toggleCategory = useCallback( (category: PolicyOption) => { @@ -163,7 +173,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { ); const toggleAllCategories = () => { - const availableCategories = categoryList.filter((category) => category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + const availableCategories = filteredCategoryList.filter((category) => category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); const someSelected = availableCategories.some((category) => selectedCategories.includes(category.keyForList)); setSelectedCategories(someSelected ? [] : availableCategories.map((item) => item.keyForList)); }; @@ -315,7 +325,6 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { }, [setSelectedCategories, selectionMode?.isEnabled]); const hasVisibleCategories = categoryList.some((category) => category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline); - const getHeaderText = () => ( {!hasSyncError && isConnectionVerified ? ( @@ -426,7 +435,15 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { danger /> {shouldUseNarrowLayout && {getHeaderButtons()}} - {(!shouldUseNarrowLayout || !hasVisibleCategories || isLoading) && getHeaderText()} + {hasVisibleCategories && !isLoading && getHeaderText()} + {categoryList.length > CONST.SEARCH_ITEM_LIMIT && ( + + )} {isLoading && ( )} - - {!hasVisibleCategories && !isLoading && ( + {!hasVisibleCategories && !isLoading && inputValue.length === 0 && ( item && toggleCategory(item)} - sections={[{data: categoryList, isDisabled: false}]} + sections={[{data: filteredCategoryList, isDisabled: false}]} onCheckboxPress={toggleCategory} onSelectRow={navigateToCategorySettings} shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} @@ -463,7 +479,6 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { onDismissError={dismissError} customListHeader={getCustomListHeader()} listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} - listHeaderContent={shouldUseNarrowLayout ? getHeaderText() : null} showScrollIndicator={false} addBottomSafeAreaPadding /> diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx index 4915c99a54b70..639ae4e73fc7a 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx @@ -5,11 +5,13 @@ import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {PressableWithFeedback} from '@components/Pressable'; +import SearchBar from '@components/SearchBar'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; +import useSearchResults from '@hooks/useSearchResults'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getDefaultCardName, sortCardsByCardholderName} from '@libs/CardUtils'; +import {filterCardsByPersonalDetails, getCardsByCardholderName, getDefaultCardName, sortCardsByCardholderName} from '@libs/CardUtils'; import {getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; @@ -40,10 +42,14 @@ function WorkspaceCompanyCardsList({cardsList, policyID, handleAssignCard, isDis const [customCardNames] = useOnyx(ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES, {canBeMissing: true}); const policy = usePolicy(policyID); - const sortedCards = useMemo( - () => sortCardsByCardholderName(cardsList, personalDetails, Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList))), - [cardsList, personalDetails, policy?.employeeList], - ); + const allCards = useMemo(() => { + const policyMembersAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList)); + return getCardsByCardholderName(cardsList, policyMembersAccountIDs); + }, [cardsList, policy?.employeeList]); + + const filterCard = useCallback((card: Card, searchInput: string) => filterCardsByPersonalDetails(card, searchInput, personalDetails), [personalDetails]); + const sortCards = useCallback((cards: Card[]) => sortCardsByCardholderName(cards, personalDetails), [personalDetails]); + const [inputValue, setInputValue, filteredSortedCards] = useSearchResults(allCards, filterCard, sortCards); const renderItem = useCallback( ({item, index}: ListRenderItemInfo) => { @@ -101,7 +107,7 @@ function WorkspaceCompanyCardsList({cardsList, policyID, handleAssignCard, isDis [styles, translate], ); - if (sortedCards.length === 0) { + if (allCards.length === 0) { return ( 0; + return ( - + <> + {allCards.length > 0 && ( + + )} + + ); } diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index 1e5710ed94046..69fdf3df33f34 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -7,6 +7,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; +import SearchBar from '@components/SearchBar'; import TableListItem from '@components/SelectionList/TableListItem'; import type {ListItem} from '@components/SelectionList/types'; import SelectionListWithModal from '@components/SelectionListWithModal'; @@ -20,6 +21,7 @@ import useNetwork from '@hooks/useNetwork'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; +import useSearchResults from '@hooks/useSearchResults'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; @@ -44,7 +46,7 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Rate} from '@src/types/onyx/Policy'; -type RateForList = ListItem & {value: string}; +type RateForList = ListItem & {value: string; rate?: number}; type PolicyDistanceRatesPageProps = PlatformStackScreenProps; @@ -76,12 +78,12 @@ function PolicyDistanceRatesPage({ [customUnitRates], ); - const filterRate = useCallback( + const filterRateSelection = useCallback( (rate?: Rate) => !!rate && !!customUnitRates?.[rate.customUnitRateID] && customUnitRates?.[rate.customUnitRateID]?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, [customUnitRates], ); - const [selectedDistanceRates, setSelectedDistanceRates] = useFilteredSelection(selectableRates, filterRate); + const [selectedDistanceRates, setSelectedDistanceRates] = useFilteredSelection(selectableRates, filterRateSelection); const canDisableOrDeleteSelectedRates = useMemo( () => @@ -146,37 +148,40 @@ function PolicyDistanceRatesPage({ const distanceRatesList = useMemo( () => - Object.values(customUnitRates) - .sort((rateA, rateB) => (rateA?.rate ?? 0) - (rateB?.rate ?? 0)) - .map((value) => ({ - value: value.customUnitRateID, - text: `${convertAmountToDisplayString(value.rate, value.currency ?? CONST.CURRENCY.USD)} / ${translate( - `common.${customUnit?.attributes?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}`, - )}`, - keyForList: value.customUnitRateID, - isSelected: selectedDistanceRates.includes(value.customUnitRateID) && canSelectMultiple, - isDisabled: value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - pendingAction: - value.pendingAction ?? - value.pendingFields?.rate ?? - value.pendingFields?.enabled ?? - value.pendingFields?.currency ?? - value.pendingFields?.taxRateExternalID ?? - value.pendingFields?.taxClaimablePercentage ?? - (policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD ? policy?.pendingAction : undefined), - errors: value.errors ?? undefined, - rightElement: ( - updateDistanceRateEnabled(newValue, value.customUnitRateID)} - disabled={value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE} - /> - ), - })), + Object.values(customUnitRates).map((value) => ({ + rate: value.rate, + value: value.customUnitRateID, + text: `${convertAmountToDisplayString(value.rate, value.currency ?? CONST.CURRENCY.USD)} / ${translate( + `common.${customUnit?.attributes?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}`, + )}`, + keyForList: value.customUnitRateID, + isSelected: selectedDistanceRates.includes(value.customUnitRateID) && canSelectMultiple, + isDisabled: value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + pendingAction: + value.pendingAction ?? + value.pendingFields?.rate ?? + value.pendingFields?.enabled ?? + value.pendingFields?.currency ?? + value.pendingFields?.taxRateExternalID ?? + value.pendingFields?.taxClaimablePercentage ?? + (policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD ? policy?.pendingAction : undefined), + errors: value.errors ?? undefined, + rightElement: ( + updateDistanceRateEnabled(newValue, value.customUnitRateID)} + disabled={value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE} + /> + ), + })), [customUnitRates, translate, customUnit, selectedDistanceRates, canSelectMultiple, policy?.pendingAction, updateDistanceRateEnabled], ); + const filterRate = useCallback((rate: RateForList, searchInput: string) => !!rate.text?.toLowerCase().includes(searchInput.toLowerCase()), []); + const sortRates = useCallback((rates: RateForList[]) => rates.sort((a, b) => (a.rate ?? 0) - (b.rate ?? 0)), []); + const [inputValue, setInputValue, filteredDistanceRatesList] = useSearchResults(distanceRatesList, filterRate, sortRates); + const addRate = () => { Navigation.navigate(ROUTES.WORKSPACE_CREATE_DISTANCE_RATE.getRoute(policyID)); }; @@ -246,7 +251,7 @@ function PolicyDistanceRatesPage({ } else { setSelectedDistanceRates( Object.entries(selectableRates) - .filter(([, rate]) => rate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) + .filter(([, rate]) => rate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && filteredDistanceRatesList.some((item) => item.value === rate.customUnitRateID)) .map(([key]) => key), ); } @@ -370,7 +375,15 @@ function PolicyDistanceRatesPage({ {!shouldUseNarrowLayout && headerButtons} {shouldUseNarrowLayout && {headerButtons}} - {!shouldUseNarrowLayout && getHeaderText()} + {Object.values(customUnitRates).length > 0 && getHeaderText()} + {Object.values(customUnitRates).length > CONST.SEARCH_ITEM_LIMIT && ( + + )} {isLoading && ( item && toggleRate(item)} - sections={[{data: distanceRatesList, isDisabled: false}]} + sections={[{data: filteredDistanceRatesList, isDisabled: false}]} onCheckboxPress={toggleRate} onSelectRow={openRateDetails} onSelectAll={toggleAllRates} @@ -393,7 +406,6 @@ function PolicyDistanceRatesPage({ shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} customListHeader={getCustomListHeader()} listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} - listHeaderContent={shouldUseNarrowLayout ? getHeaderText() : null} showScrollIndicator={false} /> )} diff --git a/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx b/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx index 9bab132b174fb..efc7a8f996746 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx @@ -1,102 +1,32 @@ import React from 'react'; import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; import FormHelpMessage from '@components/FormHelpMessage'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID'; import {getLatestErrorMessage} from '@libs/ErrorUtils'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type {ExpensifyCardSettings} from '@src/types/onyx'; -import WorkspaceCardsListLabel from './WorkspaceCardsListLabel'; type WorkspaceCardListHeaderProps = { - /** ID of the current policy */ - policyID: string; - /** Card settings */ cardSettings: ExpensifyCardSettings | undefined; }; -function WorkspaceCardListHeader({policyID, cardSettings}: WorkspaceCardListHeaderProps) { +function WorkspaceCardListHeader({cardSettings}: WorkspaceCardListHeaderProps) { // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isMediumScreenWidth, isSmallScreenWidth} = useResponsiveLayout(); const styles = useThemeStyles(); const {translate} = useLocalize(); - const workspaceAccountID = useWorkspaceAccountID(policyID); const isLessThanMediumScreen = isMediumScreenWidth || isSmallScreenWidth; - const [cardManualBilling] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_MANUAL_BILLING}${workspaceAccountID}`); - const errorMessage = getLatestErrorMessage(cardSettings) ?? ''; - const shouldShowSettlementButtonOrDate = !!cardSettings?.isMonthlySettlementAllowed || cardManualBilling; - - const getLabelsLayout = () => { - if (!isLessThanMediumScreen) { - return ( - <> - - - - - ); - } - return shouldShowSettlementButtonOrDate ? ( - <> - - - - - - - ) : ( - <> - - - - - - - ); - }; - return ( - {getLabelsLayout()} {!!errorMessage && ( - + + + + + + ); + } + return shouldShowSettlementButtonOrDate ? ( + + + + + + + + ) : ( + + + + + + + + ); +} + +WorkspaceCardListLabels.displayName = 'WorkspaceCardListLabels'; +export default WorkspaceCardListLabels; diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx index f12c6508ab4ec..a8d1d56d2f353 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx @@ -13,14 +13,16 @@ import {HandCard} from '@components/Icon/Illustrations'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {PressableWithFeedback} from '@components/Pressable'; import ScreenWrapper from '@components/ScreenWrapper'; +import SearchBar from '@components/SearchBar'; import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle'; import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchResults from '@hooks/useSearchResults'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearDeletePaymentMethodError} from '@libs/actions/PaymentMethods'; -import {sortCardsByCardholderName} from '@libs/CardUtils'; +import {filterCardsByPersonalDetails, getCardsByCardholderName, sortCardsByCardholderName} from '@libs/CardUtils'; import goBackFromWorkspaceCentralScreen from '@libs/Navigation/helpers/goBackFromWorkspaceCentralScreen'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import {getDescriptionForPolicyDomainCard, getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; @@ -34,6 +36,7 @@ import type {Card, WorkspaceCardsList} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import EmptyCardView from './EmptyCardView'; import WorkspaceCardListHeader from './WorkspaceCardListHeader'; +import WorkspaceCardListLabels from './WorkspaceCardListLabels'; import WorkspaceCardListRow from './WorkspaceCardListRow'; type WorkspaceExpensifyCardListPageProps = { @@ -71,10 +74,14 @@ function WorkspaceExpensifyCardListPage({route, cardsList, fundID}: WorkspaceExp const policyCurrency = useMemo(() => policy?.outputCurrency ?? CONST.CURRENCY.USD, [policy]); - const sortedCards = useMemo( - () => sortCardsByCardholderName(cardsList, personalDetails, Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList))), - [cardsList, personalDetails, policy?.employeeList], - ); + const allCards = useMemo(() => { + const policyMembersAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList)); + return getCardsByCardholderName(cardsList, policyMembersAccountIDs); + }, [cardsList, policy?.employeeList]); + + const filterCard = useCallback((card: Card, searchInput: string) => filterCardsByPersonalDetails(card, searchInput, personalDetails), [personalDetails]); + const sortCards = useCallback((cards: Card[]) => sortCardsByCardholderName(cards, personalDetails), [personalDetails]); + const [inputValue, setInputValue, filteredSortedCards] = useSearchResults(allCards, filterCard, sortCards); const handleIssueCardPress = () => { if (isActingAsDelegate) { @@ -133,15 +140,9 @@ function WorkspaceExpensifyCardListPage({route, cardsList, fundID}: WorkspaceExp [personalDetails, policyCurrency, policyID, workspaceAccountID, styles], ); - const renderListHeader = useCallback( - () => ( - - ), - [policyID, cardSettings], - ); + const isSearchEmpty = filteredSortedCards.length === 0 && inputValue.length > 0; + + const renderListHeader = useCallback(() => , [cardSettings]); const bottomSafeAreaPaddingStyle = useBottomSafeSafeAreaPaddingStyle(); @@ -177,12 +178,29 @@ function WorkspaceExpensifyCardListPage({route, cardsList, fundID}: WorkspaceExp {isEmptyObject(cardsList) ? ( ) : ( - + <> + + + {allCards.length > CONST.SEARCH_ITEM_LIMIT && ( + + )} + + + )} allSubRates.filter((subRate) => subRate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE), [allSubRates]); - const canSelectMultiple = shouldUseNarrowLayout ? selectionMode?.isEnabled : true; const fetchPerDiem = useCallback(() => { @@ -161,7 +160,7 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { const subRatesList = useMemo( () => - (lodashSortBy(allSubRates, 'destination', localeCompare) as SubRateData[]).map((value) => { + allSubRates.map((value) => { const isDisabled = value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; return { text: value.destination, @@ -196,6 +195,10 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { [allSubRates, selectedPerDiem, canSelectMultiple, styles.flex2, styles.alignItemsStart, styles.textSupporting, styles.label, styles.pl2, styles.alignSelfEnd], ); + const filterRate = useCallback((rate: PolicyOption, searchInput: string) => !!rate.text?.toLowerCase().includes(searchInput), []); + const sortRates = useCallback((rates: PolicyOption[]) => lodashSortBy(rates, 'text', localeCompare) as PolicyOption[], []); + const [inputValue, setInputValue, filteredSubRatesList] = useSearchResults(subRatesList, filterRate, sortRates); + const toggleSubRate = (subRate: PolicyOption) => { if (selectedPerDiem.find((selectedSubRate) => selectedSubRate.subRateID === subRate.subRateID) !== undefined) { setSelectedPerDiem((prev) => prev.filter((selectedSubRate) => selectedSubRate.subRateID !== subRate.subRateID)); @@ -212,7 +215,11 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { if (selectedPerDiem.length > 0) { setSelectedPerDiem([]); } else { - setSelectedPerDiem([...allSelectableSubRates]); + const availablePerDiemRates = allSubRates.filter( + (subRate) => + subRate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && filteredSubRatesList.some((filteredSubRate) => filteredSubRate.subRateID === subRate.subRateID), + ); + setSelectedPerDiem(availablePerDiemRates); } }; @@ -408,7 +415,15 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { danger /> {shouldUseNarrowLayout && {getHeaderButtons()}} - {(!shouldUseNarrowLayout || !hasVisibleSubRates || isLoading) && getHeaderText()} + {(!hasVisibleSubRates || isLoading) && getHeaderText()} + {subRatesList.length > CONST.SEARCH_ITEM_LIMIT && ( + + )} {isLoading && ( item && toggleSubRate(item)} - sections={[{data: subRatesList, isDisabled: false}]} + sections={[{data: filteredSubRatesList, isDisabled: false}]} onCheckboxPress={toggleSubRate} onSelectRow={openSubRateDetails} shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} @@ -458,7 +473,6 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { ListItem={TableListItem} customListHeader={getCustomListHeader()} listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} - listHeaderContent={shouldUseNarrowLayout ? getHeaderText() : null} listItemTitleContainerStyles={styles.flex3} showScrollIndicator={false} /> diff --git a/src/pages/workspace/reportFields/ReportFieldsListValuesPage.tsx b/src/pages/workspace/reportFields/ReportFieldsListValuesPage.tsx index c6e18d27a55b6..4d01113571eac 100644 --- a/src/pages/workspace/reportFields/ReportFieldsListValuesPage.tsx +++ b/src/pages/workspace/reportFields/ReportFieldsListValuesPage.tsx @@ -11,6 +11,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; +import SearchBar from '@components/SearchBar'; import TableListItem from '@components/SelectionList/TableListItem'; import type {ListItem} from '@components/SelectionList/types'; import SelectionListWithModal from '@components/SelectionListWithModal'; @@ -22,6 +23,7 @@ import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; +import useSearchResults from '@hooks/useSearchResults'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import { @@ -116,9 +118,9 @@ function ReportFieldsListValuesPage({ onNavigationCallBack: () => Navigation.goBack(), }); - const listValuesSections = useMemo(() => { - const data = listValues - .map((value, index) => ({ + const data = useMemo( + () => + listValues.map((value, index) => ({ value, index, text: value, @@ -132,10 +134,15 @@ function ReportFieldsListValuesPage({ onToggle={(newValue: boolean) => updateReportFieldListValueEnabled(newValue, index)} /> ), - })) - .sort((a, b) => localeCompare(a.value, b.value)); - return [{data, isDisabled: false}]; - }, [canSelectMultiple, disabledListValues, listValues, selectedValues, translate, updateReportFieldListValueEnabled]); + })), + [canSelectMultiple, disabledListValues, listValues, selectedValues, translate, updateReportFieldListValueEnabled], + ); + + const filterListValue = useCallback((item: ValueListItem, searchInput: string) => !!item.text?.toLowerCase().includes(searchInput.toLowerCase()), []); + const sortListValues = useCallback((values: ValueListItem[]) => values.sort((a, b) => localeCompare(a.value, b.value)), []); + const [inputValue, setInputValue, filteredListValues] = useSearchResults(data, filterListValue, sortListValues); + + const filteredListValuesArray = filteredListValues.map((item) => item.value); const shouldShowEmptyState = Object.values(listValues ?? {}).length <= 0; const selectedValuesArray = Object.keys(selectedValues).filter((key) => selectedValues[key] && listValues.includes(key)); @@ -148,7 +155,7 @@ function ReportFieldsListValuesPage({ }; const toggleAllValues = () => { - setSelectedValues(selectedValuesArray.length > 0 ? {} : Object.fromEntries(listValues.map((value) => [value, true]))); + setSelectedValues(selectedValuesArray.length > 0 ? {} : Object.fromEntries(filteredListValuesArray.map((value) => [value, true]))); }; const handleDeleteValues = () => { @@ -323,6 +330,14 @@ function ReportFieldsListValuesPage({ {translate('workspace.reportFields.listInputSubtitle')} + {filteredListValues.length > CONST.SEARCH_ITEM_LIMIT && ( + + )} {shouldShowEmptyState && ( item && toggleValue(item)} - sections={listValuesSections} + sections={[{data: filteredListValues, isDisabled: false}]} onCheckboxPress={toggleValue} onSelectRow={openListValuePage} onSelectAll={toggleAllValues} diff --git a/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx b/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx index d099e75622e52..0326b776d7f0f 100644 --- a/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx +++ b/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx @@ -14,6 +14,7 @@ import {Pencil} from '@components/Icon/Illustrations'; import LottieAnimations from '@components/LottieAnimations'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; +import SearchBar from '@components/SearchBar'; import ListItemRightCaretWithLabel from '@components/SelectionList/ListItemRightCaretWithLabel'; import TableListItem from '@components/SelectionList/TableListItem'; import type {ListItem} from '@components/SelectionList/types'; @@ -30,6 +31,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchResults from '@hooks/useSearchResults'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {isConnectionInProgress, isConnectionUnverified} from '@libs/actions/connections'; @@ -118,30 +120,26 @@ function WorkspaceReportFieldsPage({ const reportFieldsSections = useMemo(() => { if (!policy) { - return [{data: [], isDisabled: true}]; + return []; } + return Object.values(selectionFieldList).map((reportField) => ({ + value: reportField.name, + fieldID: reportField.fieldID, + keyForList: String(reportField.fieldID), + orderWeight: reportField.orderWeight, + pendingAction: reportField.pendingAction, + isSelected: selectedReportFields.includes(getReportFieldKey(reportField.fieldID)) && canSelectMultiple, + isDisabled: reportField.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + text: reportField.name, + rightElement: , + })); + }, [canSelectMultiple, selectionFieldList, policy, selectedReportFields, translate]); - return [ - { - data: Object.values(selectionFieldList) - .sort((a, b) => localeCompare(a.name, b.name)) - .map((reportField) => ({ - value: reportField.name, - fieldID: reportField.fieldID, - keyForList: String(reportField.fieldID), - orderWeight: reportField.orderWeight, - pendingAction: reportField.pendingAction, - isSelected: selectedReportFields.includes(getReportFieldKey(reportField.fieldID)) && canSelectMultiple, - isDisabled: reportField.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - text: reportField.name, - rightElement: , - })), - isDisabled: false, - }, - ]; - }, [selectionFieldList, policy, canSelectMultiple, translate, selectedReportFields]); + const filterReportField = useCallback((reportField: ReportFieldForList, searchInput: string) => !!reportField.text?.toLowerCase().includes(searchInput), []); + const sortReportFields = useCallback((reportFields: ReportFieldForList[]) => reportFields.sort((a, b) => localeCompare(a.value, b.value)), []); + const [inputValue, setInputValue, filteredReportFields] = useSearchResults(reportFieldsSections, filterReportField, sortReportFields); - useAutoTurnSelectionModeOffWhenHasNoActiveOption(reportFieldsSections.at(0)?.data ?? ([] as ListItem[])); + useAutoTurnSelectionModeOffWhenHasNoActiveOption(filteredReportFields); const updateSelectedReportFields = (item: ReportFieldForList) => { const fieldKey = getReportFieldKey(item.fieldID); @@ -154,7 +152,9 @@ function WorkspaceReportFieldsPage({ }; const toggleAllReportFields = () => { - const availableReportFields = Object.values(selectionFieldList).filter((reportField) => reportField.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + const availableReportFields = Object.values(reportFieldsSections).filter( + (reportField) => reportField.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && filteredReportFields.find((item) => item.fieldID === reportField.fieldID), + ); setSelectedReportFields(selectedReportFields.length > 0 ? [] : Object.values(availableReportFields).map((reportField) => getReportFieldKey(reportField.fieldID))); }; @@ -281,6 +281,15 @@ function WorkspaceReportFieldsPage({ danger /> {(!shouldUseNarrowLayout || !hasVisibleReportField || isLoading) && getHeaderText()} + {!shouldShowEmptyState && !isLoading && shouldUseNarrowLayout && getHeaderText()} + {reportFieldsSections.length > CONST.SEARCH_ITEM_LIMIT && ( + + )} {isLoading && ( )} - {shouldShowEmptyState && ( + {shouldShowEmptyState && filteredReportFields.length === 0 && ( item && updateSelectedReportFields(item)} - sections={reportFieldsSections} + sections={[ + { + data: filteredReportFields, + isDisabled: !policy, + }, + ]} onCheckboxPress={updateSelectedReportFields} onSelectRow={navigateToReportFieldsSettings} onSelectAll={toggleAllReportFields} diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 3ec448e9bd9f5..fab5709f22ec5 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -15,6 +15,7 @@ import LottieAnimations from '@components/LottieAnimations'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; +import SearchBar from '@components/SearchBar'; import TableListItem from '@components/SelectionList/TableListItem'; import SelectionListWithModal from '@components/SelectionListWithModal'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; @@ -30,6 +31,7 @@ import useNetwork from '@hooks/useNetwork'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; +import useSearchResults from '@hooks/useSearchResults'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useThreeDotsAnchorPosition from '@hooks/useThreeDotsAnchorPosition'; @@ -187,8 +189,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }; }); } - const sortedTags = lodashSortBy(Object.values(policyTagLists.at(0)?.tags ?? {}), 'name', localeCompare) as PolicyTag[]; - return sortedTags.map((tag) => ({ + return Object.values(policyTagLists?.at(0)?.tags ?? {}).map((tag) => ({ value: tag.name, text: getCleanedTagName(tag.name), keyForList: tag.name, @@ -208,13 +209,17 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { })); }, [isMultiLevelTags, policyTagLists, selectedTags, canSelectMultiple, translate, updateWorkspaceRequiresTag, updateWorkspaceTagEnabled]); - const tagListKeyedByName = useMemo( + const filterTag = useCallback((tag: TagListItem, searchInput: string) => !!tag.text?.toLowerCase().includes(searchInput) || !!tag.value?.toLowerCase().includes(searchInput), []); + const sortTags = useCallback((tags: TagListItem[]) => lodashSortBy(tags, 'value', localeCompare) as TagListItem[], []); + const [inputValue, setInputValue, filteredTagList] = useSearchResults(tagList, filterTag, sortTags); + + const filteredTagListKeyedByName = useMemo( () => - tagList.reduce>((acc, tag) => { + filteredTagList.reduce>((acc, tag) => { acc[tag.value] = tag; return acc; }, {}), - [tagList], + [filteredTagList], ); const toggleTag = (tag: TagListItem) => { @@ -225,7 +230,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }; const toggleAllTags = () => { - const availableTags = tagList.filter((tag) => tag.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + const availableTags = filteredTagList.filter((tag) => tag.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); setSelectedTags(Object.keys(selectedTags).length > 0 ? {} : Object.fromEntries(availableTags.map((item) => [item.value, true]))); }; @@ -312,7 +317,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { let disabledTagCount = 0; const tagsToEnable: Record = {}; for (const tagName of selectedTagsArray) { - if (tagListKeyedByName[tagName]?.enabled) { + if (filteredTagListKeyedByName[tagName]?.enabled) { enabledTagCount++; tagsToDisable[tagName] = { name: tagName, @@ -480,6 +485,14 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { danger /> {(!shouldUseNarrowLayout || !hasVisibleTags || isLoading) && getHeaderText()} + {tagList.length > CONST.SEARCH_ITEM_LIMIT && ( + + )} {isLoading && ( item && toggleTag(item)} - sections={[{data: tagList, isDisabled: false}]} + sections={[{data: filteredTagList, isDisabled: false}]} onCheckboxPress={toggleTag} onSelectRow={navigateToTagSettings} shouldSingleExecuteRowSelect={!canSelectMultiple} diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index cb3c1689160bf..caee3ea059bdb 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -10,6 +10,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; +import SearchBar from '@components/SearchBar'; import TableListItem from '@components/SelectionList/TableListItem'; import SelectionListWithModal from '@components/SelectionListWithModal'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; @@ -21,6 +22,7 @@ import useNetwork from '@hooks/useNetwork'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; +import useSearchResults from '@hooks/useSearchResults'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; @@ -65,7 +67,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const policyID = route.params.policyID; const backTo = route.params.backTo; const policy = usePolicy(policyID); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: false}); const {selectionMode} = useMobileSelectionMode(); const currentTagListName = useMemo(() => getTagListName(policyTags, route.params.orderWeight), [policyTags, route.params.orderWeight]); const currentPolicyTag = policyTags?.[currentTagListName]; @@ -108,38 +110,40 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const tagList = useMemo( () => - Object.values(currentPolicyTag?.tags ?? {}) - .sort((tagA, tagB) => localeCompare(tagA.name, tagB.name)) - .map((tag) => ({ - value: tag.name, - text: getCleanedTagName(tag.name), - keyForList: tag.name, - isSelected: selectedTags.includes(tag.name) && canSelectMultiple, - pendingAction: tag.pendingAction, - errors: tag.errors ?? undefined, - enabled: tag.enabled, - isDisabled: tag.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - rightElement: ( - updateWorkspaceTagEnabled(newValue, tag.name)} - /> - ), - })), + Object.values(currentPolicyTag?.tags ?? {}).map((tag) => ({ + value: tag.name, + text: getCleanedTagName(tag.name), + keyForList: tag.name, + isSelected: selectedTags.includes(tag.name) && canSelectMultiple, + pendingAction: tag.pendingAction, + errors: tag.errors ?? undefined, + enabled: tag.enabled, + isDisabled: tag.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + rightElement: ( + updateWorkspaceTagEnabled(newValue, tag.name)} + /> + ), + })), [currentPolicyTag?.tags, selectedTags, canSelectMultiple, translate, updateWorkspaceTagEnabled], ); + const filterTag = useCallback((tag: TagListItem, searchInput: string) => !!tag.text?.toLowerCase().includes(searchInput) || !!tag.value?.toLowerCase().includes(searchInput), []); + const sortTags = useCallback((tags: TagListItem[]) => tags.sort((tagA, tagB) => localeCompare(tagA.value, tagB.value)), []); + const [inputValue, setInputValue, filteredTagList] = useSearchResults(tagList, filterTag, sortTags); + const hasDependentTags = useMemo(() => hasDependentTagsPolicyUtils(policy, policyTags), [policy, policyTags]); const tagListKeyedByName = useMemo( () => - tagList.reduce>((acc, tag) => { + filteredTagList.reduce>((acc, tag) => { acc[tag.value] = tag; return acc; }, {}), - [tagList], + [filteredTagList], ); if (!currentPolicyTag) { @@ -156,7 +160,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { }; const toggleAllTags = () => { - const availableTags = tagList.filter((tag) => tag.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + const availableTags = filteredTagList.filter((tag) => tag.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); const anySelected = availableTags.some((tag) => selectedTags.includes(tag.value)); setSelectedTags(anySelected ? [] : availableTags.map((tag) => tag.value)); @@ -349,12 +353,20 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { color={theme.spinner} /> )} - {tagList.length > 0 && !isLoading && ( + {tagList.length > CONST.SEARCH_ITEM_LIMIT && ( + + )} + {filteredTagList.length > 0 && !isLoading && ( item && toggleTag(item)} - sections={[{data: tagList, isDisabled: false}]} + sections={[{data: filteredTagList, isDisabled: false}]} onCheckboxPress={toggleTag} onSelectRow={navigateToTagSettings} onSelectAll={toggleAllTags} diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index 3e50c1aa59502..d28469d97744a 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -9,6 +9,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; +import SearchBar from '@components/SearchBar'; import TableListItem from '@components/SelectionList/TableListItem'; import type {ListItem} from '@components/SelectionList/types'; import SelectionListWithModal from '@components/SelectionListWithModal'; @@ -23,6 +24,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; +import useSearchResults from '@hooks/useSearchResults'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {isConnectionInProgress, isConnectionUnverified} from '@libs/actions/connections'; @@ -157,32 +159,44 @@ function WorkspaceTaxesPage({ if (!policy) { return []; } - return Object.entries(policy.taxRates?.taxes ?? {}) - .map(([key, value]) => { - const canEditTaxRate = policy && canEditTaxRatePolicyUtils(policy, key); - - return { - text: value.name, - alternateText: textForDefault(key, value), - keyForList: key, - isSelected: !!selectedTaxesIDs.includes(key) && canSelectMultiple, - isDisabledCheckbox: !canEditTaxRatePolicyUtils(policy, key), - isDisabled: value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - pendingAction: value.pendingAction ?? (Object.keys(value.pendingFields ?? {}).length > 0 ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : null), - errors: value.errors ?? getLatestErrorFieldForAnyField(value), - rightElement: ( - updateWorkspaceTaxEnabled(newValue, key)} - /> - ), - }; - }) - .sort((a, b) => (a.text ?? a.keyForList ?? '').localeCompare(b.text ?? b.keyForList ?? '')); + return Object.entries(policy.taxRates?.taxes ?? {}).map(([key, value]) => { + const canEditTaxRate = policy && canEditTaxRatePolicyUtils(policy, key); + + return { + text: value.name, + alternateText: textForDefault(key, value), + keyForList: key, + isSelected: !!selectedTaxesIDs.includes(key) && canSelectMultiple, + isDisabledCheckbox: !canEditTaxRatePolicyUtils(policy, key), + isDisabled: value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + pendingAction: value.pendingAction ?? (Object.keys(value.pendingFields ?? {}).length > 0 ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : null), + errors: value.errors ?? getLatestErrorFieldForAnyField(value), + rightElement: ( + updateWorkspaceTaxEnabled(newValue, key)} + /> + ), + }; + }); }, [policy, textForDefault, selectedTaxesIDs, canSelectMultiple, translate, updateWorkspaceTaxEnabled]); + const filterTax = useCallback((tax: ListItem, searchInput: string) => { + const taxName = tax.text?.toLowerCase() ?? ''; + const taxAlternateText = tax.alternateText?.toLowerCase() ?? ''; + return taxName.includes(searchInput) || taxAlternateText.includes(searchInput); + }, []); + const sortTaxes = useCallback((taxes: ListItem[]) => { + return taxes.sort((a, b) => { + const aText = a.text ?? a.keyForList ?? ''; + const bText = b.text ?? b.keyForList ?? ''; + return aText.localeCompare(bText); + }); + }, []); + const [inputValue, setInputValue, filteredTaxesList] = useSearchResults(taxesList, filterTax, sortTaxes); + const isLoading = !isOffline && taxesList === undefined; const toggleTax = (tax: ListItem) => { @@ -200,7 +214,7 @@ function WorkspaceTaxesPage({ }; const toggleAllTaxes = () => { - const taxesToSelect = taxesList.filter( + const taxesToSelect = filteredTaxesList.filter( (tax) => tax.keyForList !== defaultExternalID && tax.keyForList !== foreignTaxDefault && tax.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); setSelectedTaxesIDs((prev) => { @@ -370,7 +384,15 @@ function WorkspaceTaxesPage({ {shouldUseNarrowLayout && {headerButtons}} - {!shouldUseNarrowLayout && getHeaderText()} + {getHeaderText()} + {taxesList.length > CONST.SEARCH_ITEM_LIMIT && ( + + )} {isLoading && ( item && toggleTax(item)} - sections={[{data: taxesList, isDisabled: false}]} + sections={[{data: filteredTaxesList, isDisabled: false}]} onCheckboxPress={toggleTax} onSelectRow={navigateToEditTaxRate} onSelectAll={toggleAllTaxes} ListItem={TableListItem} customListHeader={getCustomListHeader()} - listHeaderContent={shouldUseNarrowLayout ? getHeaderText() : null} shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} onDismissError={(item) => (item.keyForList ? clearTaxRateError(policyID, item.keyForList, item.pendingAction) : undefined)} diff --git a/src/styles/index.ts b/src/styles/index.ts index 4ceffc5851d3f..9508934603d2e 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -5651,6 +5651,12 @@ const styles = (theme: ThemeColors) => right: 0, }, + getSearchBarStyle: (shouldUseNarrowLayout: boolean) => ({ + maxWidth: shouldUseNarrowLayout ? '100%' : 300, + marginHorizontal: 20, + marginBottom: 20, + }), + earlyDiscountButton: { flexGrow: 1, flexShrink: 1, diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index 0bffc32ab0f93..947b67a3570bd 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -10,6 +10,7 @@ import { getBankCardDetailsImage, getBankName, getCardFeedIcon, + getCardsByCardholderName, getCompanyFeeds, getCustomOrFormattedFeedName, getFeedType, @@ -923,7 +924,8 @@ describe('CardUtils', () => { it('should sort cards by cardholder name in ascending order', () => { const policyMembersAccountIDs = [1, 2, 3]; - const sortedCards = sortCardsByCardholderName(mockCards, mockPersonalDetails, policyMembersAccountIDs); + const cards = getCardsByCardholderName(mockCards, policyMembersAccountIDs); + const sortedCards = sortCardsByCardholderName(cards, mockPersonalDetails); expect(sortedCards).toHaveLength(3); expect(sortedCards.at(0)?.cardID).toBe(2); @@ -933,7 +935,8 @@ describe('CardUtils', () => { it('should filter out cards that are not associated with policy members', () => { const policyMembersAccountIDs = [1, 2]; // Exclude accountID 3 - const sortedCards = sortCardsByCardholderName(mockCards, mockPersonalDetails, policyMembersAccountIDs); + const cards = getCardsByCardholderName(mockCards, policyMembersAccountIDs); + const sortedCards = sortCardsByCardholderName(cards, mockPersonalDetails); expect(sortedCards).toHaveLength(2); expect(sortedCards.at(0)?.cardID).toBe(2); @@ -942,14 +945,16 @@ describe('CardUtils', () => { it('should handle undefined cardsList', () => { const policyMembersAccountIDs = [1, 2, 3]; - const sortedCards = sortCardsByCardholderName(undefined, mockPersonalDetails, policyMembersAccountIDs); + const cards = getCardsByCardholderName(undefined, policyMembersAccountIDs); + const sortedCards = sortCardsByCardholderName(cards, mockPersonalDetails); expect(sortedCards).toHaveLength(0); }); it('should handle undefined personalDetails', () => { const policyMembersAccountIDs = [1, 2, 3]; - const sortedCards = sortCardsByCardholderName(mockCards, undefined, policyMembersAccountIDs); + const cards = getCardsByCardholderName(mockCards, policyMembersAccountIDs); + const sortedCards = sortCardsByCardholderName(cards, undefined); expect(sortedCards).toHaveLength(3); // All cards should be sorted with default names @@ -986,7 +991,8 @@ describe('CardUtils', () => { }; const policyMembersAccountIDs = [1, 2]; - const sortedCards = sortCardsByCardholderName(cardsWithMissingAccountID, mockPersonalDetails, policyMembersAccountIDs); + const cards = getCardsByCardholderName(cardsWithMissingAccountID, policyMembersAccountIDs); + const sortedCards = sortCardsByCardholderName(cards, mockPersonalDetails); expect(sortedCards).toHaveLength(1); expect(sortedCards.at(0)?.cardID).toBe(1);