@navikt/ds-react 8.5.1 → 8.6.0

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.
Files changed (102) hide show
  1. package/cjs/data/table/root/DataTableRoot.d.ts +27 -0
  2. package/cjs/data/table/root/DataTableRoot.js +8 -2
  3. package/cjs/data/table/root/DataTableRoot.js.map +1 -1
  4. package/cjs/data/table/td/DataTableTd.d.ts +5 -4
  5. package/cjs/data/table/td/DataTableTd.js +2 -2
  6. package/cjs/data/table/td/DataTableTd.js.map +1 -1
  7. package/cjs/data/token-filter/AutoSuggest.d.ts +2 -14
  8. package/cjs/data/token-filter/AutoSuggest.js +13 -89
  9. package/cjs/data/token-filter/AutoSuggest.js.map +1 -1
  10. package/cjs/data/token-filter/AutoSuggest.types.d.ts +11 -0
  11. package/cjs/data/token-filter/AutoSuggest.types.js +3 -0
  12. package/cjs/data/token-filter/AutoSuggest.types.js.map +1 -0
  13. package/cjs/data/token-filter/TokenFilter.d.ts +5 -0
  14. package/cjs/data/token-filter/TokenFilter.js +20 -10
  15. package/cjs/data/token-filter/TokenFilter.js.map +1 -1
  16. package/cjs/data/token-filter/TokenFilter.types.d.ts +8 -4
  17. package/cjs/data/token-filter/helpers/generate-autocomplete-options.d.ts +13 -61
  18. package/cjs/data/token-filter/helpers/generate-autocomplete-options.js +152 -135
  19. package/cjs/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -1
  20. package/cjs/data/token-filter/helpers/grouping.d.ts +28 -0
  21. package/cjs/data/token-filter/helpers/grouping.js +61 -0
  22. package/cjs/data/token-filter/helpers/grouping.js.map +1 -0
  23. package/cjs/data/token-filter/helpers/operators.d.ts +22 -0
  24. package/cjs/data/token-filter/helpers/operators.js +66 -0
  25. package/cjs/data/token-filter/helpers/operators.js.map +1 -0
  26. package/cjs/data/token-filter/helpers/parse-query-text.d.ts +1 -7
  27. package/cjs/data/token-filter/helpers/parse-query-text.js +5 -50
  28. package/cjs/data/token-filter/helpers/parse-query-text.js.map +1 -1
  29. package/cjs/data/token-filter/helpers/query-builder.d.ts +20 -0
  30. package/cjs/data/token-filter/helpers/query-builder.js +38 -0
  31. package/cjs/data/token-filter/helpers/query-builder.js.map +1 -0
  32. package/cjs/data/token-filter/helpers/text-matching.d.ts +16 -0
  33. package/cjs/data/token-filter/helpers/text-matching.js +47 -0
  34. package/cjs/data/token-filter/helpers/text-matching.js.map +1 -0
  35. package/cjs/form/combobox/Input/InputController.js +1 -1
  36. package/cjs/form/combobox/Input/InputController.js.map +1 -1
  37. package/cjs/form/file-upload/dropzone/FileUploadDropzone.js +1 -1
  38. package/cjs/form/file-upload/dropzone/FileUploadDropzone.js.map +1 -1
  39. package/cjs/toggle-group/useToggleGroup.js +5 -3
  40. package/cjs/toggle-group/useToggleGroup.js.map +1 -1
  41. package/esm/data/table/root/DataTableRoot.d.ts +27 -0
  42. package/esm/data/table/root/DataTableRoot.js +8 -2
  43. package/esm/data/table/root/DataTableRoot.js.map +1 -1
  44. package/esm/data/table/td/DataTableTd.d.ts +5 -4
  45. package/esm/data/table/td/DataTableTd.js +2 -2
  46. package/esm/data/table/td/DataTableTd.js.map +1 -1
  47. package/esm/data/token-filter/AutoSuggest.d.ts +2 -14
  48. package/esm/data/token-filter/AutoSuggest.js +14 -90
  49. package/esm/data/token-filter/AutoSuggest.js.map +1 -1
  50. package/esm/data/token-filter/AutoSuggest.types.d.ts +11 -0
  51. package/esm/data/token-filter/AutoSuggest.types.js +2 -0
  52. package/esm/data/token-filter/AutoSuggest.types.js.map +1 -0
  53. package/esm/data/token-filter/TokenFilter.d.ts +5 -0
  54. package/esm/data/token-filter/TokenFilter.js +20 -10
  55. package/esm/data/token-filter/TokenFilter.js.map +1 -1
  56. package/esm/data/token-filter/TokenFilter.types.d.ts +8 -4
  57. package/esm/data/token-filter/helpers/generate-autocomplete-options.d.ts +13 -61
  58. package/esm/data/token-filter/helpers/generate-autocomplete-options.js +152 -135
  59. package/esm/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -1
  60. package/esm/data/token-filter/helpers/grouping.d.ts +28 -0
  61. package/esm/data/token-filter/helpers/grouping.js +59 -0
  62. package/esm/data/token-filter/helpers/grouping.js.map +1 -0
  63. package/esm/data/token-filter/helpers/operators.d.ts +22 -0
  64. package/esm/data/token-filter/helpers/operators.js +60 -0
  65. package/esm/data/token-filter/helpers/operators.js.map +1 -0
  66. package/esm/data/token-filter/helpers/parse-query-text.d.ts +1 -7
  67. package/esm/data/token-filter/helpers/parse-query-text.js +2 -45
  68. package/esm/data/token-filter/helpers/parse-query-text.js.map +1 -1
  69. package/esm/data/token-filter/helpers/query-builder.d.ts +20 -0
  70. package/esm/data/token-filter/helpers/query-builder.js +34 -0
  71. package/esm/data/token-filter/helpers/query-builder.js.map +1 -0
  72. package/esm/data/token-filter/helpers/text-matching.d.ts +16 -0
  73. package/esm/data/token-filter/helpers/text-matching.js +45 -0
  74. package/esm/data/token-filter/helpers/text-matching.js.map +1 -0
  75. package/esm/form/combobox/Input/InputController.js +1 -1
  76. package/esm/form/combobox/Input/InputController.js.map +1 -1
  77. package/esm/form/file-upload/dropzone/FileUploadDropzone.js +1 -1
  78. package/esm/form/file-upload/dropzone/FileUploadDropzone.js.map +1 -1
  79. package/esm/toggle-group/useToggleGroup.js +6 -4
  80. package/esm/toggle-group/useToggleGroup.js.map +1 -1
  81. package/package.json +3 -3
  82. package/src/data/table/root/DataTableRoot.tsx +30 -1
  83. package/src/data/table/td/DataTableTd.tsx +13 -6
  84. package/src/data/token-filter/AutoSuggest.tsx +33 -163
  85. package/src/data/token-filter/AutoSuggest.types.ts +13 -0
  86. package/src/data/token-filter/TokenFilter.tsx +21 -13
  87. package/src/data/token-filter/TokenFilter.types.ts +8 -4
  88. package/src/data/token-filter/helpers/generate-autocomplete-options.test.ts +836 -0
  89. package/src/data/token-filter/helpers/generate-autocomplete-options.ts +241 -186
  90. package/src/data/token-filter/helpers/grouping.test.ts +206 -0
  91. package/src/data/token-filter/helpers/grouping.ts +73 -0
  92. package/src/data/token-filter/helpers/operators.test.ts +281 -0
  93. package/src/data/token-filter/helpers/operators.ts +91 -0
  94. package/src/data/token-filter/helpers/parse-query-text.test.ts +4 -213
  95. package/src/data/token-filter/helpers/parse-query-text.ts +7 -69
  96. package/src/data/token-filter/helpers/query-builder.test.ts +126 -0
  97. package/src/data/token-filter/helpers/query-builder.ts +41 -0
  98. package/src/data/token-filter/helpers/text-matching.test.ts +125 -0
  99. package/src/data/token-filter/helpers/text-matching.ts +58 -0
  100. package/src/form/combobox/Input/InputController.tsx +0 -1
  101. package/src/form/file-upload/dropzone/FileUploadDropzone.tsx +0 -1
  102. package/src/toggle-group/useToggleGroup.ts +6 -5
