diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 4f96090be9d0..5db88ae47e8d 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -58,9 +58,10 @@ type QueryFilter = { type AdvancedFiltersKeys = ValueOf; -type QueryFilters = { - [K in AdvancedFiltersKeys]?: QueryFilter[]; -}; +type QueryFilters = Array<{ + key: AdvancedFiltersKeys; + filters: QueryFilter[]; +}>; type SearchQueryString = string; diff --git a/src/libs/SearchParser/autocompleteParser.js b/src/libs/SearchParser/autocompleteParser.js index a9c8870e2f79..5fc13ac07f17 100644 --- a/src/libs/SearchParser/autocompleteParser.js +++ b/src/libs/SearchParser/autocompleteParser.js @@ -185,17 +185,19 @@ function peg$parse(input, options) { var peg$c7 = "expenseType"; var peg$c8 = "type"; var peg$c9 = "status"; - var peg$c10 = "!="; - var peg$c11 = ">="; - var peg$c12 = ">"; - var peg$c13 = "<="; - var peg$c14 = "<"; - var peg$c15 = "\""; + var peg$c10 = ","; + var peg$c11 = "!="; + var peg$c12 = ">="; + var peg$c13 = ">"; + var peg$c14 = "<="; + var peg$c15 = "<"; + var peg$c16 = "\""; var peg$r0 = /^[:=]/; - var peg$r1 = /^[^"\r\n]/; - var peg$r2 = /^[A-Za-z0-9_@.\/#&+\-\\',;%]/; - var peg$r3 = /^[ \t\r\n]/; + var peg$r1 = /^[^ ,"\t\n\r]/; + var peg$r2 = /^[^"\r\n]/; + var peg$r3 = /^[^ ,\t\n\r]/; + var peg$r4 = /^[ \t\r\n]/; var peg$e0 = peg$otherExpectation("key"); var peg$e1 = peg$literalExpectation("in", false); @@ -208,20 +210,22 @@ function peg$parse(input, options) { var peg$e8 = peg$literalExpectation("expenseType", false); var peg$e9 = peg$literalExpectation("type", false); var peg$e10 = peg$literalExpectation("status", false); - var peg$e11 = peg$otherExpectation("operator"); - var peg$e12 = peg$classExpectation([":", "="], false, false); - var peg$e13 = peg$literalExpectation("!=", 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$otherExpectation("quote"); - var peg$e19 = peg$literalExpectation("\"", false); - var peg$e20 = peg$classExpectation(["\"", "\r", "\n"], true, false); - var peg$e21 = peg$otherExpectation("word"); - var peg$e22 = peg$classExpectation([["A", "Z"], ["a", "z"], ["0", "9"], "_", "@", ".", "/", "#", "&", "+", "-", "\\", "'", ",", ";", "%"], false, false); - var peg$e23 = peg$otherExpectation("whitespace"); - var peg$e24 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false); + var peg$e11 = peg$literalExpectation(",", 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$f0 = function(ranges) { return { autocomplete, ranges }; }; var peg$f1 = function(filters) { return filters.filter(Boolean).flat(); }; @@ -241,21 +245,30 @@ function peg$parse(input, options) { ...value[value.length - 1], }; - return value.map(({ start, length }) => ({ - key, - start, - length, - })); + return value + .filter((filter) => filter.length > 0) + .map(({ start, length }) => ({ + key, + start, + length, + })); }; var peg$f3 = function() { autocomplete = null; }; - var peg$f4 = function(parts) { + var peg$f4 = function(parts, empty) { const ends = location(); const value = parts.flat(); + if (empty) { + value.push(""); + } let count = ends.start.offset; const result = []; value.forEach((filter) => { + let word = filter; + if (word.startsWith('"') && word.endsWith('"') && word.length >= 2) { + word = word.slice(1, -1); + } result.push({ - value: filter, + value: word, start: count, length: filter.length, }); @@ -269,10 +282,10 @@ function peg$parse(input, options) { var peg$f8 = function() { return "gt"; }; var peg$f9 = function() { return "lte"; }; var peg$f10 = function() { return "lt"; }; - var peg$f11 = function(chars) { return chars.join(""); }; - var peg$f12 = function(chars) { - return chars.join("").trim().split(",").filter(Boolean); + var peg$f11 = function(start, inner, end) { + return [...start, '"', ...inner, '"', ...end].join(""); }; + var peg$f12 = function(chars) { return chars.join("").trim(); }; var peg$f13 = function() { return "and"; }; var peg$currPos = options.peg$currPos | 0; var peg$savedPos = peg$currPos; @@ -648,30 +661,63 @@ function peg$parse(input, options) { } function peg$parseidentifier() { - var s0, s1, s2; + var s0, s1, s2, s3, s4; s0 = peg$currPos; - s1 = []; - s2 = peg$parsequotedString(); - if (s2 === peg$FAILED) { - s2 = peg$parsealphanumeric(); + s1 = peg$currPos; + s2 = []; + s3 = peg$parsequotedString(); + if (s3 === peg$FAILED) { + s3 = peg$parsealphanumeric(); } - if (s2 !== peg$FAILED) { - while (s2 !== peg$FAILED) { - s1.push(s2); - s2 = peg$parsequotedString(); - if (s2 === peg$FAILED) { - s2 = peg$parsealphanumeric(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 44) { + s4 = peg$c10; + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e11); } + } + if (s4 !== peg$FAILED) { + s4 = peg$parsequotedString(); + if (s4 === peg$FAILED) { + s4 = peg$parsealphanumeric(); } + if (s4 === peg$FAILED) { + peg$currPos = s3; + s3 = peg$FAILED; + } else { + s3 = s4; + } + } else { + s3 = s4; } - } else { + } + if (s2.length < 1) { + peg$currPos = s1; s1 = peg$FAILED; + } else { + s1 = s2; } if (s1 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s2 = peg$c10; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e11); } + } + if (s2 === peg$FAILED) { + s2 = null; + } peg$savedPos = s0; - s1 = peg$f4(s1); + s0 = peg$f4(s1, s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; } - s0 = s1; return s0; } @@ -686,7 +732,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e12); } + if (peg$silentFails === 0) { peg$fail(peg$e13); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -695,12 +741,12 @@ function peg$parse(input, options) { s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c10) { - s1 = peg$c10; + if (input.substr(peg$currPos, 2) === peg$c11) { + s1 = peg$c11; peg$currPos += 2; } 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; @@ -709,12 +755,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; @@ -724,11 +770,11 @@ function peg$parse(input, options) { if (s0 === peg$FAILED) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 62) { - s1 = peg$c12; + s1 = peg$c13; peg$currPos++; } 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; @@ -737,12 +783,12 @@ function peg$parse(input, options) { s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c13) { - s1 = peg$c13; + if (input.substr(peg$currPos, 2) === peg$c14) { + s1 = peg$c14; peg$currPos += 2; } 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; @@ -752,11 +798,11 @@ function peg$parse(input, options) { if (s0 === peg$FAILED) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 60) { - s1 = peg$c14; + s1 = peg$c15; peg$currPos++; } 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; @@ -771,53 +817,89 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e11); } + if (peg$silentFails === 0) { peg$fail(peg$e12); } } return s0; } function peg$parsequotedString() { - var s0, s1, s2, s3; + var s0, s1, s2, s3, s4, s5, s6; peg$silentFails++; s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 34) { - s1 = peg$c15; + s1 = []; + s2 = input.charAt(peg$currPos); + if (peg$r1.test(s2)) { peg$currPos++; } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e19); } + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e20); } } - if (s1 !== peg$FAILED) { - s2 = []; - s3 = input.charAt(peg$currPos); - if (peg$r1.test(s3)) { + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = input.charAt(peg$currPos); + if (peg$r1.test(s2)) { peg$currPos++; } else { - s3 = peg$FAILED; + s2 = peg$FAILED; if (peg$silentFails === 0) { peg$fail(peg$e20); } } - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = input.charAt(peg$currPos); - if (peg$r1.test(s3)) { + } + if (input.charCodeAt(peg$currPos) === 34) { + s2 = peg$c16; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e21); } + } + if (s2 !== peg$FAILED) { + s3 = []; + s4 = input.charAt(peg$currPos); + if (peg$r2.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e22); } + } + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = input.charAt(peg$currPos); + if (peg$r2.test(s4)) { peg$currPos++; } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e20); } + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e22); } } } if (input.charCodeAt(peg$currPos) === 34) { - s3 = peg$c15; + s4 = peg$c16; peg$currPos++; } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e19); } + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e21); } } - if (s3 !== peg$FAILED) { + if (s4 !== peg$FAILED) { + s5 = []; + s6 = input.charAt(peg$currPos); + if (peg$r3.test(s6)) { + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e23); } + } + while (s6 !== peg$FAILED) { + s5.push(s6); + s6 = input.charAt(peg$currPos); + if (peg$r3.test(s6)) { + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e23); } + } + } peg$savedPos = s0; - s0 = peg$f11(s2); + s0 = peg$f11(s1, s3, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -829,7 +911,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e18); } + if (peg$silentFails === 0) { peg$fail(peg$e19); } } return s0; @@ -842,21 +924,21 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = []; s2 = input.charAt(peg$currPos); - if (peg$r2.test(s2)) { + if (peg$r3.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e22); } + if (peg$silentFails === 0) { peg$fail(peg$e23); } } if (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) { s1.push(s2); s2 = input.charAt(peg$currPos); - if (peg$r2.test(s2)) { + if (peg$r3.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e22); } + if (peg$silentFails === 0) { peg$fail(peg$e23); } } } } else { @@ -870,7 +952,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e21); } + if (peg$silentFails === 0) { peg$fail(peg$e24); } } return s0; @@ -894,25 +976,25 @@ function peg$parse(input, options) { peg$silentFails++; s0 = []; s1 = input.charAt(peg$currPos); - if (peg$r3.test(s1)) { + if (peg$r4.test(s1)) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e24); } + if (peg$silentFails === 0) { peg$fail(peg$e26); } } while (s1 !== peg$FAILED) { s0.push(s1); s1 = input.charAt(peg$currPos); - if (peg$r3.test(s1)) { + if (peg$r4.test(s1)) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e24); } + if (peg$silentFails === 0) { peg$fail(peg$e26); } } } peg$silentFails--; s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e23); } + if (peg$silentFails === 0) { peg$fail(peg$e25); } return s0; } diff --git a/src/libs/SearchParser/autocompleteParser.peggy b/src/libs/SearchParser/autocompleteParser.peggy index 003d35485d69..bc96c86eb74d 100644 --- a/src/libs/SearchParser/autocompleteParser.peggy +++ b/src/libs/SearchParser/autocompleteParser.peggy @@ -39,11 +39,13 @@ defaultFilter ...value[value.length - 1], }; - return value.map(({ start, length }) => ({ - key, - start, - length, - })); + return value + .filter((filter) => filter.length > 0) + .map(({ start, length }) => ({ + key, + start, + length, + })); } freeTextFilter = _ identifier _ { autocomplete = null; } @@ -63,14 +65,21 @@ autocompleteKey "key" ) identifier - = parts:(quotedString / alphanumeric)+ { + = parts:(quotedString / alphanumeric)|1.., ","| empty:","? { const ends = location(); const value = parts.flat(); + if (empty) { + value.push(""); + } let count = ends.start.offset; const result = []; value.forEach((filter) => { + let word = filter; + if (word.startsWith('"') && word.endsWith('"') && word.length >= 2) { + word = word.slice(1, -1); + } result.push({ - value: filter, + value: word, start: count, length: filter.length, }); diff --git a/src/libs/SearchParser/baseRules.peggy b/src/libs/SearchParser/baseRules.peggy index 62948fdb573b..7605d888ba43 100644 --- a/src/libs/SearchParser/baseRules.peggy +++ b/src/libs/SearchParser/baseRules.peggy @@ -16,13 +16,13 @@ operator "operator" / "<=" { return "lte"; } / "<" { return "lt"; } -quotedString "quote" = "\"" chars:[^"\r\n]* "\"" { return chars.join(""); } - -alphanumeric "word" - = chars:[A-Za-z0-9_@./#&+\-\\',;%]+ { - return chars.join("").trim().split(",").filter(Boolean); +quotedString "quote" + = start:[^ ,"\t\n\r]* "\"" inner:[^"\r\n]* "\"" end:[^ ,\t\n\r]* { + return [...start, '"', ...inner, '"', ...end].join(""); } +alphanumeric "word" = chars:[^ ,\t\n\r]+ { return chars.join("").trim(); } + logicalAnd = _ { return "and"; } _ "whitespace" = [ \t\r\n]* diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js index 49e819ada3e5..1e8d12f16a32 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -200,17 +200,19 @@ function peg$parse(input, options) { var peg$c17 = "sortBy"; var peg$c18 = "sortOrder"; var peg$c19 = "policyID"; - var peg$c20 = "!="; - var peg$c21 = ">="; - var peg$c22 = ">"; - var peg$c23 = "<="; - var peg$c24 = "<"; - var peg$c25 = "\""; + var peg$c20 = ","; + var peg$c21 = "!="; + var peg$c22 = ">="; + var peg$c23 = ">"; + var peg$c24 = "<="; + var peg$c25 = "<"; + var peg$c26 = "\""; var peg$r0 = /^[:=]/; - var peg$r1 = /^[^"\r\n]/; - var peg$r2 = /^[A-Za-z0-9_@.\/#&+\-\\',;%]/; - var peg$r3 = /^[ \t\r\n]/; + var peg$r1 = /^[^ ,"\t\n\r]/; + var peg$r2 = /^[^"\r\n]/; + var peg$r3 = /^[^ ,\t\n\r]/; + var peg$r4 = /^[ \t\r\n]/; var peg$e0 = peg$otherExpectation("key"); var peg$e1 = peg$literalExpectation("date", false); @@ -234,20 +236,22 @@ function peg$parse(input, options) { var peg$e19 = peg$literalExpectation("sortBy", false); var peg$e20 = peg$literalExpectation("sortOrder", false); var peg$e21 = peg$literalExpectation("policyID", false); - var peg$e22 = peg$otherExpectation("operator"); - var peg$e23 = peg$classExpectation([":", "="], false, false); - var peg$e24 = peg$literalExpectation("!=", false); - var peg$e25 = peg$literalExpectation(">=", false); - var peg$e26 = peg$literalExpectation(">", false); - var peg$e27 = peg$literalExpectation("<=", false); - var peg$e28 = peg$literalExpectation("<", false); - var peg$e29 = peg$otherExpectation("quote"); - var peg$e30 = peg$literalExpectation("\"", false); - var peg$e31 = peg$classExpectation(["\"", "\r", "\n"], true, false); - var peg$e32 = peg$otherExpectation("word"); - var peg$e33 = peg$classExpectation([["A", "Z"], ["a", "z"], ["0", "9"], "_", "@", ".", "/", "#", "&", "+", "-", "\\", "'", ",", ";", "%"], false, false); - var peg$e34 = peg$otherExpectation("whitespace"); - var peg$e35 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false); + var peg$e22 = peg$literalExpectation(",", false); + var peg$e23 = peg$otherExpectation("operator"); + var peg$e24 = peg$classExpectation([":", "="], false, false); + var peg$e25 = peg$literalExpectation("!=", false); + var peg$e26 = peg$literalExpectation(">=", false); + var peg$e27 = peg$literalExpectation(">", false); + var peg$e28 = peg$literalExpectation("<=", false); + var peg$e29 = peg$literalExpectation("<", false); + var peg$e30 = peg$otherExpectation("quote"); + var peg$e31 = peg$classExpectation([" ", ",", "\"", "\t", "\n", "\r"], true, false); + var peg$e32 = peg$literalExpectation("\"", false); + var peg$e33 = peg$classExpectation(["\"", "\r", "\n"], true, false); + var peg$e34 = peg$classExpectation([" ", ",", "\t", "\n", "\r"], true, false); + var peg$e35 = peg$otherExpectation("word"); + var peg$e36 = peg$otherExpectation("whitespace"); + var peg$e37 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false); var peg$f0 = function(filters) { return applyDefaults(filters); }; var peg$f1 = function(head, tail) { @@ -287,9 +291,14 @@ function peg$parse(input, options) { return buildFilter(op, field, values); }; var peg$f5 = function(parts) { - const value = parts.flat(); + const value = parts.flat().map((word) => { + if (word.startsWith('"') && word.endsWith('"') && word.length >= 2) { + return word.slice(1,-1); + } + return word; + }); if (value.length > 1) { - return value; + return value.filter((word) => word.length > 0); } return value[0]; }; @@ -299,10 +308,10 @@ function peg$parse(input, options) { var peg$f9 = function() { return "gt"; }; var peg$f10 = function() { return "lte"; }; var peg$f11 = function() { return "lt"; }; - var peg$f12 = function(chars) { return chars.join(""); }; - var peg$f13 = function(chars) { - return chars.join("").trim().split(",").filter(Boolean); + var peg$f12 = function(start, inner, end) { + return [...start, '"', ...inner, '"', ...end].join(""); }; + var peg$f13 = function(chars) { return chars.join("").trim(); }; var peg$f14 = function() { return "and"; }; var peg$currPos = options.peg$currPos | 0; var peg$savedPos = peg$currPos; @@ -747,15 +756,6 @@ function peg$parse(input, options) { s1 = peg$FAILED; if (peg$silentFails === 0) { peg$fail(peg$e15); } } - if (s1 === peg$FAILED) { - if (input.substr(peg$currPos, 2) === peg$c6) { - s1 = peg$c6; - peg$currPos += 2; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e7); } - } - } } } } @@ -849,24 +849,45 @@ function peg$parse(input, options) { } function peg$parseidentifier() { - var s0, s1, s2; + var s0, s1, s2, s3, s4; s0 = peg$currPos; - s1 = []; - s2 = peg$parsequotedString(); - if (s2 === peg$FAILED) { - s2 = peg$parsealphanumeric(); + s1 = peg$currPos; + s2 = []; + s3 = peg$parsequotedString(); + if (s3 === peg$FAILED) { + s3 = peg$parsealphanumeric(); } - if (s2 !== peg$FAILED) { - while (s2 !== peg$FAILED) { - s1.push(s2); - s2 = peg$parsequotedString(); - if (s2 === peg$FAILED) { - s2 = peg$parsealphanumeric(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 44) { + s4 = peg$c20; + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e22); } + } + if (s4 !== peg$FAILED) { + s4 = peg$parsequotedString(); + if (s4 === peg$FAILED) { + s4 = peg$parsealphanumeric(); } + if (s4 === peg$FAILED) { + peg$currPos = s3; + s3 = peg$FAILED; + } else { + s3 = s4; + } + } else { + s3 = s4; } - } else { + } + if (s2.length < 1) { + peg$currPos = s1; s1 = peg$FAILED; + } else { + s1 = s2; } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -887,7 +908,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e23); } + if (peg$silentFails === 0) { peg$fail(peg$e24); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -896,12 +917,12 @@ function peg$parse(input, options) { s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c20) { - s1 = peg$c20; + if (input.substr(peg$currPos, 2) === peg$c21) { + s1 = peg$c21; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e24); } + if (peg$silentFails === 0) { peg$fail(peg$e25); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -910,12 +931,12 @@ function peg$parse(input, options) { s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c21) { - s1 = peg$c21; + if (input.substr(peg$currPos, 2) === peg$c22) { + s1 = peg$c22; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e25); } + if (peg$silentFails === 0) { peg$fail(peg$e26); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -925,11 +946,11 @@ function peg$parse(input, options) { if (s0 === peg$FAILED) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 62) { - s1 = peg$c22; + s1 = peg$c23; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e26); } + if (peg$silentFails === 0) { peg$fail(peg$e27); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -938,12 +959,12 @@ function peg$parse(input, options) { s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c23) { - s1 = peg$c23; + if (input.substr(peg$currPos, 2) === peg$c24) { + s1 = peg$c24; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e27); } + if (peg$silentFails === 0) { peg$fail(peg$e28); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -953,11 +974,11 @@ function peg$parse(input, options) { if (s0 === peg$FAILED) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 60) { - s1 = peg$c24; + s1 = peg$c25; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e28); } + if (peg$silentFails === 0) { peg$fail(peg$e29); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -972,53 +993,89 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e22); } + if (peg$silentFails === 0) { peg$fail(peg$e23); } } return s0; } function peg$parsequotedString() { - var s0, s1, s2, s3; + var s0, s1, s2, s3, s4, s5, s6; peg$silentFails++; s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 34) { - s1 = peg$c25; + s1 = []; + s2 = input.charAt(peg$currPos); + if (peg$r1.test(s2)) { peg$currPos++; } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e30); } + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e31); } } - if (s1 !== peg$FAILED) { - s2 = []; - s3 = input.charAt(peg$currPos); - if (peg$r1.test(s3)) { + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = input.charAt(peg$currPos); + if (peg$r1.test(s2)) { peg$currPos++; } else { - s3 = peg$FAILED; + s2 = peg$FAILED; if (peg$silentFails === 0) { peg$fail(peg$e31); } } - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = input.charAt(peg$currPos); - if (peg$r1.test(s3)) { + } + if (input.charCodeAt(peg$currPos) === 34) { + s2 = peg$c26; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e32); } + } + if (s2 !== peg$FAILED) { + s3 = []; + s4 = input.charAt(peg$currPos); + if (peg$r2.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e33); } + } + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = input.charAt(peg$currPos); + if (peg$r2.test(s4)) { peg$currPos++; } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e31); } + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e33); } } } if (input.charCodeAt(peg$currPos) === 34) { - s3 = peg$c25; + s4 = peg$c26; peg$currPos++; } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e30); } + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e32); } } - if (s3 !== peg$FAILED) { + if (s4 !== peg$FAILED) { + s5 = []; + s6 = input.charAt(peg$currPos); + if (peg$r3.test(s6)) { + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e34); } + } + while (s6 !== peg$FAILED) { + s5.push(s6); + s6 = input.charAt(peg$currPos); + if (peg$r3.test(s6)) { + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e34); } + } + } peg$savedPos = s0; - s0 = peg$f12(s2); + s0 = peg$f12(s1, s3, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1030,7 +1087,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e29); } + if (peg$silentFails === 0) { peg$fail(peg$e30); } } return s0; @@ -1043,21 +1100,21 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = []; s2 = input.charAt(peg$currPos); - if (peg$r2.test(s2)) { + if (peg$r3.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e33); } + if (peg$silentFails === 0) { peg$fail(peg$e34); } } if (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) { s1.push(s2); s2 = input.charAt(peg$currPos); - if (peg$r2.test(s2)) { + if (peg$r3.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e33); } + if (peg$silentFails === 0) { peg$fail(peg$e34); } } } } else { @@ -1071,7 +1128,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e32); } + if (peg$silentFails === 0) { peg$fail(peg$e35); } } return s0; @@ -1095,25 +1152,25 @@ function peg$parse(input, options) { peg$silentFails++; s0 = []; s1 = input.charAt(peg$currPos); - if (peg$r3.test(s1)) { + if (peg$r4.test(s1)) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e35); } + if (peg$silentFails === 0) { peg$fail(peg$e37); } } while (s1 !== peg$FAILED) { s0.push(s1); s1 = input.charAt(peg$currPos); - if (peg$r3.test(s1)) { + if (peg$r4.test(s1)) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e35); } + if (peg$silentFails === 0) { peg$fail(peg$e37); } } } peg$silentFails--; s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e34); } + if (peg$silentFails === 0) { peg$fail(peg$e36); } return s0; } diff --git a/src/libs/SearchParser/searchParser.peggy b/src/libs/SearchParser/searchParser.peggy index f775117b5f4e..7d5815d41459 100644 --- a/src/libs/SearchParser/searchParser.peggy +++ b/src/libs/SearchParser/searchParser.peggy @@ -108,17 +108,21 @@ key "key" / "cardID" / "from" / "expenseType" - / "in" ) defaultKey "default key" = @("type" / "status" / "sortBy" / "sortOrder" / "policyID") identifier - = parts:(quotedString / alphanumeric)+ { - const value = parts.flat(); + = parts:(quotedString / alphanumeric)|1.., ","| { + const value = parts.flat().map((word) => { + if (word.startsWith('"') && word.endsWith('"') && word.length >= 2) { + return word.slice(1,-1); + } + return word; + }); if (value.length > 1) { - return value; + return value.filter((word) => word.length > 0); } return value[0]; } diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 32c2eff72007..51db9fd56ea6 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -1,13 +1,15 @@ import cloneDeep from 'lodash/cloneDeep'; import type {OnyxCollection} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import type {AdvancedFiltersKeys, ASTNode, QueryFilter, QueryFilters, SearchQueryJSON, SearchQueryString, SearchStatus} from '@components/Search/types'; +import type {ASTNode, QueryFilter, QueryFilters, SearchQueryJSON, SearchQueryString, SearchStatus} from '@components/Search/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {SearchAdvancedFiltersForm} from '@src/types/form'; import FILTER_KEYS from '@src/types/form/SearchAdvancedFiltersForm'; import type * as OnyxTypes from '@src/types/onyx'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; +import * as CurrencyUtils from './CurrencyUtils'; +import localeCompare from './LocaleCompare'; import {validateAmount} from './MoneyRequestUtils'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import {getTagNamesFromTagsLists} from './PolicyUtils'; @@ -115,7 +117,7 @@ function buildFilterValuesString(filterName: string, queryFilters: QueryFilter[] * Traverses the AST and returns filters as a QueryFilters object. */ function getFilters(queryJSON: SearchQueryJSON) { - const filters = {} as QueryFilters; + const filters = [] as QueryFilters; const filterKeys = Object.values(CONST.SEARCH.SYNTAX_FILTER_KEYS); function traverse(node: ASTNode) { @@ -136,12 +138,7 @@ function getFilters(queryJSON: SearchQueryJSON) { return; } - if (!filters[nodeKey]) { - filters[nodeKey] = []; - } - - // the "?? []" is added only for typescript because otherwise TS throws an error, in newer TS versions this should be fixed - const filterArray = filters[nodeKey] ?? []; + const filterArray = []; if (!Array.isArray(node.right)) { filterArray.push({ operator: node.operator, @@ -155,6 +152,7 @@ function getFilters(queryJSON: SearchQueryJSON) { }); }); } + filters.push({key: nodeKey, filters: filterArray}); } if (queryJSON.filters) { @@ -200,6 +198,16 @@ function findIDFromDisplayValue(filterName: ValueOf { + const backendAmount = CurrencyUtils.convertToBackendAmount(Number(amount)); + return Number.isNaN(backendAmount) ? amount : backendAmount.toString(); + }); + } return filter; } @@ -218,18 +226,14 @@ function getQueryHash(query: SearchQueryJSON): number { orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_BY}:${query.sortBy}`; orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_ORDER}:${query.sortOrder}`; - Object.keys(query.flatFilters) + query.flatFilters.forEach((filter) => { + filter.filters.sort((a, b) => localeCompare(a.value.toString(), b.value.toString())); + }); + + query.flatFilters + .map((filter) => buildFilterValuesString(filter.key, filter.filters)) .sort() - .forEach((key) => { - const filterValues = query.flatFilters?.[key as AdvancedFiltersKeys]; - const sortedFilterValues = filterValues?.sort((queryFilter1, queryFilter2) => { - if (queryFilter1.value > queryFilter2.value) { - return 1; - } - return -1; - }); - orderedQuery += ` ${buildFilterValuesString(key, sortedFilterValues ?? [])}`; - }); + .forEach((filterString) => (orderedQuery += ` ${filterString}`)); return UserUtils.hashText(orderedQuery, 2 ** 32); } @@ -281,13 +285,9 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) { const filters = queryJSON.flatFilters; - for (const [, filterKey] of Object.entries(CONST.SEARCH.SYNTAX_FILTER_KEYS)) { - const queryFilter = filters[filterKey]; - - if (queryFilter) { - const filterValueString = buildFilterValuesString(filterKey, queryFilter); - queryParts.push(filterValueString); - } + for (const filter of filters) { + const filterValueString = buildFilterValuesString(filter.key, filter.filters); + queryParts.push(filterValueString); } return queryParts.join(' '); @@ -388,32 +388,35 @@ function buildFilterFormValuesFromQuery( taxRates: Record, ) { const filters = queryJSON.flatFilters; - const filterKeys = Object.keys(filters); const filtersForm = {} as Partial; const policyID = queryJSON.policyID; - for (const filterKey of filterKeys) { + for (const queryFilter of filters) { + const filterKey = queryFilter.key; + const filterList = queryFilter.filters; + const filterValues = filterList.map((item) => item.value.toString()); if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_ID || filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.MERCHANT || filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.DESCRIPTION) { - filtersForm[filterKey] = filters[filterKey]?.[0]?.value.toString(); + filtersForm[filterKey] = filterValues.at(0); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPENSE_TYPE) { - filtersForm[filterKey] = filters[filterKey] - ?.map((expenseType) => expenseType.value.toString()) - .filter((expenseType) => Object.values(CONST.SEARCH.TRANSACTION_TYPE).includes(expenseType as ValueOf)); + const validExpenseTypes = new Set(Object.values(CONST.SEARCH.TRANSACTION_TYPE)); + filtersForm[filterKey] = filterValues.filter((expenseType) => validExpenseTypes.has(expenseType as ValueOf)); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID) { - filtersForm[filterKey] = filters[filterKey]?.map((card) => card.value.toString()).filter((card) => Object.keys(cardList).includes(card)); + filtersForm[filterKey] = filterValues.filter((card) => cardList[card]); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) { - filtersForm[filterKey] = filters[filterKey]?.map((tax) => tax.value.toString()).filter((tax) => [...Object.values(taxRates)].flat().includes(tax)); + const allTaxRates = new Set(Object.values(taxRates).flat()); + filtersForm[filterKey] = filterValues.filter((tax) => allTaxRates.has(tax)); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.IN) { - filtersForm[filterKey] = filters[filterKey]?.map((report) => report.value.toString()).filter((id) => reports?.[`${ONYXKEYS.COLLECTION.REPORT}${id}`]); + filtersForm[filterKey] = filterValues.filter((id) => reports?.[`${ONYXKEYS.COLLECTION.REPORT}${id}`]); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) { - filtersForm[filterKey] = filters[filterKey]?.map((id) => id.value.toString()).filter((id) => Object.keys(personalDetails).includes(id)); + filtersForm[filterKey] = filterValues.filter((id) => personalDetails[id]); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY) { - filtersForm[filterKey] = filters[filterKey]?.filter((currency) => Object.keys(currencyList).includes(currency.value.toString())).map((currency) => currency.value.toString()); + const validCurrency = new Set(Object.keys(currencyList)); + filtersForm[filterKey] = filterValues.filter((currency) => validCurrency.has(currency)); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG) { const tags = policyID @@ -422,8 +425,9 @@ function buildFilterFormValuesFromQuery( .filter((item) => !!item) .map((tagList) => getTagNamesFromTagsLists(tagList ?? {})) .flat(); - tags.push(CONST.SEARCH.EMPTY_VALUE); - filtersForm[filterKey] = filters[filterKey]?.map((tag) => tag.value.toString()).filter((name) => tags.includes(name)); + const uniqueTags = new Set(tags); + uniqueTags.add(CONST.SEARCH.EMPTY_VALUE); + filtersForm[filterKey] = filterValues.filter((name) => uniqueTags.has(name)); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY) { const categories = policyID @@ -431,13 +435,13 @@ function buildFilterFormValuesFromQuery( : Object.values(policyCategories ?? {}) .map((item) => Object.values(item ?? {}).map((category) => category.name)) .flat(); - categories.push(CONST.SEARCH.EMPTY_VALUE); - filtersForm[filterKey] = filters[filterKey]?.map((category) => category.value.toString()).filter((name) => categories.includes(name)); + const uniqueCategories = new Set(categories); + uniqueCategories.add(CONST.SEARCH.EMPTY_VALUE); + filtersForm[filterKey] = filterValues.filter((name) => uniqueCategories.has(name)); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD) { - filtersForm[filterKey] = filters[filterKey] - ?.map((filter) => filter.value.toString()) - .map((filter) => { + filtersForm[filterKey] = filterValues + ?.map((filter) => { if (filter.includes(' ')) { return `"${filter}"`; } @@ -446,12 +450,19 @@ function buildFilterFormValuesFromQuery( .join(' '); } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE) { - filtersForm[FILTER_KEYS.DATE_BEFORE] = filters[filterKey]?.find((filter) => filter.operator === 'lt' && ValidationUtils.isValidDate(filter.value.toString()))?.value.toString(); - filtersForm[FILTER_KEYS.DATE_AFTER] = filters[filterKey]?.find((filter) => filter.operator === 'gt' && ValidationUtils.isValidDate(filter.value.toString()))?.value.toString(); + filtersForm[FILTER_KEYS.DATE_BEFORE] = + filterList.find((filter) => filter.operator === 'lt' && ValidationUtils.isValidDate(filter.value.toString()))?.value.toString() ?? filtersForm[FILTER_KEYS.DATE_BEFORE]; + filtersForm[FILTER_KEYS.DATE_AFTER] = + filterList.find((filter) => filter.operator === 'gt' && ValidationUtils.isValidDate(filter.value.toString()))?.value.toString() ?? filtersForm[FILTER_KEYS.DATE_AFTER]; } if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT) { - filtersForm[FILTER_KEYS.LESS_THAN] = filters[filterKey]?.find((filter) => filter.operator === 'lt' && validateAmount(filter.value.toString(), 2))?.value.toString(); - filtersForm[FILTER_KEYS.GREATER_THAN] = filters[filterKey]?.find((filter) => filter.operator === 'gt' && validateAmount(filter.value.toString(), 2))?.value.toString(); + // backend amount is an integer and is 2 digits longer than frontend amount + filtersForm[FILTER_KEYS.LESS_THAN] = + filterList.find((filter) => filter.operator === 'lt' && validateAmount(filter.value.toString(), 0, CONST.IOU.AMOUNT_MAX_LENGTH + 2))?.value.toString() ?? + filtersForm[FILTER_KEYS.LESS_THAN]; + filtersForm[FILTER_KEYS.GREATER_THAN] = + filterList.find((filter) => filter.operator === 'gt' && validateAmount(filter.value.toString(), 0, CONST.IOU.AMOUNT_MAX_LENGTH + 2))?.value.toString() ?? + filtersForm[FILTER_KEYS.GREATER_THAN]; } } @@ -503,6 +514,10 @@ function getDisplayValue(filterName: string, filter: string, personalDetails: On if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.IN) { return ReportUtils.getReportName(reports?.[`${ONYXKEYS.COLLECTION.REPORT}${filter}`]) || filter; } + if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT) { + const frontendAmount = CurrencyUtils.convertToFrontendAmountAsInteger(Number(filter)); + return Number.isNaN(frontendAmount) ? filter : frontendAmount.toString(); + } return filter; } @@ -524,8 +539,10 @@ function buildUserReadableQueryString( let title = `type:${type} status:${status}`; - Object.keys(filters).forEach((key) => { - const queryFilter = filters[key as ValueOf] ?? []; + for (const filterObject of filters) { + const key = filterObject.key; + const queryFilter = filterObject.filters; + let displayQueryFilters: QueryFilter[] = []; if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) { const taxRateIDs = queryFilter.map((filter) => filter.value.toString()); @@ -549,7 +566,7 @@ function buildUserReadableQueryString( })); } title += buildFilterValuesString(key, displayQueryFilters); - }); + } return title; }