@navikt/ds-react 8.5.0 → 8.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/data/table/helpers/table-grid-nav.d.ts +9 -15
- package/cjs/data/table/helpers/table-grid-nav.js +18 -25
- package/cjs/data/table/helpers/table-grid-nav.js.map +1 -1
- package/cjs/data/table/helpers/table-keyboard.d.ts +1 -1
- package/cjs/data/table/helpers/table-keyboard.js +1 -6
- package/cjs/data/table/helpers/table-keyboard.js.map +1 -1
- package/cjs/data/table/root/DataTableRoot.d.ts +41 -4
- package/cjs/data/table/root/DataTableRoot.js +10 -6
- package/cjs/data/table/root/DataTableRoot.js.map +1 -1
- package/cjs/data/table/root/useTableKeyboardNav.d.ts +1 -1
- package/cjs/data/table/root/useTableKeyboardNav.js +32 -19
- package/cjs/data/table/root/useTableKeyboardNav.js.map +1 -1
- package/cjs/data/table/td/DataTableTd.d.ts +5 -4
- package/cjs/data/table/td/DataTableTd.js +2 -2
- package/cjs/data/table/td/DataTableTd.js.map +1 -1
- package/cjs/data/token-filter/AutoSuggest.d.ts +9 -0
- package/cjs/data/token-filter/AutoSuggest.js +56 -0
- package/cjs/data/token-filter/AutoSuggest.js.map +1 -0
- package/cjs/data/token-filter/AutoSuggest.types.d.ts +12 -0
- package/cjs/data/token-filter/AutoSuggest.types.js +3 -0
- package/cjs/data/token-filter/AutoSuggest.types.js.map +1 -0
- package/cjs/data/token-filter/TokenFilter.d.ts +11 -0
- package/cjs/data/token-filter/TokenFilter.js +102 -0
- package/cjs/data/token-filter/TokenFilter.js.map +1 -0
- package/cjs/data/token-filter/TokenFilter.types.d.ts +52 -0
- package/cjs/data/token-filter/TokenFilter.types.js +3 -0
- package/cjs/data/token-filter/TokenFilter.types.js.map +1 -0
- package/cjs/data/token-filter/helpers/generate-autocomplete-options.d.ts +24 -0
- package/cjs/data/token-filter/helpers/generate-autocomplete-options.js +197 -0
- package/cjs/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -0
- package/cjs/data/token-filter/helpers/grouping.d.ts +28 -0
- package/cjs/data/token-filter/helpers/grouping.js +61 -0
- package/cjs/data/token-filter/helpers/grouping.js.map +1 -0
- package/cjs/data/token-filter/helpers/operators.d.ts +22 -0
- package/cjs/data/token-filter/helpers/operators.js +66 -0
- package/cjs/data/token-filter/helpers/operators.js.map +1 -0
- package/cjs/data/token-filter/helpers/parse-query-text.d.ts +25 -0
- package/cjs/data/token-filter/helpers/parse-query-text.js +46 -0
- package/cjs/data/token-filter/helpers/parse-query-text.js.map +1 -0
- package/cjs/data/token-filter/helpers/query-builder.d.ts +20 -0
- package/cjs/data/token-filter/helpers/query-builder.js +38 -0
- package/cjs/data/token-filter/helpers/query-builder.js.map +1 -0
- package/cjs/data/token-filter/helpers/text-matching.d.ts +16 -0
- package/cjs/data/token-filter/helpers/text-matching.js +47 -0
- package/cjs/data/token-filter/helpers/text-matching.js.map +1 -0
- package/cjs/form/combobox/Input/InputController.js +1 -1
- package/cjs/form/combobox/Input/InputController.js.map +1 -1
- package/cjs/form/file-upload/dropzone/FileUploadDropzone.js +1 -1
- package/cjs/form/file-upload/dropzone/FileUploadDropzone.js.map +1 -1
- package/cjs/tooltip/Tooltip.js +1 -1
- package/cjs/tooltip/Tooltip.js.map +1 -1
- package/cjs/utils/i18n/locales/nb.d.ts +75 -154
- package/cjs/utils/i18n/locales/nb.js +75 -154
- package/cjs/utils/i18n/locales/nb.js.map +1 -1
- package/esm/data/table/helpers/table-grid-nav.d.ts +9 -15
- package/esm/data/table/helpers/table-grid-nav.js +18 -25
- package/esm/data/table/helpers/table-grid-nav.js.map +1 -1
- package/esm/data/table/helpers/table-keyboard.d.ts +1 -1
- package/esm/data/table/helpers/table-keyboard.js +1 -6
- package/esm/data/table/helpers/table-keyboard.js.map +1 -1
- package/esm/data/table/root/DataTableRoot.d.ts +41 -4
- package/esm/data/table/root/DataTableRoot.js +10 -6
- package/esm/data/table/root/DataTableRoot.js.map +1 -1
- package/esm/data/table/root/useTableKeyboardNav.d.ts +1 -1
- package/esm/data/table/root/useTableKeyboardNav.js +32 -19
- package/esm/data/table/root/useTableKeyboardNav.js.map +1 -1
- package/esm/data/table/td/DataTableTd.d.ts +5 -4
- package/esm/data/table/td/DataTableTd.js +2 -2
- package/esm/data/table/td/DataTableTd.js.map +1 -1
- package/esm/data/token-filter/AutoSuggest.d.ts +9 -0
- package/esm/data/token-filter/AutoSuggest.js +20 -0
- package/esm/data/token-filter/AutoSuggest.js.map +1 -0
- package/esm/data/token-filter/AutoSuggest.types.d.ts +12 -0
- package/esm/data/token-filter/AutoSuggest.types.js +2 -0
- package/esm/data/token-filter/AutoSuggest.types.js.map +1 -0
- package/esm/data/token-filter/TokenFilter.d.ts +11 -0
- package/esm/data/token-filter/TokenFilter.js +66 -0
- package/esm/data/token-filter/TokenFilter.js.map +1 -0
- package/esm/data/token-filter/TokenFilter.types.d.ts +52 -0
- package/esm/data/token-filter/TokenFilter.types.js +2 -0
- package/esm/data/token-filter/TokenFilter.types.js.map +1 -0
- package/esm/data/token-filter/helpers/generate-autocomplete-options.d.ts +24 -0
- package/esm/data/token-filter/helpers/generate-autocomplete-options.js +195 -0
- package/esm/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -0
- package/esm/data/token-filter/helpers/grouping.d.ts +28 -0
- package/esm/data/token-filter/helpers/grouping.js +59 -0
- package/esm/data/token-filter/helpers/grouping.js.map +1 -0
- package/esm/data/token-filter/helpers/operators.d.ts +22 -0
- package/esm/data/token-filter/helpers/operators.js +60 -0
- package/esm/data/token-filter/helpers/operators.js.map +1 -0
- package/esm/data/token-filter/helpers/parse-query-text.d.ts +25 -0
- package/esm/data/token-filter/helpers/parse-query-text.js +44 -0
- package/esm/data/token-filter/helpers/parse-query-text.js.map +1 -0
- package/esm/data/token-filter/helpers/query-builder.d.ts +20 -0
- package/esm/data/token-filter/helpers/query-builder.js +34 -0
- package/esm/data/token-filter/helpers/query-builder.js.map +1 -0
- package/esm/data/token-filter/helpers/text-matching.d.ts +16 -0
- package/esm/data/token-filter/helpers/text-matching.js +45 -0
- package/esm/data/token-filter/helpers/text-matching.js.map +1 -0
- package/esm/form/combobox/Input/InputController.js +1 -1
- package/esm/form/combobox/Input/InputController.js.map +1 -1
- package/esm/form/file-upload/dropzone/FileUploadDropzone.js +1 -1
- package/esm/form/file-upload/dropzone/FileUploadDropzone.js.map +1 -1
- package/esm/tooltip/Tooltip.js +2 -2
- package/esm/tooltip/Tooltip.js.map +1 -1
- package/esm/utils/i18n/locales/nb.d.ts +75 -154
- package/esm/utils/i18n/locales/nb.js +75 -154
- package/esm/utils/i18n/locales/nb.js.map +1 -1
- package/package.json +3 -3
- package/src/data/table/helpers/table-grid-nav.test.ts +659 -0
- package/src/data/table/helpers/table-grid-nav.ts +19 -38
- package/src/data/table/helpers/table-keyboard.ts +1 -10
- package/src/data/table/root/DataTableRoot.tsx +50 -10
- package/src/data/table/root/useTableKeyboardNav.ts +35 -23
- package/src/data/table/td/DataTableTd.tsx +13 -6
- package/src/data/token-filter/AutoSuggest.tsx +55 -0
- package/src/data/token-filter/AutoSuggest.types.ts +14 -0
- package/src/data/token-filter/TokenFilter.tsx +129 -0
- package/src/data/token-filter/TokenFilter.types.ts +85 -0
- package/src/data/token-filter/helpers/generate-autocomplete-options.test.ts +896 -0
- package/src/data/token-filter/helpers/generate-autocomplete-options.ts +289 -0
- package/src/data/token-filter/helpers/grouping.test.ts +206 -0
- package/src/data/token-filter/helpers/grouping.ts +73 -0
- package/src/data/token-filter/helpers/operators.test.ts +281 -0
- package/src/data/token-filter/helpers/operators.ts +91 -0
- package/src/data/token-filter/helpers/parse-query-text.test.ts +201 -0
- package/src/data/token-filter/helpers/parse-query-text.ts +86 -0
- package/src/data/token-filter/helpers/query-builder.test.ts +126 -0
- package/src/data/token-filter/helpers/query-builder.ts +41 -0
- package/src/data/token-filter/helpers/text-matching.test.ts +125 -0
- package/src/data/token-filter/helpers/text-matching.ts +58 -0
- package/src/form/combobox/Input/InputController.tsx +0 -1
- package/src/form/file-upload/dropzone/FileUploadDropzone.tsx +0 -1
- package/src/tooltip/Tooltip.tsx +3 -3
- package/src/utils/i18n/locales/nb.ts +4 -83
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { ParsedProperty } from "../TokenFilter.types";
|
|
3
|
+
import {
|
|
4
|
+
QUERY_OPERATORS,
|
|
5
|
+
matchFilteringProperty,
|
|
6
|
+
matchOperator,
|
|
7
|
+
matchOperatorPrefix,
|
|
8
|
+
} from "./operators";
|
|
9
|
+
|
|
10
|
+
describe("QUERY_OPERATORS", () => {
|
|
11
|
+
test("should return QUERY_OPERATORS in specificity order", () => {
|
|
12
|
+
expect(QUERY_OPERATORS[0]).toBe(">=");
|
|
13
|
+
expect(QUERY_OPERATORS[1]).toBe("<=");
|
|
14
|
+
expect(QUERY_OPERATORS[2]).toBe("!=");
|
|
15
|
+
expect(QUERY_OPERATORS[3]).toBe("!:");
|
|
16
|
+
expect(QUERY_OPERATORS[4]).toBe("!^");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("should have all required QUERY_OPERATORS", () => {
|
|
20
|
+
const requiredOperators = [
|
|
21
|
+
"=",
|
|
22
|
+
"!=",
|
|
23
|
+
":",
|
|
24
|
+
"!:",
|
|
25
|
+
"^",
|
|
26
|
+
"!^",
|
|
27
|
+
">=",
|
|
28
|
+
"<=",
|
|
29
|
+
"<",
|
|
30
|
+
">",
|
|
31
|
+
];
|
|
32
|
+
requiredOperators.forEach((op) => {
|
|
33
|
+
expect(QUERY_OPERATORS).toContain(op);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("matchOperator", () => {
|
|
39
|
+
test("should match exact operator", () => {
|
|
40
|
+
const result = matchOperator(QUERY_OPERATORS, "=value");
|
|
41
|
+
expect(result).toBe("=");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("should match >= operator before > operator (specificity)", () => {
|
|
45
|
+
const result = matchOperator(QUERY_OPERATORS, ">=value");
|
|
46
|
+
expect(result).toBe(">=");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("should match <= operator before < operator (specificity)", () => {
|
|
50
|
+
const result = matchOperator(QUERY_OPERATORS, "<=value");
|
|
51
|
+
expect(result).toBe("<=");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("should match != operator before = operator", () => {
|
|
55
|
+
const result = matchOperator(QUERY_OPERATORS, "!=value");
|
|
56
|
+
expect(result).toBe("!=");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("should match !: operator", () => {
|
|
60
|
+
const result = matchOperator(QUERY_OPERATORS, "!:value");
|
|
61
|
+
expect(result).toBe("!:");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("should match !^ operator", () => {
|
|
65
|
+
const result = matchOperator(QUERY_OPERATORS, "!^value");
|
|
66
|
+
expect(result).toBe("!^");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("should match : operator", () => {
|
|
70
|
+
const result = matchOperator(QUERY_OPERATORS, ":value");
|
|
71
|
+
expect(result).toBe(":");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("should match ^ operator", () => {
|
|
75
|
+
const result = matchOperator(QUERY_OPERATORS, "^value");
|
|
76
|
+
expect(result).toBe("^");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("should match > operator", () => {
|
|
80
|
+
const result = matchOperator(QUERY_OPERATORS, ">value");
|
|
81
|
+
expect(result).toBe(">");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("should match < operator", () => {
|
|
85
|
+
const result = matchOperator(QUERY_OPERATORS, "<value");
|
|
86
|
+
expect(result).toBe("<");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("should match operator case-insensitively", () => {
|
|
90
|
+
const result = matchOperator(QUERY_OPERATORS, "=value");
|
|
91
|
+
expect(result).toBe("=");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("should return undefined when no operator matches", () => {
|
|
95
|
+
const result = matchOperator(QUERY_OPERATORS, "somevalue");
|
|
96
|
+
expect(result).toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("should return undefined for empty input", () => {
|
|
100
|
+
const result = matchOperator(QUERY_OPERATORS, "");
|
|
101
|
+
expect(result).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("should match operator from custom operator list", () => {
|
|
105
|
+
const customOperators = ["=", "!="] as const;
|
|
106
|
+
const result = matchOperator(customOperators as any, "!=value");
|
|
107
|
+
expect(result).toBe("!=");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("matchFilteringProperty", () => {
|
|
112
|
+
const properties: ParsedProperty[] = [
|
|
113
|
+
{
|
|
114
|
+
propertyKey: "status",
|
|
115
|
+
propertyLabel: "Status",
|
|
116
|
+
groupValuesLabel: "",
|
|
117
|
+
propertyGroup: "testgroup",
|
|
118
|
+
externalProperty: {} as any,
|
|
119
|
+
operators: [],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
propertyKey: "hostname",
|
|
123
|
+
propertyLabel: "Hostname",
|
|
124
|
+
groupValuesLabel: "",
|
|
125
|
+
propertyGroup: "testgroup",
|
|
126
|
+
externalProperty: {} as any,
|
|
127
|
+
operators: [],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
propertyKey: "instance-id",
|
|
131
|
+
propertyLabel: "Instance ID",
|
|
132
|
+
groupValuesLabel: "",
|
|
133
|
+
propertyGroup: "testgroup",
|
|
134
|
+
externalProperty: {} as any,
|
|
135
|
+
operators: [],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
propertyKey: "region",
|
|
139
|
+
propertyLabel: "Region",
|
|
140
|
+
groupValuesLabel: "",
|
|
141
|
+
propertyGroup: "testgroup",
|
|
142
|
+
externalProperty: {} as any,
|
|
143
|
+
operators: [],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
propertyKey: "availability-zone",
|
|
147
|
+
propertyLabel: "Availability Zone",
|
|
148
|
+
groupValuesLabel: "",
|
|
149
|
+
propertyGroup: "testgroup",
|
|
150
|
+
externalProperty: {} as any,
|
|
151
|
+
operators: [],
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
test("should match a basic property", () => {
|
|
156
|
+
const result = matchFilteringProperty(properties, "Status");
|
|
157
|
+
expect(result?.propertyKey).toBe("status");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("should match property case-insensitively", () => {
|
|
161
|
+
const result = matchFilteringProperty(properties, "status");
|
|
162
|
+
expect(result?.propertyKey).toBe("status");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("should match property with mixed casing", () => {
|
|
166
|
+
const result = matchFilteringProperty(properties, "sTaTuS");
|
|
167
|
+
expect(result?.propertyKey).toBe("status");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("should use longest matching property when properties overlap", () => {
|
|
171
|
+
const result = matchFilteringProperty(properties, "Instance ID");
|
|
172
|
+
expect(result?.propertyKey).toBe("instance-id");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("should prefer longest property: Availability Zone vs Zone", () => {
|
|
176
|
+
const result = matchFilteringProperty(properties, "Availability Zone");
|
|
177
|
+
expect(result?.propertyKey).toBe("availability-zone");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("should return undefined when no property matches", () => {
|
|
181
|
+
const result = matchFilteringProperty(properties, "NonExistentProperty");
|
|
182
|
+
expect(result).toBeUndefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("should return undefined for empty input", () => {
|
|
186
|
+
const result = matchFilteringProperty(properties, "");
|
|
187
|
+
expect(result).toBeUndefined();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("should match property from partial input", () => {
|
|
191
|
+
const result = matchFilteringProperty(properties, "Status=value");
|
|
192
|
+
expect(result?.propertyKey).toBe("status");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("should match property with spaces in label", () => {
|
|
196
|
+
const result = matchFilteringProperty(properties, "Instance ID:value");
|
|
197
|
+
expect(result?.propertyKey).toBe("instance-id");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("matchOperatorPrefix", () => {
|
|
202
|
+
test("should return empty string when input is empty", () => {
|
|
203
|
+
const result = matchOperatorPrefix(QUERY_OPERATORS, "");
|
|
204
|
+
expect(result).toBe("");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("should return empty string when input is whitespace only", () => {
|
|
208
|
+
const result = matchOperatorPrefix(QUERY_OPERATORS, " ");
|
|
209
|
+
expect(result).toBe("");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("should match exact operator as prefix", () => {
|
|
213
|
+
const result = matchOperatorPrefix(QUERY_OPERATORS, "=");
|
|
214
|
+
expect(result).toBe("=");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("should match incomplete >= operator as < prefix", () => {
|
|
218
|
+
const result = matchOperatorPrefix(QUERY_OPERATORS, "<");
|
|
219
|
+
expect(result).toBe("<");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("should match incomplete >= operator as = prefix", () => {
|
|
223
|
+
const result = matchOperatorPrefix(QUERY_OPERATORS, ">");
|
|
224
|
+
expect(result).toBe(">");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("should match !: operator prefix", () => {
|
|
228
|
+
const result = matchOperatorPrefix(QUERY_OPERATORS, "!");
|
|
229
|
+
expect(result).toBe("!");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("should match !: as operator prefix", () => {
|
|
233
|
+
const result = matchOperatorPrefix(QUERY_OPERATORS, "!:");
|
|
234
|
+
expect(result).toBe("!:");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("should match !^ as operator prefix", () => {
|
|
238
|
+
const result = matchOperatorPrefix(QUERY_OPERATORS, "!^");
|
|
239
|
+
expect(result).toBe("!^");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("should match : operator prefix", () => {
|
|
243
|
+
const result = matchOperatorPrefix(QUERY_OPERATORS, ":");
|
|
244
|
+
expect(result).toBe(":");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("should match ^ operator prefix", () => {
|
|
248
|
+
const result = matchOperatorPrefix(QUERY_OPERATORS, "^");
|
|
249
|
+
expect(result).toBe("^");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("should return null for invalid operator prefix", () => {
|
|
253
|
+
const result = matchOperatorPrefix(QUERY_OPERATORS, "@");
|
|
254
|
+
expect(result).toBeNull();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("should return null for completely invalid prefix", () => {
|
|
258
|
+
const result = matchOperatorPrefix(QUERY_OPERATORS, "xyz");
|
|
259
|
+
expect(result).toBeNull();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("should trim whitespace before checking prefix", () => {
|
|
263
|
+
const result = matchOperatorPrefix(QUERY_OPERATORS, " =");
|
|
264
|
+
expect(result).toBe("=");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("should handle case-insensitive prefix matching", () => {
|
|
268
|
+
const result = matchOperatorPrefix(QUERY_OPERATORS, "!");
|
|
269
|
+
expect(result).toBe("!");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("should match >= as valid prefix", () => {
|
|
273
|
+
const result = matchOperatorPrefix(QUERY_OPERATORS, ">");
|
|
274
|
+
expect(result).toBe(">");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("should match >= as valid prefix (incomplete)", () => {
|
|
278
|
+
const result = matchOperatorPrefix(QUERY_OPERATORS, ">");
|
|
279
|
+
expect(result).toBe(">");
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ParsedProperty, QueryFilterOperator } from "../TokenFilter.types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Operators ordered by specificity (longest/most specific first)
|
|
5
|
+
* This ensures longer operators like ">=" and "<=" are matched
|
|
6
|
+
* before shorter ones like ">" and "<"
|
|
7
|
+
*/
|
|
8
|
+
const Operators: Record<QueryFilterOperator, null> = {
|
|
9
|
+
">=": null,
|
|
10
|
+
"<=": null,
|
|
11
|
+
"!=": null,
|
|
12
|
+
"!:": null,
|
|
13
|
+
"!^": null,
|
|
14
|
+
"=": null,
|
|
15
|
+
":": null,
|
|
16
|
+
"^": null,
|
|
17
|
+
">": null,
|
|
18
|
+
"<": null,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const QUERY_OPERATORS: QueryFilterOperator[] = Object.keys(Operators);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Match an operator from the input text.
|
|
25
|
+
* Operators are already sorted by specificity, so no re-sorting needed.
|
|
26
|
+
*/
|
|
27
|
+
function matchOperator(
|
|
28
|
+
allowedOperators: QueryFilterOperator[],
|
|
29
|
+
text: string,
|
|
30
|
+
): QueryFilterOperator | undefined {
|
|
31
|
+
return allowedOperators.find((operator) =>
|
|
32
|
+
text.toLowerCase().startsWith(operator.toLowerCase()),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Match a property from the input text by longest property label.
|
|
38
|
+
*
|
|
39
|
+
* properties: [{ propertyLabel: "Instance" }, { propertyLabel: "Instance ID" }]
|
|
40
|
+
* text = "Instance ID:"
|
|
41
|
+
*
|
|
42
|
+
* Result: { propertyLabel: "Instance ID" }
|
|
43
|
+
*/
|
|
44
|
+
function matchFilteringProperty(
|
|
45
|
+
filteringProperties: ParsedProperty[],
|
|
46
|
+
text: string,
|
|
47
|
+
): ParsedProperty | undefined {
|
|
48
|
+
const lowerText = text.toLowerCase();
|
|
49
|
+
let bestMatch: ParsedProperty | undefined;
|
|
50
|
+
|
|
51
|
+
for (const prop of filteringProperties) {
|
|
52
|
+
if (lowerText.startsWith(prop.propertyLabel.toLowerCase())) {
|
|
53
|
+
if (
|
|
54
|
+
!bestMatch ||
|
|
55
|
+
prop.propertyLabel.length > bestMatch.propertyLabel.length
|
|
56
|
+
) {
|
|
57
|
+
bestMatch = prop;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return bestMatch;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if the input text is a valid prefix of any allowed operator.
|
|
67
|
+
* Returns the prefix if valid, null otherwise.
|
|
68
|
+
*/
|
|
69
|
+
function matchOperatorPrefix(
|
|
70
|
+
allowedOperators: QueryFilterOperator[],
|
|
71
|
+
filteringText: string,
|
|
72
|
+
): string | null {
|
|
73
|
+
const trimmedText = filteringText.trim();
|
|
74
|
+
|
|
75
|
+
if (trimmedText.length === 0) {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const isValidPrefix = allowedOperators.some((operator) =>
|
|
80
|
+
operator.toLowerCase().startsWith(trimmedText.toLowerCase()),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return isValidPrefix ? trimmedText : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
QUERY_OPERATORS,
|
|
88
|
+
matchOperator,
|
|
89
|
+
matchFilteringProperty,
|
|
90
|
+
matchOperatorPrefix,
|
|
91
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type {
|
|
3
|
+
ParsedProperty,
|
|
4
|
+
QueryFilteringProperty,
|
|
5
|
+
} from "../TokenFilter.types";
|
|
6
|
+
import { parseQueryText } from "./parse-query-text";
|
|
7
|
+
import type { ParsedText } from "./parse-query-text";
|
|
8
|
+
|
|
9
|
+
const properties: QueryFilteringProperty[] = [
|
|
10
|
+
{
|
|
11
|
+
groupValuesLabel: "",
|
|
12
|
+
group: "testgroup",
|
|
13
|
+
key: "status",
|
|
14
|
+
propertyLabel: "Status",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
groupValuesLabel: "",
|
|
18
|
+
group: "testgroup",
|
|
19
|
+
key: "hostname",
|
|
20
|
+
propertyLabel: "Hostname",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
groupValuesLabel: "",
|
|
24
|
+
group: "testgroup",
|
|
25
|
+
key: "instance-id",
|
|
26
|
+
propertyLabel: "Instance ID",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
groupValuesLabel: "",
|
|
30
|
+
group: "testgroup",
|
|
31
|
+
key: "region",
|
|
32
|
+
propertyLabel: "Region",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
groupValuesLabel: "",
|
|
36
|
+
group: "testgroup",
|
|
37
|
+
key: "availability-zone",
|
|
38
|
+
propertyLabel: "Availability Zone",
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const parsedProperties: ParsedProperty[] = properties.map((prop) => ({
|
|
43
|
+
propertyKey: prop.key,
|
|
44
|
+
propertyLabel: prop.propertyLabel,
|
|
45
|
+
groupValuesLabel: prop.groupValuesLabel ?? "",
|
|
46
|
+
propertyGroup: prop.group ?? "",
|
|
47
|
+
externalProperty: prop,
|
|
48
|
+
operators: prop.operators ?? [],
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
describe("parseQueryText", () => {
|
|
52
|
+
describe("value extraction", () => {
|
|
53
|
+
test("should extract value after operator", () => {
|
|
54
|
+
const result = parseQueryText("Status=active", parsedProperties);
|
|
55
|
+
expect(result.step).toBe("property");
|
|
56
|
+
const propertyResult = result as Extract<
|
|
57
|
+
ParsedText,
|
|
58
|
+
{ step: "property" }
|
|
59
|
+
>;
|
|
60
|
+
expect(propertyResult.value).toBe("active");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("should trim whitespace after operator", () => {
|
|
64
|
+
const result = parseQueryText("Status= active", parsedProperties);
|
|
65
|
+
expect(result.step).toBe("property");
|
|
66
|
+
const propertyResult = result as Extract<
|
|
67
|
+
ParsedText,
|
|
68
|
+
{ step: "property" }
|
|
69
|
+
>;
|
|
70
|
+
expect(propertyResult.value).toBe("active");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("should handle empty value", () => {
|
|
74
|
+
const result = parseQueryText("Status=", parsedProperties);
|
|
75
|
+
expect(result.step).toBe("property");
|
|
76
|
+
const propertyResult = result as Extract<
|
|
77
|
+
ParsedText,
|
|
78
|
+
{ step: "property" }
|
|
79
|
+
>;
|
|
80
|
+
expect(propertyResult.value).toBe("");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("should handle whitespace-only value", () => {
|
|
84
|
+
const result = parseQueryText("Status= ", parsedProperties);
|
|
85
|
+
expect(result.step).toBe("property");
|
|
86
|
+
const propertyResult = result as Extract<
|
|
87
|
+
ParsedText,
|
|
88
|
+
{ step: "property" }
|
|
89
|
+
>;
|
|
90
|
+
expect(propertyResult.value).toBe("");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("should preserve value content including spaces", () => {
|
|
94
|
+
const result = parseQueryText(
|
|
95
|
+
"Status=active and running",
|
|
96
|
+
parsedProperties,
|
|
97
|
+
);
|
|
98
|
+
expect(result.step).toBe("property");
|
|
99
|
+
const propertyResult = result as Extract<
|
|
100
|
+
ParsedText,
|
|
101
|
+
{ step: "property" }
|
|
102
|
+
>;
|
|
103
|
+
expect(propertyResult.value).toBe("active and running");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("should extract multi-part property value", () => {
|
|
107
|
+
const result = parseQueryText("Instance ID=server-123", parsedProperties);
|
|
108
|
+
expect(result.step).toBe("property");
|
|
109
|
+
const propertyResult = result as Extract<
|
|
110
|
+
ParsedText,
|
|
111
|
+
{ step: "property" }
|
|
112
|
+
>;
|
|
113
|
+
expect(propertyResult.value).toBe("server-123");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("whitespace handling", () => {
|
|
118
|
+
test("should handle whitespace between property and operator", () => {
|
|
119
|
+
const result = parseQueryText("Status =value", parsedProperties);
|
|
120
|
+
expect(result.step).toBe("property");
|
|
121
|
+
const propertyResult = result as Extract<
|
|
122
|
+
ParsedText,
|
|
123
|
+
{ step: "property" }
|
|
124
|
+
>;
|
|
125
|
+
expect(propertyResult.operator).toBe("=");
|
|
126
|
+
expect(propertyResult.value).toBe("value");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("should handle whitespace after property but before operator", () => {
|
|
130
|
+
const result = parseQueryText("Status >value", parsedProperties);
|
|
131
|
+
expect(result.step).toBe("property");
|
|
132
|
+
const propertyResult = result as Extract<
|
|
133
|
+
ParsedText,
|
|
134
|
+
{ step: "property" }
|
|
135
|
+
>;
|
|
136
|
+
expect(propertyResult.operator).toBe(">");
|
|
137
|
+
expect(propertyResult.value).toBe("value");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("should trim operator prefix whitespace", () => {
|
|
141
|
+
const result = parseQueryText("Status ", parsedProperties);
|
|
142
|
+
expect(result.step).toBe("operator");
|
|
143
|
+
const operatorResult = result as Extract<
|
|
144
|
+
ParsedText,
|
|
145
|
+
{ step: "operator" }
|
|
146
|
+
>;
|
|
147
|
+
expect(operatorResult.operatorPrefix).toBe("");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("edge cases", () => {
|
|
152
|
+
test("should handle empty input", () => {
|
|
153
|
+
const result = parseQueryText("", parsedProperties);
|
|
154
|
+
expect(result.step).toBe("free-text");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("should handle whitespace-only input", () => {
|
|
158
|
+
const result = parseQueryText(" ", parsedProperties);
|
|
159
|
+
expect(result.step).toBe("free-text");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("should handle property-only input", () => {
|
|
163
|
+
const result = parseQueryText("Status", parsedProperties);
|
|
164
|
+
expect(result.step).toBe("operator");
|
|
165
|
+
const operatorResult = result as Extract<
|
|
166
|
+
ParsedText,
|
|
167
|
+
{ step: "operator" }
|
|
168
|
+
>;
|
|
169
|
+
expect(operatorResult.operatorPrefix).toBe("");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("should handle property with spaces", () => {
|
|
173
|
+
const result = parseQueryText("Instance ID:value", parsedProperties);
|
|
174
|
+
expect(result.step).toBe("property");
|
|
175
|
+
const propertyResult = result as Extract<
|
|
176
|
+
ParsedText,
|
|
177
|
+
{ step: "property" }
|
|
178
|
+
>;
|
|
179
|
+
expect(propertyResult.property.propertyKey).toBe("instance-id");
|
|
180
|
+
expect(propertyResult.operator).toBe(":");
|
|
181
|
+
expect(propertyResult.value).toBe("value");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("free-text fallback", () => {
|
|
186
|
+
test("should fallback to free-text when no property is matched", () => {
|
|
187
|
+
const result = parseQueryText("Random text", parsedProperties);
|
|
188
|
+
expect(result.step).toBe("free-text");
|
|
189
|
+
const freeTextResult = result as Extract<
|
|
190
|
+
ParsedText,
|
|
191
|
+
{ step: "free-text" }
|
|
192
|
+
>;
|
|
193
|
+
expect(freeTextResult.value).toBe("Random text");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("should fallback to free-text with partial operator match that is invalid", () => {
|
|
197
|
+
const result = parseQueryText("Status@somevalue", parsedProperties);
|
|
198
|
+
expect(result.step).toBe("free-text");
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ParsedProperty, QueryFilterOperator } from "../TokenFilter.types";
|
|
2
|
+
import {
|
|
3
|
+
QUERY_OPERATORS,
|
|
4
|
+
matchFilteringProperty,
|
|
5
|
+
matchOperator,
|
|
6
|
+
matchOperatorPrefix,
|
|
7
|
+
} from "./operators";
|
|
8
|
+
|
|
9
|
+
type ParsedText =
|
|
10
|
+
| {
|
|
11
|
+
/** User has typed property + complete operator + value (e.g., "Status != active") */
|
|
12
|
+
step: "property";
|
|
13
|
+
property: ParsedProperty;
|
|
14
|
+
operator: QueryFilterOperator;
|
|
15
|
+
value: string;
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
/** User is typing the operator after property (e.g., "Status !") */
|
|
19
|
+
step: "operator";
|
|
20
|
+
property: ParsedProperty;
|
|
21
|
+
operatorPrefix: string;
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
/** No property match; treat as free-text search */
|
|
25
|
+
step: "free-text";
|
|
26
|
+
value: string;
|
|
27
|
+
operator?: QueryFilterOperator;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse user input text to extract property, operator, and value components.
|
|
32
|
+
* Handles partial input (e.g., user typing "Status !" to complete the operator).
|
|
33
|
+
*/
|
|
34
|
+
function parseQueryText(
|
|
35
|
+
filteringText: string,
|
|
36
|
+
filteringProperties: ParsedProperty[],
|
|
37
|
+
): ParsedText {
|
|
38
|
+
const property = matchFilteringProperty(filteringProperties, filteringText);
|
|
39
|
+
if (!property) {
|
|
40
|
+
const freeTextOperator = matchOperator(QUERY_OPERATORS, filteringText);
|
|
41
|
+
if (freeTextOperator) {
|
|
42
|
+
return {
|
|
43
|
+
step: "free-text",
|
|
44
|
+
operator: freeTextOperator,
|
|
45
|
+
value: filteringText.substring(freeTextOperator.length).trimStart(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
step: "free-text",
|
|
51
|
+
value: filteringText,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const textWithoutProperty = filteringText
|
|
56
|
+
.substring(property.propertyLabel.length)
|
|
57
|
+
.trimStart();
|
|
58
|
+
|
|
59
|
+
const operator = matchOperator(QUERY_OPERATORS, textWithoutProperty);
|
|
60
|
+
|
|
61
|
+
if (operator) {
|
|
62
|
+
return {
|
|
63
|
+
step: "property",
|
|
64
|
+
property,
|
|
65
|
+
operator,
|
|
66
|
+
value: textWithoutProperty.substring(operator.length).trimStart(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const operatorPrefix = matchOperatorPrefix(
|
|
71
|
+
QUERY_OPERATORS,
|
|
72
|
+
textWithoutProperty,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (operatorPrefix !== null) {
|
|
76
|
+
return { step: "operator", property, operatorPrefix };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
step: "free-text",
|
|
81
|
+
value: filteringText,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { parseQueryText };
|
|
86
|
+
export type { ParsedText };
|