@open-mercato/ui 0.6.1-develop.3260.1.dc719cac84 → 0.6.1-develop.3278.1.a4d26475e4

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 (62) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/backend/DataTable.js +149 -53
  3. package/dist/backend/DataTable.js.map +2 -2
  4. package/dist/backend/filters/ActiveFilterChips.js +142 -0
  5. package/dist/backend/filters/ActiveFilterChips.js.map +7 -0
  6. package/dist/backend/filters/AdvancedFilterBuilder.js +689 -163
  7. package/dist/backend/filters/AdvancedFilterBuilder.js.map +2 -2
  8. package/dist/backend/filters/AdvancedFilterPanel.js +354 -0
  9. package/dist/backend/filters/AdvancedFilterPanel.js.map +7 -0
  10. package/dist/backend/filters/FilterEmptyState.js +41 -0
  11. package/dist/backend/filters/FilterEmptyState.js.map +7 -0
  12. package/dist/backend/filters/FilterFieldPicker.js +148 -0
  13. package/dist/backend/filters/FilterFieldPicker.js.map +7 -0
  14. package/dist/backend/filters/FilteredEmptyResults.js +26 -0
  15. package/dist/backend/filters/FilteredEmptyResults.js.map +7 -0
  16. package/dist/backend/filters/QuickFilters.js +41 -0
  17. package/dist/backend/filters/QuickFilters.js.map +7 -0
  18. package/dist/backend/filters/filterValidation.js +47 -0
  19. package/dist/backend/filters/filterValidation.js.map +7 -0
  20. package/dist/backend/filters/treeReducer.js +159 -0
  21. package/dist/backend/filters/treeReducer.js.map +7 -0
  22. package/dist/backend/hooks/useAdvancedFilter.js +82 -1
  23. package/dist/backend/hooks/useAdvancedFilter.js.map +2 -2
  24. package/dist/backend/utils/api.js +18 -1
  25. package/dist/backend/utils/api.js.map +2 -2
  26. package/dist/backend/utils/useAutoDiscoveredFields.js +1 -0
  27. package/dist/backend/utils/useAutoDiscoveredFields.js.map +2 -2
  28. package/dist/backend/utils/useCurrentUserId.js +31 -0
  29. package/dist/backend/utils/useCurrentUserId.js.map +7 -0
  30. package/dist/primitives/dialog.js +7 -5
  31. package/dist/primitives/dialog.js.map +2 -2
  32. package/dist/primitives/tag.js +4 -2
  33. package/dist/primitives/tag.js.map +2 -2
  34. package/package.json +3 -3
  35. package/src/backend/DataTable.tsx +307 -63
  36. package/src/backend/__tests__/DataTable.extensions.test.tsx +170 -0
  37. package/src/backend/filters/ActiveFilterChips.tsx +179 -0
  38. package/src/backend/filters/AdvancedFilterBuilder.tsx +840 -237
  39. package/src/backend/filters/AdvancedFilterPanel.tsx +456 -0
  40. package/src/backend/filters/FilterEmptyState.tsx +51 -0
  41. package/src/backend/filters/FilterFieldPicker.tsx +139 -0
  42. package/src/backend/filters/FilteredEmptyResults.tsx +40 -0
  43. package/src/backend/filters/QuickFilters.tsx +62 -0
  44. package/src/backend/filters/__tests__/ActiveFilterChips.test.tsx +75 -0
  45. package/src/backend/filters/__tests__/AdvancedFilterBuilder.test.tsx +210 -0
  46. package/src/backend/filters/__tests__/AdvancedFilterPanel.test.tsx +212 -0
  47. package/src/backend/filters/__tests__/FilterEmptyState.test.tsx +31 -0
  48. package/src/backend/filters/__tests__/FilterFieldPicker.test.tsx +61 -0
  49. package/src/backend/filters/__tests__/FilteredEmptyResults.test.tsx +35 -0
  50. package/src/backend/filters/__tests__/QuickFilters.test.tsx +34 -0
  51. package/src/backend/filters/__tests__/filterValidation.test.ts +69 -0
  52. package/src/backend/filters/__tests__/treeReducer.test.ts +189 -0
  53. package/src/backend/filters/filterValidation.ts +61 -0
  54. package/src/backend/filters/treeReducer.ts +201 -0
  55. package/src/backend/hooks/__tests__/useAdvancedFilter.test.tsx +213 -0
  56. package/src/backend/hooks/useAdvancedFilter.ts +130 -0
  57. package/src/backend/utils/api.ts +27 -2
  58. package/src/backend/utils/useAutoDiscoveredFields.ts +2 -0
  59. package/src/backend/utils/useCurrentUserId.ts +40 -0
  60. package/src/primitives/__tests__/zindex-overlay.test.tsx +22 -0
  61. package/src/primitives/dialog.tsx +18 -7
  62. package/src/primitives/tag.tsx +3 -1
@@ -1,7 +1,24 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { ChevronDown, Plus, Trash2, X } from "lucide-react";
4
+ import { createPortal } from "react-dom";
5
+ import { Plus, Trash2, X, GripVertical, ChevronDown } from "lucide-react";
6
+ import {
7
+ DndContext,
8
+ DragOverlay,
9
+ PointerSensor,
10
+ KeyboardSensor,
11
+ useSensor,
12
+ useSensors,
13
+ closestCenter
14
+ } from "@dnd-kit/core";
15
+ import {
16
+ SortableContext,
17
+ sortableKeyboardCoordinates,
18
+ useSortable,
19
+ verticalListSortingStrategy
20
+ } from "@dnd-kit/sortable";
21
+ import { CSS } from "@dnd-kit/utilities";
5
22
  import { Button } from "../../primitives/button.js";
6
23
  import { IconButton } from "../../primitives/icon-button.js";
7
24
  import { Input } from "../../primitives/input.js";
@@ -12,14 +29,19 @@ import {
12
29
  SelectTrigger,
13
30
  SelectValue
14
31
  } from "../../primitives/select.js";
