@navikt/ds-react 8.4.1 → 8.5.1
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/accordion/Accordion.d.ts +10 -0
- package/cjs/accordion/Accordion.js +2 -2
- package/cjs/accordion/Accordion.js.map +1 -1
- package/cjs/data/table/helpers/table-cell.d.ts +2 -2
- package/cjs/data/table/helpers/table-cell.js +2 -5
- package/cjs/data/table/helpers/table-cell.js.map +1 -1
- package/cjs/data/table/helpers/table-focus.d.ts +26 -2
- package/cjs/data/table/helpers/table-focus.js +60 -9
- package/cjs/data/table/helpers/table-focus.js.map +1 -1
- package/cjs/data/table/helpers/table-grid-nav.d.ts +40 -10
- package/cjs/data/table/helpers/table-grid-nav.js +102 -25
- package/cjs/data/table/helpers/table-grid-nav.js.map +1 -1
- package/cjs/data/table/helpers/table-keyboard.d.ts +24 -3
- package/cjs/data/table/helpers/table-keyboard.js +25 -5
- package/cjs/data/table/helpers/table-keyboard.js.map +1 -1
- package/cjs/data/table/hooks/useGridCache.d.ts +17 -0
- package/cjs/data/table/hooks/useGridCache.js +65 -0
- package/cjs/data/table/hooks/useGridCache.js.map +1 -0
- package/cjs/data/table/root/DataTableRoot.d.ts +14 -4
- package/cjs/data/table/root/DataTableRoot.js +4 -6
- package/cjs/data/table/root/DataTableRoot.js.map +1 -1
- package/cjs/data/table/root/useTableKeyboardNav.d.ts +10 -4
- package/cjs/data/table/root/useTableKeyboardNav.js +70 -99
- package/cjs/data/table/root/useTableKeyboardNav.js.map +1 -1
- package/cjs/data/token-filter/AutoSuggest.d.ts +21 -0
- package/cjs/data/token-filter/AutoSuggest.js +129 -0
- package/cjs/data/token-filter/AutoSuggest.js.map +1 -0
- package/cjs/data/token-filter/TokenFilter.d.ts +11 -0
- package/cjs/data/token-filter/TokenFilter.js +91 -0
- package/cjs/data/token-filter/TokenFilter.js.map +1 -0
- package/cjs/data/token-filter/TokenFilter.types.d.ts +46 -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 +70 -0
- package/cjs/data/token-filter/helpers/generate-autocomplete-options.js +171 -0
- package/cjs/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -0
- package/cjs/data/token-filter/helpers/parse-query-text.d.ts +31 -0
- package/cjs/data/token-filter/helpers/parse-query-text.js +91 -0
- package/cjs/data/token-filter/helpers/parse-query-text.js.map +1 -0
- package/cjs/link-card/LinkCard.d.ts +13 -0
- package/cjs/link-card/LinkCard.js +2 -2
- package/cjs/link-card/LinkCard.js.map +1 -1
- package/cjs/process/Process.d.ts +1 -1
- package/cjs/tooltip/Tooltip.js +1 -1
- package/cjs/tooltip/Tooltip.js.map +1 -1
- package/esm/accordion/Accordion.d.ts +10 -0
- package/esm/accordion/Accordion.js +2 -2
- package/esm/accordion/Accordion.js.map +1 -1
- package/esm/data/table/helpers/table-cell.d.ts +2 -2
- package/esm/data/table/helpers/table-cell.js +2 -5
- package/esm/data/table/helpers/table-cell.js.map +1 -1
- package/esm/data/table/helpers/table-focus.d.ts +26 -2
- package/esm/data/table/helpers/table-focus.js +55 -9
- package/esm/data/table/helpers/table-focus.js.map +1 -1
- package/esm/data/table/helpers/table-grid-nav.d.ts +40 -10
- package/esm/data/table/helpers/table-grid-nav.js +96 -24
- package/esm/data/table/helpers/table-grid-nav.js.map +1 -1
- package/esm/data/table/helpers/table-keyboard.d.ts +24 -3
- package/esm/data/table/helpers/table-keyboard.js +24 -4
- package/esm/data/table/helpers/table-keyboard.js.map +1 -1
- package/esm/data/table/hooks/useGridCache.d.ts +17 -0
- package/esm/data/table/hooks/useGridCache.js +63 -0
- package/esm/data/table/hooks/useGridCache.js.map +1 -0
- package/esm/data/table/root/DataTableRoot.d.ts +14 -4
- package/esm/data/table/root/DataTableRoot.js +4 -6
- package/esm/data/table/root/DataTableRoot.js.map +1 -1
- package/esm/data/table/root/useTableKeyboardNav.d.ts +10 -4
- package/esm/data/table/root/useTableKeyboardNav.js +75 -104
- package/esm/data/table/root/useTableKeyboardNav.js.map +1 -1
- package/esm/data/token-filter/AutoSuggest.d.ts +21 -0
- package/esm/data/token-filter/AutoSuggest.js +93 -0
- package/esm/data/token-filter/AutoSuggest.js.map +1 -0
- package/esm/data/token-filter/TokenFilter.d.ts +11 -0
- package/esm/data/token-filter/TokenFilter.js +55 -0
- package/esm/data/token-filter/TokenFilter.js.map +1 -0
- package/esm/data/token-filter/TokenFilter.types.d.ts +46 -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 +70 -0
- package/esm/data/token-filter/helpers/generate-autocomplete-options.js +169 -0
- package/esm/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -0
- package/esm/data/token-filter/helpers/parse-query-text.d.ts +31 -0
- package/esm/data/token-filter/helpers/parse-query-text.js +87 -0
- package/esm/data/token-filter/helpers/parse-query-text.js.map +1 -0
- package/esm/link-card/LinkCard.d.ts +13 -0
- package/esm/link-card/LinkCard.js +2 -2
- package/esm/link-card/LinkCard.js.map +1 -1
- package/esm/process/Process.d.ts +1 -1
- package/esm/tooltip/Tooltip.js +2 -2
- package/esm/tooltip/Tooltip.js.map +1 -1
- package/package.json +3 -3
- package/src/accordion/Accordion.tsx +19 -2
- package/src/data/table/helpers/table-cell.ts +2 -7
- package/src/data/table/helpers/table-focus.ts +70 -9
- package/src/data/table/helpers/table-grid-nav.test.ts +659 -0
- package/src/data/table/helpers/table-grid-nav.ts +128 -32
- package/src/data/table/helpers/table-keyboard.test.ts +27 -27
- package/src/data/table/helpers/table-keyboard.ts +34 -4
- package/src/data/table/hooks/useGridCache.ts +73 -0
- package/src/data/table/root/DataTableRoot.tsx +21 -11
- package/src/data/table/root/useTableKeyboardNav.ts +110 -128
- package/src/data/token-filter/AutoSuggest.tsx +179 -0
- package/src/data/token-filter/TokenFilter.tsx +124 -0
- package/src/data/token-filter/TokenFilter.types.ts +79 -0
- package/src/data/token-filter/helpers/generate-autocomplete-options.ts +244 -0
- package/src/data/token-filter/helpers/parse-query-text.test.ts +410 -0
- package/src/data/token-filter/helpers/parse-query-text.ts +148 -0
- package/src/link-card/LinkCard.tsx +15 -1
- package/src/process/Process.tsx +1 -1
- package/src/tooltip/Tooltip.tsx +3 -3
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { ParsedOption, ParsedProperty } from "../TokenFilter.types";
|
|
2
|
+
import { type ParsedText, QUERY_OPERATORS } from "./parse-query-text";
|
|
3
|
+
|
|
4
|
+
interface OptionGroup<T> {
|
|
5
|
+
label: string;
|
|
6
|
+
options: T[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface AutoCompleteOption {
|
|
10
|
+
value: string;
|
|
11
|
+
label: string;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
filteringTags?: string[];
|
|
14
|
+
description?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildQueryString(
|
|
18
|
+
propertyLabel: string,
|
|
19
|
+
operator: string,
|
|
20
|
+
value: string,
|
|
21
|
+
): string {
|
|
22
|
+
const parts = [propertyLabel, operator, value].filter(Boolean);
|
|
23
|
+
return parts.join(" ");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* TODO: i18n */
|
|
27
|
+
const OPERATOR_LABELS: Record<string, string> = {
|
|
28
|
+
":": "contains",
|
|
29
|
+
"!:": "does not contain",
|
|
30
|
+
"=": "is",
|
|
31
|
+
"!=": "is not",
|
|
32
|
+
"^": "starts with",
|
|
33
|
+
"!^": "does not start with",
|
|
34
|
+
">=": "is greater than or equal to",
|
|
35
|
+
"<=": "is less than or equal to",
|
|
36
|
+
">": "is greater than",
|
|
37
|
+
"<": "is less than",
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Grouping option for autocomplete suggestions structures:
|
|
41
|
+
*
|
|
42
|
+
* Step: "free-text" + empty value:
|
|
43
|
+
* - Group: "Properties" with all properties.
|
|
44
|
+
*
|
|
45
|
+
* Step: "free-text" with non-empty value:
|
|
46
|
+
* - Group: "Properties". All properties including the filter text in label or description or tags. String match.
|
|
47
|
+
* - Group: "Values". All "property = value" combinations where either the property label or value label or description or tags include the filter text. String match.
|
|
48
|
+
* - - Ignore all other operators than "=" for value suggestions.
|
|
49
|
+
*
|
|
50
|
+
* Step: "property" + empty value:
|
|
51
|
+
* - Group: "Operators". All operators valid for the selected property.
|
|
52
|
+
*
|
|
53
|
+
* Step: "property" + non-empty value:
|
|
54
|
+
* - Group: "Operators". All operators valid for the selected property with string match. Only relevant for multi letter operators like "!="
|
|
55
|
+
*
|
|
56
|
+
* Step: "operator" + empty value:
|
|
57
|
+
* - Group: "<Property> values". All values valid for the selected property and operator. String match on value label, description and tags.
|
|
58
|
+
*
|
|
59
|
+
* Step: "operator" + non-empty value:
|
|
60
|
+
* - Group: "<Property> values". All values valid for the selected property and operator with string match. String match on value label, description and tags.
|
|
61
|
+
*
|
|
62
|
+
*
|
|
63
|
+
* TODO:
|
|
64
|
+
* - Handle custom groups
|
|
65
|
+
* - Multi vs single-select: Allow operators for each options where user can define type to be enum: { operator: "=", tokenType: "enum" }. Enum-type options allow selecting multiple values, i.e state = ("active", "pending"))
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* TODO: Update based on instructions above.
|
|
70
|
+
*/
|
|
71
|
+
function generateAutoCompleteOptions(
|
|
72
|
+
queryState: ParsedText,
|
|
73
|
+
filteringProperties: ParsedProperty[] = [],
|
|
74
|
+
filteringOptions: ParsedOption[] = [],
|
|
75
|
+
) {
|
|
76
|
+
if (queryState.step === "property") {
|
|
77
|
+
if (!queryState.property) {
|
|
78
|
+
return {
|
|
79
|
+
value: queryState.value,
|
|
80
|
+
options: [],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const { propertyLabel, groupValuesLabel } = queryState.property;
|
|
84
|
+
const options = filteringOptions.filter(
|
|
85
|
+
(o) => o.property === queryState.property,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
value: queryState.value,
|
|
90
|
+
options: [
|
|
91
|
+
{
|
|
92
|
+
label: groupValuesLabel,
|
|
93
|
+
options: options.map(({ label, value, tags, filteringTags }) => ({
|
|
94
|
+
value: buildQueryString(propertyLabel, queryState.operator, value),
|
|
95
|
+
label,
|
|
96
|
+
tags,
|
|
97
|
+
filteringTags,
|
|
98
|
+
})),
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (queryState.step === "operator") {
|
|
104
|
+
return {
|
|
105
|
+
value: buildQueryString(
|
|
106
|
+
queryState.property.propertyLabel,
|
|
107
|
+
queryState.operatorPrefix,
|
|
108
|
+
"",
|
|
109
|
+
),
|
|
110
|
+
options: [
|
|
111
|
+
...generatePropertySuggestions(filteringProperties),
|
|
112
|
+
{
|
|
113
|
+
options: QUERY_OPERATORS.map((value) => ({
|
|
114
|
+
value: buildQueryString(
|
|
115
|
+
queryState.property.propertyLabel,
|
|
116
|
+
value,
|
|
117
|
+
"",
|
|
118
|
+
),
|
|
119
|
+
label: buildQueryString(
|
|
120
|
+
queryState.property.propertyLabel,
|
|
121
|
+
value,
|
|
122
|
+
"",
|
|
123
|
+
),
|
|
124
|
+
description: OPERATOR_LABELS[value] ?? "",
|
|
125
|
+
})),
|
|
126
|
+
/* TODO: i18n */
|
|
127
|
+
label: "Operator",
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const needsValueSuggestions = !!queryState.value;
|
|
134
|
+
const needsPropertySuggestions = !(
|
|
135
|
+
queryState.step === "free-text" && queryState.operator === "!:"
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
value: queryState.value,
|
|
140
|
+
options: [
|
|
141
|
+
...(needsPropertySuggestions
|
|
142
|
+
? generatePropertySuggestions(filteringProperties)
|
|
143
|
+
: []),
|
|
144
|
+
...(needsValueSuggestions
|
|
145
|
+
? generateAllValueSuggestions(filteringOptions)
|
|
146
|
+
: []),
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function createAutoCompleteOption(
|
|
152
|
+
propertyLabel: string,
|
|
153
|
+
operator: string,
|
|
154
|
+
value: string,
|
|
155
|
+
label: string,
|
|
156
|
+
tags?: string[],
|
|
157
|
+
filteringTags?: string[],
|
|
158
|
+
): AutoCompleteOption {
|
|
159
|
+
return {
|
|
160
|
+
value: buildQueryString(propertyLabel, operator, value),
|
|
161
|
+
label: buildQueryString(propertyLabel, operator, label),
|
|
162
|
+
tags,
|
|
163
|
+
filteringTags,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function generateAllValueSuggestions(
|
|
168
|
+
filteringOptions: ParsedOption[] = [],
|
|
169
|
+
): OptionGroup<AutoCompleteOption>[] {
|
|
170
|
+
const groups: Record<string, OptionGroup<AutoCompleteOption>> = {};
|
|
171
|
+
|
|
172
|
+
for (const option of filteringOptions) {
|
|
173
|
+
if (!option || !option.property) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const groupLabel = option.property.groupValuesLabel || "Values";
|
|
178
|
+
|
|
179
|
+
if (!groups[groupLabel]) {
|
|
180
|
+
groups[groupLabel] = {
|
|
181
|
+
label: groupLabel,
|
|
182
|
+
options: [],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const { label, value, tags, filteringTags, property } = option;
|
|
187
|
+
const options = QUERY_OPERATORS.map((operator) =>
|
|
188
|
+
createAutoCompleteOption(
|
|
189
|
+
property.propertyLabel,
|
|
190
|
+
operator,
|
|
191
|
+
value,
|
|
192
|
+
label,
|
|
193
|
+
tags,
|
|
194
|
+
filteringTags,
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
groups[groupLabel].options.push(...options);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return Object.values(groups);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function generatePropertySuggestions(
|
|
205
|
+
filteringProperties: ParsedProperty[] = [],
|
|
206
|
+
): OptionGroup<ParsedProperty>[] {
|
|
207
|
+
const defaultGroup: OptionGroup<ParsedProperty> = {
|
|
208
|
+
label: "Properties",
|
|
209
|
+
options: [],
|
|
210
|
+
};
|
|
211
|
+
const customGroups: Record<string, OptionGroup<ParsedProperty>> = {};
|
|
212
|
+
|
|
213
|
+
for (const property of filteringProperties) {
|
|
214
|
+
if (!property) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const groupLabel = property.propertyGroup?.trim();
|
|
218
|
+
|
|
219
|
+
if (groupLabel) {
|
|
220
|
+
if (!customGroups[groupLabel]) {
|
|
221
|
+
customGroups[groupLabel] = {
|
|
222
|
+
label: groupLabel,
|
|
223
|
+
options: [],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
customGroups[groupLabel].options.push(property);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
defaultGroup.options.push(property);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const groups: OptionGroup<ParsedProperty>[] = [
|
|
234
|
+
...Object.values(customGroups),
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
if (defaultGroup.options.length > 0) {
|
|
238
|
+
groups.push(defaultGroup);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return groups;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export { generateAutoCompleteOptions };
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type {
|
|
3
|
+
ParsedProperty,
|
|
4
|
+
QueryFilteringProperty,
|
|
5
|
+
} from "../TokenFilter.types";
|
|
6
|
+
import { QUERY_OPERATORS, 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
|
+
}));
|
|
49
|
+
|
|
50
|
+
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
|
+
describe("value extraction", () => {
|
|
232
|
+
test("should extract value after operator", () => {
|
|
233
|
+
const result = parseQueryText("Status=active", parsedProperties);
|
|
234
|
+
expect(result.step).toBe("property");
|
|
235
|
+
const propertyResult = result as Extract<
|
|
236
|
+
ParsedText,
|
|
237
|
+
{ step: "property" }
|
|
238
|
+
>;
|
|
239
|
+
expect(propertyResult.value).toBe("active");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("should trim whitespace after operator", () => {
|
|
243
|
+
const result = parseQueryText("Status= active", parsedProperties);
|
|
244
|
+
expect(result.step).toBe("property");
|
|
245
|
+
const propertyResult = result as Extract<
|
|
246
|
+
ParsedText,
|
|
247
|
+
{ step: "property" }
|
|
248
|
+
>;
|
|
249
|
+
expect(propertyResult.value).toBe("active");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("should handle empty value", () => {
|
|
253
|
+
const result = parseQueryText("Status=", parsedProperties);
|
|
254
|
+
expect(result.step).toBe("property");
|
|
255
|
+
const propertyResult = result as Extract<
|
|
256
|
+
ParsedText,
|
|
257
|
+
{ step: "property" }
|
|
258
|
+
>;
|
|
259
|
+
expect(propertyResult.value).toBe("");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("should handle whitespace-only value", () => {
|
|
263
|
+
const result = parseQueryText("Status= ", parsedProperties);
|
|
264
|
+
expect(result.step).toBe("property");
|
|
265
|
+
const propertyResult = result as Extract<
|
|
266
|
+
ParsedText,
|
|
267
|
+
{ step: "property" }
|
|
268
|
+
>;
|
|
269
|
+
expect(propertyResult.value).toBe("");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("should preserve value content including spaces", () => {
|
|
273
|
+
const result = parseQueryText(
|
|
274
|
+
"Status=active and running",
|
|
275
|
+
parsedProperties,
|
|
276
|
+
);
|
|
277
|
+
expect(result.step).toBe("property");
|
|
278
|
+
const propertyResult = result as Extract<
|
|
279
|
+
ParsedText,
|
|
280
|
+
{ step: "property" }
|
|
281
|
+
>;
|
|
282
|
+
expect(propertyResult.value).toBe("active and running");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("should extract multi-part property value", () => {
|
|
286
|
+
const result = parseQueryText("Instance ID=server-123", parsedProperties);
|
|
287
|
+
expect(result.step).toBe("property");
|
|
288
|
+
const propertyResult = result as Extract<
|
|
289
|
+
ParsedText,
|
|
290
|
+
{ step: "property" }
|
|
291
|
+
>;
|
|
292
|
+
expect(propertyResult.value).toBe("server-123");
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe("whitespace handling", () => {
|
|
297
|
+
test("should handle whitespace between property and operator", () => {
|
|
298
|
+
const result = parseQueryText("Status =value", parsedProperties);
|
|
299
|
+
expect(result.step).toBe("property");
|
|
300
|
+
const propertyResult = result as Extract<
|
|
301
|
+
ParsedText,
|
|
302
|
+
{ step: "property" }
|
|
303
|
+
>;
|
|
304
|
+
expect(propertyResult.operator).toBe("=");
|
|
305
|
+
expect(propertyResult.value).toBe("value");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("should handle whitespace after property but before operator", () => {
|
|
309
|
+
const result = parseQueryText("Status >value", parsedProperties);
|
|
310
|
+
expect(result.step).toBe("property");
|
|
311
|
+
const propertyResult = result as Extract<
|
|
312
|
+
ParsedText,
|
|
313
|
+
{ step: "property" }
|
|
314
|
+
>;
|
|
315
|
+
expect(propertyResult.operator).toBe(">");
|
|
316
|
+
expect(propertyResult.value).toBe("value");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("should trim operator prefix whitespace", () => {
|
|
320
|
+
const result = parseQueryText("Status ", parsedProperties);
|
|
321
|
+
expect(result.step).toBe("operator");
|
|
322
|
+
const operatorResult = result as Extract<
|
|
323
|
+
ParsedText,
|
|
324
|
+
{ step: "operator" }
|
|
325
|
+
>;
|
|
326
|
+
expect(operatorResult.operatorPrefix).toBe("");
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe("edge cases", () => {
|
|
331
|
+
test("should handle empty input", () => {
|
|
332
|
+
const result = parseQueryText("", parsedProperties);
|
|
333
|
+
expect(result.step).toBe("free-text");
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("should handle whitespace-only input", () => {
|
|
337
|
+
const result = parseQueryText(" ", parsedProperties);
|
|
338
|
+
expect(result.step).toBe("free-text");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("should handle property-only input", () => {
|
|
342
|
+
const result = parseQueryText("Status", parsedProperties);
|
|
343
|
+
expect(result.step).toBe("operator");
|
|
344
|
+
const operatorResult = result as Extract<
|
|
345
|
+
ParsedText,
|
|
346
|
+
{ step: "operator" }
|
|
347
|
+
>;
|
|
348
|
+
expect(operatorResult.operatorPrefix).toBe("");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("should handle property with spaces", () => {
|
|
352
|
+
const result = parseQueryText("Instance ID:value", parsedProperties);
|
|
353
|
+
expect(result.step).toBe("property");
|
|
354
|
+
const propertyResult = result as Extract<
|
|
355
|
+
ParsedText,
|
|
356
|
+
{ step: "property" }
|
|
357
|
+
>;
|
|
358
|
+
expect(propertyResult.property.propertyKey).toBe("instance-id");
|
|
359
|
+
expect(propertyResult.operator).toBe(":");
|
|
360
|
+
expect(propertyResult.value).toBe("value");
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
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
|
+
describe("free-text fallback", () => {
|
|
395
|
+
test("should fallback to free-text when no property is matched", () => {
|
|
396
|
+
const result = parseQueryText("Random text", parsedProperties);
|
|
397
|
+
expect(result.step).toBe("free-text");
|
|
398
|
+
const freeTextResult = result as Extract<
|
|
399
|
+
ParsedText,
|
|
400
|
+
{ step: "free-text" }
|
|
401
|
+
>;
|
|
402
|
+
expect(freeTextResult.value).toBe("Random text");
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("should fallback to free-text with partial operator match that is invalid", () => {
|
|
406
|
+
const result = parseQueryText("Status@somevalue", parsedProperties);
|
|
407
|
+
expect(result.step).toBe("free-text");
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
});
|