@@ -32,7 +32,19 @@ const operatorToCharMap = {
32
32
33
33
/**
34
34
* @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 - Z a - z 0 - 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.
36
48
*/
37
49
function buildDateFilterQuery ( filterValues : Partial < SearchAdvancedFiltersForm > ) {
38
50
const dateBefore = filterValues [ FILTER_KEYS . DATE_BEFORE ] ;
@@ -54,7 +66,7 @@ function buildDateFilterQuery(filterValues: Partial<SearchAdvancedFiltersForm>)
54
66
55
67
/**
56
68
* @private
57
- * returns Date filter query string part, which needs special logic
69
+ * Returns amount filter value for QueryString.
58
70
*/
59
71
function buildAmountFilterQuery ( filterValues : Partial < SearchAdvancedFiltersForm > ) {
60
72
const lessThan = filterValues [ FILTER_KEYS . LESS_THAN ] ;
@@ -74,17 +86,33 @@ function buildAmountFilterQuery(filterValues: Partial<SearchAdvancedFiltersForm>
74
86
return amountFilter ;
75
87
}
76
88
77
- function sanitizeString ( str : string ) {
78
- const regexp = / [ ^ A - Z a - z 0 - 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 ;
83
111
}
84
112
85
113
/**
86
114
* @private
87
- * traverses the AST and returns filters as a QueryFilters object
115
+ * Traverses the AST and returns filters as a QueryFilters object.
88
116
*/
89
117
function getFilters ( queryJSON : SearchQueryJSON ) {
90
118
const filters = { } as QueryFilters ;
@@ -136,6 +164,51 @@ function getFilters(queryJSON: SearchQueryJSON) {
136
164
return filters ;
137
165
}
138
166
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
+ */
139
212
function buildSearchQueryJSON ( query : SearchQueryString ) {
140
213
try {
141
214
const result = searchParser . parse ( query ) as SearchQueryJSON ;
@@ -154,6 +227,12 @@ function buildSearchQueryJSON(query: SearchQueryString) {
154
227
}
155
228
}
156
229
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
+ */
157
236
function buildSearchQueryString ( queryJSON ?: SearchQueryJSON ) {
158
237
const queryParts : string [ ] = [ ] ;
159
238
const defaultQueryJSON = buildSearchQueryJSON ( '' ) ;
@@ -177,7 +256,7 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
177
256
const queryFilter = filters [ filterKey ] ;
178
257
179
258
if ( queryFilter ) {
180
- const filterValueString = buildFilterString ( filterKey , queryFilter ) ;
259
+ const filterValueString = buildFilterValuesString ( filterKey , queryFilter ) ;
181
260
queryParts . push ( filterValueString ) ;
182
261
}
183
262
}
@@ -186,25 +265,28 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) {
186
265
}
187
266
188
267
/**
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()
190
272
*/
191
273
function buildQueryStringFromFilterFormValues ( filterValues : Partial < SearchAdvancedFiltersForm > ) {
192
274
// We separate type and status filters from other filters to maintain hashes consistency for saved searches
193
275
const { type, status, policyID, ...otherFilters } = filterValues ;
194
276
const filtersString : string [ ] = [ ] ;
195
277
196
278
if ( type ) {
197
- const sanitizedType = sanitizeString ( type ) ;
279
+ const sanitizedType = sanitizeSearchValue ( type ) ;
198
280
filtersString . push ( `${ CONST . SEARCH . SYNTAX_ROOT_KEYS . TYPE } :${ sanitizedType } ` ) ;
199
281
}
200
282
201
283
if ( status ) {
202
- const sanitizedStatus = sanitizeString ( status ) ;
284
+ const sanitizedStatus = sanitizeSearchValue ( status ) ;
203
285
filtersString . push ( `${ CONST . SEARCH . SYNTAX_ROOT_KEYS . STATUS } :${ sanitizedStatus } ` ) ;
204
286
}
205
287
206
288
if ( policyID ) {
207
- const sanitizedPolicyID = sanitizeString ( policyID ) ;
289
+ const sanitizedPolicyID = sanitizeSearchValue ( policyID ) ;
208
290
filtersString . push ( `${ CONST . SEARCH . SYNTAX_ROOT_KEYS . POLICY_ID } :${ sanitizedPolicyID } ` ) ;
209
291
}
210
292
@@ -213,12 +295,12 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc
213
295
if ( ( filterKey === FILTER_KEYS . MERCHANT || filterKey === FILTER_KEYS . DESCRIPTION || filterKey === FILTER_KEYS . REPORT_ID ) && filterValue ) {
214
296
const keyInCorrectForm = ( Object . keys ( CONST . SEARCH . SYNTAX_FILTER_KEYS ) as FilterKeys [ ] ) . find ( ( key ) => CONST . SEARCH . SYNTAX_FILTER_KEYS [ key ] === filterKey ) ;
215
297
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 ) } ` ;
217
299
}
218
300
}
219
301
220
302
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 ( ' ' ) ;
222
304
return `${ value } ` ;
223
305
}
224
306
@@ -238,7 +320,7 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc
238
320
const filterValueArray = [ ...new Set < string > ( filterValue ) ] ;
239
321
const keyInCorrectForm = ( Object . keys ( CONST . SEARCH . SYNTAX_FILTER_KEYS ) as FilterKeys [ ] ) . find ( ( key ) => CONST . SEARCH . SYNTAX_FILTER_KEYS [ key ] === filterKey ) ;
240
322
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 ( ',' ) } ` ;
242
324
}
243
325
}
244
326
@@ -258,7 +340,10 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial<SearchAdvanc
258
340
}
259
341
260
342
/**
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()
262
347
*/
263
348
function buildFilterFormValuesFromQuery (
264
349
queryJSON : SearchQueryJSON ,
@@ -368,6 +453,10 @@ function getPolicyIDFromSearchQuery(queryJSON: SearchQueryJSON) {
368
453
return policyID ;
369
454
}
370
455
456
+ /**
457
+ * @private
458
+ * Returns the human-readable "pretty" value for a filter.
459
+ */
371
460
function getDisplayValue ( filterName : string , filter : string , personalDetails : OnyxTypes . PersonalDetailsList , cardList : OnyxTypes . CardList , reports : OnyxCollection < OnyxTypes . Report > ) {
372
461
if ( filterName === CONST . SEARCH . SYNTAX_FILTER_KEYS . FROM || filterName === CONST . SEARCH . SYNTAX_FILTER_KEYS . TO ) {
373
462
// login can be an empty string
@@ -383,27 +472,13 @@ function getDisplayValue(filterName: string, filter: string, personalDetails: On
383
472
return filter ;
384
473
}
385
474
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 (
407
482
queryJSON : SearchQueryJSON ,
408
483
PersonalDetails : OnyxTypes . PersonalDetailsList ,
409
484
cardList : OnyxTypes . CardList ,
@@ -439,12 +514,15 @@ function getSearchHeaderTitle(
439
514
value : getDisplayValue ( key , filter . value . toString ( ) , PersonalDetails , cardList , reports ) ,
440
515
} ) ) ;
441
516
}
442
- title += buildFilterString ( key , displayQueryFilters ) ;
517
+ title += buildFilterValuesString ( key , displayQueryFilters ) ;
443
518
} ) ;
444
519
445
520
return title ;
446
521
}
447
522
523
+ /**
524
+ * Returns properly built QueryString for a canned query, with the optional policyID.
525
+ */
448
526
function buildCannedSearchQuery ( {
449
527
type = CONST . SEARCH . DATA_TYPES . EXPENSE ,
450
528
status = CONST . SEARCH . STATUS . EXPENSE . ALL ,
@@ -462,42 +540,14 @@ function buildCannedSearchQuery({
462
540
}
463
541
464
542
/**
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.
467
548
*/
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 ;
501
551
}
502
552
503
553
/**
@@ -531,29 +581,14 @@ function standardizeQueryJSON(queryJSON: SearchQueryJSON, cardList: OnyxTypes.Ca
531
581
return standardQuery ;
532
582
}
533
583
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
-
548
584
export {
549
585
buildSearchQueryJSON ,
550
586
buildSearchQueryString ,
587
+ buildUserReadableQueryString ,
551
588
buildQueryStringFromFilterFormValues ,
552
589
buildFilterFormValuesFromQuery ,
553
590
getPolicyIDFromSearchQuery ,
554
- getSearchHeaderTitle ,
555
591
buildCannedSearchQuery ,
556
592
isCannedSearchQuery ,
557
593
standardizeQueryJSON ,
558
- getContextualSuggestionQuery ,
559
594
} ;
0 commit comments