Skip to content

Commit 04544cf

Browse files
authored
Merge pull request #53453 from software-mansion-labs/kicu/53036-email-autocomplete
Make autocomplete accountIDs work when pasting user emails
2 parents cefad67 + f38005a commit 04544cf

File tree

4 files changed

+122
-41
lines changed

4 files changed

+122
-41
lines changed

src/components/Search/SearchPageHeaderInput.tsx

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import Text from '@components/Text';
1313
import useLocalize from '@hooks/useLocalize';
1414
import useThemeStyles from '@hooks/useThemeStyles';
1515
import * as SearchActions from '@libs/actions/Search';
16-
import Log from '@libs/Log';
1716
import Navigation from '@libs/Navigation/Navigation';
1817
import {getAllTaxRates} from '@libs/PolicyUtils';
1918
import type {OptionData} from '@libs/ReportUtils';
@@ -112,28 +111,15 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps
112111

113112
const submitSearch = useCallback(
114113
(queryString: SearchQueryString) => {
115-
if (!queryString) {
114+
const queryWithSubstitutions = getQueryWithSubstitutions(queryString, autocompleteSubstitutions);
115+
const updatedQuery = SearchQueryUtils.getQueryWithUpdatedValues(queryWithSubstitutions, queryJSON.policyID);
116+
if (!updatedQuery) {
116117
return;
117118
}
118119

119-
const cleanedQueryString = getQueryWithSubstitutions(queryString, autocompleteSubstitutions);
120-
const userQueryJSON = SearchQueryUtils.buildSearchQueryJSON(cleanedQueryString);
120+
Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: updatedQuery}));
121121

122-
if (!userQueryJSON) {
123-
Log.alert(`${CONST.ERROR.ENSURE_BUGBOT} user query failed to parse`, {}, false);
124-
return;
125-
}
126-
127-
if (queryJSON.policyID) {
128-
userQueryJSON.policyID = queryJSON.policyID;
129-
}
130-
131-
const standardizedQuery = SearchQueryUtils.traverseAndUpdatedQuery(userQueryJSON, SearchQueryUtils.getUpdatedAmountValue);
132-
const query = SearchQueryUtils.buildSearchQueryString(standardizedQuery);
133-
134-
Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query}));
135-
136-
if (query !== originalInputQuery) {
122+
if (updatedQuery !== originalInputQuery) {
137123
SearchActions.clearAllFilters();
138124
setTextInputValue('');
139125
setAutocompleteQueryValue('');

src/components/Search/SearchRouter/SearchRouter.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -250,17 +250,14 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps)
250250

251251
const submitSearch = useCallback(
252252
(queryString: SearchQueryString) => {
253-
const cleanedQueryString = getQueryWithSubstitutions(queryString, autocompleteSubstitutions);
254-
const queryJSON = SearchQueryUtils.buildSearchQueryJSON(cleanedQueryString);
255-
if (!queryJSON) {
253+
const queryWithSubstitutions = getQueryWithSubstitutions(queryString, autocompleteSubstitutions);
254+
const updatedQuery = SearchQueryUtils.getQueryWithUpdatedValues(queryWithSubstitutions, activeWorkspaceID);
255+
if (!updatedQuery) {
256256
return;
257257
}
258-
queryJSON.policyID = activeWorkspaceID;
259-
onRouterClose();
260258

261-
const standardizedQuery = SearchQueryUtils.traverseAndUpdatedQuery(queryJSON, SearchQueryUtils.getUpdatedAmountValue);
262-
const query = SearchQueryUtils.buildSearchQueryString(standardizedQuery);
263-
Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query}));
259+
onRouterClose();
260+
Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: updatedQuery}));
264261

265262
setTextInputValue('');
266263
setAutocompleteQueryValue('');

src/libs/SearchQueryUtils.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
1111
import * as CardUtils from './CardUtils';
1212
import * as CurrencyUtils from './CurrencyUtils';
1313
import localeCompare from './LocaleCompare';
14+
import Log from './Log';
1415
import {validateAmount} from './MoneyRequestUtils';
16+
import * as PersonalDetailsUtils from './PersonalDetailsUtils';
1517
import {getTagNamesFromTagsLists} from './PolicyUtils';
1618
import * as ReportUtils from './ReportUtils';
1719
import * as searchParser from './SearchParser/searchParser';
@@ -163,21 +165,32 @@ function getFilters(queryJSON: SearchQueryJSON) {
163165
}
164166

