@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/backend/DataTable.js +149 -53
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/filters/ActiveFilterChips.js +142 -0
- package/dist/backend/filters/ActiveFilterChips.js.map +7 -0
- package/dist/backend/filters/AdvancedFilterBuilder.js +689 -163
- package/dist/backend/filters/AdvancedFilterBuilder.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterPanel.js +354 -0
- package/dist/backend/filters/AdvancedFilterPanel.js.map +7 -0
- package/dist/backend/filters/FilterEmptyState.js +41 -0
- package/dist/backend/filters/FilterEmptyState.js.map +7 -0
- package/dist/backend/filters/FilterFieldPicker.js +148 -0
- package/dist/backend/filters/FilterFieldPicker.js.map +7 -0
- package/dist/backend/filters/FilteredEmptyResults.js +26 -0
- package/dist/backend/filters/FilteredEmptyResults.js.map +7 -0
- package/dist/backend/filters/QuickFilters.js +41 -0
- package/dist/backend/filters/QuickFilters.js.map +7 -0
- package/dist/backend/filters/filterValidation.js +47 -0
- package/dist/backend/filters/filterValidation.js.map +7 -0
- package/dist/backend/filters/treeReducer.js +159 -0
- package/dist/backend/filters/treeReducer.js.map +7 -0
- package/dist/backend/hooks/useAdvancedFilter.js +82 -1
- package/dist/backend/hooks/useAdvancedFilter.js.map +2 -2
- package/dist/backend/utils/api.js +18 -1
- package/dist/backend/utils/api.js.map +2 -2
- package/dist/backend/utils/useAutoDiscoveredFields.js +1 -0
- package/dist/backend/utils/useAutoDiscoveredFields.js.map +2 -2
- package/dist/backend/utils/useCurrentUserId.js +31 -0
- package/dist/backend/utils/useCurrentUserId.js.map +7 -0
- package/dist/primitives/dialog.js +7 -5
- package/dist/primitives/dialog.js.map +2 -2
- package/dist/primitives/tag.js +4 -2
- package/dist/primitives/tag.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/DataTable.tsx +307 -63
- package/src/backend/__tests__/DataTable.extensions.test.tsx +170 -0
- package/src/backend/filters/ActiveFilterChips.tsx +179 -0
- package/src/backend/filters/AdvancedFilterBuilder.tsx +840 -237
- package/src/backend/filters/AdvancedFilterPanel.tsx +456 -0
- package/src/backend/filters/FilterEmptyState.tsx +51 -0
- package/src/backend/filters/FilterFieldPicker.tsx +139 -0
- package/src/backend/filters/FilteredEmptyResults.tsx +40 -0
- package/src/backend/filters/QuickFilters.tsx +62 -0
- package/src/backend/filters/__tests__/ActiveFilterChips.test.tsx +75 -0
- package/src/backend/filters/__tests__/AdvancedFilterBuilder.test.tsx +210 -0
- package/src/backend/filters/__tests__/AdvancedFilterPanel.test.tsx +212 -0
- package/src/backend/filters/__tests__/FilterEmptyState.test.tsx +31 -0
- package/src/backend/filters/__tests__/FilterFieldPicker.test.tsx +61 -0
- package/src/backend/filters/__tests__/FilteredEmptyResults.test.tsx +35 -0
- package/src/backend/filters/__tests__/QuickFilters.test.tsx +34 -0
- package/src/backend/filters/__tests__/filterValidation.test.ts +69 -0
- package/src/backend/filters/__tests__/treeReducer.test.ts +189 -0
- package/src/backend/filters/filterValidation.ts +61 -0
- package/src/backend/filters/treeReducer.ts +201 -0
- package/src/backend/hooks/__tests__/useAdvancedFilter.test.tsx +213 -0
- package/src/backend/hooks/useAdvancedFilter.ts +130 -0
- package/src/backend/utils/api.ts +27 -2
- package/src/backend/utils/useAutoDiscoveredFields.ts +2 -0
- package/src/backend/utils/useCurrentUserId.ts +40 -0
- package/src/primitives/__tests__/zindex-overlay.test.tsx +22 -0
- package/src/primitives/dialog.tsx +18 -7
- 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 {
|
|
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
|
-
|
|
51
|
-
return field?.type ?? "text";
|
|
72
|
+
return fields.find((f) => f.key === fieldKey)?.type ?? "text";
|
|
52
73
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
89
|
+
value,
|
|
90
|
+
onChange,
|
|
91
|
+
onApply,
|
|
92
|
+
onClear,
|
|
93
|
+
pendingErrors
|
|
62
94
|
}) {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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(
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
256
|
+
/* @__PURE__ */ jsx(
|
|
257
|
+
SortableChildrenList,
|
|
127
258
|
{
|
|
128
|
-
|
|
259
|
+
group,
|
|
260
|
+
level,
|
|
129
261
|
fields,
|
|
130
|
-
|
|
131
|
-
|
|
262
|
+
tree,
|
|
263
|
+
dispatch,
|
|
264
|
+
defaultField,
|
|
265
|
+
errorByRuleId,
|
|
132
266
|
t
|
|
133
267
|
}
|
|
134
|
-
)
|
|
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
|
-
|
|
370
|
+
"button",
|
|
137
371
|
{
|
|
138
|
-
variant: "ghost",
|
|
139
|
-
size: "sm",
|
|
140
372
|
type: "button",
|
|
141
|
-
|
|
142
|
-
"
|
|
143
|
-
|
|
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
|
-
|
|
504
|
+
rule,
|
|
150
505
|
fields,
|
|
151
506
|
fieldType,
|
|
152
|
-
|
|
507
|
+
dispatch,
|
|
508
|
+
hasError,
|
|
153
509
|
t
|
|
154
510
|
}) {
|
|
155
|
-
const fieldDef = fields.find((f) => f.key ===
|
|
156
|
-
const 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) =>
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
564
|
+
Input,
|
|
180
565
|
{
|
|
181
566
|
type: "date",
|
|
182
|
-
|
|
567
|
+
size: "sm",
|
|
568
|
+
className: `w-auto min-w-[160px] ${errorClass}`,
|
|
183
569
|
value: typeof value === "string" ? value : "",
|
|
184
|
-
onChange: (e) =>
|
|
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:
|
|
581
|
+
className: `w-[120px] ${errorClass}`,
|
|
196
582
|
value: typeof value === "number" ? value : typeof value === "string" ? value : "",
|
|
197
|
-
onChange: (e) =>
|
|
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:
|
|
594
|
+
className: `w-auto min-w-[160px] ${errorClass}`,
|
|
209
595
|
value: typeof value === "string" ? value : "",
|
|
210
|
-
onChange: (e) =>
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
602
|
+
function BetweenInput({
|
|
603
|
+
rule,
|
|
604
|
+
fieldType,
|
|
605
|
+
dispatch,
|
|
606
|
+
errorClass,
|
|
607
|
+
t
|
|
222
608
|
}) {
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
280
|
-
|
|
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.
|
|
777
|
+
t("ui.advancedFilter.addCondition", "+ Add condition")
|
|
284
778
|
]
|
|
285
779
|
}
|
|
286
780
|
),
|
|
287
|
-
|
|
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
|
-
|
|
294
|
-
|
|
802
|
+
disabled: !addGroupEnabled,
|
|
803
|
+
title: addGroupTitle,
|
|
804
|
+
onClick: () => dispatch({ type: "addGroup", groupId: group.id, defaultField }),
|
|
805
|
+
className: ctaClass,
|
|
295
806
|
children: [
|
|
296
|
-
/* @__PURE__ */ jsx(
|
|
297
|
-
|
|
807
|
+
/* @__PURE__ */ jsx(Plus, { className: "size-4" }),
|
|
808
|
+
addGroupLabel
|
|
298
809
|
]
|
|
299
810
|
}
|
|
300
|
-
)
|
|
301
|
-
|
|
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 {
|