@marktiderman/genesis-ui 2.9.0 → 2.10.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/SKILL.md +17 -0
- package/dist/{AppShell-LXQykEqe.d.ts → AppShell-BVr4V0UK.d.ts} +1 -110
- package/dist/PageHeader-V9lVkZtF.d.ts +114 -0
- package/dist/{chunk-S7NXD37O.js → chunk-25XEXYGC.js} +4 -4
- package/dist/chunk-25XEXYGC.js.map +1 -0
- package/dist/chunk-C7PQ54JI.js +763 -0
- package/dist/chunk-C7PQ54JI.js.map +1 -0
- package/dist/chunk-I4NBE2KL.js +16 -0
- package/dist/chunk-I4NBE2KL.js.map +1 -0
- package/dist/components/data/index.d.ts +42 -1012
- package/dist/components/data/index.js +411 -704
- package/dist/components/data/index.js.map +1 -1
- package/dist/components/layout/index.d.ts +2 -1
- package/dist/components/patterns/index.js +2 -1
- package/dist/components/patterns/sortable-list.js +2 -1
- package/dist/hooks/index.d.ts +7 -1
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/use-resource-page.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/{use-resource-page-BDnnCssE.d.ts → use-resource-page-DaJzuq3M.d.ts} +1 -1
- package/dist/use-resource-table-DCVf_lEG.d.ts +1113 -0
- package/package.json +2 -2
- package/dist/chunk-S7NXD37O.js.map +0 -1
|
@@ -7,13 +7,13 @@ import { Textarea } from '../../chunk-YETMISCE.js';
|
|
|
7
7
|
import { Separator } from '../../chunk-SXSCHWZU.js';
|
|
8
8
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '../../chunk-XCGZPFT7.js';
|
|
9
9
|
import { Skeleton } from '../../chunk-QFM4Z2SS.js';
|
|
10
|
-
import { toast } from '../../chunk-4RAZJ6LL.js';
|
|
11
10
|
import { Progress } from '../../chunk-W5Y3LG5K.js';
|
|
12
11
|
import { Checkbox } from '../../chunk-SEBU6JCK.js';
|
|
13
12
|
import { Card, CardContent, CardHeader, CardTitle } from '../../chunk-FN3FZVUR.js';
|
|
14
13
|
import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from '../../chunk-4C4ESW6O.js';
|
|
15
14
|
import { PageLoading } from '../../chunk-YRYQXU3G.js';
|
|
16
15
|
import { Spinner } from '../../chunk-OVU3Z2MT.js';
|
|
16
|
+
import { useAnnouncer } from '../../chunk-I4NBE2KL.js';
|
|
17
17
|
import { Input } from '../../chunk-QSXVL5UB.js';
|
|
18
18
|
import '../../chunk-B2GIBUQD.js';
|
|
19
19
|
import { Badge } from '../../chunk-NPNADZHM.js';
|
|
@@ -29,27 +29,30 @@ export { EmptyState } from '../../chunk-65SPT7EA.js';
|
|
|
29
29
|
import { Button } from '../../chunk-NWSCYLPT.js';
|
|
30
30
|
import { FormField, FormLabel, FormError, FormDescription } from '../../chunk-R7QFP6EL.js';
|
|
31
31
|
import '../../chunk-F47U5KNW.js';
|
|
32
|
-
import {
|
|
33
|
-
|
|
32
|
+
import { showUndoToast, showWriteErrorToast, useResourceTableModel } from '../../chunk-C7PQ54JI.js';
|
|
33
|
+
export { showUndoToast, showWriteErrorToast, useResourceTable } from '../../chunk-C7PQ54JI.js';
|
|
34
|
+
import { titleCase } from '../../chunk-RKPS7ERS.js';
|
|
35
|
+
import '../../chunk-DIV4BYXX.js';
|
|
34
36
|
export { useSavedViews } from '../../chunk-GCQFVMPI.js';
|
|
35
37
|
import '../../chunk-PKFDGGZV.js';
|
|
36
|
-
import
|
|
38
|
+
import '../../chunk-OZ7ZAYCS.js';
|
|
39
|
+
import '../../chunk-4RAZJ6LL.js';
|
|
37
40
|
export { CommandPalette, useCommandPaletteHotkey } from '../../chunk-CNTWQSUT.js';
|
|
38
41
|
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator } from '../../chunk-SEFK4NZV.js';
|
|
39
42
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '../../chunk-7XMZVISM.js';
|
|
40
43
|
import { cn } from '../../chunk-NZ5WBA4R.js';
|
|
41
|
-
import { DefaultLink
|
|
44
|
+
import { DefaultLink } from '../../chunk-WGMEVX3Y.js';
|
|
42
45
|
export { DefaultLink, defaultNavigate, isPathActive, resolveNavigation } from '../../chunk-WGMEVX3Y.js';
|
|
43
|
-
import
|
|
46
|
+
import '../../chunk-YJ6MPS6U.js';
|
|
44
47
|
import { useIsMobile } from '../../chunk-VFO3I7PV.js';
|
|
45
|
-
import { AlertTriangle, RefreshCw, ChevronDown, Check, Search, X, ArrowUpDown, Bookmark, Trash2, BookmarkPlus, Loader2, Columns3, ArrowUp, ArrowDown, ChevronLeft, ChevronRight, ExternalLink, ArrowRightLeft, Pencil, ChevronsUpDown, ImageIcon, Upload, AlertCircle, FileIcon, Plus } from 'lucide-react';
|
|
48
|
+
import { AlertTriangle, RefreshCw, ChevronDown, Check, Search, X, ArrowUpDown, Bookmark, Trash2, BookmarkPlus, Loader2, Columns3, ArrowUp, ArrowDown, ChevronLeft, ChevronRight, ExternalLink, GripVertical, ArrowRightLeft, Pencil, ChevronsUpDown, ImageIcon, Upload, AlertCircle, FileIcon, Plus } from 'lucide-react';
|
|
46
49
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
47
|
-
import { useState, useRef, useEffect, useCallback, useMemo, useId, Fragment as Fragment$1 } from 'react';
|
|
50
|
+
import { useState, useRef, useEffect, useCallback, useMemo, useId, Fragment as Fragment$1, useLayoutEffect } from 'react';
|
|
48
51
|
import { useReactTable, getPaginationRowModel, getSortedRowModel, getCoreRowModel } from '@tanstack/react-table';
|
|
49
52
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
50
53
|
import { formatDistanceToNow, format } from 'date-fns';
|
|
51
54
|
import { useFieldArray, Controller, useWatch } from 'react-hook-form';
|
|
52
|
-
import { useResource, useWizard, useResourceForm, useOne,
|
|
55
|
+
import { useResource, useWizard, useResourceForm, useOne, buildArrayItemDefaults } from '@marktiderman/genesis-core/hooks';
|
|
53
56
|
import { useDataProvider, isDataReadError } from '@marktiderman/genesis-core/provider';
|
|
54
57
|
import { useDropzone } from 'react-dropzone';
|
|
55
58
|
|
|
@@ -1140,8 +1143,8 @@ function DataTable({
|
|
|
1140
1143
|
setCurrentPage(0);
|
|
1141
1144
|
}, [items.length, currentPageSize]);
|
|
1142
1145
|
const sorting = useMemo(
|
|
1143
|
-
() => sorts.map((s) => ({ id: s.key, desc: s.dir === "desc" })),
|
|
1144
|
-
[sorts]
|
|
1146
|
+
() => sorts.filter((s) => columns.some((c) => c.key === s.key)).map((s) => ({ id: s.key, desc: s.dir === "desc" })),
|
|
1147
|
+
[sorts, columns]
|
|
1145
1148
|
);
|
|
1146
1149
|
const tanstackColumns = useMemo(
|
|
1147
1150
|
() => columns.map((col) => ({
|
|
@@ -1195,7 +1198,11 @@ function DataTable({
|
|
|
1195
1198
|
requestAnimationFrame(() => focusRowAt(bodyRef.current, target));
|
|
1196
1199
|
};
|
|
1197
1200
|
const openKey = !rowKeys ? null : openColumn !== void 0 ? openColumn : activeColumns.find((c) => !c.editable)?.key ?? null;
|
|
1198
|
-
const
|
|
1201
|
+
const renderedItems = virtualRows ? virtualRows.map((vr) => displayItems[vr.index]).filter(Boolean) : displayItems;
|
|
1202
|
+
const scrollTop = rowVirtualizer.scrollOffset ?? 0;
|
|
1203
|
+
const firstInView = virtualRows ? virtualRows.find((vr) => vr.start >= scrollTop) ?? virtualRows.find((vr) => vr.end > scrollTop) ?? virtualRows[0] : void 0;
|
|
1204
|
+
const fallbackItem = firstInView ? displayItems[firstInView.index] : renderedItems[0];
|
|
1205
|
+
const tabStopKey = activeRowKey && renderedItems.some((it) => getKey(it) === activeRowKey) ? activeRowKey : fallbackItem ? getKey(fallbackItem) : null;
|
|
1199
1206
|
const rowLabel = (item, id) => {
|
|
1200
1207
|
const col = activeColumns.find((c) => c.key === (openKey ?? activeColumns[0]?.key));
|
|
1201
1208
|
const value = col ? (col.accessor ?? ((it) => it[col.key]))(item) : void 0;
|
|
@@ -1789,6 +1796,7 @@ var statusColorMap = {
|
|
|
1789
1796
|
primary: { color: "text-primary", bgColor: "bg-primary/15" }
|
|
1790
1797
|
};
|
|
1791
1798
|
var DRAG_TYPE = "application/x-genesis-kanban-item";
|
|
1799
|
+
var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
1792
1800
|
function KanbanBoard({
|
|
1793
1801
|
columns,
|
|
1794
1802
|
renderCard,
|
|
@@ -1801,112 +1809,301 @@ function KanbanBoard({
|
|
|
1801
1809
|
}) {
|
|
1802
1810
|
const [dragging, setDragging] = useState(null);
|
|
1803
1811
|
const [over, setOver] = useState(null);
|
|
1812
|
+
const [grabbed, setGrabbed] = useState(null);
|
|
1813
|
+
const { message: announcement, announce } = useAnnouncer();
|
|
1814
|
+
const handles = useRef(/* @__PURE__ */ new Map());
|
|
1815
|
+
const refocus = useRef(null);
|
|
1816
|
+
const instructionsId = useId();
|
|
1804
1817
|
const keyOf = (item, index) => getKey ? getKey(item) : String(index);
|
|
1805
|
-
const
|
|
1806
|
-
|
|
1807
|
-
|
|
1818
|
+
const movable = Boolean(onMoveItem && getKey);
|
|
1819
|
+
const find = (key) => {
|
|
1820
|
+
if (!getKey) return null;
|
|
1821
|
+
for (const column of columns) {
|
|
1822
|
+
const item = column.items.find((i) => getKey(i) === key);
|
|
1823
|
+
if (item !== void 0) return { item, column };
|
|
1824
|
+
}
|
|
1825
|
+
return null;
|
|
1826
|
+
};
|
|
1827
|
+
const labelOf = (key) => {
|
|
1828
|
+
const found = find(key);
|
|
1829
|
+
return found && getItemLabel ? getItemLabel(found.item) : "Card";
|
|
1830
|
+
};
|
|
1831
|
+
const columnLabel = (key) => columns.find((c) => c.key === key)?.label ?? key;
|
|
1832
|
+
const move = (item, to, name) => {
|
|
1833
|
+
let result;
|
|
1834
|
+
try {
|
|
1835
|
+
result = onMoveItem?.(item, to);
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
result = Promise.reject(error);
|
|
1838
|
+
}
|
|
1839
|
+
void Promise.resolve(result).then(
|
|
1840
|
+
() => announce(`${name} moved to ${columnLabel(to)}.`),
|
|
1841
|
+
() => announce(`Couldn\u2019t move ${name}.`)
|
|
1842
|
+
);
|
|
1843
|
+
};
|
|
1844
|
+
const moveKey = (key, to) => {
|
|
1845
|
+
const found = find(key);
|
|
1846
|
+
if (!found || found.column.key === to) return;
|
|
1847
|
+
move(found.item, to, labelOf(key));
|
|
1808
1848
|
};
|
|
1809
1849
|
const onDrop = (event, to) => {
|
|
1810
1850
|
event.preventDefault();
|
|
1811
1851
|
setOver(null);
|
|
1812
1852
|
const key = event.dataTransfer.getData(DRAG_TYPE) || dragging?.key;
|
|
1813
1853
|
setDragging(null);
|
|
1814
|
-
if (
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1854
|
+
if (key) moveKey(key, to);
|
|
1855
|
+
};
|
|
1856
|
+
const pickUp = (key, from) => {
|
|
1857
|
+
setGrabbed({ key, from, target: from });
|
|
1858
|
+
announce(
|
|
1859
|
+
`${labelOf(key)} picked up in ${columnLabel(from)}. Use the left and right arrow keys to choose a column, Space to drop, Escape to cancel.`
|
|
1860
|
+
);
|
|
1861
|
+
};
|
|
1862
|
+
const aim = (index) => {
|
|
1863
|
+
if (!grabbed) return;
|
|
1864
|
+
const column = columns[Math.max(0, Math.min(columns.length - 1, index))];
|
|
1865
|
+
if (!column || column.key === grabbed.target) return;
|
|
1866
|
+
setGrabbed({ ...grabbed, target: column.key });
|
|
1867
|
+
announce(`Over ${column.label}, column ${columns.indexOf(column) + 1} of ${columns.length}.`);
|
|
1868
|
+
};
|
|
1869
|
+
const drop = () => {
|
|
1870
|
+
if (!grabbed) return;
|
|
1871
|
+
setGrabbed(null);
|
|
1872
|
+
if (grabbed.target === grabbed.from) {
|
|
1873
|
+
announce(`${labelOf(grabbed.key)} dropped back in ${columnLabel(grabbed.from)}.`);
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
refocus.current = grabbed.key;
|
|
1877
|
+
moveKey(grabbed.key, grabbed.target);
|
|
1878
|
+
};
|
|
1879
|
+
const cancel = () => {
|
|
1880
|
+
if (!grabbed) return;
|
|
1881
|
+
setGrabbed(null);
|
|
1882
|
+
announce(`Move cancelled. ${labelOf(grabbed.key)} stays in ${columnLabel(grabbed.from)}.`);
|
|
1883
|
+
};
|
|
1884
|
+
useEffect(() => {
|
|
1885
|
+
if (!grabbed) return;
|
|
1886
|
+
const onKey = (event) => {
|
|
1887
|
+
if (event.key !== "Escape") return;
|
|
1888
|
+
event.preventDefault();
|
|
1889
|
+
event.stopPropagation();
|
|
1890
|
+
cancel();
|
|
1891
|
+
};
|
|
1892
|
+
window.addEventListener("keydown", onKey, true);
|
|
1893
|
+
return () => window.removeEventListener("keydown", onKey, true);
|
|
1894
|
+
});
|
|
1895
|
+
useEffect(() => {
|
|
1896
|
+
if (grabbed && (!movable || !find(grabbed.key))) setGrabbed(null);
|
|
1897
|
+
if (dragging && (!movable || !find(dragging.key))) {
|
|
1898
|
+
setDragging(null);
|
|
1899
|
+
setOver(null);
|
|
1900
|
+
}
|
|
1901
|
+
});
|
|
1902
|
+
useIsomorphicLayoutEffect(() => {
|
|
1903
|
+
const key = refocus.current;
|
|
1904
|
+
if (!key) return;
|
|
1905
|
+
const handle = handles.current.get(key);
|
|
1906
|
+
if (!handle) return;
|
|
1907
|
+
if (handle.isConnected && document.activeElement === handle) {
|
|
1908
|
+
refocus.current = null;
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
const active = document.activeElement;
|
|
1912
|
+
if (active === null || active === document.body) {
|
|
1913
|
+
handle.focus();
|
|
1914
|
+
refocus.current = null;
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
const onHandleKeyDown = (event, key, from) => {
|
|
1918
|
+
const delta = event.key === "ArrowLeft" ? -1 : event.key === "ArrowRight" ? 1 : 0;
|
|
1919
|
+
if (grabbed?.key === key) {
|
|
1920
|
+
const index = columns.findIndex((c) => c.key === grabbed.target);
|
|
1921
|
+
if (delta) {
|
|
1922
|
+
event.preventDefault();
|
|
1923
|
+
aim(index + delta);
|
|
1924
|
+
} else if (event.key === "Home" || event.key === "End") {
|
|
1925
|
+
event.preventDefault();
|
|
1926
|
+
aim(event.key === "Home" ? 0 : columns.length - 1);
|
|
1927
|
+
}
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
if (delta && event.altKey && !grabbed) {
|
|
1931
|
+
event.preventDefault();
|
|
1932
|
+
const to = columns[columns.findIndex((c) => c.key === from) + delta];
|
|
1933
|
+
if (to) {
|
|
1934
|
+
refocus.current = key;
|
|
1935
|
+
moveKey(key, to.key);
|
|
1820
1936
|
}
|
|
1821
1937
|
}
|
|
1822
1938
|
};
|
|
1823
|
-
const
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1939
|
+
const onHandleClick = (event, key, from) => {
|
|
1940
|
+
if (event.detail !== 0) return;
|
|
1941
|
+
if (grabbed?.key === key) drop();
|
|
1942
|
+
else if (!grabbed) pickUp(key, from);
|
|
1943
|
+
};
|
|
1944
|
+
const columnAt = (x, y) => (typeof document.elementFromPoint === "function" ? document.elementFromPoint(x, y) : null)?.closest("[data-kanban-column]")?.dataset.kanbanColumn ?? null;
|
|
1945
|
+
const onHandlePointerDown = (event, key, from) => {
|
|
1946
|
+
if (event.pointerType === "mouse" || grabbed || dragging) return;
|
|
1947
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
1948
|
+
setDragging({ key, from, pointer: event.pointerId });
|
|
1949
|
+
announce(`${labelOf(key)} picked up in ${columnLabel(from)}.`);
|
|
1950
|
+
};
|
|
1951
|
+
const onHandlePointerMove = (event) => {
|
|
1952
|
+
if (dragging?.pointer !== event.pointerId) return;
|
|
1953
|
+
const to = columnAt(event.clientX, event.clientY);
|
|
1954
|
+
if (to !== over) setOver(to);
|
|
1955
|
+
};
|
|
1956
|
+
const onHandlePointerUp = (event) => {
|
|
1957
|
+
if (dragging?.pointer !== event.pointerId) return;
|
|
1958
|
+
const to = columnAt(event.clientX, event.clientY) ?? over;
|
|
1959
|
+
const { key, from } = dragging;
|
|
1960
|
+
setDragging(null);
|
|
1961
|
+
setOver(null);
|
|
1962
|
+
if (to && to !== from) moveKey(key, to);
|
|
1963
|
+
else announce(`${labelOf(key)} dropped back in ${columnLabel(from)}.`);
|
|
1964
|
+
};
|
|
1965
|
+
const onHandlePointerCancel = (event) => {
|
|
1966
|
+
if (dragging?.pointer !== event.pointerId) return;
|
|
1967
|
+
setDragging(null);
|
|
1968
|
+
setOver(null);
|
|
1969
|
+
};
|
|
1970
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1971
|
+
/* @__PURE__ */ jsx(
|
|
1972
|
+
"div",
|
|
1973
|
+
{
|
|
1974
|
+
className: cn(
|
|
1975
|
+
"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
|
|
1976
|
+
className
|
|
1977
|
+
),
|
|
1978
|
+
children: columns.map((column) => {
|
|
1979
|
+
const resolved = column.status ? statusColorMap[column.status] : { color: column.color ?? "", bgColor: column.bgColor ?? "" };
|
|
1980
|
+
return /* @__PURE__ */ jsxs(
|
|
1981
|
+
"section",
|
|
1982
|
+
{
|
|
1983
|
+
"aria-label": column.label,
|
|
1984
|
+
"data-kanban-column": column.key,
|
|
1985
|
+
className: cn(
|
|
1986
|
+
"flex flex-col gap-3 rounded-lg",
|
|
1987
|
+
movable && (dragging && over === column.key && dragging.from !== column.key || grabbed && grabbed.target === column.key && grabbed.from !== column.key) && "bg-accent/40 ring-2 ring-primary/40"
|
|
1988
|
+
),
|
|
1989
|
+
onDragOver: movable ? (e) => {
|
|
1990
|
+
if (!dragging || dragging.pointer !== void 0) return;
|
|
1991
|
+
e.preventDefault();
|
|
1992
|
+
e.dataTransfer.dropEffect = "move";
|
|
1993
|
+
if (over !== column.key) setOver(column.key);
|
|
1994
|
+
} : void 0,
|
|
1995
|
+
onDragLeave: movable ? (e) => {
|
|
1996
|
+
if (!e.currentTarget.contains(e.relatedTarget)) setOver(null);
|
|
1997
|
+
} : void 0,
|
|
1998
|
+
onDrop: movable ? (e) => onDrop(e, column.key) : void 0,
|
|
1999
|
+
children: [
|
|
2000
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2001
|
+
/* @__PURE__ */ jsx("span", { className: cn("text-sm font-semibold", resolved.color), children: column.label }),
|
|
2002
|
+
/* @__PURE__ */ jsx(
|
|
2003
|
+
Badge,
|
|
2004
|
+
{
|
|
2005
|
+
variant: "secondary",
|
|
2006
|
+
className: cn("text-[10px] px-1.5 py-0", resolved.bgColor),
|
|
2007
|
+
children: column.total ?? column.items.length
|
|
2008
|
+
}
|
|
2009
|
+
)
|
|
2010
|
+
] }),
|
|
2011
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 min-h-[80px]", children: column.items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-lg border border-dashed border-border p-6 text-xs text-muted-foreground", children: emptyMessage }) : column.items.map((item, i) => {
|
|
2012
|
+
const key = keyOf(item, i);
|
|
2013
|
+
const name = getItemLabel?.(item) ?? "card";
|
|
2014
|
+
const isGrabbed = grabbed?.key === key;
|
|
2015
|
+
return /* @__PURE__ */ jsxs(
|
|
2016
|
+
"div",
|
|
2017
|
+
{
|
|
2018
|
+
className: cn(
|
|
2019
|
+
"animate-fade-in",
|
|
2020
|
+
movable && "group relative rounded-lg",
|
|
2021
|
+
dragging?.key === key && "opacity-50",
|
|
2022
|
+
isGrabbed && "ring-2 ring-primary/50"
|
|
2023
|
+
),
|
|
2024
|
+
style: { animationDelay: `${i * 30}ms`, animationFillMode: "both" },
|
|
2025
|
+
draggable: movable || void 0,
|
|
2026
|
+
onDragStart: movable ? (e) => {
|
|
2027
|
+
e.dataTransfer.setData(DRAG_TYPE, key);
|
|
2028
|
+
e.dataTransfer.effectAllowed = "move";
|
|
2029
|
+
setDragging({ key, from: column.key });
|
|
2030
|
+
} : void 0,
|
|
2031
|
+
onDragEnd: movable ? () => {
|
|
2032
|
+
setDragging(null);
|
|
2033
|
+
setOver(null);
|
|
2034
|
+
} : void 0,
|
|
2035
|
+
children: [
|
|
2036
|
+
renderCard(item),
|
|
2037
|
+
movable && columns.length > 1 && /* @__PURE__ */ jsx(
|
|
2038
|
+
"button",
|
|
1886
2039
|
{
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2040
|
+
type: "button",
|
|
2041
|
+
ref: (node) => {
|
|
2042
|
+
if (node) handles.current.set(key, node);
|
|
2043
|
+
else handles.current.delete(key);
|
|
2044
|
+
},
|
|
2045
|
+
"aria-label": `Pick up ${name}`,
|
|
2046
|
+
"aria-describedby": instructionsId,
|
|
2047
|
+
"data-grabbed": isGrabbed || void 0,
|
|
2048
|
+
className: cn(
|
|
2049
|
+
"absolute right-8 top-1.5 cursor-grab touch-none rounded p-1 text-muted-foreground",
|
|
2050
|
+
"hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
2051
|
+
isGrabbed && "bg-accent text-foreground"
|
|
2052
|
+
),
|
|
2053
|
+
onClick: (e) => onHandleClick(e, key, column.key),
|
|
2054
|
+
onKeyDown: (e) => onHandleKeyDown(e, key, column.key),
|
|
2055
|
+
onBlur: () => {
|
|
2056
|
+
if (isGrabbed) cancel();
|
|
2057
|
+
},
|
|
2058
|
+
onPointerDown: (e) => onHandlePointerDown(e, key, column.key),
|
|
2059
|
+
onPointerMove: onHandlePointerMove,
|
|
2060
|
+
onPointerUp: onHandlePointerUp,
|
|
2061
|
+
onPointerCancel: onHandlePointerCancel,
|
|
2062
|
+
children: /* @__PURE__ */ jsx(GripVertical, { className: "h-3.5 w-3.5", "aria-hidden": true })
|
|
1890
2063
|
}
|
|
1891
2064
|
),
|
|
1892
|
-
/* @__PURE__ */ jsxs(
|
|
1893
|
-
/* @__PURE__ */ jsx(
|
|
1894
|
-
|
|
2065
|
+
movable && columns.length > 1 && /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
2066
|
+
/* @__PURE__ */ jsx(
|
|
2067
|
+
DropdownMenuTrigger,
|
|
2068
|
+
{
|
|
2069
|
+
"aria-label": `Move ${name}`,
|
|
2070
|
+
className: "absolute right-1.5 top-1.5 rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
2071
|
+
children: /* @__PURE__ */ jsx(ArrowRightLeft, { className: "h-3.5 w-3.5", "aria-hidden": true })
|
|
2072
|
+
}
|
|
2073
|
+
),
|
|
2074
|
+
/* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
|
|
2075
|
+
/* @__PURE__ */ jsx(DropdownMenuLabel, { children: "Move to" }),
|
|
2076
|
+
columns.filter((c) => c.key !== column.key).map((c) => /* @__PURE__ */ jsx(
|
|
2077
|
+
DropdownMenuItem,
|
|
2078
|
+
{
|
|
2079
|
+
onSelect: () => {
|
|
2080
|
+
refocus.current = key;
|
|
2081
|
+
move(item, c.key, getItemLabel?.(item) ?? "Card");
|
|
2082
|
+
},
|
|
2083
|
+
children: c.label
|
|
2084
|
+
},
|
|
2085
|
+
c.key
|
|
2086
|
+
))
|
|
2087
|
+
] })
|
|
1895
2088
|
] })
|
|
1896
|
-
]
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
)
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
)
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
|
|
2089
|
+
]
|
|
2090
|
+
},
|
|
2091
|
+
key
|
|
2092
|
+
);
|
|
2093
|
+
}) }),
|
|
2094
|
+
renderColumnFooter?.(column)
|
|
2095
|
+
]
|
|
2096
|
+
},
|
|
2097
|
+
column.key
|
|
2098
|
+
);
|
|
2099
|
+
})
|
|
2100
|
+
}
|
|
2101
|
+
),
|
|
2102
|
+
movable && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2103
|
+
/* @__PURE__ */ jsx("p", { id: instructionsId, hidden: true, children: "Space or Enter picks the card up. Left and right arrows, Home and End choose a column. Space or Enter drops it, Escape puts it back. Alt with an arrow moves it one column." }),
|
|
2104
|
+
/* @__PURE__ */ jsx("div", { "aria-live": "polite", "aria-atomic": "true", className: "sr-only", children: announcement })
|
|
2105
|
+
] })
|
|
2106
|
+
] });
|
|
1910
2107
|
}
|
|
1911
2108
|
function DataList({
|
|
1912
2109
|
items,
|
|
@@ -2122,39 +2319,6 @@ function DetailPanel({
|
|
|
2122
2319
|
}
|
|
2123
2320
|
);
|
|
2124
2321
|
}
|
|
2125
|
-
|
|
2126
|
-
// src/components/data/undo-toast.ts
|
|
2127
|
-
var DONE = {
|
|
2128
|
-
create: "Created.",
|
|
2129
|
-
update: "Saved.",
|
|
2130
|
-
delete: "Deleted."
|
|
2131
|
-
};
|
|
2132
|
-
function showUndoToast(write) {
|
|
2133
|
-
toast(DONE[write.kind], {
|
|
2134
|
-
duration: write.delayMs,
|
|
2135
|
-
action: {
|
|
2136
|
-
label: "Undo",
|
|
2137
|
-
onClick: () => {
|
|
2138
|
-
void write.undo().then((undone) => {
|
|
2139
|
-
if (!undone && write.strategy === "delay") {
|
|
2140
|
-
toast("Already saved. It can no longer be undone.");
|
|
2141
|
-
}
|
|
2142
|
-
});
|
|
2143
|
-
}
|
|
2144
|
-
}
|
|
2145
|
-
});
|
|
2146
|
-
}
|
|
2147
|
-
function showWriteErrorToast(error) {
|
|
2148
|
-
toast.error(error.message || "The change could not be saved.");
|
|
2149
|
-
}
|
|
2150
|
-
function writeModeOptions(resourceOptions, mutationMode) {
|
|
2151
|
-
const mode = resourceOptions?.mutationMode ?? mutationMode ?? "pessimistic";
|
|
2152
|
-
return {
|
|
2153
|
-
mutationMode: mode,
|
|
2154
|
-
onUndoable: resourceOptions?.onUndoable ?? showUndoToast,
|
|
2155
|
-
onError: resourceOptions?.onError ?? (mode === "pessimistic" ? void 0 : showWriteErrorToast)
|
|
2156
|
-
};
|
|
2157
|
-
}
|
|
2158
2322
|
function RelationField({
|
|
2159
2323
|
field,
|
|
2160
2324
|
value,
|
|
@@ -3087,6 +3251,8 @@ function ResourceForm(props) {
|
|
|
3087
3251
|
prefillFromUrl,
|
|
3088
3252
|
autoPlaceholder,
|
|
3089
3253
|
warnWhenUnsaved,
|
|
3254
|
+
// A closed dialog is not touched by a save that fails later.
|
|
3255
|
+
visible: open,
|
|
3090
3256
|
mutationMode,
|
|
3091
3257
|
undoDelayMs,
|
|
3092
3258
|
onUndoable: onUndoable ?? showUndoToast
|
|
@@ -3218,589 +3384,130 @@ function ConfirmDeleteDialog({
|
|
|
3218
3384
|
}
|
|
3219
3385
|
function ResourcePage(props) {
|
|
3220
3386
|
const {
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
total: totalProp,
|
|
3224
|
-
pagination = "client",
|
|
3225
|
-
clientRowLimit = 1e3,
|
|
3226
|
-
title,
|
|
3227
|
-
subtitle,
|
|
3228
|
-
icon,
|
|
3229
|
-
columns: columnsProp,
|
|
3230
|
-
getKey: getKeyProp,
|
|
3231
|
-
detail = "panel",
|
|
3232
|
-
detailWidth = "md",
|
|
3233
|
-
titleField: titleFieldProp,
|
|
3234
|
-
subtitleField: subtitleFieldProp,
|
|
3235
|
-
getTitle,
|
|
3236
|
-
getSubtitle,
|
|
3237
|
-
renderDetail: renderDetailProp,
|
|
3238
|
-
detailFields,
|
|
3239
|
-
statusFilter,
|
|
3240
|
-
searchFields,
|
|
3241
|
-
searchPlaceholder,
|
|
3242
|
-
initialFilters,
|
|
3243
|
-
onFiltersChange,
|
|
3244
|
-
filters: filtersProp,
|
|
3245
|
-
page: pageProp,
|
|
3246
|
-
perPage: perPageProp,
|
|
3247
|
-
onPageChange,
|
|
3248
|
-
onPerPageChange,
|
|
3249
|
-
onSortChange,
|
|
3250
|
-
onSearchChange,
|
|
3251
|
-
loading: loadingProp,
|
|
3252
|
-
error: errorProp,
|
|
3253
|
-
errorMessage: errorMessage2,
|
|
3254
|
-
onRetry: onRetryProp,
|
|
3255
|
-
savedViews,
|
|
3256
|
-
onLoadView,
|
|
3257
|
-
onSaveView,
|
|
3258
|
-
onDeleteView,
|
|
3387
|
+
bulkActions,
|
|
3388
|
+
can,
|
|
3259
3389
|
canSaveViews,
|
|
3390
|
+
capabilities,
|
|
3391
|
+
className,
|
|
3260
3392
|
columnVisibility,
|
|
3261
3393
|
columnVisibilityKey,
|
|
3262
|
-
resizableColumns,
|
|
3263
|
-
resizeKey,
|
|
3264
|
-
stickyHeader,
|
|
3265
|
-
stickyFirstColumn,
|
|
3266
|
-
pageSizeOptions,
|
|
3267
|
-
onCreate,
|
|
3268
|
-
onEdit,
|
|
3269
|
-
onDelete,
|
|
3270
|
-
can,
|
|
3271
|
-
canEditItem,
|
|
3272
3394
|
createLabel = "New",
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3395
|
+
detail = "panel",
|
|
3396
|
+
detailFields,
|
|
3397
|
+
detailWidth = "md",
|
|
3398
|
+
emptyDescription,
|
|
3399
|
+
emptyIcon,
|
|
3400
|
+
emptyMessage,
|
|
3401
|
+
emptyState,
|
|
3402
|
+
errorMessage: errorMessage2,
|
|
3403
|
+
filters: filtersProp,
|
|
3281
3404
|
formFields,
|
|
3282
|
-
|
|
3405
|
+
formLayout: formLayoutProp,
|
|
3283
3406
|
formMode,
|
|
3284
3407
|
formSteps,
|
|
3285
|
-
|
|
3408
|
+
gridCols: gridColsProp,
|
|
3409
|
+
header: headerMode = "page",
|
|
3410
|
+
icon,
|
|
3411
|
+
keyboardNavigation = true,
|
|
3286
3412
|
mutationMode: mutationModeProp,
|
|
3287
|
-
|
|
3288
|
-
|
|
3413
|
+
onCreate,
|
|
3414
|
+
onDelete,
|
|
3415
|
+
onDeleteView,
|
|
3416
|
+
onEdit,
|
|
3417
|
+
onLoadView,
|
|
3418
|
+
onPageChange,
|
|
3419
|
+
onRetry: onRetryProp,
|
|
3420
|
+
onSaveView,
|
|
3421
|
+
onSearchChange,
|
|
3422
|
+
onSortChange,
|
|
3289
3423
|
openOnCardClick = false,
|
|
3424
|
+
pageSizeOptions,
|
|
3425
|
+
pagination = "client",
|
|
3426
|
+
renderCard: renderCardProp,
|
|
3427
|
+
renderDetail: renderDetailProp,
|
|
3428
|
+
renderForm: renderFormProp,
|
|
3290
3429
|
renderTable: renderTableProp,
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
emptyMessage,
|
|
3298
|
-
emptyDescription,
|
|
3299
|
-
header: headerMode = "page",
|
|
3300
|
-
resourceOptions,
|
|
3301
|
-
urlState: urlStateProp,
|
|
3302
|
-
defaultView = "table",
|
|
3303
|
-
onNavigate = defaultNavigate,
|
|
3304
|
-
viewSettingsKey: viewSettingsKeyProp,
|
|
3305
|
-
slots,
|
|
3430
|
+
resizableColumns,
|
|
3431
|
+
resizeKey,
|
|
3432
|
+
resource: resourceName,
|
|
3433
|
+
savedViews,
|
|
3434
|
+
searchPlaceholder,
|
|
3435
|
+
secondaryActions,
|
|
3306
3436
|
slotProps,
|
|
3307
|
-
|
|
3437
|
+
slots,
|
|
3438
|
+
stickyFirstColumn,
|
|
3439
|
+
stickyHeader,
|
|
3440
|
+
subtitle,
|
|
3441
|
+
title,
|
|
3442
|
+
toolbar
|
|
3308
3443
|
} = props;
|
|
3309
|
-
const isClientMode = !!resourceName;
|
|
3310
|
-
const resourceServerPaging = isClientMode && pagination === "server";
|
|
3311
|
-
const url = useResourceUrlState(
|
|
3312
|
-
urlStateProp ? "setSearchParams" in urlStateProp ? { searchParams: urlStateProp } : urlStateProp : void 0
|
|
3313
|
-
);
|
|
3314
|
-
const [urlSeed] = useState(() => {
|
|
3315
|
-
if (!url) return null;
|
|
3316
|
-
const sortable = !url.sort || !columnsProp ? true : columnsProp.some(
|
|
3317
|
-
(col) => typeof col === "string" ? col === url.sort.field : col.key === url.sort.field && col.sortable !== false
|
|
3318
|
-
);
|
|
3319
|
-
return { q: url.q, sort: sortable ? url.sort : null, page: url.page };
|
|
3320
|
-
});
|
|
3321
|
-
const urlPeek = detail === "panel" || detail === "modal" ? url : void 0;
|
|
3322
|
-
const urlSortKey = urlSeed?.sort ? `${urlSeed.sort.field}:${urlSeed.sort.order}` : "";
|
|
3323
|
-
const isServerControlled = resourceServerPaging || pageProp !== void 0 || perPageProp !== void 0 || onPageChange !== void 0 || onPerPageChange !== void 0 || onSortChange !== void 0 || onSearchChange !== void 0;
|
|
3324
|
-
const viewSettingsKey = viewSettingsKeyProp ?? resourceName ?? title.toLowerCase().replace(/\s+/g, "-");
|
|
3325
|
-
const { settings, updateSetting } = useViewSettings(viewSettingsKey);
|
|
3326
|
-
const serverSearchFields = resourceOptions?.searchFields ?? searchFields;
|
|
3327
|
-
const resourceHook = useResource(
|
|
3328
|
-
resourceName ?? "__noop__",
|
|
3329
|
-
isClientMode ? {
|
|
3330
|
-
...resourceOptions,
|
|
3331
|
-
// Client paging fetches everything it can in one request; server
|
|
3332
|
-
// paging starts at the user's stored page size. An explicit
|
|
3333
|
-
// `defaultPerPage` wins over both.
|
|
3334
|
-
defaultPerPage: resourceOptions?.defaultPerPage ?? (resourceServerPaging ? perPageProp ?? settings.pageSize : clientRowLimit),
|
|
3335
|
-
searchFields: resourceServerPaging ? serverSearchFields : resourceOptions?.searchFields,
|
|
3336
|
-
// Paging, searching and sorting change the query key; keep the
|
|
3337
|
-
// current page on screen while the next one loads.
|
|
3338
|
-
keepPreviousData: resourceOptions?.keepPreviousData ?? resourceServerPaging,
|
|
3339
|
-
// Start where the URL says, so restoring a link doesn't push a
|
|
3340
|
-
// search or sort that resets the page to 1.
|
|
3341
|
-
...resourceServerPaging && urlSeed ? {
|
|
3342
|
-
defaultSearch: urlSeed.q,
|
|
3343
|
-
...urlSeed.page ? { defaultPage: urlSeed.page } : {},
|
|
3344
|
-
...urlSeed.sort ? { defaultSort: urlSeed.sort } : {}
|
|
3345
|
-
} : {},
|
|
3346
|
-
...writeModeOptions(resourceOptions, mutationModeProp)
|
|
3347
|
-
} : { enabled: false }
|
|
3348
|
-
);
|
|
3349
|
-
const effectivePageSize = resourceServerPaging ? resourceHook.params.pagination?.perPage ?? settings.pageSize : perPageProp ?? settings.pageSize;
|
|
3350
|
-
const displayData = isClientMode ? resourceHook.list.data?.data ?? [] : dataProp ?? [];
|
|
3351
|
-
const displayTotal = isClientMode ? resourceHook.list.data?.total ?? 0 : totalProp ?? 0;
|
|
3352
|
-
const isLoading = isClientMode ? resourceHook.list.isLoading : loadingProp ?? false;
|
|
3353
|
-
const hasError = isClientMode ? resourceHook.list.isError : errorProp ?? false;
|
|
3354
|
-
const page = useResourcePage({
|
|
3355
|
-
resource: resourceName,
|
|
3356
|
-
data: displayData,
|
|
3357
|
-
total: displayTotal,
|
|
3358
|
-
columns: columnsProp,
|
|
3359
|
-
statusFilter,
|
|
3360
|
-
searchFields,
|
|
3361
|
-
initialFilters: initialFilters ?? (urlSeed && (urlSeed.q || urlSeed.sort) ? [
|
|
3362
|
-
...urlSeed.q ? [{ field: "__search__", operator: "contains", value: urlSeed.q }] : [],
|
|
3363
|
-
...urlSeed.sort ? [{ field: "__sort__", operator: "eq", value: urlSortKey }] : []
|
|
3364
|
-
] : void 0),
|
|
3365
|
-
onFiltersChange,
|
|
3366
|
-
defaultView,
|
|
3367
|
-
// Each page remembers its own table/cards choice, under the same key as
|
|
3368
|
-
// its density and page size — not one key shared by every data page.
|
|
3369
|
-
viewKey: viewSettingsKey,
|
|
3370
|
-
detail,
|
|
3371
|
-
titleField: titleFieldProp,
|
|
3372
|
-
subtitleField: subtitleFieldProp,
|
|
3373
|
-
controlled: isServerControlled
|
|
3374
|
-
});
|
|
3375
3444
|
const {
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
const statusField = statusFilter ? typeof statusFilter === "string" ? statusFilter : statusFilter.field : void 0;
|
|
3408
|
-
const defaultFilters = resourceOptions?.defaultFilters;
|
|
3409
|
-
const pushedStatusRef = useRef("");
|
|
3410
|
-
useEffect(() => {
|
|
3411
|
-
if (!resourceServerPaging || !statusField) return;
|
|
3412
|
-
const key = JSON.stringify([page.statusValue, defaultFilters ?? null]);
|
|
3413
|
-
if (key === pushedStatusRef.current) return;
|
|
3414
|
-
pushedStatusRef.current = key;
|
|
3415
|
-
setResourceFilters([
|
|
3416
|
-
...defaultFilters ?? [],
|
|
3417
|
-
...page.statusValue.length > 0 ? [{ field: statusField, operator: "in", value: page.statusValue }] : []
|
|
3418
|
-
]);
|
|
3419
|
-
}, [
|
|
3420
|
-
resourceServerPaging,
|
|
3421
|
-
statusField,
|
|
3422
|
-
page.statusValue,
|
|
3423
|
-
defaultFilters,
|
|
3424
|
-
setResourceFilters
|
|
3425
|
-
]);
|
|
3426
|
-
const clientTruncated = isClientMode && !resourceServerPaging && !isLoading && !hasError && displayTotal > displayData.length;
|
|
3427
|
-
const warnedTruncatedRef = useRef(false);
|
|
3428
|
-
useEffect(() => {
|
|
3429
|
-
if (!clientTruncated || warnedTruncatedRef.current) return;
|
|
3430
|
-
warnedTruncatedRef.current = true;
|
|
3431
|
-
console.warn(
|
|
3432
|
-
`ResourcePage: "${resourceName}" has ${displayTotal} rows but only ${displayData.length} were fetched. Pass \`pagination="server"\` to page through all of them, or raise \`clientRowLimit\`.`
|
|
3433
|
-
);
|
|
3434
|
-
}, [clientTruncated, resourceName, displayTotal, displayData.length]);
|
|
3435
|
-
const actions = useMemo(() => {
|
|
3436
|
-
if (isClientMode) {
|
|
3437
|
-
return {
|
|
3438
|
-
update: resourceHook.update,
|
|
3439
|
-
remove: resourceHook.remove,
|
|
3440
|
-
refetch: () => resourceHook.list.refetch()
|
|
3441
|
-
};
|
|
3442
|
-
}
|
|
3443
|
-
return {
|
|
3444
|
-
update: async () => {
|
|
3445
|
-
throw new Error(
|
|
3446
|
-
"ResourceActions.update not available in server-data mode"
|
|
3447
|
-
);
|
|
3448
|
-
},
|
|
3449
|
-
remove: async () => {
|
|
3450
|
-
throw new Error(
|
|
3451
|
-
"ResourceActions.remove not available in server-data mode"
|
|
3452
|
-
);
|
|
3453
|
-
},
|
|
3454
|
-
refetch: () => {
|
|
3455
|
-
}
|
|
3456
|
-
};
|
|
3457
|
-
}, [
|
|
3458
|
-
isClientMode,
|
|
3459
|
-
resourceHook.update,
|
|
3460
|
-
resourceHook.remove,
|
|
3461
|
-
resourceHook.list
|
|
3462
|
-
]);
|
|
3463
|
-
const keyOf = useCallback(
|
|
3464
|
-
(item) => getKeyProp ? getKeyProp(item) : String(item.id ?? ""),
|
|
3465
|
-
[getKeyProp]
|
|
3466
|
-
);
|
|
3467
|
-
const snapshotKey = page.selectedItem ? keyOf(page.selectedItem) : null;
|
|
3468
|
-
const selectedKey = urlPeek ? urlPeek.detailId : snapshotKey;
|
|
3469
|
-
const snapshot = page.selectedItem && snapshotKey === selectedKey ? page.selectedItem : null;
|
|
3470
|
-
const rowOrSnapshot = !selectedKey ? snapshot : displayData.find((row) => keyOf(row) === selectedKey) ?? snapshot;
|
|
3471
|
-
const linkedId = urlPeek && selectedKey && !rowOrSnapshot ? selectedKey : null;
|
|
3472
|
-
const linked = useOne(resourceName ?? "__noop__", linkedId, {
|
|
3473
|
-
enabled: isClientMode && !!linkedId
|
|
3474
|
-
});
|
|
3475
|
-
const selectedItem = rowOrSnapshot ?? (linkedId && linked.data ? linked.data : null);
|
|
3476
|
-
const linkMissing = !!linkedId && !selectedItem && !isLoading && (isClientMode ? linked.isError : true);
|
|
3477
|
-
const detailOpen = urlPeek ? selectedItem !== null || linkMissing : page.detailOpen;
|
|
3478
|
-
const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
|
|
3479
|
-
const handleToggleSelect = useCallback((id) => {
|
|
3480
|
-
setSelectedIds((prev) => {
|
|
3481
|
-
const next = new Set(prev);
|
|
3482
|
-
if (next.has(id)) next.delete(id);
|
|
3483
|
-
else next.add(id);
|
|
3484
|
-
return next;
|
|
3485
|
-
});
|
|
3486
|
-
}, []);
|
|
3487
|
-
const handleToggleAll = useCallback(() => {
|
|
3488
|
-
setSelectedIds((prev) => {
|
|
3489
|
-
if (prev.size === page.data.length) return /* @__PURE__ */ new Set();
|
|
3490
|
-
return new Set(page.data.map(keyOf));
|
|
3491
|
-
});
|
|
3492
|
-
}, [page.data, keyOf]);
|
|
3493
|
-
const handleClearSelection = useCallback(() => {
|
|
3494
|
-
setSelectedIds(/* @__PURE__ */ new Set());
|
|
3495
|
-
}, []);
|
|
3496
|
-
const [formOpen, setFormOpen] = useState(false);
|
|
3497
|
-
const [formAction, setFormAction] = useState("create");
|
|
3498
|
-
const [formItem, setFormItem] = useState(null);
|
|
3499
|
-
const useBuiltInCreate = allowCreate && !onCreate && isClientMode;
|
|
3500
|
-
const useBuiltInEdit = allowEdit && !onEdit && isClientMode;
|
|
3501
|
-
const handleCreate = useCallback(() => {
|
|
3502
|
-
if (useBuiltInCreate) {
|
|
3503
|
-
setFormAction("create");
|
|
3504
|
-
setFormItem(null);
|
|
3505
|
-
setFormOpen(true);
|
|
3506
|
-
} else if (onCreate) {
|
|
3507
|
-
onCreate();
|
|
3508
|
-
}
|
|
3509
|
-
}, [useBuiltInCreate, onCreate]);
|
|
3510
|
-
const allows = useCallback(
|
|
3511
|
-
(action, item) => {
|
|
3512
|
-
if (can && !can(action, item)) return false;
|
|
3513
|
-
if (!item) return true;
|
|
3514
|
-
if (canEditItem && !canEditItem(item)) return false;
|
|
3515
|
-
if (action === "edit" && canEdit && !canEdit(item)) return false;
|
|
3516
|
-
if (action === "delete" && canDelete && !canDelete(item)) return false;
|
|
3517
|
-
return true;
|
|
3518
|
-
},
|
|
3519
|
-
[can, canEditItem, canEdit, canDelete]
|
|
3520
|
-
);
|
|
3521
|
-
const handleEdit = useCallback(
|
|
3522
|
-
(item) => {
|
|
3523
|
-
if (!allows("edit", item)) return;
|
|
3524
|
-
if (useBuiltInEdit) {
|
|
3525
|
-
setFormAction("edit");
|
|
3526
|
-
setFormItem(item);
|
|
3527
|
-
setFormOpen(true);
|
|
3528
|
-
} else if (onEdit) {
|
|
3529
|
-
onEdit(item);
|
|
3530
|
-
}
|
|
3531
|
-
},
|
|
3532
|
-
[useBuiltInEdit, onEdit, allows]
|
|
3533
|
-
);
|
|
3534
|
-
const closeDetail = useCallback(() => {
|
|
3535
|
-
page.setDetailOpen(false);
|
|
3536
|
-
page.setSelectedItem(null);
|
|
3537
|
-
urlPeek?.setDetailId(null);
|
|
3538
|
-
}, [page, urlPeek]);
|
|
3539
|
-
const useBuiltInDelete = !!allowDelete && !onDelete && isClientMode;
|
|
3540
|
-
const deleteConfirmed = confirmDelete === void 0 ? useBuiltInDelete : !!confirmDelete;
|
|
3541
|
-
const deleteCopy = typeof confirmDelete === "object" ? confirmDelete : void 0;
|
|
3542
|
-
const [pendingDelete, setPendingDelete] = useState(null);
|
|
3543
|
-
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
3544
|
-
const { remove: removeRecord } = resourceHook;
|
|
3545
|
-
const { data: providerCaps } = useCapabilities(resourceName, {
|
|
3546
|
-
enabled: useBuiltInDelete && deleteConfirmed
|
|
3547
|
-
});
|
|
3548
|
-
const deleteKind = providerCaps?.softDelete ? "recycle" : "permanent";
|
|
3549
|
-
const performDelete = useCallback(
|
|
3550
|
-
async (item) => {
|
|
3551
|
-
if (useBuiltInDelete) {
|
|
3552
|
-
await removeRecord(
|
|
3553
|
-
getKeyProp ? keyOf(item) : item.id
|
|
3554
|
-
);
|
|
3555
|
-
} else if (onDelete) {
|
|
3556
|
-
await onDelete(item);
|
|
3557
|
-
}
|
|
3558
|
-
closeDetail();
|
|
3559
|
-
onDeleted?.(item);
|
|
3560
|
-
},
|
|
3561
|
-
[
|
|
3562
|
-
useBuiltInDelete,
|
|
3563
|
-
removeRecord,
|
|
3564
|
-
getKeyProp,
|
|
3565
|
-
keyOf,
|
|
3566
|
-
onDelete,
|
|
3567
|
-
closeDetail,
|
|
3568
|
-
onDeleted
|
|
3569
|
-
]
|
|
3570
|
-
);
|
|
3571
|
-
const deleteHandler = !(useBuiltInDelete || onDelete) ? void 0 : deleteConfirmed ? (item) => {
|
|
3572
|
-
setPendingDelete(item);
|
|
3573
|
-
setDeleteOpen(true);
|
|
3574
|
-
} : useBuiltInDelete ? (item) => {
|
|
3575
|
-
performDelete(item).catch((err) => {
|
|
3576
|
-
if (!resourceOptions?.onError) {
|
|
3577
|
-
toast.error(err instanceof Error ? err.message : "The delete failed.");
|
|
3578
|
-
}
|
|
3579
|
-
});
|
|
3580
|
-
} : (
|
|
3581
|
-
// Unconfirmed `onDelete` keeps its original contract exactly:
|
|
3582
|
-
// called straight from the peek, nothing closed for it.
|
|
3583
|
-
(item) => {
|
|
3584
|
-
const report = (err) => {
|
|
3585
|
-
if (resourceOptions?.onError) {
|
|
3586
|
-
resourceOptions.onError(err instanceof Error ? err : new Error(String(err)));
|
|
3587
|
-
} else {
|
|
3588
|
-
toast.error(err instanceof Error ? err.message : "The delete failed.");
|
|
3589
|
-
}
|
|
3590
|
-
};
|
|
3591
|
-
try {
|
|
3592
|
-
Promise.resolve(onDelete?.(item)).catch(report);
|
|
3593
|
-
} catch (err) {
|
|
3594
|
-
report(err);
|
|
3595
|
-
}
|
|
3596
|
-
}
|
|
3597
|
-
);
|
|
3598
|
-
const selectedEditable = !!selectedItem && allows("edit", selectedItem);
|
|
3599
|
-
const selectedDeletable = !!selectedItem && allows("delete", selectedItem);
|
|
3600
|
-
const tableColumns = useMemo(() => {
|
|
3601
|
-
const base = isClientMode ? page.columns.map((col) => ({
|
|
3602
|
-
key: col.key,
|
|
3603
|
-
header: col.header ?? col.key,
|
|
3604
|
-
sortable: col.sortable ?? true,
|
|
3605
|
-
hideBelow: col.hideBelow,
|
|
3606
|
-
width: col.width,
|
|
3607
|
-
minWidth: col.minWidth,
|
|
3608
|
-
defaultVisible: col.defaultVisible,
|
|
3609
|
-
editable: col.editable,
|
|
3610
|
-
accessor: col.accessor,
|
|
3611
|
-
render: col.render ? (item, value) => col.render(item, value, actions) : void 0
|
|
3612
|
-
})) : page.tableColumns;
|
|
3613
|
-
if (!isServerControlled) return base;
|
|
3614
|
-
return base.map((col) => ({ ...col, sortable: false }));
|
|
3615
|
-
}, [
|
|
3445
|
+
actions,
|
|
3446
|
+
allows,
|
|
3447
|
+
clientTruncated,
|
|
3448
|
+
closeDetail,
|
|
3449
|
+
currentPage,
|
|
3450
|
+
deleteConfirmed,
|
|
3451
|
+
deleteCopy,
|
|
3452
|
+
deleteHandler,
|
|
3453
|
+
deleteKind,
|
|
3454
|
+
deleteOpen,
|
|
3455
|
+
detailOpen,
|
|
3456
|
+
detailSubtitle,
|
|
3457
|
+
detailTitle,
|
|
3458
|
+
displayData,
|
|
3459
|
+
displayTotal,
|
|
3460
|
+
effectivePageSize,
|
|
3461
|
+
formAction,
|
|
3462
|
+
formItem,
|
|
3463
|
+
formOpen,
|
|
3464
|
+
handleClearFilters,
|
|
3465
|
+
handleClearSelection,
|
|
3466
|
+
handleCreate,
|
|
3467
|
+
handleEdit,
|
|
3468
|
+
handlePageSizeChange,
|
|
3469
|
+
handleRowClick,
|
|
3470
|
+
handleSearchChange,
|
|
3471
|
+
handleSortSelect,
|
|
3472
|
+
handleTableSort,
|
|
3473
|
+
handleToggleAll,
|
|
3474
|
+
handleToggleSelect,
|
|
3475
|
+
hasError,
|
|
3616
3476
|
isClientMode,
|
|
3477
|
+
isLoading,
|
|
3617
3478
|
isServerControlled,
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
const pageSort = page.sort;
|
|
3624
|
-
const setPageSort = page.setSort;
|
|
3625
|
-
const tableSort = useMemo(() => {
|
|
3626
|
-
if (!pageSort) return [];
|
|
3627
|
-
const primary = { key: pageSort.field, dir: pageSort.order };
|
|
3628
|
-
const rest = tableTieBreaks && tableTieBreaks.primary === `${primary.key}:${primary.dir}` ? tableTieBreaks.rest : [];
|
|
3629
|
-
return [primary, ...rest];
|
|
3630
|
-
}, [pageSort, tableTieBreaks]);
|
|
3631
|
-
const handleTableSort = useCallback(
|
|
3632
|
-
(next) => {
|
|
3633
|
-
const [primary, ...rest] = next;
|
|
3634
|
-
setTableTieBreaks(primary ? { primary: `${primary.key}:${primary.dir}`, rest } : null);
|
|
3635
|
-
setPageSort(primary ? { field: primary.key, order: primary.dir } : null);
|
|
3636
|
-
},
|
|
3637
|
-
[setPageSort]
|
|
3638
|
-
);
|
|
3639
|
-
const pagerPage = resourceServerPaging ? resourceHook.params.pagination?.page ?? 1 : pageProp;
|
|
3640
|
-
const pagerPerPage = resourceServerPaging ? resourceHook.params.pagination?.perPage : perPageProp;
|
|
3641
|
-
const pagerTotal = resourceServerPaging ? displayTotal : totalProp;
|
|
3642
|
-
const pagerOnPageChange = resourceServerPaging ? resourceHook.setPage : onPageChange;
|
|
3643
|
-
const currentPage = pagerPage ?? 1;
|
|
3644
|
-
const writeUrl = url?.write;
|
|
3645
|
-
useEffect(() => {
|
|
3646
|
-
writeUrl?.({
|
|
3647
|
-
q: page.search,
|
|
3648
|
-
sort: page.sort,
|
|
3649
|
-
...resourceServerPaging ? { page: currentPage } : {}
|
|
3650
|
-
});
|
|
3651
|
-
}, [writeUrl, page.search, page.sort, currentPage, resourceServerPaging]);
|
|
3652
|
-
const handleSearchChange = useCallback(
|
|
3653
|
-
(value) => {
|
|
3654
|
-
page.filterConfig.onSearchChange(value);
|
|
3655
|
-
if (!isServerControlled) return;
|
|
3656
|
-
onSearchChange?.(value);
|
|
3657
|
-
if (currentPage !== 1 && !resourceServerPaging) onPageChange?.(1);
|
|
3658
|
-
},
|
|
3659
|
-
[
|
|
3660
|
-
page.filterConfig,
|
|
3661
|
-
isServerControlled,
|
|
3662
|
-
onSearchChange,
|
|
3663
|
-
onPageChange,
|
|
3664
|
-
resourceServerPaging,
|
|
3665
|
-
currentPage
|
|
3666
|
-
]
|
|
3667
|
-
);
|
|
3668
|
-
const handleSortSelect = useCallback(
|
|
3669
|
-
(value) => {
|
|
3670
|
-
page.filterConfig.onSortChange(value);
|
|
3671
|
-
if (!isServerControlled) return;
|
|
3672
|
-
const [field, order] = value.split(":");
|
|
3673
|
-
onSortChange?.(
|
|
3674
|
-
field && (order === "asc" || order === "desc") ? { field, order } : null
|
|
3675
|
-
);
|
|
3676
|
-
if (!resourceServerPaging) onPageChange?.(1);
|
|
3677
|
-
},
|
|
3678
|
-
[
|
|
3679
|
-
page.filterConfig,
|
|
3680
|
-
isServerControlled,
|
|
3681
|
-
onSortChange,
|
|
3682
|
-
onPageChange,
|
|
3683
|
-
resourceServerPaging
|
|
3684
|
-
]
|
|
3685
|
-
);
|
|
3686
|
-
const handleClearFilters = useCallback(() => {
|
|
3687
|
-
page.clearFilters();
|
|
3688
|
-
if (!isServerControlled) return;
|
|
3689
|
-
onSearchChange?.("");
|
|
3690
|
-
onSortChange?.(null);
|
|
3691
|
-
if (!resourceServerPaging) onPageChange?.(1);
|
|
3692
|
-
}, [
|
|
3479
|
+
keyOf,
|
|
3480
|
+
keyboardNav,
|
|
3481
|
+
linkMissing,
|
|
3482
|
+
linked,
|
|
3483
|
+
missingPagerProps,
|
|
3693
3484
|
page,
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
(size) => {
|
|
3721
|
-
updateSetting("pageSize", size);
|
|
3722
|
-
onPerPageChange?.(size);
|
|
3723
|
-
if (resourceServerPaging) resourceHook.setPerPage(size);
|
|
3724
|
-
else if (isServerControlled) onPageChange?.(1);
|
|
3725
|
-
},
|
|
3726
|
-
[
|
|
3727
|
-
updateSetting,
|
|
3728
|
-
onPerPageChange,
|
|
3729
|
-
resourceServerPaging,
|
|
3730
|
-
resourceHook.setPerPage,
|
|
3731
|
-
isServerControlled,
|
|
3732
|
-
onPageChange
|
|
3733
|
-
]
|
|
3734
|
-
);
|
|
3735
|
-
const missingPerPage = isServerControlled && !!pagerOnPageChange && pagerPerPage === void 0;
|
|
3736
|
-
const missingTotal = isServerControlled && !!pagerOnPageChange && pagerTotal === void 0;
|
|
3737
|
-
const pagerMisconfigured = missingPerPage || missingTotal;
|
|
3738
|
-
const missingPagerProps = [
|
|
3739
|
-
...missingPerPage ? ["perPage"] : [],
|
|
3740
|
-
...missingTotal ? ["total"] : []
|
|
3741
|
-
];
|
|
3742
|
-
const loggedMissingPerPageRef = useRef(false);
|
|
3743
|
-
useEffect(() => {
|
|
3744
|
-
if (!pagerMisconfigured) return;
|
|
3745
|
-
if (loggedMissingPerPageRef.current) return;
|
|
3746
|
-
loggedMissingPerPageRef.current = true;
|
|
3747
|
-
console.error(
|
|
3748
|
-
`ResourcePage: ${missingPagerProps.map((name) => `\`${name}\``).join(" and ")} ${missingPagerProps.length > 1 ? "are" : "is"} required when \`onPageChange\` is supplied.`
|
|
3749
|
-
);
|
|
3750
|
-
}, [pagerMisconfigured]);
|
|
3751
|
-
const pagerPageSize = pagerPerPage ?? effectivePageSize;
|
|
3752
|
-
const totalPages = Math.max(
|
|
3753
|
-
1,
|
|
3754
|
-
Math.ceil(displayTotal / Math.max(1, pagerPageSize))
|
|
3755
|
-
);
|
|
3756
|
-
const rangeFrom = page.data.length === 0 ? 0 : (currentPage - 1) * pagerPageSize + 1;
|
|
3757
|
-
const rangeTo = rangeFrom === 0 ? 0 : rangeFrom + page.data.length - 1;
|
|
3758
|
-
const { setPage: setResourcePage } = resourceHook;
|
|
3759
|
-
const settledPage = !isLoading && !hasError && !resourceHook.list.isPlaceholderData;
|
|
3760
|
-
useEffect(() => {
|
|
3761
|
-
if (!resourceServerPaging || !settledPage) return;
|
|
3762
|
-
if (currentPage > totalPages) setResourcePage(totalPages);
|
|
3763
|
-
}, [resourceServerPaging, settledPage, currentPage, totalPages, setResourcePage]);
|
|
3764
|
-
const titleOf = (item) => getTitle ? getTitle(item) : String(item[page.titleField] ?? "");
|
|
3765
|
-
const detailTitle = selectedItem ? titleOf(selectedItem) : "";
|
|
3766
|
-
const detailSubtitle = !selectedItem ? void 0 : getSubtitle ? getSubtitle(selectedItem) : page.subtitleField ? String(
|
|
3767
|
-
selectedItem[page.subtitleField] ?? ""
|
|
3768
|
-
) : void 0;
|
|
3769
|
-
const handleRowClick = (item) => {
|
|
3770
|
-
if (detail === "route") {
|
|
3771
|
-
const id = item.id;
|
|
3772
|
-
if (resourceName && id) {
|
|
3773
|
-
onNavigate(`/${resourceName}/${id}`);
|
|
3774
|
-
}
|
|
3775
|
-
return;
|
|
3776
|
-
}
|
|
3777
|
-
page.handleRowClick(item);
|
|
3778
|
-
urlPeek?.setDetailId(keyOf(item));
|
|
3779
|
-
};
|
|
3780
|
-
const hasBulkSelect = !!bulkActions && bulkActions.length > 0;
|
|
3781
|
-
const overlayOpen = detailOpen || formOpen || deleteOpen;
|
|
3782
|
-
const keyboardNav = useKeyboardNavigation({
|
|
3783
|
-
itemCount: page.data.length,
|
|
3784
|
-
enabled: keyboardNavigation && page.viewMode !== "table",
|
|
3785
|
-
overlayOpen,
|
|
3786
|
-
onOpen: (index) => {
|
|
3787
|
-
const item = page.data[index];
|
|
3788
|
-
if (item) handleRowClick(item);
|
|
3789
|
-
},
|
|
3790
|
-
onToggleSelect: hasBulkSelect ? (index) => {
|
|
3791
|
-
const item = page.data[index];
|
|
3792
|
-
if (item) {
|
|
3793
|
-
handleToggleSelect(keyOf(item));
|
|
3794
|
-
}
|
|
3795
|
-
} : void 0,
|
|
3796
|
-
// Supplied only while there is an open detail to close — the hook
|
|
3797
|
-
// suppresses Backspace's default whenever this is present, so leaving it
|
|
3798
|
-
// wired up permanently would swallow the browser/app's Backspace on
|
|
3799
|
-
// every grid/list page (including detail="none" and detail="route",
|
|
3800
|
-
// which never open one). `detailOpen` is only ever true for the panel
|
|
3801
|
-
// and modal modes, so it is the precise precondition here.
|
|
3802
|
-
onBack: detailOpen ? closeDetail : void 0
|
|
3803
|
-
});
|
|
3485
|
+
pagerMisconfigured,
|
|
3486
|
+
pagerOnPageChange,
|
|
3487
|
+
pendingDelete,
|
|
3488
|
+
performDelete,
|
|
3489
|
+
rangeFrom,
|
|
3490
|
+
rangeTo,
|
|
3491
|
+
resourceHook,
|
|
3492
|
+
resourceServerPaging,
|
|
3493
|
+
selectedDeletable,
|
|
3494
|
+
selectedEditable,
|
|
3495
|
+
selectedIds,
|
|
3496
|
+
selectedItem,
|
|
3497
|
+
serverSearchFields,
|
|
3498
|
+
setDeleteOpen,
|
|
3499
|
+
setFormOpen,
|
|
3500
|
+
settings,
|
|
3501
|
+
statusChipsConfig,
|
|
3502
|
+
tableColumns,
|
|
3503
|
+
tableSort,
|
|
3504
|
+
titleOf,
|
|
3505
|
+
totalPages,
|
|
3506
|
+
updateSetting,
|
|
3507
|
+
useBuiltInCreate,
|
|
3508
|
+
useBuiltInDelete,
|
|
3509
|
+
useBuiltInEdit
|
|
3510
|
+
} = useResourceTableModel(props);
|
|
3804
3511
|
const showCreateButton = (onCreate || useBuiltInCreate) && allows("create");
|
|
3805
3512
|
const createButton = showCreateButton ? /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: handleCreate, children: [
|
|
3806
3513
|
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4 mr-1" }),
|