@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.
- package/cjs/data/table/root/DataTableRoot.d.ts +27 -0
- package/cjs/data/table/root/DataTableRoot.js +8 -2
- package/cjs/data/table/root/DataTableRoot.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 +2 -14
- package/cjs/data/token-filter/AutoSuggest.js +13 -89
- package/cjs/data/token-filter/AutoSuggest.js.map +1 -1
- package/cjs/data/token-filter/AutoSuggest.types.d.ts +11 -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 +5 -0
- package/cjs/data/token-filter/TokenFilter.js +20 -10
- package/cjs/data/token-filter/TokenFilter.js.map +1 -1
- package/cjs/data/token-filter/TokenFilter.types.d.ts +8 -4
- package/cjs/data/token-filter/helpers/generate-autocomplete-options.d.ts +13 -61
- package/cjs/data/token-filter/helpers/generate-autocomplete-options.js +152 -135
- package/cjs/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -1
- 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 +1 -7
- package/cjs/data/token-filter/helpers/parse-query-text.js +5 -50
- package/cjs/data/token-filter/helpers/parse-query-text.js.map +1 -1
- 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/toggle-group/useToggleGroup.js +5 -3
- package/cjs/toggle-group/useToggleGroup.js.map +1 -1
- package/esm/data/table/root/DataTableRoot.d.ts +27 -0
- package/esm/data/table/root/DataTableRoot.js +8 -2
- package/esm/data/table/root/DataTableRoot.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 +2 -14
- package/esm/data/token-filter/AutoSuggest.js +14 -90
- package/esm/data/token-filter/AutoSuggest.js.map +1 -1
- package/esm/data/token-filter/AutoSuggest.types.d.ts +11 -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 +5 -0
- package/esm/data/token-filter/TokenFilter.js +20 -10
- package/esm/data/token-filter/TokenFilter.js.map +1 -1
- package/esm/data/token-filter/TokenFilter.types.d.ts +8 -4
- package/esm/data/token-filter/helpers/generate-autocomplete-options.d.ts +13 -61
- package/esm/data/token-filter/helpers/generate-autocomplete-options.js +152 -135
- package/esm/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -1
- 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 +1 -7
- package/esm/data/token-filter/helpers/parse-query-text.js +2 -45
- package/esm/data/token-filter/helpers/parse-query-text.js.map +1 -1
- 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/toggle-group/useToggleGroup.js +6 -4
- package/esm/toggle-group/useToggleGroup.js.map +1 -1
- package/package.json +3 -3
- package/src/data/table/root/DataTableRoot.tsx +30 -1
- package/src/data/table/td/DataTableTd.tsx +13 -6
- package/src/data/token-filter/AutoSuggest.tsx +33 -163
- package/src/data/token-filter/AutoSuggest.types.ts +13 -0
- package/src/data/token-filter/TokenFilter.tsx +21 -13
- package/src/data/token-filter/TokenFilter.types.ts +8 -4
- package/src/data/token-filter/helpers/generate-autocomplete-options.test.ts +836 -0
- package/src/data/token-filter/helpers/generate-autocomplete-options.ts +241 -186
- 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 +4 -213
- package/src/data/token-filter/helpers/parse-query-text.ts +7 -69
- 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/toggle-group/useToggleGroup.ts +6 -5
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { AutoCompleteOption } from "../AutoSuggest.types";
|
|
3
|
+
import type {
|
|
4
|
+
ParsedOption,
|
|
5
|
+
ParsedProperty,
|
|
6
|
+
QueryFilteringOption,
|
|
7
|
+
QueryFilteringProperty,
|
|
8
|
+
} from "../TokenFilter.types";
|
|
9
|
+
import { generateAutoCompleteOptions } from "./generate-autocomplete-options";
|
|
10
|
+
import type { ParsedText } from "./parse-query-text";
|
|
11
|
+
|
|
12
|
+
const properties: QueryFilteringProperty[] = [
|
|
13
|
+
{
|
|
14
|
+
groupValuesLabel: "Status values",
|
|
15
|
+
group: "Metadata",
|
|
16
|
+
key: "status",
|
|
17
|
+
propertyLabel: "Status",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
groupValuesLabel: "Region values",
|
|
21
|
+
group: "Location",
|
|
22
|
+
key: "region",
|
|
23
|
+
propertyLabel: "Region",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
groupValuesLabel: "Type values",
|
|
27
|
+
group: "",
|
|
28
|
+
key: "type",
|
|
29
|
+
propertyLabel: "Type",
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const parsedProperties: ParsedProperty[] = properties.map((prop) => ({
|
|
34
|
+
propertyKey: prop.key,
|
|
35
|
+
propertyLabel: prop.propertyLabel,
|
|
36
|
+
groupValuesLabel: prop.groupValuesLabel ?? "",
|
|
37
|
+
propertyGroup: prop.group ?? "",
|
|
38
|
+
externalProperty: prop,
|
|
39
|
+
operators: prop.operators ?? [],
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const statusOptions: QueryFilteringOption[] = [
|
|
43
|
+
{ propertyKey: "status", value: "active", label: "Active" },
|
|
44
|
+
{ propertyKey: "status", value: "pending", label: "Pending" },
|
|
45
|
+
{ propertyKey: "status", value: "inactive", label: "Inactive" },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const regionOptions: QueryFilteringOption[] = [
|
|
49
|
+
{
|
|
50
|
+
propertyKey: "region",
|
|
51
|
+
value: "us-east-1",
|
|
52
|
+
label: "US East",
|
|
53
|
+
tags: ["north america", "usa"],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
propertyKey: "region",
|
|
57
|
+
value: "eu-west-1",
|
|
58
|
+
label: "EU West",
|
|
59
|
+
tags: ["europe"],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const allOptions: QueryFilteringOption[] = [...statusOptions, ...regionOptions];
|
|
64
|
+
|
|
65
|
+
const parsedOptions: ParsedOption[] = allOptions.map((option) => {
|
|
66
|
+
const property = parsedProperties.find(
|
|
67
|
+
(p) => p.propertyKey === option.propertyKey,
|
|
68
|
+
);
|
|
69
|
+
return {
|
|
70
|
+
property: property || null,
|
|
71
|
+
value: option.value,
|
|
72
|
+
label: option.label || String(option.value),
|
|
73
|
+
tags: option.tags || [],
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("generateAutoCompleteOptions v2", () => {
|
|
78
|
+
describe("free-text step", () => {
|
|
79
|
+
test("empty value: should return all properties", () => {
|
|
80
|
+
const queryState: ParsedText = {
|
|
81
|
+
step: "free-text",
|
|
82
|
+
value: "",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const result = generateAutoCompleteOptions(
|
|
86
|
+
queryState,
|
|
87
|
+
parsedProperties,
|
|
88
|
+
parsedOptions,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(result.value).toBe("");
|
|
92
|
+
expect(result.options).toHaveLength(3);
|
|
93
|
+
expect(
|
|
94
|
+
result.options.find((g) => g.label === "Metadata")?.options,
|
|
95
|
+
).toHaveLength(1);
|
|
96
|
+
expect(
|
|
97
|
+
result.options.find((g) => g.label === "Location")?.options,
|
|
98
|
+
).toHaveLength(1);
|
|
99
|
+
expect(
|
|
100
|
+
result.options.find((g) => g.label === "Properties")?.options,
|
|
101
|
+
).toHaveLength(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("non-empty value: should return filtered properties and values", () => {
|
|
105
|
+
const queryState: ParsedText = {
|
|
106
|
+
step: "free-text",
|
|
107
|
+
value: "statu",
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const result = generateAutoCompleteOptions(
|
|
111
|
+
queryState,
|
|
112
|
+
parsedProperties,
|
|
113
|
+
parsedOptions,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(result.value).toBe("statu");
|
|
117
|
+
expect(result.options.length).toBeGreaterThan(0);
|
|
118
|
+
|
|
119
|
+
const valueGroup = result.options.find(
|
|
120
|
+
(g) => g.label === "Status values",
|
|
121
|
+
);
|
|
122
|
+
expect(valueGroup).toBeDefined();
|
|
123
|
+
expect(valueGroup?.options).toHaveLength(3);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("non-empty value with operator '!:': should skip property suggestions", () => {
|
|
127
|
+
const queryState: ParsedText = {
|
|
128
|
+
step: "free-text",
|
|
129
|
+
value: "test",
|
|
130
|
+
operator: "!:",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const result = generateAutoCompleteOptions(
|
|
134
|
+
queryState,
|
|
135
|
+
parsedProperties,
|
|
136
|
+
parsedOptions,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(result.value).toBe("test");
|
|
140
|
+
const hasPropertyGroups = result.options.some(
|
|
141
|
+
(g) => g.label === "Metadata" || g.label === "Location",
|
|
142
|
+
);
|
|
143
|
+
expect(hasPropertyGroups).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("non-empty value: case-insensitive filtering", () => {
|
|
147
|
+
const queryState: ParsedText = {
|
|
148
|
+
step: "free-text",
|
|
149
|
+
value: "STATU",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const result = generateAutoCompleteOptions(
|
|
153
|
+
queryState,
|
|
154
|
+
parsedProperties,
|
|
155
|
+
parsedOptions,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(result.value).toBe("STATU");
|
|
159
|
+
expect(result.options.length).toBeGreaterThan(0);
|
|
160
|
+
|
|
161
|
+
const valueGroup = result.options.find(
|
|
162
|
+
(g) => g.label === "Status values",
|
|
163
|
+
);
|
|
164
|
+
expect(valueGroup).toBeDefined();
|
|
165
|
+
expect(valueGroup?.options).toHaveLength(3);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("non-empty value: should match tags", () => {
|
|
169
|
+
const queryState: ParsedText = {
|
|
170
|
+
step: "free-text",
|
|
171
|
+
value: "europe",
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const result = generateAutoCompleteOptions(
|
|
175
|
+
queryState,
|
|
176
|
+
parsedProperties,
|
|
177
|
+
parsedOptions,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const valueGroup = result.options.find(
|
|
181
|
+
(g) => g.label === "Region values",
|
|
182
|
+
);
|
|
183
|
+
expect(valueGroup).toBeDefined();
|
|
184
|
+
expect(valueGroup?.options).toHaveLength(1);
|
|
185
|
+
expect((valueGroup?.options[0] as AutoCompleteOption).label).toBe(
|
|
186
|
+
"EU West",
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("non-empty value: whitespace-aware matching", () => {
|
|
191
|
+
const queryState: ParsedText = {
|
|
192
|
+
step: "free-text",
|
|
193
|
+
value: "us east",
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const result = generateAutoCompleteOptions(
|
|
197
|
+
queryState,
|
|
198
|
+
parsedProperties,
|
|
199
|
+
parsedOptions,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const valueGroup = result.options.find(
|
|
203
|
+
(g) => g.label === "Region values",
|
|
204
|
+
);
|
|
205
|
+
expect(valueGroup).toBeDefined();
|
|
206
|
+
expect(valueGroup?.options).toHaveLength(1);
|
|
207
|
+
expect((valueGroup?.options[0] as AutoCompleteOption).label).toBe(
|
|
208
|
+
"US East",
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("property step", () => {
|
|
214
|
+
const statusProperty = parsedProperties[0];
|
|
215
|
+
|
|
216
|
+
test("empty value: should return all values for selected property", () => {
|
|
217
|
+
const queryState: ParsedText = {
|
|
218
|
+
step: "property",
|
|
219
|
+
property: statusProperty,
|
|
220
|
+
operator: "=",
|
|
221
|
+
value: "",
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const result = generateAutoCompleteOptions(
|
|
225
|
+
queryState,
|
|
226
|
+
parsedProperties,
|
|
227
|
+
parsedOptions,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
expect(result.value).toBe("");
|
|
231
|
+
expect(result.options).toHaveLength(1);
|
|
232
|
+
expect(result.options[0].label).toBe("Status values");
|
|
233
|
+
expect(result.options[0].options).toHaveLength(3);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("non-empty value: should return filtered values", () => {
|
|
237
|
+
const queryState: ParsedText = {
|
|
238
|
+
step: "property",
|
|
239
|
+
property: statusProperty,
|
|
240
|
+
operator: "=",
|
|
241
|
+
value: "act",
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const result = generateAutoCompleteOptions(
|
|
245
|
+
queryState,
|
|
246
|
+
parsedProperties,
|
|
247
|
+
parsedOptions,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
expect(result.value).toBe("act");
|
|
251
|
+
expect(result.options).toHaveLength(1);
|
|
252
|
+
expect(result.options[0].options).toHaveLength(2);
|
|
253
|
+
const labels = result.options[0].options.map((o) => o.label);
|
|
254
|
+
expect(labels).toContain("Active");
|
|
255
|
+
expect(labels).toContain("Inactive");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("should use specified operator in value suggestions", () => {
|
|
259
|
+
const queryState: ParsedText = {
|
|
260
|
+
step: "property",
|
|
261
|
+
property: statusProperty,
|
|
262
|
+
operator: "!=",
|
|
263
|
+
value: "",
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const result = generateAutoCompleteOptions(
|
|
267
|
+
queryState,
|
|
268
|
+
parsedProperties,
|
|
269
|
+
parsedOptions,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
expect(
|
|
273
|
+
(result.options[0].options[0] as AutoCompleteOption).value,
|
|
274
|
+
).toContain("!=");
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe("operator step", () => {
|
|
279
|
+
const statusProperty = parsedProperties[0];
|
|
280
|
+
|
|
281
|
+
test("empty prefix: should return all operators", () => {
|
|
282
|
+
const queryState: ParsedText = {
|
|
283
|
+
step: "operator",
|
|
284
|
+
property: statusProperty,
|
|
285
|
+
operatorPrefix: "",
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const result = generateAutoCompleteOptions(
|
|
289
|
+
queryState,
|
|
290
|
+
parsedProperties,
|
|
291
|
+
parsedOptions,
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
expect(result.options).toHaveLength(1);
|
|
295
|
+
expect(result.options[0].label).toBe("Operators");
|
|
296
|
+
expect(result.options[0].options.length).toBe(10);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("operator prefix '!': should filter operators starting with '!'", () => {
|
|
300
|
+
const queryState: ParsedText = {
|
|
301
|
+
step: "operator",
|
|
302
|
+
property: statusProperty,
|
|
303
|
+
operatorPrefix: "!",
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const result = generateAutoCompleteOptions(
|
|
307
|
+
queryState,
|
|
308
|
+
parsedProperties,
|
|
309
|
+
parsedOptions,
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
expect(result.options).toHaveLength(1);
|
|
313
|
+
expect(result.options[0].label).toBe("Operators");
|
|
314
|
+
expect(result.options[0].options).toHaveLength(3);
|
|
315
|
+
const operators = result.options[0].options.map(
|
|
316
|
+
(o) => o.value.split(" ")[1],
|
|
317
|
+
);
|
|
318
|
+
expect(operators).toEqual(["!=", "!:", "!^"]);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("operator prefix '>=': should filter to single operator", () => {
|
|
322
|
+
const queryState: ParsedText = {
|
|
323
|
+
step: "operator",
|
|
324
|
+
property: statusProperty,
|
|
325
|
+
operatorPrefix: ">=",
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const result = generateAutoCompleteOptions(
|
|
329
|
+
queryState,
|
|
330
|
+
parsedProperties,
|
|
331
|
+
parsedOptions,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
expect(result.options).toHaveLength(1);
|
|
335
|
+
expect(result.options[0].options).toHaveLength(1);
|
|
336
|
+
expect(
|
|
337
|
+
(result.options[0].options[0] as AutoCompleteOption).description,
|
|
338
|
+
).toBe("is greater than or equal to");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("invalid prefix: should return empty suggestions", () => {
|
|
342
|
+
const queryState: ParsedText = {
|
|
343
|
+
step: "operator",
|
|
344
|
+
property: statusProperty,
|
|
345
|
+
operatorPrefix: "invalid",
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const result = generateAutoCompleteOptions(
|
|
349
|
+
queryState,
|
|
350
|
+
parsedProperties,
|
|
351
|
+
parsedOptions,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
expect(result.value).toBe("Status invalid");
|
|
355
|
+
expect(result.options).toEqual([]);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe("operator filtering by description", () => {
|
|
360
|
+
test("should filter operators by description text", () => {
|
|
361
|
+
const queryState: ParsedText = {
|
|
362
|
+
step: "operator",
|
|
363
|
+
property: parsedProperties[0],
|
|
364
|
+
operatorPrefix: "",
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const result = generateAutoCompleteOptions(
|
|
368
|
+
queryState,
|
|
369
|
+
parsedProperties,
|
|
370
|
+
parsedOptions,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const containsOp = result.options[0].options.find(
|
|
374
|
+
(o) => (o as AutoCompleteOption).description === "contains",
|
|
375
|
+
);
|
|
376
|
+
expect(containsOp).toBeDefined();
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("empty groups filtering", () => {
|
|
381
|
+
test("should not return empty option groups", () => {
|
|
382
|
+
const queryState: ParsedText = {
|
|
383
|
+
step: "free-text",
|
|
384
|
+
value: "nonexistent",
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const result = generateAutoCompleteOptions(
|
|
388
|
+
queryState,
|
|
389
|
+
parsedProperties,
|
|
390
|
+
parsedOptions,
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
expect(result.options.every((group) => group.options.length > 0)).toBe(
|
|
394
|
+
true,
|
|
395
|
+
);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe("value suggestions use correct operator", () => {
|
|
400
|
+
test("free-text step uses '=' operator for all values", () => {
|
|
401
|
+
const queryState: ParsedText = {
|
|
402
|
+
step: "free-text",
|
|
403
|
+
value: "active",
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const result = generateAutoCompleteOptions(
|
|
407
|
+
queryState,
|
|
408
|
+
parsedProperties,
|
|
409
|
+
parsedOptions,
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
const valueGroup = result.options.find(
|
|
413
|
+
(g) => g.label === "Status values",
|
|
414
|
+
);
|
|
415
|
+
expect(valueGroup).toBeDefined();
|
|
416
|
+
expect((valueGroup?.options[0] as AutoCompleteOption).value).toContain(
|
|
417
|
+
" = ",
|
|
418
|
+
);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("property step uses selected operator", () => {
|
|
422
|
+
const queryState: ParsedText = {
|
|
423
|
+
step: "property",
|
|
424
|
+
property: parsedProperties[0],
|
|
425
|
+
operator: "!:",
|
|
426
|
+
value: "",
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const result = generateAutoCompleteOptions(
|
|
430
|
+
queryState,
|
|
431
|
+
parsedProperties,
|
|
432
|
+
parsedOptions,
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
expect(
|
|
436
|
+
(result.options[0].options[0] as AutoCompleteOption).value,
|
|
437
|
+
).toContain(" !: ");
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
describe("custom operators configuration", () => {
|
|
442
|
+
test("property with string-format operators should filter to only those operators", () => {
|
|
443
|
+
const propertyWithCustomOps: ParsedProperty = {
|
|
444
|
+
propertyKey: "custom",
|
|
445
|
+
propertyLabel: "Custom",
|
|
446
|
+
groupValuesLabel: "Custom values",
|
|
447
|
+
propertyGroup: "Custom",
|
|
448
|
+
operators: ["=", "!="],
|
|
449
|
+
externalProperty: {
|
|
450
|
+
key: "custom",
|
|
451
|
+
propertyLabel: "Custom",
|
|
452
|
+
operators: ["=", "!="],
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const queryState: ParsedText = {
|
|
457
|
+
step: "operator",
|
|
458
|
+
property: propertyWithCustomOps,
|
|
459
|
+
operatorPrefix: "",
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const result = generateAutoCompleteOptions(
|
|
463
|
+
queryState,
|
|
464
|
+
[propertyWithCustomOps],
|
|
465
|
+
[],
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
expect(result.options[0].options).toHaveLength(2);
|
|
469
|
+
const operatorSymbols = result.options[0].options.map(
|
|
470
|
+
(o) => (o as AutoCompleteOption).value.split(" ")[1],
|
|
471
|
+
);
|
|
472
|
+
expect(operatorSymbols).toEqual(["=", "!="]);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("property with object-format operators should extract operator strings", () => {
|
|
476
|
+
const propertyWithObjectOps: ParsedProperty = {
|
|
477
|
+
propertyKey: "custom2",
|
|
478
|
+
propertyLabel: "Custom2",
|
|
479
|
+
groupValuesLabel: "Custom2 values",
|
|
480
|
+
propertyGroup: "Custom",
|
|
481
|
+
operators: [
|
|
482
|
+
{ operator: ":", tokenType: "single" },
|
|
483
|
+
{ operator: "!:", tokenType: "single" },
|
|
484
|
+
],
|
|
485
|
+
externalProperty: {
|
|
486
|
+
key: "custom2",
|
|
487
|
+
propertyLabel: "Custom2",
|
|
488
|
+
operators: [
|
|
489
|
+
{ operator: ":", tokenType: "single" },
|
|
490
|
+
{ operator: "!:", tokenType: "single" },
|
|
491
|
+
],
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const queryState: ParsedText = {
|
|
496
|
+
step: "operator",
|
|
497
|
+
property: propertyWithObjectOps,
|
|
498
|
+
operatorPrefix: "",
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const result = generateAutoCompleteOptions(
|
|
502
|
+
queryState,
|
|
503
|
+
[propertyWithObjectOps],
|
|
504
|
+
[],
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
expect(result.options[0].options).toHaveLength(2);
|
|
508
|
+
const operatorSymbols = result.options[0].options.map(
|
|
509
|
+
(o) => (o as AutoCompleteOption).value.split(" ")[1],
|
|
510
|
+
);
|
|
511
|
+
expect(operatorSymbols).toEqual([":", "!:"]);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("property with mixed operator formats should normalize and filter", () => {
|
|
515
|
+
const propertyWithMixedOps: ParsedProperty = {
|
|
516
|
+
propertyKey: "mixed",
|
|
517
|
+
propertyLabel: "Mixed",
|
|
518
|
+
groupValuesLabel: "Mixed values",
|
|
519
|
+
propertyGroup: "Custom",
|
|
520
|
+
operators: [
|
|
521
|
+
"=",
|
|
522
|
+
{ operator: "!=", tokenType: "single" },
|
|
523
|
+
"invalid-operator",
|
|
524
|
+
],
|
|
525
|
+
externalProperty: {
|
|
526
|
+
key: "mixed",
|
|
527
|
+
propertyLabel: "Mixed",
|
|
528
|
+
operators: [
|
|
529
|
+
"=",
|
|
530
|
+
{ operator: "!=", tokenType: "single" },
|
|
531
|
+
"invalid-operator",
|
|
532
|
+
],
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const queryState: ParsedText = {
|
|
537
|
+
step: "operator",
|
|
538
|
+
property: propertyWithMixedOps,
|
|
539
|
+
operatorPrefix: "",
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const result = generateAutoCompleteOptions(
|
|
543
|
+
queryState,
|
|
544
|
+
[propertyWithMixedOps],
|
|
545
|
+
[],
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
expect(result.options[0].options).toHaveLength(2);
|
|
549
|
+
const operatorSymbols = result.options[0].options.map(
|
|
550
|
+
(o) => (o as AutoCompleteOption).value.split(" ")[1],
|
|
551
|
+
);
|
|
552
|
+
expect(operatorSymbols).toEqual(["=", "!="]);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
describe("edge case: operator without value", () => {
|
|
557
|
+
test("should return empty options when operator is present but value is empty", () => {
|
|
558
|
+
const queryState: ParsedText = {
|
|
559
|
+
step: "free-text",
|
|
560
|
+
value: "",
|
|
561
|
+
operator: "!=",
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const result = generateAutoCompleteOptions(
|
|
565
|
+
queryState,
|
|
566
|
+
parsedProperties,
|
|
567
|
+
parsedOptions,
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
expect(result.value).toBe("");
|
|
571
|
+
expect(result.options).toEqual([]);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test("should return empty options when operator-start input has no value", () => {
|
|
575
|
+
const queryState: ParsedText = {
|
|
576
|
+
step: "free-text",
|
|
577
|
+
value: "",
|
|
578
|
+
operator: ":",
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const result = generateAutoCompleteOptions(
|
|
582
|
+
queryState,
|
|
583
|
+
parsedProperties,
|
|
584
|
+
parsedOptions,
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
expect(result.value).toBe("");
|
|
588
|
+
expect(result.options).toEqual([]);
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
describe("scoped vs non-scoped value suggestions", () => {
|
|
593
|
+
test("scoped to property should only show values from that property", () => {
|
|
594
|
+
const statusProperty = parsedProperties[0];
|
|
595
|
+
|
|
596
|
+
const queryState: ParsedText = {
|
|
597
|
+
step: "property",
|
|
598
|
+
property: statusProperty,
|
|
599
|
+
operator: "=",
|
|
600
|
+
value: "",
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const result = generateAutoCompleteOptions(
|
|
604
|
+
queryState,
|
|
605
|
+
parsedProperties,
|
|
606
|
+
parsedOptions,
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
expect(result.options).toHaveLength(1);
|
|
610
|
+
expect(result.options[0].label).toBe("Status values");
|
|
611
|
+
expect(result.options[0].options).toHaveLength(3);
|
|
612
|
+
expect(
|
|
613
|
+
(result.options[0].options[0] as AutoCompleteOption).value,
|
|
614
|
+
).toContain("Status");
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("non-scoped (free-text) with empty value should show all properties", () => {
|
|
618
|
+
const queryState: ParsedText = {
|
|
619
|
+
step: "free-text",
|
|
620
|
+
value: "",
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const result = generateAutoCompleteOptions(
|
|
624
|
+
queryState,
|
|
625
|
+
parsedProperties,
|
|
626
|
+
parsedOptions,
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
const propertyGroups = result.options.filter(
|
|
630
|
+
(g) => g.label === "Metadata" || g.label === "Location",
|
|
631
|
+
);
|
|
632
|
+
expect(propertyGroups.length).toBeGreaterThan(0);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test("scoped property search should not include property labels in search fields", () => {
|
|
636
|
+
const statusProperty = parsedProperties[0];
|
|
637
|
+
|
|
638
|
+
const queryState: ParsedText = {
|
|
639
|
+
step: "property",
|
|
640
|
+
property: statusProperty,
|
|
641
|
+
operator: "=",
|
|
642
|
+
value: "region",
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
const result = generateAutoCompleteOptions(
|
|
646
|
+
queryState,
|
|
647
|
+
parsedProperties,
|
|
648
|
+
parsedOptions,
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
expect(result.options).toHaveLength(0);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test("non-scoped search should include property labels in search", () => {
|
|
655
|
+
const queryState: ParsedText = {
|
|
656
|
+
step: "free-text",
|
|
657
|
+
value: "region",
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const result = generateAutoCompleteOptions(
|
|
661
|
+
queryState,
|
|
662
|
+
parsedProperties,
|
|
663
|
+
parsedOptions,
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
const _regionOptions = result.options.find(
|
|
667
|
+
(g) => g.label === "Region values",
|
|
668
|
+
);
|
|
669
|
+
expect(_regionOptions).toBeDefined();
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
describe("properties with no groupValuesLabel", () => {
|
|
674
|
+
test("should default to 'Values' when groupValuesLabel is empty", () => {
|
|
675
|
+
const propertyWithoutGroupLabel: ParsedProperty = {
|
|
676
|
+
propertyKey: "nogroup",
|
|
677
|
+
propertyLabel: "NoGroup",
|
|
678
|
+
groupValuesLabel: "",
|
|
679
|
+
propertyGroup: "Custom",
|
|
680
|
+
operators: [],
|
|
681
|
+
externalProperty: {
|
|
682
|
+
key: "nogroup",
|
|
683
|
+
propertyLabel: "NoGroup",
|
|
684
|
+
},
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
const optionsForProperty: ParsedOption[] = [
|
|
688
|
+
{
|
|
689
|
+
property: propertyWithoutGroupLabel,
|
|
690
|
+
value: "val1",
|
|
691
|
+
label: "Value 1",
|
|
692
|
+
tags: [],
|
|
693
|
+
},
|
|
694
|
+
];
|
|
695
|
+
|
|
696
|
+
const queryState: ParsedText = {
|
|
697
|
+
step: "property",
|
|
698
|
+
property: propertyWithoutGroupLabel,
|
|
699
|
+
operator: "=",
|
|
700
|
+
value: "",
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
const result = generateAutoCompleteOptions(
|
|
704
|
+
queryState,
|
|
705
|
+
[propertyWithoutGroupLabel],
|
|
706
|
+
optionsForProperty,
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
expect(result.options[0].label).toBe("Values");
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
describe("null or missing property references", () => {
|
|
714
|
+
test("should skip options with null property reference", () => {
|
|
715
|
+
const optionsWithNull: ParsedOption[] = [
|
|
716
|
+
{
|
|
717
|
+
property: null,
|
|
718
|
+
value: "orphaned",
|
|
719
|
+
label: "Orphaned Value",
|
|
720
|
+
tags: [],
|
|
721
|
+
},
|
|
722
|
+
...parsedOptions,
|
|
723
|
+
];
|
|
724
|
+
|
|
725
|
+
const queryState: ParsedText = {
|
|
726
|
+
step: "free-text",
|
|
727
|
+
value: "orphaned",
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
const result = generateAutoCompleteOptions(
|
|
731
|
+
queryState,
|
|
732
|
+
parsedProperties,
|
|
733
|
+
optionsWithNull,
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
const orphanedOption = result.options.some((g) =>
|
|
737
|
+
g.options.some((o) => o.label === "Orphaned Value"),
|
|
738
|
+
);
|
|
739
|
+
expect(orphanedOption).toBe(false);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
test("should skip properties with no data", () => {
|
|
743
|
+
const queryState: ParsedText = {
|
|
744
|
+
step: "free-text",
|
|
745
|
+
value: "",
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
const result = generateAutoCompleteOptions(queryState, [], []);
|
|
749
|
+
|
|
750
|
+
expect(result.options).toEqual([]);
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
describe("operator prefix filtering", () => {
|
|
755
|
+
test("prefix ':' should match ':' and '!:' operators", () => {
|
|
756
|
+
const queryState: ParsedText = {
|
|
757
|
+
step: "operator",
|
|
758
|
+
property: parsedProperties[0],
|
|
759
|
+
operatorPrefix: ":",
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
const result = generateAutoCompleteOptions(
|
|
763
|
+
queryState,
|
|
764
|
+
parsedProperties,
|
|
765
|
+
parsedOptions,
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
expect(result.options[0].options.length).toBeGreaterThan(0);
|
|
769
|
+
const operators = result.options[0].options.map(
|
|
770
|
+
(o) => (o as AutoCompleteOption).value.split(" ")[1],
|
|
771
|
+
);
|
|
772
|
+
expect(operators).toContain(":");
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
test("prefix '>' should match '>', '>=', '!>' operators", () => {
|
|
776
|
+
const queryState: ParsedText = {
|
|
777
|
+
step: "operator",
|
|
778
|
+
property: parsedProperties[0],
|
|
779
|
+
operatorPrefix: ">",
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
const result = generateAutoCompleteOptions(
|
|
783
|
+
queryState,
|
|
784
|
+
parsedProperties,
|
|
785
|
+
parsedOptions,
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
expect(result.options[0].options.length).toBeGreaterThan(0);
|
|
789
|
+
const operators = result.options[0].options.map(
|
|
790
|
+
(o) => (o as AutoCompleteOption).value.split(" ")[1],
|
|
791
|
+
);
|
|
792
|
+
expect(operators).toContain(">");
|
|
793
|
+
expect(operators).toContain(">=");
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
describe("buildQueryString integration", () => {
|
|
798
|
+
test("query string should contain property, operator, and value", () => {
|
|
799
|
+
const queryState: ParsedText = {
|
|
800
|
+
step: "property",
|
|
801
|
+
property: parsedProperties[0],
|
|
802
|
+
operator: "=",
|
|
803
|
+
value: "active",
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
const result = generateAutoCompleteOptions(
|
|
807
|
+
queryState,
|
|
808
|
+
parsedProperties,
|
|
809
|
+
parsedOptions,
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
expect(result.options[0].options.length).toBeGreaterThan(0);
|
|
813
|
+
const queryStr = (result.options[0].options[0] as AutoCompleteOption)
|
|
814
|
+
.value;
|
|
815
|
+
expect(queryStr).toContain("Status");
|
|
816
|
+
expect(queryStr).toContain("=");
|
|
817
|
+
expect(queryStr).toContain("active");
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test("operator-step value should only contain property and operator", () => {
|
|
821
|
+
const queryState: ParsedText = {
|
|
822
|
+
step: "operator",
|
|
823
|
+
property: parsedProperties[0],
|
|
824
|
+
operatorPrefix: "=",
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
const result = generateAutoCompleteOptions(
|
|
828
|
+
queryState,
|
|
829
|
+
parsedProperties,
|
|
830
|
+
parsedOptions,
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
expect(result.value).toBe("Status =");
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
});
|