32
+ import { Popover, PopoverContent, PopoverTrigger } from "../../primitives/popover.js";
33
+ import { Checkbox } from "../../primitives/checkbox.js";
15
34
  import { useT } from "@open-mercato/shared/lib/i18n/context";
16
35
  import {
17
36
  OPERATORS_BY_FIELD_TYPE,
18
37
  getDefaultOperator,
19
- isValuelessOperator,
20
- createEmptyCondition,
21
- normalizeAdvancedFilterState
38
+ isValuelessOperator
22
39
  } from "@open-mercato/shared/lib/query/advanced-filter";
40
+ import {
41
+ TREE_LIMITS
42
+ } from "@open-mercato/shared/lib/query/advanced-filter-tree";
43
+ import { treeReducer, canAddRule, canAddGroup } from "./treeReducer.js";
44
+ import { FilterFieldPicker } from "./FilterFieldPicker.js";
23
45
  const OPERATOR_LABELS = {
24
46
  is: "is",
25
47
  is_not: "is not",
@@ -47,141 +69,505 @@ const OPERATOR_LABELS = {
47
69
  has_none_of: "has none of"
48
70
  };
49
71
  function getFieldType(fields, fieldKey) {
50
- const field = fields.find((f) => f.key === fieldKey);
51
- return field?.type ?? "text";
72
+ return fields.find((f) => f.key === fieldKey)?.type ?? "text";
52
73
  }
53
- function ConditionRow({
54
- condition,
55
- index,
74
+ const MULTI_VALUE_OPS = /* @__PURE__ */ new Set([
75
+ "is_any_of",
76
+ "is_none_of",
77
+ "has_any_of",
78
+ "has_all_of",
79
+ "has_none_of"
80
+ ]);
81
+ function getOperatorShape(op) {
82
+ if (isValuelessOperator(op)) return "valueless";
83
+ if (MULTI_VALUE_OPS.has(op)) return "multi";
84
+ if (op === "between") return "range";
85
+ return "single";
86
+ }
87
+ function AdvancedFilterBuilder({
56
88
  fields,
57
- join,
58
- onUpdate,
59
- onRemove,
60
- onToggleJoin,
61
- t
89
+ value,
90
+ onChange,
91
+ onApply,
92
+ onClear,
93
+ pendingErrors
62
94
  }) {
63
- const fieldType = getFieldType(fields, condition.field);
64
- const operators = OPERATORS_BY_FIELD_TYPE[fieldType] ?? OPERATORS_BY_FIELD_TYPE.text;
65
- const valueless = isValuelessOperator(condition.operator);
66
- const handleFieldChange = (newField) => {
67
- const newType = getFieldType(fields, newField);
68
- const newOp = getDefaultOperator(newType);
69
- onUpdate(condition.id, { field: newField, operator: newOp, value: "" });
70
- };
71
- const joinLabel = join === "and" ? t("ui.advancedFilter.and", "And") : t("ui.advancedFilter.or", "Or");
72
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start gap-2", children: [
73
- /* @__PURE__ */ jsx("div", { className: "w-20 shrink-0 pt-0.5 text-sm text-muted-foreground", children: index === 0 ? /* @__PURE__ */ jsx("span", { children: t("ui.advancedFilter.where", "Where") }) : /* @__PURE__ */ jsxs(
74
- Button,
75
- {
76
- type: "button",
77
- variant: "outline",
78
- size: "sm",
79
- className: "h-9 min-w-[5rem] justify-between px-3 text-sm font-medium",
80
- onClick: () => onToggleJoin(condition.id),
81
- "aria-label": t("ui.advancedFilter.toggleJoin", "Toggle filter join operator"),
82
- title: t("ui.advancedFilter.toggleJoinHint", "Click to switch between AND and OR"),
83
- children: [
84
- /* @__PURE__ */ jsx("span", { children: joinLabel }),
85
- /* @__PURE__ */ jsx(ChevronDown, { className: "size-3.5 text-muted-foreground" })
86
- ]
95
+ const t = useT();
96
+ const dispatch = React.useCallback((action) => {
97
+ onChange(treeReducer(value, action));
98
+ }, [onChange, value]);
99
+ React.useEffect(() => {
100
+ function onKey(e) {
101
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
102
+ e.preventDefault();
103
+ onApply();
87
104
  }
88
- ) }),
89
- /* @__PURE__ */ jsxs(
90
- Select,
91
- {
92
- value: condition.field || void 0,
93
- onValueChange: (next) => handleFieldChange(next ?? ""),
94
- children: [
95
- /* @__PURE__ */ jsx(
96
- SelectTrigger,
105
+ }
106
+ window.addEventListener("keydown", onKey);
107
+ return () => window.removeEventListener("keydown", onKey);
108
+ }, [onApply]);
109
+ const errorByRuleId = React.useMemo(() => {
110
+ const m = /* @__PURE__ */ new Map();
111
+ for (const e of pendingErrors ?? []) m.set(e.ruleId, t(e.messageKey, e.message));
112
+ return m;
113
+ }, [pendingErrors, t]);
114
+ const empty = value.root.children.length === 0;
115
+ const defaultField = fields.length > 0 ? fields[0].key : void 0;
116
+ const sensors = useSensors(
117
+ useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
118
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
119
+ );
120
+ const [activeDrag, setActiveDrag] = React.useState(null);
121
+ const findNode = React.useCallback((id) => locateInTree(value.root, id), [value]);
122
+ const handleDragStart = React.useCallback((e) => {
123
+ const found = findNode(String(e.active.id));
124
+ if (found) setActiveDrag(found);
125
+ }, [findNode]);
126
+ const handleDragEnd = React.useCallback((e) => {
127
+ setActiveDrag(null);
128
+ const { active, over } = e;
129
+ if (!over || active.id === over.id) return;
130
+ const from = findNode(String(active.id));
131
+ const to = findNode(String(over.id));
132
+ if (!from || !to) return;
133
+ if (from.parent.id !== to.parent.id) return;
134
+ const fromIdx = from.parent.children.findIndex((c) => c.id === active.id);
135
+ const toIdx = to.parent.children.findIndex((c) => c.id === over.id);
136
+ if (fromIdx < 0 || toIdx < 0) return;
137
+ dispatch({ type: "reorderChildren", groupId: from.parent.id, fromIdx, toIdx });
138
+ }, [dispatch, findNode]);
139
+ const handleDragCancel = React.useCallback(() => setActiveDrag(null), []);
140
+ return /* @__PURE__ */ jsxs(
141
+ DndContext,
142
+ {
143
+ sensors,
144
+ collisionDetection: closestCenter,
145
+ onDragStart: handleDragStart,
146
+ onDragEnd: handleDragEnd,
147
+ onDragCancel: handleDragCancel,
148
+ children: [
149
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 p-3", children: [
150
+ !empty ? /* @__PURE__ */ jsx(
151
+ GroupView,
97
152
  {
98
- className: "min-w-[140px]",
99
- "aria-label": t("ui.advancedFilter.selectField", "Select field"),
100
- children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("ui.advancedFilter.selectFieldPlaceholder", "Select field...") })
153
+ group: value.root,
154
+ level: 1,
155
+ fields,
156
+ tree: value,
157
+ dispatch,
158
+ defaultField,
159
+ errorByRuleId,
160
+ t
101
161
  }
102
- ),
103
- /* @__PURE__ */ jsx(SelectContent, { children: fields.map((f) => /* @__PURE__ */ jsx(SelectItem, { value: f.key, children: f.label }, f.key)) })
104
- ]
105
- }
106
- ),
107
- /* @__PURE__ */ jsxs(
108
- Select,
109
- {
110
- value: condition.operator,
111
- onValueChange: (next) => onUpdate(condition.id, { operator: next, value: "" }),
112
- children: [
113
- /* @__PURE__ */ jsx(
114
- SelectTrigger,
162
+ ) : null,
163
+ !empty ? /* @__PURE__ */ jsx("div", { className: "flex items-center gap-3 pt-2 border-t border-border", children: /* @__PURE__ */ jsxs(
164
+ Button,
115
165
  {
116
- className: "min-w-[120px]",
117
- "aria-label": t("ui.advancedFilter.selectOperator", "Select operator"),
118
- children: /* @__PURE__ */ jsx(SelectValue, {})
166
+ type: "button",
167
+ variant: "ghost",
168
+ size: "sm",
169
+ className: "h-auto px-0 py-0 text-base text-muted-foreground hover:bg-transparent hover:text-foreground",
170
+ onClick: onClear,
171
+ children: [
172
+ /* @__PURE__ */ jsx(X, { className: "size-4" }),
173
+ t("ui.advancedFilter.clearAll", "Clear all")
174
+ ]
119
175
  }
120
- ),
121
- /* @__PURE__ */ jsx(SelectContent, { children: operators.map((op) => /* @__PURE__ */ jsx(SelectItem, { value: op, children: t(`ui.advancedFilter.operator.${op}`, OPERATOR_LABELS[op]) }, op)) })
122
- ]
176
+ ) }) : null
177
+ ] }),
178
+ typeof document !== "undefined" ? createPortal(
179
+ // dnd-kit's DragOverlay sets its own inline z-index, so a Tailwind
180
+ // utility class would be overridden — read the DS token via CSS var.
181
+ /* @__PURE__ */ jsx(DragOverlay, { dropAnimation: null, style: { zIndex: "var(--z-index-modal-elevated, 55)" }, children: activeDrag ? /* @__PURE__ */ jsx(DragGhost, { node: activeDrag.node, parent: activeDrag.parent, fields, t }) : null }),
182
+ document.body
183
+ ) : null
184
+ ]
185
+ }
186
+ );
187
+ }
188
+ function locateInTree(root, id) {
189
+ for (const child of root.children) {
190
+ if (child.id === id) return { node: child, parent: root };
191
+ if (child.type === "group") {
192
+ const found = locateInTree(child, id);
193
+ if (found) return found;
194
+ }
195
+ }
196
+ return null;
197
+ }
198
+ function DragGhost({
199
+ node,
200
+ parent,
201
+ fields,
202
+ t
203
+ }) {
204
+ const isGroup = node.type === "group";
205
+ const labelParts = [];
206
+ if (isGroup) {
207
+ const ruleCount = countRulesIn(node);
208
+ labelParts.push(t("ui.advancedFilter.dragGhost.group", "Group"));
209
+ labelParts.push(`(${ruleCount})`);
210
+ } else {
211
+ const def = fields.find((f) => f.key === node.field);
212
+ labelParts.push(def?.label ?? (node.field || t("ui.advancedFilter.dragGhost.unsetField", "Unset field")));
213
+ labelParts.push(OPERATOR_LABELS[node.operator] ?? node.operator);
214
+ if (typeof node.value === "string" && node.value) labelParts.push(`"${node.value}"`);
215
+ }
216
+ return /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2 rounded-md border border-border bg-popover px-3 py-1.5 text-sm shadow-md cursor-grabbing", children: [
217
+ /* @__PURE__ */ jsx(GripVertical, { className: "size-4 text-muted-foreground" }),
218
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: parent.combinator === "and" ? t("ui.advancedFilter.connector.and", "and") : t("ui.advancedFilter.connector.or", "or") }),
219
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: labelParts.join(" ") })
220
+ ] });
221
+ }
222
+ function countRulesIn(group) {
223
+ let n = 0;
224
+ for (const c of group.children) {
225
+ if (c.type === "rule") n += 1;
226
+ else n += countRulesIn(c);
227
+ }
228
+ return n;
229
+ }
230
+ function GroupView({
231
+ group,
232
+ level,
233
+ fields,
234
+ tree,
235
+ dispatch,
236
+ defaultField,
237
+ errorByRuleId,
238
+ t
239
+ }) {
240
+ const isRoot = level === 1;
241
+ const containerClass = isRoot ? "space-y-3" : "space-y-3 rounded-lg border border-border bg-muted/30 p-3";
242
+ return /* @__PURE__ */ jsxs("div", { className: containerClass, children: [
243
+ /* @__PURE__ */ jsx(
244
+ GroupToolbar,
245
+ {
246
+ group,
247
+ level,
248
+ tree,
249
+ dispatch,
250
+ defaultField,
251
+ fields,
252
+ isRoot,
253
+ t
123
254
  }
124
255
  ),
125
- !valueless ? /* @__PURE__ */ jsx(
126
- ValueInput,
256
+ /* @__PURE__ */ jsx(
257
+ SortableChildrenList,
127
258
  {
128
- condition,
259
+ group,
260
+ level,
129
261
  fields,
130
- fieldType,
131
- onUpdate,
262
+ tree,
263
+ dispatch,
264
+ defaultField,
265
+ errorByRuleId,
132
266
  t
133
267
  }
134
- ) : null,
268
+ )
269
+ ] });
270
+ }
271
+ function AndOrToggle({
272
+ combinator,
273
+ onChange,
274
+ t
275
+ }) {
276
+ const andLabel = t("ui.advancedFilter.combinator.and", "And");
277
+ const orLabel = t("ui.advancedFilter.combinator.or", "Or");
278
+ const baseBtn = "h-8 px-3 text-sm font-medium outline-none focus-visible:shadow-focus disabled:cursor-not-allowed";
279
+ const selBtn = "bg-brand-violet text-white hover:bg-brand-violet/90";
280
+ const unselBtn = "bg-background text-foreground hover:bg-accent";
281
+ return /* @__PURE__ */ jsxs(
282
+ "div",
283
+ {
284
+ role: "group",
285
+ "aria-label": t("ui.advancedFilter.combinator.label", "Group combinator"),
286
+ className: "inline-flex rounded-md border border-input overflow-hidden shrink-0",
287
+ "data-testid": "advanced-filter-combinator-toggle",
288
+ "data-combinator": combinator,
289
+ children: [
290
+ /* @__PURE__ */ jsx(
291
+ "button",
292
+ {
293
+ type: "button",
294
+ "aria-pressed": combinator === "and",
295
+ onClick: () => combinator !== "and" && onChange("and"),
296
+ className: `${baseBtn} ${combinator === "and" ? selBtn : unselBtn}`,
297
+ children: andLabel
298
+ }
299
+ ),
300
+ /* @__PURE__ */ jsx(
301
+ "button",
302
+ {
303
+ type: "button",
304
+ "aria-pressed": combinator === "or",
305
+ onClick: () => combinator !== "or" && onChange("or"),
306
+ className: `${baseBtn} border-l border-input ${combinator === "or" ? selBtn : unselBtn}`,
307
+ children: orLabel
308
+ }
309
+ )
310
+ ]
311
+ }
312
+ );
313
+ }
314
+ function SortableChildrenList({
315
+ group,
316
+ level,
317
+ fields,
318
+ tree,
319
+ dispatch,
320
+ defaultField,
321
+ errorByRuleId,
322
+ t
323
+ }) {
324
+ return /* @__PURE__ */ jsx(SortableContext, { items: group.children.map((c) => c.id), strategy: verticalListSortingStrategy, children: group.children.map((child, idx) => /* @__PURE__ */ jsx(
325
+ SortableRow,
326
+ {
327
+ child,
328
+ idx,
329
+ parent: group,
330
+ level,
331
+ fields,
332
+ tree,
333
+ dispatch,
334
+ defaultField,
335
+ errorByRuleId,
336
+ t
337
+ },
338
+ child.id
339
+ )) });
340
+ }
341
+ function SortableRow({
342
+ child,
343
+ idx,
344
+ parent,
345
+ level,
346
+ fields,
347
+ tree,
348
+ dispatch,
349
+ defaultField,
350
+ errorByRuleId,
351
+ t
352
+ }) {
353
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
354
+ id: child.id
355
+ });
356
+ const style = isDragging ? { visibility: "hidden" } : { transform: CSS.Transform.toString(transform), transition };
357
+ const dragHandleProps = { ...attributes, ...listeners };
358
+ return /* @__PURE__ */ jsx("div", { ref: setNodeRef, style, "data-testid": "filter-rule-row", "data-filter-node-id": child.id, children: child.type === "rule" ? /* @__PURE__ */ jsx(
359
+ RuleRow,
360
+ {
361
+ rule: child,
362
+ fields,
363
+ dispatch,
364
+ errorByRuleId,
365
+ dragHandleProps,
366
+ t
367
+ }
368
+ ) : /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
135
369
  /* @__PURE__ */ jsx(
136
- IconButton,
370
+ "button",
137
371
  {
138
- variant: "ghost",
139
- size: "sm",
140
372
  type: "button",
141
- onClick: () => onRemove(condition.id),
142
- "aria-label": t("ui.advancedFilter.removeCondition", "Remove condition"),
143
- children: /* @__PURE__ */ jsx(Trash2, { className: "size-4 text-muted-foreground" })
373
+ ...dragHandleProps,
374
+ "data-testid": "filter-drag-handle",
375
+ className: "mt-1 text-muted-foreground/60 hover:text-muted-foreground cursor-grab outline-none focus-visible:shadow-focus rounded-sm",
376
+ "aria-label": t("ui.advancedFilter.dragHandle", "Drag to reorder"),
377
+ children: /* @__PURE__ */ jsx(GripVertical, { className: "size-4" })
144
378
  }
145
- )
379
+ ),
380
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
381
+ GroupView,
382
+ {
383
+ group: child,
384
+ level: level + 1,
385
+ fields,
386
+ tree,
387
+ dispatch,
388
+ defaultField,
389
+ errorByRuleId,
390
+ t
391
+ }
392
+ ) })
393
+ ] }) });
394
+ }
395
+ function RuleRow({
396
+ rule,
397
+ fields,
398
+ dispatch,
399
+ errorByRuleId,
400
+ dragHandleProps,
401
+ t
402
+ }) {
403
+ const fieldType = getFieldType(fields, rule.field);
404
+ const operators = OPERATORS_BY_FIELD_TYPE[fieldType] ?? OPERATORS_BY_FIELD_TYPE.text;
405
+ const valueless = isValuelessOperator(rule.operator);
406
+ const errorMsg = errorByRuleId.get(rule.id);
407
+ const fieldDef = fields.find((f) => f.key === rule.field);
408
+ const [pickerOpen, setPickerOpen] = React.useState(false);
409
+ const fieldBtnRef = React.useRef(null);
410
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", "data-testid": "filter-rule", "data-filter-rule-id": rule.id, children: [
411
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start gap-2", children: [
412
+ /* @__PURE__ */ jsx(
413
+ "button",
414
+ {
415
+ type: "button",
416
+ ...dragHandleProps,
417
+ "data-testid": "filter-drag-handle",
418
+ className: "mt-1 text-muted-foreground/60 hover:text-muted-foreground cursor-grab outline-none focus-visible:shadow-focus rounded-sm",
419
+ "aria-label": t("ui.advancedFilter.dragHandle", "Drag to reorder"),
420
+ children: /* @__PURE__ */ jsx(GripVertical, { className: "size-4" })
421
+ }
422
+ ),
423
+ /* @__PURE__ */ jsx(
424
+ Button,
425
+ {
426
+ ref: fieldBtnRef,
427
+ type: "button",
428
+ variant: "outline",
429
+ size: "sm",
430
+ onClick: () => setPickerOpen(true),
431
+ className: "min-w-[140px] justify-start",
432
+ children: fieldDef?.label ?? t("ui.advancedFilter.selectField", "Select field")
433
+ }
434
+ ),
435
+ /* @__PURE__ */ jsx(
436
+ FilterFieldPicker,
437
+ {
438
+ fields,
439
+ open: pickerOpen,
440
+ onOpenChange: setPickerOpen,
441
+ onSelect: (f) => {
442
+ const nextType = getFieldType(fields, f.key);
443
+ dispatch({
444
+ type: "updateRule",
445
+ ruleId: rule.id,
446
+ updates: { field: f.key, operator: getDefaultOperator(nextType), value: "" }
447
+ });
448
+ },
449
+ triggerRef: fieldBtnRef
450
+ }
451
+ ),
452
+ /* @__PURE__ */ jsxs(
453
+ Select,
454
+ {
455
+ value: rule.operator,
456
+ onValueChange: (next) => {
457
+ const nextOp = next;
458
+ const sameShape = getOperatorShape(rule.operator) === getOperatorShape(nextOp);
459
+ const updates = sameShape ? { operator: nextOp } : { operator: nextOp, value: getOperatorShape(nextOp) === "multi" || getOperatorShape(nextOp) === "range" ? [] : "" };
460
+ dispatch({ type: "updateRule", ruleId: rule.id, updates });
461
+ },
462
+ children: [
463
+ /* @__PURE__ */ jsx(
464
+ SelectTrigger,
465
+ {
466
+ size: "sm",
467
+ className: "w-auto min-w-[140px]",
468
+ "aria-label": t("ui.advancedFilter.selectOperator", "Select operator"),
469
+ children: /* @__PURE__ */ jsx(SelectValue, {})
470
+ }
471
+ ),
472
+ /* @__PURE__ */ jsx(SelectContent, { "data-advanced-filter-portal": "", children: operators.map((op) => /* @__PURE__ */ jsx(SelectItem, { value: op, children: t(`ui.advancedFilter.operator.${op}`, OPERATOR_LABELS[op]) }, op)) })
473
+ ]
474
+ }
475
+ ),
476
+ !valueless ? /* @__PURE__ */ jsx(
477
+ ValueInput,
478
+ {
479
+ rule,
480
+ fields,
481
+ fieldType,
482
+ dispatch,
483
+ hasError: !!errorMsg,
484
+ t
485
+ }
486
+ ) : null,
487
+ /* @__PURE__ */ jsx(
488
+ IconButton,
489
+ {
490
+ variant: "ghost",
491
+ size: "sm",
492
+ type: "button",
493
+ onClick: () => dispatch({ type: "removeNode", nodeId: rule.id }),
494
+ "aria-label": t("ui.advancedFilter.removeCondition", "Remove condition"),
495
+ title: t("ui.advancedFilter.removeCondition", "Remove condition"),
496
+ children: /* @__PURE__ */ jsx(Trash2, { className: "size-4 text-muted-foreground" })
497
+ }
498
+ )
499
+ ] }),
500
+ errorMsg ? /* @__PURE__ */ jsx("div", { className: "ml-6 pl-12 text-xs text-status-error-text", children: errorMsg }) : null
146
501
  ] });
