@parallelworks/ui 0.88.2 → 0.90.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/ai/components/AllocationSelector.js +3 -2
- package/dist/ai/strings.d.ts +1 -0
- package/dist/ai/strings.js +1 -0
- package/dist/components/Provider.d.ts +17 -1
- package/dist/components/Provider.js +20 -1
- package/dist/components/RunStatusIcon.d.ts +4 -4
- package/dist/components/RunStatusIcon.js +4 -4
- package/dist/components/StatusBadge.d.ts +12 -8
- package/dist/components/StatusBadge.js +13 -19
- package/dist/components/StatusLabel.d.ts +44 -0
- package/dist/components/StatusLabel.js +92 -0
- package/dist/components/Tag.d.ts +7 -2
- package/dist/components/Tag.js +7 -2
- package/dist/components/ToggleGroup.d.ts +9 -1
- package/dist/components/ToggleGroup.js +9 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/list/ListFilterBar.d.ts +6 -2
- package/dist/list/ListFilterBar.js +252 -39
- package/dist/list/filterModel.d.ts +31 -2
- package/dist/list/filterModel.js +51 -24
- package/dist/list/index.d.ts +1 -1
- package/dist/list/listView.d.ts +5 -2
- package/dist/list/listView.js +3 -2
- package/dist/list/useListFilters.d.ts +6 -2
- package/dist/list/useListFilters.js +3 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ import { Input } from "../components/Input.js";
|
|
|
9
9
|
import { Combobox } from "../components/Combobox.js";
|
|
10
10
|
import { Calendar } from "../components/DatePicker.js";
|
|
11
11
|
import { Drawer } from "../components/Drawer.js";
|
|
12
|
+
import { ToggleGroup } from "../components/ToggleGroup.js";
|
|
12
13
|
import { DATE_PRESETS, fieldOperators, isDatePreset, isMultiple, operatorCount, parseDay, toDay } from "./filterModel.js";
|
|
13
14
|
import { Menu } from "@base-ui/react/menu";
|
|
14
15
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
@@ -458,7 +459,7 @@ function BooleanValuePicker({ filters, filter, open, onOpenChange, trigger }) {
|
|
|
458
459
|
}) })]
|
|
459
460
|
});
|
|
460
461
|
}
|
|
461
|
-
function DateValuePicker({ filters, filter, open, onOpenChange, trigger }) {
|
|
462
|
+
function DateValuePicker({ filters, filter, field, open, onOpenChange, trigger }) {
|
|
462
463
|
const t = useStrings().filters;
|
|
463
464
|
const [custom, setCustom] = useState(false);
|
|
464
465
|
const [first, second] = filter.values;
|
|
@@ -484,7 +485,7 @@ function DateValuePicker({ filters, filter, open, onOpenChange, trigger }) {
|
|
|
484
485
|
className: "p-0",
|
|
485
486
|
children: [filter.op === "is" && /* @__PURE__ */ jsxs("div", {
|
|
486
487
|
className: cx("flex flex-col p-1", showCalendar && "border-b border-(--theme-border)"),
|
|
487
|
-
children: [DATE_PRESETS.map((preset) => /* @__PURE__ */ jsxs("button", {
|
|
488
|
+
children: [(field.presets ?? DATE_PRESETS).map((preset) => /* @__PURE__ */ jsxs("button", {
|
|
488
489
|
type: "button",
|
|
489
490
|
"aria-pressed": first === preset,
|
|
490
491
|
onClick: () => choosePreset(preset),
|
|
@@ -636,27 +637,199 @@ function AddFilterMenu({ filters, trigger, align = "start" }) {
|
|
|
636
637
|
children: menu
|
|
637
638
|
}) : menu;
|
|
638
639
|
}
|
|
640
|
+
var NO_TOGGLE = "\0none";
|
|
641
|
+
/** A quick filter's toggles and what they show pressed, from the field's filter. */
|
|
642
|
+
function quickToggles(filters, quick, field) {
|
|
643
|
+
const filter = filters.active.find((f) => f.field === quick.field);
|
|
644
|
+
if (field.kind === "boolean") {
|
|
645
|
+
const on = filter?.op === "is" && filter.values[0] !== "false";
|
|
646
|
+
return {
|
|
647
|
+
options: [{
|
|
648
|
+
value: "true",
|
|
649
|
+
label: field.label,
|
|
650
|
+
count: quick.counts?.["true"],
|
|
651
|
+
tone: quick.tone
|
|
652
|
+
}],
|
|
653
|
+
value: on ? ["true"] : filter ? [NO_TOGGLE] : [],
|
|
654
|
+
multiple: true
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
const multiple = quick.multiple ?? isMultiple(field);
|
|
658
|
+
const values = quick.values ?? (field.options ?? []).map((o) => o.value);
|
|
659
|
+
const options = values.map((value) => {
|
|
660
|
+
const option = field.options?.find((o) => o.value === value);
|
|
661
|
+
return {
|
|
662
|
+
value,
|
|
663
|
+
label: option?.label ?? field.valueLabel?.(value) ?? value,
|
|
664
|
+
icon: option?.icon,
|
|
665
|
+
count: quick.counts?.[value] ?? option?.count,
|
|
666
|
+
tone: quick.tone
|
|
667
|
+
};
|
|
668
|
+
});
|
|
669
|
+
const shows = filter?.op === "is" && filter.values.every((v) => values.includes(v)) && (multiple || filter.values.length === 1);
|
|
670
|
+
return {
|
|
671
|
+
options,
|
|
672
|
+
value: !filter ? [] : shows ? [...filter.values] : [NO_TOGGLE],
|
|
673
|
+
multiple
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
/** Whether a filter is said by its quick filter's toggles alone, so it needs no chip. */
|
|
677
|
+
function saidByToggles(filters, filter) {
|
|
678
|
+
const quick = filters.quick.find((q) => q.field === filter.field);
|
|
679
|
+
const field = filters.fields.find((f) => f.key === filter.field);
|
|
680
|
+
if (!quick || !field || filters.draft === filter.field) return false;
|
|
681
|
+
const { value } = quickToggles(filters, quick, field);
|
|
682
|
+
return (value.length > 0 || Boolean(quick.all)) && !value.includes(NO_TOGGLE);
|
|
683
|
+
}
|
|
684
|
+
/** Every quick filter's toggles on one row, a rule between fields. On a narrow row they keep to
|
|
685
|
+
* one line, the pressed ones always among them, and "+n more" opens the rest in the sheet. */
|
|
686
|
+
function QuickRow({ filters, groups, narrow, onMore, end, stacked = false }) {
|
|
687
|
+
const t = useStrings().filters;
|
|
688
|
+
const row = useRef(null);
|
|
689
|
+
const [limit, setLimit] = useState(null);
|
|
690
|
+
const [width, setWidth] = useState(0);
|
|
691
|
+
const fits = narrow && Boolean(onMore);
|
|
692
|
+
const built = groups.map(({ quick, field }) => {
|
|
693
|
+
const toggles = quickToggles(filters, quick, field);
|
|
694
|
+
return {
|
|
695
|
+
quick,
|
|
696
|
+
field,
|
|
697
|
+
toggles,
|
|
698
|
+
items: [...quick.all ? [{
|
|
699
|
+
value: ALL_ITEM,
|
|
700
|
+
pressed: toggles.value.length === 0
|
|
701
|
+
}] : [], ...toggles.options.map((o) => ({
|
|
702
|
+
value: o.value,
|
|
703
|
+
pressed: toggles.value.includes(o.value)
|
|
704
|
+
}))]
|
|
705
|
+
};
|
|
706
|
+
});
|
|
707
|
+
const total = built.reduce((n, g) => n + g.items.length, 0);
|
|
708
|
+
const keys = built.map((g) => g.items.map((i) => `${i.value}${i.pressed ? "+" : ""}`).join(",")).join("|");
|
|
709
|
+
useLayoutEffect(() => {
|
|
710
|
+
const el = row.current;
|
|
711
|
+
if (!el || !fits) return;
|
|
712
|
+
let last = el.getBoundingClientRect().width;
|
|
713
|
+
setWidth(last);
|
|
714
|
+
const observer = new ResizeObserver(() => {
|
|
715
|
+
const next = el.getBoundingClientRect().width;
|
|
716
|
+
if (next !== last) {
|
|
717
|
+
last = next;
|
|
718
|
+
setWidth(next);
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
observer.observe(el);
|
|
722
|
+
return () => observer.disconnect();
|
|
723
|
+
}, [fits]);
|
|
724
|
+
useLayoutEffect(() => setLimit(null), [
|
|
725
|
+
fits,
|
|
726
|
+
keys,
|
|
727
|
+
width
|
|
728
|
+
]);
|
|
729
|
+
useLayoutEffect(() => {
|
|
730
|
+
const el = row.current;
|
|
731
|
+
if (!el || !fits) return;
|
|
732
|
+
const tops = /* @__PURE__ */ new Set();
|
|
733
|
+
for (const button of el.querySelectorAll("button")) tops.add(Math.round(button.getBoundingClientRect().top));
|
|
734
|
+
const shown = limit ?? total;
|
|
735
|
+
if (tops.size > 1 && shown > 0) setLimit(shown - 1);
|
|
736
|
+
});
|
|
737
|
+
const cap = fits && limit !== null ? limit : total;
|
|
738
|
+
let index = 0;
|
|
739
|
+
let hidden = 0;
|
|
740
|
+
const parts = [];
|
|
741
|
+
for (const { quick, field, toggles, items } of built) {
|
|
742
|
+
const keep = /* @__PURE__ */ new Set();
|
|
743
|
+
for (const item of items) {
|
|
744
|
+
if (index < cap || item.pressed) keep.add(item.value);
|
|
745
|
+
else hidden += 1;
|
|
746
|
+
index += 1;
|
|
747
|
+
}
|
|
748
|
+
if (keep.size === 0) continue;
|
|
749
|
+
const all = quick.all;
|
|
750
|
+
const set = (next) => {
|
|
751
|
+
const values = next.filter((v) => v !== NO_TOGGLE);
|
|
752
|
+
if (values.length > 0) filters.update(quick.field, {
|
|
753
|
+
op: "is",
|
|
754
|
+
values
|
|
755
|
+
});
|
|
756
|
+
else if (field.required || field.pinned) filters.update(quick.field, { values: [] });
|
|
757
|
+
else filters.remove(quick.field);
|
|
758
|
+
};
|
|
759
|
+
if (parts.length > 0 && !stacked) parts.push(/* @__PURE__ */ jsx(QuickRule, {}, `rule-${quick.field}`));
|
|
760
|
+
parts.push(/* @__PURE__ */ jsx(ToggleGroup, {
|
|
761
|
+
label: field.label,
|
|
762
|
+
options: toggles.options.filter((o) => keep.has(o.value)),
|
|
763
|
+
value: toggles.value,
|
|
764
|
+
multiple: toggles.multiple,
|
|
765
|
+
all: keep.has(ALL_ITEM) && (typeof all === "object" ? { count: all.count } : all),
|
|
766
|
+
allLabel: typeof all === "object" ? all.label : void 0,
|
|
767
|
+
onValueChange: set,
|
|
768
|
+
dimIcons: false,
|
|
769
|
+
className: "min-w-0"
|
|
770
|
+
}, quick.field));
|
|
771
|
+
}
|
|
772
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
773
|
+
ref: row,
|
|
774
|
+
className: cx("flex min-w-0", stacked ? "flex-col items-start gap-2" : "flex-wrap items-center gap-1"),
|
|
775
|
+
children: [
|
|
776
|
+
parts,
|
|
777
|
+
hidden > 0 && /* @__PURE__ */ jsx("button", {
|
|
778
|
+
type: "button",
|
|
779
|
+
onClick: onMore,
|
|
780
|
+
className: buttonClasses({
|
|
781
|
+
variant: "ghost",
|
|
782
|
+
size: "sm"
|
|
783
|
+
}),
|
|
784
|
+
children: t.quickMore(hidden)
|
|
785
|
+
}),
|
|
786
|
+
end
|
|
787
|
+
]
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
var ALL_ITEM = "\0all";
|
|
791
|
+
function QuickRule() {
|
|
792
|
+
return /* @__PURE__ */ jsx("span", {
|
|
793
|
+
"aria-hidden": "true",
|
|
794
|
+
className: "mx-1 h-5 w-px shrink-0 bg-(--theme-border)"
|
|
795
|
+
});
|
|
796
|
+
}
|
|
639
797
|
/** The chips that say what narrows a list, under its toolbar: one per filter, each editable in
|
|
640
798
|
* place, then "+ Filter" and Clear (or Reset, where the page has default filters). It shows
|
|
641
799
|
* only while a filter is on or a field is required. At phone width it stops at two lines and
|
|
642
|
-
* a "+n filters" chip opens the rest in a sheet.
|
|
800
|
+
* a "+n filters" chip opens the rest in a sheet.
|
|
801
|
+
*
|
|
802
|
+
* The list's quick filters show above the chips as toggles, always, setting the same filters
|
|
803
|
+
* the chips edit. At phone width each keeps to one line, and "+n more" opens the rest in the
|
|
804
|
+
* same sheet. */
|
|
643
805
|
function ListFilterBar({ filters, trailing, label, className }) {
|
|
644
806
|
const strings = useStrings();
|
|
645
807
|
const t = strings.filters;
|
|
646
808
|
const registerBar = filters?.registerBar;
|
|
647
809
|
useEffect(() => registerBar?.(), [registerBar]);
|
|
810
|
+
const box = useRef(null);
|
|
648
811
|
const row = useRef(null);
|
|
649
812
|
const [narrow, setNarrow] = useState(false);
|
|
650
813
|
const [width, setWidth] = useState(0);
|
|
651
814
|
const [limit, setLimit] = useState(null);
|
|
815
|
+
const [clearFolded, setClearFolded] = useState(false);
|
|
652
816
|
const [sheet, setSheet] = useState(false);
|
|
653
817
|
const [focusAt, setFocusAt] = useState(null);
|
|
654
818
|
const chips = filters ? orderedChips(filters) : [];
|
|
655
819
|
const fieldOf = (key) => filters?.fields.find((f) => f.key === key);
|
|
820
|
+
const quick = (filters?.quick ?? []).flatMap((q) => {
|
|
821
|
+
const field = fieldOf(q.field);
|
|
822
|
+
return field ? [{
|
|
823
|
+
quick: q,
|
|
824
|
+
field
|
|
825
|
+
}] : [];
|
|
826
|
+
});
|
|
656
827
|
const empty = chips.length === 0;
|
|
828
|
+
const hasQuick = quick.length > 0;
|
|
657
829
|
const chipKeys = chips.map((c) => c.filter.field).join("\0");
|
|
830
|
+
const fixedChips = chips.filter((c) => c.field.required || c.field.pinned).length;
|
|
658
831
|
useLayoutEffect(() => {
|
|
659
|
-
const el =
|
|
832
|
+
const el = box.current;
|
|
660
833
|
if (!el) return;
|
|
661
834
|
const measure = () => {
|
|
662
835
|
const w = el.getBoundingClientRect().width;
|
|
@@ -667,8 +840,11 @@ function ListFilterBar({ filters, trailing, label, className }) {
|
|
|
667
840
|
const observer = new ResizeObserver(measure);
|
|
668
841
|
observer.observe(el);
|
|
669
842
|
return () => observer.disconnect();
|
|
670
|
-
}, [empty]);
|
|
671
|
-
useLayoutEffect(() =>
|
|
843
|
+
}, [empty, hasQuick]);
|
|
844
|
+
useLayoutEffect(() => {
|
|
845
|
+
setLimit(null);
|
|
846
|
+
setClearFolded(false);
|
|
847
|
+
}, [
|
|
672
848
|
width,
|
|
673
849
|
chipKeys,
|
|
674
850
|
narrow
|
|
@@ -677,9 +853,15 @@ function ListFilterBar({ filters, trailing, label, className }) {
|
|
|
677
853
|
const el = row.current;
|
|
678
854
|
if (!el || !narrow) return;
|
|
679
855
|
const tops = /* @__PURE__ */ new Set();
|
|
680
|
-
for (const child of el.children) tops.add(Math.round(child.offsetTop));
|
|
856
|
+
for (const child of el.children) if (!child.hasAttribute("data-filter-clear")) tops.add(Math.round(child.offsetTop));
|
|
681
857
|
const shown = limit ?? chips.length;
|
|
682
|
-
if (tops.size > 2 && shown >
|
|
858
|
+
if (tops.size > 2 && shown > Math.max(fixedChips, 1)) {
|
|
859
|
+
setLimit(shown - 1);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const add = el.querySelector("[data-filter-add]");
|
|
863
|
+
const clear = el.querySelector("[data-filter-clear]");
|
|
864
|
+
if (add && clear && Math.round(clear.offsetTop) > Math.round(add.offsetTop)) setClearFolded(true);
|
|
683
865
|
});
|
|
684
866
|
useEffect(() => {
|
|
685
867
|
if (focusAt === null || !row.current) return;
|
|
@@ -687,18 +869,19 @@ function ListFilterBar({ filters, trailing, label, className }) {
|
|
|
687
869
|
setFocusAt(null);
|
|
688
870
|
}, [focusAt]);
|
|
689
871
|
const visible = narrow && limit !== null ? limit : chips.length;
|
|
690
|
-
const shownChips = chips.filter((c, i) => i < visible || c.filter.field === filters?.draft);
|
|
872
|
+
const shownChips = chips.filter((c, i) => i < visible || c.field.required || c.field.pinned || c.filter.field === filters?.draft);
|
|
691
873
|
const overflow = chips.length - shownChips.length;
|
|
692
874
|
const request = filters?.editRequest;
|
|
693
875
|
const hiddenRequest = request && !shownChips.some((c) => c.filter.field === request.field) && chips.some((c) => c.filter.field === request.field);
|
|
694
876
|
useEffect(() => {
|
|
695
877
|
if (hiddenRequest) setSheet(true);
|
|
696
878
|
}, [hiddenRequest]);
|
|
697
|
-
if (!filters || empty) return null;
|
|
879
|
+
if (!filters || empty && !hasQuick) return null;
|
|
698
880
|
const removable = filters.active.some((f) => !fieldOf(f.field)?.required);
|
|
699
881
|
const resetting = filters.hasDefaults;
|
|
700
882
|
const clearButton = (resetting ? !filters.isDefault : removable) && /* @__PURE__ */ jsx("button", {
|
|
701
883
|
type: "button",
|
|
884
|
+
"data-filter-clear": "",
|
|
702
885
|
onClick: resetting ? filters.reset : filters.clear,
|
|
703
886
|
className: buttonClasses({
|
|
704
887
|
variant: "ghost",
|
|
@@ -706,23 +889,53 @@ function ListFilterBar({ filters, trailing, label, className }) {
|
|
|
706
889
|
}),
|
|
707
890
|
children: resetting ? strings.common.reset : strings.common.clear
|
|
708
891
|
});
|
|
709
|
-
const addButton = /* @__PURE__ */ jsx(AddFilterMenu, {
|
|
892
|
+
const addButton = (narrow) => /* @__PURE__ */ jsx(AddFilterMenu, {
|
|
710
893
|
filters,
|
|
711
894
|
trigger: /* @__PURE__ */ jsxs("button", {
|
|
712
895
|
type: "button",
|
|
713
896
|
"data-filter-add": "",
|
|
897
|
+
"aria-label": narrow ? t.addFilter : void 0,
|
|
714
898
|
className: buttonClasses({
|
|
715
899
|
variant: "ghost",
|
|
716
|
-
size: "sm"
|
|
900
|
+
size: "sm",
|
|
901
|
+
iconOnly: narrow
|
|
717
902
|
}),
|
|
718
|
-
children: [/* @__PURE__ */ jsx(AddIcon, { "aria-hidden": "true" }), t.addFilter]
|
|
903
|
+
children: [/* @__PURE__ */ jsx(AddIcon, { "aria-hidden": "true" }), !narrow && t.addFilter]
|
|
719
904
|
})
|
|
720
905
|
});
|
|
721
|
-
|
|
722
|
-
|
|
906
|
+
const drawer = /* @__PURE__ */ jsx(Drawer, {
|
|
907
|
+
open: sheet,
|
|
908
|
+
onClose: () => setSheet(false),
|
|
909
|
+
title: t.sheetTitle,
|
|
910
|
+
width: 400,
|
|
911
|
+
footer: /* @__PURE__ */ jsxs("div", {
|
|
912
|
+
className: "flex w-full items-center gap-1.5",
|
|
913
|
+
children: [addButton(false), clearButton]
|
|
914
|
+
}),
|
|
915
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
916
|
+
className: "flex flex-col gap-2",
|
|
917
|
+
children: [hasQuick && /* @__PURE__ */ jsx(QuickRow, {
|
|
918
|
+
filters,
|
|
919
|
+
groups: quick,
|
|
920
|
+
narrow: false,
|
|
921
|
+
stacked: true
|
|
922
|
+
}), chips.map(({ filter, field }) => /* @__PURE__ */ jsx(FilterChip, {
|
|
923
|
+
filters,
|
|
924
|
+
filter,
|
|
925
|
+
field,
|
|
926
|
+
stretch: true,
|
|
927
|
+
answers: sheet
|
|
928
|
+
}, filter.field))]
|
|
929
|
+
})
|
|
930
|
+
});
|
|
931
|
+
const chipRow = !empty && /* @__PURE__ */ jsxs("div", {
|
|
932
|
+
ref: hasQuick ? row : (node) => {
|
|
933
|
+
row.current = node;
|
|
934
|
+
box.current = node;
|
|
935
|
+
},
|
|
723
936
|
role: "group",
|
|
724
937
|
"aria-label": label ?? t.activeFilters,
|
|
725
|
-
className: cx("flex flex-wrap items-center gap-1.5 py-1.5", className),
|
|
938
|
+
className: cx("flex flex-wrap items-center gap-1.5", hasQuick ? void 0 : cx("py-1.5", className)),
|
|
726
939
|
children: [
|
|
727
940
|
shownChips.map(({ filter, field }, i) => /* @__PURE__ */ jsx(FilterChip, {
|
|
728
941
|
filters,
|
|
@@ -737,32 +950,32 @@ function ListFilterBar({ filters, trailing, label, className }) {
|
|
|
737
950
|
className: "flex h-7 cursor-pointer items-center rounded-md border border-dashed border-(--theme-border) px-2 text-[12px] text-(--theme-app) hover:bg-(--theme-hover) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--theme-focus)",
|
|
738
951
|
children: t.overflow(overflow)
|
|
739
952
|
}),
|
|
740
|
-
addButton,
|
|
741
|
-
clearButton,
|
|
953
|
+
addButton(narrow),
|
|
954
|
+
!(narrow && clearFolded) && clearButton,
|
|
742
955
|
trailing && /* @__PURE__ */ jsx("div", {
|
|
743
956
|
className: "ml-auto flex items-center gap-1.5",
|
|
744
957
|
children: trailing
|
|
745
958
|
}),
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
959
|
+
!hasQuick && drawer
|
|
960
|
+
]
|
|
961
|
+
});
|
|
962
|
+
if (!hasQuick) return chipRow;
|
|
963
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
964
|
+
ref: box,
|
|
965
|
+
className: cx("flex min-w-0 flex-col gap-1.5 py-1.5", className),
|
|
966
|
+
children: [
|
|
967
|
+
/* @__PURE__ */ jsx(QuickRow, {
|
|
968
|
+
filters,
|
|
969
|
+
groups: quick,
|
|
970
|
+
narrow,
|
|
971
|
+
onMore: () => setSheet(true),
|
|
972
|
+
end: empty && /* @__PURE__ */ jsxs(Fragment$1, { children: [clearButton, trailing && /* @__PURE__ */ jsx("div", {
|
|
973
|
+
className: "ml-auto flex items-center gap-1.5",
|
|
974
|
+
children: trailing
|
|
975
|
+
})] })
|
|
976
|
+
}),
|
|
977
|
+
chipRow,
|
|
978
|
+
drawer
|
|
766
979
|
]
|
|
767
980
|
});
|
|
768
981
|
}
|
|
@@ -784,7 +997,7 @@ function orderedChips(filters) {
|
|
|
784
997
|
field
|
|
785
998
|
});
|
|
786
999
|
}
|
|
787
|
-
return out;
|
|
1000
|
+
return out.filter(({ filter }) => !saidByToggles(filters, filter));
|
|
788
1001
|
}
|
|
789
1002
|
//#endregion
|
|
790
1003
|
export { AddFilterMenu, FilterChip, ListFilterBar };
|
|
@@ -51,6 +51,10 @@ export interface FilterField<T = never> {
|
|
|
51
51
|
valueLabel?: ((value: string) => string | undefined) | undefined;
|
|
52
52
|
/** enum and people: whether more than one value can be chosen (default true). */
|
|
53
53
|
multiple?: boolean | undefined;
|
|
54
|
+
/** date: the presets its picker offers, in order, such as `['-24h', '-7d', '-30d', 'year',
|
|
55
|
+
* 'all']`. Defaults to {@link DATE_PRESETS}. Any `-Nh` or `-Nd` works, as do `today`,
|
|
56
|
+
* `month`, `year` and `all`. */
|
|
57
|
+
presets?: readonly string[] | undefined;
|
|
54
58
|
/** Whether the picker has a search field. Default: more than eight options, or `loadOptions`. */
|
|
55
59
|
searchable?: boolean | undefined;
|
|
56
60
|
/** Always shows its chip, with no remove button, reading "Any" while unset; Clear leaves it. */
|
|
@@ -65,6 +69,28 @@ export interface FilterField<T = never> {
|
|
|
65
69
|
* on the server. */
|
|
66
70
|
matches?: ((row: T, filter: ActiveFilter) => boolean) | undefined;
|
|
67
71
|
}
|
|
72
|
+
/** A field's values as a row of toggles above the chips, for the few a reader picks most, such as
|
|
73
|
+
* a job's state. The toggles set the same filter its chip edits, so either changes both. */
|
|
74
|
+
export interface QuickFilter {
|
|
75
|
+
/** The field the toggles set. */
|
|
76
|
+
field: string;
|
|
77
|
+
/** enum and people: the options shown as toggles, in order; every option by default. */
|
|
78
|
+
values?: readonly string[] | undefined;
|
|
79
|
+
/** How many rows hold each value, shown after its label; a yes-or-no field's is under `true`.
|
|
80
|
+
* An option's own `count` is used where this has none. */
|
|
81
|
+
counts?: Readonly<Record<string, number>> | undefined;
|
|
82
|
+
/** Leads with an "All" toggle, pressed while the field is unset, that clears it. `{ count }`
|
|
83
|
+
* shows the total beside it and `{ label }` names what it covers, such as "All states". */
|
|
84
|
+
all?: boolean | {
|
|
85
|
+
count?: number | undefined;
|
|
86
|
+
label?: string | undefined;
|
|
87
|
+
} | undefined;
|
|
88
|
+
/** Whether several can be pressed at once; the field's own `multiple` by default. */
|
|
89
|
+
multiple?: boolean | undefined;
|
|
90
|
+
/** `warning`: a toggle asking to be looked at, such as "Needs attention", in the warning tone
|
|
91
|
+
* while it is off. */
|
|
92
|
+
tone?: 'warning' | undefined;
|
|
93
|
+
}
|
|
68
94
|
/** One filter in effect: plain data, so it can go in a URL, storage or a saved view. */
|
|
69
95
|
export interface ActiveFilter {
|
|
70
96
|
field: string;
|
|
@@ -73,9 +99,12 @@ export interface ActiveFilter {
|
|
|
73
99
|
* relative wherever they are saved. A boolean is `true` or `false`. */
|
|
74
100
|
values: readonly string[];
|
|
75
101
|
}
|
|
76
|
-
/** The relative date presets the date picker offers, as a filter
|
|
102
|
+
/** The relative date presets the date picker offers unless a field names its own, as a filter
|
|
103
|
+
* stores them. */
|
|
77
104
|
export declare const DATE_PRESETS: readonly ['today', '-7d', '-30d', '-90d', 'month'];
|
|
78
|
-
|
|
105
|
+
/** A relative date a filter can hold: today, this month or year so far, every date (`all`), or
|
|
106
|
+
* the last N hours or days (`-24h`, `-365d`). */
|
|
107
|
+
export type DatePreset = (typeof DATE_PRESETS)[number] | 'year' | 'all' | `-${number}h` | `-${number}d`;
|
|
79
108
|
type AnyField = FilterField<any>;
|
|
80
109
|
export declare function fieldOperators(field: AnyField): readonly FilterOperator[];
|
|
81
110
|
export declare function isMultiple(field: AnyField): boolean;
|
package/dist/list/filterModel.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
//#region src/list/filterModel.ts
|
|
2
|
-
/** The relative date presets the date picker offers, as a filter
|
|
2
|
+
/** The relative date presets the date picker offers unless a field names its own, as a filter
|
|
3
|
+
* stores them. */
|
|
3
4
|
var DATE_PRESETS = [
|
|
4
5
|
"today",
|
|
5
6
|
"-7d",
|
|
@@ -212,47 +213,72 @@ function parseDay(value) {
|
|
|
212
213
|
}
|
|
213
214
|
var startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
214
215
|
var addDays = (d, n) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
|
|
216
|
+
var NAMED_PRESETS = /* @__PURE__ */ new Set([
|
|
217
|
+
"today",
|
|
218
|
+
"month",
|
|
219
|
+
"year",
|
|
220
|
+
"all"
|
|
221
|
+
]);
|
|
215
222
|
function isDatePreset(value) {
|
|
216
|
-
return
|
|
223
|
+
return NAMED_PRESETS.has(value ?? "") || /^-\d+[dh]$/.test(value ?? "");
|
|
217
224
|
}
|
|
218
|
-
/** The
|
|
219
|
-
|
|
225
|
+
/** The span a preset or a day covers: `from` included, `to` excluded, either open. Whole local
|
|
226
|
+
* days, except the last N hours, which run from that moment on. */
|
|
227
|
+
function presetSpan(value, now) {
|
|
220
228
|
const today = startOfDay(now);
|
|
221
|
-
|
|
222
|
-
if (value === "
|
|
229
|
+
const tomorrow = addDays(today, 1);
|
|
230
|
+
if (value === "all") return {};
|
|
231
|
+
if (value === "today") return {
|
|
232
|
+
from: today,
|
|
233
|
+
to: tomorrow
|
|
234
|
+
};
|
|
235
|
+
if (value === "month") return {
|
|
236
|
+
from: new Date(today.getFullYear(), today.getMonth(), 1),
|
|
237
|
+
to: tomorrow
|
|
238
|
+
};
|
|
239
|
+
if (value === "year") return {
|
|
240
|
+
from: new Date(today.getFullYear(), 0, 1),
|
|
241
|
+
to: tomorrow
|
|
242
|
+
};
|
|
243
|
+
const hours = value.match(/^-(\d+)h$/);
|
|
244
|
+
if (hours) return { from: /* @__PURE__ */ new Date(now.getTime() - Number(hours[1]) * 36e5) };
|
|
223
245
|
const days = value.match(/^-(\d+)d$/);
|
|
224
|
-
if (days) return
|
|
246
|
+
if (days) return {
|
|
247
|
+
from: addDays(today, 1 - Number(days[1])),
|
|
248
|
+
to: tomorrow
|
|
249
|
+
};
|
|
225
250
|
const day = parseDay(value);
|
|
226
|
-
return day ?
|
|
251
|
+
return day ? {
|
|
252
|
+
from: day,
|
|
253
|
+
to: addDays(day, 1)
|
|
254
|
+
} : void 0;
|
|
227
255
|
}
|
|
228
256
|
/** The span a date filter covers right now: `from` included, `to` excluded (the start of the
|
|
229
257
|
* day after the last), either open. Relative presets resolve against `now`. */
|
|
230
258
|
function dateFilterRange(filter, now = /* @__PURE__ */ new Date()) {
|
|
231
259
|
const [first, second] = filter.values;
|
|
232
|
-
const
|
|
260
|
+
const span = (v) => v ? presetSpan(v, now) : void 0;
|
|
261
|
+
const range = (from, to) => ({
|
|
262
|
+
...from && { from },
|
|
263
|
+
...to && { to }
|
|
264
|
+
});
|
|
233
265
|
switch (filter.op) {
|
|
234
266
|
case "is": {
|
|
235
|
-
const
|
|
236
|
-
return
|
|
237
|
-
from: span[0],
|
|
238
|
-
to: addDays(span[1], 1)
|
|
239
|
-
};
|
|
267
|
+
const s = span(first);
|
|
268
|
+
return s && range(s.from, s.to);
|
|
240
269
|
}
|
|
241
270
|
case "after": {
|
|
242
|
-
const
|
|
243
|
-
return
|
|
271
|
+
const s = span(first);
|
|
272
|
+
return s && range(s.from, void 0);
|
|
244
273
|
}
|
|
245
274
|
case "before": {
|
|
246
|
-
const
|
|
247
|
-
return
|
|
275
|
+
const s = span(first);
|
|
276
|
+
return s && range(void 0, s.to);
|
|
248
277
|
}
|
|
249
278
|
case "between": {
|
|
250
|
-
const a =
|
|
251
|
-
const b =
|
|
252
|
-
return a && b ?
|
|
253
|
-
from: a[0],
|
|
254
|
-
to: addDays(b[1], 1)
|
|
255
|
-
} : void 0;
|
|
279
|
+
const a = span(first);
|
|
280
|
+
const b = span(second);
|
|
281
|
+
return a && b ? range(a.from, b.to) : void 0;
|
|
256
282
|
}
|
|
257
283
|
default: return;
|
|
258
284
|
}
|
|
@@ -291,6 +317,7 @@ function matchFilter(field, filter, row, now) {
|
|
|
291
317
|
}
|
|
292
318
|
if (field.kind === "date") {
|
|
293
319
|
const range = dateFilterRange(filter, now);
|
|
320
|
+
if (range && range.from === void 0 && range.to === void 0) return true;
|
|
294
321
|
const t = asTime(v);
|
|
295
322
|
if (!range || t === void 0) return range === void 0;
|
|
296
323
|
return (range.from === void 0 || t >= range.from.getTime()) && (range.to === void 0 || t < range.to.getTime());
|
package/dist/list/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { ActionToolbar, type ToolbarAction, ToolbarButton, type ToolbarItem, } from './ActionToolbar.js';
|
|
2
|
-
export { type ActiveFilter, applyFilterList, DATE_PRESETS, type DatePreset, dateFilterRange, decodeFilters, encodeFilters, type FilterField, type FilterKind, type FilterOperator, type FilterOption, type FilterRowValue, filterParamKeys, matchFilter, sameFilters, withFilterParams, } from './filterModel.js';
|
|
2
|
+
export { type ActiveFilter, applyFilterList, DATE_PRESETS, type DatePreset, dateFilterRange, decodeFilters, encodeFilters, type FilterField, type FilterKind, type FilterOperator, type FilterOption, type FilterRowValue, filterParamKeys, matchFilter, type QuickFilter, sameFilters, withFilterParams, } from './filterModel.js';
|
|
3
3
|
export { AddFilterMenu, type AddFilterMenuProps, FilterChip, ListFilterBar, type ListFilterBarProps, } from './ListFilterBar.js';
|
|
4
4
|
export { GroupRow, ListColumns, ListPage, type ListPageEmpty, type ListPageProps, ListRow, ListRowActionsProvider, useListNavigate, } from './ListPage.js';
|
|
5
5
|
export { actionsColumnWidth, ListActionsHeader, ListCellText, ListError, ListSelectAllHeader, ListSkeleton, listTableFixedProps, listTableProps, NAME_MIN_WIDTH_PX, NameCell, RowSelectCheckbox, STRETCHED_LINK, } from './ListTable.js';
|
package/dist/list/listView.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type { ActiveFilter, FilterField } from './filterModel.js';
|
|
2
|
+
import type { ActiveFilter, FilterField, QuickFilter } from './filterModel.js';
|
|
3
3
|
import { type FilterStore, type ListFilters } from './useListFilters.js';
|
|
4
4
|
/** A toggleable column. Rendering stays in the page's row component, gated by
|
|
5
5
|
* {@link ListView.isVisible}; this is just the header metadata + visibility. */
|
|
@@ -97,6 +97,9 @@ export interface ListViewConfig<T> {
|
|
|
97
97
|
filterStore?: FilterStore | undefined;
|
|
98
98
|
/** False leaves remembering the filters to `filterStore` alone. */
|
|
99
99
|
rememberFilters?: boolean | undefined;
|
|
100
|
+
/** Fields whose values also show as toggles above the chips, such as a job's state with how
|
|
101
|
+
* many jobs are in each. */
|
|
102
|
+
quickFilters?: readonly QuickFilter[] | undefined;
|
|
100
103
|
actions?: ActionDef[];
|
|
101
104
|
/** Explicit pinned-button count when not using actions+isPinned; sizes the actions column. */
|
|
102
105
|
actionCount?: number;
|
|
@@ -192,5 +195,5 @@ export interface ListView<T> extends ListViewChrome {
|
|
|
192
195
|
}
|
|
193
196
|
/** Persisted grouping + column-visibility + field-filter state for a list page,
|
|
194
197
|
* driving the shared Display/Filter menus. */
|
|
195
|
-
export declare function useListView<T>({ storageKey, sharedFiltersKey, columns, groupBys, defaultGroupBy, orderBys, defaultOrderBy, facets, filterFields, defaultFilters, filterStore, rememberFilters, actions, actionCount, defaultShowColumnHeaders, defaultShowActions, isFavorite, isOn, createdAt, }: ListViewConfig<T>): ListView<T>;
|
|
198
|
+
export declare function useListView<T>({ storageKey, sharedFiltersKey, columns, groupBys, defaultGroupBy, orderBys, defaultOrderBy, facets, filterFields, defaultFilters, filterStore, rememberFilters, quickFilters, actions, actionCount, defaultShowColumnHeaders, defaultShowActions, isFavorite, isOn, createdAt, }: ListViewConfig<T>): ListView<T>;
|
|
196
199
|
export {};
|
package/dist/list/listView.js
CHANGED
|
@@ -28,7 +28,7 @@ var DEFAULT_ORDER = "__default";
|
|
|
28
28
|
var FAVORITE_ORDER = "__favorite";
|
|
29
29
|
/** Persisted grouping + column-visibility + field-filter state for a list page,
|
|
30
30
|
* driving the shared Display/Filter menus. */
|
|
31
|
-
function useListView({ storageKey, sharedFiltersKey, columns, groupBys = [], defaultGroupBy, orderBys = [], defaultOrderBy, facets = [], filterFields, defaultFilters, filterStore, rememberFilters = true, actions = [], actionCount, defaultShowColumnHeaders = true, defaultShowActions = true, isFavorite, isOn, createdAt }) {
|
|
31
|
+
function useListView({ storageKey, sharedFiltersKey, columns, groupBys = [], defaultGroupBy, orderBys = [], defaultOrderBy, facets = [], filterFields, defaultFilters, filterStore, rememberFilters = true, quickFilters, actions = [], actionCount, defaultShowColumnHeaders = true, defaultShowActions = true, isFavorite, isOn, createdAt }) {
|
|
32
32
|
const t = useStrings().list;
|
|
33
33
|
const tiebreak = orderBys[0]?.compare;
|
|
34
34
|
const effectiveOrderBys = useMemo(() => {
|
|
@@ -76,7 +76,8 @@ function useListView({ storageKey, sharedFiltersKey, columns, groupBys = [], def
|
|
|
76
76
|
fields,
|
|
77
77
|
defaultFilters,
|
|
78
78
|
store: filterStore,
|
|
79
|
-
remember: rememberFilters
|
|
79
|
+
remember: rememberFilters,
|
|
80
|
+
quick: quickFilters
|
|
80
81
|
});
|
|
81
82
|
const filters = useMemo(() => {
|
|
82
83
|
const out = {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ActiveFilter, type FilterField, type FilterOperator } from './filterModel.js';
|
|
1
|
+
import { type ActiveFilter, type FilterField, type FilterOperator, type QuickFilter } from './filterModel.js';
|
|
2
2
|
/** Where filters live besides what the list remembers, such as the page's URL. */
|
|
3
3
|
export interface FilterStore {
|
|
4
4
|
/** The filters it holds, or `null` when it holds none, such as a URL with no filter params,
|
|
@@ -20,6 +20,8 @@ export interface ListFiltersConfig<T> {
|
|
|
20
20
|
store?: FilterStore | undefined;
|
|
21
21
|
/** False leaves remembering to the store alone. */
|
|
22
22
|
remember?: boolean | undefined;
|
|
23
|
+
/** Fields whose values also show as toggles above the chips. */
|
|
24
|
+
quick?: readonly QuickFilter[] | undefined;
|
|
23
25
|
}
|
|
24
26
|
/** A request for a chip to open one of its pickers, which the chip clears once it has. */
|
|
25
27
|
export interface FilterEditRequest {
|
|
@@ -30,6 +32,8 @@ export interface FilterEditRequest {
|
|
|
30
32
|
/** A list's filters, the state behind the chip row and the Filter menu. */
|
|
31
33
|
export interface ListFilters {
|
|
32
34
|
fields: readonly FilterField<any>[];
|
|
35
|
+
/** Fields whose values also show as toggles above the chips. */
|
|
36
|
+
quick: readonly QuickFilter[];
|
|
33
37
|
/** The filters in effect, in the order they were added. */
|
|
34
38
|
active: readonly ActiveFilter[];
|
|
35
39
|
/** The chips to show: the filters in effect, then one being added that has no value yet. */
|
|
@@ -65,4 +69,4 @@ export interface ListFilters {
|
|
|
65
69
|
/** A list's active filters: which, in what order, where they are remembered and how rows
|
|
66
70
|
* match them. `useListView` builds one from its facets and fields; a page not on it, or
|
|
67
71
|
* filtering on the server, can call this alone. */
|
|
68
|
-
export declare function useListFilters<T>({ storageKey, sharedFiltersKey, fields, defaultFilters, store, remember, }: ListFiltersConfig<T>): ListFilters;
|
|
72
|
+
export declare function useListFilters<T>({ storageKey, sharedFiltersKey, fields, defaultFilters, store, remember, quick, }: ListFiltersConfig<T>): ListFilters;
|