Skip to content

Commit

Permalink
Merge pull request #53453 from software-mansion-labs/kicu/53036-email…
Browse files Browse the repository at this point in the history
…-autocomplete

Make autocomplete accountIDs work when pasting user emails
  • Loading branch information
luacmartins authored Dec 4, 2024
2 parents cefad67 + f38005a commit 04544cf
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 41 deletions.
24 changes: 5 additions & 19 deletions src/components/Search/SearchPageHeaderInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as SearchActions from '@libs/actions/Search';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import {getAllTaxRates} from '@libs/PolicyUtils';
import type {OptionData} from '@libs/ReportUtils';
Expand Down Expand Up @@ -112,28 +111,15 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps

const submitSearch = useCallback(
(queryString: SearchQueryString) => {
if (!queryString) {
const queryWithSubstitutions = getQueryWithSubstitutions(queryString, autocompleteSubstitutions);
const updatedQuery = SearchQueryUtils.getQueryWithUpdatedValues(queryWithSubstitutions, queryJSON.policyID);
if (!updatedQuery) {
return;
}

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

if (!userQueryJSON) {
Log.alert(`${CONST.ERROR.ENSURE_BUGBOT} user query failed to parse`, {}, false);
return;
}

if (queryJSON.policyID) {
userQueryJSON.policyID = queryJSON.policyID;
}

const standardizedQuery = SearchQueryUtils.traverseAndUpdatedQuery(userQueryJSON, SearchQueryUtils.getUpdatedAmountValue);
const query = SearchQueryUtils.buildSearchQueryString(standardizedQuery);

Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query}));

if (query !== originalInputQuery) {
if (updatedQuery !== originalInputQuery) {
SearchActions.clearAllFilters();
setTextInputValue('');
setAutocompleteQueryValue('');
Expand Down
13 changes: 5 additions & 8 deletions src/components/Search/SearchRouter/SearchRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,17 +250,14 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps)

const submitSearch = useCallback(
(queryString: SearchQueryString) => {
const cleanedQueryString = getQueryWithSubstitutions(queryString, autocompleteSubstitutions);
const queryJSON = SearchQueryUtils.buildSearchQueryJSON(cleanedQueryString);
if (!queryJSON) {
const queryWithSubstitutions = getQueryWithSubstitutions(queryString, autocompleteSubstitutions);
const updatedQuery = SearchQueryUtils.getQueryWithUpdatedValues(queryWithSubstitutions, activeWorkspaceID);
if (!updatedQuery) {
return;
}
queryJSON.policyID = activeWorkspaceID;
onRouterClose();

const standardizedQuery = SearchQueryUtils.traverseAndUpdatedQuery(queryJSON, SearchQueryUtils.getUpdatedAmountValue);
const query = SearchQueryUtils.buildSearchQueryString(standardizedQuery);
Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query}));
onRouterClose();
Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: updatedQuery}));

setTextInputValue('');
setAutocompleteQueryValue('');
Expand Down
60 changes: 46 additions & 14 deletions src/libs/SearchQueryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
import * as CardUtils from './CardUtils';
import * as CurrencyUtils from './CurrencyUtils';
import localeCompare from './LocaleCompare';
import Log from './Log';
import {validateAmount} from './MoneyRequestUtils';
import * as PersonalDetailsUtils from './PersonalDetailsUtils';
import {getTagNamesFromTagsLists} from './PolicyUtils';
import * as ReportUtils from './ReportUtils';
import * as searchParser from './SearchParser/searchParser';
Expand Down Expand Up @@ -163,21 +165,32 @@ function getFilters(queryJSON: SearchQueryJSON) {
}