147
502
  }
148
503
  function ValueInput({
149
- condition,
504
+ rule,
150
505
  fields,
151
506
  fieldType,
152
- onUpdate,
507
+ dispatch,
508
+ hasError,
153
509
  t
154
510
  }) {
155
- const fieldDef = fields.find((f) => f.key === condition.field);
156
- const value = condition.value;
511
+ const fieldDef = fields.find((f) => f.key === rule.field);
512
+ const value = rule.value;
513
+ const errorClass = hasError ? "border-status-error-border ring-1 ring-status-error-border focus-visible:border-status-error-border focus-within:border-status-error-border" : "";
157
514
  if (fieldType === "select" && fieldDef?.options) {
515
+ if (getOperatorShape(rule.operator) === "multi") {
516
+ return /* @__PURE__ */ jsx(
517
+ MultiValuePicker,
518
+ {
519
+ rule,
520
+ options: fieldDef.options,
521
+ dispatch,
522
+ errorClass,
523
+ t
524
+ }
525
+ );
526
+ }
158
527
  return /* @__PURE__ */ jsxs(
159
528
  Select,
160
529
  {
161
- value: typeof value === "string" && value ? value : void 0,
162
- onValueChange: (next) => onUpdate(condition.id, { value: next ?? "" }),
530
+ value: typeof value === "string" && value.length ? value : void 0,
531
+ onValueChange: (next) => dispatch({ type: "updateRule", ruleId: rule.id, updates: { value: next ?? "" } }),
163
532
  children: [
164
533
  /* @__PURE__ */ jsx(
165
534
  SelectTrigger,
166
535
  {
167
- className: "min-w-[140px]",
536
+ size: "sm",
537
+ className: `w-auto min-w-[160px] ${errorClass}`,
168
538
  "aria-label": t("ui.advancedFilter.selectValue", "Select value"),
169
539
  children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("ui.advancedFilter.selectValuePlaceholder", "Select...") })
170
540
  }
171
541
  ),
172
- /* @__PURE__ */ jsx(SelectContent, { children: fieldDef.options.map((opt) => /* @__PURE__ */ jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })
542
+ /* @__PURE__ */ jsx(SelectContent, { "data-advanced-filter-portal": "", children: fieldDef.options.map((opt) => /* @__PURE__ */ jsx(SelectItem, { value: opt.value, children: opt.tone ? /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2", children: [
543
+ /* @__PURE__ */ jsx("span", { className: toneDotClass(opt.tone), "aria-hidden": "true" }),
544
+ opt.label
545
+ ] }) : opt.label }, opt.value)) })
173
546
  ]
