Skip to content
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@
"pdfs",
"Perfetto",
"persistable",
"peterparker",
"Pettinella",
"phonenumber",
"Picklist",
Expand Down
6 changes: 4 additions & 2 deletions src/components/Search/FilterDropdowns/UserSelectPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,18 @@ 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,
isSelected: selectedAccountIDs.has(newOption.accountID.toString()),
};
}
return newOption;
}, [currentOption, cleanSearchTerm, selectedAccountIDs]);
}, [currentOption, cleanSearchTerm, selectedAccountIDs, currentUserSearchTerms]);

const listData = useMemo(() => {
if (!filteredCurrentUserOption) {
Expand Down
13 changes: 8 additions & 5 deletions src/libs/PersonalDetailOptionsListUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,18 +178,21 @@ function filterUserToInvite(options: Omit<Options, 'userToInvite'>, 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;
Expand Down Expand Up @@ -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};
61 changes: 60 additions & 1 deletion tests/unit/PersonalDetailOptionsListUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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, '');
Expand Down Expand Up @@ -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();
});
});
});
Loading