From 1e9929d9577a8bf36f897b7cf8bf2ecd8d601c25 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Wed, 30 Oct 2024 12:04:43 +0100 Subject: [PATCH] Add autocomplete for cardIDs and correctly compute backend query --- src/CONST.ts | 2 +- src/components/Search/SearchPageHeader.tsx | 5 +- .../Search/SearchRouter/SearchRouter.tsx | 39 +++--- .../Search/SearchRouter/SearchRouterList.tsx | 6 +- src/components/Search/types.ts | 19 +-- src/libs/SearchParser/autocompleteParser.js | 117 ++++++++++-------- .../SearchParser/autocompleteParser.peggy | 1 + src/libs/SearchQueryUtils.ts | 110 +++++++++------- 8 files changed, 161 insertions(+), 138 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index f65409ef4d64..416a523141bc 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5806,7 +5806,7 @@ const CONST = { CATEGORY: 'category', TAG: 'tag', TAX_RATE: 'taxRate', - CARD_ID: 'cardID', // Fixme substitute bank id? + CARD_ID: 'cardID', REPORT_ID: 'reportID', KEYWORD: 'keyword', IN: 'in', diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 5665909185c4..a330be3d5ff6 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -340,7 +340,10 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { } const inputQueryJSON = SearchQueryUtils.buildSearchQueryJSON(inputValue); if (inputQueryJSON) { - const standardizedQuery = SearchQueryUtils.standardizeQueryJSON(inputQueryJSON, cardList, taxRates); + // Todo traverse the tree to update all the display values into id values; this is only temporary until autocomplete code from SearchRouter is implement here + // After https://github.com/Expensify/App/pull/51633 is merged, autocomplete functionality will be included into this component, and `getFindIDFromDisplayValue` can be removed + const computeNodeValueFn = SearchQueryUtils.getFindIDFromDisplayValue(cardList, taxRates); + const standardizedQuery = SearchQueryUtils.traverseAndUpdatedQuery(inputQueryJSON, computeNodeValueFn); const query = SearchQueryUtils.buildSearchQueryString(standardizedQuery); SearchActions.clearAllFilters(); Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query})); diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 47d2d08cc03a..f97b459c60b5 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -6,7 +6,7 @@ import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {usePersonalDetails} from '@components/OnyxProvider'; import {useOptionsList} from '@components/OptionListContextProvider'; -import type {AutocompleteRange, SearchQueryJSON} from '@components/Search/types'; +import type {SearchAutocompleteQueryRange, SearchQueryString} from '@components/Search/types'; import type {SelectionListHandle} from '@components/SelectionList/types'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useDebouncedState from '@hooks/useDebouncedState'; @@ -63,10 +63,6 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { return state?.routes.at(-1)?.params?.reportID; }); - const cleanQuery = useMemo(() => { - return getQueryWithSubstitutions(textInputValue, autocompleteSubstitutions); - }, [autocompleteSubstitutions, textInputValue]); - const {activeWorkspaceID} = useActiveWorkspace(); const policy = usePolicy(activeWorkspaceID); @@ -74,7 +70,7 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { const statusAutocompleteList = Object.values({...CONST.SEARCH.STATUS.TRIP, ...CONST.SEARCH.STATUS.INVOICE, ...CONST.SEARCH.STATUS.CHAT, ...CONST.SEARCH.STATUS.TRIP}); const expenseTypes = Object.values(CONST.SEARCH.TRANSACTION_TYPE); const [cardList = {}] = useOnyx(ONYXKEYS.CARD_LIST); - const cardAutocompleteList = Object.values(cardList ?? {}).map((card) => card.bank); + const cardAutocompleteList = Object.values(cardList); const personalDetailsForParticipants = usePersonalDetails(); const participantsAutocompleteList = Object.values(personalDetailsForParticipants) .filter((details): details is NonNullable => !!(details && details?.login)) @@ -155,7 +151,7 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { const contextualReportData = contextualReportID ? searchOptions.recentReports?.find((option) => option.reportID === contextualReportID) : undefined; const updateAutocomplete = useCallback( - (autocompleteValue: string, ranges: AutocompleteRange[], autocompleteType?: ValueOf) => { + (autocompleteValue: string, ranges: SearchAutocompleteQueryRange[], autocompleteType?: ValueOf) => { const alreadyAutocompletedKeys: string[] = []; ranges.forEach((range) => { if (!autocompleteType || range.key !== autocompleteType) { @@ -282,15 +278,16 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { })); break; } - // Fixme implement card autocomplete ids case CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID: { const filteredCards = cardAutocompleteList - .filter((card) => card.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.includes(card.toLowerCase())) + .filter((card) => card.bank.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.includes(card.bank.toLowerCase())) .sort() .slice(0, 10); + filteredAutocompleteSuggestions = filteredCards.map((card) => ({ filterKey: CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID, - text: card, + text: card.bank, + autocompleteID: card.cardID.toString(), })); break; } @@ -340,17 +337,23 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { ); const onSearchSubmit = useCallback( - (query: SearchQueryJSON | undefined) => { - if (!query) { + (queryString: SearchQueryString) => { + const cleanedQueryString = getQueryWithSubstitutions(queryString, autocompleteSubstitutions); + const queryJSON = SearchQueryUtils.buildSearchQueryJSON(cleanedQueryString); + if (!queryJSON) { return; } + onRouterClose(); - const standardizedQuery = SearchQueryUtils.standardizeQueryJSON(query, cardList, allTaxRates); - const queryString = SearchQueryUtils.buildSearchQueryString(standardizedQuery); - Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: queryString})); + + const computeNodeValueFn = SearchQueryUtils.getUpdatedAmountValue; + const standardizedQuery = SearchQueryUtils.traverseAndUpdatedQuery(queryJSON, computeNodeValueFn); + const query = SearchQueryUtils.buildSearchQueryString(standardizedQuery); + Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query})); + setTextInputValue(''); }, - [allTaxRates, cardList, onRouterClose, setTextInputValue], + [autocompleteSubstitutions, onRouterClose, setTextInputValue], ); const updateSubstitutionsMap = (key: string, value: string) => { @@ -365,8 +368,6 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { const modalWidth = shouldUseNarrowLayout ? styles.w100 : {width: variables.searchRouterPopoverWidth}; - // console.log('[ROUTER]', {user: textInputValue, cleanQuery, autocompleteSubstitutions}); - return ( { - onSearchSubmit(SearchQueryUtils.buildSearchQueryJSON(cleanQuery)); + onSearchSubmit(textInputValue); }} routerListRef={listRef} shouldShowOfflineMessage diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index c517626cb235..74c6aa293622 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -3,7 +3,7 @@ import type {ForwardedRef} from 'react'; import {useOnyx} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import {usePersonalDetails} from '@components/OnyxProvider'; -import type {SearchQueryJSON} from '@components/Search/types'; +import type {SearchQueryString} from '@components/Search/types'; import SelectionList from '@components/SelectionList'; import SearchQueryListItem from '@components/SelectionList/Search/SearchQueryListItem'; import type {SearchQueryItem, SearchQueryListItemProps} from '@components/SelectionList/Search/SearchQueryListItem'; @@ -56,7 +56,7 @@ type SearchRouterListProps = { autocompleteItems: AutocompleteItemData[] | undefined; /** Callback to submit query when selecting a list item */ - onSearchSubmit: (query: SearchQueryJSON | undefined) => void; + onSearchSubmit: (query: SearchQueryString) => void; /** Context present when opening SearchRouter from a report, invoice or workspace page */ reportForContextualSearch?: OptionData; @@ -213,7 +213,7 @@ function SearchRouterList( return; } - onSearchSubmit(SearchQueryUtils.buildSearchQueryJSON(item.searchQuery)); + onSearchSubmit(item.searchQuery); } // Handle selection of "Recent chat" diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 925f7fcb3e30..35d156350cd3 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -80,24 +80,16 @@ type SearchQueryJSON = { flatFilters: QueryFilters; } & SearchQueryAST; -// Fixme [Search] remove duplicate -type AutocompleteRange = { - key: ValueOf; - length: number; - start: number; - value: string; -}; - type SearchAutocompleteResult = { - autocomplete: AutocompleteRange | null; - ranges: AutocompleteRange[]; + autocomplete: SearchAutocompleteQueryRange | null; + ranges: SearchAutocompleteQueryRange[]; }; type SearchAutocompleteQueryRange = { - key: ValueOf; - value: string; - start: number; + key: ValueOf; length: number; + start: number; + value: string; }; export type { @@ -119,6 +111,5 @@ export type { TripSearchStatus, ChatSearchStatus, SearchAutocompleteResult, - AutocompleteRange, SearchAutocompleteQueryRange, }; diff --git a/src/libs/SearchParser/autocompleteParser.js b/src/libs/SearchParser/autocompleteParser.js index be57ff8a67a5..bd114b56e099 100644 --- a/src/libs/SearchParser/autocompleteParser.js +++ b/src/libs/SearchParser/autocompleteParser.js @@ -186,12 +186,13 @@ function peg$parse(input, options) { var peg$c8 = "expenseType"; var peg$c9 = "type"; var peg$c10 = "status"; - var peg$c11 = "!="; - var peg$c12 = ">="; - var peg$c13 = ">"; - var peg$c14 = "<="; - var peg$c15 = "<"; - var peg$c16 = "\""; + var peg$c11 = "cardID"; + var peg$c12 = "!="; + var peg$c13 = ">="; + var peg$c14 = ">"; + var peg$c15 = "<="; + var peg$c16 = "<"; + var peg$c17 = "\""; var peg$r0 = /^[:=]/; var peg$r1 = /^[^ ,"\t\n\r]/; @@ -211,21 +212,22 @@ function peg$parse(input, options) { var peg$e9 = peg$literalExpectation("expenseType", false); var peg$e10 = peg$literalExpectation("type", false); var peg$e11 = peg$literalExpectation("status", false); - var peg$e12 = peg$otherExpectation("operator"); - var peg$e13 = peg$classExpectation([":", "="], false, false); - var peg$e14 = peg$literalExpectation("!=", false); - var peg$e15 = peg$literalExpectation(">=", false); - var peg$e16 = peg$literalExpectation(">", false); - var peg$e17 = peg$literalExpectation("<=", false); - var peg$e18 = peg$literalExpectation("<", false); - var peg$e19 = peg$otherExpectation("quote"); - var peg$e20 = peg$classExpectation([" ", ",", "\"", "\t", "\n", "\r"], true, false); - var peg$e21 = peg$literalExpectation("\"", false); - var peg$e22 = peg$classExpectation(["\"", "\r", "\n"], true, false); - var peg$e23 = peg$classExpectation([" ", ",", "\t", "\n", "\r"], true, false); - var peg$e24 = peg$otherExpectation("word"); - var peg$e25 = peg$otherExpectation("whitespace"); - var peg$e26 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false); + var peg$e12 = peg$literalExpectation("cardID", false); + var peg$e13 = peg$otherExpectation("operator"); + var peg$e14 = peg$classExpectation([":", "="], false, false); + var peg$e15 = peg$literalExpectation("!=", false); + var peg$e16 = peg$literalExpectation(">=", false); + var peg$e17 = peg$literalExpectation(">", false); + var peg$e18 = peg$literalExpectation("<=", false); + var peg$e19 = peg$literalExpectation("<", false); + var peg$e20 = peg$otherExpectation("quote"); + var peg$e21 = peg$classExpectation([" ", ",", "\"", "\t", "\n", "\r"], true, false); + var peg$e22 = peg$literalExpectation("\"", false); + var peg$e23 = peg$classExpectation(["\"", "\r", "\n"], true, false); + var peg$e24 = peg$classExpectation([" ", ",", "\t", "\n", "\r"], true, false); + var peg$e25 = peg$otherExpectation("word"); + var peg$e26 = peg$otherExpectation("whitespace"); + var peg$e27 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false); var peg$f0 = function(ranges) { return { autocomplete, ranges }; }; var peg$f1 = function(filters) { return filters.filter(Boolean).flat(); }; @@ -644,6 +646,15 @@ function peg$parse(input, options) { s1 = peg$FAILED; if (peg$silentFails === 0) { peg$fail(peg$e11); } } + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 6) === peg$c11) { + s1 = peg$c11; + peg$currPos += 6; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e12); } + } + } } } } @@ -740,7 +751,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e13); } + if (peg$silentFails === 0) { peg$fail(peg$e14); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -749,12 +760,12 @@ function peg$parse(input, options) { s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c11) { - s1 = peg$c11; + if (input.substr(peg$currPos, 2) === peg$c12) { + s1 = peg$c12; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e14); } + if (peg$silentFails === 0) { peg$fail(peg$e15); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -763,12 +774,12 @@ function peg$parse(input, options) { s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c12) { - s1 = peg$c12; + if (input.substr(peg$currPos, 2) === peg$c13) { + s1 = peg$c13; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -778,11 +789,11 @@ function peg$parse(input, options) { if (s0 === peg$FAILED) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 62) { - s1 = peg$c13; + s1 = peg$c14; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e16); } + if (peg$silentFails === 0) { peg$fail(peg$e17); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -791,12 +802,12 @@ function peg$parse(input, options) { s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c14) { - s1 = peg$c14; + if (input.substr(peg$currPos, 2) === peg$c15) { + s1 = peg$c15; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e17); } + if (peg$silentFails === 0) { peg$fail(peg$e18); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -806,11 +817,11 @@ function peg$parse(input, options) { if (s0 === peg$FAILED) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 60) { - s1 = peg$c15; + s1 = peg$c16; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e18); } + if (peg$silentFails === 0) { peg$fail(peg$e19); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -825,7 +836,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e12); } + if (peg$silentFails === 0) { peg$fail(peg$e13); } } return s0; @@ -842,7 +853,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e20); } + if (peg$silentFails === 0) { peg$fail(peg$e21); } } while (s2 !== peg$FAILED) { s1.push(s2); @@ -851,15 +862,15 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e20); } + if (peg$silentFails === 0) { peg$fail(peg$e21); } } } if (input.charCodeAt(peg$currPos) === 34) { - s2 = peg$c16; + s2 = peg$c17; peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e21); } + if (peg$silentFails === 0) { peg$fail(peg$e22); } } if (s2 !== peg$FAILED) { s3 = []; @@ -868,7 +879,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e22); } + if (peg$silentFails === 0) { peg$fail(peg$e23); } } while (s4 !== peg$FAILED) { s3.push(s4); @@ -877,15 +888,15 @@ function peg$parse(input, options) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e22); } + if (peg$silentFails === 0) { peg$fail(peg$e23); } } } if (input.charCodeAt(peg$currPos) === 34) { - s4 = peg$c16; + s4 = peg$c17; peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e21); } + if (peg$silentFails === 0) { peg$fail(peg$e22); } } if (s4 !== peg$FAILED) { s5 = []; @@ -894,7 +905,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e23); } + if (peg$silentFails === 0) { peg$fail(peg$e24); } } while (s6 !== peg$FAILED) { s5.push(s6); @@ -903,7 +914,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e23); } + if (peg$silentFails === 0) { peg$fail(peg$e24); } } } peg$savedPos = s0; @@ -919,7 +930,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e19); } + if (peg$silentFails === 0) { peg$fail(peg$e20); } } return s0; @@ -936,7 +947,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e23); } + if (peg$silentFails === 0) { peg$fail(peg$e24); } } if (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) { @@ -946,7 +957,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e23); } + if (peg$silentFails === 0) { peg$fail(peg$e24); } } } } else { @@ -960,7 +971,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e24); } + if (peg$silentFails === 0) { peg$fail(peg$e25); } } return s0; @@ -988,7 +999,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e26); } + if (peg$silentFails === 0) { peg$fail(peg$e27); } } while (s1 !== peg$FAILED) { s0.push(s1); @@ -997,12 +1008,12 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e26); } + if (peg$silentFails === 0) { peg$fail(peg$e27); } } } peg$silentFails--; s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e25); } + if (peg$silentFails === 0) { peg$fail(peg$e26); } return s0; } diff --git a/src/libs/SearchParser/autocompleteParser.peggy b/src/libs/SearchParser/autocompleteParser.peggy index 89d89fd07cd4..e2a8bed9a9cc 100644 --- a/src/libs/SearchParser/autocompleteParser.peggy +++ b/src/libs/SearchParser/autocompleteParser.peggy @@ -61,6 +61,7 @@ autocompleteKey "key" / "expenseType" / "type" / "status" + / "cardID" ) identifier diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index c84e42704fb9..731639942a5a 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -125,11 +125,11 @@ function getFilters(queryJSON: SearchQueryJSON) { return; } - if (typeof node?.left === 'object' && node.left) { + if (typeof node.left === 'object' && node.left) { traverse(node.left); } - if (typeof node?.right === 'object' && node.right && !Array.isArray(node.right)) { + if (typeof node.right === 'object' && node.right && !Array.isArray(node.right)) { traverse(node.right); } @@ -148,7 +148,7 @@ function getFilters(queryJSON: SearchQueryJSON) { node.right.forEach((element) => { filterArray.push({ operator: node.operator, - value: element as string | number, + value: element, }); }); } @@ -163,52 +163,66 @@ function getFilters(queryJSON: SearchQueryJSON) { } /** - * @private * Given a filter name and its value, this function returns the corresponding ID found in Onyx data. + * Returns a function that can be used as a computeNodeValue callback for traversing the filters tree */ -function findIDFromDisplayValue(filterName: ValueOf, filter: string | string[], cardList: OnyxTypes.CardList, taxRates: Record) { - if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) { - if (typeof filter === 'string') { - const email = filter; - return PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? filter; +function getFindIDFromDisplayValue(cardList: OnyxTypes.CardList, taxRates: Record) { + return (filterName: ValueOf, filter: string | string[]) => { + if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) { + if (typeof filter === 'string') { + const email = filter; + return PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? filter; + } + const emails = filter; + return emails.map((email) => PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? email); } - const emails = filter; - return emails.map((email) => PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? email); - } - if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) { - const names = Array.isArray(filter) ? filter : ([filter] as string[]); - return names.map((name) => taxRates[name] ?? name).flat(); - } - if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID) { - if (typeof filter === 'string') { - const bank = filter; - const ids = - Object.values(cardList) - .filter((card) => card.bank === bank) - .map((card) => card.cardID.toString()) ?? filter; - return ids.length > 0 ? ids : bank; + if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) { + const names = Array.isArray(filter) ? filter : ([filter] as string[]); + return names.map((name) => taxRates[name] ?? name).flat(); } - const banks = filter; - return banks - .map( - (bank) => + if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID) { + if (typeof filter === 'string') { + const bank = filter; + const ids = Object.values(cardList) .filter((card) => card.bank === bank) - .map((card) => card.cardID.toString()) ?? bank, - ) - .flat(); - } - if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT) { - if (typeof filter === 'string') { - const backendAmount = CurrencyUtils.convertToBackendAmount(Number(filter)); - return Number.isNaN(backendAmount) ? filter : backendAmount.toString(); + .map((card) => card.cardID.toString()) ?? filter; + return ids.length > 0 ? ids : bank; + } + const banks = filter; + return banks + .map( + (bank) => + Object.values(cardList) + .filter((card) => card.bank === bank) + .map((card) => card.cardID.toString()) ?? bank, + ) + .flat(); + } + if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT) { + return getUpdatedAmountValue(filterName, filter); } - return filter.map((amount) => { - const backendAmount = CurrencyUtils.convertToBackendAmount(Number(amount)); - return Number.isNaN(backendAmount) ? amount : backendAmount.toString(); - }); + + return filter; + }; +} + +/** + * Returns an updated amount value for query filters, correctly formatted to "backend" amount + */ +function getUpdatedAmountValue(filterName: ValueOf, filter: string | string[]) { + if (filterName !== CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT) { + return filter; } - return filter; + + if (typeof filter === 'string') { + const backendAmount = CurrencyUtils.convertToBackendAmount(Number(filter)); + return Number.isNaN(backendAmount) ? filter : backendAmount.toString(); + } + return filter.map((amount) => { + const backendAmount = CurrencyUtils.convertToBackendAmount(Number(amount)); + return Number.isNaN(backendAmount) ? amount : backendAmount.toString(); + }); } /** @@ -604,23 +618,23 @@ function isCannedSearchQuery(queryJSON: SearchQueryJSON) { /** * Given a search query, this function will standardize the query by replacing display values with their corresponding IDs. */ -function standardizeQueryJSON(queryJSON: SearchQueryJSON, cardList: OnyxTypes.CardList, taxRates: Record) { +function traverseAndUpdatedQuery(queryJSON: SearchQueryJSON, computeNodeValue: (left: ValueOf, right: string | string[]) => string | string[]) { const standardQuery = cloneDeep(queryJSON); const filters = standardQuery.filters; const traverse = (node: ASTNode) => { if (!node.operator) { return; } - if (typeof node.left === 'object' && node.left) { + if (typeof node.left === 'object') { traverse(node.left); } - if (typeof node.right === 'object' && node.right && !Array.isArray(node.right)) { + if (typeof node.right === 'object' && !Array.isArray(node.right)) { traverse(node.right); } - if (typeof node.left !== 'object') { + if (typeof node.left !== 'object' && (Array.isArray(node.right) || typeof node.right === 'string')) { // eslint-disable-next-line no-param-reassign - node.right = findIDFromDisplayValue(node.left, node.right as string | string[], cardList, taxRates); + node.right = computeNodeValue(node.left, node.right); } }; @@ -641,6 +655,8 @@ export { getPolicyIDFromSearchQuery, buildCannedSearchQuery, isCannedSearchQuery, - standardizeQueryJSON, + traverseAndUpdatedQuery, + getFindIDFromDisplayValue, + getUpdatedAmountValue, sanitizeSearchValue, };