174
547
  }
175
548
  );
176
549
  }
550
+ if (rule.operator === "between" && (fieldType === "number" || fieldType === "date")) {
551
+ return /* @__PURE__ */ jsx(
552
+ BetweenInput,
553
+ {
554
+ rule,
555
+ fieldType,
556
+ dispatch,
557
+ errorClass,
558
+ t
559
+ }
560
+ );
561
+ }
177
562
  if (fieldType === "date") {
178
563
  return /* @__PURE__ */ jsx(
179
- "input",
564
+ Input,
180
565
  {
181
566
  type: "date",
182
- className: "rounded border bg-background px-2 py-1.5 text-sm",
567
+ size: "sm",
568
+ className: `w-auto min-w-[160px] ${errorClass}`,
183
569
  value: typeof value === "string" ? value : "",
184
- onChange: (e) => onUpdate(condition.id, { value: e.target.value }),
570
+ onChange: (e) => dispatch({ type: "updateRule", ruleId: rule.id, updates: { value: e.target.value } }),
185
571
  "aria-label": t("ui.advancedFilter.dateValue", "Date value")
186
572
  }
187
573
  );
@@ -192,9 +578,9 @@ function ValueInput({
192
578
  {
193
579
  type: "number",
194
580
  size: "sm",
195
- className: "w-[120px]",
581
+ className: `w-[120px] ${errorClass}`,
196
582
  value: typeof value === "number" ? value : typeof value === "string" ? value : "",
197
- onChange: (e) => onUpdate(condition.id, { value: e.target.value }),
583
+ onChange: (e) => dispatch({ type: "updateRule", ruleId: rule.id, updates: { value: e.target.value } }),
198
584
  placeholder: t("ui.advancedFilter.numberPlaceholder", "Value"),
199
585
  "aria-label": t("ui.advancedFilter.numberValue", "Number value")
200
586
  }
@@ -205,101 +591,241 @@ function ValueInput({
205
591
  {
206
592
  type: "text",
207
593
  size: "sm",
208
- className: "min-w-[140px]",
594
+ className: `w-auto min-w-[160px] ${errorClass}`,
209
595
  value: typeof value === "string" ? value : "",
210
- onChange: (e) => onUpdate(condition.id, { value: e.target.value }),
596
+ onChange: (e) => dispatch({ type: "updateRule", ruleId: rule.id, updates: { value: e.target.value } }),
211
597
  placeholder: t("ui.advancedFilter.textPlaceholder", "Value..."),
212
598
  "aria-label": t("ui.advancedFilter.textValue", "Text value")
213
599
  }
214
600
  );
215
601
  }
216
- function AdvancedFilterBuilder({
217
- fields,
218
- value,
219
- onChange,
220
- onApply,
221
- onClear
602
+ function BetweenInput({
603
+ rule,
604
+ fieldType,
605
+ dispatch,
606
+ errorClass,
607
+ t
222
608
  }) {
223
- const t = useT();
224
- const normalizedValue = React.useMemo(() => normalizeAdvancedFilterState(value), [value]);
225
- const emitChange = React.useCallback((next) => {
226
- onChange(normalizeAdvancedFilterState(next));
227
- }, [onChange]);
228
- const updateCondition = React.useCallback((id, updates) => {
229
- emitChange({
230
- ...normalizedValue,
231
- conditions: normalizedValue.conditions.map((c) => c.id === id ? { ...c, ...updates } : c)
232
- });
233
- }, [emitChange, normalizedValue]);
234
- const removeCondition = React.useCallback((id) => {
235
- emitChange({
236
- ...normalizedValue,
237
- conditions: normalizedValue.conditions.filter((c) => c.id !== id)
238
- });
239
- }, [emitChange, normalizedValue]);
240
- const addCondition = React.useCallback(() => {
241
- const newCondition = createEmptyCondition();
242
- if (fields.length > 0) {
243
- newCondition.field = fields[0].key;
244
- newCondition.operator = getDefaultOperator(fields[0].type);
245
- }
246
- emitChange({
247
- ...normalizedValue,
248
- conditions: [...normalizedValue.conditions, newCondition]
249
- });
250
- }, [emitChange, fields, normalizedValue]);
251
- const toggleConditionJoin = React.useCallback((id) => {
252
- emitChange({
253
- ...normalizedValue,
254
- conditions: normalizedValue.conditions.map((condition) => condition.id === id ? { ...condition, join: condition.join === "or" ? "and" : "or" } : condition)
255
- });
256
- }, [emitChange, normalizedValue]);
257
- return /* @__PURE__ */ jsxs("div", { className: "inline-flex flex-col gap-3 p-3", children: [
258
- normalizedValue.conditions.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("ui.advancedFilter.noConditions", 'No filter conditions. Click "Add filter" to start.') }) : /* @__PURE__ */ jsx("div", { className: "space-y-2", children: normalizedValue.conditions.map((condition, index) => /* @__PURE__ */ jsx(
259
- ConditionRow,
609
+ const pair = Array.isArray(rule.value) ? rule.value : [];
610
+ const start = typeof pair[0] === "string" || typeof pair[0] === "number" ? String(pair[0] ?? "") : "";
611
+ const end = typeof pair[1] === "string" || typeof pair[1] === "number" ? String(pair[1] ?? "") : "";
612
+ const update = (which, next) => {
613
+ const value = which === "start" ? [next, end] : [start, next];
614
+ dispatch({ type: "updateRule", ruleId: rule.id, updates: { value } });
615
+ };
616
+ const inputType = fieldType === "date" ? "date" : "number";
617
+ const inputWidth = fieldType === "date" ? "w-auto min-w-[140px]" : "w-[110px]";
618
+ return /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-1", children: [
619
+ /* @__PURE__ */ jsx(
620
+ Input,
260
621
  {
261
- condition,
262
- index,
263
- fields,
264
- join: condition.join ?? normalizedValue.logic,
265
- onUpdate: updateCondition,
266
- onRemove: removeCondition,
267
- onToggleJoin: toggleConditionJoin,
268
- t
269
- },
270
- condition.id
271
- )) }),
272
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3 pt-2", children: [
622
+ type: inputType,
623
+ size: "sm",
624
+ className: `${inputWidth} ${errorClass}`,
625
+ value: start,
626
+ onChange: (e) => update("start", e.target.value),
627
+ placeholder: fieldType === "number" ? t("ui.advancedFilter.numberPlaceholder", "Value") : void 0,
628
+ "aria-label": t("ui.advancedFilter.betweenStart", "Start")
629
+ }
630
+ ),
631
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("ui.advancedFilter.and", "and") }),
632
+ /* @__PURE__ */ jsx(
633
+ Input,
634
+ {
635
+ type: inputType,
636
+ size: "sm",
637
+ className: `${inputWidth} ${errorClass}`,
638
+ value: end,
639
+ onChange: (e) => update("end", e.target.value),
640
+ placeholder: fieldType === "number" ? t("ui.advancedFilter.numberPlaceholder", "Value") : void 0,
641
+ "aria-label": t("ui.advancedFilter.betweenEnd", "End")
642
+ }
643
+ )
644
+ ] });
645
+ }
646
+ function MultiValuePicker({
647
+ rule,
648
+ options,
649
+ dispatch,
650
+ errorClass,
651
+ t
652
+ }) {
653
+ const selected = Array.isArray(rule.value) ? rule.value.map((v) => String(v)).filter((v) => v.length > 0) : [];
654
+ const selectedLabel = (() => {
655
+ if (selected.length === 0) return t("ui.advancedFilter.selectValuePlaceholder", "Select...");
656
+ const firstOpt = options.find((o) => o.value === selected[0]);
657
+ const head = firstOpt?.label ?? selected[0];
658
+ const more = selected.length - 1;
659
+ return more > 0 ? `${head} +${more}` : head;
660
+ })();
661
+ const toggle = (val) => {
662
+ const next = selected.includes(val) ? selected.filter((v) => v !== val) : [...selected, val];
663
+ dispatch({ type: "updateRule", ruleId: rule.id, updates: { value: next } });
664
+ };
665
+ return /* @__PURE__ */ jsxs(Popover, { children: [
666
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
667
+ "button",
668
+ {
669
+ type: "button",
670
+ className: `inline-flex h-8 items-center justify-between gap-2 rounded-md border border-input bg-background px-2.5 text-xs shadow-xs transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:shadow-focus focus-visible:border-foreground w-auto min-w-[160px] ${errorClass}`,
671
+ "aria-label": t("ui.advancedFilter.selectValue", "Select value"),
672
+ "data-testid": "multi-value-trigger",
673
+ children: [
674
+ /* @__PURE__ */ jsx("span", { className: selected.length === 0 ? "text-muted-foreground" : "", children: selectedLabel }),
675
+ /* @__PURE__ */ jsx(ChevronDown, { className: "size-3 text-muted-foreground" })
676
+ ]
677
+ }
678
+ ) }),
679
+ /* @__PURE__ */ jsx(PopoverContent, { className: "w-64 p-1", align: "start", "data-advanced-filter-portal": "", children: /* @__PURE__ */ jsx("div", { className: "max-h-64 overflow-y-auto", role: "listbox", "aria-multiselectable": "true", children: options.map((opt) => {
680
+ const isOn = selected.includes(opt.value);
681
+ return (
682
+ // Raw <button>: needs `role="option"` + `aria-selected`; <Button> would override role.
683
+ /* @__PURE__ */ jsxs(
684
+ "button",
685
+ {
686
+ type: "button",
687
+ role: "option",
688
+ "aria-selected": isOn,
689
+ onClick: () => toggle(opt.value),
690
+ className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm hover:bg-accent focus-visible:bg-accent focus-visible:outline-none",
691
+ children: [
692
+ /* @__PURE__ */ jsx(Checkbox, { checked: isOn, "aria-hidden": "true", tabIndex: -1, className: "pointer-events-none" }),
693
+ opt.tone ? /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2", children: [
694
+ /* @__PURE__ */ jsx("span", { className: toneDotClass(opt.tone), "aria-hidden": "true" }),
695
+ opt.label
696
+ ] }) : /* @__PURE__ */ jsx("span", { children: opt.label })
697
+ ]
698
+ },
699
+ opt.value
700
+ )
701
+ );
702
+ }) }) })
703
+ ] });
704
+ }
705
+ function toneDotClass(tone) {
706
+ const base = "inline-block size-2 rounded-full shrink-0";
707
+ switch (tone) {
708
+ case "success":
709
+ return `${base} bg-status-success-icon`;
710
+ case "error":
711
+ return `${base} bg-status-error-icon`;
712
+ case "warning":
713
+ return `${base} bg-status-warning-icon`;
714
+ case "info":
715
+ return `${base} bg-status-info-icon`;
716
+ case "neutral":
717
+ return `${base} bg-status-neutral-icon`;
718
+ case "pink":
719
+ return `${base} bg-status-pink-icon`;
720
+ case "brand":
721
+ return `${base} bg-brand-violet`;
722
+ default:
723
+ return `${base} bg-muted-foreground/40`;
724
+ }
725
+ }
726
+ function GroupToolbar({
727
+ group,
728
+ level,
729
+ tree,
730
+ dispatch,
731
+ defaultField,
732
+ fields,
733
+ isRoot,
734
+ t
735
+ }) {
736
+ const [pickerOpen, setPickerOpen] = React.useState(false);
737
+ const addBtnRef = React.useRef(null);
738
+ const addRuleEnabled = canAddRule(tree, group.id);
739
+ const addGroupEnabled = canAddGroup(tree, group.id);
740
+ const widthCapHit = group.children.length >= TREE_LIMITS.maxChildrenPerGroup;
741
+ const depthCapHit = level >= TREE_LIMITS.maxGroupLevel;
742
+ const addRuleTitle = !addRuleEnabled ? widthCapHit ? t("ui.advancedFilter.limitWidthReached", "Maximum {max} conditions per group", {
743
+ max: TREE_LIMITS.maxChildrenPerGroup
744
+ }) : t("ui.advancedFilter.limitTotalReached", "Maximum {max} conditions reached", {
745
+ max: TREE_LIMITS.maxTotalRules
746
+ }) : void 0;
747
+ const addGroupTitle = !addGroupEnabled ? depthCapHit ? t("ui.advancedFilter.limitDepthReached", "Maximum nesting depth reached") : widthCapHit ? t("ui.advancedFilter.limitWidthReached", "Maximum {max} conditions per group", {
748
+ max: TREE_LIMITS.maxChildrenPerGroup
749
+ }) : t("ui.advancedFilter.limitTotalReached", "Maximum {max} conditions reached", {
750
+ max: TREE_LIMITS.maxTotalRules
751
+ }) : void 0;
752
+ const addGroupLabel = level === 1 ? t("ui.advancedFilter.addGroup", "+ Add group") : t("ui.advancedFilter.addSubgroup", "+ Add subgroup");
753
+ const ctaClass = "text-brand-violet hover:text-brand-violet/80 hover:bg-transparent px-0 disabled:text-muted-foreground/50 disabled:hover:text-muted-foreground/50";
754
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
755
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
756
+ /* @__PURE__ */ jsx(
757
+ AndOrToggle,
758
+ {
759
+ combinator: group.combinator,
760
+ onChange: (c) => dispatch({ type: "updateGroupCombinator", groupId: group.id, combinator: c }),
761
+ t
762
+ }
763
+ ),
273
764
  /* @__PURE__ */ jsxs(
274
765
  Button,
275
766
  {
767
+ ref: addBtnRef,
276
768
  type: "button",
277
769
  variant: "ghost",
278
770
  size: "sm",
279
- className: "h-auto px-0 py-0 text-base font-medium text-primary hover:bg-transparent hover:text-primary/90",
280
- onClick: addCondition,
771
+ disabled: !addRuleEnabled,
772
+ title: addRuleTitle,
773
+ onClick: () => setPickerOpen(true),
774
+ className: ctaClass,
281
775
  children: [
282
776
  /* @__PURE__ */ jsx(Plus, { className: "size-4" }),
283
- t("ui.advancedFilter.addFilter", "Add filter")
777
+ t("ui.advancedFilter.addCondition", "+ Add condition")
284
778
  ]
285
779
  }
286
780
  ),
287
- normalizedValue.conditions.length > 0 ? /* @__PURE__ */ jsxs(
781
+ /* @__PURE__ */ jsx(
782
+ FilterFieldPicker,
783
+ {
784
+ fields,
785
+ open: pickerOpen,
786
+ onOpenChange: setPickerOpen,
787
+ onSelect: (f) => dispatch({
788
+ type: "addRule",
789
+ groupId: group.id,
790
+ defaultField: f.key,
791
+ defaultOperator: getDefaultOperator(f.type)
792
+ }),
793
+ triggerRef: addBtnRef
794
+ }
795
+ ),
796
+ /* @__PURE__ */ jsxs(
288
797
  Button,
289
798
  {
290
799
  type: "button",
291
800
  variant: "ghost",
292
801
  size: "sm",
293
- className: "h-auto px-0 py-0 text-base text-muted-foreground hover:bg-transparent hover:text-foreground",
294
- onClick: onClear,
802
+ disabled: !addGroupEnabled,
803
+ title: addGroupTitle,
804
+ onClick: () => dispatch({ type: "addGroup", groupId: group.id, defaultField }),
805
+ className: ctaClass,
295
806
  children: [
296
- /* @__PURE__ */ jsx(X, { className: "size-4" }),
297
- t("ui.advancedFilter.clear", "Clear")
807
+ /* @__PURE__ */ jsx(Plus, { className: "size-4" }),
808
+ addGroupLabel
298
809
  ]
299
810
  }
300
- ) : null,
301
- normalizedValue.conditions.length > 0 ? /* @__PURE__ */ jsx(Button, { type: "button", className: "ml-auto min-w-[8rem]", onClick: onApply, children: t("ui.advancedFilter.apply", "Apply") }) : null
302
- ] })
811
+ ),
812
+ !isRoot ? /* @__PURE__ */ jsx(
813
+ IconButton,
814
+ {
815
+ variant: "ghost",
816
+ size: "sm",
817
+ type: "button",
818
+ onClick: () => dispatch({ type: "removeNode", nodeId: group.id }),
819
+ "aria-label": t("ui.advancedFilter.deleteGroup", "Delete group"),
820
+ title: t("ui.advancedFilter.deleteGroup", "Delete group"),
821
+ className: "ml-auto",
822
+ children: /* @__PURE__ */ jsx(X, { className: "size-4 text-muted-foreground" })
823
+ }
824
+ ) : null
825
+ ] }),
826
+ !addGroupEnabled && depthCapHit ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", "data-testid": "filter-depth-limit-hint", children: t("ui.advancedFilter.limitDepthHint", "You've reached the maximum nesting depth ({max} levels). Switch this group's combinator with the toggle above instead of nesting deeper.", {
827
+ max: TREE_LIMITS.maxGroupLevel
828
+ }) }) : null
303
829
  ] });
304
830
  }
305
831
  export {