Skip to content

Commit 0bd6464

Browse files
committed
Cleanup Search Utils and add jsdoc for all methods
1 parent ea1271e commit 0bd6464

File tree

5 files changed

+247
-124
lines changed

5 files changed

+247
-124
lines changed

src/components/Search/SearchPageHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) {
137137

138138
const {status, type} = queryJSON;
139139
const isCannedQuery = SearchQueryUtils.isCannedSearchQuery(queryJSON);
140-
const headerText = isCannedQuery ? translate(getHeaderContent(type).titleText) : SearchQueryUtils.getSearchHeaderTitle(queryJSON, personalDetails, cardList, reports, taxRates);
140+
const headerText = isCannedQuery ? translate(getHeaderContent(type).titleText) : SearchQueryUtils.buildUserReadableQueryString(queryJSON, personalDetails, cardList, reports, taxRates);
141141
const [inputValue, setInputValue] = useState(headerText);
142142

143143
useEffect(() => {

src/components/Search/SearchRouter/SearchRouterList.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ const setPerformanceTimersEnd = () => {
5555
Performance.markEnd(CONST.TIMING.SEARCH_ROUTER_RENDER);
5656
};
5757

58+
function getContextualSearchQuery(reportID: string) {
59+
return `${CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE}:${CONST.SEARCH.DATA_TYPES.CHAT} in:${reportID}`;
60+
}
61+
5862
function isSearchQueryItem(item: OptionData | SearchQueryItem): item is SearchQueryItem {
5963
if ('singleIcon' in item && item.singleIcon && 'query' in item && item.query) {
6064
return true;
@@ -120,7 +124,7 @@ function SearchRouterList(
120124
{
121125
text: `${translate('search.searchIn')} ${reportForContextualSearch.text ?? reportForContextualSearch.alternateText}`,
122126
singleIcon: Expensicons.MagnifyingGlass,
123-
query: SearchQueryUtils.getContextualSuggestionQuery(reportForContextualSearch.reportID),
127+
query: getContextualSearchQuery(reportForContextualSearch.reportID),
124128
itemStyle: styles.activeComponentBG,
125129
keyForList: 'contextualSearch',
126130
isContextualSearchItem: true,
@@ -132,7 +136,7 @@ function SearchRouterList(
132136
const recentSearchesData = recentSearches?.map(({query}) => {
133137
const searchQueryJSON = SearchQueryUtils.buildSearchQueryJSON(query);
134138
return {
135-
text: searchQueryJSON ? SearchQueryUtils.getSearchHeaderTitle(searchQueryJSON, personalDetails, cardList, reports, taxRates) : query,
139+
text: searchQueryJSON ? SearchQueryUtils.buildUserReadableQueryString(searchQueryJSON, personalDetails, cardList, reports, taxRates) : query,
136140
singleIcon: Expensicons.History,
137141
query,
138142
keyForList: query,

src/libs/SearchQueryUtils.ts

Lines changed: 126 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,19 @@ const operatorToCharMap = {
3232

3333
/**
3434
* @private
35-
* returns Date filter query string part, which needs special logic
35+
* Returns string value wrapped in quotes "", if the value contains special characters.
36+
*/
37+
function sanitizeSearchValue(str: string) {
38+
const regexp = /[^A-Za-z0-9_@./#&+\-\\';,"]/g;
39+
if (regexp.test(str)) {
40+
return `"${str}"`;
41+
}
42+
return str;
43+
}
44+
45+
/**
46+
* @private
47+
* Returns date filter value for QueryString.
3648
*/
3749
function buildDateFilterQuery(filterValues: Partial<SearchAdvancedFiltersForm>) {
3850
const dateBefore = filterValues[FILTER_KEYS.DATE_BEFORE];
@@ -54,7 +66,7 @@ function buildDateFilterQuery(filterValues: Partial<SearchAdvancedFiltersForm>)
5466

5567
/**
5668
* @private
57-
* returns Date filter query string part, which needs special logic
69+
* Returns amount filter value for QueryString.
5870
*/
5971
function buildAmountFilterQuery(filterValues: Partial<SearchAdvancedFiltersForm>) {
6072
const lessThan = filterValues[FILTER_KEYS.LESS_THAN];
@@ -74,17 +86,33 @@ function buildAmountFilterQuery(filterValues: Partial<SearchAdvancedFiltersForm>
7486
return amountFilter;
7587
}
7688

77-
function sanitizeString(str: string) {
78-
const regexp = /[^A-Za-z0-9_@./#&+\-\\';,"]/g;
79-
if (regexp.test(str)) {
80-
return `"${str}"`;
81-
}
82-
return str;
89+
/**
90+
* @private
91+
* Returns string of correctly formatted filter values from QueryFilters object.
92+
*/
93+
function buildFilterValuesString(filterName: string, queryFilters: QueryFilter[]) {
94+
const delimiter = filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD ? ' ' : ',';
95+
let filterValueString = '';
96+
queryFilters.forEach((queryFilter, index) => {
97+
// If the previous queryFilter has the same operator (this rule applies only to eq and neq operators) then append the current value
98+
if (
99+
index !== 0 &&
100+
((queryFilter.operator === 'eq' && queryFilters?.at(index - 1)?.operator === 'eq') || (queryFilter.operator === 'neq' && queryFilters.at(index - 1)?.operator === 'neq'))
101+
) {
102+
filterValueString += `${delimiter}${sanitizeSearchValue(queryFilter.value.toString())}`;
103+
} else if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD) {
104+
filterValueString += `${delimiter}${sanitizeSearchValue(queryFilter.value.toString())}`;
105+
} else {
106+
filterValueString += ` ${filterName}${operatorToCharMap[queryFilter.operator]}${sanitizeSearchValue(queryFilter.value.toString())}`;
107+
}
108+
});
109+
110+
return filterValueString;
83111
}
84112

85113
/**
86114
* @private
87-
* traverses the AST and returns filters as a QueryFilters object
115+
* Traverses the AST and returns filters as a QueryFilters object.
88116
*/
89117
function getFilters(queryJSON: SearchQueryJSON) {
90118
const filters = {} as QueryFilters;
@@ -136,6 +164,51 @@ function getFilters(queryJSON: SearchQueryJSON) {
136164
return filters;
137165
}
138166

167+
/**
168+
* @private
169+
* Given a filter name and its value, this function returns the corresponding ID found in Onyx data.
170+
*/
171+
function findIDFromDisplayValue(filterName: ValueOf<typeof CONST.SEARCH.SYNTAX_FILTER_KEYS>, filter: string | string[], cardList: OnyxTypes.CardList, taxRates: Record<string, string[]>) {
172+
if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) {
173+
if (typeof filter === 'string') {
174+
const email = filter;
175+
return PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? filter;
176+
}
177+
const emails = filter;
178+
return emails.map((email) => PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? email);
179+
}
180+
if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) {
181+
const names = Array.isArray(filter) ? filter : ([filter] as string[]);
182+
return names.map((name) => taxRates[name] ?? name).flat();
183+
}
184+
if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID) {
185+
if (typeof filter === 'string') {
186+
const bank = filter;
187+
const ids =
188+
Object.values(cardList)
189+
.filter((card) => card.bank === bank)
190+
.map((card) => card.cardID.toString()) ?? filter;
191+
return ids.length > 0 ? ids : bank;
192+
}
193+
const banks = filter;
194+
return banks
195+
.map(
196+
(bank) =>
197+
Object.values(cardList)
198+
.filter((card) => card.bank === bank)
199+
.map((card) => card.cardID.toString()) ?? bank,
200+
)
201+
.flat();
202+
}
203+
return filter;
204+
}
205+
206+
/**
207+
* Parses a given search query string into a structured `SearchQueryJSON` format.
208+
* This format of query is most commonly shared between components and also sent to backend to retrieve search results.
209+
*
210+
* In a way this is the reverse of buildSearchQueryString()
211+
*/
139212
function buildSearchQueryJSON(query: SearchQueryString) {
140213
try {
141214
const result = searchParser.parse(query) as SearchQueryJSON;
@@ -154,6 +227,12 @@ function buildSearchQueryJSON(query: SearchQueryString) {
154227
}
155228
}
156229

230+
/**
231+
* Formats a given `SearchQueryJSON` object into the string version of query.
232+
* This format of query is the most basic string format and is used as the query param `q` in search URLs.
233+
*
234+
* In a way this is the reverse of buildSearchQueryJSON()
235+
*/
157236
function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
158237
const queryParts: string[] = [];
159238
const defaultQueryJSON = buildSearchQueryJSON('');
@@ -177,7 +256,7 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
177256
const queryFilter = filters[filterKey];
178257

179258
if (queryFilter) {
180-
const filterValueString = buildFilterString(filterKey, queryFilter);
259+
const filterValueString = buildFilterValuesString(filterKey, queryFilter);
181260
queryParts.push(filterValueString);
182261
}
183262
}
@@ -186,25 +265,28 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
186265
}
187266

188267
/**
189-
* Given object with chosen search filters builds correct query string from them
268+
* Formats a given object with search filter values into the string version of the query.
269+
* Main usage is to consume data format that comes from AdvancedFilters Onyx Form Data, and generate query string.
270+
*
271+
* Reverse operation of buildFilterFormValuesFromQuery()
190272
*/
191273
function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvancedFiltersForm>) {
192274
// We separate type and status filters from other filters to maintain hashes consistency for saved searches
193275
const {type, status, policyID, ...otherFilters} = filterValues;
194276
const filtersString: string[] = [];
195277

196278
if (type) {
197-
const sanitizedType = sanitizeString(type);
279+
const sanitizedType = sanitizeSearchValue(type);
198280
filtersString.push(`${CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE}:${sanitizedType}`);
199281
}
200282

201283
if (status) {
202-
const sanitizedStatus = sanitizeString(status);
284+
const sanitizedStatus = sanitizeSearchValue(status);
203285
filtersString.push(`${CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS}:${sanitizedStatus}`);
204286
}
205287

206288
if (policyID) {
207-
const sanitizedPolicyID = sanitizeString(policyID);
289+
const sanitizedPolicyID = sanitizeSearchValue(policyID);
208290
filtersString.push(`${CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID}:${sanitizedPolicyID}`);
209291
}
210292

@@ -213,12 +295,12 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc
213295
if ((filterKey === FILTER_KEYS.MERCHANT || filterKey === FILTER_KEYS.DESCRIPTION || filterKey === FILTER_KEYS.REPORT_ID) && filterValue) {
214296
const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as FilterKeys[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey);
215297
if (keyInCorrectForm) {
216-
return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${sanitizeString(filterValue as string)}`;
298+
return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${sanitizeSearchValue(filterValue as string)}`;
217299
}
218300
}
219301

220302
if (filterKey === FILTER_KEYS.KEYWORD && filterValue) {
221-
const value = (filterValue as string).split(' ').map(sanitizeString).join(' ');
303+
const value = (filterValue as string).split(' ').map(sanitizeSearchValue).join(' ');
222304
return `${value}`;
223305
}
224306

@@ -238,7 +320,7 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc
238320
const filterValueArray = [...new Set<string>(filterValue)];
239321
const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as FilterKeys[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey);
240322
if (keyInCorrectForm) {
241-
return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValueArray.map(sanitizeString).join(',')}`;
323+
return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValueArray.map(sanitizeSearchValue).join(',')}`;
242324
}
243325
}
244326

@@ -258,7 +340,10 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc
258340
}
259341

260342
/**
261-
* returns the values of the filters in a format that can be used in the SearchAdvancedFiltersForm as initial form values
343+
* Generates object with search filter values, in a format that can be consumed by SearchAdvancedFiltersForm.
344+
* Main usage of this is to generate the initial values for AdvancedFilters from existing query.
345+
*
346+
* Reverse operation of buildQueryStringFromFilterFormValues()
262347
*/
263348
function buildFilterFormValuesFromQuery(
264349
queryJSON: SearchQueryJSON,
@@ -368,6 +453,10 @@ function getPolicyIDFromSearchQuery(queryJSON: SearchQueryJSON) {
368453
return policyID;
369454
}
370455

456+
/**
457+
* @private
458+
* Returns the human-readable "pretty" value for a filter.
459+
*/
371460
function getDisplayValue(filterName: string, filter: string, personalDetails: OnyxTypes.PersonalDetailsList, cardList: OnyxTypes.CardList, reports: OnyxCollection<OnyxTypes.Report>) {
372461
if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) {
373462
// login can be an empty string
@@ -383,27 +472,13 @@ function getDisplayValue(filterName: string, filter: string, personalDetails: On
383472
return filter;
384473
}
385474

386-
function buildFilterString(filterName: string, queryFilters: QueryFilter[]) {
387-
const delimiter = filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD ? ' ' : ',';
388-
let filterValueString = '';
389-
queryFilters.forEach((queryFilter, index) => {
390-
// If the previous queryFilter has the same operator (this rule applies only to eq and neq operators) then append the current value
391-
if (
392-
index !== 0 &&
393-
((queryFilter.operator === 'eq' && queryFilters?.at(index - 1)?.operator === 'eq') || (queryFilter.operator === 'neq' && queryFilters.at(index - 1)?.operator === 'neq'))
394-
) {
395-
filterValueString += `${delimiter}${sanitizeString(queryFilter.value.toString())}`;
396-
} else if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD) {
397-
filterValueString += `${delimiter}${sanitizeString(queryFilter.value.toString())}`;
398-
} else {
399-
filterValueString += ` ${filterName}${operatorToCharMap[queryFilter.operator]}${sanitizeString(queryFilter.value.toString())}`;
400-
}
401-
});
402-
403-
return filterValueString;
404-
}
405-
406-
function getSearchHeaderTitle(
475+
/**
476+
* Formats a given `SearchQueryJSON` object into the human-readable string version of query.
477+
* This format of query is the one which we want to display to users.
478+
* We try to replace every numeric id value with a display version of this value,
479+
* So: user IDs get turned into emails, report ids into report names etc.
480+
*/
481+
function buildUserReadableQueryString(
407482
queryJSON: SearchQueryJSON,
408483
PersonalDetails: OnyxTypes.PersonalDetailsList,
409484
cardList: OnyxTypes.CardList,
@@ -439,12 +514,15 @@ function getSearchHeaderTitle(
439514
value: getDisplayValue(key, filter.value.toString(), PersonalDetails, cardList, reports),
440515
}));
441516
}
442-
title += buildFilterString(key, displayQueryFilters);
517+
title += buildFilterValuesString(key, displayQueryFilters);
443518
});
444519

445520
return title;
446521
}
447522

523+
/**
524+
* Returns properly built QueryString for a canned query, with the optional policyID.
525+
*/
448526
function buildCannedSearchQuery({
449527
type = CONST.SEARCH.DATA_TYPES.EXPENSE,
450528
status = CONST.SEARCH.STATUS.EXPENSE.ALL,
@@ -462,42 +540,14 @@ function buildCannedSearchQuery({
462540
}
463541

464542
/**
465-
* @private
466-
* Given a filter name and its value, this function will try to find the corresponding ID.
543+
* Returns whether a given search query is a Canned query.
544+
*
545+
* Canned queries are simple predefined queries, that are defined only using type and status and no additional filters.
546+
* In addition, they can contain an optional policyID.
547+
* For example: "type:trip status:all" is a canned query.
467548
*/
468-
function findIDFromDisplayValue(filterName: ValueOf<typeof CONST.SEARCH.SYNTAX_FILTER_KEYS>, filter: string | string[], cardList: OnyxTypes.CardList, taxRates: Record<string, string[]>) {
469-
if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) {
470-
if (typeof filter === 'string') {
471-
const email = filter;
472-
return PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? filter;
473-
}
474-
const emails = filter;
475-
return emails.map((email) => PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? email);
476-
}
477-
if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) {
478-
const names = Array.isArray(filter) ? filter : ([filter] as string[]);
479-
return names.map((name) => taxRates[name] ?? name).flat();
480-
}
481-
if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID) {
482-
if (typeof filter === 'string') {
483-
const bank = filter;
484-
const ids =
485-
Object.values(cardList)
486-
.filter((card) => card.bank === bank)
487-
.map((card) => card.cardID.toString()) ?? filter;
488-
return ids.length > 0 ? ids : bank;
489-
}
490-
const banks = filter;
491-
return banks
492-
.map(
493-
(bank) =>
494-
Object.values(cardList)
495-
.filter((card) => card.bank === bank)
496-
.map((card) => card.cardID.toString()) ?? bank,
497-
)
498-
.flat();
499-
}
500-
return filter;
549+
function isCannedSearchQuery(queryJSON: SearchQueryJSON) {
550+
return !queryJSON.filters;
501551
}
502552

503553
/**
@@ -531,29 +581,14 @@ function standardizeQueryJSON(queryJSON: SearchQueryJSON, cardList: OnyxTypes.Ca
531581
return standardQuery;
532582
}
533583

534-
/**
535-
* Returns whether a given search query is a Canned query.
536-
*
537-
* Canned queries are simple predefined queries, that are defined only using type and status and no additional filters.
538-
* For example: "type:trip status:all" is a canned query.
539-
*/
540-
function isCannedSearchQuery(queryJSON: SearchQueryJSON) {
541-
return !queryJSON.filters;
542-
}
543-
544-
function getContextualSuggestionQuery(reportID: string) {
545-
return `type:chat in:${reportID}`;
546-
}
547-
548584
export {
549585
buildSearchQueryJSON,
550586
buildSearchQueryString,
587+
buildUserReadableQueryString,
551588
buildQueryStringFromFilterFormValues,
552589
buildFilterFormValuesFromQuery,
553590
getPolicyIDFromSearchQuery,
554-
getSearchHeaderTitle,
555591
buildCannedSearchQuery,
556592
isCannedSearchQuery,
557593
standardizeQueryJSON,
558-
getContextualSuggestionQuery,
559594
};

0 commit comments

Comments
 (0)