Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions src/components/Search/SearchRouter/SearchRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -276,7 +276,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
setTextInputValue('');
setAutocompleteQueryValue('');
},
[autocompleteSubstitutions, onRouterClose, setTextInputValue, setShouldResetSearchQuery],
[autocompleteSubstitutions, currentUserAccountID, onRouterClose, setTextInputValue, setShouldResetSearchQuery],
);

const onListItemPress = useCallback(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, string>;

const getSubstitutionMapKey = (filterKey: SearchFilterKey, value: string) => `${filterKey}:${value}`;

const USER_FILTER_KEYS = new Set<string>([
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.
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/libs/SearchAutocompleteUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
61 changes: 61 additions & 0 deletions tests/unit/Search/getQueryWithSubstitutionsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
26 changes: 26 additions & 0 deletions tests/unit/SearchAutocompleteUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Loading