@cfast/ui 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +727 -0
- package/dist/chunk-755IRYDN.js +941 -0
- package/dist/chunk-7SNK37GF.js +418 -0
- package/dist/chunk-ASMYTWTR.js +356 -0
- package/dist/chunk-B2XXH5V4.js +66 -0
- package/dist/chunk-BQMXYYEV.js +348 -0
- package/dist/chunk-DTKBXCTU.js +211 -0
- package/dist/chunk-EYIBATYR.js +33 -0
- package/dist/chunk-FPZAQ2YQ.js +474 -0
- package/dist/chunk-G2OU4BYC.js +205 -0
- package/dist/chunk-JEGEIQ3R.js +925 -0
- package/dist/chunk-JUNLQJ6H.js +1013 -0
- package/dist/chunk-NRGMW3JA.js +906 -0
- package/dist/chunk-Q6FPL2OJ.js +1086 -0
- package/dist/chunk-QHWAGKNW.js +456 -0
- package/dist/chunk-QZT62CGJ.js +924 -0
- package/dist/chunk-RDTUEOLK.js +486 -0
- package/dist/chunk-RESL4IJJ.js +112 -0
- package/dist/chunk-UDCWQUTR.js +221 -0
- package/dist/chunk-UE7PZOIJ.js +11 -0
- package/dist/chunk-UTZTHGNE.js +84 -0
- package/dist/chunk-UVRXMOX5.js +439 -0
- package/dist/chunk-XFD3N2D4.js +161 -0
- package/dist/client-CXIHCQtA.d.ts +274 -0
- package/dist/client.d.ts +617 -0
- package/dist/client.js +54 -0
- package/dist/index.d.ts +415 -0
- package/dist/index.js +296 -0
- package/dist/joy.d.ts +199 -0
- package/dist/joy.js +1150 -0
- package/dist/permission-gate-DVmY42oz.d.ts +1269 -0
- package/dist/permission-gate-apt9T9Mu.d.ts +1256 -0
- package/dist/types-1bAiH2uK.d.ts +392 -0
- package/dist/types-BX6u5sAd.d.ts +403 -0
- package/dist/types-BpdY7w5l.d.ts +403 -0
- package/dist/types-BrepeVp8.d.ts +403 -0
- package/dist/types-BvAqMZhn.d.ts +403 -0
- package/dist/types-C74nSscq.d.ts +403 -0
- package/dist/types-DD1Cpx8F.d.ts +403 -0
- package/dist/types-DHUhQwJn.d.ts +403 -0
- package/dist/types-DZSJNt_M.d.ts +392 -0
- package/dist/types-DaaJiIjW.d.ts +391 -0
- package/dist/types-LUpWJwps.d.ts +403 -0
- package/dist/types-a7zVU6WE.d.ts +394 -0
- package/dist/types-biJTHMcH.d.ts +403 -0
- package/dist/types-ow_qSEYJ.d.ts +392 -0
- package/dist/types-wnLasZaB.d.ts +1234 -0
- package/package.json +88 -0
|
@@ -0,0 +1,1013 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fieldForColumn,
|
|
3
|
+
useActionStatus,
|
|
4
|
+
useComponent,
|
|
5
|
+
useToast
|
|
6
|
+
} from "./chunk-FPZAQ2YQ.js";
|
|
7
|
+
|
|
8
|
+
// src/hooks/use-confirm.ts
|
|
9
|
+
import { createContext, useContext, useCallback } from "react";
|
|
10
|
+
var ConfirmContext = createContext(null);
|
|
11
|
+
function useConfirm() {
|
|
12
|
+
const ctx = useContext(ConfirmContext);
|
|
13
|
+
if (!ctx) {
|
|
14
|
+
throw new Error("useConfirm must be used within a <ConfirmProvider>");
|
|
15
|
+
}
|
|
16
|
+
return useCallback(
|
|
17
|
+
(options) => ctx.confirm(options),
|
|
18
|
+
[ctx]
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/hooks/use-action-toast.ts
|
|
23
|
+
import { useEffect, useRef } from "react";
|
|
24
|
+
import { useActions } from "@cfast/actions/client";
|
|
25
|
+
function useActionToast(descriptor, config) {
|
|
26
|
+
const actions = useActions(descriptor);
|
|
27
|
+
const toast = useToast();
|
|
28
|
+
const prevDataRef = useRef({});
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
for (const [name, cfg] of Object.entries(config)) {
|
|
31
|
+
const actionFn = actions[name];
|
|
32
|
+
if (!actionFn) continue;
|
|
33
|
+
const result = actionFn();
|
|
34
|
+
const prevData = prevDataRef.current[name];
|
|
35
|
+
if (result.data !== void 0 && result.data !== prevData) {
|
|
36
|
+
prevDataRef.current[name] = result.data;
|
|
37
|
+
if (cfg.success) {
|
|
38
|
+
toast.success(cfg.success);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (result.error !== void 0 && result.error !== prevData) {
|
|
42
|
+
prevDataRef.current[name] = result.error;
|
|
43
|
+
if (cfg.error) {
|
|
44
|
+
toast.error(cfg.error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/components/permission-gate.tsx
|
|
52
|
+
import { createElement, Fragment } from "react";
|
|
53
|
+
function PermissionGate({
|
|
54
|
+
action,
|
|
55
|
+
actionName,
|
|
56
|
+
input,
|
|
57
|
+
children,
|
|
58
|
+
fallback
|
|
59
|
+
}) {
|
|
60
|
+
const status = useActionStatus(action, actionName, input);
|
|
61
|
+
if (status.invisible) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (!status.permitted) {
|
|
65
|
+
return fallback ? createElement(Fragment, null, fallback) : null;
|
|
66
|
+
}
|
|
67
|
+
return createElement(Fragment, null, children);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/components/action-button.tsx
|
|
71
|
+
import { createElement as createElement2 } from "react";
|
|
72
|
+
function ActionButton({
|
|
73
|
+
action,
|
|
74
|
+
actionName,
|
|
75
|
+
input,
|
|
76
|
+
children,
|
|
77
|
+
whenForbidden = "disable",
|
|
78
|
+
confirmation: _confirmation,
|
|
79
|
+
variant,
|
|
80
|
+
color,
|
|
81
|
+
size,
|
|
82
|
+
startDecorator
|
|
83
|
+
}) {
|
|
84
|
+
const status = useActionStatus(action, actionName, input);
|
|
85
|
+
const Button = useComponent("button");
|
|
86
|
+
if (status.invisible) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (!status.permitted && whenForbidden === "hide") {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const disabled = !status.permitted && whenForbidden === "disable";
|
|
93
|
+
return createElement2(Button, {
|
|
94
|
+
children,
|
|
95
|
+
onClick: () => status.submit(),
|
|
96
|
+
disabled,
|
|
97
|
+
loading: status.pending,
|
|
98
|
+
variant,
|
|
99
|
+
color,
|
|
100
|
+
size,
|
|
101
|
+
startDecorator
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/components/confirm-provider.tsx
|
|
106
|
+
import { createElement as createElement3, useState, useCallback as useCallback2, useRef as useRef2 } from "react";
|
|
107
|
+
function ConfirmProvider({ children }) {
|
|
108
|
+
const [state, setState] = useState(null);
|
|
109
|
+
const ConfirmDialog = useComponent("confirmDialog");
|
|
110
|
+
const resolveRef = useRef2(null);
|
|
111
|
+
const confirm = useCallback2((options) => {
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
resolveRef.current = resolve;
|
|
114
|
+
setState({ ...options, resolve });
|
|
115
|
+
});
|
|
116
|
+
}, []);
|
|
117
|
+
const handleClose = useCallback2(() => {
|
|
118
|
+
resolveRef.current?.(false);
|
|
119
|
+
resolveRef.current = null;
|
|
120
|
+
setState(null);
|
|
121
|
+
}, []);
|
|
122
|
+
const handleConfirm = useCallback2(() => {
|
|
123
|
+
resolveRef.current?.(true);
|
|
124
|
+
resolveRef.current = null;
|
|
125
|
+
setState(null);
|
|
126
|
+
}, []);
|
|
127
|
+
return createElement3(
|
|
128
|
+
ConfirmContext.Provider,
|
|
129
|
+
{ value: { confirm } },
|
|
130
|
+
children,
|
|
131
|
+
state ? createElement3(ConfirmDialog, {
|
|
132
|
+
open: true,
|
|
133
|
+
onClose: handleClose,
|
|
134
|
+
onConfirm: handleConfirm,
|
|
135
|
+
title: state.title,
|
|
136
|
+
description: state.description,
|
|
137
|
+
confirmLabel: state.confirmLabel,
|
|
138
|
+
cancelLabel: state.cancelLabel,
|
|
139
|
+
variant: state.variant
|
|
140
|
+
}) : null
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/components/form-status.tsx
|
|
145
|
+
import { createElement as createElement4 } from "react";
|
|
146
|
+
function FormStatus({ data }) {
|
|
147
|
+
const Alert = useComponent("alert");
|
|
148
|
+
if (!data) return null;
|
|
149
|
+
const elements = [];
|
|
150
|
+
if (data.success) {
|
|
151
|
+
elements.push(
|
|
152
|
+
createElement4(Alert, { key: "success", color: "success", children: data.success })
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (data.error) {
|
|
156
|
+
elements.push(
|
|
157
|
+
createElement4(Alert, { key: "error", color: "danger", children: data.error })
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (data.fieldErrors) {
|
|
161
|
+
const errorMessages = Object.entries(data.fieldErrors).flatMap(
|
|
162
|
+
([field, errors]) => errors.map((err) => `${field}: ${err}`)
|
|
163
|
+
);
|
|
164
|
+
if (errorMessages.length > 0) {
|
|
165
|
+
elements.push(
|
|
166
|
+
createElement4(Alert, {
|
|
167
|
+
key: "field-errors",
|
|
168
|
+
color: "danger",
|
|
169
|
+
children: createElement4(
|
|
170
|
+
"ul",
|
|
171
|
+
{ style: { margin: 0, paddingLeft: "16px" } },
|
|
172
|
+
...errorMessages.map(
|
|
173
|
+
(msg, i) => createElement4("li", { key: i }, msg)
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (elements.length === 0) return null;
|
|
181
|
+
return createElement4("div", { style: { display: "flex", flexDirection: "column", gap: "8px" } }, ...elements);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/components/role-badge.tsx
|
|
185
|
+
import { createElement as createElement5 } from "react";
|
|
186
|
+
var defaultColors = {
|
|
187
|
+
admin: "danger",
|
|
188
|
+
editor: "primary",
|
|
189
|
+
author: "success",
|
|
190
|
+
reader: "neutral"
|
|
191
|
+
};
|
|
192
|
+
function RoleBadge({ role, colors }) {
|
|
193
|
+
const Chip = useComponent("chip");
|
|
194
|
+
const colorMap = colors ? { ...defaultColors, ...Object.fromEntries(
|
|
195
|
+
Object.entries(colors).map(([k, v]) => [k, v])
|
|
196
|
+
) } : defaultColors;
|
|
197
|
+
const chipColor = colorMap[role] ?? "neutral";
|
|
198
|
+
return createElement5(Chip, {
|
|
199
|
+
children: role,
|
|
200
|
+
color: chipColor,
|
|
201
|
+
variant: "soft",
|
|
202
|
+
size: "sm"
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/components/impersonation-banner.tsx
|
|
207
|
+
import { createElement as createElement6 } from "react";
|
|
208
|
+
import { useCurrentUser } from "@cfast/auth/client";
|
|
209
|
+
function ImpersonationBanner({
|
|
210
|
+
stopAction = "/admin/stop-impersonation"
|
|
211
|
+
}) {
|
|
212
|
+
const user = useCurrentUser();
|
|
213
|
+
const Alert = useComponent("alert");
|
|
214
|
+
const Button = useComponent("button");
|
|
215
|
+
if (!user?.isImpersonating) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
return createElement6(
|
|
219
|
+
Alert,
|
|
220
|
+
{
|
|
221
|
+
color: "warning",
|
|
222
|
+
children: createElement6(
|
|
223
|
+
"div",
|
|
224
|
+
{
|
|
225
|
+
style: {
|
|
226
|
+
display: "flex",
|
|
227
|
+
alignItems: "center",
|
|
228
|
+
justifyContent: "center",
|
|
229
|
+
gap: "12px"
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
createElement6(
|
|
233
|
+
"strong",
|
|
234
|
+
null,
|
|
235
|
+
`Viewing as ${user.name} (${user.email})`
|
|
236
|
+
),
|
|
237
|
+
createElement6(
|
|
238
|
+
"form",
|
|
239
|
+
{ method: "post", action: stopAction },
|
|
240
|
+
createElement6(Button, {
|
|
241
|
+
type: "submit",
|
|
242
|
+
variant: "outlined",
|
|
243
|
+
size: "sm",
|
|
244
|
+
children: "Stop Impersonating"
|
|
245
|
+
})
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/components/data-table.tsx
|
|
253
|
+
import { createElement as createElement7, useState as useState2, useCallback as useCallback3 } from "react";
|
|
254
|
+
function normalizeColumns(columns) {
|
|
255
|
+
if (!columns) return [];
|
|
256
|
+
return columns.map((col) => {
|
|
257
|
+
if (typeof col === "string") {
|
|
258
|
+
return {
|
|
259
|
+
key: col,
|
|
260
|
+
label: col.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim(),
|
|
261
|
+
sortable: true
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return col;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function DataTable({
|
|
268
|
+
data,
|
|
269
|
+
columns: columnsProp,
|
|
270
|
+
selectable = false,
|
|
271
|
+
selectedRows: externalSelectedRows,
|
|
272
|
+
onSelectionChange,
|
|
273
|
+
onRowClick,
|
|
274
|
+
getRowId,
|
|
275
|
+
emptyMessage = "No data"
|
|
276
|
+
}) {
|
|
277
|
+
const Table = useComponent("table");
|
|
278
|
+
const TableHead = useComponent("tableHead");
|
|
279
|
+
const TableBody = useComponent("tableBody");
|
|
280
|
+
const TableRow = useComponent("tableRow");
|
|
281
|
+
const TableCell = useComponent("tableCell");
|
|
282
|
+
const columns = normalizeColumns(columnsProp);
|
|
283
|
+
const [sortKey, setSortKey] = useState2(null);
|
|
284
|
+
const [sortDir, setSortDir] = useState2("asc");
|
|
285
|
+
const [internalSelected, setInternalSelected] = useState2(/* @__PURE__ */ new Set());
|
|
286
|
+
const selectedSet = externalSelectedRows ? new Set(externalSelectedRows.map((r) => (getRowId ?? defaultGetId)(r))) : internalSelected;
|
|
287
|
+
const handleSort = useCallback3((key) => {
|
|
288
|
+
if (sortKey === key) {
|
|
289
|
+
setSortDir((d) => d === "asc" ? "desc" : "asc");
|
|
290
|
+
} else {
|
|
291
|
+
setSortKey(key);
|
|
292
|
+
setSortDir("asc");
|
|
293
|
+
}
|
|
294
|
+
}, [sortKey]);
|
|
295
|
+
const toggleRow = useCallback3((id) => {
|
|
296
|
+
if (onSelectionChange) {
|
|
297
|
+
const row = data.items.find((r) => (getRowId ?? defaultGetId)(r) === id);
|
|
298
|
+
if (!row) return;
|
|
299
|
+
const current = externalSelectedRows ?? [];
|
|
300
|
+
const isSelected = current.some((r) => (getRowId ?? defaultGetId)(r) === id);
|
|
301
|
+
onSelectionChange(isSelected ? current.filter((r) => (getRowId ?? defaultGetId)(r) !== id) : [...current, row]);
|
|
302
|
+
} else {
|
|
303
|
+
setInternalSelected((prev) => {
|
|
304
|
+
const next = new Set(prev);
|
|
305
|
+
if (next.has(id)) next.delete(id);
|
|
306
|
+
else next.add(id);
|
|
307
|
+
return next;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}, [data.items, externalSelectedRows, onSelectionChange, getRowId]);
|
|
311
|
+
if (data.items.length === 0 && !data.isLoading) {
|
|
312
|
+
return createElement7("div", { style: { textAlign: "center", padding: "32px", color: "#666" } }, emptyMessage);
|
|
313
|
+
}
|
|
314
|
+
return createElement7(
|
|
315
|
+
Table,
|
|
316
|
+
{ hoverRow: true, children: createElement7("div") },
|
|
317
|
+
createElement7(
|
|
318
|
+
TableHead,
|
|
319
|
+
{ children: createElement7("div") },
|
|
320
|
+
createElement7(
|
|
321
|
+
TableRow,
|
|
322
|
+
{ children: createElement7("div") },
|
|
323
|
+
selectable ? createElement7(TableCell, { header: true, children: "" }) : null,
|
|
324
|
+
...columns.map(
|
|
325
|
+
(col) => createElement7(TableCell, {
|
|
326
|
+
key: col.key,
|
|
327
|
+
header: true,
|
|
328
|
+
sortable: col.sortable !== false,
|
|
329
|
+
sortDirection: sortKey === col.key ? sortDir : null,
|
|
330
|
+
onSort: () => handleSort(col.key),
|
|
331
|
+
children: col.label ?? col.key
|
|
332
|
+
})
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
),
|
|
336
|
+
createElement7(
|
|
337
|
+
TableBody,
|
|
338
|
+
{ children: createElement7("div") },
|
|
339
|
+
...data.items.map((row) => {
|
|
340
|
+
const id = (getRowId ?? defaultGetId)(row);
|
|
341
|
+
const isSelected = selectedSet.has(id);
|
|
342
|
+
return createElement7(
|
|
343
|
+
TableRow,
|
|
344
|
+
{
|
|
345
|
+
key: String(id),
|
|
346
|
+
selected: isSelected,
|
|
347
|
+
onClick: onRowClick ? () => onRowClick(row) : void 0,
|
|
348
|
+
children: createElement7("div")
|
|
349
|
+
},
|
|
350
|
+
selectable ? createElement7(TableCell, {
|
|
351
|
+
children: createElement7("input", {
|
|
352
|
+
type: "checkbox",
|
|
353
|
+
checked: isSelected,
|
|
354
|
+
onChange: () => toggleRow(id)
|
|
355
|
+
})
|
|
356
|
+
}) : null,
|
|
357
|
+
...columns.map((col) => {
|
|
358
|
+
const value = row[col.key];
|
|
359
|
+
return createElement7(TableCell, {
|
|
360
|
+
key: col.key,
|
|
361
|
+
children: col.render ? col.render(value, row) : String(value ?? "")
|
|
362
|
+
});
|
|
363
|
+
})
|
|
364
|
+
);
|
|
365
|
+
})
|
|
366
|
+
)
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
function defaultGetId(row) {
|
|
370
|
+
return row.id;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/components/filter-bar.tsx
|
|
374
|
+
import { createElement as createElement8, useCallback as useCallback4 } from "react";
|
|
375
|
+
import { useSearchParams, useNavigate, useLocation } from "react-router";
|
|
376
|
+
function FilterBar({
|
|
377
|
+
filters,
|
|
378
|
+
searchable
|
|
379
|
+
}) {
|
|
380
|
+
const [searchParams] = useSearchParams();
|
|
381
|
+
const navigate = useNavigate();
|
|
382
|
+
const location = useLocation();
|
|
383
|
+
const updateParam = useCallback4(
|
|
384
|
+
(key, value) => {
|
|
385
|
+
const params = new URLSearchParams(searchParams);
|
|
386
|
+
if (value === null || value === "") {
|
|
387
|
+
params.delete(key);
|
|
388
|
+
} else {
|
|
389
|
+
params.set(key, value);
|
|
390
|
+
}
|
|
391
|
+
params.delete("page");
|
|
392
|
+
params.delete("cursor");
|
|
393
|
+
navigate(`${location.pathname}?${params.toString()}`);
|
|
394
|
+
},
|
|
395
|
+
[searchParams, navigate, location.pathname]
|
|
396
|
+
);
|
|
397
|
+
return createElement8(
|
|
398
|
+
"div",
|
|
399
|
+
{
|
|
400
|
+
style: {
|
|
401
|
+
display: "flex",
|
|
402
|
+
gap: "8px",
|
|
403
|
+
flexWrap: "wrap",
|
|
404
|
+
alignItems: "center",
|
|
405
|
+
marginBottom: "16px"
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
// Search input
|
|
409
|
+
searchable && searchable.length > 0 ? createElement8("input", {
|
|
410
|
+
type: "search",
|
|
411
|
+
placeholder: `Search ${searchable.join(", ")}...`,
|
|
412
|
+
value: searchParams.get("q") ?? "",
|
|
413
|
+
onChange: (e) => updateParam("q", e.target.value || null),
|
|
414
|
+
style: { padding: "6px 10px", border: "1px solid #ccc", borderRadius: "4px" }
|
|
415
|
+
}) : null,
|
|
416
|
+
...filters.map(
|
|
417
|
+
(filter) => createElement8(FilterInput, {
|
|
418
|
+
key: filter.column,
|
|
419
|
+
filter,
|
|
420
|
+
value: searchParams.get(filter.column) ?? "",
|
|
421
|
+
onChange: (value) => updateParam(filter.column, value || null)
|
|
422
|
+
})
|
|
423
|
+
)
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
function FilterInput({
|
|
427
|
+
filter,
|
|
428
|
+
value,
|
|
429
|
+
onChange
|
|
430
|
+
}) {
|
|
431
|
+
const label = filter.label ?? filter.column;
|
|
432
|
+
switch (filter.type) {
|
|
433
|
+
case "select":
|
|
434
|
+
case "boolean":
|
|
435
|
+
return createElement8(
|
|
436
|
+
"select",
|
|
437
|
+
{
|
|
438
|
+
value,
|
|
439
|
+
onChange: (e) => onChange(e.target.value),
|
|
440
|
+
"aria-label": label
|
|
441
|
+
},
|
|
442
|
+
createElement8("option", { value: "" }, `All ${label}`),
|
|
443
|
+
...(filter.options ?? []).map(
|
|
444
|
+
(opt) => createElement8("option", { key: String(opt.value), value: String(opt.value) }, opt.label)
|
|
445
|
+
)
|
|
446
|
+
);
|
|
447
|
+
case "text":
|
|
448
|
+
default:
|
|
449
|
+
return createElement8("input", {
|
|
450
|
+
type: "text",
|
|
451
|
+
placeholder: filter.placeholder ?? label,
|
|
452
|
+
value,
|
|
453
|
+
onChange: (e) => onChange(e.target.value),
|
|
454
|
+
"aria-label": label,
|
|
455
|
+
style: { padding: "6px 10px", border: "1px solid #ccc", borderRadius: "4px" }
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/components/bulk-action-bar.tsx
|
|
461
|
+
import { createElement as createElement9 } from "react";
|
|
462
|
+
function BulkActionBar({
|
|
463
|
+
selectedCount,
|
|
464
|
+
actions,
|
|
465
|
+
onAction,
|
|
466
|
+
onClearSelection
|
|
467
|
+
}) {
|
|
468
|
+
const Button = useComponent("button");
|
|
469
|
+
if (selectedCount === 0) return null;
|
|
470
|
+
return createElement9(
|
|
471
|
+
"div",
|
|
472
|
+
{
|
|
473
|
+
style: {
|
|
474
|
+
display: "flex",
|
|
475
|
+
alignItems: "center",
|
|
476
|
+
gap: "8px",
|
|
477
|
+
padding: "8px 16px",
|
|
478
|
+
backgroundColor: "#f0f4ff",
|
|
479
|
+
borderRadius: "4px",
|
|
480
|
+
marginBottom: "8px"
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
createElement9(
|
|
484
|
+
"span",
|
|
485
|
+
null,
|
|
486
|
+
`${selectedCount} selected`
|
|
487
|
+
),
|
|
488
|
+
...actions.map(
|
|
489
|
+
(action) => createElement9(Button, {
|
|
490
|
+
key: action.label,
|
|
491
|
+
children: action.label,
|
|
492
|
+
onClick: () => onAction(action),
|
|
493
|
+
variant: "soft",
|
|
494
|
+
size: "sm",
|
|
495
|
+
startDecorator: action.icon ? createElement9(action.icon, { className: "bulk-action-icon" }) : void 0
|
|
496
|
+
})
|
|
497
|
+
),
|
|
498
|
+
createElement9(Button, {
|
|
499
|
+
children: "Clear",
|
|
500
|
+
onClick: onClearSelection,
|
|
501
|
+
variant: "plain",
|
|
502
|
+
size: "sm"
|
|
503
|
+
})
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/hooks/use-column-inference.ts
|
|
508
|
+
import { useMemo } from "react";
|
|
509
|
+
function useColumnInference(table, columns) {
|
|
510
|
+
return useMemo(() => {
|
|
511
|
+
if (!table) return [];
|
|
512
|
+
const result = [];
|
|
513
|
+
for (const [key, col] of Object.entries(table)) {
|
|
514
|
+
if (columns && !columns.includes(key)) continue;
|
|
515
|
+
if (!col || typeof col !== "object" || !("dataType" in col) || !("name" in col)) continue;
|
|
516
|
+
const meta = col;
|
|
517
|
+
const field = fieldForColumn(meta);
|
|
518
|
+
const label = key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
|
|
519
|
+
result.push({
|
|
520
|
+
key,
|
|
521
|
+
label,
|
|
522
|
+
sortable: true,
|
|
523
|
+
field
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
if (columns) {
|
|
527
|
+
result.sort((a, b) => columns.indexOf(a.key) - columns.indexOf(b.key));
|
|
528
|
+
}
|
|
529
|
+
return result;
|
|
530
|
+
}, [table, columns]);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/components/drop-zone.tsx
|
|
534
|
+
import { createElement as createElement10, useState as useState3, useCallback as useCallback5, useRef as useRef3 } from "react";
|
|
535
|
+
function DropZone({
|
|
536
|
+
upload,
|
|
537
|
+
multiple = false,
|
|
538
|
+
children
|
|
539
|
+
}) {
|
|
540
|
+
const DropZoneSlot = useComponent("dropZone");
|
|
541
|
+
const [isDragOver, setIsDragOver] = useState3(false);
|
|
542
|
+
const inputRef = useRef3(null);
|
|
543
|
+
const handleFiles = useCallback5(
|
|
544
|
+
(files) => {
|
|
545
|
+
if (!files || files.length === 0) return;
|
|
546
|
+
if (multiple) {
|
|
547
|
+
for (let i = 0; i < files.length; i++) {
|
|
548
|
+
upload.start(files[i]);
|
|
549
|
+
}
|
|
550
|
+
} else {
|
|
551
|
+
upload.start(files[0]);
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
[upload, multiple]
|
|
555
|
+
);
|
|
556
|
+
const handleDrop = useCallback5(
|
|
557
|
+
(files) => {
|
|
558
|
+
setIsDragOver(false);
|
|
559
|
+
handleFiles(files);
|
|
560
|
+
},
|
|
561
|
+
[handleFiles]
|
|
562
|
+
);
|
|
563
|
+
const handleClick = useCallback5(() => {
|
|
564
|
+
inputRef.current?.click();
|
|
565
|
+
}, []);
|
|
566
|
+
const handleDragOver = useCallback5((_e) => {
|
|
567
|
+
setIsDragOver(true);
|
|
568
|
+
}, []);
|
|
569
|
+
const handleDragLeave = useCallback5(() => {
|
|
570
|
+
setIsDragOver(false);
|
|
571
|
+
}, []);
|
|
572
|
+
const defaultContent = upload.isUploading ? `Uploading... ${upload.progress}%` : upload.error ?? upload.validationError ?? "Drop files here or click to browse";
|
|
573
|
+
return createElement10(
|
|
574
|
+
"div",
|
|
575
|
+
null,
|
|
576
|
+
createElement10(DropZoneSlot, {
|
|
577
|
+
isDragOver,
|
|
578
|
+
isInvalid: !!(upload.error || upload.validationError),
|
|
579
|
+
onClick: handleClick,
|
|
580
|
+
onDrop: handleDrop,
|
|
581
|
+
onDragOver: handleDragOver,
|
|
582
|
+
onDragLeave: handleDragLeave,
|
|
583
|
+
accept: upload.accept,
|
|
584
|
+
children: children ?? defaultContent
|
|
585
|
+
}),
|
|
586
|
+
createElement10("input", {
|
|
587
|
+
ref: inputRef,
|
|
588
|
+
type: "file",
|
|
589
|
+
accept: upload.accept,
|
|
590
|
+
multiple,
|
|
591
|
+
style: { display: "none" },
|
|
592
|
+
onChange: (e) => handleFiles(e.target.files)
|
|
593
|
+
})
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/components/image-preview.tsx
|
|
598
|
+
import { createElement as createElement11 } from "react";
|
|
599
|
+
function ImagePreview({
|
|
600
|
+
fileKey,
|
|
601
|
+
src,
|
|
602
|
+
getUrl,
|
|
603
|
+
width = 200,
|
|
604
|
+
height = 200,
|
|
605
|
+
fallback,
|
|
606
|
+
alt = "Image preview"
|
|
607
|
+
}) {
|
|
608
|
+
const resolvedSrc = src ?? (fileKey && getUrl ? getUrl(fileKey) : null);
|
|
609
|
+
if (!resolvedSrc) {
|
|
610
|
+
return fallback ? createElement11("div", null, fallback) : createElement11(
|
|
611
|
+
"div",
|
|
612
|
+
{
|
|
613
|
+
style: {
|
|
614
|
+
width,
|
|
615
|
+
height,
|
|
616
|
+
backgroundColor: "#f5f5f5",
|
|
617
|
+
display: "flex",
|
|
618
|
+
alignItems: "center",
|
|
619
|
+
justifyContent: "center",
|
|
620
|
+
borderRadius: "4px",
|
|
621
|
+
color: "#999"
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
"No image"
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
return createElement11("img", {
|
|
628
|
+
src: resolvedSrc,
|
|
629
|
+
alt,
|
|
630
|
+
style: {
|
|
631
|
+
width,
|
|
632
|
+
height,
|
|
633
|
+
objectFit: "cover",
|
|
634
|
+
borderRadius: "4px"
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/components/file-list.tsx
|
|
640
|
+
import { createElement as createElement12 } from "react";
|
|
641
|
+
function formatBytes(bytes) {
|
|
642
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
643
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
644
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
645
|
+
}
|
|
646
|
+
function FileList({
|
|
647
|
+
files,
|
|
648
|
+
onDownload
|
|
649
|
+
}) {
|
|
650
|
+
if (files.length === 0) {
|
|
651
|
+
return createElement12("div", { style: { color: "#999" } }, "No files");
|
|
652
|
+
}
|
|
653
|
+
return createElement12(
|
|
654
|
+
"ul",
|
|
655
|
+
{
|
|
656
|
+
style: { listStyle: "none", padding: 0, margin: 0 },
|
|
657
|
+
"data-testid": "file-list"
|
|
658
|
+
},
|
|
659
|
+
...files.map(
|
|
660
|
+
(file) => createElement12(FileListItem, {
|
|
661
|
+
key: file.key,
|
|
662
|
+
file,
|
|
663
|
+
onDownload
|
|
664
|
+
})
|
|
665
|
+
)
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
function FileListItem({
|
|
669
|
+
file,
|
|
670
|
+
onDownload
|
|
671
|
+
}) {
|
|
672
|
+
return createElement12(
|
|
673
|
+
"li",
|
|
674
|
+
{
|
|
675
|
+
style: {
|
|
676
|
+
display: "flex",
|
|
677
|
+
alignItems: "center",
|
|
678
|
+
gap: "8px",
|
|
679
|
+
padding: "8px 0",
|
|
680
|
+
borderBottom: "1px solid #eee"
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
createElement12("span", { style: { flex: 1 } }, file.name),
|
|
684
|
+
file.size != null ? createElement12("span", { style: { color: "#666", fontSize: "0.85em" } }, formatBytes(file.size)) : null,
|
|
685
|
+
onDownload ? createElement12(
|
|
686
|
+
"button",
|
|
687
|
+
{
|
|
688
|
+
onClick: () => onDownload(file),
|
|
689
|
+
style: {
|
|
690
|
+
background: "none",
|
|
691
|
+
border: "none",
|
|
692
|
+
cursor: "pointer",
|
|
693
|
+
color: "#1976d2",
|
|
694
|
+
textDecoration: "underline"
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
"Download"
|
|
698
|
+
) : file.url ? createElement12(
|
|
699
|
+
"a",
|
|
700
|
+
{
|
|
701
|
+
href: file.url,
|
|
702
|
+
download: file.name,
|
|
703
|
+
style: { color: "#1976d2", textDecoration: "underline" }
|
|
704
|
+
},
|
|
705
|
+
"Download"
|
|
706
|
+
) : null
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/components/page-container.tsx
|
|
711
|
+
import { createElement as createElement13, Fragment as Fragment2 } from "react";
|
|
712
|
+
function PageContainer({
|
|
713
|
+
title,
|
|
714
|
+
breadcrumb,
|
|
715
|
+
actions,
|
|
716
|
+
tabs: _tabs,
|
|
717
|
+
children
|
|
718
|
+
}) {
|
|
719
|
+
const PageContainerSlot = useComponent("pageContainer");
|
|
720
|
+
const Breadcrumb = useComponent("breadcrumb");
|
|
721
|
+
return createElement13(
|
|
722
|
+
Fragment2,
|
|
723
|
+
null,
|
|
724
|
+
breadcrumb && breadcrumb.length > 0 ? createElement13(Breadcrumb, { items: breadcrumb }) : null,
|
|
725
|
+
createElement13(PageContainerSlot, { title, actions, children })
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/components/empty-state.tsx
|
|
730
|
+
import { createElement as createElement14 } from "react";
|
|
731
|
+
function EmptyState({
|
|
732
|
+
title,
|
|
733
|
+
description,
|
|
734
|
+
createAction,
|
|
735
|
+
createLabel = "Create",
|
|
736
|
+
icon: Icon
|
|
737
|
+
}) {
|
|
738
|
+
const Button = useComponent("button");
|
|
739
|
+
if (!createAction) {
|
|
740
|
+
return createElement14(
|
|
741
|
+
"div",
|
|
742
|
+
{ style: { textAlign: "center", padding: "48px 16px" } },
|
|
743
|
+
Icon ? createElement14(Icon, { className: "empty-state-icon" }) : null,
|
|
744
|
+
createElement14("h3", { style: { margin: "16px 0 8px" } }, title),
|
|
745
|
+
description ? createElement14("p", { style: { color: "#666" } }, description) : null
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
return createElement14(EmptyStateWithAction, {
|
|
749
|
+
title,
|
|
750
|
+
description,
|
|
751
|
+
createAction,
|
|
752
|
+
createLabel,
|
|
753
|
+
icon: Icon,
|
|
754
|
+
Button
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
function EmptyStateWithAction({
|
|
758
|
+
title,
|
|
759
|
+
description,
|
|
760
|
+
createAction,
|
|
761
|
+
createLabel,
|
|
762
|
+
icon: Icon,
|
|
763
|
+
Button
|
|
764
|
+
}) {
|
|
765
|
+
const status = useActionStatus(createAction);
|
|
766
|
+
if (status.invisible) {
|
|
767
|
+
return createElement14(
|
|
768
|
+
"div",
|
|
769
|
+
{ style: { textAlign: "center", padding: "48px 16px" } },
|
|
770
|
+
createElement14("h3", { style: { margin: "16px 0 8px" } }, "Nothing here yet")
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
return createElement14(
|
|
774
|
+
"div",
|
|
775
|
+
{ style: { textAlign: "center", padding: "48px 16px" } },
|
|
776
|
+
Icon ? createElement14(Icon, { className: "empty-state-icon" }) : null,
|
|
777
|
+
createElement14("h3", { style: { margin: "16px 0 8px" } }, title),
|
|
778
|
+
description ? createElement14("p", { style: { color: "#666" } }, description) : null,
|
|
779
|
+
status.permitted ? createElement14(
|
|
780
|
+
"div",
|
|
781
|
+
{ style: { marginTop: "16px" } },
|
|
782
|
+
createElement14(Button, {
|
|
783
|
+
children: createLabel,
|
|
784
|
+
onClick: () => status.submit(),
|
|
785
|
+
loading: status.pending
|
|
786
|
+
})
|
|
787
|
+
) : null
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/components/list-view.tsx
|
|
792
|
+
import { createElement as createElement15, useState as useState4, useCallback as useCallback6 } from "react";
|
|
793
|
+
function ListView({
|
|
794
|
+
title,
|
|
795
|
+
data,
|
|
796
|
+
table: _table,
|
|
797
|
+
columns,
|
|
798
|
+
actions: _actions,
|
|
799
|
+
filters,
|
|
800
|
+
searchable,
|
|
801
|
+
createAction,
|
|
802
|
+
createLabel = "Create",
|
|
803
|
+
selectable = false,
|
|
804
|
+
bulkActions,
|
|
805
|
+
breadcrumb
|
|
806
|
+
}) {
|
|
807
|
+
const [selectedRows, setSelectedRows] = useState4([]);
|
|
808
|
+
const handleBulkAction = useCallback6(
|
|
809
|
+
(action) => {
|
|
810
|
+
if (action.handler) {
|
|
811
|
+
action.handler(selectedRows);
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
[selectedRows]
|
|
815
|
+
);
|
|
816
|
+
const clearSelection = useCallback6(() => {
|
|
817
|
+
setSelectedRows([]);
|
|
818
|
+
}, []);
|
|
819
|
+
const createButton = createAction ? createElement15(ActionButton, {
|
|
820
|
+
action: createAction,
|
|
821
|
+
children: createLabel,
|
|
822
|
+
variant: "solid",
|
|
823
|
+
color: "primary"
|
|
824
|
+
}) : null;
|
|
825
|
+
return createElement15(
|
|
826
|
+
PageContainer,
|
|
827
|
+
{
|
|
828
|
+
title,
|
|
829
|
+
breadcrumb,
|
|
830
|
+
actions: createButton,
|
|
831
|
+
children: createElement15(
|
|
832
|
+
"div",
|
|
833
|
+
null,
|
|
834
|
+
// Filters
|
|
835
|
+
filters && filters.length > 0 ? createElement15(FilterBar, {
|
|
836
|
+
filters,
|
|
837
|
+
searchable
|
|
838
|
+
}) : null,
|
|
839
|
+
// Bulk actions
|
|
840
|
+
selectable && bulkActions && bulkActions.length > 0 ? createElement15(BulkActionBar, {
|
|
841
|
+
selectedCount: selectedRows.length,
|
|
842
|
+
actions: bulkActions,
|
|
843
|
+
onAction: handleBulkAction,
|
|
844
|
+
onClearSelection: clearSelection
|
|
845
|
+
}) : null,
|
|
846
|
+
// Data table or empty state
|
|
847
|
+
data.items.length === 0 && !data.isLoading ? createElement15(EmptyState, {
|
|
848
|
+
title: `No ${title.toLowerCase()} found`,
|
|
849
|
+
description: filters ? "Try adjusting your filters" : void 0,
|
|
850
|
+
createAction,
|
|
851
|
+
createLabel
|
|
852
|
+
}) : createElement15(DataTable, {
|
|
853
|
+
data,
|
|
854
|
+
columns,
|
|
855
|
+
selectable,
|
|
856
|
+
selectedRows: selectable ? selectedRows : void 0,
|
|
857
|
+
onSelectionChange: selectable ? (rows) => setSelectedRows(rows) : void 0
|
|
858
|
+
}),
|
|
859
|
+
// Pagination controls
|
|
860
|
+
data.totalPages && data.totalPages > 1 && data.goToPage ? createElement15(
|
|
861
|
+
"div",
|
|
862
|
+
{
|
|
863
|
+
style: {
|
|
864
|
+
display: "flex",
|
|
865
|
+
justifyContent: "center",
|
|
866
|
+
gap: "8px",
|
|
867
|
+
marginTop: "16px"
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
createElement15(
|
|
871
|
+
"button",
|
|
872
|
+
{
|
|
873
|
+
disabled: data.currentPage === 1,
|
|
874
|
+
onClick: () => data.goToPage?.(Math.max(1, (data.currentPage ?? 1) - 1))
|
|
875
|
+
},
|
|
876
|
+
"Previous"
|
|
877
|
+
),
|
|
878
|
+
createElement15(
|
|
879
|
+
"span",
|
|
880
|
+
null,
|
|
881
|
+
`Page ${data.currentPage ?? 1} of ${data.totalPages}`
|
|
882
|
+
),
|
|
883
|
+
createElement15(
|
|
884
|
+
"button",
|
|
885
|
+
{
|
|
886
|
+
disabled: data.currentPage === data.totalPages,
|
|
887
|
+
onClick: () => data.goToPage?.(
|
|
888
|
+
Math.min(data.totalPages ?? 1, (data.currentPage ?? 1) + 1)
|
|
889
|
+
)
|
|
890
|
+
},
|
|
891
|
+
"Next"
|
|
892
|
+
)
|
|
893
|
+
) : null,
|
|
894
|
+
// Load more (cursor-based)
|
|
895
|
+
data.hasMore && data.loadMore ? createElement15(
|
|
896
|
+
"div",
|
|
897
|
+
{ style: { textAlign: "center", marginTop: "16px" } },
|
|
898
|
+
createElement15(
|
|
899
|
+
"button",
|
|
900
|
+
{ onClick: data.loadMore },
|
|
901
|
+
"Load more"
|
|
902
|
+
)
|
|
903
|
+
) : null
|
|
904
|
+
)
|
|
905
|
+
}
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/components/detail-view.tsx
|
|
910
|
+
import { createElement as createElement16 } from "react";
|
|
911
|
+
function normalizeFields(fields) {
|
|
912
|
+
if (!fields) return [];
|
|
913
|
+
return fields.map((col) => {
|
|
914
|
+
if (typeof col === "string") {
|
|
915
|
+
return {
|
|
916
|
+
key: col,
|
|
917
|
+
label: col.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim()
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
return col;
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
function DetailView({
|
|
924
|
+
title,
|
|
925
|
+
table,
|
|
926
|
+
record,
|
|
927
|
+
fields: fieldsProp,
|
|
928
|
+
exclude,
|
|
929
|
+
breadcrumb
|
|
930
|
+
}) {
|
|
931
|
+
const fields = normalizeFields(fieldsProp);
|
|
932
|
+
const displayFields = fields.length > 0 ? fields : inferFieldsFromRecord(record, exclude);
|
|
933
|
+
return createElement16(
|
|
934
|
+
PageContainer,
|
|
935
|
+
{
|
|
936
|
+
title,
|
|
937
|
+
breadcrumb,
|
|
938
|
+
children: createElement16(
|
|
939
|
+
"div",
|
|
940
|
+
{
|
|
941
|
+
style: {
|
|
942
|
+
display: "grid",
|
|
943
|
+
gridTemplateColumns: "1fr 1fr",
|
|
944
|
+
gap: "16px"
|
|
945
|
+
}
|
|
946
|
+
},
|
|
947
|
+
...displayFields.map((field) => {
|
|
948
|
+
const value = record[field.key];
|
|
949
|
+
const FieldComponent = field.render ? null : resolveFieldComponent(field.key, table);
|
|
950
|
+
return createElement16(
|
|
951
|
+
"div",
|
|
952
|
+
{ key: field.key },
|
|
953
|
+
createElement16(
|
|
954
|
+
"div",
|
|
955
|
+
{
|
|
956
|
+
style: {
|
|
957
|
+
fontSize: "0.85em",
|
|
958
|
+
color: "#666",
|
|
959
|
+
marginBottom: "4px",
|
|
960
|
+
fontWeight: 600
|
|
961
|
+
}
|
|
962
|
+
},
|
|
963
|
+
field.label ?? field.key
|
|
964
|
+
),
|
|
965
|
+
createElement16(
|
|
966
|
+
"div",
|
|
967
|
+
null,
|
|
968
|
+
field.render ? field.render(value, record) : FieldComponent ? createElement16(FieldComponent, { value }) : String(value ?? "\u2014")
|
|
969
|
+
)
|
|
970
|
+
);
|
|
971
|
+
})
|
|
972
|
+
)
|
|
973
|
+
}
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
function inferFieldsFromRecord(record, exclude) {
|
|
977
|
+
if (!record || typeof record !== "object") return [];
|
|
978
|
+
return Object.keys(record).filter((key) => !exclude || !exclude.includes(key)).map((key) => ({
|
|
979
|
+
key,
|
|
980
|
+
label: key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim()
|
|
981
|
+
}));
|
|
982
|
+
}
|
|
983
|
+
function resolveFieldComponent(_key, table) {
|
|
984
|
+
if (!table || typeof table !== "object") return null;
|
|
985
|
+
const col = table[_key];
|
|
986
|
+
if (!col || typeof col !== "object" || !("dataType" in col) || !("name" in col)) {
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
const meta = col;
|
|
990
|
+
return fieldForColumn(meta);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
export {
|
|
994
|
+
useConfirm,
|
|
995
|
+
useActionToast,
|
|
996
|
+
PermissionGate,
|
|
997
|
+
ActionButton,
|
|
998
|
+
ConfirmProvider,
|
|
999
|
+
FormStatus,
|
|
1000
|
+
RoleBadge,
|
|
1001
|
+
ImpersonationBanner,
|
|
1002
|
+
DataTable,
|
|
1003
|
+
FilterBar,
|
|
1004
|
+
BulkActionBar,
|
|
1005
|
+
useColumnInference,
|
|
1006
|
+
DropZone,
|
|
1007
|
+
ImagePreview,
|
|
1008
|
+
FileList,
|
|
1009
|
+
PageContainer,
|
|
1010
|
+
EmptyState,
|
|
1011
|
+
ListView,
|
|
1012
|
+
DetailView
|
|
1013
|
+
};
|