@@ -0,0 +1,73 @@
1
+ import type { OptionGroup } from "../AutoSuggest.types";
2
+
3
+ /**
4
+ * Groups items into labeled groups with support for a default group.
5
+ * Empty or whitespace-only group labels are treated as belonging to the default group.
6
+ *
7
+ * @returns Array of groups with non-empty options
8
+ *
9
+ * @example
10
+ * const properties = [
11
+ * { name: "Status", group: "Metadata" },
12
+ * { name: "Region", group: "Location" },
13
+ * { name: "Type", group: "" },
14
+ * ];
15
+ * const groups = createGroups(
16
+ * properties,
17
+ * (p) => p.group,
18
+ * "Properties"
19
+ * );
20
+ *
21
+ * Returns:
22
+ * [
23
+ * { label: "Metadata", options: [{ name: "Status", group: "Metadata" }] },
24
+ * { label: "Location", options: [{ name: "Region", group: "Location" }] },
25
+ * { label: "Properties", options: [{ name: "Type", group: "" }] }
26
+ * ]
27
+ */
28
+ function createGroups<T>(
29
+ items: T[],
30
+ getGroupLabel: (item: T) => string | undefined | null,
31
+ defaultGroupLabel = "Default",
32
+ ): OptionGroup<T>[] {
33
+ const defaultGroup: OptionGroup<T> = {
34
+ label: defaultGroupLabel,
35
+ options: [],
36
+ };
37
+ const customGroups: Record<string, OptionGroup<T>> = {};
38
+
39
+ for (const item of items) {
40
+ if (!item) {
41
+ continue;
42
+ }
43
+
44
+ const rawLabel = getGroupLabel(item);
45
+ const groupLabel = rawLabel?.trim();
46
+
47
+ /* Empty string after trim or falsy values go to default group */
48
+ if (!groupLabel) {
49
+ defaultGroup.options.push(item);
50
+ continue;
51
+ }
52
+
53
+ if (!customGroups[groupLabel]) {
54
+ customGroups[groupLabel] = {
55
+ label: groupLabel,
56
+ options: [],
57
+ };
58
+ }
59
+
60
+ customGroups[groupLabel].options.push(item);
61
+ }
62
+
63
+ /* Custom groups first, then default group if it has items */
64
+ const groups: OptionGroup<T>[] = [...Object.values(customGroups)];
65
+
66
+ if (defaultGroup.options.length > 0) {
67
+ groups.push(defaultGroup);
68
+ }
69
+
70
+ return groups;
71
+ }
72
+
73
+ export { createGroups };
@@ -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
+ };
@@ -3,7 +3,7 @@ import type {
3
3
  ParsedProperty,
4
4
  QueryFilteringProperty,
5
5
  } from "../TokenFilter.types";
