@bmsuisse/datagrid 0.35.3 → 0.36.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/dist/index.d.ts +25 -2
- package/dist/index.js +77 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -258,6 +258,16 @@ type BooleanColumn<TRow> = BaseColumn<TRow> & {
|
|
|
258
258
|
interface EnumOption {
|
|
259
259
|
value: string;
|
|
260
260
|
label: string;
|
|
261
|
+
/**
|
|
262
|
+
* Groups this option under a sticky header in `EnumFilter`'s dropdown,
|
|
263
|
+
* with a tri-state checkbox that selects/deselects the whole group at
|
|
264
|
+
* once — mirrors `@bmsuisse/ui`'s `ComboboxOption.group`/
|
|
265
|
+
* `TagComboboxOption.group`. Options sharing a `group` must be adjacent in
|
|
266
|
+
* `options`: a header renders the first time a group key is seen, so
|
|
267
|
+
* interleaved groups would render more than one header for the same key.
|
|
268
|
+
* Options with no `group` render individually, with no header.
|
|
269
|
+
*/
|
|
270
|
+
group?: string;
|
|
261
271
|
}
|
|
262
272
|
type EnumColumn<TRow> = BaseColumn<TRow> & {
|
|
263
273
|
type: "enum";
|
|
@@ -318,9 +328,22 @@ declare function DateRangeFilter<TRow>({ column, value, onChange, today, bare, }
|
|
|
318
328
|
* - a tri-state "Select all" checkbox acts on whatever subset is currently
|
|
319
329
|
* visible after that search, not the full option set — matching Excel's
|
|
320
330
|
* own column-filter behavior ("select all of what's shown").
|
|
331
|
+
* - options sharing an `EnumOption.group` render under a sticky group
|
|
332
|
+
* header with its own tri-state checkbox, letting a whole group be
|
|
333
|
+
* selected/deselected in one click — mirrors `@bmsuisse/ui`'s
|
|
334
|
+
* `Combobox`/`TagCombobox` grouped-dropdown treatment (sticky `bg-muted`
|
|
335
|
+
* header, same contiguous-group chunking), kept as a local mirror rather
|
|
336
|
+
* than a shared dependency since `datagrid` and `ui` are sibling packages
|
|
337
|
+
* with no cross-dependency (see that package's `Combobox` doc, which
|
|
338
|
+
* mirrors this component's own plain-substring search for the same
|
|
339
|
+
* reason). The header's tri-state and toggle are scoped to that group's
|
|
340
|
+
* currently-*visible* options, same as "Select all" — unlike `Combobox`,
|
|
341
|
+
* which scopes its group toggle to the full option set regardless of
|
|
342
|
+
* search.
|
|
321
343
|
* - every visible option gets its own plain checkbox + label row (a real
|
|
322
|
-
* shadcn `Checkbox`, not a menu item
|
|
323
|
-
* option
|
|
344
|
+
* shadcn `Checkbox`, not a menu item — unlike `Combobox`'s button/`role=
|
|
345
|
+
* "option"` rows), inside a scrollable list so long option lists don't
|
|
346
|
+
* blow up the popover's height.
|
|
324
347
|
* - every toggle emits the updated `FilterDescriptor` immediately — there's
|
|
325
348
|
* no OK/Cancel step, matching every other filter widget in this package.
|
|
326
349
|
*
|
package/dist/index.js
CHANGED
|
@@ -515,6 +515,27 @@ function selectedValuesOf(value) {
|
|
|
515
515
|
function toggle(selected, optionValue) {
|
|
516
516
|
return selected.includes(optionValue) ? selected.filter((v) => v !== optionValue) : [...selected, optionValue];
|
|
517
517
|
}
|
|
518
|
+
function buildRenderChunks(options) {
|
|
519
|
+
const chunks = [];
|
|
520
|
+
options.forEach((option, index) => {
|
|
521
|
+
const row = { option, index };
|
|
522
|
+
const last = chunks[chunks.length - 1];
|
|
523
|
+
if (option.group && last?.kind === "group" && last.group === option.group) {
|
|
524
|
+
last.rows.push(row);
|
|
525
|
+
} else if (option.group) {
|
|
526
|
+
chunks.push({ kind: "group", group: option.group, rows: [row] });
|
|
527
|
+
} else {
|
|
528
|
+
chunks.push({ kind: "single", row });
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
return chunks;
|
|
532
|
+
}
|
|
533
|
+
function groupCheckState(candidates, selected, group) {
|
|
534
|
+
const members = candidates.filter((o) => o.group === group).map((o) => o.value);
|
|
535
|
+
const selectedCount = members.filter((v) => selected.includes(v)).length;
|
|
536
|
+
if (members.length === 0 || selectedCount === 0) return "unchecked";
|
|
537
|
+
return selectedCount === members.length ? "checked" : "indeterminate";
|
|
538
|
+
}
|
|
518
539
|
function EnumFilter({
|
|
519
540
|
column,
|
|
520
541
|
value,
|
|
@@ -528,6 +549,7 @@ function EnumFilter({
|
|
|
528
549
|
if (!term) return column.options;
|
|
529
550
|
return column.options.filter((option) => option.label.toLowerCase().includes(term));
|
|
530
551
|
}, [column.options, search]);
|
|
552
|
+
const renderChunks = useMemo(() => buildRenderChunks(visibleOptions), [visibleOptions]);
|
|
531
553
|
function emit(next) {
|
|
532
554
|
onChange(next.length === 0 ? void 0 : { field: column.id, operator: "in", value: next });
|
|
533
555
|
}
|
|
@@ -544,6 +566,33 @@ function EnumFilter({
|
|
|
544
566
|
for (const v of visibleValues) next.add(v);
|
|
545
567
|
emit([...next]);
|
|
546
568
|
}
|
|
569
|
+
function toggleGroup(group) {
|
|
570
|
+
const members = visibleOptions.filter((o) => o.group === group).map((o) => o.value);
|
|
571
|
+
if (groupCheckState(visibleOptions, selected, group) === "checked") {
|
|
572
|
+
emit(selected.filter((v) => !members.includes(v)));
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
emit([.../* @__PURE__ */ new Set([...selected, ...members])]);
|
|
576
|
+
}
|
|
577
|
+
function renderOption(option) {
|
|
578
|
+
return /* @__PURE__ */ jsxs4(
|
|
579
|
+
"label",
|
|
580
|
+
{
|
|
581
|
+
className: "flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground",
|
|
582
|
+
children: [
|
|
583
|
+
/* @__PURE__ */ jsx9(
|
|
584
|
+
Checkbox,
|
|
585
|
+
{
|
|
586
|
+
checked: selected.includes(option.value),
|
|
587
|
+
onCheckedChange: () => emit(toggle(selected, option.value))
|
|
588
|
+
}
|
|
589
|
+
),
|
|
590
|
+
option.label
|
|
591
|
+
]
|
|
592
|
+
},
|
|
593
|
+
option.value
|
|
594
|
+
);
|
|
595
|
+
}
|
|
547
596
|
const isFiltered = selected.length > 0;
|
|
548
597
|
const summary = `${selected.length} selected`;
|
|
549
598
|
const panel = /* @__PURE__ */ jsxs4("div", { className: bare ? "flex w-full flex-col gap-2" : "flex w-64 flex-col gap-2 p-2", children: [
|
|
@@ -567,23 +616,37 @@ function EnumFilter({
|
|
|
567
616
|
),
|
|
568
617
|
"Select all"
|
|
569
618
|
] }),
|
|
570
|
-
/* @__PURE__ */ jsx9("div", { className: "flex max-h-60 flex-col gap-
|
|
571
|
-
"
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
619
|
+
/* @__PURE__ */ jsx9("div", { className: "flex max-h-60 flex-col gap-0.5 overflow-y-auto", children: renderChunks.length === 0 ? /* @__PURE__ */ jsx9("p", { className: "py-2 text-center text-sm text-muted-foreground", children: "No matches." }) : renderChunks.map((chunk) => {
|
|
620
|
+
if (chunk.kind === "single") return renderOption(chunk.row.option);
|
|
621
|
+
const headerState = groupCheckState(visibleOptions, selected, chunk.group);
|
|
622
|
+
return (
|
|
623
|
+
// The header and every one of its group's rows share this wrapper --
|
|
624
|
+
// its bounds are the sticky header's containing block, so `sticky
|
|
625
|
+
// top-0` keeps the header pinned for the group's whole scroll extent
|
|
626
|
+
// instead of just past its first row.
|
|
627
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-0.5", children: [
|
|
628
|
+
/* @__PURE__ */ jsxs4(
|
|
629
|
+
"label",
|
|
577
630
|
{
|
|
578
|
-
|
|
579
|
-
|
|
631
|
+
"data-group-header": true,
|
|
632
|
+
className: "sticky top-0 z-10 flex items-center gap-2 rounded-sm bg-muted px-2 py-1.5 text-sm font-semibold text-foreground",
|
|
633
|
+
children: [
|
|
634
|
+
/* @__PURE__ */ jsx9(
|
|
635
|
+
Checkbox,
|
|
636
|
+
{
|
|
637
|
+
checked: headerState === "indeterminate" ? "indeterminate" : headerState === "checked",
|
|
638
|
+
onCheckedChange: () => toggleGroup(chunk.group),
|
|
639
|
+
"aria-label": `Select all of ${chunk.group}`
|
|
640
|
+
}
|
|
641
|
+
),
|
|
642
|
+
chunk.group
|
|
643
|
+
]
|
|
580
644
|
}
|
|
581
645
|
),
|
|
582
|
-
option
|
|
583
|
-
]
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
)) })
|
|
646
|
+
chunk.rows.map((row) => renderOption(row.option))
|
|
647
|
+
] }, `group-${chunk.group}`)
|
|
648
|
+
);
|
|
649
|
+
}) })
|
|
587
650
|
] });
|
|
588
651
|
if (bare) {
|
|
589
652
|
return /* @__PURE__ */ jsx9("div", { "data-testid": `filter-${column.id}`, children: panel });
|