@burojs/ui-shadcn 0.4.0 → 0.6.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/README.md +22 -5
- package/dist/bulk-summary-CwSd6Yjb.cjs +108 -0
- package/dist/bulk-summary-CwSd6Yjb.cjs.map +1 -0
- package/dist/bulk-summary-GxMDP4bw.js +108 -0
- package/dist/bulk-summary-GxMDP4bw.js.map +1 -0
- package/dist/button-BxnnE28H.cjs +55 -0
- package/dist/button-BxnnE28H.cjs.map +1 -0
- package/dist/button-MWVeZaqj.js +44 -0
- package/dist/button-MWVeZaqj.js.map +1 -0
- package/dist/candidates-gate-Bb70LPAy.js +135 -0
- package/dist/candidates-gate-Bb70LPAy.js.map +1 -0
- package/dist/candidates-gate-D33jXLHO.cjs +153 -0
- package/dist/candidates-gate-D33jXLHO.cjs.map +1 -0
- package/dist/candidates-relation-picker-B116Bmqi.js +31 -0
- package/dist/candidates-relation-picker-B116Bmqi.js.map +1 -0
- package/dist/candidates-relation-picker-Cy0rTxvI.cjs +31 -0
- package/dist/candidates-relation-picker-Cy0rTxvI.cjs.map +1 -0
- package/dist/check-confirm-dialog--gPFnEeg.cjs +1547 -0
- package/dist/check-confirm-dialog--gPFnEeg.cjs.map +1 -0
- package/dist/check-confirm-dialog-bRT8lt0d.js +1245 -0
- package/dist/check-confirm-dialog-bRT8lt0d.js.map +1 -0
- package/dist/dialog-B_rt9S1c.js +63 -0
- package/dist/dialog-B_rt9S1c.js.map +1 -0
- package/dist/dialog-DRNUq9EA.cjs +112 -0
- package/dist/dialog-DRNUq9EA.cjs.map +1 -0
- package/dist/filter-builder-Bhn-dhZV.js +1621 -0
- package/dist/filter-builder-Bhn-dhZV.js.map +1 -0
- package/dist/filter-builder-nXqtM_h4.cjs +1789 -0
- package/dist/filter-builder-nXqtM_h4.cjs.map +1 -0
- package/dist/flow-runtime-D3avVH0I.cjs +255 -0
- package/dist/flow-runtime-D3avVH0I.cjs.map +1 -0
- package/dist/flow-runtime-s6lkRPKF.js +252 -0
- package/dist/flow-runtime-s6lkRPKF.js.map +1 -0
- package/dist/flow-screen-CCvgDI9G.js +704 -0
- package/dist/flow-screen-CCvgDI9G.js.map +1 -0
- package/dist/flow-screen-Ci_Sik4Y.cjs +708 -0
- package/dist/flow-screen-Ci_Sik4Y.cjs.map +1 -0
- package/dist/index.cjs +3420 -11986
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1843 -2015
- package/dist/index.d.ts +1849 -2021
- package/dist/index.js +3090 -11675
- package/dist/index.js.map +1 -1
- package/dist/inline-flow-host-B6FI0ctQ.js +112 -0
- package/dist/inline-flow-host-B6FI0ctQ.js.map +1 -0
- package/dist/inline-flow-host-DV1et3c6.cjs +112 -0
- package/dist/inline-flow-host-DV1et3c6.cjs.map +1 -0
- package/dist/input-BI0czGyI.cjs +23 -0
- package/dist/input-BI0czGyI.cjs.map +1 -0
- package/dist/input-CUPStzZO.js +17 -0
- package/dist/input-CUPStzZO.js.map +1 -0
- package/dist/label-66W1qbke.js +29 -0
- package/dist/label-66W1qbke.js.map +1 -0
- package/dist/label-w6JYf2cF.cjs +36 -0
- package/dist/label-w6JYf2cF.cjs.map +1 -0
- package/dist/markdown/index.cjs +1 -1
- package/dist/markdown/index.d.cts +2 -3
- package/dist/markdown/index.d.ts +2 -3
- package/dist/markdown/index.js +1 -1
- package/dist/page-flow-route-hdnULWqd.cjs +277 -0
- package/dist/page-flow-route-hdnULWqd.cjs.map +1 -0
- package/dist/page-flow-route-i3ZfJ1sd.js +277 -0
- package/dist/page-flow-route-i3ZfJ1sd.js.map +1 -0
- package/dist/place-node-C5SmIAYU.cjs +8139 -0
- package/dist/place-node-C5SmIAYU.cjs.map +1 -0
- package/dist/place-node-NraNYKPx.js +7597 -0
- package/dist/place-node-NraNYKPx.js.map +1 -0
- package/dist/related-list-cards-BWZ9GfOG.js +42 -0
- package/dist/related-list-cards-BWZ9GfOG.js.map +1 -0
- package/dist/related-list-cards-CFq3meUn.cjs +42 -0
- package/dist/related-list-cards-CFq3meUn.cjs.map +1 -0
- package/dist/related-list-timeline-CCzl71aA.cjs +142 -0
- package/dist/related-list-timeline-CCzl71aA.cjs.map +1 -0
- package/dist/related-list-timeline-yrPsjA3F.js +142 -0
- package/dist/related-list-timeline-yrPsjA3F.js.map +1 -0
- package/dist/related-rows-ea43qhhy.cjs +208 -0
- package/dist/related-rows-ea43qhhy.cjs.map +1 -0
- package/dist/related-rows-o0_t-Yce.js +185 -0
- package/dist/related-rows-o0_t-Yce.js.map +1 -0
- package/dist/relation-create-IA40t1hi.cjs +364 -0
- package/dist/relation-create-IA40t1hi.cjs.map +1 -0
- package/dist/relation-create-h6F9B4C4.js +339 -0
- package/dist/relation-create-h6F9B4C4.js.map +1 -0
- package/dist/relation-many-picker-C8zFEjwd.cjs +467 -0
- package/dist/relation-many-picker-C8zFEjwd.cjs.map +1 -0
- package/dist/relation-many-picker-bvkjkv-k.js +466 -0
- package/dist/relation-many-picker-bvkjkv-k.js.map +1 -0
- package/dist/relation-picker-Z0ZJJ5uS.js +493 -0
- package/dist/relation-picker-Z0ZJJ5uS.js.map +1 -0
- package/dist/relation-picker-dialog-0IzkjmiA.cjs +304 -0
- package/dist/relation-picker-dialog-0IzkjmiA.cjs.map +1 -0
- package/dist/relation-picker-dialog-B6nIiJBf.js +303 -0
- package/dist/relation-picker-dialog-B6nIiJBf.js.map +1 -0
- package/dist/relation-picker-gag0qSR5.cjs +516 -0
- package/dist/relation-picker-gag0qSR5.cjs.map +1 -0
- package/dist/resource-page-router-3RUf9d1j.js +1588 -0
- package/dist/resource-page-router-3RUf9d1j.js.map +1 -0
- package/dist/resource-page-router-PXfMW9nh.cjs +1785 -0
- package/dist/resource-page-router-PXfMW9nh.cjs.map +1 -0
- package/dist/{rich-text-IDFvyvRE.js → rich-text-DFerywgz.js} +7 -4
- package/dist/{rich-text-IDFvyvRE.js.map → rich-text-DFerywgz.js.map} +1 -1
- package/dist/{rich-text-lndbYqzR.cjs → rich-text-vTyVjDoa.cjs} +25 -22
- package/dist/{rich-text-lndbYqzR.cjs.map → rich-text-vTyVjDoa.cjs.map} +1 -1
- package/dist/search-access-CPPmfuvy.js +654 -0
- package/dist/search-access-CPPmfuvy.js.map +1 -0
- package/dist/search-access-Dugvttlg.cjs +791 -0
- package/dist/search-access-Dugvttlg.cjs.map +1 -0
- package/dist/shell-BPlk8hEa.js +205 -0
- package/dist/shell-BPlk8hEa.js.map +1 -0
- package/dist/shell-DxhlsPwm.cjs +271 -0
- package/dist/shell-DxhlsPwm.cjs.map +1 -0
- package/dist/skeleton-U2noefj5.js +13 -0
- package/dist/skeleton-U2noefj5.js.map +1 -0
- package/dist/skeleton-_1gP7wan.cjs +18 -0
- package/dist/skeleton-_1gP7wan.cjs.map +1 -0
- package/dist/styles.css +241 -9
- package/dist/toaster-C02XbB9r.js +135 -0
- package/dist/toaster-C02XbB9r.js.map +1 -0
- package/dist/toaster-Cr51X7bz.cjs +195 -0
- package/dist/toaster-Cr51X7bz.cjs.map +1 -0
- package/dist/use-dialog-filter-state-C--oMAZG.js +87 -0
- package/dist/use-dialog-filter-state-C--oMAZG.js.map +1 -0
- package/dist/use-dialog-filter-state-Dhe8PcB5.cjs +92 -0
- package/dist/use-dialog-filter-state-Dhe8PcB5.cjs.map +1 -0
- package/dist/{use-theme-BwPWinB2.cjs → use-theme-DFlpXils.cjs} +12 -1
- package/dist/use-theme-DFlpXils.cjs.map +1 -0
- package/dist/{use-theme-IHmtrhEt.js → use-theme-dnE3Od3I.js} +7 -2
- package/dist/use-theme-dnE3Od3I.js.map +1 -0
- package/package.json +16 -14
- package/dist/use-theme-BwPWinB2.cjs.map +0 -1
- package/dist/use-theme-IHmtrhEt.js.map +0 -1
|
@@ -0,0 +1,1621 @@
|
|
|
1
|
+
import { l as cn } from "./use-theme-dnE3Od3I.js";
|
|
2
|
+
import { d as PopoverTrigger, s as Popover, u as PopoverContent } from "./toaster-C02XbB9r.js";
|
|
3
|
+
import { t as Button } from "./button-MWVeZaqj.js";
|
|
4
|
+
import { t as Input } from "./input-CUPStzZO.js";
|
|
5
|
+
import { _ as CommandEmpty, b as CommandItem, d as useRowTitle, h as Command, n as narrowSearchDecl, v as CommandGroup, x as CommandList, y as CommandInput } from "./search-access-CPPmfuvy.js";
|
|
6
|
+
import { EMPTY_FILTER_GROUP, convertValueForOp, enumValueText, isCompletePredicate, isFilterGroup, opArity, resolveEnumLabel } from "@burojs/core";
|
|
7
|
+
import { DEFAULT_LANGUAGE_STORAGE_KEY, useApp, useEnumValues, useI18nCaps, useList, useListAccess, useOptionalT, useRelatedRecord } from "@burojs/react";
|
|
8
|
+
import { IconCheck, IconChevronDown, IconChevronUp, IconFilter, IconFolderPlus, IconLoader2, IconPlus, IconX } from "@tabler/icons-react";
|
|
9
|
+
import * as React from "react";
|
|
10
|
+
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
11
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
import { cva } from "class-variance-authority";
|
|
13
|
+
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
14
|
+
//#region src/layout/language-storage.ts
|
|
15
|
+
function loadLocale(storageKey = DEFAULT_LANGUAGE_STORAGE_KEY) {
|
|
16
|
+
if (typeof window === "undefined") return void 0;
|
|
17
|
+
try {
|
|
18
|
+
return window.localStorage.getItem(storageKey) ?? void 0;
|
|
19
|
+
} catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function saveLocale(code, storageKey = DEFAULT_LANGUAGE_STORAGE_KEY) {
|
|
24
|
+
if (typeof window === "undefined") return;
|
|
25
|
+
try {
|
|
26
|
+
window.localStorage.setItem(storageKey, code);
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/primitives/badge.tsx
|
|
31
|
+
const badgeVariants = cva("inline-flex max-w-full items-center truncate rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2", {
|
|
32
|
+
variants: { variant: {
|
|
33
|
+
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
|
34
|
+
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
35
|
+
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
|
36
|
+
outline: "text-foreground",
|
|
37
|
+
success: "border-success/20 bg-success/15 text-success-emphasis hover:bg-success/20",
|
|
38
|
+
warning: "border-warning/20 bg-warning/15 text-warning-emphasis hover:bg-warning/20",
|
|
39
|
+
info: "border-info/20 bg-info/15 text-info-emphasis hover:bg-info/20"
|
|
40
|
+
} },
|
|
41
|
+
defaultVariants: { variant: "default" }
|
|
42
|
+
});
|
|
43
|
+
function Badge({ className, variant, children, ...props }) {
|
|
44
|
+
return jsx("div", {
|
|
45
|
+
className: cn(badgeVariants({ variant }), className),
|
|
46
|
+
...props,
|
|
47
|
+
children: jsx("span", {
|
|
48
|
+
className: "min-w-0 truncate",
|
|
49
|
+
children
|
|
50
|
+
})
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/primitives/table.tsx
|
|
55
|
+
const Table = React.forwardRef(({ className, ...props }, ref) => jsx("div", {
|
|
56
|
+
className: "relative w-full overflow-auto",
|
|
57
|
+
children: jsx("table", {
|
|
58
|
+
ref,
|
|
59
|
+
className: cn("w-full caption-bottom text-sm", className),
|
|
60
|
+
...props
|
|
61
|
+
})
|
|
62
|
+
}));
|
|
63
|
+
Table.displayName = "Table";
|
|
64
|
+
const TableHeader = React.forwardRef(({ className, ...props }, ref) => jsx("thead", {
|
|
65
|
+
ref,
|
|
66
|
+
className: cn("[&_tr]:border-b", className),
|
|
67
|
+
...props
|
|
68
|
+
}));
|
|
69
|
+
TableHeader.displayName = "TableHeader";
|
|
70
|
+
const TableBody = React.forwardRef(({ className, ...props }, ref) => jsx("tbody", {
|
|
71
|
+
ref,
|
|
72
|
+
className: cn("[&_tr:last-child]:border-0", className),
|
|
73
|
+
...props
|
|
74
|
+
}));
|
|
75
|
+
TableBody.displayName = "TableBody";
|
|
76
|
+
const TableRow = React.forwardRef(({ className, ...props }, ref) => jsx("tr", {
|
|
77
|
+
ref,
|
|
78
|
+
className: cn("group/row border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-primary/10 data-[state=selected]:hover:bg-primary/15", className),
|
|
79
|
+
...props
|
|
80
|
+
}));
|
|
81
|
+
TableRow.displayName = "TableRow";
|
|
82
|
+
const TableHead = React.forwardRef(({ className, ...props }, ref) => jsx("th", {
|
|
83
|
+
ref,
|
|
84
|
+
className: cn("h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", className),
|
|
85
|
+
...props
|
|
86
|
+
}));
|
|
87
|
+
TableHead.displayName = "TableHead";
|
|
88
|
+
const TableCell = React.forwardRef(({ className, ...props }, ref) => jsx("td", {
|
|
89
|
+
ref,
|
|
90
|
+
className: cn("p-2 align-middle [&:has([role=checkbox])]:pr-0", className),
|
|
91
|
+
...props
|
|
92
|
+
}));
|
|
93
|
+
TableCell.displayName = "TableCell";
|
|
94
|
+
const TableCaption = React.forwardRef(({ className, ...props }, ref) => jsx("caption", {
|
|
95
|
+
ref,
|
|
96
|
+
className: cn("mt-4 text-sm text-muted-foreground", className),
|
|
97
|
+
...props
|
|
98
|
+
}));
|
|
99
|
+
TableCaption.displayName = "TableCaption";
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/primitives/select.tsx
|
|
102
|
+
const Select = SelectPrimitive.Root;
|
|
103
|
+
const SelectGroup = SelectPrimitive.Group;
|
|
104
|
+
const SelectValue = SelectPrimitive.Value;
|
|
105
|
+
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => jsxs(SelectPrimitive.Trigger, {
|
|
106
|
+
ref,
|
|
107
|
+
className: cn("flex h-9 w-full items-center justify-between rounded-md border border-input bg-background text-foreground px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-input-disabled [&>span]:line-clamp-1", className),
|
|
108
|
+
...props,
|
|
109
|
+
children: [children, jsx(SelectPrimitive.Icon, {
|
|
110
|
+
asChild: true,
|
|
111
|
+
children: jsx(IconChevronDown, { className: "h-4 w-4 opacity-50" })
|
|
112
|
+
})]
|
|
113
|
+
}));
|
|
114
|
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
|
115
|
+
const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => jsx(SelectPrimitive.ScrollUpButton, {
|
|
116
|
+
ref,
|
|
117
|
+
className: cn("flex cursor-default items-center justify-center py-1", className),
|
|
118
|
+
...props,
|
|
119
|
+
children: jsx(IconChevronUp, { className: "h-4 w-4" })
|
|
120
|
+
}));
|
|
121
|
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
|
122
|
+
const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => jsx(SelectPrimitive.ScrollDownButton, {
|
|
123
|
+
ref,
|
|
124
|
+
className: cn("flex cursor-default items-center justify-center py-1", className),
|
|
125
|
+
...props,
|
|
126
|
+
children: jsx(IconChevronDown, { className: "h-4 w-4" })
|
|
127
|
+
}));
|
|
128
|
+
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
|
|
129
|
+
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => jsx(SelectPrimitive.Portal, { children: jsxs(SelectPrimitive.Content, {
|
|
130
|
+
ref,
|
|
131
|
+
className: cn("relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md duration-[var(--duration-overlay)] ease-[var(--ease-overlay)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className),
|
|
132
|
+
position,
|
|
133
|
+
...props,
|
|
134
|
+
children: [
|
|
135
|
+
jsx(SelectScrollUpButton, {}),
|
|
136
|
+
jsx(SelectPrimitive.Viewport, {
|
|
137
|
+
className: cn("p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"),
|
|
138
|
+
children
|
|
139
|
+
}),
|
|
140
|
+
jsx(SelectScrollDownButton, {})
|
|
141
|
+
]
|
|
142
|
+
}) }));
|
|
143
|
+
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
|
144
|
+
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => jsx(SelectPrimitive.Label, {
|
|
145
|
+
ref,
|
|
146
|
+
className: cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className),
|
|
147
|
+
...props
|
|
148
|
+
}));
|
|
149
|
+
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
|
150
|
+
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => jsxs(SelectPrimitive.Item, {
|
|
151
|
+
ref,
|
|
152
|
+
className: cn("relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className),
|
|
153
|
+
...props,
|
|
154
|
+
children: [jsx("span", {
|
|
155
|
+
className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center",
|
|
156
|
+
children: jsx(SelectPrimitive.ItemIndicator, { children: jsx(IconCheck, { className: "h-4 w-4" }) })
|
|
157
|
+
}), jsx(SelectPrimitive.ItemText, { children })]
|
|
158
|
+
}));
|
|
159
|
+
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
|
160
|
+
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => jsx(SelectPrimitive.Separator, {
|
|
161
|
+
ref,
|
|
162
|
+
className: cn("-mx-1 my-1 h-px bg-muted", className),
|
|
163
|
+
...props
|
|
164
|
+
}));
|
|
165
|
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/fields/format.ts
|
|
168
|
+
function localeTag(locale) {
|
|
169
|
+
return locale || loadLocale() || "en-US";
|
|
170
|
+
}
|
|
171
|
+
const dateCache = new Map();
|
|
172
|
+
const dateTimeCache = new Map();
|
|
173
|
+
const numberCache = new Map();
|
|
174
|
+
function cached(cache, key, make) {
|
|
175
|
+
const hit = cache.get(key);
|
|
176
|
+
if (hit) return hit;
|
|
177
|
+
let made;
|
|
178
|
+
try {
|
|
179
|
+
made = make(key);
|
|
180
|
+
} catch {
|
|
181
|
+
made = make("en-US");
|
|
182
|
+
}
|
|
183
|
+
cache.set(key, made);
|
|
184
|
+
return made;
|
|
185
|
+
}
|
|
186
|
+
function formatDate(d, locale) {
|
|
187
|
+
const tag = localeTag(locale);
|
|
188
|
+
return cached(dateCache, tag, (t) => new Intl.DateTimeFormat(t, {
|
|
189
|
+
year: "numeric",
|
|
190
|
+
month: "short",
|
|
191
|
+
day: "numeric"
|
|
192
|
+
})).format(d);
|
|
193
|
+
}
|
|
194
|
+
function formatDateTime(d, locale) {
|
|
195
|
+
const tag = localeTag(locale);
|
|
196
|
+
return cached(dateTimeCache, tag, (t) => new Intl.DateTimeFormat(t, {
|
|
197
|
+
year: "numeric",
|
|
198
|
+
month: "short",
|
|
199
|
+
day: "numeric",
|
|
200
|
+
hour: "2-digit",
|
|
201
|
+
minute: "2-digit"
|
|
202
|
+
})).format(d);
|
|
203
|
+
}
|
|
204
|
+
function formatNumber(n, locale) {
|
|
205
|
+
const tag = localeTag(locale);
|
|
206
|
+
return cached(numberCache, tag, (t) => new Intl.NumberFormat(t)).format(n);
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/list-kit/filter-builder/describe-row.ts
|
|
210
|
+
const MAX_DESCRIBE_DEPTH = 2;
|
|
211
|
+
function describeValue(value, t, spec, formatValue) {
|
|
212
|
+
if (value === void 0) return null;
|
|
213
|
+
if (Array.isArray(value)) return t("filters.selectedCount", { count: value.length }, `${value.length} selected`);
|
|
214
|
+
const optionLabel = spec?.options?.find((o) => o.value === String(value))?.label;
|
|
215
|
+
if (optionLabel !== void 0) return optionLabel;
|
|
216
|
+
if (spec && formatValue) {
|
|
217
|
+
const formatted = formatValue(spec.kind, value);
|
|
218
|
+
if (formatted !== void 0) return formatted;
|
|
219
|
+
}
|
|
220
|
+
return String(value);
|
|
221
|
+
}
|
|
222
|
+
function describeAt(row, available, t, depth, formatValue) {
|
|
223
|
+
if (isFilterGroup(row)) {
|
|
224
|
+
if (depth > MAX_DESCRIBE_DEPTH) return "…";
|
|
225
|
+
const joinLabel = t(`filters.${row.join}`, void 0, row.join);
|
|
226
|
+
return `(${row.rows.map((child) => describeAt(child, available, t, depth + 1, formatValue)).join(` ${joinLabel} `)})`;
|
|
227
|
+
}
|
|
228
|
+
const spec = available.find((s) => s.field === row.field);
|
|
229
|
+
const label = spec?.label ?? row.field;
|
|
230
|
+
const opLabel = t(`filters.operators.${row.op}`, void 0, row.op);
|
|
231
|
+
const valueText = opArity(row.op) === "none" ? null : describeValue(row.value, t, spec, formatValue);
|
|
232
|
+
return valueText === null ? `${label} ${opLabel}` : `${label} ${opLabel} ${valueText}`;
|
|
233
|
+
}
|
|
234
|
+
function describeRow(row, available, t, formatValue) {
|
|
235
|
+
return describeAt(row, available, t, 1, formatValue);
|
|
236
|
+
}
|
|
237
|
+
function relationLabelKey(to, by, value) {
|
|
238
|
+
return `${by === void 0 ? to : `${to}#${by}`}::${value}`;
|
|
239
|
+
}
|
|
240
|
+
function collectFormatRefs(rows, available) {
|
|
241
|
+
const enumNames = new Set();
|
|
242
|
+
const relationRefs = new Map();
|
|
243
|
+
const walk = (nodes) => {
|
|
244
|
+
for (const row of nodes) {
|
|
245
|
+
if (isFilterGroup(row)) {
|
|
246
|
+
walk(row.rows);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (row.value === void 0 || Array.isArray(row.value)) continue;
|
|
250
|
+
const spec = available.find((s) => s.field === row.field);
|
|
251
|
+
if (!spec) continue;
|
|
252
|
+
if (spec.kind.kind === "enum") enumNames.add(spec.kind.enumName);
|
|
253
|
+
else if (spec.kind.kind === "relation") {
|
|
254
|
+
const id = String(row.value);
|
|
255
|
+
const { to, by } = spec.kind;
|
|
256
|
+
relationRefs.set(relationLabelKey(to, by, id), by === void 0 ? {
|
|
257
|
+
to,
|
|
258
|
+
id
|
|
259
|
+
} : {
|
|
260
|
+
to,
|
|
261
|
+
id,
|
|
262
|
+
by
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
walk(rows);
|
|
268
|
+
return {
|
|
269
|
+
enumNames: Array.from(enumNames),
|
|
270
|
+
relationRefs: Array.from(relationRefs.values())
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/list-kit/filter-builder/describe-value-resolvers.tsx
|
|
275
|
+
function EnumLabelResolver(props) {
|
|
276
|
+
const { enumName, onResolve } = props;
|
|
277
|
+
const { values } = useEnumValues(enumName);
|
|
278
|
+
const caps = useI18nCaps();
|
|
279
|
+
const def = useApp().registry.enums.get(enumName);
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
if (!values) return;
|
|
282
|
+
const labels = {};
|
|
283
|
+
for (const entry of values) labels[entry.value] = resolveEnumLabel(caps, enumName, entry.value, enumValueText(def, entry.value) ?? entry.label);
|
|
284
|
+
onResolve(enumName, labels);
|
|
285
|
+
}, [
|
|
286
|
+
values,
|
|
287
|
+
enumName,
|
|
288
|
+
caps,
|
|
289
|
+
def,
|
|
290
|
+
onResolve
|
|
291
|
+
]);
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
function RelationLabelResolver(props) {
|
|
295
|
+
const { to, id, by, onResolve } = props;
|
|
296
|
+
const targetDef = useApp().registry.get(to);
|
|
297
|
+
const { record } = useRelatedRecord(to, id, by === void 0 ? void 0 : { by });
|
|
298
|
+
const rowTitle = useRowTitle(to, targetDef?.displayField);
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
if (!record) return;
|
|
301
|
+
onResolve(relationLabelKey(to, by, id), rowTitle(record));
|
|
302
|
+
}, [
|
|
303
|
+
record,
|
|
304
|
+
to,
|
|
305
|
+
by,
|
|
306
|
+
id,
|
|
307
|
+
rowTitle,
|
|
308
|
+
onResolve
|
|
309
|
+
]);
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
function FormatResolvers(props) {
|
|
313
|
+
const { refs, onEnumResolved, onRelationResolved } = props;
|
|
314
|
+
return jsxs(Fragment$1, { children: [refs.enumNames.map((name) => jsx(EnumLabelResolver, {
|
|
315
|
+
enumName: name,
|
|
316
|
+
onResolve: onEnumResolved
|
|
317
|
+
}, `enum:${name}`)), refs.relationRefs.map(({ to, id, by }) => jsx(RelationLabelResolver, {
|
|
318
|
+
to,
|
|
319
|
+
id,
|
|
320
|
+
...by === void 0 ? {} : { by },
|
|
321
|
+
onResolve: onRelationResolved
|
|
322
|
+
}, `relation:${relationLabelKey(to, by, id)}`))] });
|
|
323
|
+
}
|
|
324
|
+
function useFormatValueMaps() {
|
|
325
|
+
const [enumLabels, setEnumLabels] = useState({});
|
|
326
|
+
const [relationLabels, setRelationLabels] = useState({});
|
|
327
|
+
return {
|
|
328
|
+
enumLabels,
|
|
329
|
+
relationLabels,
|
|
330
|
+
onEnumResolved: useCallback((enumName, labels) => {
|
|
331
|
+
setEnumLabels((prev) => prev[enumName] === labels ? prev : {
|
|
332
|
+
...prev,
|
|
333
|
+
[enumName]: labels
|
|
334
|
+
});
|
|
335
|
+
}, []),
|
|
336
|
+
onRelationResolved: useCallback((key, label) => {
|
|
337
|
+
setRelationLabels((prev) => prev[key] === label ? prev : {
|
|
338
|
+
...prev,
|
|
339
|
+
[key]: label
|
|
340
|
+
});
|
|
341
|
+
}, [])
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/list-kit/filter-builder/pinned-ownership.ts
|
|
346
|
+
function isFilterPredicateExpr(expr) {
|
|
347
|
+
return "field" in expr;
|
|
348
|
+
}
|
|
349
|
+
function findOwnedIndex(value, spec) {
|
|
350
|
+
const op = spec.operators[0] ?? "eq";
|
|
351
|
+
return value.findIndex((expr) => isFilterPredicateExpr(expr) && expr.field === spec.field && expr.op === op);
|
|
352
|
+
}
|
|
353
|
+
function claimedPinnedIndices(value, pinnedFields) {
|
|
354
|
+
const claimed = new Set();
|
|
355
|
+
for (const spec of pinnedFields) {
|
|
356
|
+
const op = spec.operators[0] ?? "eq";
|
|
357
|
+
const index = value.findIndex((expr, i) => !claimed.has(i) && isFilterPredicateExpr(expr) && expr.field === spec.field && expr.op === op);
|
|
358
|
+
if (index >= 0) claimed.add(index);
|
|
359
|
+
}
|
|
360
|
+
return claimed;
|
|
361
|
+
}
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/list-kit/filter-builder/filter-badges.tsx
|
|
364
|
+
function badgeKey(row, index) {
|
|
365
|
+
if (isFilterGroup(row)) return `${index}:group:${row.join}`;
|
|
366
|
+
return `${index}:${row.field}:${row.op}`;
|
|
367
|
+
}
|
|
368
|
+
function FilterBadges(props) {
|
|
369
|
+
const { group, setGroup, available, fixed, testIdBase, onOpenAt, onRemoved, pinnedFields = [], pinnedValue = [], setPinnedValue } = props;
|
|
370
|
+
const t = useOptionalT();
|
|
371
|
+
const tt = (key, params, fallback) => t(key, {
|
|
372
|
+
params,
|
|
373
|
+
...typeof params?.count === "number" ? { count: params.count } : {},
|
|
374
|
+
defaultValue: fallback
|
|
375
|
+
});
|
|
376
|
+
const removeLabel = t("filters.removeRow", { defaultValue: "Remove filter" });
|
|
377
|
+
const { enumLabels, relationLabels, onEnumResolved, onRelationResolved } = useFormatValueMaps();
|
|
378
|
+
const formatValue = (kind, value) => {
|
|
379
|
+
if (kind.kind === "enum") return enumLabels[kind.enumName]?.[String(value)];
|
|
380
|
+
if (kind.kind === "relation") return relationLabels[relationLabelKey(kind.to, kind.by, String(value))];
|
|
381
|
+
if (kind.kind === "date") {
|
|
382
|
+
const d = new Date(String(value));
|
|
383
|
+
if (Number.isNaN(d.getTime())) return void 0;
|
|
384
|
+
return kind.time ? formatDateTime(d) : formatDate(d);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
const orphanPinned = useMemo(() => {
|
|
388
|
+
const claimed = claimedPinnedIndices(pinnedValue, pinnedFields);
|
|
389
|
+
return pinnedValue.map((expr, index) => ({
|
|
390
|
+
expr,
|
|
391
|
+
index
|
|
392
|
+
})).filter(({ expr, index }) => !claimed.has(index) && isFilterPredicateExpr(expr));
|
|
393
|
+
}, [pinnedValue, pinnedFields]);
|
|
394
|
+
const refs = useMemo(() => collectFormatRefs([...group.rows, ...orphanPinned.map(({ expr }) => expr)], available), [
|
|
395
|
+
group,
|
|
396
|
+
orphanPinned,
|
|
397
|
+
available
|
|
398
|
+
]);
|
|
399
|
+
const handleRemove = (index) => {
|
|
400
|
+
setGroup({
|
|
401
|
+
...group,
|
|
402
|
+
rows: group.rows.filter((_, i) => i !== index)
|
|
403
|
+
});
|
|
404
|
+
onRemoved?.(index);
|
|
405
|
+
};
|
|
406
|
+
const handleRemovePinned = (index) => {
|
|
407
|
+
setPinnedValue?.(pinnedValue.filter((_, i) => i !== index));
|
|
408
|
+
};
|
|
409
|
+
const labelledFixed = fixed.filter((entry) => entry.label !== void 0);
|
|
410
|
+
return jsxs("div", {
|
|
411
|
+
className: "flex flex-wrap items-center gap-1",
|
|
412
|
+
children: [
|
|
413
|
+
jsx(FormatResolvers, {
|
|
414
|
+
refs,
|
|
415
|
+
onEnumResolved,
|
|
416
|
+
onRelationResolved
|
|
417
|
+
}),
|
|
418
|
+
group.rows.map((row, index) => jsxs(Badge, {
|
|
419
|
+
variant: "secondary",
|
|
420
|
+
className: "gap-1 py-1 pr-1",
|
|
421
|
+
children: [jsx("button", {
|
|
422
|
+
type: "button",
|
|
423
|
+
"data-testid": `${testIdBase}:badge:${index}`,
|
|
424
|
+
onClick: () => onOpenAt([index]),
|
|
425
|
+
className: "max-w-full cursor-pointer truncate bg-transparent p-0 text-left",
|
|
426
|
+
children: describeRow(row, available, tt, formatValue)
|
|
427
|
+
}), jsx("button", {
|
|
428
|
+
type: "button",
|
|
429
|
+
"data-testid": `${testIdBase}:badge:${index}:remove`,
|
|
430
|
+
"aria-label": removeLabel,
|
|
431
|
+
onClick: () => handleRemove(index),
|
|
432
|
+
className: "rounded-sm hover:bg-accent",
|
|
433
|
+
children: jsx(IconX, {
|
|
434
|
+
className: "h-3 w-3",
|
|
435
|
+
"aria-hidden": "true"
|
|
436
|
+
})
|
|
437
|
+
})]
|
|
438
|
+
}, badgeKey(row, index))),
|
|
439
|
+
labelledFixed.map((entry, index) => jsx(Badge, {
|
|
440
|
+
variant: "outline",
|
|
441
|
+
"data-testid": `${testIdBase}:badge:fixed:${index}`,
|
|
442
|
+
children: entry.label
|
|
443
|
+
}, `fixed-${index}`)),
|
|
444
|
+
orphanPinned.map(({ expr, index }) => jsxs(Badge, {
|
|
445
|
+
variant: "secondary",
|
|
446
|
+
className: "gap-1 py-1",
|
|
447
|
+
children: [jsx("span", {
|
|
448
|
+
"data-testid": `${testIdBase}:badge:pinned:${index}`,
|
|
449
|
+
children: describeRow(expr, available, tt, formatValue)
|
|
450
|
+
}), jsx("button", {
|
|
451
|
+
type: "button",
|
|
452
|
+
"data-testid": `${testIdBase}:badge:pinned:${index}:remove`,
|
|
453
|
+
"aria-label": removeLabel,
|
|
454
|
+
onClick: () => handleRemovePinned(index),
|
|
455
|
+
className: "rounded-sm hover:bg-accent",
|
|
456
|
+
children: jsx(IconX, {
|
|
457
|
+
className: "h-3 w-3",
|
|
458
|
+
"aria-hidden": "true"
|
|
459
|
+
})
|
|
460
|
+
})]
|
|
461
|
+
}, `pinned-${index}`))
|
|
462
|
+
]
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
let counter = 0;
|
|
466
|
+
function nextId() {
|
|
467
|
+
counter += 1;
|
|
468
|
+
return `r${counter}`;
|
|
469
|
+
}
|
|
470
|
+
function isDraftGroup(node) {
|
|
471
|
+
return Object.hasOwn(node, "rows");
|
|
472
|
+
}
|
|
473
|
+
function toDraft(group) {
|
|
474
|
+
return {
|
|
475
|
+
rowId: nextId(),
|
|
476
|
+
join: group.join,
|
|
477
|
+
rows: group.rows.map(toDraftNode)
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function toDraftNode(row) {
|
|
481
|
+
if (isFilterGroup(row)) return toDraft(row);
|
|
482
|
+
const predicate = row;
|
|
483
|
+
return {
|
|
484
|
+
rowId: nextId(),
|
|
485
|
+
field: predicate.field,
|
|
486
|
+
op: predicate.op,
|
|
487
|
+
...Object.hasOwn(predicate, "value") ? { value: predicate.value } : {}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function fromDraft(draft) {
|
|
491
|
+
return {
|
|
492
|
+
join: draft.join,
|
|
493
|
+
rows: draft.rows.map((node) => isDraftGroup(node) ? fromDraft(node) : {
|
|
494
|
+
field: node.field,
|
|
495
|
+
op: node.op,
|
|
496
|
+
...Object.hasOwn(node, "value") ? { value: node.value } : {}
|
|
497
|
+
})
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function pruneIncompleteRows(group) {
|
|
501
|
+
const rows = [];
|
|
502
|
+
for (const row of group.rows) if (isFilterGroup(row)) {
|
|
503
|
+
const pruned = pruneIncompleteRows(row);
|
|
504
|
+
if (pruned.rows.length > 0) rows.push(pruned);
|
|
505
|
+
} else if (isCompletePredicate(row)) rows.push(row);
|
|
506
|
+
return {
|
|
507
|
+
join: group.join,
|
|
508
|
+
rows
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function mapGroupAt(draft, path, replace) {
|
|
512
|
+
if (path.length === 0) return replace(draft);
|
|
513
|
+
const [head, ...rest] = path;
|
|
514
|
+
const child = draft.rows[head];
|
|
515
|
+
if (child === void 0 || !isDraftGroup(child)) return draft;
|
|
516
|
+
const next = mapGroupAt(child, rest, replace);
|
|
517
|
+
if (next === child) return draft;
|
|
518
|
+
const rows = draft.rows.slice();
|
|
519
|
+
rows[head] = next;
|
|
520
|
+
return {
|
|
521
|
+
...draft,
|
|
522
|
+
rows
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function depthOf(path) {
|
|
526
|
+
return path.length + 1;
|
|
527
|
+
}
|
|
528
|
+
function defaultValueForOp(op) {
|
|
529
|
+
const arity = opArity(op);
|
|
530
|
+
if (arity === "none") return void 0;
|
|
531
|
+
if (arity === "multi") return [];
|
|
532
|
+
return "";
|
|
533
|
+
}
|
|
534
|
+
function addRow(draft, path, available) {
|
|
535
|
+
const spec = available[0];
|
|
536
|
+
if (!spec) return draft;
|
|
537
|
+
const op = spec.operators[0] ?? "eq";
|
|
538
|
+
const value = defaultValueForOp(op);
|
|
539
|
+
const row = {
|
|
540
|
+
rowId: nextId(),
|
|
541
|
+
field: spec.field,
|
|
542
|
+
op,
|
|
543
|
+
...value === void 0 ? {} : { value }
|
|
544
|
+
};
|
|
545
|
+
return mapGroupAt(draft, path, (group) => ({
|
|
546
|
+
...group,
|
|
547
|
+
rows: [...group.rows, row]
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
550
|
+
function addGroup(draft, path) {
|
|
551
|
+
if (depthOf(path) >= 3) return draft;
|
|
552
|
+
const group = {
|
|
553
|
+
rowId: nextId(),
|
|
554
|
+
join: "and",
|
|
555
|
+
rows: []
|
|
556
|
+
};
|
|
557
|
+
return mapGroupAt(draft, path, (parent) => ({
|
|
558
|
+
...parent,
|
|
559
|
+
rows: [...parent.rows, group]
|
|
560
|
+
}));
|
|
561
|
+
}
|
|
562
|
+
function removeAt(draft, path) {
|
|
563
|
+
if (path.length === 0) return draft;
|
|
564
|
+
const parentPath = path.slice(0, -1);
|
|
565
|
+
const index = path[path.length - 1];
|
|
566
|
+
return mapGroupAt(draft, parentPath, (group) => {
|
|
567
|
+
if (group.rows[index] === void 0) return group;
|
|
568
|
+
const rows = group.rows.slice();
|
|
569
|
+
rows.splice(index, 1);
|
|
570
|
+
return {
|
|
571
|
+
...group,
|
|
572
|
+
rows
|
|
573
|
+
};
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
function setJoin(draft, path, join) {
|
|
577
|
+
return mapGroupAt(draft, path, (group) => group.join === join ? group : {
|
|
578
|
+
...group,
|
|
579
|
+
join
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
function updateRow(draft, path, patch) {
|
|
583
|
+
if (path.length === 0) return draft;
|
|
584
|
+
const parentPath = path.slice(0, -1);
|
|
585
|
+
const index = path[path.length - 1];
|
|
586
|
+
return mapGroupAt(draft, parentPath, (group) => {
|
|
587
|
+
const current = group.rows[index];
|
|
588
|
+
if (current === void 0 || isDraftGroup(current)) return group;
|
|
589
|
+
const fieldChanged = patch.field !== void 0 && patch.field !== current.field;
|
|
590
|
+
const op = patch.op ?? current.op;
|
|
591
|
+
let value;
|
|
592
|
+
if (fieldChanged) value = defaultValueForOp(op);
|
|
593
|
+
else if (patch.op !== void 0 && patch.op !== current.op) value = convertValueForOp(current.value, current.op, patch.op);
|
|
594
|
+
else value = Object.hasOwn(patch, "value") ? patch.value : current.value;
|
|
595
|
+
const next = {
|
|
596
|
+
rowId: current.rowId,
|
|
597
|
+
field: patch.field ?? current.field,
|
|
598
|
+
op,
|
|
599
|
+
...value === void 0 ? {} : { value }
|
|
600
|
+
};
|
|
601
|
+
const rows = group.rows.slice();
|
|
602
|
+
rows[index] = next;
|
|
603
|
+
return {
|
|
604
|
+
...group,
|
|
605
|
+
rows
|
|
606
|
+
};
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
const EMPTY_DRAFT = () => toDraft(EMPTY_FILTER_GROUP);
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region src/list-kit/filter-builder/values/use-selectable-value-control.ts
|
|
612
|
+
function useSelectableValueControl(options) {
|
|
613
|
+
const { value, arity, onChange, emitSingle = (token) => token } = options;
|
|
614
|
+
const [open, setOpen] = useState(false);
|
|
615
|
+
const selected = arity === "multi" ? new Set(Array.isArray(value) ? value.map(String) : []) : new Set(value === void 0 || value === null ? [] : [String(value)]);
|
|
616
|
+
const select = (token) => {
|
|
617
|
+
if (arity === "single") {
|
|
618
|
+
onChange(emitSingle(token));
|
|
619
|
+
setOpen(false);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const next = new Set(selected);
|
|
623
|
+
if (next.has(token)) next.delete(token);
|
|
624
|
+
else next.add(token);
|
|
625
|
+
onChange(next.size === 0 ? void 0 : Array.from(next));
|
|
626
|
+
};
|
|
627
|
+
const triggerLabel = (labelOf, placeholder) => {
|
|
628
|
+
const picked = Array.from(selected);
|
|
629
|
+
return picked.length === 0 ? placeholder : picked.map(labelOf).join(", ");
|
|
630
|
+
};
|
|
631
|
+
const clear = () => {
|
|
632
|
+
onChange(void 0);
|
|
633
|
+
setOpen(false);
|
|
634
|
+
};
|
|
635
|
+
return {
|
|
636
|
+
open,
|
|
637
|
+
setOpen,
|
|
638
|
+
selected,
|
|
639
|
+
select,
|
|
640
|
+
triggerLabel,
|
|
641
|
+
clear
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
//#endregion
|
|
645
|
+
//#region src/list-kit/filter-builder/values/boolean-value-control.tsx
|
|
646
|
+
const OPTIONS = ["true", "false"];
|
|
647
|
+
function BooleanValueControl(props) {
|
|
648
|
+
const { value, onChange, testId, arity, autoFocus, fieldLabel, fill } = props;
|
|
649
|
+
const t = useOptionalT();
|
|
650
|
+
const ariaLabel = fieldLabel ? t("filters.valueFor", {
|
|
651
|
+
defaultValue: `Value for ${fieldLabel}`,
|
|
652
|
+
params: { field: fieldLabel }
|
|
653
|
+
}) : t("filters.value", { defaultValue: "Value" });
|
|
654
|
+
const yesLabel = t("boolean.yes", { defaultValue: "Yes" });
|
|
655
|
+
const noLabel = t("boolean.no", { defaultValue: "No" });
|
|
656
|
+
const labelOf = (token) => token === "true" ? yesLabel : noLabel;
|
|
657
|
+
const clearLabel = t("filters.clear", { defaultValue: "Clear" });
|
|
658
|
+
const { open, setOpen, selected, select, triggerLabel, clear } = useSelectableValueControl({
|
|
659
|
+
value,
|
|
660
|
+
arity,
|
|
661
|
+
onChange,
|
|
662
|
+
emitSingle: (token) => token === "true"
|
|
663
|
+
});
|
|
664
|
+
return jsxs(Popover, {
|
|
665
|
+
open,
|
|
666
|
+
onOpenChange: setOpen,
|
|
667
|
+
children: [jsx(PopoverTrigger, {
|
|
668
|
+
asChild: true,
|
|
669
|
+
children: jsx(Button, {
|
|
670
|
+
type: "button",
|
|
671
|
+
variant: "outline",
|
|
672
|
+
size: "sm",
|
|
673
|
+
autoFocus,
|
|
674
|
+
"data-testid": testId,
|
|
675
|
+
"aria-label": ariaLabel,
|
|
676
|
+
className: cn("h-8 justify-between", fill && "w-full"),
|
|
677
|
+
children: triggerLabel(labelOf, ariaLabel)
|
|
678
|
+
})
|
|
679
|
+
}), jsx(PopoverContent, {
|
|
680
|
+
className: "w-[140px] p-0",
|
|
681
|
+
align: "start",
|
|
682
|
+
children: jsx(Command, { children: jsx(CommandList, { children: jsxs(CommandGroup, { children: [arity === "single" && selected.size > 0 && jsxs(CommandItem, {
|
|
683
|
+
"data-testid": `${testId}:clear`,
|
|
684
|
+
value: "__clear__",
|
|
685
|
+
onSelect: () => clear(),
|
|
686
|
+
children: [jsx(IconX, {
|
|
687
|
+
className: "mr-2 h-4 w-4",
|
|
688
|
+
"aria-hidden": "true"
|
|
689
|
+
}), clearLabel]
|
|
690
|
+
}), OPTIONS.map((token) => {
|
|
691
|
+
const isSelected = selected.has(token);
|
|
692
|
+
return jsxs(CommandItem, {
|
|
693
|
+
value: labelOf(token),
|
|
694
|
+
"data-testid": `${testId}:option:${token}`,
|
|
695
|
+
onSelect: () => select(token),
|
|
696
|
+
children: [jsx(IconCheck, {
|
|
697
|
+
className: cn("mr-2 h-4 w-4", isSelected ? "opacity-100" : "opacity-0"),
|
|
698
|
+
"aria-hidden": "true"
|
|
699
|
+
}), labelOf(token)]
|
|
700
|
+
}, token);
|
|
701
|
+
})] }) }) })
|
|
702
|
+
})]
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
//#endregion
|
|
706
|
+
//#region src/list-kit/filter-builder/values/select-list-value-control.tsx
|
|
707
|
+
function SelectListValueControl(props) {
|
|
708
|
+
const { options, value, onChange, testId, arity, autoFocus, fieldLabel, isLoading = false } = props;
|
|
709
|
+
const t = useOptionalT();
|
|
710
|
+
const ariaLabel = fieldLabel ? t("filters.valueFor", {
|
|
711
|
+
defaultValue: `Value for ${fieldLabel}`,
|
|
712
|
+
params: { field: fieldLabel }
|
|
713
|
+
}) : t("filters.value", { defaultValue: "Value" });
|
|
714
|
+
const loadingLabel = t("table.loading", { defaultValue: "Loading…" });
|
|
715
|
+
const clearLabel = t("filters.clear", { defaultValue: "Clear" });
|
|
716
|
+
const labelFor = props.labelFor ?? ((raw) => options.find((o) => o.value === raw)?.label ?? raw);
|
|
717
|
+
const { open, setOpen, selected, select, triggerLabel, clear } = useSelectableValueControl({
|
|
718
|
+
value,
|
|
719
|
+
arity,
|
|
720
|
+
onChange
|
|
721
|
+
});
|
|
722
|
+
return jsxs(Popover, {
|
|
723
|
+
open,
|
|
724
|
+
onOpenChange: setOpen,
|
|
725
|
+
children: [jsx(PopoverTrigger, {
|
|
726
|
+
asChild: true,
|
|
727
|
+
children: jsx(Button, {
|
|
728
|
+
type: "button",
|
|
729
|
+
variant: "outline",
|
|
730
|
+
size: "sm",
|
|
731
|
+
autoFocus,
|
|
732
|
+
"data-testid": testId,
|
|
733
|
+
"aria-label": ariaLabel,
|
|
734
|
+
className: cn("h-8 justify-between truncate", props.fill ? "w-full" : "max-w-[200px]"),
|
|
735
|
+
children: jsx("span", {
|
|
736
|
+
className: "truncate",
|
|
737
|
+
children: isLoading ? loadingLabel : triggerLabel(labelFor, ariaLabel)
|
|
738
|
+
})
|
|
739
|
+
})
|
|
740
|
+
}), jsx(PopoverContent, {
|
|
741
|
+
className: "w-[220px] p-0",
|
|
742
|
+
align: "start",
|
|
743
|
+
children: jsxs(Command, { children: [jsx(CommandInput, {
|
|
744
|
+
placeholder: ariaLabel,
|
|
745
|
+
"data-testid": `${testId}:search`
|
|
746
|
+
}), jsxs(CommandList, { children: [
|
|
747
|
+
isLoading && jsxs("div", {
|
|
748
|
+
className: "flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground",
|
|
749
|
+
"data-testid": `${testId}:loading`,
|
|
750
|
+
children: [jsx(IconLoader2, {
|
|
751
|
+
className: "h-4 w-4 animate-spin",
|
|
752
|
+
"aria-hidden": "true"
|
|
753
|
+
}), loadingLabel]
|
|
754
|
+
}),
|
|
755
|
+
!isLoading && jsx(CommandEmpty, { children: t("relation.noResults", { defaultValue: "No results." }) }),
|
|
756
|
+
!isLoading && jsxs(CommandGroup, { children: [arity === "single" && selected.size > 0 && jsxs(CommandItem, {
|
|
757
|
+
"data-testid": `${testId}:clear`,
|
|
758
|
+
value: "__clear__",
|
|
759
|
+
onSelect: () => clear(),
|
|
760
|
+
children: [jsx(IconX, {
|
|
761
|
+
className: "mr-2 h-4 w-4",
|
|
762
|
+
"aria-hidden": "true"
|
|
763
|
+
}), clearLabel]
|
|
764
|
+
}), options.map((entry) => {
|
|
765
|
+
const isSelected = selected.has(entry.value);
|
|
766
|
+
const label = labelFor(entry.value);
|
|
767
|
+
return jsxs(CommandItem, {
|
|
768
|
+
value: label,
|
|
769
|
+
"data-testid": `${testId}:option:${entry.value}`,
|
|
770
|
+
onSelect: () => select(entry.value),
|
|
771
|
+
children: [jsx(IconCheck, {
|
|
772
|
+
className: cn("mr-2 h-4 w-4", isSelected ? "opacity-100" : "opacity-0"),
|
|
773
|
+
"aria-hidden": "true"
|
|
774
|
+
}), label]
|
|
775
|
+
}, entry.value);
|
|
776
|
+
})] })
|
|
777
|
+
] })] })
|
|
778
|
+
})]
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
//#endregion
|
|
782
|
+
//#region src/list-kit/filter-builder/values/enum-value-control.tsx
|
|
783
|
+
function EnumValueControl(props) {
|
|
784
|
+
const { enumName, value, onChange, testId, arity, autoFocus, fieldLabel, fill } = props;
|
|
785
|
+
const caps = useI18nCaps();
|
|
786
|
+
const def = useApp().registry.enums.get(enumName);
|
|
787
|
+
const { values, isLoading } = useEnumValues(enumName);
|
|
788
|
+
const labelFor = (raw) => resolveEnumLabel(caps, enumName, raw, enumValueText(def, raw) ?? values?.find((v) => v.value === raw)?.label);
|
|
789
|
+
const options = (values ?? []).map((entry) => ({
|
|
790
|
+
value: entry.value,
|
|
791
|
+
label: entry.value
|
|
792
|
+
}));
|
|
793
|
+
return jsx(SelectListValueControl, {
|
|
794
|
+
value,
|
|
795
|
+
onChange,
|
|
796
|
+
testId,
|
|
797
|
+
arity,
|
|
798
|
+
autoFocus,
|
|
799
|
+
fieldLabel,
|
|
800
|
+
options,
|
|
801
|
+
labelFor,
|
|
802
|
+
isLoading,
|
|
803
|
+
fill
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
//#endregion
|
|
807
|
+
//#region src/list-kit/use-related-labels.ts
|
|
808
|
+
function useRelatedLabels(to, ids, by) {
|
|
809
|
+
const targetDef = useApp().registry.get(to);
|
|
810
|
+
const idField = (targetDef?.data)?.idField;
|
|
811
|
+
const rowTitle = useRowTitle(to, targetDef?.displayField);
|
|
812
|
+
const field = by ?? (typeof idField === "string" && idField.length > 0 ? idField : "id");
|
|
813
|
+
const query = useList(to, {
|
|
814
|
+
pagination: {
|
|
815
|
+
kind: "offset",
|
|
816
|
+
page: 1,
|
|
817
|
+
pageSize: Math.min(Math.max(1, ids.length), 100)
|
|
818
|
+
},
|
|
819
|
+
...ids.length ? { filter: {
|
|
820
|
+
field,
|
|
821
|
+
op: "in",
|
|
822
|
+
value: ids
|
|
823
|
+
} } : {},
|
|
824
|
+
scope: "none",
|
|
825
|
+
enabled: ids.length > 0
|
|
826
|
+
});
|
|
827
|
+
return useMemo(() => {
|
|
828
|
+
const out = {};
|
|
829
|
+
for (const row of query.data?.data ?? []) {
|
|
830
|
+
const key = row[field];
|
|
831
|
+
if (key !== null && key !== void 0) out[String(key)] = rowTitle(row);
|
|
832
|
+
}
|
|
833
|
+
return out;
|
|
834
|
+
}, [
|
|
835
|
+
query.data,
|
|
836
|
+
rowTitle,
|
|
837
|
+
field
|
|
838
|
+
]);
|
|
839
|
+
}
|
|
840
|
+
//#endregion
|
|
841
|
+
//#region src/list-kit/filter-builder/values/relation-value-control.tsx
|
|
842
|
+
function RelationValueControl(props) {
|
|
843
|
+
const { to, by, value, onChange, testId, arity, autoFocus, fieldLabel } = props;
|
|
844
|
+
const app = useApp();
|
|
845
|
+
const t = useOptionalT();
|
|
846
|
+
const targetDef = app.registry.get(to);
|
|
847
|
+
const displayField = targetDef?.displayField ?? "name";
|
|
848
|
+
const [search, setSearch] = useState("");
|
|
849
|
+
const { open, setOpen, selected, select, triggerLabel, clear } = useSelectableValueControl({
|
|
850
|
+
value,
|
|
851
|
+
arity,
|
|
852
|
+
onChange
|
|
853
|
+
});
|
|
854
|
+
const rowTitle = useRowTitle(to, targetDef?.displayField);
|
|
855
|
+
const access = useListAccess(to);
|
|
856
|
+
const searchable = useMemo(() => access.status === "ready" && narrowSearchDecl({ fields: [displayField] }, (name) => access.fieldVisible(name)) !== void 0, [access, displayField]);
|
|
857
|
+
const listQuery = useList(to, {
|
|
858
|
+
pagination: {
|
|
859
|
+
kind: "offset",
|
|
860
|
+
page: 1,
|
|
861
|
+
pageSize: 10
|
|
862
|
+
},
|
|
863
|
+
...searchable && search.trim() ? { filter: {
|
|
864
|
+
field: displayField,
|
|
865
|
+
op: "contains",
|
|
866
|
+
value: search.trim()
|
|
867
|
+
} } : {},
|
|
868
|
+
scope: "session",
|
|
869
|
+
enabled: open && access.status === "ready"
|
|
870
|
+
});
|
|
871
|
+
const rows = listQuery.data?.data ?? [];
|
|
872
|
+
const fallbackLabels = useRelatedLabels(to, Array.from(selected), by);
|
|
873
|
+
const idField = (targetDef?.data)?.idField;
|
|
874
|
+
const valueField = by ?? (typeof idField === "string" && idField.length > 0 ? idField : "id");
|
|
875
|
+
const keyOf = (row) => {
|
|
876
|
+
const key = row[valueField];
|
|
877
|
+
return key === null || key === void 0 || key === "" ? void 0 : String(key);
|
|
878
|
+
};
|
|
879
|
+
const labelFor = (id) => {
|
|
880
|
+
const fromRows = rows.find((r) => keyOf(r) === id);
|
|
881
|
+
if (fromRows) return rowTitle(fromRows);
|
|
882
|
+
return fallbackLabels[id] ?? id;
|
|
883
|
+
};
|
|
884
|
+
const ariaLabel = fieldLabel ? t("filters.valueFor", {
|
|
885
|
+
defaultValue: `Value for ${fieldLabel}`,
|
|
886
|
+
params: { field: fieldLabel }
|
|
887
|
+
}) : t("filters.value", { defaultValue: "Value" });
|
|
888
|
+
const clearLabel = t("filters.clear", { defaultValue: "Clear" });
|
|
889
|
+
return jsxs(Popover, {
|
|
890
|
+
open,
|
|
891
|
+
onOpenChange: setOpen,
|
|
892
|
+
children: [jsx(PopoverTrigger, {
|
|
893
|
+
asChild: true,
|
|
894
|
+
children: jsx(Button, {
|
|
895
|
+
type: "button",
|
|
896
|
+
variant: "outline",
|
|
897
|
+
size: "sm",
|
|
898
|
+
autoFocus,
|
|
899
|
+
"data-testid": testId,
|
|
900
|
+
"aria-label": ariaLabel,
|
|
901
|
+
className: cn("h-8 justify-between truncate", props.fill ? "w-full" : "max-w-[220px]"),
|
|
902
|
+
children: jsx("span", {
|
|
903
|
+
className: "truncate",
|
|
904
|
+
children: triggerLabel(labelFor, ariaLabel)
|
|
905
|
+
})
|
|
906
|
+
})
|
|
907
|
+
}), jsx(PopoverContent, {
|
|
908
|
+
className: "w-[240px] p-0",
|
|
909
|
+
align: "start",
|
|
910
|
+
children: jsxs(Command, {
|
|
911
|
+
shouldFilter: false,
|
|
912
|
+
children: [searchable && jsx(CommandInput, {
|
|
913
|
+
value: search,
|
|
914
|
+
onValueChange: setSearch,
|
|
915
|
+
placeholder: ariaLabel,
|
|
916
|
+
"data-testid": `${testId}:search`
|
|
917
|
+
}), jsxs(CommandList, { children: [
|
|
918
|
+
listQuery.isLoading && jsxs("div", {
|
|
919
|
+
className: "flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground",
|
|
920
|
+
"data-testid": `${testId}:loading`,
|
|
921
|
+
children: [jsx(IconLoader2, {
|
|
922
|
+
className: "h-4 w-4 animate-spin",
|
|
923
|
+
"aria-hidden": "true"
|
|
924
|
+
}), t("table.loading", { defaultValue: "Loading…" })]
|
|
925
|
+
}),
|
|
926
|
+
!listQuery.isLoading && rows.length === 0 && jsx(CommandEmpty, { children: t("relation.noResults", { defaultValue: "No results." }) }),
|
|
927
|
+
!listQuery.isLoading && arity === "single" && selected.size > 0 && jsxs(CommandItem, {
|
|
928
|
+
"data-testid": `${testId}:clear`,
|
|
929
|
+
value: "__clear__",
|
|
930
|
+
onSelect: () => clear(),
|
|
931
|
+
children: [jsx(IconX, {
|
|
932
|
+
className: "mr-2 h-4 w-4",
|
|
933
|
+
"aria-hidden": "true"
|
|
934
|
+
}), clearLabel]
|
|
935
|
+
}),
|
|
936
|
+
!listQuery.isLoading && rows.map((row) => {
|
|
937
|
+
const id = keyOf(row);
|
|
938
|
+
if (id === void 0) return null;
|
|
939
|
+
const label = rowTitle(row);
|
|
940
|
+
const isSelected = selected.has(id);
|
|
941
|
+
return jsxs(CommandItem, {
|
|
942
|
+
value: `${label} ${id}`,
|
|
943
|
+
"data-testid": `${testId}:option:${id}`,
|
|
944
|
+
onSelect: () => select(id),
|
|
945
|
+
children: [jsx(IconCheck, {
|
|
946
|
+
className: cn("mr-2 h-4 w-4", isSelected ? "opacity-100" : "opacity-0"),
|
|
947
|
+
"aria-hidden": "true"
|
|
948
|
+
}), jsx("span", {
|
|
949
|
+
className: "truncate",
|
|
950
|
+
children: label
|
|
951
|
+
})]
|
|
952
|
+
}, id);
|
|
953
|
+
})
|
|
954
|
+
] })]
|
|
955
|
+
})
|
|
956
|
+
})]
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
//#endregion
|
|
960
|
+
//#region src/list-kit/filter-builder/values/scalar-value-input.tsx
|
|
961
|
+
function isDateTime(kind) {
|
|
962
|
+
return kind.kind === "date" && kind.time === true;
|
|
963
|
+
}
|
|
964
|
+
function inputTypeFor(kind) {
|
|
965
|
+
switch (kind.kind) {
|
|
966
|
+
case "number": return "number";
|
|
967
|
+
case "date": return isDateTime(kind) ? "datetime-local" : "date";
|
|
968
|
+
default: return "text";
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
function toText(kind, value) {
|
|
972
|
+
if (value === void 0 || value === null || value === "") return "";
|
|
973
|
+
if (!isDateTime(kind)) return String(value);
|
|
974
|
+
const d = new Date(String(value));
|
|
975
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
976
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
977
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
978
|
+
}
|
|
979
|
+
function parseOne(kind, raw) {
|
|
980
|
+
if (raw === "") return void 0;
|
|
981
|
+
if (kind.kind === "number") {
|
|
982
|
+
const n = Number(raw);
|
|
983
|
+
return Number.isFinite(n) ? n : void 0;
|
|
984
|
+
}
|
|
985
|
+
if (isDateTime(kind)) {
|
|
986
|
+
const d = new Date(raw);
|
|
987
|
+
return Number.isNaN(d.getTime()) ? void 0 : d.toISOString();
|
|
988
|
+
}
|
|
989
|
+
return raw;
|
|
990
|
+
}
|
|
991
|
+
function ScalarValueInput(props) {
|
|
992
|
+
const t = useOptionalT();
|
|
993
|
+
const inputType = inputTypeFor(props.kind);
|
|
994
|
+
const ariaLabel = props.fieldLabel ? t("filters.valueFor", {
|
|
995
|
+
defaultValue: `Value for ${props.fieldLabel}`,
|
|
996
|
+
params: { field: props.fieldLabel }
|
|
997
|
+
}) : t("filters.value", { defaultValue: "Value" });
|
|
998
|
+
if (props.arity === "multi") return jsx(ScalarMultiInput, {
|
|
999
|
+
...props,
|
|
1000
|
+
inputType,
|
|
1001
|
+
ariaLabel
|
|
1002
|
+
});
|
|
1003
|
+
return jsx(ScalarSingleInput, {
|
|
1004
|
+
...props,
|
|
1005
|
+
inputType,
|
|
1006
|
+
ariaLabel
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
function ScalarSingleInput(props) {
|
|
1010
|
+
const { value, onChange, kind, testId, autoFocus, inputType, ariaLabel, debounceMs = 300 } = props;
|
|
1011
|
+
const storedText = toText(kind, value);
|
|
1012
|
+
const [text, setText] = useState(storedText);
|
|
1013
|
+
const lastPushedRef = useRef(storedText);
|
|
1014
|
+
const timerRef = useRef(null);
|
|
1015
|
+
const pendingRawRef = useRef(null);
|
|
1016
|
+
const commitRef = useRef(() => {});
|
|
1017
|
+
useEffect(() => {
|
|
1018
|
+
commitRef.current = (raw) => {
|
|
1019
|
+
const parsed = parseOne(kind, raw);
|
|
1020
|
+
lastPushedRef.current = toText(kind, parsed);
|
|
1021
|
+
pendingRawRef.current = null;
|
|
1022
|
+
onChange(parsed);
|
|
1023
|
+
};
|
|
1024
|
+
});
|
|
1025
|
+
useEffect(() => {
|
|
1026
|
+
if (storedText !== lastPushedRef.current) {
|
|
1027
|
+
setText(storedText);
|
|
1028
|
+
lastPushedRef.current = storedText;
|
|
1029
|
+
if (timerRef.current) {
|
|
1030
|
+
clearTimeout(timerRef.current);
|
|
1031
|
+
timerRef.current = null;
|
|
1032
|
+
}
|
|
1033
|
+
pendingRawRef.current = null;
|
|
1034
|
+
}
|
|
1035
|
+
}, [storedText]);
|
|
1036
|
+
useEffect(() => () => {
|
|
1037
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
1038
|
+
if (pendingRawRef.current !== null) commitRef.current(pendingRawRef.current);
|
|
1039
|
+
}, []);
|
|
1040
|
+
const handleChange = (raw) => {
|
|
1041
|
+
setText(raw);
|
|
1042
|
+
if (timerRef.current) {
|
|
1043
|
+
clearTimeout(timerRef.current);
|
|
1044
|
+
timerRef.current = null;
|
|
1045
|
+
}
|
|
1046
|
+
if (debounceMs <= 0) {
|
|
1047
|
+
pendingRawRef.current = null;
|
|
1048
|
+
commitRef.current(raw);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
pendingRawRef.current = raw;
|
|
1052
|
+
timerRef.current = setTimeout(() => commitRef.current(raw), debounceMs);
|
|
1053
|
+
};
|
|
1054
|
+
return jsx(Input, {
|
|
1055
|
+
type: inputType,
|
|
1056
|
+
value: text,
|
|
1057
|
+
autoFocus,
|
|
1058
|
+
"aria-label": ariaLabel,
|
|
1059
|
+
placeholder: props.placeholder,
|
|
1060
|
+
"data-testid": testId,
|
|
1061
|
+
onChange: (event) => handleChange(event.target.value),
|
|
1062
|
+
className: cn("h-8", props.fill ? "w-full" : "w-[150px]")
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
function ScalarMultiInput(props) {
|
|
1066
|
+
const { value, onChange, kind, testId, autoFocus, inputType, ariaLabel } = props;
|
|
1067
|
+
const chips = Array.isArray(value) ? value.map((v) => String(v)) : [];
|
|
1068
|
+
const [draft, setDraft] = useState("");
|
|
1069
|
+
const addChip = () => {
|
|
1070
|
+
const parsed = parseOne(kind, draft.trim());
|
|
1071
|
+
if (parsed === void 0) return;
|
|
1072
|
+
const text = String(parsed);
|
|
1073
|
+
setDraft("");
|
|
1074
|
+
if (chips.includes(text)) return;
|
|
1075
|
+
onChange([...chips, text]);
|
|
1076
|
+
};
|
|
1077
|
+
const removeChip = (chip) => {
|
|
1078
|
+
const next = chips.filter((c) => c !== chip);
|
|
1079
|
+
onChange(next.length === 0 ? void 0 : next);
|
|
1080
|
+
};
|
|
1081
|
+
return jsxs("div", {
|
|
1082
|
+
className: cn("flex flex-wrap items-center gap-1", props.fill && "w-full"),
|
|
1083
|
+
children: [chips.map((chip) => jsxs(Badge, {
|
|
1084
|
+
variant: "secondary",
|
|
1085
|
+
"data-testid": `${testId}:chip:${chip}`,
|
|
1086
|
+
className: "gap-1 rounded-sm px-1 font-normal",
|
|
1087
|
+
children: [chip, jsx("button", {
|
|
1088
|
+
type: "button",
|
|
1089
|
+
"data-testid": `${testId}:chip:${chip}:remove`,
|
|
1090
|
+
"aria-label": `Remove ${chip}`,
|
|
1091
|
+
onClick: () => removeChip(chip),
|
|
1092
|
+
className: "rounded-sm hover:bg-accent",
|
|
1093
|
+
children: jsx(IconX, {
|
|
1094
|
+
className: "h-3 w-3",
|
|
1095
|
+
"aria-hidden": "true"
|
|
1096
|
+
})
|
|
1097
|
+
})]
|
|
1098
|
+
}, chip)), jsx(Input, {
|
|
1099
|
+
type: inputType,
|
|
1100
|
+
value: draft,
|
|
1101
|
+
autoFocus,
|
|
1102
|
+
"aria-label": ariaLabel,
|
|
1103
|
+
placeholder: props.placeholder,
|
|
1104
|
+
"data-testid": testId,
|
|
1105
|
+
onChange: (event) => setDraft(event.target.value),
|
|
1106
|
+
onKeyDown: (event) => {
|
|
1107
|
+
if (event.key !== "Enter") return;
|
|
1108
|
+
event.preventDefault();
|
|
1109
|
+
addChip();
|
|
1110
|
+
},
|
|
1111
|
+
className: cn("h-8", props.fill ? "min-w-[8rem] flex-1" : "w-[120px]")
|
|
1112
|
+
})]
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
//#endregion
|
|
1116
|
+
//#region src/list-kit/filter-builder/values/filter-value-control.tsx
|
|
1117
|
+
function FilterValueControl(props) {
|
|
1118
|
+
const arity = opArity(props.op);
|
|
1119
|
+
if (arity === "none") return null;
|
|
1120
|
+
if (props.options && props.options.length > 0) return jsx(SelectListValueControl, {
|
|
1121
|
+
value: props.value,
|
|
1122
|
+
onChange: props.onChange,
|
|
1123
|
+
testId: props.testId,
|
|
1124
|
+
arity,
|
|
1125
|
+
autoFocus: props.autoFocus,
|
|
1126
|
+
fieldLabel: props.fieldLabel,
|
|
1127
|
+
options: props.options,
|
|
1128
|
+
fill: props.fill
|
|
1129
|
+
});
|
|
1130
|
+
switch (props.kind.kind) {
|
|
1131
|
+
case "enum": return jsx(EnumValueControl, {
|
|
1132
|
+
...props,
|
|
1133
|
+
enumName: props.kind.enumName,
|
|
1134
|
+
arity
|
|
1135
|
+
});
|
|
1136
|
+
case "enum-values": return jsx(SelectListValueControl, {
|
|
1137
|
+
value: props.value,
|
|
1138
|
+
onChange: props.onChange,
|
|
1139
|
+
testId: props.testId,
|
|
1140
|
+
arity,
|
|
1141
|
+
autoFocus: props.autoFocus,
|
|
1142
|
+
fieldLabel: props.fieldLabel,
|
|
1143
|
+
options: props.kind.values.map((entry) => ({
|
|
1144
|
+
value: entry,
|
|
1145
|
+
label: entry
|
|
1146
|
+
}))
|
|
1147
|
+
});
|
|
1148
|
+
case "relation": return jsx(RelationValueControl, {
|
|
1149
|
+
...props,
|
|
1150
|
+
to: props.kind.to,
|
|
1151
|
+
...props.kind.by === void 0 ? {} : { by: props.kind.by },
|
|
1152
|
+
arity
|
|
1153
|
+
});
|
|
1154
|
+
case "boolean": return jsx(BooleanValueControl, {
|
|
1155
|
+
...props,
|
|
1156
|
+
arity
|
|
1157
|
+
});
|
|
1158
|
+
default: return jsx(ScalarValueInput, {
|
|
1159
|
+
...props,
|
|
1160
|
+
arity
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
//#endregion
|
|
1165
|
+
//#region src/list-kit/filter-builder/filter-row.tsx
|
|
1166
|
+
const ROW_CLASS = "flex min-w-0 items-center gap-2";
|
|
1167
|
+
const FIELD_CLASS = "h-8 min-w-0 shrink basis-40";
|
|
1168
|
+
const OPERATOR_CLASS = "h-8 min-w-0 shrink basis-[9.5rem]";
|
|
1169
|
+
function FilterRow(props) {
|
|
1170
|
+
const { row, path, available, onPatch, onRemove, testIdBase, debounceMs } = props;
|
|
1171
|
+
const t = useOptionalT();
|
|
1172
|
+
const base = `${testIdBase}:row:${path.join("-")}`;
|
|
1173
|
+
const spec = available.find((s) => s.field === row.field);
|
|
1174
|
+
const removeLabel = t("filters.removeRow", { defaultValue: "Remove filter" });
|
|
1175
|
+
if (!spec) return jsxs("div", {
|
|
1176
|
+
className: ROW_CLASS,
|
|
1177
|
+
children: [
|
|
1178
|
+
jsx(Button, {
|
|
1179
|
+
type: "button",
|
|
1180
|
+
variant: "outline",
|
|
1181
|
+
size: "sm",
|
|
1182
|
+
disabled: true,
|
|
1183
|
+
"data-testid": `${base}:field`,
|
|
1184
|
+
className: cn(FIELD_CLASS, "justify-start truncate opacity-60"),
|
|
1185
|
+
children: row.field
|
|
1186
|
+
}),
|
|
1187
|
+
jsx("div", { className: "flex-1" }),
|
|
1188
|
+
jsx(Button, {
|
|
1189
|
+
type: "button",
|
|
1190
|
+
variant: "ghost",
|
|
1191
|
+
size: "icon",
|
|
1192
|
+
"aria-label": removeLabel,
|
|
1193
|
+
"data-testid": `${base}:remove`,
|
|
1194
|
+
onClick: () => onRemove(path),
|
|
1195
|
+
className: "shrink-0",
|
|
1196
|
+
children: jsx(IconX, {
|
|
1197
|
+
className: "h-4 w-4",
|
|
1198
|
+
"aria-hidden": "true"
|
|
1199
|
+
})
|
|
1200
|
+
})
|
|
1201
|
+
]
|
|
1202
|
+
});
|
|
1203
|
+
const fieldTestId = `${base}:field`;
|
|
1204
|
+
const opTestId = `${base}:op`;
|
|
1205
|
+
const handleFieldChange = (field) => {
|
|
1206
|
+
const op = available.find((s) => s.field === field)?.operators[0] ?? row.op;
|
|
1207
|
+
onPatch(path, {
|
|
1208
|
+
field,
|
|
1209
|
+
op
|
|
1210
|
+
});
|
|
1211
|
+
};
|
|
1212
|
+
const handleOpChange = (op) => onPatch(path, { op });
|
|
1213
|
+
const handleValueChange = (value) => onPatch(path, { value });
|
|
1214
|
+
const fieldAriaLabel = t("filters.field", { defaultValue: "Field" });
|
|
1215
|
+
const operatorAriaLabel = t("filters.operator", { defaultValue: "Operator" });
|
|
1216
|
+
return jsxs("div", {
|
|
1217
|
+
className: ROW_CLASS,
|
|
1218
|
+
children: [
|
|
1219
|
+
jsxs(Select, {
|
|
1220
|
+
value: row.field,
|
|
1221
|
+
onValueChange: handleFieldChange,
|
|
1222
|
+
children: [jsx(SelectTrigger, {
|
|
1223
|
+
"data-testid": fieldTestId,
|
|
1224
|
+
"aria-label": fieldAriaLabel,
|
|
1225
|
+
className: FIELD_CLASS,
|
|
1226
|
+
children: jsx(SelectValue, {})
|
|
1227
|
+
}), jsx(SelectContent, { children: available.map((s) => jsx(SelectItem, {
|
|
1228
|
+
value: s.field,
|
|
1229
|
+
"data-testid": `${fieldTestId}:option:${s.field}`,
|
|
1230
|
+
children: s.label
|
|
1231
|
+
}, s.field)) })]
|
|
1232
|
+
}),
|
|
1233
|
+
jsxs(Select, {
|
|
1234
|
+
value: row.op,
|
|
1235
|
+
onValueChange: handleOpChange,
|
|
1236
|
+
children: [jsx(SelectTrigger, {
|
|
1237
|
+
"data-testid": opTestId,
|
|
1238
|
+
"aria-label": operatorAriaLabel,
|
|
1239
|
+
className: OPERATOR_CLASS,
|
|
1240
|
+
children: jsx(SelectValue, {})
|
|
1241
|
+
}), jsx(SelectContent, { children: spec.operators.map((op) => jsx(SelectItem, {
|
|
1242
|
+
value: op,
|
|
1243
|
+
"data-testid": `${opTestId}:option:${op}`,
|
|
1244
|
+
children: t(`filters.operators.${op}`, { defaultValue: op })
|
|
1245
|
+
}, op)) })]
|
|
1246
|
+
}),
|
|
1247
|
+
jsx("div", {
|
|
1248
|
+
className: "min-w-0 flex-1",
|
|
1249
|
+
children: jsx(FilterValueControl, {
|
|
1250
|
+
kind: spec.kind,
|
|
1251
|
+
op: row.op,
|
|
1252
|
+
value: row.value,
|
|
1253
|
+
onChange: handleValueChange,
|
|
1254
|
+
testId: `${base}:value`,
|
|
1255
|
+
debounceMs,
|
|
1256
|
+
fieldLabel: spec.label,
|
|
1257
|
+
options: spec.options,
|
|
1258
|
+
fill: true
|
|
1259
|
+
})
|
|
1260
|
+
}),
|
|
1261
|
+
jsx(Button, {
|
|
1262
|
+
type: "button",
|
|
1263
|
+
variant: "ghost",
|
|
1264
|
+
size: "icon",
|
|
1265
|
+
"aria-label": removeLabel,
|
|
1266
|
+
"data-testid": `${base}:remove`,
|
|
1267
|
+
onClick: () => onRemove(path),
|
|
1268
|
+
className: "shrink-0",
|
|
1269
|
+
children: jsx(IconX, {
|
|
1270
|
+
className: "h-4 w-4",
|
|
1271
|
+
"aria-hidden": "true"
|
|
1272
|
+
})
|
|
1273
|
+
})
|
|
1274
|
+
]
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
//#endregion
|
|
1278
|
+
//#region src/list-kit/filter-builder/filter-group-editor.tsx
|
|
1279
|
+
const JOIN_VALUES = ["and", "or"];
|
|
1280
|
+
const GROUP_CLASS = "grid grid-cols-[auto_minmax(0,1fr)] items-start gap-2";
|
|
1281
|
+
const CONNECTOR_CLASS = "pt-1.5 text-right text-sm text-muted-foreground";
|
|
1282
|
+
function FilterGroupEditor(props) {
|
|
1283
|
+
const { group, onChange } = props;
|
|
1284
|
+
const addGroupAt = (targetPath) => onChange(addGroup(group, targetPath));
|
|
1285
|
+
return jsx(GroupEditorInner, {
|
|
1286
|
+
...props,
|
|
1287
|
+
addGroupAt
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
function GroupEditorInner(props) {
|
|
1291
|
+
const { group, path, available, onChange, testIdBase, onRemove, addGroupAt, debounceMs } = props;
|
|
1292
|
+
const t = useOptionalT();
|
|
1293
|
+
const base = `${testIdBase}:group:${path.join("-")}`;
|
|
1294
|
+
const canNestGroup = depthOf(path) < 3;
|
|
1295
|
+
const joinLabel = (join) => t(`filters.${join}`, { defaultValue: join });
|
|
1296
|
+
const handlePatch = (rowPath, patch) => {
|
|
1297
|
+
const index = rowPath[rowPath.length - 1];
|
|
1298
|
+
if (index === void 0) return;
|
|
1299
|
+
onChange(updateRow(group, [index], patch));
|
|
1300
|
+
};
|
|
1301
|
+
const handleRemove = (nodePath) => {
|
|
1302
|
+
const index = nodePath[nodePath.length - 1];
|
|
1303
|
+
if (index === void 0) return;
|
|
1304
|
+
onChange(removeAt(group, [index]));
|
|
1305
|
+
};
|
|
1306
|
+
const handleChildGroupChange = (index, next) => {
|
|
1307
|
+
if (next === group.rows[index]) return;
|
|
1308
|
+
const rows = group.rows.slice();
|
|
1309
|
+
rows[index] = next;
|
|
1310
|
+
onChange({
|
|
1311
|
+
...group,
|
|
1312
|
+
rows
|
|
1313
|
+
});
|
|
1314
|
+
};
|
|
1315
|
+
const handleAddRow = () => onChange(addRow(group, [], available));
|
|
1316
|
+
const handleAddGroup = () => addGroupAt(path);
|
|
1317
|
+
const handleSetJoin = (join) => onChange(setJoin(group, [], join));
|
|
1318
|
+
const otherJoin = group.join === "and" ? "or" : "and";
|
|
1319
|
+
return jsxs("div", {
|
|
1320
|
+
className: GROUP_CLASS,
|
|
1321
|
+
"data-testid": base,
|
|
1322
|
+
children: [
|
|
1323
|
+
group.rows.map((node, index) => {
|
|
1324
|
+
const childPath = [...path, index];
|
|
1325
|
+
return jsxs(Fragment, { children: [jsxs("div", {
|
|
1326
|
+
className: CONNECTOR_CLASS,
|
|
1327
|
+
children: [
|
|
1328
|
+
index === 0 && t("filters.where", { defaultValue: "Where" }),
|
|
1329
|
+
index === 1 && jsx("button", {
|
|
1330
|
+
type: "button",
|
|
1331
|
+
"data-testid": `${base}:join`,
|
|
1332
|
+
"aria-label": t("filters.toggleJoin", {
|
|
1333
|
+
defaultValue: `Join: ${joinLabel(group.join)} — activate to switch to ${joinLabel(otherJoin)}`,
|
|
1334
|
+
params: {
|
|
1335
|
+
current: joinLabel(group.join),
|
|
1336
|
+
next: joinLabel(otherJoin)
|
|
1337
|
+
}
|
|
1338
|
+
}),
|
|
1339
|
+
"aria-pressed": group.join === "or",
|
|
1340
|
+
onClick: () => handleSetJoin(otherJoin),
|
|
1341
|
+
className: "inline-grid justify-items-end rounded-xs font-medium text-foreground underline underline-offset-2 hover:no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
|
|
1342
|
+
children: JOIN_VALUES.map((j) => jsx("span", {
|
|
1343
|
+
className: cn("col-start-1 row-start-1", j !== group.join && "invisible"),
|
|
1344
|
+
children: joinLabel(j)
|
|
1345
|
+
}, j))
|
|
1346
|
+
}),
|
|
1347
|
+
index >= 2 && joinLabel(group.join)
|
|
1348
|
+
]
|
|
1349
|
+
}), isDraftGroup(node) ? jsx("div", {
|
|
1350
|
+
className: "min-w-0 rounded-md border bg-muted/30 p-2",
|
|
1351
|
+
children: jsx(GroupEditorInner, {
|
|
1352
|
+
group: node,
|
|
1353
|
+
path: childPath,
|
|
1354
|
+
available,
|
|
1355
|
+
onChange: (next) => handleChildGroupChange(index, next),
|
|
1356
|
+
testIdBase,
|
|
1357
|
+
onRemove: handleRemove,
|
|
1358
|
+
addGroupAt,
|
|
1359
|
+
debounceMs
|
|
1360
|
+
})
|
|
1361
|
+
}) : jsx(FilterRow, {
|
|
1362
|
+
row: node,
|
|
1363
|
+
path: childPath,
|
|
1364
|
+
available,
|
|
1365
|
+
onPatch: handlePatch,
|
|
1366
|
+
onRemove: handleRemove,
|
|
1367
|
+
testIdBase,
|
|
1368
|
+
debounceMs
|
|
1369
|
+
})] }, node.rowId);
|
|
1370
|
+
}),
|
|
1371
|
+
jsx("div", { "aria-hidden": "true" }),
|
|
1372
|
+
jsxs("div", {
|
|
1373
|
+
className: "flex items-center gap-2",
|
|
1374
|
+
children: [
|
|
1375
|
+
jsxs(Button, {
|
|
1376
|
+
type: "button",
|
|
1377
|
+
variant: "ghost",
|
|
1378
|
+
size: "sm",
|
|
1379
|
+
"data-testid": `${base}:add-row`,
|
|
1380
|
+
onClick: handleAddRow,
|
|
1381
|
+
children: [jsx(IconPlus, {
|
|
1382
|
+
className: "h-4 w-4",
|
|
1383
|
+
"aria-hidden": "true"
|
|
1384
|
+
}), t("filters.addRow", { defaultValue: "Add filter" })]
|
|
1385
|
+
}),
|
|
1386
|
+
canNestGroup && jsxs(Button, {
|
|
1387
|
+
type: "button",
|
|
1388
|
+
variant: "ghost",
|
|
1389
|
+
size: "sm",
|
|
1390
|
+
"data-testid": `${base}:add-group`,
|
|
1391
|
+
onClick: handleAddGroup,
|
|
1392
|
+
children: [jsx(IconFolderPlus, {
|
|
1393
|
+
className: "h-4 w-4",
|
|
1394
|
+
"aria-hidden": "true"
|
|
1395
|
+
}), t("filters.addGroup", { defaultValue: "Add group" })]
|
|
1396
|
+
}),
|
|
1397
|
+
onRemove && jsxs(Button, {
|
|
1398
|
+
type: "button",
|
|
1399
|
+
variant: "ghost",
|
|
1400
|
+
size: "sm",
|
|
1401
|
+
"data-testid": `${base}:remove`,
|
|
1402
|
+
onClick: () => onRemove(path),
|
|
1403
|
+
children: [jsx(IconX, {
|
|
1404
|
+
className: "h-4 w-4",
|
|
1405
|
+
"aria-hidden": "true"
|
|
1406
|
+
}), t("filters.removeGroup", { defaultValue: "Remove group" })]
|
|
1407
|
+
})
|
|
1408
|
+
]
|
|
1409
|
+
})
|
|
1410
|
+
]
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
//#endregion
|
|
1414
|
+
//#region src/list-kit/filter-builder/filter-panel.tsx
|
|
1415
|
+
function countGroupRows(group) {
|
|
1416
|
+
let total = 0;
|
|
1417
|
+
for (const row of group.rows) total += "rows" in row ? countGroupRows(row) : 1;
|
|
1418
|
+
return total;
|
|
1419
|
+
}
|
|
1420
|
+
function FilterPanel(props) {
|
|
1421
|
+
const { group, setGroup, available, setPage, testIdBase, open, onOpenChange, focusPath, externalRemoval } = props;
|
|
1422
|
+
const t = useOptionalT();
|
|
1423
|
+
const [draft, setDraft] = useState(() => toDraft(group));
|
|
1424
|
+
const wasOpenRef = useRef(open);
|
|
1425
|
+
const topLevelIdsRef = useRef([]);
|
|
1426
|
+
const panelRef = useRef(null);
|
|
1427
|
+
useEffect(() => {
|
|
1428
|
+
if (open && !wasOpenRef.current) {
|
|
1429
|
+
const seeded = toDraft(group);
|
|
1430
|
+
setDraft(seeded);
|
|
1431
|
+
topLevelIdsRef.current = seeded.rows.map((node) => node.rowId);
|
|
1432
|
+
if (focusPath && focusPath.length > 0) {
|
|
1433
|
+
const targetTestId = `${testIdBase}:row:${focusPath.join("-")}:field`;
|
|
1434
|
+
requestAnimationFrame(() => {
|
|
1435
|
+
panelRef.current?.querySelector(`[data-testid="${targetTestId}"]`)?.focus();
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
wasOpenRef.current = open;
|
|
1440
|
+
}, [open]);
|
|
1441
|
+
useEffect(() => {
|
|
1442
|
+
if (!externalRemoval) return;
|
|
1443
|
+
const { index } = externalRemoval;
|
|
1444
|
+
const rowId = topLevelIdsRef.current[index];
|
|
1445
|
+
topLevelIdsRef.current = topLevelIdsRef.current.filter((_, i) => i !== index);
|
|
1446
|
+
if (rowId === void 0) return;
|
|
1447
|
+
setDraft((current) => {
|
|
1448
|
+
const foundIndex = current.rows.findIndex((node) => node.rowId === rowId);
|
|
1449
|
+
return foundIndex < 0 ? current : removeAt(current, [foundIndex]);
|
|
1450
|
+
});
|
|
1451
|
+
}, [externalRemoval]);
|
|
1452
|
+
const handleApply = () => {
|
|
1453
|
+
setGroup(pruneIncompleteRows(fromDraft(draft)));
|
|
1454
|
+
setPage(1);
|
|
1455
|
+
onOpenChange(false);
|
|
1456
|
+
};
|
|
1457
|
+
const handleReset = () => setDraft(EMPTY_DRAFT());
|
|
1458
|
+
const appliedCount = countGroupRows(group);
|
|
1459
|
+
return jsxs(Popover, {
|
|
1460
|
+
open,
|
|
1461
|
+
onOpenChange,
|
|
1462
|
+
children: [jsx(PopoverTrigger, {
|
|
1463
|
+
asChild: true,
|
|
1464
|
+
children: jsxs(Button, {
|
|
1465
|
+
type: "button",
|
|
1466
|
+
variant: "outline",
|
|
1467
|
+
size: "sm",
|
|
1468
|
+
"data-testid": `${testIdBase}:button`,
|
|
1469
|
+
children: [
|
|
1470
|
+
jsx(IconFilter, {
|
|
1471
|
+
className: "h-4 w-4",
|
|
1472
|
+
"aria-hidden": "true"
|
|
1473
|
+
}),
|
|
1474
|
+
t("filters.title", { defaultValue: "Filters" }),
|
|
1475
|
+
appliedCount > 0 && jsx("span", {
|
|
1476
|
+
"data-testid": `${testIdBase}:count`,
|
|
1477
|
+
className: "ml-1 rounded-full bg-primary px-1.5 text-xs text-primary-foreground",
|
|
1478
|
+
children: appliedCount
|
|
1479
|
+
})
|
|
1480
|
+
]
|
|
1481
|
+
})
|
|
1482
|
+
}), jsxs(PopoverContent, {
|
|
1483
|
+
ref: panelRef,
|
|
1484
|
+
"data-testid": `${testIdBase}:panel`,
|
|
1485
|
+
align: "start",
|
|
1486
|
+
className: "w-[min(46rem,calc(100vw-2rem))]",
|
|
1487
|
+
children: [jsx(FilterGroupEditor, {
|
|
1488
|
+
group: draft,
|
|
1489
|
+
path: [],
|
|
1490
|
+
available,
|
|
1491
|
+
onChange: setDraft,
|
|
1492
|
+
testIdBase,
|
|
1493
|
+
debounceMs: 0
|
|
1494
|
+
}), jsxs("div", {
|
|
1495
|
+
className: "mt-3 flex justify-end gap-2 border-t pt-3",
|
|
1496
|
+
children: [jsx(Button, {
|
|
1497
|
+
type: "button",
|
|
1498
|
+
variant: "ghost",
|
|
1499
|
+
size: "sm",
|
|
1500
|
+
"data-testid": `${testIdBase}:reset`,
|
|
1501
|
+
onClick: handleReset,
|
|
1502
|
+
children: t("filters.reset", { defaultValue: "Reset" })
|
|
1503
|
+
}), jsx(Button, {
|
|
1504
|
+
type: "button",
|
|
1505
|
+
size: "sm",
|
|
1506
|
+
"data-testid": `${testIdBase}:apply`,
|
|
1507
|
+
onClick: handleApply,
|
|
1508
|
+
children: t("filters.apply", { defaultValue: "Apply" })
|
|
1509
|
+
})]
|
|
1510
|
+
})]
|
|
1511
|
+
})]
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
//#endregion
|
|
1515
|
+
//#region src/list-kit/filter-builder/pinned-filter-controls.tsx
|
|
1516
|
+
function isEmptyValue(value) {
|
|
1517
|
+
return value === void 0 || value === "" || Array.isArray(value) && value.length === 0;
|
|
1518
|
+
}
|
|
1519
|
+
function PinnedFilterControls(props) {
|
|
1520
|
+
const { pinnedFields, value, set, testIdBase } = props;
|
|
1521
|
+
return jsx(Fragment$1, { children: pinnedFields.map((spec) => jsx(PinnedControl, {
|
|
1522
|
+
spec,
|
|
1523
|
+
value,
|
|
1524
|
+
set,
|
|
1525
|
+
testIdBase
|
|
1526
|
+
}, spec.field)) });
|
|
1527
|
+
}
|
|
1528
|
+
function PinnedControl(props) {
|
|
1529
|
+
const { spec, value, set, testIdBase } = props;
|
|
1530
|
+
const op = spec.operators[0] ?? "eq";
|
|
1531
|
+
const index = findOwnedIndex(value, spec);
|
|
1532
|
+
const owned = index >= 0 ? value[index] : void 0;
|
|
1533
|
+
const handleChange = (next) => {
|
|
1534
|
+
if (isEmptyValue(next)) {
|
|
1535
|
+
if (index < 0) return;
|
|
1536
|
+
set(value.filter((_, i) => i !== index));
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
const predicate = {
|
|
1540
|
+
field: spec.field,
|
|
1541
|
+
op,
|
|
1542
|
+
value: next
|
|
1543
|
+
};
|
|
1544
|
+
if (index < 0) set([...value, predicate]);
|
|
1545
|
+
else {
|
|
1546
|
+
const nextValue = value.slice();
|
|
1547
|
+
nextValue[index] = predicate;
|
|
1548
|
+
set(nextValue);
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
return jsx(FilterValueControl, {
|
|
1552
|
+
kind: spec.kind,
|
|
1553
|
+
op,
|
|
1554
|
+
value: owned?.value,
|
|
1555
|
+
onChange: handleChange,
|
|
1556
|
+
testId: `${testIdBase}:pinned:${spec.field}`,
|
|
1557
|
+
fieldLabel: spec.label,
|
|
1558
|
+
placeholder: spec.label,
|
|
1559
|
+
options: spec.options
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
//#endregion
|
|
1563
|
+
//#region src/list-kit/filter-builder/filter-builder.tsx
|
|
1564
|
+
const EMPTY_PINNED_FIELDS = Object.freeze([]);
|
|
1565
|
+
const EMPTY_PINNED_VALUE = Object.freeze([]);
|
|
1566
|
+
function FilterBuilder(props) {
|
|
1567
|
+
const { group, setGroup, available, fixed, pinnedFields = EMPTY_PINNED_FIELDS, pinnedValue = EMPTY_PINNED_VALUE, setPinnedValue, setPage, testIdBase } = props;
|
|
1568
|
+
const [open, setOpen] = useState(false);
|
|
1569
|
+
const [focusPath, setFocusPath] = useState(void 0);
|
|
1570
|
+
const [externalRemoval, setExternalRemoval] = useState(void 0);
|
|
1571
|
+
const handleOpenChange = (next) => {
|
|
1572
|
+
setOpen(next);
|
|
1573
|
+
if (!next) setFocusPath(void 0);
|
|
1574
|
+
};
|
|
1575
|
+
const setGroupResettingPage = (next) => {
|
|
1576
|
+
setPage(1);
|
|
1577
|
+
setGroup(next);
|
|
1578
|
+
};
|
|
1579
|
+
const setPinnedResettingPage = (next) => {
|
|
1580
|
+
setPage(1);
|
|
1581
|
+
setPinnedValue?.(next);
|
|
1582
|
+
};
|
|
1583
|
+
return jsxs(Fragment$1, { children: [
|
|
1584
|
+
jsx(PinnedFilterControls, {
|
|
1585
|
+
pinnedFields,
|
|
1586
|
+
value: pinnedValue,
|
|
1587
|
+
set: setPinnedResettingPage,
|
|
1588
|
+
testIdBase
|
|
1589
|
+
}),
|
|
1590
|
+
available.length > 0 && jsx(FilterPanel, {
|
|
1591
|
+
group,
|
|
1592
|
+
setGroup,
|
|
1593
|
+
available,
|
|
1594
|
+
setPage,
|
|
1595
|
+
testIdBase,
|
|
1596
|
+
open,
|
|
1597
|
+
onOpenChange: handleOpenChange,
|
|
1598
|
+
focusPath,
|
|
1599
|
+
externalRemoval
|
|
1600
|
+
}),
|
|
1601
|
+
jsx(FilterBadges, {
|
|
1602
|
+
group,
|
|
1603
|
+
setGroup: setGroupResettingPage,
|
|
1604
|
+
available,
|
|
1605
|
+
fixed,
|
|
1606
|
+
pinnedFields,
|
|
1607
|
+
pinnedValue,
|
|
1608
|
+
setPinnedValue: setPinnedResettingPage,
|
|
1609
|
+
testIdBase,
|
|
1610
|
+
onOpenAt: (path) => {
|
|
1611
|
+
setFocusPath(path);
|
|
1612
|
+
setOpen(true);
|
|
1613
|
+
},
|
|
1614
|
+
onRemoved: (index) => setExternalRemoval({ index })
|
|
1615
|
+
})
|
|
1616
|
+
] });
|
|
1617
|
+
}
|
|
1618
|
+
//#endregion
|
|
1619
|
+
export { TableHeader as C, DEFAULT_LANGUAGE_STORAGE_KEY as D, badgeVariants as E, loadLocale as O, TableHead as S, Badge as T, SelectValue as _, FilterBadges as a, TableCaption as b, Select as c, SelectItem as d, SelectLabel as f, SelectTrigger as g, SelectSeparator as h, FilterValueControl as i, saveLocale as k, SelectContent as l, SelectScrollUpButton as m, PinnedFilterControls as n, formatDate as o, SelectScrollDownButton as p, FilterPanel as r, formatNumber as s, FilterBuilder as t, SelectGroup as u, Table as v, TableRow as w, TableCell as x, TableBody as y };
|
|
1620
|
+
|
|
1621
|
+
//# sourceMappingURL=filter-builder-Bhn-dhZV.js.map
|