From ac918a0c845f4e51edb1a8b4a2f265ab3a28bbe2 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Tue, 24 Feb 2026 03:23:43 +0000 Subject: [PATCH 1/5] Restore You/Me search matching for current user in From filter When PR #82490 migrated UserSelectPopup from OptionsListUtils to PersonalDetailOptionsListUtils, the "You"/"me" keyword matching for the current user was lost. The old getCurrentUserSearchTerms function explicitly added translated "You" and "me" strings as searchable terms for the current user, but the new filterOption/matchesSearchTerms functions only match against display name and email. This adds an optional extraSearchTerms parameter to filterOption and matchesSearchTerms, and passes the translated "You"/"me" strings from UserSelectPopup when filtering the current user option. Co-authored-by: Shubham Agrawal --- .../Search/FilterDropdowns/UserSelectPopup.tsx | 6 ++++-- .../PersonalDetailOptionsListUtils/index.ts | 11 +++++++---- .../unit/PersonalDetailOptionsListUtilsTest.ts | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) 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..88199f1b4ef18 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 += ` ${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; diff --git a/tests/unit/PersonalDetailOptionsListUtilsTest.ts b/tests/unit/PersonalDetailOptionsListUtilsTest.ts index 6ba15ec87493c..64677ff7fdbc6 100644 --- a/tests/unit/PersonalDetailOptionsListUtilsTest.ts +++ b/tests/unit/PersonalDetailOptionsListUtilsTest.ts @@ -808,5 +808,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(); + }); }); }); From c8ba18ca2bb7f24a678655510882efd89f860316 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Tue, 24 Feb 2026 06:25:46 +0000 Subject: [PATCH 2/5] Add unit tests for matchesSearchTerms with and without extraSearchTerms Co-authored-by: Hans --- .../PersonalDetailOptionsListUtils/index.ts | 2 +- .../PersonalDetailOptionsListUtilsTest.ts | 46 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/libs/PersonalDetailOptionsListUtils/index.ts b/src/libs/PersonalDetailOptionsListUtils/index.ts index 88199f1b4ef18..24837731e015a 100644 --- a/src/libs/PersonalDetailOptionsListUtils/index.ts +++ b/src/libs/PersonalDetailOptionsListUtils/index.ts @@ -432,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 64677ff7fdbc6..b886f7a066133 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, matchesSearchTerms, getValidOptions} from '@libs/PersonalDetailOptionsListUtils'; import type {OptionData} from '@libs/PersonalDetailOptionsListUtils/types'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; @@ -778,6 +778,50 @@ describe('PersonalDetailOptionsListUtils', () => { }); }); + describe('matchesSearchTerms', () => { + it('should match when search terms are found in option text', () => { + const option = OPTIONS.options.find((o) => o.text === 'Spider-Man') as OptionData; + expect(matchesSearchTerms(option, ['spider'])).toBe(true); + }); + + it('should match when search terms are found in option login', () => { + const option = OPTIONS.options.find((o) => o.login === 'peterparker@expensify.com') as OptionData; + expect(matchesSearchTerms(option, ['peterparker'])).toBe(true); + }); + + it('should not match when search terms are not found', () => { + const option = OPTIONS.options.find((o) => o.text === 'Spider-Man') as OptionData; + expect(matchesSearchTerms(option, ['nonexistent'])).toBe(false); + }); + + it('should require all search terms to match', () => { + const option = OPTIONS.options.find((o) => o.text === 'Spider-Man') as OptionData; + expect(matchesSearchTerms(option, ['spider', 'man'])).toBe(true); + expect(matchesSearchTerms(option, ['spider', 'nonexistent'])).toBe(false); + }); + + it('should match against extraSearchTerms when provided', () => { + const option = OPTIONS.currentUserOption as OptionData; + expect(matchesSearchTerms(option, ['you'], ['You', 'me'])).toBe(true); + expect(matchesSearchTerms(option, ['me'], ['You', 'me'])).toBe(true); + }); + + it('should not match unrelated search terms even with extraSearchTerms', () => { + const option = OPTIONS.currentUserOption as OptionData; + expect(matchesSearchTerms(option, ['nonexistent'], ['You', 'me'])).toBe(false); + }); + + it('should match against option text without needing extraSearchTerms', () => { + const option = OPTIONS.currentUserOption as OptionData; + expect(matchesSearchTerms(option, ['iron'])).toBe(true); + }); + + it('should match with empty search terms', () => { + const option = OPTIONS.currentUserOption as OptionData; + expect(matchesSearchTerms(option, [])).toBe(true); + }); + }); + describe('filterOption', () => { it('should return the option when there are no search string', () => { const result = filterOption(OPTIONS.currentUserOption, ''); From f158d87159796f6bc636eb7e623f1a543ece338e Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Tue, 24 Feb 2026 06:56:12 +0000 Subject: [PATCH 3/5] Fix ESLint, spellcheck, and Prettier errors in unit tests - Replace `as OptionData` type assertions with `!` non-null assertions using helper functions to satisfy non-nullable-type-assertion-style rule - Add 'peterparker' to cspell.json word list - Fix import ordering for Prettier compliance Co-authored-by: Hans --- cspell.json | 1 + .../PersonalDetailOptionsListUtilsTest.ts | 35 +++++++++---------- 2 files changed, 17 insertions(+), 19 deletions(-) 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/tests/unit/PersonalDetailOptionsListUtilsTest.ts b/tests/unit/PersonalDetailOptionsListUtilsTest.ts index b886f7a066133..3b930747d1ac8 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, matchesSearchTerms, 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'; @@ -779,46 +779,43 @@ 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', () => { - const option = OPTIONS.options.find((o) => o.text === 'Spider-Man') as OptionData; - expect(matchesSearchTerms(option, ['spider'])).toBe(true); + expect(matchesSearchTerms(getSpiderManOption(), ['spider'])).toBe(true); }); it('should match when search terms are found in option login', () => { - const option = OPTIONS.options.find((o) => o.login === 'peterparker@expensify.com') as OptionData; - expect(matchesSearchTerms(option, ['peterparker'])).toBe(true); + expect(matchesSearchTerms(getSpiderManOption(), ['peterparker'])).toBe(true); }); it('should not match when search terms are not found', () => { - const option = OPTIONS.options.find((o) => o.text === 'Spider-Man') as OptionData; - expect(matchesSearchTerms(option, ['nonexistent'])).toBe(false); + expect(matchesSearchTerms(getSpiderManOption(), ['nonexistent'])).toBe(false); }); it('should require all search terms to match', () => { - const option = OPTIONS.options.find((o) => o.text === 'Spider-Man') as OptionData; - expect(matchesSearchTerms(option, ['spider', 'man'])).toBe(true); - expect(matchesSearchTerms(option, ['spider', 'nonexistent'])).toBe(false); + expect(matchesSearchTerms(getSpiderManOption(), ['spider', 'man'])).toBe(true); + expect(matchesSearchTerms(getSpiderManOption(), ['spider', 'nonexistent'])).toBe(false); }); it('should match against extraSearchTerms when provided', () => { - const option = OPTIONS.currentUserOption as OptionData; - expect(matchesSearchTerms(option, ['you'], ['You', 'me'])).toBe(true); - expect(matchesSearchTerms(option, ['me'], ['You', 'me'])).toBe(true); + 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', () => { - const option = OPTIONS.currentUserOption as OptionData; - expect(matchesSearchTerms(option, ['nonexistent'], ['You', 'me'])).toBe(false); + expect(matchesSearchTerms(getCurrentUserOption(), ['nonexistent'], ['You', 'me'])).toBe(false); }); it('should match against option text without needing extraSearchTerms', () => { - const option = OPTIONS.currentUserOption as OptionData; - expect(matchesSearchTerms(option, ['iron'])).toBe(true); + expect(matchesSearchTerms(getCurrentUserOption(), ['iron'])).toBe(true); }); it('should match with empty search terms', () => { - const option = OPTIONS.currentUserOption as OptionData; - expect(matchesSearchTerms(option, [])).toBe(true); + expect(matchesSearchTerms(getCurrentUserOption(), [])).toBe(true); }); }); From 15563be1559f71c6e5bf9ff9bee7eb4ec9282439 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Tue, 24 Feb 2026 06:57:16 +0000 Subject: [PATCH 4/5] Fix: Resolve ESLint, Prettier, and spellcheck failures in test file - Replace type assertions (as/!) with proper null guards to satisfy both non-nullable-type-assertion-style and no-non-null-assertion rules - Add cspell:disable-next-line for peterparker test data references Co-authored-by: Shubham Agrawal --- .../PersonalDetailOptionsListUtilsTest.ts | 54 ++++++++++++------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/tests/unit/PersonalDetailOptionsListUtilsTest.ts b/tests/unit/PersonalDetailOptionsListUtilsTest.ts index b886f7a066133..152cb6e76e70d 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, matchesSearchTerms, 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'; @@ -779,46 +779,62 @@ describe('PersonalDetailOptionsListUtils', () => { }); describe('matchesSearchTerms', () => { + let spiderManOption: OptionData; + let currentUserOpt: OptionData; + + beforeEach(() => { + const foundSpider = OPTIONS.options.find((o) => o.text === 'Spider-Man'); + if (!foundSpider) { + throw new Error('Spider-Man option not found in test data'); + } + spiderManOption = foundSpider; + + const foundUser = OPTIONS.currentUserOption; + if (!foundUser) { + throw new Error('currentUserOption not found in test data'); + } + currentUserOpt = foundUser; + }); + it('should match when search terms are found in option text', () => { - const option = OPTIONS.options.find((o) => o.text === 'Spider-Man') as OptionData; - expect(matchesSearchTerms(option, ['spider'])).toBe(true); + expect(matchesSearchTerms(spiderManOption, ['spider'])).toBe(true); }); it('should match when search terms are found in option login', () => { - const option = OPTIONS.options.find((o) => o.login === 'peterparker@expensify.com') as OptionData; - expect(matchesSearchTerms(option, ['peterparker'])).toBe(true); + // cspell:disable-next-line + const peterOption = OPTIONS.options.find((o) => o.login === 'peterparker@expensify.com'); + if (!peterOption) { + // cspell:disable-next-line + throw new Error('peterparker option not found in test data'); + } + // cspell:disable-next-line + expect(matchesSearchTerms(peterOption, ['peterparker'])).toBe(true); }); it('should not match when search terms are not found', () => { - const option = OPTIONS.options.find((o) => o.text === 'Spider-Man') as OptionData; - expect(matchesSearchTerms(option, ['nonexistent'])).toBe(false); + expect(matchesSearchTerms(spiderManOption, ['nonexistent'])).toBe(false); }); it('should require all search terms to match', () => { - const option = OPTIONS.options.find((o) => o.text === 'Spider-Man') as OptionData; - expect(matchesSearchTerms(option, ['spider', 'man'])).toBe(true); - expect(matchesSearchTerms(option, ['spider', 'nonexistent'])).toBe(false); + expect(matchesSearchTerms(spiderManOption, ['spider', 'man'])).toBe(true); + expect(matchesSearchTerms(spiderManOption, ['spider', 'nonexistent'])).toBe(false); }); it('should match against extraSearchTerms when provided', () => { - const option = OPTIONS.currentUserOption as OptionData; - expect(matchesSearchTerms(option, ['you'], ['You', 'me'])).toBe(true); - expect(matchesSearchTerms(option, ['me'], ['You', 'me'])).toBe(true); + expect(matchesSearchTerms(currentUserOpt, ['you'], ['You', 'me'])).toBe(true); + expect(matchesSearchTerms(currentUserOpt, ['me'], ['You', 'me'])).toBe(true); }); it('should not match unrelated search terms even with extraSearchTerms', () => { - const option = OPTIONS.currentUserOption as OptionData; - expect(matchesSearchTerms(option, ['nonexistent'], ['You', 'me'])).toBe(false); + expect(matchesSearchTerms(currentUserOpt, ['nonexistent'], ['You', 'me'])).toBe(false); }); it('should match against option text without needing extraSearchTerms', () => { - const option = OPTIONS.currentUserOption as OptionData; - expect(matchesSearchTerms(option, ['iron'])).toBe(true); + expect(matchesSearchTerms(currentUserOpt, ['iron'])).toBe(true); }); it('should match with empty search terms', () => { - const option = OPTIONS.currentUserOption as OptionData; - expect(matchesSearchTerms(option, [])).toBe(true); + expect(matchesSearchTerms(currentUserOpt, [])).toBe(true); }); }); From 11ca06e237b6183ef10da004bda5b18c9e3638b0 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Tue, 24 Feb 2026 15:57:40 +0000 Subject: [PATCH 5/5] Fix: Deburr extraSearchTerms in matchesSearchTerms for accent-insensitive matching Co-authored-by: Shubham Agrawal --- src/libs/PersonalDetailOptionsListUtils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/PersonalDetailOptionsListUtils/index.ts b/src/libs/PersonalDetailOptionsListUtils/index.ts index 24837731e015a..dd3673a1d17b6 100644 --- a/src/libs/PersonalDetailOptionsListUtils/index.ts +++ b/src/libs/PersonalDetailOptionsListUtils/index.ts @@ -181,7 +181,7 @@ function filterUserToInvite(options: Omit, currentUserL function matchesSearchTerms(option: OptionData, searchTerms: string[], extraSearchTerms?: string[]): boolean { let searchText = deburr(`${option.text} ${option.login ?? ''}`.toLocaleLowerCase()); if (extraSearchTerms?.length) { - searchText += ` ${extraSearchTerms.join(' ').toLocaleLowerCase()}`; + searchText += ` ${deburr(extraSearchTerms.join(' ').toLocaleLowerCase())}`; } return searchTerms.every((term) => searchText.includes(term)); }