diff --git a/cspell.json b/cspell.json index 8b0d60fc35c3b..94660d110a723 100644 --- a/cspell.json +++ b/cspell.json @@ -519,6 +519,7 @@ "pdfs", "Perfetto", "persistable", + "peterparker", "Pettinella", "phonenumber", "Picklist", diff --git a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx index 0c500ec23a2d1..c64a5394ae431 100644 --- a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx +++ b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx @@ -89,8 +89,10 @@ function UserSelectPopup({value, closeOverlay, onChange, isSearchable}: UserSele }); }, [transformedOptions, currentUserEmail, cleanSearchTerm, formatPhoneNumber, countryCode, loginList]); + const currentUserSearchTerms = useMemo(() => [translate('common.you'), translate('common.me')], [translate]); + const filteredCurrentUserOption = useMemo(() => { - const newOption = filterOption(currentOption, cleanSearchTerm); + const newOption = filterOption(currentOption, cleanSearchTerm, currentUserSearchTerms); if (newOption) { return { ...newOption, @@ -98,7 +100,7 @@ function UserSelectPopup({value, closeOverlay, onChange, isSearchable}: UserSele }; } return newOption; - }, [currentOption, cleanSearchTerm, selectedAccountIDs]); + }, [currentOption, cleanSearchTerm, selectedAccountIDs, currentUserSearchTerms]); const listData = useMemo(() => { if (!filteredCurrentUserOption) { diff --git a/src/libs/PersonalDetailOptionsListUtils/index.ts b/src/libs/PersonalDetailOptionsListUtils/index.ts index 848ed5044c8bc..dd3673a1d17b6 100644 --- a/src/libs/PersonalDetailOptionsListUtils/index.ts +++ b/src/libs/PersonalDetailOptionsListUtils/index.ts @@ -178,18 +178,21 @@ function filterUserToInvite(options: Omit, currentUserL }); } -function matchesSearchTerms(option: OptionData, searchTerms: string[]): boolean { - const searchText = deburr(`${option.text} ${option.login ?? ''}`.toLocaleLowerCase()); +function matchesSearchTerms(option: OptionData, searchTerms: string[], extraSearchTerms?: string[]): boolean { + let searchText = deburr(`${option.text} ${option.login ?? ''}`.toLocaleLowerCase()); + if (extraSearchTerms?.length) { + searchText += ` ${deburr(extraSearchTerms.join(' ').toLocaleLowerCase())}`; + } return searchTerms.every((term) => searchText.includes(term)); } -function filterOption(option: OptionData | undefined, searchValue: string | undefined) { +function filterOption(option: OptionData | undefined, searchValue: string | undefined, extraSearchTerms?: string[]) { if (!option) { return null; } const searchTerms = processSearchString(searchValue); - const isMatchingSearch = matchesSearchTerms(option, searchTerms); + const isMatchingSearch = matchesSearchTerms(option, searchTerms, extraSearchTerms); if (isMatchingSearch) { return option; @@ -429,6 +432,6 @@ function getHeaderMessage(translate: LocaleContextProps['translate'], searchValu return translate('common.noResultsFound'); } -export {createOption, getUserToInviteOption, canCreateOptimisticPersonalDetailOption, filterOption, getValidOptions, createOptionList, getHeaderMessage}; +export {createOption, getUserToInviteOption, canCreateOptimisticPersonalDetailOption, filterOption, matchesSearchTerms, getValidOptions, createOptionList, getHeaderMessage}; export type {OptionData, Options}; diff --git a/tests/unit/PersonalDetailOptionsListUtilsTest.ts b/tests/unit/PersonalDetailOptionsListUtilsTest.ts index 6ba15ec87493c..964660395dfe4 100644 --- a/tests/unit/PersonalDetailOptionsListUtilsTest.ts +++ b/tests/unit/PersonalDetailOptionsListUtilsTest.ts @@ -4,7 +4,7 @@ import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import {FallbackAvatar} from '@components/Icon/Expensicons'; import DateUtils from '@libs/DateUtils'; -import {canCreateOptimisticPersonalDetailOption, createOption, createOptionList, filterOption, getValidOptions} from '@libs/PersonalDetailOptionsListUtils'; +import {canCreateOptimisticPersonalDetailOption, createOption, createOptionList, filterOption, getValidOptions, matchesSearchTerms} from '@libs/PersonalDetailOptionsListUtils'; import type {OptionData} from '@libs/PersonalDetailOptionsListUtils/types'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; @@ -778,6 +778,48 @@ describe('PersonalDetailOptionsListUtils', () => { }); }); + describe('matchesSearchTerms', () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const getSpiderManOption = () => OPTIONS.options.find((o) => o.text === 'Spider-Man')!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const getCurrentUserOption = () => OPTIONS.currentUserOption!; + + it('should match when search terms are found in option text', () => { + expect(matchesSearchTerms(getSpiderManOption(), ['spider'])).toBe(true); + }); + + it('should match when search terms are found in option login', () => { + // cspell:disable-next-line + expect(matchesSearchTerms(getSpiderManOption(), ['peterparker'])).toBe(true); + }); + + it('should not match when search terms are not found', () => { + expect(matchesSearchTerms(getSpiderManOption(), ['nonexistent'])).toBe(false); + }); + + it('should require all search terms to match', () => { + expect(matchesSearchTerms(getSpiderManOption(), ['spider', 'man'])).toBe(true); + expect(matchesSearchTerms(getSpiderManOption(), ['spider', 'nonexistent'])).toBe(false); + }); + + it('should match against extraSearchTerms when provided', () => { + expect(matchesSearchTerms(getCurrentUserOption(), ['you'], ['You', 'me'])).toBe(true); + expect(matchesSearchTerms(getCurrentUserOption(), ['me'], ['You', 'me'])).toBe(true); + }); + + it('should not match unrelated search terms even with extraSearchTerms', () => { + expect(matchesSearchTerms(getCurrentUserOption(), ['nonexistent'], ['You', 'me'])).toBe(false); + }); + + it('should match against option text without needing extraSearchTerms', () => { + expect(matchesSearchTerms(getCurrentUserOption(), ['iron'])).toBe(true); + }); + + it('should match with empty search terms', () => { + expect(matchesSearchTerms(getCurrentUserOption(), [])).toBe(true); + }); + }); + describe('filterOption', () => { it('should return the option when there are no search string', () => { const result = filterOption(OPTIONS.currentUserOption, ''); @@ -808,5 +850,22 @@ describe('PersonalDetailOptionsListUtils', () => { const result = filterOption(OPTIONS.currentUserOption, currentUserLogin); expect(result).toBeDefined(); }); + + it('should match current user option when searching "You" with extraSearchTerms', () => { + const result = filterOption(OPTIONS.currentUserOption, 'you', ['You', 'me']); + expect(result).toBeDefined(); + expect(result?.accountID).toBe(currentUserAccountID); + }); + + it('should match current user option when searching "me" with extraSearchTerms', () => { + const result = filterOption(OPTIONS.currentUserOption, 'me', ['You', 'me']); + expect(result).toBeDefined(); + expect(result?.accountID).toBe(currentUserAccountID); + }); + + it('should not match current user option when searching unrelated term even with extraSearchTerms', () => { + const result = filterOption(OPTIONS.currentUserOption, 'non-matching-string', ['You', 'me']); + expect(result).toBeNull(); + }); }); });