/**
* Returns an updated amount value for query filters, correctly formatted to "backend" amount
* @private
* Returns an updated filter value for some query filters.
* - for `AMOUNT` it formats value to "backend" amount
* - for personal filters it tries to substitute any user emails with accountIDs
*/
function getUpdatedAmountValue(filterName: ValueOf<typeof CONST.SEARCH.SYNTAX_FILTER_KEYS>, filter: string | string[]) {
if (filterName !== CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT) {
return filter;
function getUpdatedFilterValue(filterName: ValueOf<typeof CONST.SEARCH.SYNTAX_FILTER_KEYS>, filterValue: string | string[]) {
if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) {
if (typeof filterValue === 'string') {
return PersonalDetailsUtils.getPersonalDetailByEmail(filterValue)?.accountID.toString() ?? filterValue;
}

return filterValue.map((email) => PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? email);
}

if (typeof filter === 'string') {
const backendAmount = CurrencyUtils.convertToBackendAmount(Number(filter));
return Number.isNaN(backendAmount) ? filter : backendAmount.toString();
if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT) {
if (typeof filterValue === 'string') {
const backendAmount = CurrencyUtils.convertToBackendAmount(Number(filterValue));
return Number.isNaN(backendAmount) ? filterValue : backendAmount.toString();
}
return filterValue.map((amount) => {
const backendAmount = CurrencyUtils.convertToBackendAmount(Number(amount));
return Number.isNaN(backendAmount) ? amount : backendAmount.toString();
});
}
return filter.map((amount) => {
const backendAmount = CurrencyUtils.convertToBackendAmount(Number(amount));
return Number.isNaN(backendAmount) ? amount : backendAmount.toString();
});

return filterValue;
}

/**
Expand Down Expand Up @@ -266,7 +279,7 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) {

for (const filter of filters) {
const filterValueString = buildFilterValuesString(filter.key, filter.filters);
queryParts.push(filterValueString);
queryParts.push(filterValueString.trim());
}

return queryParts.join(' ');
Expand Down Expand Up @@ -625,6 +638,26 @@ function traverseAndUpdatedQuery(queryJSON: SearchQueryJSON, computeNodeValue: (
return standardQuery;
}

/**
* Returns new string query, after parsing it and traversing to update some filter values.
* If there are any personal emails, it will try to substitute them with accountIDs
*/
function getQueryWithUpdatedValues(query: string, policyID?: string) {
const queryJSON = buildSearchQueryJSON(query);

if (!queryJSON) {
Log.alert(`${CONST.ERROR.ENSURE_BUGBOT} user query failed to parse`, {}, false);
return;
}

if (policyID) {
queryJSON.policyID = policyID;
}

const standardizedQuery = traverseAndUpdatedQuery(queryJSON, getUpdatedFilterValue);
return buildSearchQueryString(standardizedQuery);
}

export {
buildSearchQueryJSON,
buildSearchQueryString,
Expand All @@ -635,7 +668,6 @@ export {
getPolicyIDFromSearchQuery,
buildCannedSearchQuery,
isCannedSearchQuery,
traverseAndUpdatedQuery,
getUpdatedAmountValue,
sanitizeSearchValue,
getQueryWithUpdatedValues,
};
66 changes: 66 additions & 0 deletions tests/unit/Search/SearchQueryUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* eslint-disable @typescript-eslint/naming-convention */
// we need "dirty" object key names in these tests
import {getQueryWithUpdatedValues} from '@src/libs/SearchQueryUtils';

const personalDetailsFakeData = {
'[email protected]': {
accountID: 12345,
},
'[email protected]': {
accountID: 78901,
},
} as Record<string, {accountID: number}>;

jest.mock('@libs/PersonalDetailsUtils', () => {
return {
getPersonalDetailByEmail(email: string) {
return personalDetailsFakeData[email];
},
};
});

// The default query is generated by default values from parser, which are defined in grammar.
// We don't want to test or mock the grammar and the parser, so we're simply defining this string directly here.
const defaultQuery = `type:expense status:all sortBy:date sortOrder:desc`;

describe('getQueryWithUpdatedValues', () => {
test('returns default query for empty value', () => {
const userQuery = '';

const result = getQueryWithUpdatedValues(userQuery);

expect(result).toEqual(defaultQuery);
});

test('returns query with updated amounts', () => {
const userQuery = 'foo test amount:20000';

const result = getQueryWithUpdatedValues(userQuery);

expect(result).toEqual(`${defaultQuery} amount:2000000 foo test`);
});

test('returns query with user emails substituted', () => {
const userQuery = 'from:[email protected] hello';

const result = getQueryWithUpdatedValues(userQuery);

expect(result).toEqual(`${defaultQuery} from:12345 hello`);
});

test('returns query with user emails substituted and preserves user ids', () => {
const userQuery = 'from:[email protected] to:112233';

const result = getQueryWithUpdatedValues(userQuery);

expect(result).toEqual(`${defaultQuery} from:12345 to:112233`);
});

test('returns query with all of the fields correctly substituted', () => {
const userQuery = 'from:9876,87654 to:[email protected] hello amount:150 test';

const result = getQueryWithUpdatedValues(userQuery);

expect(result).toEqual(`${defaultQuery} from:9876,87654 to:78901 amount:15000 hello test`);
});
});

0 comments on commit 04544cf

Please sign in to comment.