165167
/**
166-
* Returns an updated amount value for query filters, correctly formatted to "backend" amount
168+
* @private
169+
* Returns an updated filter value for some query filters.
170+
* - for `AMOUNT` it formats value to "backend" amount
171+
* - for personal filters it tries to substitute any user emails with accountIDs
167172
*/
168-
function getUpdatedAmountValue(filterName: ValueOf<typeof CONST.SEARCH.SYNTAX_FILTER_KEYS>, filter: string | string[]) {
169-
if (filterName !== CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT) {
170-
return filter;
173+
function getUpdatedFilterValue(filterName: ValueOf<typeof CONST.SEARCH.SYNTAX_FILTER_KEYS>, filterValue: string | string[]) {
174+
if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) {
175+
if (typeof filterValue === 'string') {
176+
return PersonalDetailsUtils.getPersonalDetailByEmail(filterValue)?.accountID.toString() ?? filterValue;
177+
}
178+
179+
return filterValue.map((email) => PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? email);
171180
}
172181

173-
if (typeof filter === 'string') {
174-
const backendAmount = CurrencyUtils.convertToBackendAmount(Number(filter));
175-
return Number.isNaN(backendAmount) ? filter : backendAmount.toString();
182+
if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT) {
183+
if (typeof filterValue === 'string') {
184+
const backendAmount = CurrencyUtils.convertToBackendAmount(Number(filterValue));
185+
return Number.isNaN(backendAmount) ? filterValue : backendAmount.toString();
186+
}
187+
return filterValue.map((amount) => {
188+
const backendAmount = CurrencyUtils.convertToBackendAmount(Number(amount));
189+
return Number.isNaN(backendAmount) ? amount : backendAmount.toString();
190+
});
176191
}
177-
return filter.map((amount) => {
178-
const backendAmount = CurrencyUtils.convertToBackendAmount(Number(amount));
179-
return Number.isNaN(backendAmount) ? amount : backendAmount.toString();
180-
});
192+
193+
return filterValue;
181194
}
182195

183196
/**
@@ -266,7 +279,7 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
266279

267280
for (const filter of filters) {
268281
const filterValueString = buildFilterValuesString(filter.key, filter.filters);
269-
queryParts.push(filterValueString);
282+
queryParts.push(filterValueString.trim());
270283
}
271284

272285
return queryParts.join(' ');
@@ -625,6 +638,26 @@ function traverseAndUpdatedQuery(queryJSON: SearchQueryJSON, computeNodeValue: (
625638
return standardQuery;
626639
}
627640

641+
/**
642+
* Returns new string query, after parsing it and traversing to update some filter values.
643+
* If there are any personal emails, it will try to substitute them with accountIDs
644+
*/
645+
function getQueryWithUpdatedValues(query: string, policyID?: string) {
646+
const queryJSON = buildSearchQueryJSON(query);
647+
648+
if (!queryJSON) {
649+
Log.alert(`${CONST.ERROR.ENSURE_BUGBOT} user query failed to parse`, {}, false);
650+
return;
651+
}
652+
653+
if (policyID) {
654+
queryJSON.policyID = policyID;
655+
}
656+
657+
const standardizedQuery = traverseAndUpdatedQuery(queryJSON, getUpdatedFilterValue);
658+
return buildSearchQueryString(standardizedQuery);
659+
}
660+
628661
export {
629662
buildSearchQueryJSON,
630663
buildSearchQueryString,
@@ -635,7 +668,6 @@ export {
635668
getPolicyIDFromSearchQuery,
636669
buildCannedSearchQuery,
637670
isCannedSearchQuery,
638-
traverseAndUpdatedQuery,
639-
getUpdatedAmountValue,
640671
sanitizeSearchValue,
672+
getQueryWithUpdatedValues,
641673
};
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
2+
// we need "dirty" object key names in these tests
3+
import {getQueryWithUpdatedValues} from '@src/libs/SearchQueryUtils';
4+
5+
const personalDetailsFakeData = {
6+
7+
accountID: 12345,
8+
},
9+
10+
accountID: 78901,
11+
},
12+
} as Record<string, {accountID: number}>;
13+
14+
jest.mock('@libs/PersonalDetailsUtils', () => {
15+
return {
16+
getPersonalDetailByEmail(email: string) {
17+
return personalDetailsFakeData[email];
18+
},
19+
};
20+
});
21+
22+
// The default query is generated by default values from parser, which are defined in grammar.
23+
// We don't want to test or mock the grammar and the parser, so we're simply defining this string directly here.
24+
const defaultQuery = `type:expense status:all sortBy:date sortOrder:desc`;
25+
26+
describe('getQueryWithUpdatedValues', () => {
27+
test('returns default query for empty value', () => {
28+
const userQuery = '';
29+
30+
const result = getQueryWithUpdatedValues(userQuery);
31+
32+
expect(result).toEqual(defaultQuery);
33+
});
34+
35+
test('returns query with updated amounts', () => {
36+
const userQuery = 'foo test amount:20000';
37+
38+
const result = getQueryWithUpdatedValues(userQuery);
39+
40+
expect(result).toEqual(`${defaultQuery} amount:2000000 foo test`);
41+
});
42+
43+
test('returns query with user emails substituted', () => {
44+
const userQuery = 'from:[email protected] hello';
45+
46+
const result = getQueryWithUpdatedValues(userQuery);
47+
48+
expect(result).toEqual(`${defaultQuery} from:12345 hello`);
49+
});
50+
51+
test('returns query with user emails substituted and preserves user ids', () => {
52+
const userQuery = 'from:[email protected] to:112233';
53+
54+
const result = getQueryWithUpdatedValues(userQuery);
55+
56+
expect(result).toEqual(`${defaultQuery} from:12345 to:112233`);
57+
});
58+
59+
test('returns query with all of the fields correctly substituted', () => {
60+
const userQuery = 'from:9876,87654 to:[email protected] hello amount:150 test';
61+
62+
const result = getQueryWithUpdatedValues(userQuery);
63+
64+
expect(result).toEqual(`${defaultQuery} from:9876,87654 to:78901 amount:15000 hello test`);
65+
});
66+
});

0 commit comments

Comments
 (0)