@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
package/README.md
CHANGED
|
@@ -51,15 +51,32 @@ are the working examples to copy from. (This line named `apps/playground` as
|
|
|
51
51
|
the Next example. It was wrong twice over: that application had been a Vite
|
|
52
52
|
app since 2026-07-31, and SP9 has now deleted it.)
|
|
53
53
|
|
|
54
|
+
## Router setup
|
|
55
|
+
|
|
56
|
+
Mount your router's `RouterAdapterProvider` (`@burojs/react`) above
|
|
57
|
+
`<BuroProvider>`: it is the router's only binding, and every route match, active
|
|
58
|
+
nav item, breadcrumb and framework navigation goes through it. Then give
|
|
59
|
+
`<LinkComponentProvider linkComponent={…}>` your router's link component, which
|
|
60
|
+
only renders anchors. `RouterAdapterProvider`'s docblock has the react-router and
|
|
61
|
+
Next App Router adapters; `examples/vite` and `examples/next` mount them.
|
|
62
|
+
|
|
63
|
+
## App shell
|
|
64
|
+
|
|
65
|
+
**→ [docs/app-shell-guide.md](../../docs/app-shell-guide.md)** covers
|
|
66
|
+
`AppShell`, the ready-made application chrome, and how to replace any part of it.
|
|
67
|
+
|
|
54
68
|
## What's included
|
|
55
69
|
|
|
56
|
-
- Layout: AppLayout, Sidebar, Breadcrumbs
|
|
70
|
+
- Layout: AppLayout, AppSidebar and AppHeader (each slot you omit gets its default — brand, `<NavMain />` and `<UserMenu />`; `<HeaderBreadcrumbs />`, the palette chip, app actions and refresh — and `false` empties it), Sidebar, Breadcrumbs, HeaderBreadcrumbs (the header trail of the current route)
|
|
57
71
|
- Auth: LoginForm
|
|
58
|
-
- Resource views: ResourceList,
|
|
72
|
+
- Resource views: ResourceList, and `<RecordScreen resource mode recordId? …>` — a record's show, edit or create screen outside a route, in the built-in frame or in your own layout of its pieces (`RecordTitle`, `RecordActions`, `CopyId`, `FormActions`)
|
|
73
|
+
- Record pages: `<RecordPage resource action />` (plus `defaults` on `action="create"`) — show, edit and create wired to data (`useOne`, `useUpdateOne`, `useCreateOne`), with translated loading / not-found / session-expired / error states and the open panel of a keyed tabs node in the URL (`?<tabsKey>=<panelKey>`); `<ResourcePageRouter>` / `<NavAreaPage>` render it for every record route (a different layout is a frame on the screen, `views.<mode>.frame`; record overrides were removed in P6); the page draws no breadcrumb trail of its own: `<HeaderBreadcrumbs />` in `<AppHeader breadcrumbs>` draws the record trail there (`<RecordBreadcrumbs />` standalone), with the record's title once the page has loaded it and the id while it loads or has failed, and on a page-hosted flow's route ending with the flow's label; each show and edit visit is recorded under the signed-in user for the command palette's Recent group
|
|
74
|
+
- Custom pages: a page declared with a `body` (`defineCustomPage({ …, body: (l) => [l.place(block, { key })] })`) renders on its route under its label as the `<h1>`, behind the page's and its groups' `visible`; a page without `body` is a nav entry over a route the host owns
|
|
59
75
|
- Field renderers: text, number, boolean, date, enum, textarea (display + form input)
|
|
60
|
-
- Actions: ActionButton, BulkActionToolbar (
|
|
61
|
-
- Notifications: Toaster
|
|
62
|
-
-
|
|
76
|
+
- Actions: ActionButton (links, modals and flows), FlowActionButton, AppFlowButton, BulkActionToolbar (bulk flows); `useAction` from `@burojs/react/flows` runs a flow from your own control
|
|
77
|
+
- Notifications: Toaster (Sonner) — render it inside `<BuroProvider>`, where it registers itself as the dispatcher's notification sink
|
|
78
|
+
- Unsaved-changes dialog: `<NavigationGuardDialog>`, registered by `<AppLayout>` (the guard itself is `<BuroProvider>`'s)
|
|
79
|
+
- UI presentation hosts: dialog/sheet chrome for the framework-mounted `<UIPresentations/>` overlay stack (`shadcnPresentationHosts`), backing `modal` actions and the flow host (`screens.flow`, the `buro:flow` dialog or sheet)
|
|
63
80
|
- Vendored shadcn primitives: Button, Input, Label, Form, Table, Card, Dialog, Sheet, Select, Checkbox, Textarea, Badge
|
|
64
81
|
|
|
65
82
|
## License
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const require_button = require("./button-BxnnE28H.cjs");
|
|
2
|
+
let _burojs_core = require("@burojs/core");
|
|
3
|
+
let _burojs_react = require("@burojs/react");
|
|
4
|
+
let react = require("react");
|
|
5
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
6
|
+
let _burojs_react_flows = require("@burojs/react/flows");
|
|
7
|
+
//#region src/flows/bulk-summary.tsx
|
|
8
|
+
const text = (t, key, fallback, params) => {
|
|
9
|
+
const value = t(key, params === void 0 ? void 0 : { params });
|
|
10
|
+
return value === key ? fallback : value;
|
|
11
|
+
};
|
|
12
|
+
function FlowBulkSummary({ resource, actionName, label, total, progress }) {
|
|
13
|
+
const t = (0, _burojs_react.useOptionalT)();
|
|
14
|
+
const caps = (0, _burojs_react.useI18nCaps)(resource);
|
|
15
|
+
const forget = (0, _burojs_react_flows.useForgetEachAttempt)({
|
|
16
|
+
resource,
|
|
17
|
+
action: actionName
|
|
18
|
+
});
|
|
19
|
+
const [forgotten, setForgotten] = (0, react.useState)(() => new Set());
|
|
20
|
+
const forgetText = text(t, "flow.forget", "I've checked — forget it");
|
|
21
|
+
const testid = (0, _burojs_react.useTestIds)().bulkSummary(resource, actionName);
|
|
22
|
+
const own = `resources.${resource}.actions.${actionName}.summary`;
|
|
23
|
+
const params = {
|
|
24
|
+
count: progress.success.length,
|
|
25
|
+
total
|
|
26
|
+
};
|
|
27
|
+
const heading = caps.has(own) ? caps.t(own, { params }) : `${label}: ${text(t, "flow.bulkSummary", `${progress.success.length} processed`, params)} / ${total}`;
|
|
28
|
+
const notProcessed = progress.rejected.filter((entry) => entry.reason !== "server");
|
|
29
|
+
const refused = progress.rejected.filter((entry) => entry.reason === "server");
|
|
30
|
+
const groups = [
|
|
31
|
+
{
|
|
32
|
+
key: "not-processed",
|
|
33
|
+
title: text(t, "flow.rejected.precondition", "The record changed. Nothing was sent."),
|
|
34
|
+
ids: notProcessed.map((entry) => entry.id)
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: "rejected",
|
|
38
|
+
title: text(t, "flow.rejected.validation", "The server refused the values."),
|
|
39
|
+
ids: refused.map((entry) => entry.id)
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: "failed",
|
|
43
|
+
title: text(t, "flow.failed", "The request could not be sent."),
|
|
44
|
+
ids: progress.failed
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: "unknown",
|
|
48
|
+
title: text(t, "flow.unknown", "It may have been applied. Nothing was resent."),
|
|
49
|
+
ids: progress.unknown.filter((id) => !forgotten.has(id))
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: "unresolved",
|
|
53
|
+
title: text(t, "flow.bulkUnresolved", "Skipped: an earlier request for it may have been applied."),
|
|
54
|
+
ids: progress.unresolved.filter((id) => !forgotten.has(id))
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: "not-attempted",
|
|
58
|
+
title: text(t, "flow.bulkNotAttempted", "Not attempted."),
|
|
59
|
+
ids: progress.notAttempted
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
63
|
+
"data-testid": testid,
|
|
64
|
+
className: "flex flex-col gap-3 text-sm",
|
|
65
|
+
children: [(0, react_jsx_runtime.jsx)("p", {
|
|
66
|
+
"data-testid": `${testid}:heading`,
|
|
67
|
+
className: "font-medium",
|
|
68
|
+
children: heading
|
|
69
|
+
}), groups.filter((group) => group.ids.length > 0).map((group) => (0, react_jsx_runtime.jsxs)("section", {
|
|
70
|
+
"data-testid": `${testid}:${group.key}`,
|
|
71
|
+
children: [(0, react_jsx_runtime.jsxs)("h3", {
|
|
72
|
+
className: "font-medium",
|
|
73
|
+
children: [
|
|
74
|
+
group.title,
|
|
75
|
+
" ",
|
|
76
|
+
(0, react_jsx_runtime.jsxs)("span", {
|
|
77
|
+
"data-testid": `${testid}:${group.key}:count`,
|
|
78
|
+
className: "tabular-nums",
|
|
79
|
+
children: [
|
|
80
|
+
"(",
|
|
81
|
+
group.ids.length,
|
|
82
|
+
")"
|
|
83
|
+
]
|
|
84
|
+
})
|
|
85
|
+
]
|
|
86
|
+
}), (0, react_jsx_runtime.jsxs)("ul", {
|
|
87
|
+
className: "flex flex-wrap gap-1 text-muted-foreground",
|
|
88
|
+
children: [group.ids.slice(0, _burojs_core.BULK_SUMMARY_IDS).map((id) => (0, react_jsx_runtime.jsxs)("li", {
|
|
89
|
+
"data-testid": `${testid}:${group.key}:${id}`,
|
|
90
|
+
className: "flex items-center gap-1",
|
|
91
|
+
children: [id, group.key === "unknown" || group.key === "unresolved" ? (0, react_jsx_runtime.jsx)(require_button.Button, {
|
|
92
|
+
type: "button",
|
|
93
|
+
size: "sm",
|
|
94
|
+
variant: "ghost",
|
|
95
|
+
className: "h-6 px-1.5 text-xs",
|
|
96
|
+
"data-testid": `${testid}:forget:${id}`,
|
|
97
|
+
onClick: () => void forget(id).then(() => setForgotten((previous) => new Set([...previous, id]))),
|
|
98
|
+
children: forgetText
|
|
99
|
+
}) : null]
|
|
100
|
+
}, id)), group.ids.length > _burojs_core.BULK_SUMMARY_IDS ? (0, react_jsx_runtime.jsxs)("li", { children: ["+", group.ids.length - _burojs_core.BULK_SUMMARY_IDS] }) : null]
|
|
101
|
+
})]
|
|
102
|
+
}, group.key))]
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
exports.FlowBulkSummary = FlowBulkSummary;
|
|
107
|
+
|
|
108
|
+
//# sourceMappingURL=bulk-summary-CwSd6Yjb.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bulk-summary-CwSd6Yjb.cjs","names":["useOptionalT","useI18nCaps","useForgetEachAttempt","useState","useTestIds","BULK_SUMMARY_IDS","Button"],"sources":["../src/flows/bulk-summary.tsx"],"sourcesContent":["'use client';\nimport { BULK_SUMMARY_IDS, type BulkProgress } from '@burojs/core';\nimport { useI18nCaps, useOptionalT, useTestIds } from '@burojs/react';\nimport { useForgetEachAttempt } from '@burojs/react/flows';\nimport { type ReactElement, useState } from 'react';\nimport { Button } from '../primitives/button.js';\n\nexport interface FlowBulkSummaryProps {\n readonly resource: string;\n readonly actionName: string;\n /** The action's label. */\n readonly label: string;\n /** How many ids the run was given. */\n readonly total: number;\n readonly progress: BulkProgress;\n}\n\ntype Translate = ReturnType<typeof useOptionalT>;\nconst text = (t: Translate, key: string, fallback: string, params?: Record<string, unknown>) => {\n const value = t(key, params === undefined ? undefined : { params });\n return value === key ? fallback : value;\n};\n\n/**\n * A bulk flow's summary (P4 spec 4.16): the count that succeeded of the ids given (the flow's\n * own `resources.<r>.actions.<name>.summary`, `{{count}}` of `{{total}}`, else the framework's\n * count text), then one group per other outcome with its count and up to `BULK_SUMMARY_IDS` ids —\n * not processed (gone or its precondition failed), refused, failed, unknown (may have been\n * applied), unresolved from an earlier run, not attempted.\n */\nexport function FlowBulkSummary({\n resource,\n actionName,\n label,\n total,\n progress,\n}: FlowBulkSummaryProps): ReactElement {\n const t = useOptionalT();\n const caps = useI18nCaps(resource);\n // An id the run left unknown, or found unresolved, has its exit here (P4-C review I3): the user\n // checked the record, and the id's attempt is forgotten — the next run sends it again.\n const forget = useForgetEachAttempt({ resource, action: actionName });\n const [forgotten, setForgotten] = useState<ReadonlySet<string>>(() => new Set());\n const forgetText = text(t, 'flow.forget', \"I've checked — forget it\");\n const tid = useTestIds();\n const testid = tid.bulkSummary(resource, actionName);\n const own = `resources.${resource}.actions.${actionName}.summary`;\n const params = { count: progress.success.length, total };\n const heading = caps.has(own)\n ? caps.t(own, { params })\n : `${label}: ${text(t, 'flow.bulkSummary', `${progress.success.length} processed`, params)} / ${total}`;\n const notProcessed = progress.rejected.filter((entry) => entry.reason !== 'server');\n const refused = progress.rejected.filter((entry) => entry.reason === 'server');\n const groups: readonly { key: string; title: string; ids: readonly string[] }[] = [\n {\n key: 'not-processed',\n title: text(t, 'flow.rejected.precondition', 'The record changed. Nothing was sent.'),\n ids: notProcessed.map((entry) => entry.id),\n },\n {\n key: 'rejected',\n title: text(t, 'flow.rejected.validation', 'The server refused the values.'),\n ids: refused.map((entry) => entry.id),\n },\n {\n key: 'failed',\n title: text(t, 'flow.failed', 'The request could not be sent.'),\n ids: progress.failed,\n },\n {\n key: 'unknown',\n title: text(t, 'flow.unknown', 'It may have been applied. Nothing was resent.'),\n ids: progress.unknown.filter((id) => !forgotten.has(id)),\n },\n {\n key: 'unresolved',\n title: text(\n t,\n 'flow.bulkUnresolved',\n 'Skipped: an earlier request for it may have been applied.',\n ),\n ids: progress.unresolved.filter((id) => !forgotten.has(id)),\n },\n {\n key: 'not-attempted',\n title: text(t, 'flow.bulkNotAttempted', 'Not attempted.'),\n ids: progress.notAttempted,\n },\n ];\n return (\n <div data-testid={testid} className=\"flex flex-col gap-3 text-sm\">\n <p data-testid={`${testid}:heading`} className=\"font-medium\">\n {heading}\n </p>\n {groups\n .filter((group) => group.ids.length > 0)\n .map((group) => (\n <section key={group.key} data-testid={`${testid}:${group.key}`}>\n <h3 className=\"font-medium\">\n {group.title}{' '}\n <span data-testid={`${testid}:${group.key}:count`} className=\"tabular-nums\">\n ({group.ids.length})\n </span>\n </h3>\n <ul className=\"flex flex-wrap gap-1 text-muted-foreground\">\n {group.ids.slice(0, BULK_SUMMARY_IDS).map((id) => (\n <li\n key={id}\n data-testid={`${testid}:${group.key}:${id}`}\n className=\"flex items-center gap-1\"\n >\n {id}\n {group.key === 'unknown' || group.key === 'unresolved' ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-1.5 text-xs\"\n data-testid={`${testid}:forget:${id}`}\n onClick={() =>\n void forget(id).then(() =>\n setForgotten((previous) => new Set([...previous, id])),\n )\n }\n >\n {forgetText}\n </Button>\n ) : null}\n </li>\n ))}\n {group.ids.length > BULK_SUMMARY_IDS ? (\n <li>+{group.ids.length - BULK_SUMMARY_IDS}</li>\n ) : null}\n </ul>\n </section>\n ))}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { t as Button } from "./button-MWVeZaqj.js";
|
|
2
|
+
import { BULK_SUMMARY_IDS } from "@burojs/core";
|
|
3
|
+
import { useI18nCaps, useOptionalT, useTestIds } from "@burojs/react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { useForgetEachAttempt } from "@burojs/react/flows";
|
|
7
|
+
//#region src/flows/bulk-summary.tsx
|
|
8
|
+
const text = (t, key, fallback, params) => {
|
|
9
|
+
const value = t(key, params === void 0 ? void 0 : { params });
|
|
10
|
+
return value === key ? fallback : value;
|
|
11
|
+
};
|
|
12
|
+
function FlowBulkSummary({ resource, actionName, label, total, progress }) {
|
|
13
|
+
const t = useOptionalT();
|
|
14
|
+
const caps = useI18nCaps(resource);
|
|
15
|
+
const forget = useForgetEachAttempt({
|
|
16
|
+
resource,
|
|
17
|
+
action: actionName
|
|
18
|
+
});
|
|
19
|
+
const [forgotten, setForgotten] = useState(() => new Set());
|
|
20
|
+
const forgetText = text(t, "flow.forget", "I've checked — forget it");
|
|
21
|
+
const testid = useTestIds().bulkSummary(resource, actionName);
|
|
22
|
+
const own = `resources.${resource}.actions.${actionName}.summary`;
|
|
23
|
+
const params = {
|
|
24
|
+
count: progress.success.length,
|
|
25
|
+
total
|
|
26
|
+
};
|
|
27
|
+
const heading = caps.has(own) ? caps.t(own, { params }) : `${label}: ${text(t, "flow.bulkSummary", `${progress.success.length} processed`, params)} / ${total}`;
|
|
28
|
+
const notProcessed = progress.rejected.filter((entry) => entry.reason !== "server");
|
|
29
|
+
const refused = progress.rejected.filter((entry) => entry.reason === "server");
|
|
30
|
+
const groups = [
|
|
31
|
+
{
|
|
32
|
+
key: "not-processed",
|
|
33
|
+
title: text(t, "flow.rejected.precondition", "The record changed. Nothing was sent."),
|
|
34
|
+
ids: notProcessed.map((entry) => entry.id)
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: "rejected",
|
|
38
|
+
title: text(t, "flow.rejected.validation", "The server refused the values."),
|
|
39
|
+
ids: refused.map((entry) => entry.id)
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: "failed",
|
|
43
|
+
title: text(t, "flow.failed", "The request could not be sent."),
|
|
44
|
+
ids: progress.failed
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: "unknown",
|
|
48
|
+
title: text(t, "flow.unknown", "It may have been applied. Nothing was resent."),
|
|
49
|
+
ids: progress.unknown.filter((id) => !forgotten.has(id))
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: "unresolved",
|
|
53
|
+
title: text(t, "flow.bulkUnresolved", "Skipped: an earlier request for it may have been applied."),
|
|
54
|
+
ids: progress.unresolved.filter((id) => !forgotten.has(id))
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: "not-attempted",
|
|
58
|
+
title: text(t, "flow.bulkNotAttempted", "Not attempted."),
|
|
59
|
+
ids: progress.notAttempted
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
return jsxs("div", {
|
|
63
|
+
"data-testid": testid,
|
|
64
|
+
className: "flex flex-col gap-3 text-sm",
|
|
65
|
+
children: [jsx("p", {
|
|
66
|
+
"data-testid": `${testid}:heading`,
|
|
67
|
+
className: "font-medium",
|
|
68
|
+
children: heading
|
|
69
|
+
}), groups.filter((group) => group.ids.length > 0).map((group) => jsxs("section", {
|
|
70
|
+
"data-testid": `${testid}:${group.key}`,
|
|
71
|
+
children: [jsxs("h3", {
|
|
72
|
+
className: "font-medium",
|
|
73
|
+
children: [
|
|
74
|
+
group.title,
|
|
75
|
+
" ",
|
|
76
|
+
jsxs("span", {
|
|
77
|
+
"data-testid": `${testid}:${group.key}:count`,
|
|
78
|
+
className: "tabular-nums",
|
|
79
|
+
children: [
|
|
80
|
+
"(",
|
|
81
|
+
group.ids.length,
|
|
82
|
+
")"
|
|
83
|
+
]
|
|
84
|
+
})
|
|
85
|
+
]
|
|
86
|
+
}), jsxs("ul", {
|
|
87
|
+
className: "flex flex-wrap gap-1 text-muted-foreground",
|
|
88
|
+
children: [group.ids.slice(0, BULK_SUMMARY_IDS).map((id) => jsxs("li", {
|
|
89
|
+
"data-testid": `${testid}:${group.key}:${id}`,
|
|
90
|
+
className: "flex items-center gap-1",
|
|
91
|
+
children: [id, group.key === "unknown" || group.key === "unresolved" ? jsx(Button, {
|
|
92
|
+
type: "button",
|
|
93
|
+
size: "sm",
|
|
94
|
+
variant: "ghost",
|
|
95
|
+
className: "h-6 px-1.5 text-xs",
|
|
96
|
+
"data-testid": `${testid}:forget:${id}`,
|
|
97
|
+
onClick: () => void forget(id).then(() => setForgotten((previous) => new Set([...previous, id]))),
|
|
98
|
+
children: forgetText
|
|
99
|
+
}) : null]
|
|
100
|
+
}, id)), group.ids.length > BULK_SUMMARY_IDS ? jsxs("li", { children: ["+", group.ids.length - BULK_SUMMARY_IDS] }) : null]
|
|
101
|
+
})]
|
|
102
|
+
}, group.key))]
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
export { FlowBulkSummary };
|
|
107
|
+
|
|
108
|
+
//# sourceMappingURL=bulk-summary-GxMDP4bw.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bulk-summary-GxMDP4bw.js","names":[],"sources":["../src/flows/bulk-summary.tsx"],"sourcesContent":["'use client';\nimport { BULK_SUMMARY_IDS, type BulkProgress } from '@burojs/core';\nimport { useI18nCaps, useOptionalT, useTestIds } from '@burojs/react';\nimport { useForgetEachAttempt } from '@burojs/react/flows';\nimport { type ReactElement, useState } from 'react';\nimport { Button } from '../primitives/button.js';\n\nexport interface FlowBulkSummaryProps {\n readonly resource: string;\n readonly actionName: string;\n /** The action's label. */\n readonly label: string;\n /** How many ids the run was given. */\n readonly total: number;\n readonly progress: BulkProgress;\n}\n\ntype Translate = ReturnType<typeof useOptionalT>;\nconst text = (t: Translate, key: string, fallback: string, params?: Record<string, unknown>) => {\n const value = t(key, params === undefined ? undefined : { params });\n return value === key ? fallback : value;\n};\n\n/**\n * A bulk flow's summary (P4 spec 4.16): the count that succeeded of the ids given (the flow's\n * own `resources.<r>.actions.<name>.summary`, `{{count}}` of `{{total}}`, else the framework's\n * count text), then one group per other outcome with its count and up to `BULK_SUMMARY_IDS` ids —\n * not processed (gone or its precondition failed), refused, failed, unknown (may have been\n * applied), unresolved from an earlier run, not attempted.\n */\nexport function FlowBulkSummary({\n resource,\n actionName,\n label,\n total,\n progress,\n}: FlowBulkSummaryProps): ReactElement {\n const t = useOptionalT();\n const caps = useI18nCaps(resource);\n // An id the run left unknown, or found unresolved, has its exit here (P4-C review I3): the user\n // checked the record, and the id's attempt is forgotten — the next run sends it again.\n const forget = useForgetEachAttempt({ resource, action: actionName });\n const [forgotten, setForgotten] = useState<ReadonlySet<string>>(() => new Set());\n const forgetText = text(t, 'flow.forget', \"I've checked — forget it\");\n const tid = useTestIds();\n const testid = tid.bulkSummary(resource, actionName);\n const own = `resources.${resource}.actions.${actionName}.summary`;\n const params = { count: progress.success.length, total };\n const heading = caps.has(own)\n ? caps.t(own, { params })\n : `${label}: ${text(t, 'flow.bulkSummary', `${progress.success.length} processed`, params)} / ${total}`;\n const notProcessed = progress.rejected.filter((entry) => entry.reason !== 'server');\n const refused = progress.rejected.filter((entry) => entry.reason === 'server');\n const groups: readonly { key: string; title: string; ids: readonly string[] }[] = [\n {\n key: 'not-processed',\n title: text(t, 'flow.rejected.precondition', 'The record changed. Nothing was sent.'),\n ids: notProcessed.map((entry) => entry.id),\n },\n {\n key: 'rejected',\n title: text(t, 'flow.rejected.validation', 'The server refused the values.'),\n ids: refused.map((entry) => entry.id),\n },\n {\n key: 'failed',\n title: text(t, 'flow.failed', 'The request could not be sent.'),\n ids: progress.failed,\n },\n {\n key: 'unknown',\n title: text(t, 'flow.unknown', 'It may have been applied. Nothing was resent.'),\n ids: progress.unknown.filter((id) => !forgotten.has(id)),\n },\n {\n key: 'unresolved',\n title: text(\n t,\n 'flow.bulkUnresolved',\n 'Skipped: an earlier request for it may have been applied.',\n ),\n ids: progress.unresolved.filter((id) => !forgotten.has(id)),\n },\n {\n key: 'not-attempted',\n title: text(t, 'flow.bulkNotAttempted', 'Not attempted.'),\n ids: progress.notAttempted,\n },\n ];\n return (\n <div data-testid={testid} className=\"flex flex-col gap-3 text-sm\">\n <p data-testid={`${testid}:heading`} className=\"font-medium\">\n {heading}\n </p>\n {groups\n .filter((group) => group.ids.length > 0)\n .map((group) => (\n <section key={group.key} data-testid={`${testid}:${group.key}`}>\n <h3 className=\"font-medium\">\n {group.title}{' '}\n <span data-testid={`${testid}:${group.key}:count`} className=\"tabular-nums\">\n ({group.ids.length})\n </span>\n </h3>\n <ul className=\"flex flex-wrap gap-1 text-muted-foreground\">\n {group.ids.slice(0, BULK_SUMMARY_IDS).map((id) => (\n <li\n key={id}\n data-testid={`${testid}:${group.key}:${id}`}\n className=\"flex items-center gap-1\"\n >\n {id}\n {group.key === 'unknown' || group.key === 'unresolved' ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-1.5 text-xs\"\n data-testid={`${testid}:forget:${id}`}\n onClick={() =>\n void forget(id).then(() =>\n setForgotten((previous) => new Set([...previous, id])),\n )\n }\n >\n {forgetText}\n </Button>\n ) : null}\n </li>\n ))}\n {group.ids.length > BULK_SUMMARY_IDS ? (\n <li>+{group.ids.length - BULK_SUMMARY_IDS}</li>\n ) : null}\n </ul>\n </section>\n ))}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const require_use_theme = require("./use-theme-DFlpXils.cjs");
|
|
2
|
+
let react = require("react");
|
|
3
|
+
react = require_use_theme.__toESM(react, 1);
|
|
4
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
5
|
+
let _radix_ui_react_slot = require("@radix-ui/react-slot");
|
|
6
|
+
//#region src/primitives/button.tsx
|
|
7
|
+
const buttonVariants = (0, require("class-variance-authority").cva)("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 disabled:saturate-0 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:saturate-0 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", {
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
11
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
12
|
+
outline: "border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
13
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
14
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
15
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
default: "h-9 px-4 py-2",
|
|
19
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
20
|
+
lg: "h-10 rounded-md px-6",
|
|
21
|
+
icon: "h-9 w-9"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: "default",
|
|
26
|
+
size: "default"
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
const Button = react.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
30
|
+
return (0, react_jsx_runtime.jsx)(asChild ? _radix_ui_react_slot.Slot : "button", {
|
|
31
|
+
className: require_use_theme.cn(buttonVariants({
|
|
32
|
+
variant,
|
|
33
|
+
size,
|
|
34
|
+
className
|
|
35
|
+
})),
|
|
36
|
+
ref,
|
|
37
|
+
...props
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
Button.displayName = "Button";
|
|
41
|
+
//#endregion
|
|
42
|
+
Object.defineProperty(exports, "Button", {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
get: function() {
|
|
45
|
+
return Button;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
Object.defineProperty(exports, "buttonVariants", {
|
|
49
|
+
enumerable: true,
|
|
50
|
+
get: function() {
|
|
51
|
+
return buttonVariants;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
//# sourceMappingURL=button-BxnnE28H.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"button-BxnnE28H.cjs","names":["cva","React","Slot","cn"],"sources":["../src/primitives/button.tsx"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../lib/cn.js';\n\nconst buttonVariants = cva(\n 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 disabled:saturate-0 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:saturate-0 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',\n {\n variants: {\n variant: {\n default: 'bg-primary text-primary-foreground hover:bg-primary/90',\n destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',\n outline:\n 'border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground',\n secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',\n ghost: 'hover:bg-accent hover:text-accent-foreground',\n link: 'text-primary underline-offset-4 hover:underline',\n },\n size: {\n // Shadcn new-york current defaults. The shared stylesheet keeps the\n // standard 16px root; editable controls choose responsive typography\n // locally when they need mobile `text-base` and desktop `md:text-sm`.\n default: 'h-9 px-4 py-2',\n sm: 'h-8 rounded-md px-3 text-xs',\n lg: 'h-10 rounded-md px-6',\n icon: 'h-9 w-9',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n },\n);\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n ({ className, variant, size, asChild = false, ...props }, ref) => {\n const Comp = asChild ? Slot : 'button';\n return (\n <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />\n );\n },\n);\nButton.displayName = 'Button';\n\nexport { Button, buttonVariants };\n"],"mappings":";;;;;;AAOA,MAAM,kBAAA,qCAAiBA,CAAAA,CAAAA,IAAAA,CACrB,ieACA;CACE,UAAU;EACR,SAAS;GACP,SAAS;GACT,aAAa;GACb,SACE;GACF,WAAW;GACX,OAAO;GACP,MAAM;EACR;EACA,MAAM;GAIJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,MAAM;EACR;CACF;CACA,iBAAiB;EACf,SAAS;EACT,MAAM;CACR;AACF,CACF;AAQA,MAAM,SAASC,MAAM,YAClB,EAAE,WAAW,SAAS,MAAM,UAAU,OAAO,GAAG,SAAS,QAAQ;CAEhE,QACE,GAAA,kBAAA,IAAA,CAFW,UAAUC,qBAAAA,OAAO,UAE5B;EAAM,WAAWC,kBAAAA,GAAG,eAAe;GAAE;GAAS;GAAM;EAAU,CAAC,CAAC;EAAQ;EAAK,GAAI;CAAQ,CAAA;AAE7F,CACF;AACA,OAAO,cAAc"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { l as cn } from "./use-theme-dnE3Od3I.js";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import { cva } from "class-variance-authority";
|
|
6
|
+
//#region src/primitives/button.tsx
|
|
7
|
+
const buttonVariants = cva("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 disabled:saturate-0 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:saturate-0 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", {
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
11
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
12
|
+
outline: "border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
13
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
14
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
15
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
default: "h-9 px-4 py-2",
|
|
19
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
20
|
+
lg: "h-10 rounded-md px-6",
|
|
21
|
+
icon: "h-9 w-9"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: "default",
|
|
26
|
+
size: "default"
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
30
|
+
return jsx(asChild ? Slot : "button", {
|
|
31
|
+
className: cn(buttonVariants({
|
|
32
|
+
variant,
|
|
33
|
+
size,
|
|
34
|
+
className
|
|
35
|
+
})),
|
|
36
|
+
ref,
|
|
37
|
+
...props
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
Button.displayName = "Button";
|
|
41
|
+
//#endregion
|
|
42
|
+
export { buttonVariants as n, Button as t };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=button-MWVeZaqj.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"button-MWVeZaqj.js","names":[],"sources":["../src/primitives/button.tsx"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../lib/cn.js';\n\nconst buttonVariants = cva(\n 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 disabled:saturate-0 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:saturate-0 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',\n {\n variants: {\n variant: {\n default: 'bg-primary text-primary-foreground hover:bg-primary/90',\n destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',\n outline:\n 'border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground',\n secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',\n ghost: 'hover:bg-accent hover:text-accent-foreground',\n link: 'text-primary underline-offset-4 hover:underline',\n },\n size: {\n // Shadcn new-york current defaults. The shared stylesheet keeps the\n // standard 16px root; editable controls choose responsive typography\n // locally when they need mobile `text-base` and desktop `md:text-sm`.\n default: 'h-9 px-4 py-2',\n sm: 'h-8 rounded-md px-3 text-xs',\n lg: 'h-10 rounded-md px-6',\n icon: 'h-9 w-9',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n },\n);\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n ({ className, variant, size, asChild = false, ...props }, ref) => {\n const Comp = asChild ? Slot : 'button';\n return (\n <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />\n );\n },\n);\nButton.displayName = 'Button';\n\nexport { Button, buttonVariants };\n"],"mappings":";;;;;;AAOA,MAAM,iBAAiB,IACrB,ieACA;CACE,UAAU;EACR,SAAS;GACP,SAAS;GACT,aAAa;GACb,SACE;GACF,WAAW;GACX,OAAO;GACP,MAAM;EACR;EACA,MAAM;GAIJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,MAAM;EACR;CACF;CACA,iBAAiB;EACf,SAAS;EACT,MAAM;CACR;AACF,CACF;AAQA,MAAM,SAAS,MAAM,YAClB,EAAE,WAAW,SAAS,MAAM,UAAU,OAAO,GAAG,SAAS,QAAQ;CAEhE,OACE,IAFW,UAAU,OAAO,UAE5B;EAAM,WAAW,GAAG,eAAe;GAAE;GAAS;GAAM;EAAU,CAAC,CAAC;EAAQ;EAAK,GAAI;CAAQ,CAAA;AAE7F,CACF;AACA,OAAO,cAAc"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { declaredText, resolveCandidates, resolveFieldLabel } from "@burojs/core";
|
|
2
|
+
import { useApp, useCandidatesAccess, useI18nCaps, useOptionalT, useTestIds } from "@burojs/react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Suspense, lazy } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
//#region src/fields/lazy-relation-picker-dialog.tsx
|
|
7
|
+
const Dialog = lazy(() => import("./relation-picker-dialog-B6nIiJBf.js").then((module) => ({ default: module.RelationPickerDialog })));
|
|
8
|
+
function LazyRelationPickerDialog(props) {
|
|
9
|
+
return jsx(Suspense, {
|
|
10
|
+
fallback: null,
|
|
11
|
+
children: jsx(Dialog, { ...props })
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/fields/candidates-match.ts
|
|
16
|
+
function rowMatches(row, filters) {
|
|
17
|
+
let unknown = false;
|
|
18
|
+
for (const filter of filters) {
|
|
19
|
+
const group = filter;
|
|
20
|
+
if (Array.isArray(group.and) || Array.isArray(group.or)) {
|
|
21
|
+
const answers = (Array.isArray(group.and) ? group.and : group.or).map((part) => rowMatches(row, [part]));
|
|
22
|
+
const all = Array.isArray(group.and);
|
|
23
|
+
const decided = answers.filter((answer) => answer !== void 0);
|
|
24
|
+
if (all ? decided.includes(false) : decided.length > 0 && !decided.includes(true)) return false;
|
|
25
|
+
if (decided.length < answers.length && !(all ? false : decided.includes(true))) unknown = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const { field, op, value } = filter;
|
|
29
|
+
if (typeof field !== "string" || typeof op !== "string") continue;
|
|
30
|
+
if (!Object.hasOwn(row, field)) {
|
|
31
|
+
unknown = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const held = holds(row[field], op, value);
|
|
35
|
+
if (held === false) return false;
|
|
36
|
+
if (held === void 0) unknown = true;
|
|
37
|
+
}
|
|
38
|
+
return unknown ? void 0 : true;
|
|
39
|
+
}
|
|
40
|
+
function holds(actual, op, expected) {
|
|
41
|
+
const list = Array.isArray(expected) ? expected : void 0;
|
|
42
|
+
const text = (value) => String(value ?? "").toLowerCase();
|
|
43
|
+
switch (op) {
|
|
44
|
+
case "eq": return Object.is(actual, expected) || String(actual) === String(expected);
|
|
45
|
+
case "neq": return !(Object.is(actual, expected) || String(actual) === String(expected));
|
|
46
|
+
case "in": return list === void 0 ? void 0 : list.some((entry) => String(entry) === String(actual));
|
|
47
|
+
case "notIn": return list === void 0 ? void 0 : !list.some((entry) => String(entry) === String(actual));
|
|
48
|
+
case "gt": return compare(actual, expected, (a, b) => a > b);
|
|
49
|
+
case "gte": return compare(actual, expected, (a, b) => a >= b);
|
|
50
|
+
case "lt": return compare(actual, expected, (a, b) => a < b);
|
|
51
|
+
case "lte": return compare(actual, expected, (a, b) => a <= b);
|
|
52
|
+
case "contains": return text(actual).includes(text(expected));
|
|
53
|
+
case "startsWith": return text(actual).startsWith(text(expected));
|
|
54
|
+
case "endsWith": return text(actual).endsWith(text(expected));
|
|
55
|
+
case "isNull": return actual === null || actual === void 0;
|
|
56
|
+
case "isNotNull": return actual !== null && actual !== void 0;
|
|
57
|
+
default: return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function compare(actual, expected, test) {
|
|
61
|
+
return typeof actual === "number" && typeof expected === "number" || typeof actual === "string" && typeof expected === "string" ? test(actual, expected) : void 0;
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/fields/candidates-gate.ts
|
|
65
|
+
const NO_SOURCES = Object.freeze({});
|
|
66
|
+
const NO_FILTERS = Object.freeze([]);
|
|
67
|
+
function useCandidatesGate(input) {
|
|
68
|
+
const { to, candidates, sources, resource, fieldName } = input;
|
|
69
|
+
const t = useOptionalT();
|
|
70
|
+
const app = useApp();
|
|
71
|
+
const tid = useTestIds();
|
|
72
|
+
const ownerCaps = useI18nCaps(resource);
|
|
73
|
+
const targetCaps = useI18nCaps(to ?? resource);
|
|
74
|
+
const access = useCandidatesAccess(to ?? "", candidates);
|
|
75
|
+
const resolution = React.useMemo(() => resolveCandidates(candidates, sources ?? NO_SOURCES), [candidates, sources]);
|
|
76
|
+
if (candidates === void 0 || candidates.length === 0) return { filters: NO_FILTERS };
|
|
77
|
+
const testId = tid.relationCandidatesHint(resource, fieldName);
|
|
78
|
+
if (access.status === "pending") return {
|
|
79
|
+
filters: NO_FILTERS,
|
|
80
|
+
gate: { kind: "pending" }
|
|
81
|
+
};
|
|
82
|
+
if (access.status === "forbidden") {
|
|
83
|
+
const declared = declaredText("field", (app.registry.get(to ?? "")?.fields)?.[access.field], "label");
|
|
84
|
+
const label = resolveFieldLabel(targetCaps, access.field, declared);
|
|
85
|
+
return {
|
|
86
|
+
filters: NO_FILTERS,
|
|
87
|
+
gate: {
|
|
88
|
+
kind: "forbidden",
|
|
89
|
+
testId,
|
|
90
|
+
message: t("relationPicker.cannotSearchBy", {
|
|
91
|
+
params: { label },
|
|
92
|
+
defaultValue: `You cannot search by ${label}`
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (resolution.status === "pending") return {
|
|
98
|
+
filters: NO_FILTERS,
|
|
99
|
+
gate: { kind: "pending" }
|
|
100
|
+
};
|
|
101
|
+
if (resolution.status === "empty") {
|
|
102
|
+
const { source, path } = resolution.hint;
|
|
103
|
+
const head = path[0];
|
|
104
|
+
const declared = (source === "values" || source === "record") && head !== void 0 ? declaredText("field", (app.registry.get(resource)?.fields)?.[head], "label") : void 0;
|
|
105
|
+
const label = resolveFieldLabel(ownerCaps, head ?? source, declared);
|
|
106
|
+
return {
|
|
107
|
+
filters: NO_FILTERS,
|
|
108
|
+
gate: {
|
|
109
|
+
kind: "empty",
|
|
110
|
+
testId,
|
|
111
|
+
message: t("relationPicker.chooseFirst", {
|
|
112
|
+
params: { label },
|
|
113
|
+
defaultValue: `Choose ${label} first`
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return { filters: resolution.filters };
|
|
119
|
+
}
|
|
120
|
+
function useCandidateSelection(input) {
|
|
121
|
+
const { fixedFilters } = input;
|
|
122
|
+
const gate = useCandidatesGate(input);
|
|
123
|
+
const filters = gate.filters;
|
|
124
|
+
const combined = React.useMemo(() => filters.length === 0 ? fixedFilters : [...filters, ...fixedFilters ?? []], [filters, fixedFilters]);
|
|
125
|
+
const rowFits = React.useMemo(() => filters.length === 0 ? void 0 : (row) => rowMatches(row, filters), [filters]);
|
|
126
|
+
return {
|
|
127
|
+
fixedFilters: combined,
|
|
128
|
+
...gate.gate === void 0 ? {} : { gate: gate.gate },
|
|
129
|
+
...rowFits === void 0 ? {} : { rowFits }
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
export { useCandidatesGate as n, LazyRelationPickerDialog as r, useCandidateSelection as t };
|
|
134
|
+
|
|
135
|
+
//# sourceMappingURL=candidates-gate-Bb70LPAy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"candidates-gate-Bb70LPAy.js","names":[],"sources":["../src/fields/lazy-relation-picker-dialog.tsx","../src/fields/candidates-match.ts","../src/fields/candidates-gate.ts"],"sourcesContent":["import { type ComponentProps, type ReactElement, Suspense, lazy } from 'react';\nimport type { RelationPickerDialog } from './relation-picker-dialog.js';\n\nconst Dialog = lazy(() =>\n import('./relation-picker-dialog.js').then((module) => ({\n default: module.RelationPickerDialog,\n })),\n);\n\n/** The \"Advanced search\" dialog, loaded on the first open: nothing is drawn until it arrives. */\nexport function LazyRelationPickerDialog(\n props: ComponentProps<typeof RelationPickerDialog>,\n): ReactElement {\n return (\n <Suspense fallback={null}>\n <Dialog {...props} />\n </Suspense>\n );\n}\n","import type { FixedFilterInput } from '@burojs/core';\n\ntype Entry = { readonly field?: unknown; readonly op?: unknown; readonly value?: unknown };\n\n/**\n * Whether `row` meets every candidate filter (P7 3.4): `true`, `false`, or `undefined` when the\n * row does not carry a field a filter names (a narrower projection), so nothing can be said.\n * Only the flat `{ field, op, value }` entries candidates resolve to are evaluated; this is what\n * \"does not match the current filter\" is decided on, never what is sent.\n */\nexport function rowMatches(\n row: Readonly<Record<string, unknown>>,\n filters: readonly FixedFilterInput[],\n): boolean | undefined {\n let unknown = false;\n for (const filter of filters) {\n const group = filter as { readonly and?: unknown; readonly or?: unknown };\n if (Array.isArray(group.and) || Array.isArray(group.or)) {\n const parts = (\n Array.isArray(group.and) ? group.and : group.or\n ) as readonly FixedFilterInput[];\n const answers = parts.map((part) => rowMatches(row, [part]));\n const all = Array.isArray(group.and);\n const decided = answers.filter((answer) => answer !== undefined);\n if (all ? decided.includes(false) : decided.length > 0 && !decided.includes(true))\n return false;\n if (decided.length < answers.length && !(all ? false : decided.includes(true)))\n unknown = true;\n continue;\n }\n const { field, op, value } = filter as Entry;\n if (typeof field !== 'string' || typeof op !== 'string') continue;\n if (!Object.hasOwn(row, field)) {\n unknown = true;\n continue;\n }\n const held = holds(row[field], op, value);\n if (held === false) return false;\n if (held === undefined) unknown = true;\n }\n return unknown ? undefined : true;\n}\n\nfunction holds(actual: unknown, op: string, expected: unknown): boolean | undefined {\n const list = Array.isArray(expected) ? (expected as readonly unknown[]) : undefined;\n const text = (value: unknown) => String(value ?? '').toLowerCase();\n switch (op) {\n case 'eq':\n return Object.is(actual, expected) || String(actual) === String(expected);\n case 'neq':\n return !(Object.is(actual, expected) || String(actual) === String(expected));\n case 'in':\n return list === undefined\n ? undefined\n : list.some((entry) => String(entry) === String(actual));\n case 'notIn':\n return list === undefined\n ? undefined\n : !list.some((entry) => String(entry) === String(actual));\n case 'gt':\n return compare(actual, expected, (a, b) => a > b);\n case 'gte':\n return compare(actual, expected, (a, b) => a >= b);\n case 'lt':\n return compare(actual, expected, (a, b) => a < b);\n case 'lte':\n return compare(actual, expected, (a, b) => a <= b);\n case 'contains':\n return text(actual).includes(text(expected));\n case 'startsWith':\n return text(actual).startsWith(text(expected));\n case 'endsWith':\n return text(actual).endsWith(text(expected));\n case 'isNull':\n return actual === null || actual === undefined;\n case 'isNotNull':\n return actual !== null && actual !== undefined;\n default:\n return undefined;\n }\n}\n\nfunction compare(\n actual: unknown,\n expected: unknown,\n test: (a: number | string, b: number | string) => boolean,\n): boolean | undefined {\n const both =\n (typeof actual === 'number' && typeof expected === 'number') ||\n (typeof actual === 'string' && typeof expected === 'string');\n return both ? test(actual as number | string, expected as number | string) : undefined;\n}\n","import {\n type FixedFilterInput,\n type RelationCandidate,\n type ScreenSources,\n resolveCandidates,\n resolveFieldLabel,\n declaredText,\n} from '@burojs/core';\nimport { useApp, useCandidatesAccess, useI18nCaps, useOptionalT, useTestIds } from '@burojs/react';\nimport * as React from 'react';\nimport { rowMatches } from './candidates-match.js';\n\n/** A select that must not read its target now (P7 3.4): why, and what it tells the user. */\nexport type CandidatesGate =\n | { readonly kind: 'pending' }\n | { readonly kind: 'empty'; readonly message: string; readonly testId: string }\n | { readonly kind: 'forbidden'; readonly message: string; readonly testId: string };\n\nconst NO_SOURCES: ScreenSources = Object.freeze({});\nconst NO_FILTERS: readonly FixedFilterInput[] = Object.freeze([]);\n\n/**\n * A relation field's `candidates` as the select applies them (P7 3.4): the filters to send and,\n * when they say the target must not be read yet, the gate that tells the user why. The refs are\n * read from the owner's projected sources, so a field P5 hides reads `null` (the select asks for\n * it first, and sends nothing); a candidate on a target field the user may not view forbids the\n * read; an undecided source or permission map keeps the select disabled.\n */\nexport function useCandidatesGate(input: {\n readonly to: string | undefined;\n readonly candidates: readonly RelationCandidate[] | undefined;\n readonly sources: ScreenSources | undefined;\n readonly resource: string;\n readonly fieldName: string;\n}): { readonly filters: readonly FixedFilterInput[]; readonly gate?: CandidatesGate } {\n const { to, candidates, sources, resource, fieldName } = input;\n const t = useOptionalT();\n const app = useApp();\n const tid = useTestIds();\n const ownerCaps = useI18nCaps(resource);\n const targetCaps = useI18nCaps(to ?? resource);\n const access = useCandidatesAccess(to ?? '', candidates);\n const resolution = React.useMemo(\n () => resolveCandidates(candidates, sources ?? NO_SOURCES),\n [candidates, sources],\n );\n if (candidates === undefined || candidates.length === 0) return { filters: NO_FILTERS };\n const testId = tid.relationCandidatesHint(resource, fieldName);\n if (access.status === 'pending') return { filters: NO_FILTERS, gate: { kind: 'pending' } };\n if (access.status === 'forbidden') {\n const declared = declaredText(\n 'field',\n (app.registry.get(to ?? '')?.fields as Record<string, unknown> | undefined)?.[access.field],\n 'label',\n );\n const label = resolveFieldLabel(targetCaps, access.field, declared);\n return {\n filters: NO_FILTERS,\n gate: {\n kind: 'forbidden',\n testId,\n message: t('relationPicker.cannotSearchBy', {\n params: { label },\n defaultValue: `You cannot search by ${label}`,\n }),\n },\n };\n }\n if (resolution.status === 'pending') return { filters: NO_FILTERS, gate: { kind: 'pending' } };\n if (resolution.status === 'empty') {\n const { source, path } = resolution.hint;\n const head = path[0];\n // The field a `values` / `record` ref reads is labelled as the owner labels it; any other\n // source (`scope`, `user`) names the key it reads, humanised.\n const own = source === 'values' || source === 'record';\n const declared =\n own && head !== undefined\n ? declaredText(\n 'field',\n (app.registry.get(resource)?.fields as Record<string, unknown> | undefined)?.[head],\n 'label',\n )\n : undefined;\n const label = resolveFieldLabel(ownerCaps, head ?? source, declared);\n return {\n filters: NO_FILTERS,\n gate: {\n kind: 'empty',\n testId,\n message: t('relationPicker.chooseFirst', {\n params: { label },\n defaultValue: `Choose ${label} first`,\n }),\n },\n };\n }\n return { filters: resolution.filters };\n}\n\n/**\n * A select's declared candidates as the shared select wiring applies them (`CandidatesRelationPicker`\n * and `useRelationPicker`): the gate, the resolved candidate filters prepended to the caller's\n * `fixedFilters`, and whether a loaded row fits them (`undefined` when no candidate filter applies).\n */\nexport function useCandidateSelection(input: {\n readonly to: string | undefined;\n readonly candidates: readonly RelationCandidate[] | undefined;\n readonly sources: ScreenSources | undefined;\n readonly resource: string;\n readonly fieldName: string;\n readonly fixedFilters: readonly FixedFilterInput[] | undefined;\n}): {\n readonly fixedFilters: readonly FixedFilterInput[] | undefined;\n readonly gate?: CandidatesGate;\n readonly rowFits?: (row: Record<string, unknown>) => boolean | undefined;\n} {\n const { fixedFilters } = input;\n const gate = useCandidatesGate(input);\n const filters = gate.filters;\n const combined = React.useMemo(\n () => (filters.length === 0 ? fixedFilters : [...filters, ...(fixedFilters ?? [])]),\n [filters, fixedFilters],\n );\n const rowFits = React.useMemo(\n () =>\n filters.length === 0 ? undefined : (row: Record<string, unknown>) => rowMatches(row, filters),\n [filters],\n );\n return {\n fixedFilters: combined,\n ...(gate.gate === undefined ? {} : { gate: gate.gate }),\n ...(rowFits === undefined ? {} : { rowFits }),\n };\n}\n"],"mappings":"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"}
|