@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.
@@ -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 { titleCase, useResourcePage } from '../../chunk-RKPS7ERS.js';
33
- import { useResourceUrlState } from '../../chunk-DIV4BYXX.js';
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 { useViewSettings } from '../../chunk-OZ7ZAYCS.js';
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, defaultNavigate } from '../../chunk-WGMEVX3Y.js';
44
+ import { DefaultLink } from '../../chunk-WGMEVX3Y.js';
42
45
  export { DefaultLink, defaultNavigate, isPathActive, resolveNavigation } from '../../chunk-WGMEVX3Y.js';
43
- import { useKeyboardNavigation } from '../../chunk-YJ6MPS6U.js';
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, useCapabilities, buildArrayItemDefaults } from '@marktiderman/genesis-core/hooks';
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 tabStopKey = activeRowKey && displayItems.some((it) => getKey(it) === activeRowKey) ? activeRowKey : displayItems[0] ? getKey(displayItems[0]) : null;
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 move = (item, to) => {
1806
- void Promise.resolve(onMoveItem?.(item, to)).catch(() => {
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 (!key || !getKey) return;
1815
- for (const column of columns) {
1816
- const item = column.items.find((i) => getKey(i) === key);
1817
- if (item) {
1818
- if (column.key !== to) move(item, to);
1819
- return;
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 movable = Boolean(onMoveItem && getKey);
1824
- return /* @__PURE__ */ jsx(
1825
- "div",
1826
- {
1827
- className: cn(
1828
- "grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
1829
- className
1830
- ),
1831
- children: columns.map((column) => {
1832
- const resolved = column.status ? statusColorMap[column.status] : { color: column.color ?? "", bgColor: column.bgColor ?? "" };
1833
- return /* @__PURE__ */ jsxs(
1834
- "section",
1835
- {
1836
- "aria-label": column.label,
1837
- "data-kanban-column": column.key,
1838
- className: cn(
1839
- "flex flex-col gap-3 rounded-lg",
1840
- movable && over === column.key && dragging?.from !== column.key && "bg-accent/40 ring-2 ring-primary/40"
1841
- ),
1842
- onDragOver: movable ? (e) => {
1843
- if (!dragging) return;
1844
- e.preventDefault();
1845
- e.dataTransfer.dropEffect = "move";
1846
- if (over !== column.key) setOver(column.key);
1847
- } : void 0,
1848
- onDragLeave: movable ? (e) => {
1849
- if (!e.currentTarget.contains(e.relatedTarget)) setOver(null);
1850
- } : void 0,
1851
- onDrop: movable ? (e) => onDrop(e, column.key) : void 0,
1852
- children: [
1853
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1854
- /* @__PURE__ */ jsx("span", { className: cn("text-sm font-semibold", resolved.color), children: column.label }),
1855
- /* @__PURE__ */ jsx(
1856
- Badge,
1857
- {
1858
- variant: "secondary",
1859
- className: cn("text-[10px] px-1.5 py-0", resolved.bgColor),
1860
- children: column.total ?? column.items.length
1861
- }
1862
- )
1863
- ] }),
1864
- /* @__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) => {
1865
- const key = keyOf(item, i);
1866
- return /* @__PURE__ */ jsxs(
1867
- "div",
1868
- {
1869
- className: cn("animate-fade-in", movable && "group relative", dragging?.key === key && "opacity-50"),
1870
- style: { animationDelay: `${i * 30}ms`, animationFillMode: "both" },
1871
- draggable: movable || void 0,
1872
- onDragStart: movable ? (e) => {
1873
- e.dataTransfer.setData(DRAG_TYPE, key);
1874
- e.dataTransfer.effectAllowed = "move";
1875
- setDragging({ key, from: column.key });
1876
- } : void 0,
1877
- onDragEnd: movable ? () => {
1878
- setDragging(null);
1879
- setOver(null);
1880
- } : void 0,
1881
- children: [
1882
- renderCard(item),
1883
- movable && columns.length > 1 && /* @__PURE__ */ jsxs(DropdownMenu, { children: [
1884
- /* @__PURE__ */ jsx(
1885
- DropdownMenuTrigger,
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
- "aria-label": `Move ${getItemLabel?.(item) ?? "card"}`,
1888
- 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",
1889
- children: /* @__PURE__ */ jsx(ArrowRightLeft, { className: "h-3.5 w-3.5", "aria-hidden": true })
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(DropdownMenuContent, { align: "end", children: [
1893
- /* @__PURE__ */ jsx(DropdownMenuLabel, { children: "Move to" }),
1894
- columns.filter((c) => c.key !== column.key).map((c) => /* @__PURE__ */ jsx(DropdownMenuItem, { onSelect: () => move(item, c.key), children: c.label }, c.key))
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
- key
1900
- );
1901
- }) }),
1902
- renderColumnFooter?.(column)
1903
- ]
1904
- },
1905
- column.key
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
- resource: resourceName,
3222
- data: dataProp,
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
- canEdit,
3274
- canDelete,
3275
- allowDelete,
3276
- confirmDelete,
3277
- onDeleted,
3278
- secondaryActions,
3279
- allowCreate,
3280
- allowEdit,
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
- renderForm: renderFormProp,
3405
+ formLayout: formLayoutProp,
3283
3406
  formMode,
3284
3407
  formSteps,
3285
- formLayout: formLayoutProp,
3408
+ gridCols: gridColsProp,
3409
+ header: headerMode = "page",
3410
+ icon,
3411
+ keyboardNavigation = true,
3286
3412
  mutationMode: mutationModeProp,
3287
- bulkActions,
3288
- renderCard: renderCardProp,
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
- gridCols: gridColsProp,
3292
- keyboardNavigation = true,
3293
- capabilities,
3294
- toolbar,
3295
- emptyState,
3296
- emptyIcon,
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
- className
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
- setSearch: setResourceSearch,
3377
- setSort: setResourceSort,
3378
- setFilters: setResourceFilters
3379
- } = resourceHook;
3380
- const pushedSearchRef = useRef(resourceServerPaging ? urlSeed?.q ?? "" : "");
3381
- const searchEffectRanRef = useRef(false);
3382
- useEffect(() => {
3383
- if (!resourceServerPaging) return;
3384
- const firstRun = !searchEffectRanRef.current;
3385
- searchEffectRanRef.current = true;
3386
- if (page.search === pushedSearchRef.current) return;
3387
- const push = () => {
3388
- pushedSearchRef.current = page.search;
3389
- setResourceSearch(page.search);
3390
- };
3391
- if (firstRun || page.search === "") {
3392
- push();
3393
- return;
3394
- }
3395
- const timer = setTimeout(push, 300);
3396
- return () => clearTimeout(timer);
3397
- }, [resourceServerPaging, page.search, setResourceSearch]);
3398
- const defaultSort = resourceOptions?.defaultSort;
3399
- const pushedSortRef = useRef(resourceServerPaging ? urlSortKey : "");
3400
- useEffect(() => {
3401
- if (!resourceServerPaging) return;
3402
- const key = page.sort ? `${page.sort.field}:${page.sort.order}` : "";
3403
- if (key === pushedSortRef.current) return;
3404
- pushedSortRef.current = key;
3405
- setResourceSort(page.sort ?? defaultSort);
3406
- }, [resourceServerPaging, page.sort, defaultSort, setResourceSort]);
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
- page.columns,
3619
- page.tableColumns,
3620
- actions
3621
- ]);
3622
- const [tableTieBreaks, setTableTieBreaks] = useState(null);
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
- isServerControlled,
3695
- onSearchChange,
3696
- onSortChange,
3697
- onPageChange,
3698
- resourceServerPaging
3699
- ]);
3700
- const handleStatusChange = useCallback(
3701
- (values) => {
3702
- page.filterConfig.statusChips?.onChange(values);
3703
- if (!isServerControlled) return;
3704
- if (currentPage !== 1 && !resourceServerPaging) onPageChange?.(1);
3705
- },
3706
- [
3707
- page.filterConfig,
3708
- isServerControlled,
3709
- onPageChange,
3710
- resourceServerPaging,
3711
- currentPage
3712
- ]
3713
- );
3714
- const statusChipsConfig = useMemo(() => {
3715
- const base = page.filterConfig.statusChips;
3716
- if (!base) return void 0;
3717
- return { ...base, onChange: handleStatusChange };
3718
- }, [page.filterConfig.statusChips, handleStatusChange]);
3719
- const handlePageSizeChange = useCallback(
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" }),