6
- import { QUERY_OPERATORS, parseQueryText } from "./parse-query-text";
6
+ import { parseQueryText } from "./parse-query-text";
7
7
  import type { ParsedText } from "./parse-query-text";
8
8
 
9
9
  const properties: QueryFilteringProperty[] = [
@@ -42,192 +42,13 @@ const properties: QueryFilteringProperty[] = [
42
42
  const parsedProperties: ParsedProperty[] = properties.map((prop) => ({
43
43
  propertyKey: prop.key,
44
44
  propertyLabel: prop.propertyLabel,
45
- groupValuesLabel: prop.groupValuesLabel,
46
- propertyGroup: prop.group,
45
+ groupValuesLabel: prop.groupValuesLabel ?? "",
46
+ propertyGroup: prop.group ?? "",
47
47
  externalProperty: prop,
48
+ operators: prop.operators ?? [],
48
49
  }));
49
50
 
50
51
  describe("parseQueryText", () => {
51
- describe("property matching", () => {
52
- test("should match a basic property", () => {
53
- const result = parseQueryText("Status", parsedProperties);
54
- expect(result.step).toBe("operator");
55
- const operatorResult = result as Extract<
56
- ParsedText,
57
- { step: "operator" }
58
- >;
59
- expect(operatorResult.property.propertyKey).toBe("status");
60
- });
61
-
62
- test("should match property case-insensitively", () => {
63
- const result = parseQueryText("status", parsedProperties);
64
- expect(result.step).toBe("operator");
65
- const operatorResult = result as Extract<
66
- ParsedText,
67
- { step: "operator" }
68
- >;
69
- expect(operatorResult.property.propertyKey).toBe("status");
70
- });
71
-
72
- test("should match property with mixed casing", () => {
73
- const result = parseQueryText("sTaTuS", parsedProperties);
74
- expect(result.step).toBe("operator");
75
- const operatorResult = result as Extract<
76
- ParsedText,
77
- { step: "operator" }
78
- >;
79
- expect(operatorResult.property.propertyKey).toBe("status");
80
- });
81
-
82
- test("should use longest matching property when properties overlap", () => {
83
- const result = parseQueryText("Instance ID", parsedProperties);
84
- expect(result.step).toBe("operator");
85
- const operatorResult = result as Extract<
86
- ParsedText,
87
- { step: "operator" }
88
- >;
89
- expect(operatorResult.property.propertyKey).toBe("instance-id");
90
- });
91
-
92
- test("should use longest matching property: Availability Zone vs Zone", () => {
93
- const result = parseQueryText("Availability Zone", parsedProperties);
94
- expect(result.step).toBe("operator");
95
- const operatorResult = result as Extract<
96
- ParsedText,
97
- { step: "operator" }
98
- >;
99
- expect(operatorResult.property.propertyKey).toBe("availability-zone");
100
- });
101
-
102
- test("should return free-text when no property matches", () => {
103
- const result = parseQueryText("NonExistentProperty", parsedProperties);
104
- expect(result.step).toBe("free-text");
105
- });
106
- });
107
-
108
- describe("operator matching", () => {
109
- test("should match exact operator", () => {
110
- const result = parseQueryText("Status=value", parsedProperties);
111
- expect(result.step).toBe("property");
112
- const propertyResult = result as Extract<
113
- ParsedText,
114
- { step: "property" }
115
- >;
116
- expect(propertyResult.operator).toBe("=");
117
- expect(propertyResult.value).toBe("value");
118
- });
119
-
120
- test("should match >= operator before > operator (specificity)", () => {
121
- const result = parseQueryText("Status>=value", parsedProperties);
122
- expect(result.step).toBe("property");
123
- const propertyResult = result as Extract<
124
- ParsedText,
125
- { step: "property" }
126
- >;
127
- expect(propertyResult.operator).toBe(">=");
128
- expect(propertyResult.value).toBe("value");
129
- });
130
-
131
- test("should match <= operator before < operator (specificity)", () => {
132
- const result = parseQueryText("Status<=value", parsedProperties);
133
- expect(result.step).toBe("property");
134
- const propertyResult = result as Extract<
135
- ParsedText,
136
- { step: "property" }
137
- >;
138
- expect(propertyResult.operator).toBe("<=");
139
- expect(propertyResult.value).toBe("value");
140
- });
141
-
142
- test("should match != operator before = operator", () => {
143
- const result = parseQueryText("Status!=value", parsedProperties);
144
- expect(result.step).toBe("property");
145
- const propertyResult = result as Extract<
146
- ParsedText,
147
- { step: "property" }
148
- >;
149
- expect(propertyResult.operator).toBe("!=");
150
- expect(propertyResult.value).toBe("value");
151
- });
152
-
153
- test("should match !: operator", () => {
154
- const result = parseQueryText("Status!:value", parsedProperties);
155
- expect(result.step).toBe("property");
156
- const propertyResult = result as Extract<
157
- ParsedText,
158
- { step: "property" }
159
- >;
160
- expect(propertyResult.operator).toBe("!:");
161
- });
162
-
163
- test("should match !^ operator", () => {
164
- const result = parseQueryText("Status!^value", parsedProperties);
165
- expect(result.step).toBe("property");
166
- const propertyResult = result as Extract<
167
- ParsedText,
168
- { step: "property" }
169
- >;
170
- expect(propertyResult.operator).toBe("!^");
171
- });
172
-
173
- test("should match : operator", () => {
174
- const result = parseQueryText("Status:value", parsedProperties);
175
- expect(result.step).toBe("property");
176
- const propertyResult = result as Extract<
177
- ParsedText,
178
- { step: "property" }
179
- >;
180
- expect(propertyResult.operator).toBe(":");
181
- });
182
-
183
- test("should match ^ operator", () => {
184
- const result = parseQueryText("Status^value", parsedProperties);
185
- expect(result.step).toBe("property");
186
- const propertyResult = result as Extract<
187
- ParsedText,
188
- { step: "property" }
189
- >;
190
- expect(propertyResult.operator).toBe("^");
191
- });
192
-
193
- test("should match operator case-insensitively", () => {
194
- const result = parseQueryText("Status=value", parsedProperties);
195
- expect(result.step).toBe("property");
196
- const propertyResult = result as Extract<
197
- ParsedText,
198
- { step: "property" }
199
- >;
200
- expect(propertyResult.operator).toBe("=");
201
- });
202
- });
203
-
204
- describe("operator prefix matching", () => {
205
- test("should recognize incomplete <= operator as < operator", () => {
206
- const result = parseQueryText("Status<", parsedProperties);
207
- expect(result.step).toBe("property");
208
- const operatorResult = result as Extract<
209
- ParsedText,
210
- { step: "property" }
211
- >;
212
- expect(operatorResult.operator).toBe("<");
213
- });
214
-
215
- test("should recognize empty string after property as operator prefix", () => {
216
- const result = parseQueryText("Status", parsedProperties);
217
- expect(result.step).toBe("operator");
218
- const operatorResult = result as Extract<
219
- ParsedText,
220
- { step: "operator" }
221
- >;
222
- expect(operatorResult.operatorPrefix).toBe("");
223
- });
224
-
225
- test("should return free-text when invalid operator character is used", () => {
226
- const result = parseQueryText("Status@value", parsedProperties);
227
- expect(result.step).toBe("free-text");
228
- });
229
- });
230
-
231
52
  describe("value extraction", () => {
232
53
  test("should extract value after operator", () => {
233
54
  const result = parseQueryText("Status=active", parsedProperties);
@@ -361,36 +182,6 @@ describe("parseQueryText", () => {
361
182
  });
362
183
  });
363
184
 
364
- describe("QUERY_OPERATORS", () => {
365
- test("should return qUERY_OPERATORS in specificity order", () => {
366
- const qUERY_OPERATORS = QUERY_OPERATORS;
367
- expect(qUERY_OPERATORS[0]).toBe(">=");
368
- expect(qUERY_OPERATORS[1]).toBe("<=");
369
- expect(qUERY_OPERATORS[2]).toBe("!=");
370
- expect(qUERY_OPERATORS[3]).toBe("!:");
371
- expect(qUERY_OPERATORS[4]).toBe("!^");
372
- });
373
-
374
- test("should have all required qUERY_OPERATORS", () => {
375
- const qUERY_OPERATORS = QUERY_OPERATORS;
376
- const requiredQUERY_OPERATORS = [
377
- "=",
378
- "!=",
379
- ":",
380
- "!:",
381
- "^",
382
- "!^",
383
- ">=",
384
- "<=",
385
- "<",
386
- ">",
387
- ];
388
- requiredQUERY_OPERATORS.forEach((op) => {
389
- expect(qUERY_OPERATORS).toContain(op);
390
- });
391
- });
392
- });
393
-
394
185
  describe("free-text fallback", () => {
395
186
  test("should fallback to free-text when no property is matched", () => {
396
187
  const result = parseQueryText("Random text", parsedProperties);