diff --git a/src/components/Search/SearchPageHeader/useSearchPageInput.tsx b/src/components/Search/SearchPageHeader/useSearchPageInput.tsx index 6402c8f7fe30..9fd3276d772a 100644 --- a/src/components/Search/SearchPageHeader/useSearchPageInput.tsx +++ b/src/components/Search/SearchPageHeader/useSearchPageInput.tsx @@ -118,7 +118,7 @@ function useSearchPageInput({queryJSON, onSearch, onSubmit}: UseSearchPageInputP } function submitSearch(queryString: SearchQueryString, shouldSkipAmountConversion = false) { - const queryWithSubstitutions = getQueryWithSubstitutions(queryString, autocompleteSubstitutions); + const queryWithSubstitutions = getQueryWithSubstitutions(queryString, autocompleteSubstitutions, currentUserAccountID); const updatedQuery = getQueryWithUpdatedValues(queryWithSubstitutions, shouldSkipAmountConversion); if (!updatedQuery) { diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 5b2e8e333dbf..53ac52205793 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -258,7 +258,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla const submitSearch = useCallback( (queryString: SearchQueryString, shouldSkipAmountConversion = false) => { - const queryWithSubstitutions = getQueryWithSubstitutions(queryString, autocompleteSubstitutions); + const queryWithSubstitutions = getQueryWithSubstitutions(queryString, autocompleteSubstitutions, currentUserAccountID); const updatedQuery = getQueryWithUpdatedValues(queryWithSubstitutions, shouldSkipAmountConversion); if (!updatedQuery) { return; @@ -276,7 +276,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla setTextInputValue(''); setAutocompleteQueryValue(''); }, - [autocompleteSubstitutions, onRouterClose, setTextInputValue, setShouldResetSearchQuery], + [autocompleteSubstitutions, currentUserAccountID, onRouterClose, setTextInputValue, setShouldResetSearchQuery], ); const onListItemPress = useCallback( diff --git a/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts b/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts index 90e46b0299d3..466b5d9ed56c 100644 --- a/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts +++ b/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts @@ -1,11 +1,21 @@ import type {SearchAutocompleteQueryRange, SearchFilterKey} from '@components/Search/types'; import {parse} from '@libs/SearchParser/autocompleteParser'; import {sanitizeSearchValue} from '@libs/SearchQueryUtils'; +import CONST from '@src/CONST'; type SubstitutionMap = Record; const getSubstitutionMapKey = (filterKey: SearchFilterKey, value: string) => `${filterKey}:${value}`; +const USER_FILTER_KEYS = new Set([ + CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM, + CONST.SEARCH.SYNTAX_FILTER_KEYS.TO, + CONST.SEARCH.SYNTAX_FILTER_KEYS.ASSIGNEE, + CONST.SEARCH.SYNTAX_FILTER_KEYS.PAYER, + CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPORTER, + CONST.SEARCH.SYNTAX_FILTER_KEYS.ATTENDEE, +]); + /** * Key for the Nth occurrence of the same filter+value (e.g. multiple workspaces with the same name). * Index 0 uses the base key for backward compatibility; index > 0 uses baseKey:index. @@ -27,7 +37,7 @@ const getSubstitutionMapKeyWithIndex = (filterKey: SearchFilterKey, value: strin * } * return: `A from:9876 A` */ -function getQueryWithSubstitutions(changedQuery: string, substitutions: SubstitutionMap) { +function getQueryWithSubstitutions(changedQuery: string, substitutions: SubstitutionMap, currentUserAccountID?: number) { const parsed = parse(changedQuery) as {ranges: SearchAutocompleteQueryRange[]}; const searchAutocompleteQueryRanges = parsed.ranges; @@ -57,6 +67,11 @@ function getQueryWithSubstitutions(changedQuery: string, substitutions: Substitu const itemKey = getSubstitutionMapKeyWithIndex(range.key, range.value, index); let substitutionEntry = substitutions[itemKey] ?? (index === 0 ? substitutions[getSubstitutionMapKey(range.key, range.value)] : undefined); + // Resolve the 'me' keyword to the current user's account ID when not in the substitution map + if (!substitutionEntry && range.value === CONST.SEARCH.ME && USER_FILTER_KEYS.has(range.key) && currentUserAccountID && currentUserAccountID > 0) { + substitutionEntry = currentUserAccountID.toString(); + } + if (substitutionEntry) { const substitutionStart = range.start + lengthDiff; const substitutionEnd = range.start + range.length; diff --git a/src/libs/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index d03dad58d391..c607e917dc3c 100644 --- a/src/libs/SearchAutocompleteUtils.ts +++ b/src/libs/SearchAutocompleteUtils.ts @@ -188,7 +188,7 @@ function filterOutRangesWithCorrectValue( case CONST.SEARCH.SYNTAX_FILTER_KEYS.PAYER: case CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPORTER: case CONST.SEARCH.SYNTAX_FILTER_KEYS.ATTENDEE: - return substitutionMap[`${range.key}:${range.value}`] !== undefined || userLogins.get().includes(range.value); + return substitutionMap[`${range.key}:${range.value}`] !== undefined || userLogins.get().includes(range.value) || range.value === CONST.SEARCH.ME; case CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY: case CONST.SEARCH.SYNTAX_FILTER_KEYS.GROUP_CURRENCY: case CONST.SEARCH.SYNTAX_FILTER_KEYS.PURCHASE_CURRENCY: diff --git a/tests/unit/Search/getQueryWithSubstitutionsTest.ts b/tests/unit/Search/getQueryWithSubstitutionsTest.ts index 2ae4eda7b25d..b7635cf86546 100644 --- a/tests/unit/Search/getQueryWithSubstitutionsTest.ts +++ b/tests/unit/Search/getQueryWithSubstitutionsTest.ts @@ -102,4 +102,65 @@ describe('getQueryWithSubstitutions should compute and return correct new query' expect(result).toBe('workspace:policyA,policyB,policyC'); }); + + test('when "me" is pasted on a user-based filter and the substitution map is empty, it resolves to currentUserAccountID', () => { + const userTypedQuery = 'type:expense-report action:submit from:me'; + const substitutionsMock = {}; + const currentUserAccountID = 1234; + + const result = getQueryWithSubstitutions(userTypedQuery, substitutionsMock, currentUserAccountID); + + expect(result).toBe('type:expense-report action:submit from:1234'); + }); + + test('when "me" is used on every user-based filter key, each occurrence resolves to currentUserAccountID', () => { + const userTypedQuery = 'from:me to:me assignee:me payer:me exporter:me attendee:me'; + const substitutionsMock = {}; + const currentUserAccountID = 9876; + + const result = getQueryWithSubstitutions(userTypedQuery, substitutionsMock, currentUserAccountID); + + expect(result).toBe('from:9876 to:9876 assignee:9876 payer:9876 exporter:9876 attendee:9876'); + }); + + test('when an existing substitution exists for "me", it takes precedence over currentUserAccountID', () => { + const userTypedQuery = 'from:me'; + const substitutionsMock = { + 'from:me': '5555', + }; + const currentUserAccountID = 1234; + + const result = getQueryWithSubstitutions(userTypedQuery, substitutionsMock, currentUserAccountID); + + expect(result).toBe('from:5555'); + }); + + test('when "me" is used on a non-user-based filter, it is not resolved to currentUserAccountID', () => { + const userTypedQuery = 'category:me'; + const substitutionsMock = {}; + const currentUserAccountID = 1234; + + const result = getQueryWithSubstitutions(userTypedQuery, substitutionsMock, currentUserAccountID); + + expect(result).toBe('category:me'); + }); + + test('when currentUserAccountID is undefined, "me" is left unresolved', () => { + const userTypedQuery = 'from:me'; + const substitutionsMock = {}; + + const result = getQueryWithSubstitutions(userTypedQuery, substitutionsMock); + + expect(result).toBe('from:me'); + }); + + test('when currentUserAccountID is -1 (not signed in), "me" is left unresolved', () => { + const userTypedQuery = 'from:me'; + const substitutionsMock = {}; + const currentUserAccountID = -1; + + const result = getQueryWithSubstitutions(userTypedQuery, substitutionsMock, currentUserAccountID); + + expect(result).toBe('from:me'); + }); }); diff --git a/tests/unit/SearchAutocompleteUtilsTest.ts b/tests/unit/SearchAutocompleteUtilsTest.ts index 1e2b08f93115..5683689297f6 100644 --- a/tests/unit/SearchAutocompleteUtilsTest.ts +++ b/tests/unit/SearchAutocompleteUtilsTest.ts @@ -186,6 +186,32 @@ describe('SearchAutocompleteUtils', () => { ]); }); + it('should highlight FROM filter with the "me" keyword as mention-here', () => { + const input = 'from:me'; + + const result = parseForLiveMarkdown(input, currentUserName, mockSubstitutionMap, mockUserLogins, mockCurrencyList, mockCategoryList, mockTagList, mockExportedToList); + + expect(result).toEqual([ + {start: 5, type: 'mention-here', length: 2}, // from:me + ]); + }); + + it('should highlight every user-based filter with the "me" keyword as mention-here', () => { + const inputs: Array<[string, number]> = [ + ['from:me', 5], + ['to:me', 3], + ['assignee:me', 9], + ['payer:me', 6], + ['exporter:me', 9], + ['attendee:me', 9], + ]; + + for (const [input, start] of inputs) { + const result = parseForLiveMarkdown(input, currentUserName, mockSubstitutionMap, mockUserLogins, mockCurrencyList, mockCategoryList, mockTagList, mockExportedToList); + expect(result).toEqual([{start, type: 'mention-here', length: 2}]); + } + }); + it('should handle complex queries with multiple new filters', () => { const input = 'type:expense purchaseCurrency:USD purchaseAmount:50.00 title:"Expense Report" attendee:john@example.com';