@bmsuisse/datagrid 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/dist/index.d.ts +1027 -0
- package/dist/index.js +2494 -0
- package/dist/index.js.map +1 -0
- package/package.json +78 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2494 @@
|
|
|
1
|
+
// src/filter/types.ts
|
|
2
|
+
function isFilterDescriptor(node) {
|
|
3
|
+
return "operator" in node;
|
|
4
|
+
}
|
|
5
|
+
function isCompositeFilterDescriptor(node) {
|
|
6
|
+
return "logic" in node;
|
|
7
|
+
}
|
|
8
|
+
var UNARY_OPERATORS = /* @__PURE__ */ new Set([
|
|
9
|
+
"isEmpty",
|
|
10
|
+
"isNotEmpty",
|
|
11
|
+
"isNull",
|
|
12
|
+
"isNotNull"
|
|
13
|
+
]);
|
|
14
|
+
function fieldKey(field) {
|
|
15
|
+
return Array.isArray(field) ? field.join(".") : field;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/filter/evaluate.ts
|
|
19
|
+
import { parseISO } from "date-fns";
|
|
20
|
+
function getFieldValue(row, field) {
|
|
21
|
+
const segments = fieldKey(field).split(".");
|
|
22
|
+
let current = row;
|
|
23
|
+
for (const segment of segments) {
|
|
24
|
+
if (current === null || typeof current !== "object") return void 0;
|
|
25
|
+
current = current[segment];
|
|
26
|
+
}
|
|
27
|
+
return current;
|
|
28
|
+
}
|
|
29
|
+
function isNullish(value) {
|
|
30
|
+
return value === null || value === void 0;
|
|
31
|
+
}
|
|
32
|
+
function isEmptyValue(value) {
|
|
33
|
+
if (isNullish(value)) return true;
|
|
34
|
+
if (typeof value === "string") return value.length === 0;
|
|
35
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
function toEpochMs(value) {
|
|
39
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.getTime();
|
|
40
|
+
if (typeof value === "number") return value;
|
|
41
|
+
if (typeof value === "string") {
|
|
42
|
+
const parsed = parseISO(value);
|
|
43
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.getTime();
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function asFiniteNumber(value) {
|
|
48
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
49
|
+
if (value.trim() === "") return null;
|
|
50
|
+
const parsed = Number(value);
|
|
51
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
52
|
+
}
|
|
53
|
+
function normalizeForCompare(value) {
|
|
54
|
+
if (isNullish(value)) return null;
|
|
55
|
+
if (typeof value === "number") return value;
|
|
56
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
57
|
+
return String(value);
|
|
58
|
+
}
|
|
59
|
+
function compareValues(a, b) {
|
|
60
|
+
if (a instanceof Date || b instanceof Date) {
|
|
61
|
+
const ta = toEpochMs(a);
|
|
62
|
+
const tb = toEpochMs(b);
|
|
63
|
+
return ta === null || tb === null ? null : ta - tb;
|
|
64
|
+
}
|
|
65
|
+
const na = normalizeForCompare(a);
|
|
66
|
+
const nb = normalizeForCompare(b);
|
|
67
|
+
if (na === null || nb === null) return null;
|
|
68
|
+
const numA = asFiniteNumber(na);
|
|
69
|
+
const numB = asFiniteNumber(nb);
|
|
70
|
+
if (numA !== null && numB !== null) return numA - numB;
|
|
71
|
+
const sa = String(na);
|
|
72
|
+
const sb = String(nb);
|
|
73
|
+
if (sa < sb) return -1;
|
|
74
|
+
if (sa > sb) return 1;
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
function valuesEqual(rowValue, target, ignoreCase) {
|
|
78
|
+
if (ignoreCase && typeof rowValue === "string" && typeof target === "string") {
|
|
79
|
+
return rowValue.toLowerCase() === target.toLowerCase();
|
|
80
|
+
}
|
|
81
|
+
if (rowValue === target) return true;
|
|
82
|
+
return compareValues(rowValue, target) === 0;
|
|
83
|
+
}
|
|
84
|
+
function stringMatch(rowValue, needle, ignoreCase, test) {
|
|
85
|
+
if (typeof rowValue !== "string" || typeof needle !== "string") return false;
|
|
86
|
+
const haystack = ignoreCase ? rowValue.toLowerCase() : rowValue;
|
|
87
|
+
const target = ignoreCase ? needle.toLowerCase() : needle;
|
|
88
|
+
return test(haystack, target);
|
|
89
|
+
}
|
|
90
|
+
function isTwoElementArray(value) {
|
|
91
|
+
return Array.isArray(value) && value.length === 2;
|
|
92
|
+
}
|
|
93
|
+
function evaluateLeaf(row, filter) {
|
|
94
|
+
const rowValue = getFieldValue(row, filter.field);
|
|
95
|
+
const { operator, value, ignoreCase } = filter;
|
|
96
|
+
switch (operator) {
|
|
97
|
+
case "isNull":
|
|
98
|
+
return isNullish(rowValue);
|
|
99
|
+
case "isNotNull":
|
|
100
|
+
return !isNullish(rowValue);
|
|
101
|
+
case "isEmpty":
|
|
102
|
+
return isEmptyValue(rowValue);
|
|
103
|
+
case "isNotEmpty":
|
|
104
|
+
return !isEmptyValue(rowValue);
|
|
105
|
+
case "eq":
|
|
106
|
+
return !isNullish(rowValue) && valuesEqual(rowValue, value, ignoreCase);
|
|
107
|
+
case "neq":
|
|
108
|
+
return !isNullish(rowValue) && !valuesEqual(rowValue, value, ignoreCase);
|
|
109
|
+
case "lt":
|
|
110
|
+
case "lte":
|
|
111
|
+
case "gt":
|
|
112
|
+
case "gte": {
|
|
113
|
+
const cmp = compareValues(rowValue, value);
|
|
114
|
+
if (cmp === null) return false;
|
|
115
|
+
if (operator === "lt") return cmp < 0;
|
|
116
|
+
if (operator === "lte") return cmp <= 0;
|
|
117
|
+
if (operator === "gt") return cmp > 0;
|
|
118
|
+
return cmp >= 0;
|
|
119
|
+
}
|
|
120
|
+
case "between": {
|
|
121
|
+
if (isNullish(rowValue) || !isTwoElementArray(value)) return false;
|
|
122
|
+
const [boundA, boundB] = value;
|
|
123
|
+
const cmpA = compareValues(rowValue, boundA);
|
|
124
|
+
const cmpB = compareValues(rowValue, boundB);
|
|
125
|
+
if (cmpA === null || cmpB === null) return false;
|
|
126
|
+
return cmpA >= 0 && cmpB <= 0 || cmpA <= 0 && cmpB >= 0;
|
|
127
|
+
}
|
|
128
|
+
case "in":
|
|
129
|
+
return !isNullish(rowValue) && Array.isArray(value) && value.some((candidate) => valuesEqual(rowValue, candidate, ignoreCase));
|
|
130
|
+
case "notIn":
|
|
131
|
+
return !isNullish(rowValue) && Array.isArray(value) && !value.some((candidate) => valuesEqual(rowValue, candidate, ignoreCase));
|
|
132
|
+
case "contains":
|
|
133
|
+
return stringMatch(rowValue, value, ignoreCase, (h, n) => h.includes(n));
|
|
134
|
+
case "doesNotContain":
|
|
135
|
+
return !isNullish(rowValue) && !stringMatch(rowValue, value, ignoreCase, (h, n) => h.includes(n));
|
|
136
|
+
case "startsWith":
|
|
137
|
+
return stringMatch(rowValue, value, ignoreCase, (h, n) => h.startsWith(n));
|
|
138
|
+
case "endsWith":
|
|
139
|
+
return stringMatch(rowValue, value, ignoreCase, (h, n) => h.endsWith(n));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function evaluateFilter(row, filter) {
|
|
143
|
+
if (!filter) return true;
|
|
144
|
+
if (isCompositeFilterDescriptor(filter)) {
|
|
145
|
+
return filter.logic === "and" ? filter.filters.every((child) => evaluateFilter(row, child)) : filter.filters.some((child) => evaluateFilter(row, child));
|
|
146
|
+
}
|
|
147
|
+
return evaluateLeaf(row, filter);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/filter/BooleanFilter.tsx
|
|
151
|
+
import { FunnelIcon } from "@heroicons/react/24/outline";
|
|
152
|
+
|
|
153
|
+
// src/components/ui/button.tsx
|
|
154
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
155
|
+
import { cva } from "class-variance-authority";
|
|
156
|
+
import { forwardRef } from "react";
|
|
157
|
+
|
|
158
|
+
// src/lib/utils.ts
|
|
159
|
+
import { clsx } from "clsx";
|
|
160
|
+
import { twMerge } from "tailwind-merge";
|
|
161
|
+
function cn(...inputs) {
|
|
162
|
+
return twMerge(clsx(inputs));
|
|
163
|
+
}
|
|
164
|
+
function stopRowClick(event) {
|
|
165
|
+
event.stopPropagation();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/components/ui/button.tsx
|
|
169
|
+
import { jsx } from "react/jsx-runtime";
|
|
170
|
+
var buttonVariants = cva(
|
|
171
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
|
172
|
+
{
|
|
173
|
+
variants: {
|
|
174
|
+
variant: {
|
|
175
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
176
|
+
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
177
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
178
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
179
|
+
},
|
|
180
|
+
size: {
|
|
181
|
+
default: "h-9 px-3",
|
|
182
|
+
sm: "h-8 px-2 text-xs",
|
|
183
|
+
icon: "h-9 w-9"
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
defaultVariants: {
|
|
187
|
+
variant: "default",
|
|
188
|
+
size: "default"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
var Button = forwardRef(
|
|
193
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
194
|
+
const Comp = asChild ? Slot : "button";
|
|
195
|
+
return /* @__PURE__ */ jsx(Comp, { className: cn(buttonVariants({ variant, size, className })), ref, ...props });
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
Button.displayName = "Button";
|
|
199
|
+
|
|
200
|
+
// src/components/ui/popover.tsx
|
|
201
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
202
|
+
import { forwardRef as forwardRef2 } from "react";
|
|
203
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
204
|
+
var Popover = PopoverPrimitive.Root;
|
|
205
|
+
var PopoverTrigger = PopoverPrimitive.Trigger;
|
|
206
|
+
var PopoverContent = forwardRef2(({ className, align = "start", sideOffset = 4, ...props }, ref) => /* @__PURE__ */ jsx2(PopoverPrimitive.Portal, { children: /* @__PURE__ */ jsx2(
|
|
207
|
+
PopoverPrimitive.Content,
|
|
208
|
+
{
|
|
209
|
+
ref,
|
|
210
|
+
align,
|
|
211
|
+
sideOffset,
|
|
212
|
+
className: cn(
|
|
213
|
+
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none",
|
|
214
|
+
className
|
|
215
|
+
),
|
|
216
|
+
...props
|
|
217
|
+
}
|
|
218
|
+
) }));
|
|
219
|
+
PopoverContent.displayName = "PopoverContent";
|
|
220
|
+
|
|
221
|
+
// src/components/ui/select.tsx
|
|
222
|
+
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
223
|
+
import { Check, ChevronDown } from "lucide-react";
|
|
224
|
+
import { forwardRef as forwardRef3 } from "react";
|
|
225
|
+
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
226
|
+
var Select = SelectPrimitive.Root;
|
|
227
|
+
var SelectValue = SelectPrimitive.Value;
|
|
228
|
+
var SelectTrigger = forwardRef3(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
229
|
+
SelectPrimitive.Trigger,
|
|
230
|
+
{
|
|
231
|
+
ref,
|
|
232
|
+
className: cn(
|
|
233
|
+
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
234
|
+
className
|
|
235
|
+
),
|
|
236
|
+
...props,
|
|
237
|
+
children: [
|
|
238
|
+
children,
|
|
239
|
+
/* @__PURE__ */ jsx3(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx3(ChevronDown, { className: "h-4 w-4 opacity-50" }) })
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
));
|
|
243
|
+
SelectTrigger.displayName = "SelectTrigger";
|
|
244
|
+
var SelectContent = forwardRef3(({ className, children, position = "popper", ...props }, ref) => /* @__PURE__ */ jsx3(SelectPrimitive.Portal, { children: /* @__PURE__ */ jsx3(
|
|
245
|
+
SelectPrimitive.Content,
|
|
246
|
+
{
|
|
247
|
+
ref,
|
|
248
|
+
position,
|
|
249
|
+
className: cn(
|
|
250
|
+
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md",
|
|
251
|
+
position === "popper" && "translate-y-1",
|
|
252
|
+
className
|
|
253
|
+
),
|
|
254
|
+
...props,
|
|
255
|
+
children: /* @__PURE__ */ jsx3(SelectPrimitive.Viewport, { className: "p-1", children })
|
|
256
|
+
}
|
|
257
|
+
) }));
|
|
258
|
+
SelectContent.displayName = "SelectContent";
|
|
259
|
+
var SelectItem = forwardRef3(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
260
|
+
SelectPrimitive.Item,
|
|
261
|
+
{
|
|
262
|
+
ref,
|
|
263
|
+
className: cn(
|
|
264
|
+
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
265
|
+
className
|
|
266
|
+
),
|
|
267
|
+
...props,
|
|
268
|
+
children: [
|
|
269
|
+
/* @__PURE__ */ jsx3("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ jsx3(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx3(Check, { className: "h-4 w-4" }) }) }),
|
|
270
|
+
/* @__PURE__ */ jsx3(SelectPrimitive.ItemText, { children })
|
|
271
|
+
]
|
|
272
|
+
}
|
|
273
|
+
));
|
|
274
|
+
SelectItem.displayName = "SelectItem";
|
|
275
|
+
|
|
276
|
+
// src/filter/BooleanFilter.tsx
|
|
277
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
278
|
+
var BOOLEAN_CHOICES = [
|
|
279
|
+
{ value: "all", label: "All" },
|
|
280
|
+
{ value: "yes", label: "Yes" },
|
|
281
|
+
{ value: "no", label: "No" }
|
|
282
|
+
];
|
|
283
|
+
function choiceOf(value) {
|
|
284
|
+
if (!value) return "all";
|
|
285
|
+
if (value.operator === "eq") {
|
|
286
|
+
if (value.value === true) return "yes";
|
|
287
|
+
if (value.value === false) return "no";
|
|
288
|
+
return "all";
|
|
289
|
+
}
|
|
290
|
+
if (value.operator === "neq") {
|
|
291
|
+
if (value.value === true) return "no";
|
|
292
|
+
if (value.value === false) return "yes";
|
|
293
|
+
return "all";
|
|
294
|
+
}
|
|
295
|
+
return "all";
|
|
296
|
+
}
|
|
297
|
+
function BooleanFilter({
|
|
298
|
+
column,
|
|
299
|
+
value,
|
|
300
|
+
onChange,
|
|
301
|
+
bare = false
|
|
302
|
+
}) {
|
|
303
|
+
const choice = choiceOf(value);
|
|
304
|
+
const isFiltered = choice !== "all";
|
|
305
|
+
const summary = choice === "yes" ? "Yes" : choice === "no" ? "No" : "";
|
|
306
|
+
function emit(next) {
|
|
307
|
+
if (next === "all") {
|
|
308
|
+
onChange(void 0);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
onChange({ field: column.id, operator: "eq", value: next === "yes" });
|
|
312
|
+
}
|
|
313
|
+
const panel = /* @__PURE__ */ jsxs2(
|
|
314
|
+
Select,
|
|
315
|
+
{
|
|
316
|
+
value: choice,
|
|
317
|
+
onValueChange: (next) => {
|
|
318
|
+
const match = BOOLEAN_CHOICES.find((option) => option.value === next)?.value;
|
|
319
|
+
if (match) emit(match);
|
|
320
|
+
},
|
|
321
|
+
children: [
|
|
322
|
+
/* @__PURE__ */ jsx4(SelectTrigger, { "aria-label": `${column.header} filter`, children: /* @__PURE__ */ jsx4(SelectValue, {}) }),
|
|
323
|
+
/* @__PURE__ */ jsx4(SelectContent, { children: BOOLEAN_CHOICES.map((option) => /* @__PURE__ */ jsx4(SelectItem, { value: option.value, children: option.label }, option.value)) })
|
|
324
|
+
]
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
if (bare) {
|
|
328
|
+
return /* @__PURE__ */ jsx4("div", { "data-testid": `filter-${column.id}`, children: panel });
|
|
329
|
+
}
|
|
330
|
+
return /* @__PURE__ */ jsxs2(Popover, { children: [
|
|
331
|
+
/* @__PURE__ */ jsx4(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs2(
|
|
332
|
+
Button,
|
|
333
|
+
{
|
|
334
|
+
variant: "outline",
|
|
335
|
+
size: "sm",
|
|
336
|
+
className: cn("gap-1", isFiltered ? "max-w-[140px] px-2" : "w-8 justify-center px-0"),
|
|
337
|
+
"aria-label": `Filter ${column.header}`,
|
|
338
|
+
"data-testid": `filter-${column.id}`,
|
|
339
|
+
children: [
|
|
340
|
+
/* @__PURE__ */ jsx4(FunnelIcon, { className: "h-3.5 w-3.5 shrink-0 opacity-70", "aria-hidden": true }),
|
|
341
|
+
isFiltered && /* @__PURE__ */ jsx4("span", { className: "truncate", children: summary })
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
) }),
|
|
345
|
+
/* @__PURE__ */ jsx4(PopoverContent, { className: "w-40 p-2", children: panel })
|
|
346
|
+
] });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/filter/DateRangeFilter.tsx
|
|
350
|
+
import { CalendarDaysIcon } from "@heroicons/react/24/outline";
|
|
351
|
+
import { endOfDay, endOfMonth, format, parseISO as parseISO2, startOfDay, startOfMonth, subDays } from "date-fns";
|
|
352
|
+
|
|
353
|
+
// src/components/ui/calendar.tsx
|
|
354
|
+
import { DayPicker } from "react-day-picker";
|
|
355
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
356
|
+
function Calendar({ className, ...props }) {
|
|
357
|
+
return /* @__PURE__ */ jsx5(DayPicker, { className: cn("p-3", className), ...props });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/filter/DateRangeFilter.tsx
|
|
361
|
+
import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
362
|
+
var PRESETS = [
|
|
363
|
+
{
|
|
364
|
+
key: "today",
|
|
365
|
+
label: "Today",
|
|
366
|
+
range: (today) => ({ from: startOfDay(today), to: endOfDay(today) })
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
key: "last7",
|
|
370
|
+
label: "Last 7 days",
|
|
371
|
+
range: (today) => ({ from: startOfDay(subDays(today, 6)), to: endOfDay(today) })
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
key: "thisMonth",
|
|
375
|
+
label: "This month",
|
|
376
|
+
range: (today) => ({ from: startOfMonth(today), to: endOfMonth(today) })
|
|
377
|
+
}
|
|
378
|
+
];
|
|
379
|
+
function isStringPair(value) {
|
|
380
|
+
return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "string";
|
|
381
|
+
}
|
|
382
|
+
function parseBound(value) {
|
|
383
|
+
return parseISO2(value);
|
|
384
|
+
}
|
|
385
|
+
function rangeOf(value) {
|
|
386
|
+
if (!value || value.operator !== "between" || !isStringPair(value.value)) return void 0;
|
|
387
|
+
const from = parseBound(value.value[0]);
|
|
388
|
+
const to = parseBound(value.value[1]);
|
|
389
|
+
if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) return void 0;
|
|
390
|
+
return { from, to };
|
|
391
|
+
}
|
|
392
|
+
function isoOf(date, columnType) {
|
|
393
|
+
return columnType === "datetime" ? date.toISOString() : format(date, "yyyy-MM-dd");
|
|
394
|
+
}
|
|
395
|
+
function DateRangeFilter({
|
|
396
|
+
column,
|
|
397
|
+
value,
|
|
398
|
+
onChange,
|
|
399
|
+
today = /* @__PURE__ */ new Date(),
|
|
400
|
+
bare = false
|
|
401
|
+
}) {
|
|
402
|
+
const range = rangeOf(value);
|
|
403
|
+
function emit(next) {
|
|
404
|
+
if (!next?.from || !next.to) {
|
|
405
|
+
onChange(void 0);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const to = column.type === "datetime" ? endOfDay(next.to) : next.to;
|
|
409
|
+
onChange({
|
|
410
|
+
field: column.id,
|
|
411
|
+
operator: "between",
|
|
412
|
+
value: [isoOf(next.from, column.type), isoOf(to, column.type)]
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
const isFiltered = range?.from !== void 0;
|
|
416
|
+
const summary = range?.from ? range.to ? `${format(range.from, "PP")} - ${format(range.to, "PP")}` : format(range.from, "PP") : "";
|
|
417
|
+
const panel = /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
418
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap gap-2", children: [
|
|
419
|
+
PRESETS.map((preset) => /* @__PURE__ */ jsx6(
|
|
420
|
+
Button,
|
|
421
|
+
{
|
|
422
|
+
type: "button",
|
|
423
|
+
variant: "outline",
|
|
424
|
+
size: "sm",
|
|
425
|
+
onClick: () => emit(preset.range(today)),
|
|
426
|
+
children: preset.label
|
|
427
|
+
},
|
|
428
|
+
preset.key
|
|
429
|
+
)),
|
|
430
|
+
/* @__PURE__ */ jsx6(Button, { type: "button", variant: "outline", size: "sm", onClick: () => emit(void 0), children: "Custom" })
|
|
431
|
+
] }),
|
|
432
|
+
/* @__PURE__ */ jsx6(Calendar, { mode: "range", selected: range, onSelect: emit, numberOfMonths: 1 })
|
|
433
|
+
] });
|
|
434
|
+
if (bare) {
|
|
435
|
+
return /* @__PURE__ */ jsx6("div", { "data-testid": `filter-${column.id}`, children: panel });
|
|
436
|
+
}
|
|
437
|
+
return /* @__PURE__ */ jsxs3(Popover, { children: [
|
|
438
|
+
/* @__PURE__ */ jsx6(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs3(
|
|
439
|
+
Button,
|
|
440
|
+
{
|
|
441
|
+
variant: "outline",
|
|
442
|
+
size: "sm",
|
|
443
|
+
className: cn("gap-1", isFiltered ? "max-w-[220px] px-2" : "w-8 justify-center px-0"),
|
|
444
|
+
"aria-label": `Filter ${column.header}`,
|
|
445
|
+
"data-testid": `filter-${column.id}`,
|
|
446
|
+
children: [
|
|
447
|
+
/* @__PURE__ */ jsx6(CalendarDaysIcon, { className: "h-3.5 w-3.5 shrink-0 opacity-70", "aria-hidden": true }),
|
|
448
|
+
isFiltered && /* @__PURE__ */ jsx6("span", { className: "truncate", children: summary })
|
|
449
|
+
]
|
|
450
|
+
}
|
|
451
|
+
) }),
|
|
452
|
+
/* @__PURE__ */ jsx6(PopoverContent, { className: "w-auto", children: panel })
|
|
453
|
+
] });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/filter/EnumFilter.tsx
|
|
457
|
+
import { FunnelIcon as FunnelIcon2 } from "@heroicons/react/24/outline";
|
|
458
|
+
import { useMemo, useState } from "react";
|
|
459
|
+
|
|
460
|
+
// src/components/ui/checkbox.tsx
|
|
461
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
|
462
|
+
import { Check as Check2, Minus } from "lucide-react";
|
|
463
|
+
import { forwardRef as forwardRef4 } from "react";
|
|
464
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
465
|
+
var Checkbox = forwardRef4(({ className, ...props }, ref) => /* @__PURE__ */ jsx7(
|
|
466
|
+
CheckboxPrimitive.Root,
|
|
467
|
+
{
|
|
468
|
+
ref,
|
|
469
|
+
className: cn(
|
|
470
|
+
// A fixed 3px radius, not the theme's scaled `rounded-sm` — that
|
|
471
|
+
// token is meant for buttons/cards sized well above 16px, and on a
|
|
472
|
+
// box this small it reads as a circle (radio button) instead of a
|
|
473
|
+
// checkbox once an app's `--radius` grows past shadcn's default.
|
|
474
|
+
"peer h-4 w-4 shrink-0 rounded-[3px] border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground",
|
|
475
|
+
className
|
|
476
|
+
),
|
|
477
|
+
...props,
|
|
478
|
+
children: /* @__PURE__ */ jsx7(CheckboxPrimitive.Indicator, { className: "flex items-center justify-center text-current", children: props.checked === "indeterminate" ? /* @__PURE__ */ jsx7(Minus, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx7(Check2, { className: "h-3 w-3" }) })
|
|
479
|
+
}
|
|
480
|
+
));
|
|
481
|
+
Checkbox.displayName = "Checkbox";
|
|
482
|
+
|
|
483
|
+
// src/components/ui/input.tsx
|
|
484
|
+
import { forwardRef as forwardRef5 } from "react";
|
|
485
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
486
|
+
var Input = forwardRef5(({ className, type, ...props }, ref) => {
|
|
487
|
+
return /* @__PURE__ */ jsx8(
|
|
488
|
+
"input",
|
|
489
|
+
{
|
|
490
|
+
type,
|
|
491
|
+
className: cn(
|
|
492
|
+
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
493
|
+
className
|
|
494
|
+
),
|
|
495
|
+
ref,
|
|
496
|
+
...props
|
|
497
|
+
}
|
|
498
|
+
);
|
|
499
|
+
});
|
|
500
|
+
Input.displayName = "Input";
|
|
501
|
+
|
|
502
|
+
// src/filter/EnumFilter.tsx
|
|
503
|
+
import { jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
504
|
+
function selectedValuesOf(value) {
|
|
505
|
+
if (!value || value.operator !== "in" || !Array.isArray(value.value)) return [];
|
|
506
|
+
return value.value.filter((entry) => typeof entry === "string");
|
|
507
|
+
}
|
|
508
|
+
function toggle(selected, optionValue) {
|
|
509
|
+
return selected.includes(optionValue) ? selected.filter((v) => v !== optionValue) : [...selected, optionValue];
|
|
510
|
+
}
|
|
511
|
+
function EnumFilter({
|
|
512
|
+
column,
|
|
513
|
+
value,
|
|
514
|
+
onChange,
|
|
515
|
+
bare = false
|
|
516
|
+
}) {
|
|
517
|
+
const [search, setSearch] = useState("");
|
|
518
|
+
const selected = selectedValuesOf(value);
|
|
519
|
+
const visibleOptions = useMemo(() => {
|
|
520
|
+
const term = search.trim().toLowerCase();
|
|
521
|
+
if (!term) return column.options;
|
|
522
|
+
return column.options.filter((option) => option.label.toLowerCase().includes(term));
|
|
523
|
+
}, [column.options, search]);
|
|
524
|
+
function emit(next) {
|
|
525
|
+
onChange(next.length === 0 ? void 0 : { field: column.id, operator: "in", value: next });
|
|
526
|
+
}
|
|
527
|
+
const visibleValues = visibleOptions.map((option) => option.value);
|
|
528
|
+
const visibleSelectedCount = visibleValues.filter((v) => selected.includes(v)).length;
|
|
529
|
+
const allVisibleSelected = visibleValues.length > 0 && visibleSelectedCount === visibleValues.length;
|
|
530
|
+
const someVisibleSelected = visibleSelectedCount > 0 && !allVisibleSelected;
|
|
531
|
+
function toggleSelectAllVisible() {
|
|
532
|
+
if (allVisibleSelected) {
|
|
533
|
+
emit(selected.filter((v) => !visibleValues.includes(v)));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const next = new Set(selected);
|
|
537
|
+
for (const v of visibleValues) next.add(v);
|
|
538
|
+
emit([...next]);
|
|
539
|
+
}
|
|
540
|
+
const isFiltered = selected.length > 0;
|
|
541
|
+
const summary = `${selected.length} selected`;
|
|
542
|
+
const panel = /* @__PURE__ */ jsxs4("div", { className: bare ? "flex w-full flex-col gap-2" : "flex w-64 flex-col gap-2 p-2", children: [
|
|
543
|
+
/* @__PURE__ */ jsx9(
|
|
544
|
+
Input,
|
|
545
|
+
{
|
|
546
|
+
placeholder: `Search ${column.header.toLowerCase()}...`,
|
|
547
|
+
value: search,
|
|
548
|
+
onChange: (event) => setSearch(event.target.value)
|
|
549
|
+
}
|
|
550
|
+
),
|
|
551
|
+
/* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-2 border-b px-1 pb-2 text-sm", children: [
|
|
552
|
+
/* @__PURE__ */ jsx9(
|
|
553
|
+
Checkbox,
|
|
554
|
+
{
|
|
555
|
+
checked: someVisibleSelected ? "indeterminate" : allVisibleSelected,
|
|
556
|
+
disabled: visibleValues.length === 0,
|
|
557
|
+
onCheckedChange: toggleSelectAllVisible,
|
|
558
|
+
"aria-label": "Select all"
|
|
559
|
+
}
|
|
560
|
+
),
|
|
561
|
+
"Select all"
|
|
562
|
+
] }),
|
|
563
|
+
/* @__PURE__ */ jsx9("div", { className: "flex max-h-60 flex-col gap-1 overflow-y-auto", children: visibleOptions.length === 0 ? /* @__PURE__ */ jsx9("p", { className: "py-2 text-center text-sm text-muted-foreground", children: "No matches." }) : visibleOptions.map((option) => /* @__PURE__ */ jsxs4(
|
|
564
|
+
"label",
|
|
565
|
+
{
|
|
566
|
+
className: "flex items-center gap-2 rounded-sm px-1 py-1 text-sm hover:bg-accent hover:text-accent-foreground",
|
|
567
|
+
children: [
|
|
568
|
+
/* @__PURE__ */ jsx9(
|
|
569
|
+
Checkbox,
|
|
570
|
+
{
|
|
571
|
+
checked: selected.includes(option.value),
|
|
572
|
+
onCheckedChange: () => emit(toggle(selected, option.value))
|
|
573
|
+
}
|
|
574
|
+
),
|
|
575
|
+
option.label
|
|
576
|
+
]
|
|
577
|
+
},
|
|
578
|
+
option.value
|
|
579
|
+
)) })
|
|
580
|
+
] });
|
|
581
|
+
if (bare) {
|
|
582
|
+
return /* @__PURE__ */ jsx9("div", { "data-testid": `filter-${column.id}`, children: panel });
|
|
583
|
+
}
|
|
584
|
+
return /* @__PURE__ */ jsxs4(Popover, { children: [
|
|
585
|
+
/* @__PURE__ */ jsx9(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs4(
|
|
586
|
+
Button,
|
|
587
|
+
{
|
|
588
|
+
variant: "outline",
|
|
589
|
+
size: "sm",
|
|
590
|
+
className: cn("gap-1", isFiltered ? "max-w-[180px] px-2" : "w-8 justify-center px-0"),
|
|
591
|
+
"aria-label": `Filter ${column.header}`,
|
|
592
|
+
"data-testid": `filter-${column.id}`,
|
|
593
|
+
children: [
|
|
594
|
+
/* @__PURE__ */ jsx9(FunnelIcon2, { className: "h-3.5 w-3.5 shrink-0 opacity-70", "aria-hidden": true }),
|
|
595
|
+
isFiltered && /* @__PURE__ */ jsx9("span", { className: "truncate", children: summary })
|
|
596
|
+
]
|
|
597
|
+
}
|
|
598
|
+
) }),
|
|
599
|
+
/* @__PURE__ */ jsx9(PopoverContent, { className: "w-64 p-2", children: panel })
|
|
600
|
+
] });
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// src/filter/NumberComparisonFilter.tsx
|
|
604
|
+
import { FunnelIcon as FunnelIcon3 } from "@heroicons/react/24/outline";
|
|
605
|
+
import { useEffect, useId, useRef, useState as useState2 } from "react";
|
|
606
|
+
|
|
607
|
+
// src/column/format.ts
|
|
608
|
+
import { parseISO as parseISO3 } from "date-fns";
|
|
609
|
+
function defaultAlign(column) {
|
|
610
|
+
switch (column.type) {
|
|
611
|
+
case "number":
|
|
612
|
+
case "currency":
|
|
613
|
+
return "right";
|
|
614
|
+
case "boolean":
|
|
615
|
+
return "center";
|
|
616
|
+
case "string":
|
|
617
|
+
case "enum":
|
|
618
|
+
case "date":
|
|
619
|
+
case "datetime":
|
|
620
|
+
return "left";
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
var ALIGN_CLASS_NAME = {
|
|
624
|
+
left: "text-left",
|
|
625
|
+
center: "text-center",
|
|
626
|
+
right: "text-right"
|
|
627
|
+
};
|
|
628
|
+
function alignClassName(column) {
|
|
629
|
+
return ALIGN_CLASS_NAME[defaultAlign(column)];
|
|
630
|
+
}
|
|
631
|
+
function toDate(value) {
|
|
632
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
|
|
633
|
+
if (typeof value === "number") {
|
|
634
|
+
const date = new Date(value);
|
|
635
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
636
|
+
}
|
|
637
|
+
if (typeof value === "string") {
|
|
638
|
+
const date = parseISO3(value);
|
|
639
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
640
|
+
}
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
function toNumber(value) {
|
|
644
|
+
if (typeof value === "number") return Number.isNaN(value) ? null : value;
|
|
645
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
646
|
+
const parsed = Number(value);
|
|
647
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
648
|
+
}
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
function defaultFormat(column, value) {
|
|
652
|
+
if (value === null || value === void 0) return "";
|
|
653
|
+
switch (column.type) {
|
|
654
|
+
case "string":
|
|
655
|
+
return String(value);
|
|
656
|
+
case "number": {
|
|
657
|
+
const num = toNumber(value);
|
|
658
|
+
return num === null ? String(value) : num.toLocaleString();
|
|
659
|
+
}
|
|
660
|
+
case "currency": {
|
|
661
|
+
const num = toNumber(value);
|
|
662
|
+
if (num === null) return String(value);
|
|
663
|
+
return new Intl.NumberFormat(void 0, {
|
|
664
|
+
style: "currency",
|
|
665
|
+
currency: column.currency ?? "USD"
|
|
666
|
+
}).format(num);
|
|
667
|
+
}
|
|
668
|
+
case "date": {
|
|
669
|
+
const date = toDate(value);
|
|
670
|
+
return date === null ? String(value) : date.toLocaleDateString();
|
|
671
|
+
}
|
|
672
|
+
case "datetime": {
|
|
673
|
+
const date = toDate(value);
|
|
674
|
+
return date === null ? String(value) : date.toLocaleString();
|
|
675
|
+
}
|
|
676
|
+
case "boolean": {
|
|
677
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
678
|
+
if (typeof value === "string") {
|
|
679
|
+
const normalized = value.trim().toLowerCase();
|
|
680
|
+
if (normalized === "true") return "Yes";
|
|
681
|
+
if (normalized === "false") return "No";
|
|
682
|
+
}
|
|
683
|
+
if (typeof value === "number") return value !== 0 ? "Yes" : "No";
|
|
684
|
+
return String(value);
|
|
685
|
+
}
|
|
686
|
+
case "enum": {
|
|
687
|
+
const match = column.options.find((option) => option.value === String(value));
|
|
688
|
+
return match ? match.label : String(value);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/filter/numberRangeShared.ts
|
|
694
|
+
function isNumberPair(value) {
|
|
695
|
+
return Array.isArray(value) && value.length === 2 && typeof value[0] === "number" && typeof value[1] === "number";
|
|
696
|
+
}
|
|
697
|
+
function rangeOf2(value) {
|
|
698
|
+
if (!value) return { min: void 0, max: void 0 };
|
|
699
|
+
if (value.operator === "between" && isNumberPair(value.value)) {
|
|
700
|
+
const [min, max] = value.value;
|
|
701
|
+
return { min, max };
|
|
702
|
+
}
|
|
703
|
+
if (value.operator === "gte" && typeof value.value === "number") {
|
|
704
|
+
return { min: value.value, max: void 0 };
|
|
705
|
+
}
|
|
706
|
+
if (value.operator === "lte" && typeof value.value === "number") {
|
|
707
|
+
return { min: void 0, max: value.value };
|
|
708
|
+
}
|
|
709
|
+
return { min: void 0, max: void 0 };
|
|
710
|
+
}
|
|
711
|
+
function descriptorFor(field, range) {
|
|
712
|
+
const { min, max } = range;
|
|
713
|
+
if (min !== void 0 && max !== void 0) {
|
|
714
|
+
const [lower, upper] = min <= max ? [min, max] : [max, min];
|
|
715
|
+
return { field, operator: "between", value: [lower, upper] };
|
|
716
|
+
}
|
|
717
|
+
if (min !== void 0) return { field, operator: "gte", value: min };
|
|
718
|
+
if (max !== void 0) return { field, operator: "lte", value: max };
|
|
719
|
+
return void 0;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/filter/NumberComparisonFilter.tsx
|
|
723
|
+
import { Fragment, jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
724
|
+
var NUMBER_OPERATORS = [
|
|
725
|
+
{ value: "gt", label: "Greater than" },
|
|
726
|
+
{ value: "gte", label: "Greater than or equal to" },
|
|
727
|
+
{ value: "lt", label: "Less than" },
|
|
728
|
+
{ value: "lte", label: "Less than or equal to" },
|
|
729
|
+
{ value: "eq", label: "Equals" },
|
|
730
|
+
{ value: "neq", label: "Does not equal" },
|
|
731
|
+
{ value: "between", label: "Between" }
|
|
732
|
+
];
|
|
733
|
+
var OPERATOR_SYMBOL = {
|
|
734
|
+
gt: ">",
|
|
735
|
+
gte: "\u2265",
|
|
736
|
+
lt: "<",
|
|
737
|
+
lte: "\u2264",
|
|
738
|
+
eq: "=",
|
|
739
|
+
neq: "\u2260"
|
|
740
|
+
};
|
|
741
|
+
var DEFAULT_OPERATOR = "gt";
|
|
742
|
+
var NUMBER_OPERATOR_VALUES = new Set(NUMBER_OPERATORS.map((option) => option.value));
|
|
743
|
+
function isNumberOperator(operator) {
|
|
744
|
+
return operator !== void 0 && NUMBER_OPERATOR_VALUES.has(operator);
|
|
745
|
+
}
|
|
746
|
+
function parseNumber(raw) {
|
|
747
|
+
if (raw.trim() === "") return void 0;
|
|
748
|
+
const parsed = Number(raw);
|
|
749
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
750
|
+
}
|
|
751
|
+
function stateFromValue(value) {
|
|
752
|
+
if (!value) return { operator: DEFAULT_OPERATOR, text: "", text2: "" };
|
|
753
|
+
if (value.operator === "between" && isNumberPair(value.value)) {
|
|
754
|
+
return { operator: "between", text: String(value.value[0]), text2: String(value.value[1]) };
|
|
755
|
+
}
|
|
756
|
+
if (isNumberOperator(value.operator) && typeof value.value === "number") {
|
|
757
|
+
return { operator: value.operator, text: value.value.toString(), text2: "" };
|
|
758
|
+
}
|
|
759
|
+
return { operator: DEFAULT_OPERATOR, text: "", text2: "" };
|
|
760
|
+
}
|
|
761
|
+
function NumberComparisonFilter({
|
|
762
|
+
column,
|
|
763
|
+
value,
|
|
764
|
+
onChange,
|
|
765
|
+
bare = true
|
|
766
|
+
}) {
|
|
767
|
+
const initial = stateFromValue(value);
|
|
768
|
+
const [operator, setOperator] = useState2(initial.operator);
|
|
769
|
+
const [text, setText] = useState2(initial.text);
|
|
770
|
+
const [text2, setText2] = useState2(initial.text2);
|
|
771
|
+
const operatorId = useId();
|
|
772
|
+
const valueId = useId();
|
|
773
|
+
const lastEmittedRef = useRef(value);
|
|
774
|
+
useEffect(() => {
|
|
775
|
+
if (value === lastEmittedRef.current) return;
|
|
776
|
+
lastEmittedRef.current = value;
|
|
777
|
+
const next = stateFromValue(value);
|
|
778
|
+
setOperator(next.operator);
|
|
779
|
+
setText(next.text);
|
|
780
|
+
setText2(next.text2);
|
|
781
|
+
}, [value]);
|
|
782
|
+
function emit(nextOperator, nextText, nextText2) {
|
|
783
|
+
let next;
|
|
784
|
+
if (nextOperator === "between") {
|
|
785
|
+
const min = parseNumber(nextText);
|
|
786
|
+
const max = parseNumber(nextText2);
|
|
787
|
+
next = min !== void 0 && max !== void 0 ? { field: column.id, operator: "between", value: [min, max] } : void 0;
|
|
788
|
+
} else {
|
|
789
|
+
const num = parseNumber(nextText);
|
|
790
|
+
next = num !== void 0 ? { field: column.id, operator: nextOperator, value: num } : void 0;
|
|
791
|
+
}
|
|
792
|
+
lastEmittedRef.current = next;
|
|
793
|
+
onChange(next);
|
|
794
|
+
}
|
|
795
|
+
const isBetween = operator === "between";
|
|
796
|
+
const isFiltered = value !== void 0;
|
|
797
|
+
const summary = (() => {
|
|
798
|
+
if (!value) return "";
|
|
799
|
+
if (value.operator === "between" && isNumberPair(value.value)) {
|
|
800
|
+
const [lo, hi] = value.value;
|
|
801
|
+
return `${defaultFormat(column, lo)} \u2013 ${defaultFormat(column, hi)}`;
|
|
802
|
+
}
|
|
803
|
+
if (isNumberOperator(value.operator) && value.operator !== "between" && typeof value.value === "number") {
|
|
804
|
+
return `${OPERATOR_SYMBOL[value.operator]} ${defaultFormat(column, value.value)}`;
|
|
805
|
+
}
|
|
806
|
+
return "";
|
|
807
|
+
})();
|
|
808
|
+
const panel = /* @__PURE__ */ jsxs5("div", { className: "flex w-full max-w-[220px] flex-col gap-2", children: [
|
|
809
|
+
/* @__PURE__ */ jsxs5(
|
|
810
|
+
Select,
|
|
811
|
+
{
|
|
812
|
+
value: operator,
|
|
813
|
+
onValueChange: (next) => {
|
|
814
|
+
const nextOperator = NUMBER_OPERATORS.find((option) => option.value === next)?.value;
|
|
815
|
+
if (!nextOperator) return;
|
|
816
|
+
setOperator(nextOperator);
|
|
817
|
+
emit(nextOperator, text, text2);
|
|
818
|
+
},
|
|
819
|
+
children: [
|
|
820
|
+
/* @__PURE__ */ jsx10(SelectTrigger, { id: operatorId, "aria-label": `${column.header} filter operator`, children: /* @__PURE__ */ jsx10(SelectValue, {}) }),
|
|
821
|
+
/* @__PURE__ */ jsx10(SelectContent, { children: NUMBER_OPERATORS.map((option) => /* @__PURE__ */ jsx10(SelectItem, { value: option.value, children: option.label }, option.value)) })
|
|
822
|
+
]
|
|
823
|
+
}
|
|
824
|
+
),
|
|
825
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2", children: [
|
|
826
|
+
/* @__PURE__ */ jsx10(
|
|
827
|
+
Input,
|
|
828
|
+
{
|
|
829
|
+
id: valueId,
|
|
830
|
+
type: "number",
|
|
831
|
+
className: "min-w-0",
|
|
832
|
+
"aria-label": isBetween ? `${column.header} minimum` : column.header,
|
|
833
|
+
placeholder: isBetween ? "Min" : "Value",
|
|
834
|
+
value: text,
|
|
835
|
+
onChange: (event) => {
|
|
836
|
+
setText(event.target.value);
|
|
837
|
+
emit(operator, event.target.value, text2);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
),
|
|
841
|
+
isBetween && /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
842
|
+
/* @__PURE__ */ jsx10("span", { className: "shrink-0 text-xs text-muted-foreground", children: "to" }),
|
|
843
|
+
/* @__PURE__ */ jsx10(
|
|
844
|
+
Input,
|
|
845
|
+
{
|
|
846
|
+
type: "number",
|
|
847
|
+
className: "min-w-0",
|
|
848
|
+
"aria-label": `${column.header} maximum`,
|
|
849
|
+
placeholder: "Max",
|
|
850
|
+
value: text2,
|
|
851
|
+
onChange: (event) => {
|
|
852
|
+
setText2(event.target.value);
|
|
853
|
+
emit(operator, text, event.target.value);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
)
|
|
857
|
+
] })
|
|
858
|
+
] })
|
|
859
|
+
] });
|
|
860
|
+
if (bare) {
|
|
861
|
+
return /* @__PURE__ */ jsx10("div", { "data-testid": `filter-${column.id}`, children: panel });
|
|
862
|
+
}
|
|
863
|
+
return /* @__PURE__ */ jsxs5(Popover, { children: [
|
|
864
|
+
/* @__PURE__ */ jsx10(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs5(
|
|
865
|
+
Button,
|
|
866
|
+
{
|
|
867
|
+
variant: "outline",
|
|
868
|
+
size: "sm",
|
|
869
|
+
className: cn("gap-1", isFiltered ? "max-w-[180px] px-2" : "w-8 justify-center px-0"),
|
|
870
|
+
"aria-label": `Filter ${column.header}`,
|
|
871
|
+
"data-testid": `filter-${column.id}`,
|
|
872
|
+
children: [
|
|
873
|
+
/* @__PURE__ */ jsx10(FunnelIcon3, { className: "h-3.5 w-3.5 shrink-0 opacity-70", "aria-hidden": true }),
|
|
874
|
+
isFiltered && /* @__PURE__ */ jsx10("span", { className: "truncate", children: summary })
|
|
875
|
+
]
|
|
876
|
+
}
|
|
877
|
+
) }),
|
|
878
|
+
/* @__PURE__ */ jsx10(PopoverContent, { className: "w-auto p-3", children: panel })
|
|
879
|
+
] });
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/filter/NumberRangeFilter.tsx
|
|
883
|
+
import { FunnelIcon as FunnelIcon4 } from "@heroicons/react/24/outline";
|
|
884
|
+
import { useEffect as useEffect2, useId as useId2, useRef as useRef2, useState as useState3 } from "react";
|
|
885
|
+
import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
886
|
+
function parseNumber2(raw) {
|
|
887
|
+
if (raw.trim() === "") return void 0;
|
|
888
|
+
const parsed = Number(raw);
|
|
889
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
890
|
+
}
|
|
891
|
+
function NumberRangeFilter({
|
|
892
|
+
column,
|
|
893
|
+
value,
|
|
894
|
+
onChange,
|
|
895
|
+
bare = false
|
|
896
|
+
}) {
|
|
897
|
+
const initial = rangeOf2(value);
|
|
898
|
+
const [minText, setMinText] = useState3(initial.min?.toString() ?? "");
|
|
899
|
+
const [maxText, setMaxText] = useState3(initial.max?.toString() ?? "");
|
|
900
|
+
const minId = useId2();
|
|
901
|
+
const maxId = useId2();
|
|
902
|
+
const lastEmittedRef = useRef2(value);
|
|
903
|
+
useEffect2(() => {
|
|
904
|
+
if (value === lastEmittedRef.current) return;
|
|
905
|
+
lastEmittedRef.current = value;
|
|
906
|
+
const next = rangeOf2(value);
|
|
907
|
+
setMinText(next.min?.toString() ?? "");
|
|
908
|
+
setMaxText(next.max?.toString() ?? "");
|
|
909
|
+
}, [value]);
|
|
910
|
+
function emit(nextMinText, nextMaxText) {
|
|
911
|
+
const next = descriptorFor(column.id, {
|
|
912
|
+
min: parseNumber2(nextMinText),
|
|
913
|
+
max: parseNumber2(nextMaxText)
|
|
914
|
+
});
|
|
915
|
+
lastEmittedRef.current = next;
|
|
916
|
+
onChange(next);
|
|
917
|
+
}
|
|
918
|
+
const { min, max } = rangeOf2(value);
|
|
919
|
+
const isFiltered = min !== void 0 || max !== void 0;
|
|
920
|
+
const summary = min !== void 0 && max !== void 0 ? `${defaultFormat(column, min)} \u2013 ${defaultFormat(column, max)}` : min !== void 0 ? `\u2265 ${defaultFormat(column, min)}` : max !== void 0 ? `\u2264 ${defaultFormat(column, max)}` : "";
|
|
921
|
+
const panel = /* @__PURE__ */ jsxs6("div", { className: "flex w-full max-w-[220px] items-center gap-2", children: [
|
|
922
|
+
/* @__PURE__ */ jsx11(
|
|
923
|
+
Input,
|
|
924
|
+
{
|
|
925
|
+
id: minId,
|
|
926
|
+
type: "number",
|
|
927
|
+
"aria-label": `${column.header} minimum`,
|
|
928
|
+
placeholder: "Min",
|
|
929
|
+
className: "min-w-0",
|
|
930
|
+
value: minText,
|
|
931
|
+
onChange: (event) => {
|
|
932
|
+
setMinText(event.target.value);
|
|
933
|
+
emit(event.target.value, maxText);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
),
|
|
937
|
+
/* @__PURE__ */ jsx11("span", { className: "shrink-0 text-xs text-muted-foreground", children: "to" }),
|
|
938
|
+
/* @__PURE__ */ jsx11(
|
|
939
|
+
Input,
|
|
940
|
+
{
|
|
941
|
+
id: maxId,
|
|
942
|
+
type: "number",
|
|
943
|
+
"aria-label": `${column.header} maximum`,
|
|
944
|
+
placeholder: "Max",
|
|
945
|
+
className: "min-w-0",
|
|
946
|
+
value: maxText,
|
|
947
|
+
onChange: (event) => {
|
|
948
|
+
setMaxText(event.target.value);
|
|
949
|
+
emit(minText, event.target.value);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
)
|
|
953
|
+
] });
|
|
954
|
+
if (bare) {
|
|
955
|
+
return /* @__PURE__ */ jsx11("div", { "data-testid": `filter-${column.id}`, children: panel });
|
|
956
|
+
}
|
|
957
|
+
return /* @__PURE__ */ jsxs6(Popover, { children: [
|
|
958
|
+
/* @__PURE__ */ jsx11(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs6(
|
|
959
|
+
Button,
|
|
960
|
+
{
|
|
961
|
+
variant: "outline",
|
|
962
|
+
size: "sm",
|
|
963
|
+
className: cn("gap-1", isFiltered ? "max-w-[180px] px-2" : "w-8 justify-center px-0"),
|
|
964
|
+
"aria-label": `Filter ${column.header}`,
|
|
965
|
+
"data-testid": `filter-${column.id}`,
|
|
966
|
+
children: [
|
|
967
|
+
/* @__PURE__ */ jsx11(FunnelIcon4, { className: "h-3.5 w-3.5 shrink-0 opacity-70", "aria-hidden": true }),
|
|
968
|
+
isFiltered && /* @__PURE__ */ jsx11("span", { className: "truncate", children: summary })
|
|
969
|
+
]
|
|
970
|
+
}
|
|
971
|
+
) }),
|
|
972
|
+
/* @__PURE__ */ jsx11(PopoverContent, { className: "w-auto p-3", children: panel })
|
|
973
|
+
] });
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// src/filter/NumberHistogramFilter.tsx
|
|
977
|
+
import { FunnelIcon as FunnelIcon5 } from "@heroicons/react/24/outline";
|
|
978
|
+
import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState4 } from "react";
|
|
979
|
+
import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
980
|
+
var BAR_HEIGHT_PX = 56;
|
|
981
|
+
var DEFAULT_BUCKETS = 24;
|
|
982
|
+
function tLog(v) {
|
|
983
|
+
return Math.sign(v) * Math.log10(Math.abs(v) + 1);
|
|
984
|
+
}
|
|
985
|
+
function tInv(t) {
|
|
986
|
+
return Math.sign(t) * (10 ** Math.abs(t) - 1);
|
|
987
|
+
}
|
|
988
|
+
function NumberHistogramFilter({
|
|
989
|
+
column,
|
|
990
|
+
value,
|
|
991
|
+
onChange,
|
|
992
|
+
allValues,
|
|
993
|
+
loadValues,
|
|
994
|
+
format: format2 = (v) => v.toLocaleString(),
|
|
995
|
+
buckets = DEFAULT_BUCKETS,
|
|
996
|
+
bare = true
|
|
997
|
+
}) {
|
|
998
|
+
const [openState, setOpen] = useState4(false);
|
|
999
|
+
const open = bare || openState;
|
|
1000
|
+
const trackRef = useRef3(null);
|
|
1001
|
+
const [minOnTop, setMinOnTop] = useState4(false);
|
|
1002
|
+
const [loadedValues, setLoadedValues] = useState4([]);
|
|
1003
|
+
const [loading, setLoading] = useState4(false);
|
|
1004
|
+
const { min, max } = rangeOf2(value);
|
|
1005
|
+
useEffect3(() => {
|
|
1006
|
+
if (!open || !loadValues) return;
|
|
1007
|
+
let cancelled = false;
|
|
1008
|
+
setLoading(true);
|
|
1009
|
+
loadValues().then((values) => {
|
|
1010
|
+
if (!cancelled) setLoadedValues(values);
|
|
1011
|
+
}).finally(() => {
|
|
1012
|
+
if (!cancelled) setLoading(false);
|
|
1013
|
+
});
|
|
1014
|
+
return () => {
|
|
1015
|
+
cancelled = true;
|
|
1016
|
+
};
|
|
1017
|
+
}, [open, loadValues]);
|
|
1018
|
+
const sourceValues = allValues ?? loadedValues;
|
|
1019
|
+
const nums = useMemo2(
|
|
1020
|
+
() => open ? sourceValues.filter((v) => v != null) : [],
|
|
1021
|
+
[sourceValues, open]
|
|
1022
|
+
);
|
|
1023
|
+
const dataMin = useMemo2(() => nums.reduce((a, b) => Math.min(a, b), Infinity), [nums]);
|
|
1024
|
+
const dataMax = useMemo2(() => nums.reduce((a, b) => Math.max(a, b), -Infinity), [nums]);
|
|
1025
|
+
const activeMin = min ?? dataMin;
|
|
1026
|
+
const activeMax = max ?? dataMax;
|
|
1027
|
+
const [minText, setMinText] = useState4("");
|
|
1028
|
+
const [maxText, setMaxText] = useState4("");
|
|
1029
|
+
useEffect3(() => {
|
|
1030
|
+
if (!open) return;
|
|
1031
|
+
setMinText(Number.isFinite(activeMin) ? String(Math.round(activeMin)) : "");
|
|
1032
|
+
setMaxText(Number.isFinite(activeMax) ? String(Math.round(activeMax)) : "");
|
|
1033
|
+
}, [open, loading]);
|
|
1034
|
+
const logDataMin = Number.isFinite(dataMin) ? tLog(dataMin) : 0;
|
|
1035
|
+
const logDataMax = Number.isFinite(dataMax) ? tLog(dataMax) : 1;
|
|
1036
|
+
const logStep = (logDataMax - logDataMin) / 200;
|
|
1037
|
+
const logPct = useCallback(
|
|
1038
|
+
(v) => logDataMax === logDataMin ? 0 : (tLog(v) - logDataMin) / (logDataMax - logDataMin) * 100,
|
|
1039
|
+
[logDataMin, logDataMax]
|
|
1040
|
+
);
|
|
1041
|
+
const histBars = useMemo2(() => {
|
|
1042
|
+
if (!nums.length || dataMin === dataMax || !Number.isFinite(dataMin) || !Number.isFinite(dataMax)) return [];
|
|
1043
|
+
const step = (logDataMax - logDataMin) / buckets;
|
|
1044
|
+
const counts = Array(buckets).fill(0);
|
|
1045
|
+
for (const v of nums) {
|
|
1046
|
+
const i = Math.min(Math.floor((tLog(v) - logDataMin) / step), buckets - 1);
|
|
1047
|
+
counts[i] = (counts[i] ?? 0) + 1;
|
|
1048
|
+
}
|
|
1049
|
+
const peak = Math.max(...counts);
|
|
1050
|
+
return counts.map((count, i) => ({
|
|
1051
|
+
count,
|
|
1052
|
+
heightPx: peak > 0 ? Math.max(count / peak * BAR_HEIGHT_PX, 2) : 2,
|
|
1053
|
+
active: tInv(logDataMin + i * step) < activeMax && tInv(logDataMin + (i + 1) * step) > activeMin
|
|
1054
|
+
}));
|
|
1055
|
+
}, [nums, dataMin, dataMax, logDataMin, logDataMax, buckets, activeMin, activeMax]);
|
|
1056
|
+
function emit(nextMin, nextMax) {
|
|
1057
|
+
onChange(
|
|
1058
|
+
descriptorFor(column.id, {
|
|
1059
|
+
min: nextMin !== void 0 && nextMin <= dataMin ? void 0 : nextMin,
|
|
1060
|
+
max: nextMax !== void 0 && nextMax >= dataMax ? void 0 : nextMax
|
|
1061
|
+
})
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
function handleMinChange(actual) {
|
|
1065
|
+
emit(Math.min(actual, activeMax - 1), max);
|
|
1066
|
+
}
|
|
1067
|
+
function handleMaxChange(actual) {
|
|
1068
|
+
emit(min, Math.max(actual, activeMin + 1));
|
|
1069
|
+
}
|
|
1070
|
+
function handleTrackHover(event) {
|
|
1071
|
+
if (!trackRef.current || !Number.isFinite(dataMin) || !Number.isFinite(dataMax)) return;
|
|
1072
|
+
const rect = trackRef.current.getBoundingClientRect();
|
|
1073
|
+
const ratio = (event.clientX - rect.left) / rect.width;
|
|
1074
|
+
const logHover = logDataMin + ratio * (logDataMax - logDataMin);
|
|
1075
|
+
setMinOnTop(Math.abs(logHover - tLog(activeMin)) <= Math.abs(logHover - tLog(activeMax)));
|
|
1076
|
+
}
|
|
1077
|
+
const isFiltered = value !== void 0;
|
|
1078
|
+
const summary = isFiltered ? `${format2(activeMin)} \u2013 ${format2(activeMax)}` : "";
|
|
1079
|
+
const panel = /* @__PURE__ */ jsxs7("div", { className: bare ? "flex w-full flex-col gap-3" : "flex w-80 flex-col gap-3", children: [
|
|
1080
|
+
loading && /* @__PURE__ */ jsx12("p", { className: "text-xs text-muted-foreground", "data-testid": `filter-${column.id}-loading`, children: "Loading\u2026" }),
|
|
1081
|
+
/* @__PURE__ */ jsx12("div", { className: "flex items-end gap-px", style: { height: BAR_HEIGHT_PX }, "data-testid": `filter-${column.id}-histogram`, children: histBars.map((bar, i) => /* @__PURE__ */ jsx12(
|
|
1082
|
+
"div",
|
|
1083
|
+
{
|
|
1084
|
+
className: cn("flex-1 rounded-sm", bar.active ? "bg-primary/70" : "bg-muted-foreground/20"),
|
|
1085
|
+
style: { height: bar.heightPx }
|
|
1086
|
+
},
|
|
1087
|
+
i
|
|
1088
|
+
)) }),
|
|
1089
|
+
/* @__PURE__ */ jsxs7("div", { ref: trackRef, className: "relative h-5", onMouseMove: handleTrackHover, children: [
|
|
1090
|
+
/* @__PURE__ */ jsx12(
|
|
1091
|
+
"input",
|
|
1092
|
+
{
|
|
1093
|
+
type: "range",
|
|
1094
|
+
"aria-label": `${column.header} minimum (slider)`,
|
|
1095
|
+
min: logDataMin,
|
|
1096
|
+
max: logDataMax,
|
|
1097
|
+
step: logStep,
|
|
1098
|
+
value: tLog(activeMin),
|
|
1099
|
+
onChange: (event) => handleMinChange(tInv(Number.parseFloat(event.target.value))),
|
|
1100
|
+
className: "absolute inset-0 h-full w-full cursor-pointer opacity-0",
|
|
1101
|
+
style: { zIndex: minOnTop ? 5 : 3 }
|
|
1102
|
+
}
|
|
1103
|
+
),
|
|
1104
|
+
/* @__PURE__ */ jsx12(
|
|
1105
|
+
"input",
|
|
1106
|
+
{
|
|
1107
|
+
type: "range",
|
|
1108
|
+
"aria-label": `${column.header} maximum (slider)`,
|
|
1109
|
+
min: logDataMin,
|
|
1110
|
+
max: logDataMax,
|
|
1111
|
+
step: logStep,
|
|
1112
|
+
value: tLog(activeMax),
|
|
1113
|
+
onChange: (event) => handleMaxChange(tInv(Number.parseFloat(event.target.value))),
|
|
1114
|
+
className: "absolute inset-0 h-full w-full cursor-pointer opacity-0",
|
|
1115
|
+
style: { zIndex: minOnTop ? 3 : 5 }
|
|
1116
|
+
}
|
|
1117
|
+
),
|
|
1118
|
+
/* @__PURE__ */ jsx12("div", { className: "pointer-events-none absolute top-1/2 h-1.5 w-full -translate-y-1/2 rounded-full bg-muted", children: /* @__PURE__ */ jsx12(
|
|
1119
|
+
"div",
|
|
1120
|
+
{
|
|
1121
|
+
className: "absolute h-full rounded-full bg-primary",
|
|
1122
|
+
style: { left: `${logPct(activeMin)}%`, right: `${100 - logPct(activeMax)}%` }
|
|
1123
|
+
}
|
|
1124
|
+
) }),
|
|
1125
|
+
[activeMin, activeMax].map((v, i) => /* @__PURE__ */ jsx12(
|
|
1126
|
+
"div",
|
|
1127
|
+
{
|
|
1128
|
+
className: "pointer-events-none absolute top-1/2 h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-primary bg-background shadow",
|
|
1129
|
+
style: { left: `${logPct(v)}%` }
|
|
1130
|
+
},
|
|
1131
|
+
i
|
|
1132
|
+
))
|
|
1133
|
+
] }),
|
|
1134
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
|
|
1135
|
+
/* @__PURE__ */ jsx12(
|
|
1136
|
+
Input,
|
|
1137
|
+
{
|
|
1138
|
+
type: "text",
|
|
1139
|
+
inputMode: "numeric",
|
|
1140
|
+
"aria-label": `${column.header} minimum`,
|
|
1141
|
+
value: minText,
|
|
1142
|
+
onChange: (event) => {
|
|
1143
|
+
setMinText(event.target.value);
|
|
1144
|
+
const parsed = Number.parseFloat(event.target.value.replace(/[^0-9.-]/g, ""));
|
|
1145
|
+
if (!Number.isNaN(parsed)) handleMinChange(parsed);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
),
|
|
1149
|
+
/* @__PURE__ */ jsx12("span", { className: "shrink-0 text-xs text-muted-foreground", children: "to" }),
|
|
1150
|
+
/* @__PURE__ */ jsx12(
|
|
1151
|
+
Input,
|
|
1152
|
+
{
|
|
1153
|
+
type: "text",
|
|
1154
|
+
inputMode: "numeric",
|
|
1155
|
+
"aria-label": `${column.header} maximum`,
|
|
1156
|
+
value: maxText,
|
|
1157
|
+
onChange: (event) => {
|
|
1158
|
+
setMaxText(event.target.value);
|
|
1159
|
+
const parsed = Number.parseFloat(event.target.value.replace(/[^0-9.-]/g, ""));
|
|
1160
|
+
if (!Number.isNaN(parsed)) handleMaxChange(parsed);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
)
|
|
1164
|
+
] }),
|
|
1165
|
+
isFiltered && /* @__PURE__ */ jsx12(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onChange(void 0), children: "Clear" })
|
|
1166
|
+
] });
|
|
1167
|
+
if (bare) {
|
|
1168
|
+
return /* @__PURE__ */ jsx12("div", { "data-testid": `filter-${column.id}`, children: panel });
|
|
1169
|
+
}
|
|
1170
|
+
return /* @__PURE__ */ jsxs7(Popover, { open, onOpenChange: setOpen, children: [
|
|
1171
|
+
/* @__PURE__ */ jsx12(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs7(
|
|
1172
|
+
Button,
|
|
1173
|
+
{
|
|
1174
|
+
variant: "outline",
|
|
1175
|
+
size: "sm",
|
|
1176
|
+
className: cn("gap-1", isFiltered ? "max-w-[200px] px-2" : "w-8 justify-center px-0"),
|
|
1177
|
+
"aria-label": `Filter ${column.header}`,
|
|
1178
|
+
"data-testid": `filter-${column.id}`,
|
|
1179
|
+
children: [
|
|
1180
|
+
/* @__PURE__ */ jsx12(FunnelIcon5, { className: "h-3.5 w-3.5 shrink-0 opacity-70", "aria-hidden": true }),
|
|
1181
|
+
isFiltered && /* @__PURE__ */ jsx12("span", { className: "truncate", children: summary })
|
|
1182
|
+
]
|
|
1183
|
+
}
|
|
1184
|
+
) }),
|
|
1185
|
+
/* @__PURE__ */ jsx12(PopoverContent, { className: "w-80", children: panel })
|
|
1186
|
+
] });
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// src/column/types.ts
|
|
1190
|
+
function getColumnValue(column, row) {
|
|
1191
|
+
if (column.accessorFn) return column.accessorFn(row);
|
|
1192
|
+
if (column.accessorKey) return row[column.accessorKey];
|
|
1193
|
+
return void 0;
|
|
1194
|
+
}
|
|
1195
|
+
function isSortable(column) {
|
|
1196
|
+
return column.sortable === true;
|
|
1197
|
+
}
|
|
1198
|
+
function isFilterable(column) {
|
|
1199
|
+
return column.filterable === true;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// src/filter/facetedValues.ts
|
|
1203
|
+
function withoutColumn(filter, columnId) {
|
|
1204
|
+
if (!filter) return null;
|
|
1205
|
+
if (!isCompositeFilterDescriptor(filter)) {
|
|
1206
|
+
return fieldKey(filter.field) === columnId ? null : filter;
|
|
1207
|
+
}
|
|
1208
|
+
const kept = filter.filters.map((child) => withoutColumn(child, columnId)).filter((child) => child !== null);
|
|
1209
|
+
if (kept.length === 0) return null;
|
|
1210
|
+
return { logic: filter.logic, filters: kept };
|
|
1211
|
+
}
|
|
1212
|
+
function facetedNumberValues(data, column, filter) {
|
|
1213
|
+
const withoutSelf = withoutColumn(filter, column.id);
|
|
1214
|
+
const values = [];
|
|
1215
|
+
for (const row of data) {
|
|
1216
|
+
if (!evaluateFilter(row, withoutSelf)) continue;
|
|
1217
|
+
const raw = getColumnValue(column, row);
|
|
1218
|
+
if (typeof raw === "number") {
|
|
1219
|
+
values.push(Number.isFinite(raw) ? raw : null);
|
|
1220
|
+
} else if (raw == null) {
|
|
1221
|
+
values.push(null);
|
|
1222
|
+
} else {
|
|
1223
|
+
const parsed = Number(raw);
|
|
1224
|
+
values.push(Number.isFinite(parsed) ? parsed : null);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
return values;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// src/filter/StringFilter.tsx
|
|
1231
|
+
import { FunnelIcon as FunnelIcon6 } from "@heroicons/react/24/outline";
|
|
1232
|
+
import { useId as useId3 } from "react";
|
|
1233
|
+
import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1234
|
+
var STRING_OPERATORS = [
|
|
1235
|
+
{ value: "contains", label: "Contains" },
|
|
1236
|
+
{ value: "eq", label: "Is" },
|
|
1237
|
+
{ value: "startsWith", label: "Starts with" },
|
|
1238
|
+
{ value: "endsWith", label: "Ends with" }
|
|
1239
|
+
];
|
|
1240
|
+
var DEFAULT_OPERATOR2 = "contains";
|
|
1241
|
+
function StringFilter({
|
|
1242
|
+
column,
|
|
1243
|
+
value,
|
|
1244
|
+
onChange,
|
|
1245
|
+
bare = false
|
|
1246
|
+
}) {
|
|
1247
|
+
const operator = value?.operator ?? DEFAULT_OPERATOR2;
|
|
1248
|
+
const text = typeof value?.value === "string" ? value.value : "";
|
|
1249
|
+
const inputId = useId3();
|
|
1250
|
+
const isFiltered = text !== "";
|
|
1251
|
+
function emit(nextOperator, nextText) {
|
|
1252
|
+
if (nextText === "") {
|
|
1253
|
+
onChange(void 0);
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
onChange({ field: column.id, operator: nextOperator, value: nextText });
|
|
1257
|
+
}
|
|
1258
|
+
const panel = /* @__PURE__ */ jsxs8("div", { className: "flex w-full max-w-[220px] flex-col gap-2", children: [
|
|
1259
|
+
/* @__PURE__ */ jsxs8(
|
|
1260
|
+
Select,
|
|
1261
|
+
{
|
|
1262
|
+
value: operator,
|
|
1263
|
+
onValueChange: (next) => {
|
|
1264
|
+
const nextOperator = STRING_OPERATORS.find((option) => option.value === next)?.value;
|
|
1265
|
+
if (nextOperator) emit(nextOperator, text);
|
|
1266
|
+
},
|
|
1267
|
+
children: [
|
|
1268
|
+
/* @__PURE__ */ jsx13(SelectTrigger, { "aria-label": `${column.header} filter operator`, children: /* @__PURE__ */ jsx13(SelectValue, {}) }),
|
|
1269
|
+
/* @__PURE__ */ jsx13(SelectContent, { children: STRING_OPERATORS.map((option) => /* @__PURE__ */ jsx13(SelectItem, { value: option.value, children: option.label }, option.value)) })
|
|
1270
|
+
]
|
|
1271
|
+
}
|
|
1272
|
+
),
|
|
1273
|
+
/* @__PURE__ */ jsx13(
|
|
1274
|
+
Input,
|
|
1275
|
+
{
|
|
1276
|
+
id: inputId,
|
|
1277
|
+
value: text,
|
|
1278
|
+
placeholder: `Filter ${column.header.toLowerCase()}...`,
|
|
1279
|
+
onChange: (event) => emit(operator, event.target.value)
|
|
1280
|
+
}
|
|
1281
|
+
)
|
|
1282
|
+
] });
|
|
1283
|
+
if (bare) {
|
|
1284
|
+
return /* @__PURE__ */ jsx13("div", { "data-testid": `filter-${column.id}`, children: panel });
|
|
1285
|
+
}
|
|
1286
|
+
return /* @__PURE__ */ jsxs8(Popover, { children: [
|
|
1287
|
+
/* @__PURE__ */ jsx13(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs8(
|
|
1288
|
+
Button,
|
|
1289
|
+
{
|
|
1290
|
+
variant: "outline",
|
|
1291
|
+
size: "sm",
|
|
1292
|
+
className: cn("gap-1", isFiltered ? "max-w-[160px] px-2" : "w-8 justify-center px-0"),
|
|
1293
|
+
"aria-label": `Filter ${column.header}`,
|
|
1294
|
+
"data-testid": `filter-${column.id}`,
|
|
1295
|
+
children: [
|
|
1296
|
+
/* @__PURE__ */ jsx13(FunnelIcon6, { className: "h-3.5 w-3.5 shrink-0 opacity-70", "aria-hidden": true }),
|
|
1297
|
+
isFiltered && /* @__PURE__ */ jsx13("span", { className: "truncate", children: text })
|
|
1298
|
+
]
|
|
1299
|
+
}
|
|
1300
|
+
) }),
|
|
1301
|
+
/* @__PURE__ */ jsx13(PopoverContent, { className: "w-auto p-3", children: panel })
|
|
1302
|
+
] });
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// src/filter/registry.tsx
|
|
1306
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
1307
|
+
function renderDefaultFilterWidget(column, value, onChange) {
|
|
1308
|
+
const bare = column.filterDisplay !== "row";
|
|
1309
|
+
switch (column.type) {
|
|
1310
|
+
case "string":
|
|
1311
|
+
return /* @__PURE__ */ jsx14(StringFilter, { column, value, onChange, bare });
|
|
1312
|
+
case "enum":
|
|
1313
|
+
return /* @__PURE__ */ jsx14(EnumFilter, { column, value, onChange, bare });
|
|
1314
|
+
case "boolean":
|
|
1315
|
+
return /* @__PURE__ */ jsx14(BooleanFilter, { column, value, onChange, bare });
|
|
1316
|
+
case "number":
|
|
1317
|
+
case "currency":
|
|
1318
|
+
return /* @__PURE__ */ jsx14(NumberRangeFilter, { column, value, onChange, bare });
|
|
1319
|
+
case "date":
|
|
1320
|
+
case "datetime":
|
|
1321
|
+
return /* @__PURE__ */ jsx14(DateRangeFilter, { column, value, onChange, bare });
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// src/column-selector/ColumnSelector.tsx
|
|
1326
|
+
import { Columns3 } from "lucide-react";
|
|
1327
|
+
import { useEffect as useEffect4 } from "react";
|
|
1328
|
+
|
|
1329
|
+
// src/components/ui/dialog.tsx
|
|
1330
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
1331
|
+
import { X } from "lucide-react";
|
|
1332
|
+
import { forwardRef as forwardRef6 } from "react";
|
|
1333
|
+
import { jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1334
|
+
var Dialog = DialogPrimitive.Root;
|
|
1335
|
+
var DialogTrigger = DialogPrimitive.Trigger;
|
|
1336
|
+
var DialogOverlay = forwardRef6(({ className, ...props }, ref) => /* @__PURE__ */ jsx15(
|
|
1337
|
+
DialogPrimitive.Overlay,
|
|
1338
|
+
{
|
|
1339
|
+
ref,
|
|
1340
|
+
className: cn("fixed inset-0 z-50 bg-black/50", className),
|
|
1341
|
+
...props
|
|
1342
|
+
}
|
|
1343
|
+
));
|
|
1344
|
+
DialogOverlay.displayName = "DialogOverlay";
|
|
1345
|
+
var DialogContent = forwardRef6(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs9(DialogPrimitive.Portal, { children: [
|
|
1346
|
+
/* @__PURE__ */ jsx15(DialogOverlay, {}),
|
|
1347
|
+
/* @__PURE__ */ jsxs9(
|
|
1348
|
+
DialogPrimitive.Content,
|
|
1349
|
+
{
|
|
1350
|
+
ref,
|
|
1351
|
+
className: cn(
|
|
1352
|
+
"fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-6 shadow-lg",
|
|
1353
|
+
className
|
|
1354
|
+
),
|
|
1355
|
+
...props,
|
|
1356
|
+
children: [
|
|
1357
|
+
children,
|
|
1358
|
+
/* @__PURE__ */ jsxs9(DialogPrimitive.Close, { className: "absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100", children: [
|
|
1359
|
+
/* @__PURE__ */ jsx15(X, { className: "h-4 w-4" }),
|
|
1360
|
+
/* @__PURE__ */ jsx15("span", { className: "sr-only", children: "Close" })
|
|
1361
|
+
] })
|
|
1362
|
+
]
|
|
1363
|
+
}
|
|
1364
|
+
)
|
|
1365
|
+
] }));
|
|
1366
|
+
DialogContent.displayName = "DialogContent";
|
|
1367
|
+
var DialogHeader = ({
|
|
1368
|
+
className,
|
|
1369
|
+
...props
|
|
1370
|
+
}) => /* @__PURE__ */ jsx15("div", { className: cn("flex flex-col gap-1.5", className), ...props });
|
|
1371
|
+
var DialogTitle = forwardRef6(({ className, ...props }, ref) => /* @__PURE__ */ jsx15(DialogPrimitive.Title, { ref, className: cn("text-lg font-semibold", className), ...props }));
|
|
1372
|
+
DialogTitle.displayName = "DialogTitle";
|
|
1373
|
+
var DialogDescription = forwardRef6(({ className, ...props }, ref) => /* @__PURE__ */ jsx15(
|
|
1374
|
+
DialogPrimitive.Description,
|
|
1375
|
+
{
|
|
1376
|
+
ref,
|
|
1377
|
+
className: cn("text-sm text-muted-foreground", className),
|
|
1378
|
+
...props
|
|
1379
|
+
}
|
|
1380
|
+
));
|
|
1381
|
+
DialogDescription.displayName = "DialogDescription";
|
|
1382
|
+
|
|
1383
|
+
// src/column-selector/persistence.ts
|
|
1384
|
+
function storageKeyFor(persistKey) {
|
|
1385
|
+
return `bmsui-datagrid:columns:${persistKey}`;
|
|
1386
|
+
}
|
|
1387
|
+
function isPlainObject(value) {
|
|
1388
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1389
|
+
}
|
|
1390
|
+
function readPersistedVisibility(persistKey) {
|
|
1391
|
+
const raw = window.localStorage.getItem(storageKeyFor(persistKey));
|
|
1392
|
+
if (raw === null) return null;
|
|
1393
|
+
try {
|
|
1394
|
+
const parsed = JSON.parse(raw);
|
|
1395
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
1396
|
+
} catch {
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
function writePersistedVisibility(persistKey, visibility) {
|
|
1401
|
+
window.localStorage.setItem(storageKeyFor(persistKey), JSON.stringify(visibility));
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// src/column-selector/visibility.ts
|
|
1405
|
+
function isColumnVisible(visibility, columnId) {
|
|
1406
|
+
return visibility[columnId] !== false;
|
|
1407
|
+
}
|
|
1408
|
+
function countVisible(columns, visibility) {
|
|
1409
|
+
return columns.filter((column) => isColumnVisible(visibility, column.id)).length;
|
|
1410
|
+
}
|
|
1411
|
+
function groupColumns(columns) {
|
|
1412
|
+
const ungrouped = [];
|
|
1413
|
+
const named = /* @__PURE__ */ new Map();
|
|
1414
|
+
for (const column of columns) {
|
|
1415
|
+
if (column.group === void 0) {
|
|
1416
|
+
ungrouped.push(column);
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
const bucket = named.get(column.group);
|
|
1420
|
+
if (bucket) bucket.push(column);
|
|
1421
|
+
else named.set(column.group, [column]);
|
|
1422
|
+
}
|
|
1423
|
+
const result = [];
|
|
1424
|
+
if (ungrouped.length > 0) result.push({ group: void 0, columns: ungrouped });
|
|
1425
|
+
for (const [group, groupedColumns] of named) result.push({ group, columns: groupedColumns });
|
|
1426
|
+
return result;
|
|
1427
|
+
}
|
|
1428
|
+
function canHideColumn(columns, visibility, columnId) {
|
|
1429
|
+
if (!isColumnVisible(visibility, columnId)) return true;
|
|
1430
|
+
return countVisible(columns, visibility) > 1;
|
|
1431
|
+
}
|
|
1432
|
+
function canHideGroup(columns, visibility, groupColumnIds) {
|
|
1433
|
+
const groupIds = new Set(groupColumnIds);
|
|
1434
|
+
const visibleOutsideGroup = columns.filter(
|
|
1435
|
+
(column) => !groupIds.has(column.id) && isColumnVisible(visibility, column.id)
|
|
1436
|
+
).length;
|
|
1437
|
+
return visibleOutsideGroup > 0;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// src/column-selector/ColumnSelector.tsx
|
|
1441
|
+
import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1442
|
+
function ColumnSelector({
|
|
1443
|
+
columns,
|
|
1444
|
+
visibility,
|
|
1445
|
+
onVisibilityChange,
|
|
1446
|
+
persistKey,
|
|
1447
|
+
trigger,
|
|
1448
|
+
open,
|
|
1449
|
+
onOpenChange
|
|
1450
|
+
}) {
|
|
1451
|
+
useEffect4(() => {
|
|
1452
|
+
if (!persistKey) return;
|
|
1453
|
+
const restored = readPersistedVisibility(persistKey);
|
|
1454
|
+
if (restored) onVisibilityChange({ ...visibility, ...restored });
|
|
1455
|
+
}, [persistKey]);
|
|
1456
|
+
function commit(next) {
|
|
1457
|
+
onVisibilityChange(next);
|
|
1458
|
+
if (persistKey) writePersistedVisibility(persistKey, next);
|
|
1459
|
+
}
|
|
1460
|
+
function toggleColumn(columnId) {
|
|
1461
|
+
if (!canHideColumn(columns, visibility, columnId)) return;
|
|
1462
|
+
commit({ ...visibility, [columnId]: !isColumnVisible(visibility, columnId) });
|
|
1463
|
+
}
|
|
1464
|
+
const totalVisible = countVisible(columns, visibility);
|
|
1465
|
+
const groups = groupColumns(columns);
|
|
1466
|
+
return /* @__PURE__ */ jsxs10(Dialog, { open, onOpenChange, children: [
|
|
1467
|
+
/* @__PURE__ */ jsx16(DialogTrigger, { asChild: true, children: trigger ?? /* @__PURE__ */ jsx16(Button, { variant: "outline", size: "icon", "aria-label": "Choose columns", children: /* @__PURE__ */ jsx16(Columns3, { className: "h-4 w-4", "aria-hidden": true }) }) }),
|
|
1468
|
+
/* @__PURE__ */ jsxs10(DialogContent, { className: "w-fit max-w-[calc(100vw-2rem)] sm:max-w-2xl", children: [
|
|
1469
|
+
/* @__PURE__ */ jsx16(DialogHeader, { children: /* @__PURE__ */ jsx16(DialogTitle, { children: "Columns" }) }),
|
|
1470
|
+
/* @__PURE__ */ jsx16("div", { className: "flex flex-wrap gap-x-8 gap-y-5", children: groups.map(({ group, columns: groupCols }) => /* @__PURE__ */ jsxs10("div", { className: "flex min-w-32 flex-col gap-1.5", children: [
|
|
1471
|
+
group && /* @__PURE__ */ jsx16("span", { className: "border-b pb-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: group }),
|
|
1472
|
+
groupCols.map((column) => {
|
|
1473
|
+
const visible = isColumnVisible(visibility, column.id);
|
|
1474
|
+
return /* @__PURE__ */ jsxs10(
|
|
1475
|
+
"label",
|
|
1476
|
+
{
|
|
1477
|
+
className: "flex items-center gap-2 rounded-md px-1.5 py-1 text-sm hover:bg-accent/50 focus-within:bg-accent/50",
|
|
1478
|
+
children: [
|
|
1479
|
+
/* @__PURE__ */ jsx16(
|
|
1480
|
+
Checkbox,
|
|
1481
|
+
{
|
|
1482
|
+
checked: visible,
|
|
1483
|
+
disabled: visible && totalVisible <= 1,
|
|
1484
|
+
onCheckedChange: () => toggleColumn(column.id)
|
|
1485
|
+
}
|
|
1486
|
+
),
|
|
1487
|
+
column.header
|
|
1488
|
+
]
|
|
1489
|
+
},
|
|
1490
|
+
column.id
|
|
1491
|
+
);
|
|
1492
|
+
})
|
|
1493
|
+
] }, group ?? "__ungrouped__")) })
|
|
1494
|
+
] })
|
|
1495
|
+
] });
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// src/menu/resolveMenuItems.ts
|
|
1499
|
+
function resolveMenuItems(items, ctx) {
|
|
1500
|
+
const resolved = [];
|
|
1501
|
+
for (const item of items) {
|
|
1502
|
+
const isVisible = item.visible ? item.visible(ctx) : true;
|
|
1503
|
+
if (!isVisible) continue;
|
|
1504
|
+
resolved.push({ ...item, isDisabled: item.disabled ? item.disabled(ctx) : false });
|
|
1505
|
+
}
|
|
1506
|
+
return resolved;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// src/menu/ActionsMenu.tsx
|
|
1510
|
+
import { MoreVertical } from "lucide-react";
|
|
1511
|
+
|
|
1512
|
+
// src/components/ui/dropdown-menu.tsx
|
|
1513
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
1514
|
+
import { forwardRef as forwardRef7 } from "react";
|
|
1515
|
+
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
1516
|
+
var DropdownMenu = DropdownMenuPrimitive.Root;
|
|
1517
|
+
var DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
1518
|
+
var DropdownMenuContent = forwardRef7(({ className, sideOffset = 4, ...props }, ref) => /* @__PURE__ */ jsx17(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx17(
|
|
1519
|
+
DropdownMenuPrimitive.Content,
|
|
1520
|
+
{
|
|
1521
|
+
ref,
|
|
1522
|
+
sideOffset,
|
|
1523
|
+
className: cn(
|
|
1524
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
1525
|
+
className
|
|
1526
|
+
),
|
|
1527
|
+
...props
|
|
1528
|
+
}
|
|
1529
|
+
) }));
|
|
1530
|
+
DropdownMenuContent.displayName = "DropdownMenuContent";
|
|
1531
|
+
var DropdownMenuItem = forwardRef7(({ className, danger, ...props }, ref) => /* @__PURE__ */ jsx17(
|
|
1532
|
+
DropdownMenuPrimitive.Item,
|
|
1533
|
+
{
|
|
1534
|
+
ref,
|
|
1535
|
+
className: cn(
|
|
1536
|
+
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
1537
|
+
danger && "text-destructive focus:bg-destructive/10 focus:text-destructive",
|
|
1538
|
+
className
|
|
1539
|
+
),
|
|
1540
|
+
...props
|
|
1541
|
+
}
|
|
1542
|
+
));
|
|
1543
|
+
DropdownMenuItem.displayName = "DropdownMenuItem";
|
|
1544
|
+
var DropdownMenuSeparator = forwardRef7(({ className, ...props }, ref) => /* @__PURE__ */ jsx17(
|
|
1545
|
+
DropdownMenuPrimitive.Separator,
|
|
1546
|
+
{
|
|
1547
|
+
ref,
|
|
1548
|
+
className: cn("-mx-1 my-1 h-px bg-muted", className),
|
|
1549
|
+
...props
|
|
1550
|
+
}
|
|
1551
|
+
));
|
|
1552
|
+
DropdownMenuSeparator.displayName = "DropdownMenuSeparator";
|
|
1553
|
+
var DropdownMenuLabel = forwardRef7(({ className, ...props }, ref) => /* @__PURE__ */ jsx17(
|
|
1554
|
+
DropdownMenuPrimitive.Label,
|
|
1555
|
+
{
|
|
1556
|
+
ref,
|
|
1557
|
+
className: cn("px-2 py-1.5 text-xs font-medium text-muted-foreground", className),
|
|
1558
|
+
...props
|
|
1559
|
+
}
|
|
1560
|
+
));
|
|
1561
|
+
DropdownMenuLabel.displayName = "DropdownMenuLabel";
|
|
1562
|
+
|
|
1563
|
+
// src/menu/ActionsMenu.tsx
|
|
1564
|
+
import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1565
|
+
function ActionsMenu({ items, ctx, triggerLabel }) {
|
|
1566
|
+
const resolved = resolveMenuItems(items, ctx);
|
|
1567
|
+
if (resolved.length === 0) return null;
|
|
1568
|
+
return /* @__PURE__ */ jsxs11(DropdownMenu, { children: [
|
|
1569
|
+
/* @__PURE__ */ jsx18(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx18(Button, { variant: "ghost", size: "icon", "aria-label": triggerLabel, onClick: stopRowClick, children: /* @__PURE__ */ jsx18(MoreVertical, { className: "h-4 w-4", "aria-hidden": true }) }) }),
|
|
1570
|
+
/* @__PURE__ */ jsx18(DropdownMenuContent, { align: "end", children: resolved.map((item) => /* @__PURE__ */ jsx18(
|
|
1571
|
+
DropdownMenuItem,
|
|
1572
|
+
{
|
|
1573
|
+
danger: item.danger,
|
|
1574
|
+
disabled: item.isDisabled,
|
|
1575
|
+
onSelect: () => item.onSelect(ctx),
|
|
1576
|
+
children: item.label
|
|
1577
|
+
},
|
|
1578
|
+
item.id
|
|
1579
|
+
)) })
|
|
1580
|
+
] });
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// src/grid/DataGrid.tsx
|
|
1584
|
+
import { BarsArrowDownIcon, BarsArrowUpIcon, FunnelIcon as FunnelIcon7 } from "@heroicons/react/24/outline";
|
|
1585
|
+
import { columnResizingFeature, columnSizingFeature, flexRender, tableFeatures, useTable } from "@tanstack/react-table";
|
|
1586
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
1587
|
+
import { ChevronRight, Loader2 } from "lucide-react";
|
|
1588
|
+
import { Fragment as Fragment2, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef5, useState as useState6 } from "react";
|
|
1589
|
+
|
|
1590
|
+
// src/grid/processClientData.ts
|
|
1591
|
+
function makeComparator(sort) {
|
|
1592
|
+
return (a, b) => {
|
|
1593
|
+
for (const { field, dir } of sort) {
|
|
1594
|
+
const av = getFieldValue(a, field);
|
|
1595
|
+
const bv = getFieldValue(b, field);
|
|
1596
|
+
const aNull = av === null || av === void 0;
|
|
1597
|
+
const bNull = bv === null || bv === void 0;
|
|
1598
|
+
if (aNull && bNull) continue;
|
|
1599
|
+
if (aNull) return 1;
|
|
1600
|
+
if (bNull) return -1;
|
|
1601
|
+
const cmp = compareValues(av, bv) ?? 0;
|
|
1602
|
+
if (cmp !== 0) return dir === "asc" ? cmp : -cmp;
|
|
1603
|
+
}
|
|
1604
|
+
return 0;
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
function processClientData(data, state) {
|
|
1608
|
+
const filtered = state.filter ? data.filter((row) => evaluateFilter(row, state.filter)) : data;
|
|
1609
|
+
const sorted = state.sort.length > 0 ? [...filtered].sort(makeComparator(state.sort)) : filtered;
|
|
1610
|
+
const start = state.page * state.pageSize;
|
|
1611
|
+
const rows = sorted.slice(start, start + state.pageSize);
|
|
1612
|
+
return { rows, rowCount: sorted.length };
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// src/grid/useGridState.ts
|
|
1616
|
+
import { useCallback as useCallback2, useEffect as useEffect6, useMemo as useMemo4, useState as useState5 } from "react";
|
|
1617
|
+
|
|
1618
|
+
// src/hooks/useDebouncedCallback.ts
|
|
1619
|
+
import { useEffect as useEffect5, useMemo as useMemo3, useRef as useRef4 } from "react";
|
|
1620
|
+
function useDebouncedCallback(callback, delayMs) {
|
|
1621
|
+
const callbackRef = useRef4(callback);
|
|
1622
|
+
callbackRef.current = callback;
|
|
1623
|
+
const timeoutRef = useRef4(void 0);
|
|
1624
|
+
useEffect5(() => {
|
|
1625
|
+
return () => {
|
|
1626
|
+
clearTimeout(timeoutRef.current);
|
|
1627
|
+
};
|
|
1628
|
+
}, []);
|
|
1629
|
+
return useMemo3(() => {
|
|
1630
|
+
return {
|
|
1631
|
+
run: (...args) => {
|
|
1632
|
+
clearTimeout(timeoutRef.current);
|
|
1633
|
+
timeoutRef.current = setTimeout(() => {
|
|
1634
|
+
callbackRef.current(...args);
|
|
1635
|
+
}, delayMs);
|
|
1636
|
+
},
|
|
1637
|
+
cancel: () => {
|
|
1638
|
+
clearTimeout(timeoutRef.current);
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
}, [delayMs]);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// src/grid/useGridState.ts
|
|
1645
|
+
var DEFAULT_STATE = { filter: null, sort: [], page: 0, pageSize: 20 };
|
|
1646
|
+
var FILTER_CHANGE_DEBOUNCE_MS = 300;
|
|
1647
|
+
function nextSortDir(current, descFirst) {
|
|
1648
|
+
const [first, second] = descFirst ? ["desc", "asc"] : ["asc", "desc"];
|
|
1649
|
+
if (current === void 0) return first;
|
|
1650
|
+
if (current === first) return second;
|
|
1651
|
+
return void 0;
|
|
1652
|
+
}
|
|
1653
|
+
function useGridState(dataSource, initialState, externalState) {
|
|
1654
|
+
const [state, setState] = useState5(
|
|
1655
|
+
() => externalState ?? { ...DEFAULT_STATE, ...initialState }
|
|
1656
|
+
);
|
|
1657
|
+
const notifyRaw = useCallback2(
|
|
1658
|
+
(next) => {
|
|
1659
|
+
if (dataSource.mode === "server") dataSource.onStateChange(next);
|
|
1660
|
+
},
|
|
1661
|
+
[dataSource]
|
|
1662
|
+
);
|
|
1663
|
+
const { run: notifyDebounced, cancel: cancelDebouncedNotify } = useDebouncedCallback(
|
|
1664
|
+
notifyRaw,
|
|
1665
|
+
FILTER_CHANGE_DEBOUNCE_MS
|
|
1666
|
+
);
|
|
1667
|
+
const notifyNow = useCallback2(
|
|
1668
|
+
(next) => {
|
|
1669
|
+
cancelDebouncedNotify();
|
|
1670
|
+
notifyRaw(next);
|
|
1671
|
+
},
|
|
1672
|
+
[notifyRaw, cancelDebouncedNotify]
|
|
1673
|
+
);
|
|
1674
|
+
useEffect6(() => {
|
|
1675
|
+
if (!externalState) return;
|
|
1676
|
+
cancelDebouncedNotify();
|
|
1677
|
+
setState(externalState);
|
|
1678
|
+
}, [externalState, cancelDebouncedNotify]);
|
|
1679
|
+
const filtersByColumn = useMemo4(() => {
|
|
1680
|
+
const map = /* @__PURE__ */ new Map();
|
|
1681
|
+
for (const child of state.filter?.filters ?? []) {
|
|
1682
|
+
if ("operator" in child) map.set(fieldKey(child.field), child);
|
|
1683
|
+
}
|
|
1684
|
+
return map;
|
|
1685
|
+
}, [state.filter]);
|
|
1686
|
+
const setColumnFilter = useCallback2(
|
|
1687
|
+
(columnId, next) => {
|
|
1688
|
+
setState((prev) => {
|
|
1689
|
+
const remaining = (prev.filter?.filters ?? []).filter(
|
|
1690
|
+
(f) => !("operator" in f) || fieldKey(f.field) !== columnId
|
|
1691
|
+
);
|
|
1692
|
+
const filters = next ? [...remaining, next] : remaining;
|
|
1693
|
+
const nextState = {
|
|
1694
|
+
...prev,
|
|
1695
|
+
filter: filters.length > 0 ? { logic: "and", filters } : null,
|
|
1696
|
+
page: 0
|
|
1697
|
+
};
|
|
1698
|
+
notifyDebounced(nextState);
|
|
1699
|
+
return nextState;
|
|
1700
|
+
});
|
|
1701
|
+
},
|
|
1702
|
+
[notifyDebounced]
|
|
1703
|
+
);
|
|
1704
|
+
const toggleSort = useCallback2(
|
|
1705
|
+
(columnId, additive, descFirst = false) => {
|
|
1706
|
+
setState((prev) => {
|
|
1707
|
+
const existing = prev.sort.find((s) => s.field === columnId);
|
|
1708
|
+
const dir = nextSortDir(existing?.dir, descFirst);
|
|
1709
|
+
const withoutColumn2 = prev.sort.filter((s) => s.field !== columnId);
|
|
1710
|
+
const base = additive ? withoutColumn2 : [];
|
|
1711
|
+
const sort = dir ? [...base, { field: columnId, dir }] : base;
|
|
1712
|
+
const nextState = { ...prev, sort };
|
|
1713
|
+
notifyNow(nextState);
|
|
1714
|
+
return nextState;
|
|
1715
|
+
});
|
|
1716
|
+
},
|
|
1717
|
+
[notifyNow]
|
|
1718
|
+
);
|
|
1719
|
+
const setPage = useCallback2(
|
|
1720
|
+
(page) => {
|
|
1721
|
+
setState((prev) => {
|
|
1722
|
+
const nextState = { ...prev, page };
|
|
1723
|
+
notifyNow(nextState);
|
|
1724
|
+
return nextState;
|
|
1725
|
+
});
|
|
1726
|
+
},
|
|
1727
|
+
[notifyNow]
|
|
1728
|
+
);
|
|
1729
|
+
const setPageSize = useCallback2(
|
|
1730
|
+
(pageSize) => {
|
|
1731
|
+
setState((prev) => {
|
|
1732
|
+
const nextState = { ...prev, pageSize, page: 0 };
|
|
1733
|
+
notifyNow(nextState);
|
|
1734
|
+
return nextState;
|
|
1735
|
+
});
|
|
1736
|
+
},
|
|
1737
|
+
[notifyNow]
|
|
1738
|
+
);
|
|
1739
|
+
return { state, filtersByColumn, setColumnFilter, toggleSort, setPage, setPageSize };
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// src/grid/DataGrid.tsx
|
|
1743
|
+
import { Fragment as Fragment3, jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1744
|
+
var gridTableFeatures = tableFeatures({ columnSizingFeature, columnResizingFeature });
|
|
1745
|
+
var DEFAULT_COLUMN_SIZE = 150;
|
|
1746
|
+
function pinnedOffsets(visibleColumns, side, getSize) {
|
|
1747
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
1748
|
+
let cursor = 0;
|
|
1749
|
+
const ordered = side === "left" ? visibleColumns : [...visibleColumns].reverse();
|
|
1750
|
+
for (const column of ordered) {
|
|
1751
|
+
if (column.pinned !== side) continue;
|
|
1752
|
+
offsets.set(column.id, cursor);
|
|
1753
|
+
cursor += getSize(column.id);
|
|
1754
|
+
}
|
|
1755
|
+
return offsets;
|
|
1756
|
+
}
|
|
1757
|
+
function computeHeaderRuns(columns) {
|
|
1758
|
+
const runs = [];
|
|
1759
|
+
for (const column of columns) {
|
|
1760
|
+
const last = runs[runs.length - 1];
|
|
1761
|
+
if (column.headerGroup && last?.kind === "group" && last.label === column.headerGroup) {
|
|
1762
|
+
last.columns.push(column);
|
|
1763
|
+
} else if (column.headerGroup) {
|
|
1764
|
+
runs.push({ kind: "group", label: column.headerGroup, columns: [column] });
|
|
1765
|
+
} else {
|
|
1766
|
+
runs.push({ kind: "single", column });
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
return runs;
|
|
1770
|
+
}
|
|
1771
|
+
function toTanstackColumns(columns) {
|
|
1772
|
+
return columns.map((column) => ({
|
|
1773
|
+
id: column.id,
|
|
1774
|
+
header: column.header,
|
|
1775
|
+
size: column.width,
|
|
1776
|
+
accessorFn: (row) => getColumnValue(column, row),
|
|
1777
|
+
cell: (info) => column.cell ? column.cell(info.getValue(), info.row.original) : defaultFormat(column, info.getValue())
|
|
1778
|
+
}));
|
|
1779
|
+
}
|
|
1780
|
+
function DataGrid({
|
|
1781
|
+
columns,
|
|
1782
|
+
dataSource,
|
|
1783
|
+
getRowId,
|
|
1784
|
+
rowActions,
|
|
1785
|
+
headerActions,
|
|
1786
|
+
enableMultiSort = false,
|
|
1787
|
+
initialState,
|
|
1788
|
+
columnVisibility,
|
|
1789
|
+
renderDetail,
|
|
1790
|
+
enableColumnResizing = false,
|
|
1791
|
+
columnResizeMode = "onChange",
|
|
1792
|
+
columnSizing,
|
|
1793
|
+
onColumnSizingChange,
|
|
1794
|
+
virtualize,
|
|
1795
|
+
showPagination = true,
|
|
1796
|
+
getRowProps,
|
|
1797
|
+
getRowTestId,
|
|
1798
|
+
gridState,
|
|
1799
|
+
selectedIds: controlledSelectedIds,
|
|
1800
|
+
onSelectedIdsChange,
|
|
1801
|
+
testId,
|
|
1802
|
+
loading: loadingProp = false
|
|
1803
|
+
}) {
|
|
1804
|
+
const { state, filtersByColumn, setColumnFilter, toggleSort, setPage } = useGridState(
|
|
1805
|
+
dataSource,
|
|
1806
|
+
initialState,
|
|
1807
|
+
gridState
|
|
1808
|
+
);
|
|
1809
|
+
const [internalSelectedIds, setInternalSelectedIds] = useState6(/* @__PURE__ */ new Set());
|
|
1810
|
+
const selectedIds = controlledSelectedIds ?? internalSelectedIds;
|
|
1811
|
+
function updateSelectedIds(next) {
|
|
1812
|
+
setInternalSelectedIds(next);
|
|
1813
|
+
onSelectedIdsChange?.(next);
|
|
1814
|
+
}
|
|
1815
|
+
const [expandedIds, setExpandedIds] = useState6(/* @__PURE__ */ new Set());
|
|
1816
|
+
function toggleRowExpanded(id) {
|
|
1817
|
+
setExpandedIds((prev) => {
|
|
1818
|
+
const next = new Set(prev);
|
|
1819
|
+
if (next.has(id)) next.delete(id);
|
|
1820
|
+
else next.add(id);
|
|
1821
|
+
return next;
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
const visibleColumns = useMemo5(
|
|
1825
|
+
() => columnVisibility ? columns.filter((column) => isColumnVisible(columnVisibility, column.id)) : columns,
|
|
1826
|
+
[columns, columnVisibility]
|
|
1827
|
+
);
|
|
1828
|
+
const serverRowCount = dataSource.mode === "server" ? dataSource.rowCount : void 0;
|
|
1829
|
+
const { rows, rowCount } = useMemo5(() => {
|
|
1830
|
+
if (dataSource.mode === "client") return processClientData(dataSource.data, state);
|
|
1831
|
+
return { rows: dataSource.data, rowCount: serverRowCount ?? 0 };
|
|
1832
|
+
}, [dataSource.mode, dataSource.data, serverRowCount, state]);
|
|
1833
|
+
const selectedRows = useMemo5(
|
|
1834
|
+
() => rows.filter((row) => selectedIds.has(getRowId(row))),
|
|
1835
|
+
[rows, selectedIds, getRowId]
|
|
1836
|
+
);
|
|
1837
|
+
const allOnPageSelected = rows.length > 0 && rows.every((row) => selectedIds.has(getRowId(row)));
|
|
1838
|
+
function toggleRowSelected(id) {
|
|
1839
|
+
const next = new Set(selectedIds);
|
|
1840
|
+
if (next.has(id)) next.delete(id);
|
|
1841
|
+
else next.add(id);
|
|
1842
|
+
updateSelectedIds(next);
|
|
1843
|
+
}
|
|
1844
|
+
function toggleAllOnPageSelected() {
|
|
1845
|
+
const next = new Set(selectedIds);
|
|
1846
|
+
for (const row of rows) {
|
|
1847
|
+
if (allOnPageSelected) next.delete(getRowId(row));
|
|
1848
|
+
else next.add(getRowId(row));
|
|
1849
|
+
}
|
|
1850
|
+
updateSelectedIds(next);
|
|
1851
|
+
}
|
|
1852
|
+
const tanstackColumns = useMemo5(() => toTanstackColumns(visibleColumns), [visibleColumns]);
|
|
1853
|
+
const columnById = useMemo5(() => {
|
|
1854
|
+
const map = /* @__PURE__ */ new Map();
|
|
1855
|
+
for (const column of visibleColumns) map.set(column.id, column);
|
|
1856
|
+
return map;
|
|
1857
|
+
}, [visibleColumns]);
|
|
1858
|
+
const table = useTable({
|
|
1859
|
+
features: gridTableFeatures,
|
|
1860
|
+
data: rows,
|
|
1861
|
+
columns: tanstackColumns,
|
|
1862
|
+
getRowId,
|
|
1863
|
+
enableColumnResizing,
|
|
1864
|
+
columnResizeMode,
|
|
1865
|
+
...columnSizing ? {
|
|
1866
|
+
state: { columnSizing },
|
|
1867
|
+
onColumnSizingChange: (updater) => {
|
|
1868
|
+
const next = typeof updater === "function" ? updater(columnSizing) : updater;
|
|
1869
|
+
onColumnSizingChange?.(next);
|
|
1870
|
+
}
|
|
1871
|
+
} : {}
|
|
1872
|
+
});
|
|
1873
|
+
const pageCount = Math.max(1, Math.ceil(rowCount / state.pageSize));
|
|
1874
|
+
const loading = loadingProp || (dataSource.mode === "server" ? Boolean(dataSource.loading) : false);
|
|
1875
|
+
const showSelectionColumn = Boolean(headerActions?.length) || controlledSelectedIds !== void 0;
|
|
1876
|
+
const showRowActionsColumn = Boolean(rowActions?.length);
|
|
1877
|
+
const showDetailColumn = Boolean(renderDetail);
|
|
1878
|
+
const totalColumnCount = visibleColumns.length + (showDetailColumn ? 1 : 0) + (showSelectionColumn ? 1 : 0) + (showRowActionsColumn ? 1 : 0);
|
|
1879
|
+
const hasFilterRow = visibleColumns.some((column) => isFilterable(column) && column.filterDisplay === "row");
|
|
1880
|
+
const headerRuns = useMemo5(() => computeHeaderRuns(visibleColumns), [visibleColumns]);
|
|
1881
|
+
const hasHeaderGroups = visibleColumns.some((column) => column.headerGroup);
|
|
1882
|
+
function renderFilterWidget(column) {
|
|
1883
|
+
const value = filtersByColumn.get(column.id);
|
|
1884
|
+
const onChange = (next) => setColumnFilter(column.id, next);
|
|
1885
|
+
return column.renderFilter ? column.renderFilter(value, onChange, state.filter) : renderDefaultFilterWidget(column, value, onChange);
|
|
1886
|
+
}
|
|
1887
|
+
const columnSize = (columnId) => table.getColumn(columnId)?.getSize() ?? DEFAULT_COLUMN_SIZE;
|
|
1888
|
+
const leftPinnedOffsets = pinnedOffsets(visibleColumns, "left", columnSize);
|
|
1889
|
+
const rightPinnedOffsets = pinnedOffsets(visibleColumns, "right", columnSize);
|
|
1890
|
+
function pinnedCellProps(column, area = "body") {
|
|
1891
|
+
const bg = area === "header" ? "bg-muted" : "bg-background";
|
|
1892
|
+
if (column.pinned === "left") {
|
|
1893
|
+
return { className: cn("sticky z-10", bg), style: { left: leftPinnedOffsets.get(column.id), width: columnSize(column.id) } };
|
|
1894
|
+
}
|
|
1895
|
+
if (column.pinned === "right") {
|
|
1896
|
+
return { className: cn("sticky z-10", bg), style: { right: rightPinnedOffsets.get(column.id), width: columnSize(column.id) } };
|
|
1897
|
+
}
|
|
1898
|
+
if (enableColumnResizing) return { style: { width: columnSize(column.id) } };
|
|
1899
|
+
return { style: column.width ? { width: column.width } : void 0 };
|
|
1900
|
+
}
|
|
1901
|
+
const tableRows = table.getRowModel().rows;
|
|
1902
|
+
const shouldVirtualize = Boolean(virtualize) && tableRows.length > (virtualize?.threshold ?? 100);
|
|
1903
|
+
const scrollRef = useRef5(null);
|
|
1904
|
+
const virtualizer = useVirtualizer({
|
|
1905
|
+
count: tableRows.length,
|
|
1906
|
+
getScrollElement: () => scrollRef.current,
|
|
1907
|
+
estimateSize: () => virtualize?.estimatedRowHeight ?? 40,
|
|
1908
|
+
overscan: virtualize?.overscan ?? 10,
|
|
1909
|
+
enabled: shouldVirtualize
|
|
1910
|
+
});
|
|
1911
|
+
const virtualItems = shouldVirtualize ? virtualizer.getVirtualItems() : [];
|
|
1912
|
+
const paddingTop = virtualItems.length > 0 ? virtualItems[0].start : 0;
|
|
1913
|
+
const paddingBottom = virtualItems.length > 0 ? virtualizer.getTotalSize() - virtualItems[virtualItems.length - 1].end : 0;
|
|
1914
|
+
const lastVisibleIndex = virtualItems.length > 0 ? virtualItems[virtualItems.length - 1].index : -1;
|
|
1915
|
+
const notifiedForLengthRef = useRef5(-1);
|
|
1916
|
+
useEffect7(() => {
|
|
1917
|
+
if (!virtualize?.onEndReached) return;
|
|
1918
|
+
if (lastVisibleIndex < 0 || lastVisibleIndex < tableRows.length - 1) return;
|
|
1919
|
+
if (virtualize.hasMore === false) return;
|
|
1920
|
+
if (notifiedForLengthRef.current === tableRows.length) return;
|
|
1921
|
+
notifiedForLengthRef.current = tableRows.length;
|
|
1922
|
+
virtualize.onEndReached();
|
|
1923
|
+
}, [lastVisibleIndex, tableRows.length, virtualize]);
|
|
1924
|
+
function renderRow(row, measureRef) {
|
|
1925
|
+
const isExpanded = expandedIds.has(row.id);
|
|
1926
|
+
const rowProps = getRowProps?.(row.original);
|
|
1927
|
+
const rowTestId = getRowTestId?.(row.original) ?? `row-${row.id}`;
|
|
1928
|
+
return /* @__PURE__ */ jsxs12("tbody", { ref: measureRef, "data-index": row.index, children: [
|
|
1929
|
+
/* @__PURE__ */ jsxs12(
|
|
1930
|
+
"tr",
|
|
1931
|
+
{
|
|
1932
|
+
...rowProps,
|
|
1933
|
+
"data-testid": rowTestId,
|
|
1934
|
+
className: cn(rowProps?.className, row.index % 2 === 1 && "bg-foreground/5"),
|
|
1935
|
+
children: [
|
|
1936
|
+
showDetailColumn && /* @__PURE__ */ jsx19("td", { className: "border-b p-2", children: /* @__PURE__ */ jsx19(
|
|
1937
|
+
"button",
|
|
1938
|
+
{
|
|
1939
|
+
type: "button",
|
|
1940
|
+
"aria-label": `${isExpanded ? "Collapse" : "Expand"} row ${row.id}`,
|
|
1941
|
+
"aria-expanded": isExpanded,
|
|
1942
|
+
onClick: (event) => {
|
|
1943
|
+
stopRowClick(event);
|
|
1944
|
+
toggleRowExpanded(row.id);
|
|
1945
|
+
},
|
|
1946
|
+
children: /* @__PURE__ */ jsx19(ChevronRight, { className: cn("h-4 w-4 transition-transform", isExpanded && "rotate-90"), "aria-hidden": true })
|
|
1947
|
+
}
|
|
1948
|
+
) }),
|
|
1949
|
+
showSelectionColumn && /* @__PURE__ */ jsx19("td", { className: "border-b p-2", children: /* @__PURE__ */ jsx19(
|
|
1950
|
+
"input",
|
|
1951
|
+
{
|
|
1952
|
+
type: "checkbox",
|
|
1953
|
+
"aria-label": `Select row ${row.id}`,
|
|
1954
|
+
checked: selectedIds.has(row.id),
|
|
1955
|
+
onClick: stopRowClick,
|
|
1956
|
+
onChange: () => toggleRowSelected(row.id)
|
|
1957
|
+
}
|
|
1958
|
+
) }),
|
|
1959
|
+
row.getAllCells().map((cell) => {
|
|
1960
|
+
const cellColumn = columnById.get(cell.column.id);
|
|
1961
|
+
const pinnedProps = cellColumn && (cellColumn.pinned || enableColumnResizing) ? pinnedCellProps(cellColumn) : void 0;
|
|
1962
|
+
return /* @__PURE__ */ jsx19("td", { style: pinnedProps?.style, className: cn("border-b p-2", pinnedProps?.className), children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id);
|
|
1963
|
+
}),
|
|
1964
|
+
showRowActionsColumn && rowActions && /* @__PURE__ */ jsx19("td", { className: "border-b p-2", children: /* @__PURE__ */ jsx19(ActionsMenu, { items: rowActions, ctx: { row: row.original }, triggerLabel: `Row actions for ${row.id}` }) })
|
|
1965
|
+
]
|
|
1966
|
+
}
|
|
1967
|
+
),
|
|
1968
|
+
showDetailColumn && isExpanded && renderDetail && /* @__PURE__ */ jsx19("tr", { "data-testid": `${rowTestId}-detail`, children: /* @__PURE__ */ jsx19("td", { colSpan: totalColumnCount, className: "border-b p-2", children: renderDetail(row.original) }) })
|
|
1969
|
+
] }, row.id);
|
|
1970
|
+
}
|
|
1971
|
+
return /* @__PURE__ */ jsxs12("div", { className: "flex flex-col gap-2", "data-testid": "datagrid", children: [
|
|
1972
|
+
showSelectionColumn && headerActions && /* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-between text-sm", children: [
|
|
1973
|
+
/* @__PURE__ */ jsxs12("span", { children: [
|
|
1974
|
+
selectedRows.length,
|
|
1975
|
+
" selected"
|
|
1976
|
+
] }),
|
|
1977
|
+
/* @__PURE__ */ jsx19(ActionsMenu, { items: headerActions, ctx: { selectedRows }, triggerLabel: "Bulk actions" })
|
|
1978
|
+
] }),
|
|
1979
|
+
/* @__PURE__ */ jsxs12("div", { className: "relative", children: [
|
|
1980
|
+
loading && /* @__PURE__ */ jsx19(
|
|
1981
|
+
"div",
|
|
1982
|
+
{
|
|
1983
|
+
"data-testid": "datagrid-loading-overlay",
|
|
1984
|
+
className: "absolute inset-0 z-10 flex items-center justify-center bg-background/60",
|
|
1985
|
+
children: /* @__PURE__ */ jsx19(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground", "aria-hidden": true })
|
|
1986
|
+
}
|
|
1987
|
+
),
|
|
1988
|
+
/* @__PURE__ */ jsx19(
|
|
1989
|
+
"div",
|
|
1990
|
+
{
|
|
1991
|
+
ref: scrollRef,
|
|
1992
|
+
"data-testid": testId,
|
|
1993
|
+
className: cn("overflow-x-auto rounded-md border", shouldVirtualize && "overflow-y-auto"),
|
|
1994
|
+
style: shouldVirtualize ? { maxHeight: virtualize?.maxBodyHeight ?? 480 } : void 0,
|
|
1995
|
+
children: /* @__PURE__ */ jsxs12("table", { className: "w-full border-collapse text-sm", children: [
|
|
1996
|
+
/* @__PURE__ */ jsxs12("thead", { className: "bg-muted", children: [
|
|
1997
|
+
table.getHeaderGroups().map((headerGroup) => {
|
|
1998
|
+
const headerById = new Map(headerGroup.headers.map((header) => [header.id, header]));
|
|
1999
|
+
function renderLeafHeaderCell(column, rowSpan) {
|
|
2000
|
+
const header = headerById.get(column.id);
|
|
2001
|
+
if (!header) return null;
|
|
2002
|
+
const sortEntry = state.sort.find((s) => s.field === column.id);
|
|
2003
|
+
const sortable = isSortable(column);
|
|
2004
|
+
const filterable = isFilterable(column) && column.filterDisplay !== "row";
|
|
2005
|
+
const pinnedProps = pinnedCellProps(column, "header");
|
|
2006
|
+
return /* @__PURE__ */ jsxs12(
|
|
2007
|
+
"th",
|
|
2008
|
+
{
|
|
2009
|
+
rowSpan,
|
|
2010
|
+
style: pinnedProps.style,
|
|
2011
|
+
className: cn("relative border-b p-2 font-medium", alignClassName(column), pinnedProps.className),
|
|
2012
|
+
children: [
|
|
2013
|
+
/* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-1", children: [
|
|
2014
|
+
/* @__PURE__ */ jsxs12(
|
|
2015
|
+
"button",
|
|
2016
|
+
{
|
|
2017
|
+
type: "button",
|
|
2018
|
+
className: cn(
|
|
2019
|
+
"flex items-center gap-1 rounded-sm px-1",
|
|
2020
|
+
sortable && "hover:bg-accent hover:text-accent-foreground"
|
|
2021
|
+
),
|
|
2022
|
+
disabled: !sortable,
|
|
2023
|
+
onClick: (event) => sortable && toggleSort(column.id, enableMultiSort && event.shiftKey, column.sortDescFirst === true),
|
|
2024
|
+
children: [
|
|
2025
|
+
column.renderHeader ? column.renderHeader(column) : column.header,
|
|
2026
|
+
sortable && sortEntry?.dir === "asc" && /* @__PURE__ */ jsx19(BarsArrowUpIcon, { className: "h-3 w-3", "aria-hidden": true }),
|
|
2027
|
+
sortable && sortEntry?.dir === "desc" && /* @__PURE__ */ jsx19(BarsArrowDownIcon, { className: "h-3 w-3", "aria-hidden": true })
|
|
2028
|
+
]
|
|
2029
|
+
}
|
|
2030
|
+
),
|
|
2031
|
+
filterable && /* @__PURE__ */ jsxs12(Popover, { children: [
|
|
2032
|
+
/* @__PURE__ */ jsx19(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx19(Button, { variant: "ghost", size: "icon", "aria-label": `Filter ${column.header}`, children: /* @__PURE__ */ jsx19(FunnelIcon7, { className: "h-3 w-3", "aria-hidden": true }) }) }),
|
|
2033
|
+
/* @__PURE__ */ jsx19(PopoverContent, { children: renderFilterWidget(column) })
|
|
2034
|
+
] })
|
|
2035
|
+
] }),
|
|
2036
|
+
enableColumnResizing && /* @__PURE__ */ jsx19(
|
|
2037
|
+
"div",
|
|
2038
|
+
{
|
|
2039
|
+
"data-testid": `resize-handle-${column.id}`,
|
|
2040
|
+
onMouseDown: header.getResizeHandler(),
|
|
2041
|
+
onTouchStart: header.getResizeHandler(),
|
|
2042
|
+
className: "absolute right-0 top-0 h-full w-1 cursor-col-resize touch-none select-none hover:bg-accent"
|
|
2043
|
+
}
|
|
2044
|
+
)
|
|
2045
|
+
]
|
|
2046
|
+
},
|
|
2047
|
+
header.id
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
return /* @__PURE__ */ jsxs12(Fragment2, { children: [
|
|
2051
|
+
hasHeaderGroups && /* @__PURE__ */ jsxs12("tr", { "data-testid": "header-group-row", children: [
|
|
2052
|
+
showDetailColumn && /* @__PURE__ */ jsx19("th", { rowSpan: 2, className: "border-b p-2", "aria-hidden": true }),
|
|
2053
|
+
showSelectionColumn && /* @__PURE__ */ jsx19("th", { rowSpan: 2, className: "border-b p-2", children: /* @__PURE__ */ jsx19(
|
|
2054
|
+
"input",
|
|
2055
|
+
{
|
|
2056
|
+
type: "checkbox",
|
|
2057
|
+
"aria-label": "Select all rows on this page",
|
|
2058
|
+
checked: allOnPageSelected,
|
|
2059
|
+
onChange: toggleAllOnPageSelected
|
|
2060
|
+
}
|
|
2061
|
+
) }),
|
|
2062
|
+
headerRuns.map(
|
|
2063
|
+
(run) => run.kind === "single" ? renderLeafHeaderCell(run.column, 2) : /* @__PURE__ */ jsx19(
|
|
2064
|
+
"th",
|
|
2065
|
+
{
|
|
2066
|
+
colSpan: run.columns.length,
|
|
2067
|
+
className: "border-b p-2 text-center font-medium",
|
|
2068
|
+
children: run.label
|
|
2069
|
+
},
|
|
2070
|
+
`group-${run.label}-${run.columns[0].id}`
|
|
2071
|
+
)
|
|
2072
|
+
),
|
|
2073
|
+
showRowActionsColumn && /* @__PURE__ */ jsx19("th", { rowSpan: 2, className: "border-b p-2", "aria-label": "Row actions" })
|
|
2074
|
+
] }, `${headerGroup.id}-groups`),
|
|
2075
|
+
/* @__PURE__ */ jsxs12("tr", { children: [
|
|
2076
|
+
!hasHeaderGroups && showDetailColumn && /* @__PURE__ */ jsx19("th", { className: "border-b p-2", "aria-hidden": true }),
|
|
2077
|
+
!hasHeaderGroups && showSelectionColumn && /* @__PURE__ */ jsx19("th", { className: "border-b p-2", children: /* @__PURE__ */ jsx19(
|
|
2078
|
+
"input",
|
|
2079
|
+
{
|
|
2080
|
+
type: "checkbox",
|
|
2081
|
+
"aria-label": "Select all rows on this page",
|
|
2082
|
+
checked: allOnPageSelected,
|
|
2083
|
+
onChange: toggleAllOnPageSelected
|
|
2084
|
+
}
|
|
2085
|
+
) }),
|
|
2086
|
+
headerGroup.headers.map((header) => {
|
|
2087
|
+
const column = columnById.get(header.id);
|
|
2088
|
+
if (!column || hasHeaderGroups && !column.headerGroup) return null;
|
|
2089
|
+
return renderLeafHeaderCell(column);
|
|
2090
|
+
}),
|
|
2091
|
+
!hasHeaderGroups && showRowActionsColumn && /* @__PURE__ */ jsx19("th", { className: "border-b p-2", "aria-label": "Row actions" })
|
|
2092
|
+
] }, headerGroup.id)
|
|
2093
|
+
] }, headerGroup.id);
|
|
2094
|
+
}),
|
|
2095
|
+
hasFilterRow && /* @__PURE__ */ jsxs12("tr", { "data-testid": "filter-row", children: [
|
|
2096
|
+
showDetailColumn && /* @__PURE__ */ jsx19("th", { className: "border-b p-2" }),
|
|
2097
|
+
showSelectionColumn && /* @__PURE__ */ jsx19("th", { className: "border-b p-2" }),
|
|
2098
|
+
visibleColumns.map((column) => {
|
|
2099
|
+
const pinnedProps = pinnedCellProps(column, "header");
|
|
2100
|
+
return /* @__PURE__ */ jsx19(
|
|
2101
|
+
"th",
|
|
2102
|
+
{
|
|
2103
|
+
style: pinnedProps.style,
|
|
2104
|
+
className: cn("border-b p-2 align-top font-normal", alignClassName(column), pinnedProps.className),
|
|
2105
|
+
children: isFilterable(column) && column.filterDisplay === "row" ? renderFilterWidget(column) : null
|
|
2106
|
+
},
|
|
2107
|
+
column.id
|
|
2108
|
+
);
|
|
2109
|
+
}),
|
|
2110
|
+
showRowActionsColumn && /* @__PURE__ */ jsx19("th", { className: "border-b p-2" })
|
|
2111
|
+
] })
|
|
2112
|
+
] }),
|
|
2113
|
+
rows.length === 0 ? /* @__PURE__ */ jsx19("tbody", { children: /* @__PURE__ */ jsx19("tr", { children: /* @__PURE__ */ jsx19("td", { colSpan: totalColumnCount, className: "p-4 text-center text-muted-foreground", children: loading ? "Loading..." : "No results." }) }) }) : shouldVirtualize ? /* @__PURE__ */ jsxs12(Fragment3, { children: [
|
|
2114
|
+
paddingTop > 0 && /* @__PURE__ */ jsx19("tbody", { children: /* @__PURE__ */ jsx19("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx19("td", { colSpan: totalColumnCount, style: { padding: 0, border: "none", height: paddingTop } }) }) }),
|
|
2115
|
+
virtualItems.map(
|
|
2116
|
+
(virtualItem) => renderRow(tableRows[virtualItem.index], (el) => virtualizer.measureElement(el))
|
|
2117
|
+
),
|
|
2118
|
+
paddingBottom > 0 && /* @__PURE__ */ jsx19("tbody", { children: /* @__PURE__ */ jsx19("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx19("td", { colSpan: totalColumnCount, style: { padding: 0, border: "none", height: paddingBottom } }) }) })
|
|
2119
|
+
] }) : tableRows.map((row) => renderRow(row))
|
|
2120
|
+
] })
|
|
2121
|
+
}
|
|
2122
|
+
)
|
|
2123
|
+
] }),
|
|
2124
|
+
showPagination && /* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-between text-sm", children: [
|
|
2125
|
+
/* @__PURE__ */ jsxs12("span", { children: [
|
|
2126
|
+
"Page ",
|
|
2127
|
+
state.page + 1,
|
|
2128
|
+
" of ",
|
|
2129
|
+
pageCount,
|
|
2130
|
+
" (",
|
|
2131
|
+
rowCount,
|
|
2132
|
+
" row",
|
|
2133
|
+
rowCount === 1 ? "" : "s",
|
|
2134
|
+
")"
|
|
2135
|
+
] }),
|
|
2136
|
+
/* @__PURE__ */ jsxs12("div", { className: "flex gap-2", children: [
|
|
2137
|
+
/* @__PURE__ */ jsx19(
|
|
2138
|
+
Button,
|
|
2139
|
+
{
|
|
2140
|
+
variant: "outline",
|
|
2141
|
+
size: "sm",
|
|
2142
|
+
disabled: state.page <= 0,
|
|
2143
|
+
onClick: () => setPage(state.page - 1),
|
|
2144
|
+
children: "Previous"
|
|
2145
|
+
}
|
|
2146
|
+
),
|
|
2147
|
+
/* @__PURE__ */ jsx19(
|
|
2148
|
+
Button,
|
|
2149
|
+
{
|
|
2150
|
+
variant: "outline",
|
|
2151
|
+
size: "sm",
|
|
2152
|
+
disabled: state.page >= pageCount - 1,
|
|
2153
|
+
onClick: () => setPage(state.page + 1),
|
|
2154
|
+
children: "Next"
|
|
2155
|
+
}
|
|
2156
|
+
)
|
|
2157
|
+
] })
|
|
2158
|
+
] })
|
|
2159
|
+
] });
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// src/tree/TreeDataGrid.tsx
|
|
2163
|
+
import { useVirtualizer as useVirtualizer2 } from "@tanstack/react-virtual";
|
|
2164
|
+
import { AlertCircle, ChevronDown as ChevronDown2, ChevronRight as ChevronRight2, Loader2 as Loader22 } from "lucide-react";
|
|
2165
|
+
import { useEffect as useEffect8, useMemo as useMemo7, useRef as useRef6 } from "react";
|
|
2166
|
+
|
|
2167
|
+
// src/tree/flattenTree.ts
|
|
2168
|
+
function flattenTree(roots, accessors, expanded, childrenOverride) {
|
|
2169
|
+
const { getRowId, getChildren } = accessors;
|
|
2170
|
+
const resolveHasChildren = accessors.hasChildren ?? ((row) => (getChildren(row)?.length ?? 0) > 0);
|
|
2171
|
+
const out = [];
|
|
2172
|
+
function visit(row, depth) {
|
|
2173
|
+
const id = getRowId(row);
|
|
2174
|
+
const isExpanded = Boolean(expanded[id]);
|
|
2175
|
+
out.push({ row, id, depth, hasChildren: resolveHasChildren(row), isExpanded });
|
|
2176
|
+
if (!isExpanded) return;
|
|
2177
|
+
const children = childrenOverride.get(id) ?? getChildren(row) ?? [];
|
|
2178
|
+
for (const child of children) visit(child, depth + 1);
|
|
2179
|
+
}
|
|
2180
|
+
for (const root of roots) visit(root, 0);
|
|
2181
|
+
return out;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// src/tree/useTreeState.ts
|
|
2185
|
+
import { useCallback as useCallback3, useMemo as useMemo6, useState as useState7 } from "react";
|
|
2186
|
+
function useTreeState({
|
|
2187
|
+
data,
|
|
2188
|
+
getRowId,
|
|
2189
|
+
getChildren,
|
|
2190
|
+
hasChildren,
|
|
2191
|
+
onLoadChildren,
|
|
2192
|
+
expanded: controlledExpanded,
|
|
2193
|
+
onExpandedChange
|
|
2194
|
+
}) {
|
|
2195
|
+
const [internalExpanded, setInternalExpanded] = useState7({});
|
|
2196
|
+
const expanded = controlledExpanded ?? internalExpanded;
|
|
2197
|
+
const setExpanded = useCallback3(
|
|
2198
|
+
(updater) => {
|
|
2199
|
+
if (controlledExpanded) {
|
|
2200
|
+
onExpandedChange?.(updater(controlledExpanded));
|
|
2201
|
+
} else {
|
|
2202
|
+
setInternalExpanded(updater);
|
|
2203
|
+
}
|
|
2204
|
+
},
|
|
2205
|
+
[controlledExpanded, onExpandedChange]
|
|
2206
|
+
);
|
|
2207
|
+
const [childrenMap, setChildrenMap] = useState7(/* @__PURE__ */ new Map());
|
|
2208
|
+
const [loadingIds, setLoadingIds] = useState7(/* @__PURE__ */ new Set());
|
|
2209
|
+
const [errorIds, setErrorIds] = useState7(/* @__PURE__ */ new Map());
|
|
2210
|
+
const resolveHasChildren = useMemo6(
|
|
2211
|
+
() => hasChildren ?? ((row) => (getChildren(row)?.length ?? 0) > 0),
|
|
2212
|
+
[hasChildren, getChildren]
|
|
2213
|
+
);
|
|
2214
|
+
const loadChildrenFor = useCallback3(
|
|
2215
|
+
async (row) => {
|
|
2216
|
+
const id = getRowId(row);
|
|
2217
|
+
const cached = childrenMap.get(id);
|
|
2218
|
+
if (cached) return cached;
|
|
2219
|
+
const eager = getChildren(row);
|
|
2220
|
+
if (eager && eager.length > 0) return eager;
|
|
2221
|
+
if (!onLoadChildren) return eager ?? [];
|
|
2222
|
+
setLoadingIds((prev) => new Set(prev).add(id));
|
|
2223
|
+
setErrorIds((prev) => {
|
|
2224
|
+
if (!prev.has(id)) return prev;
|
|
2225
|
+
const next = new Map(prev);
|
|
2226
|
+
next.delete(id);
|
|
2227
|
+
return next;
|
|
2228
|
+
});
|
|
2229
|
+
try {
|
|
2230
|
+
const result = await onLoadChildren(row);
|
|
2231
|
+
setChildrenMap((prev) => new Map(prev).set(id, result));
|
|
2232
|
+
return result;
|
|
2233
|
+
} catch (error) {
|
|
2234
|
+
const message = error instanceof Error ? error.message : "Failed to load children.";
|
|
2235
|
+
setErrorIds((prev) => new Map(prev).set(id, message));
|
|
2236
|
+
throw error;
|
|
2237
|
+
} finally {
|
|
2238
|
+
setLoadingIds((prev) => {
|
|
2239
|
+
const next = new Set(prev);
|
|
2240
|
+
next.delete(id);
|
|
2241
|
+
return next;
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
},
|
|
2245
|
+
[childrenMap, getChildren, getRowId, onLoadChildren]
|
|
2246
|
+
);
|
|
2247
|
+
const toggleExpand = useCallback3(
|
|
2248
|
+
(row) => {
|
|
2249
|
+
const id = getRowId(row);
|
|
2250
|
+
const willExpand = !expanded[id];
|
|
2251
|
+
setExpanded((prev) => ({ ...prev, [id]: willExpand }));
|
|
2252
|
+
if (willExpand && resolveHasChildren(row)) {
|
|
2253
|
+
loadChildrenFor(row).catch(() => {
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
},
|
|
2257
|
+
[expanded, getRowId, loadChildrenFor, resolveHasChildren, setExpanded]
|
|
2258
|
+
);
|
|
2259
|
+
const retry = useCallback3(
|
|
2260
|
+
(row) => {
|
|
2261
|
+
loadChildrenFor(row).catch(() => {
|
|
2262
|
+
});
|
|
2263
|
+
},
|
|
2264
|
+
[loadChildrenFor]
|
|
2265
|
+
);
|
|
2266
|
+
const expandToLevel = useCallback3(
|
|
2267
|
+
async (level) => {
|
|
2268
|
+
const nextExpanded = { ...expanded };
|
|
2269
|
+
async function visit(row, depth) {
|
|
2270
|
+
if (depth >= level || !resolveHasChildren(row)) return;
|
|
2271
|
+
nextExpanded[getRowId(row)] = true;
|
|
2272
|
+
let children;
|
|
2273
|
+
try {
|
|
2274
|
+
children = await loadChildrenFor(row);
|
|
2275
|
+
} catch {
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
for (const child of children) await visit(child, depth + 1);
|
|
2279
|
+
}
|
|
2280
|
+
for (const root of data) await visit(root, 0);
|
|
2281
|
+
setExpanded(() => nextExpanded);
|
|
2282
|
+
},
|
|
2283
|
+
[data, expanded, getRowId, loadChildrenFor, resolveHasChildren, setExpanded]
|
|
2284
|
+
);
|
|
2285
|
+
return { expanded, childrenMap, loadingIds, errorIds, toggleExpand, retry, expandToLevel };
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// src/tree/TreeDataGrid.tsx
|
|
2289
|
+
import { Fragment as Fragment4, jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2290
|
+
function renderCell(column, row) {
|
|
2291
|
+
const value = getColumnValue(column, row);
|
|
2292
|
+
return column.cell ? column.cell(value, row) : defaultFormat(column, value);
|
|
2293
|
+
}
|
|
2294
|
+
function TreeCell({
|
|
2295
|
+
flatRow,
|
|
2296
|
+
column,
|
|
2297
|
+
indentSize,
|
|
2298
|
+
isLoading,
|
|
2299
|
+
error,
|
|
2300
|
+
onToggle,
|
|
2301
|
+
onRetry
|
|
2302
|
+
}) {
|
|
2303
|
+
return /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-1", style: { paddingLeft: flatRow.depth * indentSize }, children: [
|
|
2304
|
+
flatRow.hasChildren ? /* @__PURE__ */ jsx20(
|
|
2305
|
+
"button",
|
|
2306
|
+
{
|
|
2307
|
+
type: "button",
|
|
2308
|
+
onClick: (event) => {
|
|
2309
|
+
stopRowClick(event);
|
|
2310
|
+
onToggle();
|
|
2311
|
+
},
|
|
2312
|
+
disabled: isLoading,
|
|
2313
|
+
"aria-label": flatRow.isExpanded ? "Collapse" : "Expand",
|
|
2314
|
+
"aria-expanded": flatRow.isExpanded,
|
|
2315
|
+
className: "flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground hover:text-foreground disabled:opacity-50",
|
|
2316
|
+
children: isLoading ? /* @__PURE__ */ jsx20(Loader22, { className: "h-3.5 w-3.5 animate-spin", "aria-hidden": "true" }) : flatRow.isExpanded ? /* @__PURE__ */ jsx20(ChevronDown2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx20(ChevronRight2, { className: "h-3.5 w-3.5", "aria-hidden": "true" })
|
|
2317
|
+
}
|
|
2318
|
+
) : /* @__PURE__ */ jsx20("span", { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }),
|
|
2319
|
+
/* @__PURE__ */ jsx20("span", { className: "truncate", children: renderCell(column, flatRow.row) }),
|
|
2320
|
+
flatRow.isExpanded && error ? /* @__PURE__ */ jsxs13("span", { className: "ml-1 flex shrink-0 items-center gap-1 text-xs text-destructive", children: [
|
|
2321
|
+
/* @__PURE__ */ jsx20(AlertCircle, { className: "h-3 w-3", "aria-hidden": "true" }),
|
|
2322
|
+
"Failed to load.",
|
|
2323
|
+
/* @__PURE__ */ jsx20("button", { type: "button", className: "underline underline-offset-2", onClick: onRetry, children: "Retry" })
|
|
2324
|
+
] }) : null
|
|
2325
|
+
] });
|
|
2326
|
+
}
|
|
2327
|
+
function TreeDataGrid({
|
|
2328
|
+
columns,
|
|
2329
|
+
data,
|
|
2330
|
+
treeColumnId,
|
|
2331
|
+
getRowId,
|
|
2332
|
+
getChildren,
|
|
2333
|
+
hasChildren,
|
|
2334
|
+
onLoadChildren,
|
|
2335
|
+
initialExpandedLevel = 0,
|
|
2336
|
+
expanded: controlledExpanded,
|
|
2337
|
+
onExpandedChange,
|
|
2338
|
+
indentSize = 20,
|
|
2339
|
+
getRowProps,
|
|
2340
|
+
getRowTestId,
|
|
2341
|
+
rowActions,
|
|
2342
|
+
virtualizeThreshold = 100,
|
|
2343
|
+
estimatedRowHeight = 40,
|
|
2344
|
+
maxBodyHeight = 480
|
|
2345
|
+
}) {
|
|
2346
|
+
const accessors = useMemo7(
|
|
2347
|
+
() => ({ getRowId, getChildren, hasChildren }),
|
|
2348
|
+
[getRowId, getChildren, hasChildren]
|
|
2349
|
+
);
|
|
2350
|
+
const { expanded, childrenMap, loadingIds, errorIds, toggleExpand, retry, expandToLevel } = useTreeState({
|
|
2351
|
+
data,
|
|
2352
|
+
getRowId,
|
|
2353
|
+
getChildren,
|
|
2354
|
+
hasChildren,
|
|
2355
|
+
onLoadChildren,
|
|
2356
|
+
expanded: controlledExpanded,
|
|
2357
|
+
onExpandedChange
|
|
2358
|
+
});
|
|
2359
|
+
const didAutoExpand = useRef6(false);
|
|
2360
|
+
useEffect8(() => {
|
|
2361
|
+
if (didAutoExpand.current) return;
|
|
2362
|
+
didAutoExpand.current = true;
|
|
2363
|
+
if (initialExpandedLevel > 0) void expandToLevel(initialExpandedLevel);
|
|
2364
|
+
}, []);
|
|
2365
|
+
const flatRows = useMemo7(
|
|
2366
|
+
() => flattenTree(data, accessors, expanded, childrenMap),
|
|
2367
|
+
[data, accessors, expanded, childrenMap]
|
|
2368
|
+
);
|
|
2369
|
+
const treeColId = treeColumnId ?? columns[0]?.id;
|
|
2370
|
+
const showRowActionsColumn = Boolean(rowActions?.length);
|
|
2371
|
+
const totalColumnCount = columns.length + (showRowActionsColumn ? 1 : 0);
|
|
2372
|
+
const scrollRef = useRef6(null);
|
|
2373
|
+
const shouldVirtualize = flatRows.length > virtualizeThreshold;
|
|
2374
|
+
const virtualizer = useVirtualizer2({
|
|
2375
|
+
count: flatRows.length,
|
|
2376
|
+
getScrollElement: () => scrollRef.current,
|
|
2377
|
+
estimateSize: () => estimatedRowHeight,
|
|
2378
|
+
overscan: 10,
|
|
2379
|
+
enabled: shouldVirtualize
|
|
2380
|
+
});
|
|
2381
|
+
function renderRow(flatRow, index, measureRef) {
|
|
2382
|
+
const rowProps = getRowProps?.(flatRow.row, flatRow.depth, index);
|
|
2383
|
+
const rowTestId = getRowTestId?.(flatRow.row) ?? `tree-row-${flatRow.id}`;
|
|
2384
|
+
return /* @__PURE__ */ jsxs13(
|
|
2385
|
+
"tr",
|
|
2386
|
+
{
|
|
2387
|
+
ref: measureRef,
|
|
2388
|
+
...rowProps,
|
|
2389
|
+
"data-testid": rowTestId,
|
|
2390
|
+
className: cn("border-b", rowProps?.className, index % 2 === 1 && "bg-foreground/5"),
|
|
2391
|
+
children: [
|
|
2392
|
+
columns.map((column) => /* @__PURE__ */ jsx20("td", { className: cn("p-2", alignClassName(column)), children: column.id === treeColId ? /* @__PURE__ */ jsx20(
|
|
2393
|
+
TreeCell,
|
|
2394
|
+
{
|
|
2395
|
+
flatRow,
|
|
2396
|
+
column,
|
|
2397
|
+
indentSize,
|
|
2398
|
+
isLoading: loadingIds.has(flatRow.id),
|
|
2399
|
+
error: errorIds.get(flatRow.id),
|
|
2400
|
+
onToggle: () => toggleExpand(flatRow.row),
|
|
2401
|
+
onRetry: () => retry(flatRow.row)
|
|
2402
|
+
}
|
|
2403
|
+
) : renderCell(column, flatRow.row) }, column.id)),
|
|
2404
|
+
showRowActionsColumn && rowActions && /* @__PURE__ */ jsx20("td", { className: "p-2", children: /* @__PURE__ */ jsx20(
|
|
2405
|
+
ActionsMenu,
|
|
2406
|
+
{
|
|
2407
|
+
items: rowActions,
|
|
2408
|
+
ctx: { row: flatRow.row },
|
|
2409
|
+
triggerLabel: `Row actions for ${flatRow.id}`
|
|
2410
|
+
}
|
|
2411
|
+
) })
|
|
2412
|
+
]
|
|
2413
|
+
},
|
|
2414
|
+
flatRow.id
|
|
2415
|
+
);
|
|
2416
|
+
}
|
|
2417
|
+
const virtualItems = shouldVirtualize ? virtualizer.getVirtualItems() : [];
|
|
2418
|
+
const paddingTop = virtualItems.length > 0 ? virtualItems[0].start : 0;
|
|
2419
|
+
const paddingBottom = virtualItems.length > 0 ? virtualizer.getTotalSize() - virtualItems[virtualItems.length - 1].end : 0;
|
|
2420
|
+
return /* @__PURE__ */ jsx20("div", { className: "flex flex-col gap-2", "data-testid": "tree-datagrid", children: /* @__PURE__ */ jsx20(
|
|
2421
|
+
"div",
|
|
2422
|
+
{
|
|
2423
|
+
ref: scrollRef,
|
|
2424
|
+
className: shouldVirtualize ? "overflow-y-auto" : void 0,
|
|
2425
|
+
style: shouldVirtualize ? { maxHeight: maxBodyHeight } : void 0,
|
|
2426
|
+
children: /* @__PURE__ */ jsxs13("table", { className: "w-full border-collapse text-sm", children: [
|
|
2427
|
+
/* @__PURE__ */ jsx20("thead", { children: /* @__PURE__ */ jsxs13("tr", { children: [
|
|
2428
|
+
columns.map((column) => /* @__PURE__ */ jsx20(
|
|
2429
|
+
"th",
|
|
2430
|
+
{
|
|
2431
|
+
style: column.width ? { width: column.width } : void 0,
|
|
2432
|
+
className: cn("border-b p-2 font-medium", alignClassName(column)),
|
|
2433
|
+
children: column.header
|
|
2434
|
+
},
|
|
2435
|
+
column.id
|
|
2436
|
+
)),
|
|
2437
|
+
showRowActionsColumn && /* @__PURE__ */ jsx20("th", { className: "border-b p-2", "aria-label": "Row actions" })
|
|
2438
|
+
] }) }),
|
|
2439
|
+
/* @__PURE__ */ jsx20("tbody", { children: flatRows.length === 0 ? /* @__PURE__ */ jsx20("tr", { children: /* @__PURE__ */ jsx20("td", { colSpan: totalColumnCount, className: "p-4 text-center text-muted-foreground", children: "No results." }) }) : shouldVirtualize ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
|
|
2440
|
+
paddingTop > 0 && /* @__PURE__ */ jsx20("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx20("td", { colSpan: totalColumnCount, style: { padding: 0, border: "none", height: paddingTop } }) }),
|
|
2441
|
+
virtualItems.map(
|
|
2442
|
+
(virtualItem) => renderRow(flatRows[virtualItem.index], virtualItem.index, (el) => virtualizer.measureElement(el))
|
|
2443
|
+
),
|
|
2444
|
+
paddingBottom > 0 && /* @__PURE__ */ jsx20("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx20(
|
|
2445
|
+
"td",
|
|
2446
|
+
{
|
|
2447
|
+
colSpan: totalColumnCount,
|
|
2448
|
+
style: { padding: 0, border: "none", height: paddingBottom }
|
|
2449
|
+
}
|
|
2450
|
+
) })
|
|
2451
|
+
] }) : flatRows.map((flatRow, index) => renderRow(flatRow, index)) })
|
|
2452
|
+
] })
|
|
2453
|
+
}
|
|
2454
|
+
) });
|
|
2455
|
+
}
|
|
2456
|
+
export {
|
|
2457
|
+
ActionsMenu,
|
|
2458
|
+
BooleanFilter,
|
|
2459
|
+
ColumnSelector,
|
|
2460
|
+
DataGrid,
|
|
2461
|
+
DateRangeFilter,
|
|
2462
|
+
EnumFilter,
|
|
2463
|
+
NumberComparisonFilter,
|
|
2464
|
+
NumberHistogramFilter,
|
|
2465
|
+
NumberRangeFilter,
|
|
2466
|
+
StringFilter,
|
|
2467
|
+
TreeDataGrid,
|
|
2468
|
+
UNARY_OPERATORS,
|
|
2469
|
+
alignClassName,
|
|
2470
|
+
canHideColumn,
|
|
2471
|
+
canHideGroup,
|
|
2472
|
+
countVisible,
|
|
2473
|
+
defaultAlign,
|
|
2474
|
+
defaultFormat,
|
|
2475
|
+
evaluateFilter,
|
|
2476
|
+
facetedNumberValues,
|
|
2477
|
+
fieldKey,
|
|
2478
|
+
flattenTree,
|
|
2479
|
+
getColumnValue,
|
|
2480
|
+
groupColumns,
|
|
2481
|
+
isColumnVisible,
|
|
2482
|
+
isCompositeFilterDescriptor,
|
|
2483
|
+
isFilterDescriptor,
|
|
2484
|
+
isFilterable,
|
|
2485
|
+
isSortable,
|
|
2486
|
+
processClientData,
|
|
2487
|
+
renderDefaultFilterWidget,
|
|
2488
|
+
resolveMenuItems,
|
|
2489
|
+
storageKeyFor,
|
|
2490
|
+
useDebouncedCallback,
|
|
2491
|
+
useGridState,
|
|
2492
|
+
useTreeState
|
|
2493
|
+
};
|
|
2494
|
+
//# sourceMappingURL=index.js.map
|