@marktiderman/genesis-ui 2.8.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 +75 -4
- 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-DIV4BYXX.js +58 -0
- package/dist/chunk-DIV4BYXX.js.map +1 -0
- package/dist/chunk-I4NBE2KL.js +16 -0
- package/dist/chunk-I4NBE2KL.js.map +1 -0
- package/dist/{chunk-DWCAC2E7.js → chunk-RKPS7ERS.js} +5 -4
- package/dist/chunk-RKPS7ERS.js.map +1 -0
- package/dist/components/data/index.d.ts +42 -949
- package/dist/components/data/index.js +962 -777
- package/dist/components/data/index.js.map +1 -1
- package/dist/components/layout/index.d.ts +2 -1
- package/dist/components/layout/index.js +7 -7
- package/dist/components/patterns/index.js +2 -1
- package/dist/components/patterns/sortable-list.js +2 -1
- package/dist/hooks/index.d.ts +8 -1
- package/dist/hooks/index.js +4 -1
- package/dist/hooks/use-resource-page.d.ts +1 -1
- package/dist/hooks/use-resource-page.js +1 -1
- package/dist/hooks/use-resource-url-state.d.ts +68 -0
- package/dist/hooks/use-resource-url-state.js +3 -0
- package/dist/hooks/use-resource-url-state.js.map +1 -0
- package/dist/index.js +21 -20
- package/dist/{use-resource-page-D4byuwAs.d.ts → use-resource-page-DaJzuq3M.d.ts} +63 -2
- package/dist/use-resource-table-DCVf_lEG.d.ts +1113 -0
- package/package.json +4 -8
- package/dist/chunk-DWCAC2E7.js.map +0 -1
- package/dist/chunk-S7NXD37O.js.map +0 -1
|
@@ -2,51 +2,58 @@
|
|
|
2
2
|
import { AppShell, PageHeader } from '../../chunk-UYI3GLFJ.js';
|
|
3
3
|
export { ViewSettings, ViewToggle } from '../../chunk-46TZGMFW.js';
|
|
4
4
|
import { renderSlot } from '../../chunk-7S7LKQBD.js';
|
|
5
|
+
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '../../chunk-N2ZFNOOT.js';
|
|
5
6
|
import { Textarea } from '../../chunk-YETMISCE.js';
|
|
6
7
|
import { Separator } from '../../chunk-SXSCHWZU.js';
|
|
7
8
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '../../chunk-XCGZPFT7.js';
|
|
8
9
|
import { Skeleton } from '../../chunk-QFM4Z2SS.js';
|
|
9
|
-
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '../../chunk-N2ZFNOOT.js';
|
|
10
10
|
import { Progress } from '../../chunk-W5Y3LG5K.js';
|
|
11
|
-
import { Card, CardContent, CardHeader, CardTitle } from '../../chunk-FN3FZVUR.js';
|
|
12
11
|
import { Checkbox } from '../../chunk-SEBU6JCK.js';
|
|
12
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../chunk-FN3FZVUR.js';
|
|
13
13
|
import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from '../../chunk-4C4ESW6O.js';
|
|
14
|
+
import { PageLoading } from '../../chunk-YRYQXU3G.js';
|
|
15
|
+
import { Spinner } from '../../chunk-OVU3Z2MT.js';
|
|
16
|
+
import { useAnnouncer } from '../../chunk-I4NBE2KL.js';
|
|
14
17
|
import { Input } from '../../chunk-QSXVL5UB.js';
|
|
15
18
|
import '../../chunk-B2GIBUQD.js';
|
|
16
19
|
import { Badge } from '../../chunk-NPNADZHM.js';
|
|
17
|
-
import { DetailPage } from '../../chunk-AFZ76Q26.js';
|
|
18
|
-
import '../../chunk-D2LIXA2Y.js';
|
|
19
|
-
import '../../chunk-AVFZ4P4B.js';
|
|
20
|
-
import { PageLoading } from '../../chunk-YRYQXU3G.js';
|
|
21
|
-
import { Spinner } from '../../chunk-OVU3Z2MT.js';
|
|
22
20
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../../chunk-FK42ZGGV.js';
|
|
23
21
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuSeparator, DropdownMenuItem, DropdownMenuLabel } from '../../chunk-QJ6P2DW6.js';
|
|
24
22
|
import { Switch } from '../../chunk-27ELYTGE.js';
|
|
25
23
|
import { Popover, PopoverTrigger, PopoverContent } from '../../chunk-22XB3Q6H.js';
|
|
24
|
+
import { DetailPage } from '../../chunk-AFZ76Q26.js';
|
|
25
|
+
import '../../chunk-D2LIXA2Y.js';
|
|
26
|
+
import '../../chunk-AVFZ4P4B.js';
|
|
26
27
|
import { EmptyState } from '../../chunk-65SPT7EA.js';
|
|
27
28
|
export { EmptyState } from '../../chunk-65SPT7EA.js';
|
|
28
29
|
import { Button } from '../../chunk-NWSCYLPT.js';
|
|
29
30
|
import { FormField, FormLabel, FormError, FormDescription } from '../../chunk-R7QFP6EL.js';
|
|
30
31
|
import '../../chunk-F47U5KNW.js';
|
|
31
|
-
import {
|
|
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';
|
|
32
36
|
export { useSavedViews } from '../../chunk-GCQFVMPI.js';
|
|
33
37
|
import '../../chunk-PKFDGGZV.js';
|
|
34
|
-
import
|
|
38
|
+
import '../../chunk-OZ7ZAYCS.js';
|
|
39
|
+
import '../../chunk-4RAZJ6LL.js';
|
|
35
40
|
export { CommandPalette, useCommandPaletteHotkey } from '../../chunk-CNTWQSUT.js';
|
|
36
41
|
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator } from '../../chunk-SEFK4NZV.js';
|
|
37
42
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '../../chunk-7XMZVISM.js';
|
|
38
43
|
import { cn } from '../../chunk-NZ5WBA4R.js';
|
|
39
|
-
import { DefaultLink
|
|
44
|
+
import { DefaultLink } from '../../chunk-WGMEVX3Y.js';
|
|
40
45
|
export { DefaultLink, defaultNavigate, isPathActive, resolveNavigation } from '../../chunk-WGMEVX3Y.js';
|
|
41
|
-
import
|
|
46
|
+
import '../../chunk-YJ6MPS6U.js';
|
|
42
47
|
import { useIsMobile } from '../../chunk-VFO3I7PV.js';
|
|
43
|
-
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';
|
|
44
49
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
45
|
-
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';
|
|
51
|
+
import { useReactTable, getPaginationRowModel, getSortedRowModel, getCoreRowModel } from '@tanstack/react-table';
|
|
52
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
46
53
|
import { formatDistanceToNow, format } from 'date-fns';
|
|
47
54
|
import { useFieldArray, Controller, useWatch } from 'react-hook-form';
|
|
48
55
|
import { useResource, useWizard, useResourceForm, useOne, buildArrayItemDefaults } from '@marktiderman/genesis-core/hooks';
|
|
49
|
-
import { useDataProvider } from '@marktiderman/genesis-core/provider';
|
|
56
|
+
import { useDataProvider, isDataReadError } from '@marktiderman/genesis-core/provider';
|
|
50
57
|
import { useDropzone } from 'react-dropzone';
|
|
51
58
|
|
|
52
59
|
function DataPageShell({
|
|
@@ -670,6 +677,50 @@ var DENSITY_HEAD = {
|
|
|
670
677
|
comfortable: "h-10",
|
|
671
678
|
spacious: "h-12 py-3"
|
|
672
679
|
};
|
|
680
|
+
function storeSet(key, value) {
|
|
681
|
+
try {
|
|
682
|
+
localStorage.setItem(key, value);
|
|
683
|
+
} catch {
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function storeRemove(key) {
|
|
687
|
+
try {
|
|
688
|
+
localStorage.removeItem(key);
|
|
689
|
+
} catch {
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
var ROW_HEIGHT = { compact: 33, comfortable: 41, spacious: 49 };
|
|
693
|
+
var PINNED_WIDTH = 160;
|
|
694
|
+
function compareValues(a, b) {
|
|
695
|
+
const emptyA = a == null || a === "";
|
|
696
|
+
const emptyB = b == null || b === "";
|
|
697
|
+
if (emptyA || emptyB) return emptyA === emptyB ? 0 : emptyA ? -1 : 1;
|
|
698
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
699
|
+
if (typeof a === "bigint" && typeof b === "bigint") return a < b ? -1 : a > b ? 1 : 0;
|
|
700
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
|
|
701
|
+
if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
|
|
702
|
+
return String(a).localeCompare(String(b), void 0, { numeric: true, sensitivity: "base" });
|
|
703
|
+
}
|
|
704
|
+
var ROW_OPEN = "data-row-open";
|
|
705
|
+
function focusRowAt(body, index) {
|
|
706
|
+
const target = body?.querySelector(`[${ROW_OPEN}="${index}"]`);
|
|
707
|
+
target?.focus();
|
|
708
|
+
return !!target;
|
|
709
|
+
}
|
|
710
|
+
function nextSort(prev, key, shiftKey) {
|
|
711
|
+
const existing = prev.findIndex((s) => s.key === key);
|
|
712
|
+
if (existing >= 0) {
|
|
713
|
+
const entry = prev[existing];
|
|
714
|
+
if (entry.dir === "asc") {
|
|
715
|
+
const next = [...prev];
|
|
716
|
+
next[existing] = { key, dir: "desc" };
|
|
717
|
+
return next;
|
|
718
|
+
}
|
|
719
|
+
return prev.filter((_, i) => i !== existing);
|
|
720
|
+
}
|
|
721
|
+
if (shiftKey) return [...prev, { key, dir: "asc" }];
|
|
722
|
+
return [{ key, dir: "asc" }];
|
|
723
|
+
}
|
|
673
724
|
function getCellClasses(col) {
|
|
674
725
|
const hide = col.hideBelow ? col.hideBelow === "sm" ? "hidden sm:table-cell" : col.hideBelow === "md" ? "hidden md:table-cell" : col.hideBelow === "lg" ? "hidden lg:table-cell" : "hidden xl:table-cell" : "";
|
|
675
726
|
return cn(typeof col.width === "string" && !PX_WIDTH.test(col.width) ? col.width : void 0, hide);
|
|
@@ -888,29 +939,29 @@ function DataTable({
|
|
|
888
939
|
stickyHeader = true,
|
|
889
940
|
resizeKey,
|
|
890
941
|
stickyFirstColumn,
|
|
891
|
-
density = "comfortable"
|
|
942
|
+
density = "comfortable",
|
|
943
|
+
keyboardNavigation = true,
|
|
944
|
+
openColumn,
|
|
945
|
+
virtualize,
|
|
946
|
+
pinnedColumns,
|
|
947
|
+
reorderableColumns = true,
|
|
948
|
+
columnOrderKey,
|
|
949
|
+
sort: sortProp,
|
|
950
|
+
onSortChange
|
|
892
951
|
}) {
|
|
952
|
+
const rowKeys = keyboardNavigation && !!onRowClick;
|
|
953
|
+
const [activeRowKey, setActiveRowKey] = useState(null);
|
|
893
954
|
const cellDensity = DENSITY_CELL[density];
|
|
894
955
|
const headDensity = DENSITY_HEAD[density];
|
|
895
|
-
const [
|
|
956
|
+
const [ownSorts, setOwnSorts] = useState([]);
|
|
957
|
+
const sorts = sortProp ?? ownSorts;
|
|
896
958
|
const handleSort = useCallback(
|
|
897
959
|
(key, shiftKey) => {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
const entry = prev[existing];
|
|
902
|
-
if (entry.dir === "asc") {
|
|
903
|
-
const next = [...prev];
|
|
904
|
-
next[existing] = { key, dir: "desc" };
|
|
905
|
-
return next;
|
|
906
|
-
}
|
|
907
|
-
return prev.filter((_, i) => i !== existing);
|
|
908
|
-
}
|
|
909
|
-
if (shiftKey) return [...prev, { key, dir: "asc" }];
|
|
910
|
-
return [{ key, dir: "asc" }];
|
|
911
|
-
});
|
|
960
|
+
const next = nextSort(sorts, key, shiftKey);
|
|
961
|
+
if (sortProp === void 0) setOwnSorts(next);
|
|
962
|
+
onSortChange?.(next);
|
|
912
963
|
},
|
|
913
|
-
[]
|
|
964
|
+
[sorts, sortProp, onSortChange]
|
|
914
965
|
);
|
|
915
966
|
const [visibleCols, setVisibleCols] = useState(
|
|
916
967
|
() => {
|
|
@@ -950,7 +1001,7 @@ function DataTable({
|
|
|
950
1001
|
}, [columnVisibilityKey]);
|
|
951
1002
|
useEffect(() => {
|
|
952
1003
|
if (!visibilityHydrated || !columnVisibilityKey) return;
|
|
953
|
-
|
|
1004
|
+
storeSet(
|
|
954
1005
|
`dt-vis-${columnVisibilityKey}`,
|
|
955
1006
|
JSON.stringify(visibleCols)
|
|
956
1007
|
);
|
|
@@ -964,8 +1015,9 @@ function DataTable({
|
|
|
964
1015
|
defaults[c.key] = c.defaultVisible !== false;
|
|
965
1016
|
});
|
|
966
1017
|
setVisibleCols(defaults);
|
|
1018
|
+
setColOrder(null);
|
|
967
1019
|
if (columnVisibilityKey) {
|
|
968
|
-
|
|
1020
|
+
storeRemove(`dt-vis-${columnVisibilityKey}`);
|
|
969
1021
|
}
|
|
970
1022
|
}, [columns, columnVisibilityKey]);
|
|
971
1023
|
useEffect(() => {
|
|
@@ -981,7 +1033,54 @@ function DataTable({
|
|
|
981
1033
|
return changed ? updated : prev;
|
|
982
1034
|
});
|
|
983
1035
|
}, [columns]);
|
|
984
|
-
const
|
|
1036
|
+
const [colOrder, setColOrder] = useState(null);
|
|
1037
|
+
const [orderHydrated, setOrderHydrated] = useState(false);
|
|
1038
|
+
useEffect(() => {
|
|
1039
|
+
if (columnOrderKey) {
|
|
1040
|
+
try {
|
|
1041
|
+
const saved = localStorage.getItem(`dt-order-${columnOrderKey}`);
|
|
1042
|
+
const parsed = saved ? JSON.parse(saved) : null;
|
|
1043
|
+
if (Array.isArray(parsed) && parsed.every((k) => typeof k === "string")) {
|
|
1044
|
+
setColOrder(parsed);
|
|
1045
|
+
}
|
|
1046
|
+
} catch {
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
setOrderHydrated(true);
|
|
1050
|
+
}, [columnOrderKey]);
|
|
1051
|
+
useEffect(() => {
|
|
1052
|
+
if (!orderHydrated || !columnOrderKey) return;
|
|
1053
|
+
if (colOrder) storeSet(`dt-order-${columnOrderKey}`, JSON.stringify(colOrder));
|
|
1054
|
+
else storeRemove(`dt-order-${columnOrderKey}`);
|
|
1055
|
+
}, [colOrder, columnOrderKey, orderHydrated]);
|
|
1056
|
+
const orderedColumns = useMemo(() => {
|
|
1057
|
+
if (!colOrder) return columns;
|
|
1058
|
+
const rank = new Map(colOrder.map((k, i) => [k, i]));
|
|
1059
|
+
return [...columns].sort(
|
|
1060
|
+
(a, b) => (rank.get(a.key) ?? colOrder.length) - (rank.get(b.key) ?? colOrder.length)
|
|
1061
|
+
);
|
|
1062
|
+
}, [columns, colOrder]);
|
|
1063
|
+
const moveColumn = useCallback(
|
|
1064
|
+
(key, to) => {
|
|
1065
|
+
const keys = orderedColumns.map((c) => c.key);
|
|
1066
|
+
const from = keys.indexOf(key);
|
|
1067
|
+
if (from < 0) return;
|
|
1068
|
+
keys.splice(from, 1);
|
|
1069
|
+
const at = "before" in to ? Math.max(0, keys.indexOf(to.before)) : keys.indexOf(to.after) + 1;
|
|
1070
|
+
keys.splice(at, 0, key);
|
|
1071
|
+
setColOrder(keys);
|
|
1072
|
+
},
|
|
1073
|
+
[orderedColumns]
|
|
1074
|
+
);
|
|
1075
|
+
const [dragKey, setDragKey] = useState(null);
|
|
1076
|
+
const visibleColumns = columnVisibility ? orderedColumns.filter((c) => visibleCols[c.key] !== false) : orderedColumns;
|
|
1077
|
+
const leftKeys = pinnedColumns?.left ?? (stickyFirstColumn && visibleColumns[0] ? [visibleColumns[0].key] : []);
|
|
1078
|
+
const rightKeys = pinnedColumns?.right ?? [];
|
|
1079
|
+
const activeColumns = [
|
|
1080
|
+
...visibleColumns.filter((c) => leftKeys.includes(c.key)),
|
|
1081
|
+
...visibleColumns.filter((c) => !leftKeys.includes(c.key) && !rightKeys.includes(c.key)),
|
|
1082
|
+
...visibleColumns.filter((c) => rightKeys.includes(c.key))
|
|
1083
|
+
];
|
|
985
1084
|
const [colWidths, setColWidths] = useState(() => {
|
|
986
1085
|
if (resizeKey) {
|
|
987
1086
|
try {
|
|
@@ -1008,46 +1107,236 @@ function DataTable({
|
|
|
1008
1107
|
);
|
|
1009
1108
|
const handleResizeEnd = useCallback(() => {
|
|
1010
1109
|
if (resizeKey) {
|
|
1011
|
-
|
|
1110
|
+
storeSet(`dt-resize-${resizeKey}`, JSON.stringify(colWidths));
|
|
1012
1111
|
}
|
|
1013
1112
|
}, [resizeKey, colWidths]);
|
|
1113
|
+
const pinWidth = (key) => colWidths[key] ?? PINNED_WIDTH;
|
|
1114
|
+
const pinned = {};
|
|
1115
|
+
{
|
|
1116
|
+
let left = selectable ? 40 : 0;
|
|
1117
|
+
for (const c of activeColumns) {
|
|
1118
|
+
if (!leftKeys.includes(c.key)) continue;
|
|
1119
|
+
pinned[c.key] = { side: "left", offset: left };
|
|
1120
|
+
left += pinWidth(c.key);
|
|
1121
|
+
}
|
|
1122
|
+
let right = renderActions ? 40 : 0;
|
|
1123
|
+
for (const c of [...activeColumns].reverse()) {
|
|
1124
|
+
if (!rightKeys.includes(c.key)) continue;
|
|
1125
|
+
pinned[c.key] = { side: "right", offset: right };
|
|
1126
|
+
right += pinWidth(c.key);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
const pinProps = (key, header) => {
|
|
1130
|
+
const z = header ? stickyHeader ? "z-[15]" : "z-[5]" : "z-[1]";
|
|
1131
|
+
if (key === "__select") {
|
|
1132
|
+
return leftKeys.length ? { className: cn("sticky left-0 bg-background", z), style: { left: 0 } } : null;
|
|
1133
|
+
}
|
|
1134
|
+
if (key === "__actions") {
|
|
1135
|
+
return rightKeys.length ? { className: cn("sticky right-0 bg-background", z), style: { right: 0 } } : null;
|
|
1136
|
+
}
|
|
1137
|
+
const p = pinned[key];
|
|
1138
|
+
return p ? { className: cn("sticky bg-background", z), style: { [p.side]: p.offset } } : null;
|
|
1139
|
+
};
|
|
1014
1140
|
const [currentPage, setCurrentPage] = useState(0);
|
|
1015
1141
|
const [currentPageSize, setCurrentPageSize] = useState(initialPageSize);
|
|
1016
1142
|
useEffect(() => {
|
|
1017
1143
|
setCurrentPage(0);
|
|
1018
1144
|
}, [items.length, currentPageSize]);
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1145
|
+
const sorting = useMemo(
|
|
1146
|
+
() => sorts.filter((s) => columns.some((c) => c.key === s.key)).map((s) => ({ id: s.key, desc: s.dir === "desc" })),
|
|
1147
|
+
[sorts, columns]
|
|
1148
|
+
);
|
|
1149
|
+
const tanstackColumns = useMemo(
|
|
1150
|
+
() => columns.map((col) => ({
|
|
1151
|
+
id: col.key,
|
|
1152
|
+
// Blanks read as undefined here, so `sortUndefined` keeps them last
|
|
1153
|
+
// whichever way the column sorts.
|
|
1154
|
+
accessorFn: (row) => {
|
|
1155
|
+
const v = col.accessor ? col.accessor(row) : row[col.key];
|
|
1156
|
+
return v == null || v === "" ? void 0 : v;
|
|
1157
|
+
},
|
|
1158
|
+
sortUndefined: "last",
|
|
1159
|
+
sortingFn: (a, b, id) => compareValues(a.getValue(id), b.getValue(id))
|
|
1160
|
+
})),
|
|
1161
|
+
[columns]
|
|
1162
|
+
);
|
|
1163
|
+
const table = useReactTable({
|
|
1164
|
+
data: items,
|
|
1165
|
+
columns: tanstackColumns,
|
|
1166
|
+
getRowId: (row) => getKey(row),
|
|
1167
|
+
state: {
|
|
1168
|
+
sorting,
|
|
1169
|
+
pagination: { pageIndex: currentPage, pageSize: currentPageSize }
|
|
1170
|
+
},
|
|
1171
|
+
enableMultiSort: true,
|
|
1172
|
+
getCoreRowModel: getCoreRowModel(),
|
|
1173
|
+
getSortedRowModel: getSortedRowModel(),
|
|
1174
|
+
...pagination ? { getPaginationRowModel: getPaginationRowModel() } : {}
|
|
1175
|
+
});
|
|
1176
|
+
const totalItems = table.getPrePaginationRowModel().rows.length;
|
|
1040
1177
|
const totalPages = pagination ? Math.max(1, Math.ceil(totalItems / currentPageSize)) : 1;
|
|
1041
|
-
const displayItems =
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1178
|
+
const displayItems = table.getRowModel().rows.map((row) => row.original);
|
|
1179
|
+
const scrollRef = useRef(null);
|
|
1180
|
+
const bodyRef = useRef(null);
|
|
1181
|
+
const virtualizing = !!virtualize && !pagination;
|
|
1182
|
+
const virtualOptions = typeof virtualize === "object" ? virtualize : {};
|
|
1183
|
+
const viewportHeight = virtualOptions.height ?? 600;
|
|
1184
|
+
const rowVirtualizer = useVirtualizer({
|
|
1185
|
+
count: virtualizing ? displayItems.length : 0,
|
|
1186
|
+
getScrollElement: () => scrollRef.current,
|
|
1187
|
+
estimateSize: () => virtualOptions.estimateRowHeight ?? ROW_HEIGHT[density],
|
|
1188
|
+
overscan: 10,
|
|
1189
|
+
initialRect: { width: 0, height: viewportHeight }
|
|
1190
|
+
});
|
|
1191
|
+
const virtualRows = virtualizing ? rowVirtualizer.getVirtualItems() : null;
|
|
1192
|
+
const focusRow = (index) => {
|
|
1193
|
+
const last = displayItems.length - 1;
|
|
1194
|
+
const target = Math.max(0, Math.min(last, index));
|
|
1195
|
+
if (focusRowAt(bodyRef.current, target)) return;
|
|
1196
|
+
if (!virtualizing) return;
|
|
1197
|
+
rowVirtualizer.scrollToIndex(target);
|
|
1198
|
+
requestAnimationFrame(() => focusRowAt(bodyRef.current, target));
|
|
1199
|
+
};
|
|
1200
|
+
const openKey = !rowKeys ? null : openColumn !== void 0 ? openColumn : activeColumns.find((c) => !c.editable)?.key ?? 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;
|
|
1206
|
+
const rowLabel = (item, id) => {
|
|
1207
|
+
const col = activeColumns.find((c) => c.key === (openKey ?? activeColumns[0]?.key));
|
|
1208
|
+
const value = col ? (col.accessor ?? ((it) => it[col.key]))(item) : void 0;
|
|
1209
|
+
return typeof value === "string" || typeof value === "number" ? String(value) : id;
|
|
1210
|
+
};
|
|
1045
1211
|
const showingFrom = totalItems === 0 ? 0 : currentPage * currentPageSize + 1;
|
|
1046
1212
|
const showingTo = Math.min(
|
|
1047
1213
|
(currentPage + 1) * currentPageSize,
|
|
1048
1214
|
totalItems
|
|
1049
1215
|
);
|
|
1050
1216
|
const totalCols = activeColumns.length + (selectable ? 1 : 0) + (renderActions ? 1 : 0);
|
|
1217
|
+
const renderRow = (item, index, measure) => {
|
|
1218
|
+
const id = getKey(item);
|
|
1219
|
+
return /* @__PURE__ */ jsxs(
|
|
1220
|
+
TableRow,
|
|
1221
|
+
{
|
|
1222
|
+
ref: measure,
|
|
1223
|
+
"data-index": index,
|
|
1224
|
+
"aria-rowindex": virtualizing ? index + 2 : void 0,
|
|
1225
|
+
className: cn(
|
|
1226
|
+
onRowClick && "cursor-pointer",
|
|
1227
|
+
"focus-within:bg-muted/40"
|
|
1228
|
+
),
|
|
1229
|
+
"data-state": selected?.has(id) ? "selected" : void 0,
|
|
1230
|
+
onClick: onRowClick ? () => onRowClick(item) : void 0,
|
|
1231
|
+
children: [
|
|
1232
|
+
selectable && /* @__PURE__ */ jsx(
|
|
1233
|
+
TableCell,
|
|
1234
|
+
{
|
|
1235
|
+
className: cn(cellDensity, pinProps("__select", false)?.className),
|
|
1236
|
+
style: pinProps("__select", false)?.style,
|
|
1237
|
+
onClick: (e) => e.stopPropagation(),
|
|
1238
|
+
children: /* @__PURE__ */ jsx(
|
|
1239
|
+
Checkbox,
|
|
1240
|
+
{
|
|
1241
|
+
"aria-label": `Select ${rowLabel(item, id)}`,
|
|
1242
|
+
checked: selected?.has(id),
|
|
1243
|
+
onCheckedChange: () => onToggleSelect?.(id)
|
|
1244
|
+
}
|
|
1245
|
+
)
|
|
1246
|
+
}
|
|
1247
|
+
),
|
|
1248
|
+
activeColumns.map((col, colIndex) => {
|
|
1249
|
+
const acc = col.accessor || ((it) => it[col.key]);
|
|
1250
|
+
const value = acc(item);
|
|
1251
|
+
const pin = pinProps(col.key, false);
|
|
1252
|
+
return /* @__PURE__ */ jsx(
|
|
1253
|
+
TableCell,
|
|
1254
|
+
{
|
|
1255
|
+
className: cn(
|
|
1256
|
+
getCellClasses(
|
|
1257
|
+
col
|
|
1258
|
+
),
|
|
1259
|
+
cellDensity,
|
|
1260
|
+
pin?.className
|
|
1261
|
+
),
|
|
1262
|
+
style: pin?.style,
|
|
1263
|
+
children: (() => {
|
|
1264
|
+
const content = col.editable ? /* @__PURE__ */ jsx(
|
|
1265
|
+
EditableCell,
|
|
1266
|
+
{
|
|
1267
|
+
item,
|
|
1268
|
+
column: col,
|
|
1269
|
+
value
|
|
1270
|
+
}
|
|
1271
|
+
) : col.render ? col.render(item, value) : /* @__PURE__ */ jsx("span", { className: "truncate block max-w-[300px]", children: String(value ?? "\u2014") });
|
|
1272
|
+
if (col.key !== openKey) return content;
|
|
1273
|
+
const openProps = {
|
|
1274
|
+
type: "button",
|
|
1275
|
+
[ROW_OPEN]: String(index),
|
|
1276
|
+
tabIndex: id === tabStopKey ? 0 : -1,
|
|
1277
|
+
onFocus: () => setActiveRowKey(id),
|
|
1278
|
+
onClick: (e) => {
|
|
1279
|
+
e.stopPropagation();
|
|
1280
|
+
onRowClick?.(item);
|
|
1281
|
+
},
|
|
1282
|
+
onKeyDown: (e) => {
|
|
1283
|
+
if (e.target !== e.currentTarget) return;
|
|
1284
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
1285
|
+
const move = (to) => {
|
|
1286
|
+
e.preventDefault();
|
|
1287
|
+
focusRow(to);
|
|
1288
|
+
};
|
|
1289
|
+
if (e.key === "ArrowDown" || e.key === "j") move(index + 1);
|
|
1290
|
+
else if (e.key === "ArrowUp" || e.key === "k") move(index - 1);
|
|
1291
|
+
else if (e.key === "Home") move(0);
|
|
1292
|
+
else if (e.key === "End") move(displayItems.length - 1);
|
|
1293
|
+
else if (e.key === "x" && selectable) {
|
|
1294
|
+
e.preventDefault();
|
|
1295
|
+
onToggleSelect?.(id);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
if (!col.editable && !col.render) {
|
|
1300
|
+
return /* @__PURE__ */ jsx(
|
|
1301
|
+
"button",
|
|
1302
|
+
{
|
|
1303
|
+
...openProps,
|
|
1304
|
+
className: "-mx-1 block w-[calc(100%+0.5rem)] min-w-0 rounded px-1 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
|
|
1305
|
+
children: content
|
|
1306
|
+
}
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1", children: [
|
|
1310
|
+
/* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: content }),
|
|
1311
|
+
/* @__PURE__ */ jsx(
|
|
1312
|
+
"button",
|
|
1313
|
+
{
|
|
1314
|
+
...openProps,
|
|
1315
|
+
"aria-label": `Open ${rowLabel(item, id)}`,
|
|
1316
|
+
className: "sr-only shrink-0 rounded px-1 text-xs text-muted-foreground focus-visible:not-sr-only focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
|
|
1317
|
+
children: "Open"
|
|
1318
|
+
}
|
|
1319
|
+
)
|
|
1320
|
+
] });
|
|
1321
|
+
})()
|
|
1322
|
+
},
|
|
1323
|
+
col.key
|
|
1324
|
+
);
|
|
1325
|
+
}),
|
|
1326
|
+
renderActions && /* @__PURE__ */ jsx(
|
|
1327
|
+
TableCell,
|
|
1328
|
+
{
|
|
1329
|
+
className: cn(cellDensity, pinProps("__actions", false)?.className),
|
|
1330
|
+
style: pinProps("__actions", false)?.style,
|
|
1331
|
+
onClick: (e) => e.stopPropagation(),
|
|
1332
|
+
children: renderActions(item)
|
|
1333
|
+
}
|
|
1334
|
+
)
|
|
1335
|
+
]
|
|
1336
|
+
},
|
|
1337
|
+
id
|
|
1338
|
+
);
|
|
1339
|
+
};
|
|
1051
1340
|
return /* @__PURE__ */ jsxs("div", { className: "border rounded-lg animate-fade-in flex flex-col", children: [
|
|
1052
1341
|
columnVisibility && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end px-3 py-2 border-b bg-muted/30", children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
1053
1342
|
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
@@ -1085,151 +1374,181 @@ function DataTable({
|
|
|
1085
1374
|
}
|
|
1086
1375
|
)
|
|
1087
1376
|
] }) }),
|
|
1088
|
-
/* @__PURE__ */ jsx(
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
className: colHideClass(col.hideBelow),
|
|
1097
|
-
style: w ? { width: w } : void 0
|
|
1098
|
-
},
|
|
1099
|
-
col.key
|
|
1100
|
-
);
|
|
1101
|
-
}),
|
|
1102
|
-
renderActions && /* @__PURE__ */ jsx("col", { style: { width: 40 } })
|
|
1103
|
-
] }),
|
|
1104
|
-
/* @__PURE__ */ jsx(
|
|
1105
|
-
TableHeader,
|
|
1106
|
-
{
|
|
1107
|
-
className: stickyHeader ? "sticky top-0 z-10 bg-background" : "",
|
|
1108
|
-
children: /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
1109
|
-
selectable && /* @__PURE__ */ jsx(TableHead, { className: cn("w-10", headDensity), children: /* @__PURE__ */ jsx(
|
|
1110
|
-
Checkbox,
|
|
1111
|
-
{
|
|
1112
|
-
checked: items.length > 0 && selected?.size === items.length,
|
|
1113
|
-
onCheckedChange: onToggleAll
|
|
1114
|
-
}
|
|
1115
|
-
) }),
|
|
1116
|
-
activeColumns.map((col, colIndex) => {
|
|
1117
|
-
const sortIndex = sorts.findIndex((s) => s.key === col.key);
|
|
1118
|
-
const sortEntry = sortIndex >= 0 ? sorts[sortIndex] : null;
|
|
1119
|
-
const resizeWidth = colWidths[col.key];
|
|
1120
|
-
const isFirstCol = colIndex === 0 && stickyFirstColumn;
|
|
1121
|
-
return /* @__PURE__ */ jsxs(
|
|
1122
|
-
TableHead,
|
|
1123
|
-
{
|
|
1124
|
-
className: cn(
|
|
1125
|
-
getCellClasses(col),
|
|
1126
|
-
headDensity,
|
|
1127
|
-
col.sortable && "cursor-pointer select-none",
|
|
1128
|
-
"overflow-hidden",
|
|
1129
|
-
resizableColumns && "relative",
|
|
1130
|
-
isFirstCol && "sticky left-0 bg-background",
|
|
1131
|
-
isFirstCol && stickyHeader ? "z-[15]" : isFirstCol ? "z-[5]" : void 0
|
|
1132
|
-
),
|
|
1133
|
-
style: resizeWidth ? { width: resizeWidth, minWidth: resizeWidth } : void 0,
|
|
1134
|
-
onClick: col.sortable ? (e) => handleSort(col.key, e.shiftKey) : void 0,
|
|
1135
|
-
children: [
|
|
1136
|
-
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-1", children: [
|
|
1137
|
-
/* @__PURE__ */ jsx("span", { className: "truncate", title: col.header, children: col.header }),
|
|
1138
|
-
col.sortable && (sortEntry ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1139
|
-
sortEntry.dir === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { className: "h-3 w-3 shrink-0 text-foreground" }) : /* @__PURE__ */ jsx(ArrowDown, { className: "h-3 w-3 shrink-0 text-foreground" }),
|
|
1140
|
-
sorts.length > 1 && /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: sortIndex + 1 })
|
|
1141
|
-
] }) : /* @__PURE__ */ jsx(ArrowUpDown, { className: "h-3 w-3 shrink-0 text-muted-foreground/50" }))
|
|
1142
|
-
] }),
|
|
1143
|
-
resizableColumns && /* @__PURE__ */ jsx(
|
|
1144
|
-
ResizeHandle,
|
|
1145
|
-
{
|
|
1146
|
-
onResize: (delta) => handleResize(
|
|
1147
|
-
col.key,
|
|
1148
|
-
delta,
|
|
1149
|
-
col.minWidth ?? 60
|
|
1150
|
-
),
|
|
1151
|
-
onResizeEnd: handleResizeEnd
|
|
1152
|
-
}
|
|
1153
|
-
)
|
|
1154
|
-
]
|
|
1155
|
-
},
|
|
1156
|
-
col.key
|
|
1157
|
-
);
|
|
1158
|
-
}),
|
|
1159
|
-
renderActions && /* @__PURE__ */ jsx(TableHead, { className: cn("w-10", headDensity) })
|
|
1160
|
-
] })
|
|
1161
|
-
}
|
|
1162
|
-
),
|
|
1163
|
-
/* @__PURE__ */ jsx(TableBody, { children: displayItems.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
|
|
1164
|
-
TableCell,
|
|
1165
|
-
{
|
|
1166
|
-
colSpan: totalCols,
|
|
1167
|
-
className: "text-center py-12 text-muted-foreground",
|
|
1168
|
-
children: emptyMessage
|
|
1169
|
-
}
|
|
1170
|
-
) }) : displayItems.map((item) => {
|
|
1171
|
-
const id = getKey(item);
|
|
1172
|
-
return /* @__PURE__ */ jsxs(
|
|
1173
|
-
TableRow,
|
|
1377
|
+
/* @__PURE__ */ jsx(
|
|
1378
|
+
"div",
|
|
1379
|
+
{
|
|
1380
|
+
ref: scrollRef,
|
|
1381
|
+
className: cn("overflow-x-auto", virtualizing && "overflow-y-auto"),
|
|
1382
|
+
style: virtualizing ? { maxHeight: viewportHeight } : void 0,
|
|
1383
|
+
children: /* @__PURE__ */ jsxs(
|
|
1384
|
+
Table,
|
|
1174
1385
|
{
|
|
1175
|
-
|
|
1176
|
-
"
|
|
1177
|
-
onClick: onRowClick ? () => onRowClick(item) : void 0,
|
|
1386
|
+
style: { tableLayout: "fixed" },
|
|
1387
|
+
"aria-rowcount": virtualizing ? displayItems.length + 1 : void 0,
|
|
1178
1388
|
children: [
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
{
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
Checkbox,
|
|
1389
|
+
/* @__PURE__ */ jsxs("colgroup", { children: [
|
|
1390
|
+
selectable && /* @__PURE__ */ jsx("col", { style: { width: 40 } }),
|
|
1391
|
+
activeColumns.map((col) => {
|
|
1392
|
+
const w = colWidths[col.key] ?? (pinned[col.key] ? PINNED_WIDTH : void 0);
|
|
1393
|
+
return /* @__PURE__ */ jsx(
|
|
1394
|
+
"col",
|
|
1186
1395
|
{
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
{
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1396
|
+
className: colHideClass(col.hideBelow),
|
|
1397
|
+
style: w ? { width: w } : void 0
|
|
1398
|
+
},
|
|
1399
|
+
col.key
|
|
1400
|
+
);
|
|
1401
|
+
}),
|
|
1402
|
+
renderActions && /* @__PURE__ */ jsx("col", { style: { width: 40 } })
|
|
1403
|
+
] }),
|
|
1404
|
+
/* @__PURE__ */ jsx(
|
|
1405
|
+
TableHeader,
|
|
1406
|
+
{
|
|
1407
|
+
className: stickyHeader ? "sticky top-0 z-10 bg-background" : "",
|
|
1408
|
+
children: /* @__PURE__ */ jsxs(TableRow, { "aria-rowindex": virtualizing ? 1 : void 0, children: [
|
|
1409
|
+
selectable && /* @__PURE__ */ jsx(
|
|
1410
|
+
TableHead,
|
|
1411
|
+
{
|
|
1412
|
+
className: cn("w-10", headDensity, pinProps("__select", true)?.className),
|
|
1413
|
+
style: pinProps("__select", true)?.style,
|
|
1414
|
+
children: /* @__PURE__ */ jsx(
|
|
1415
|
+
Checkbox,
|
|
1416
|
+
{
|
|
1417
|
+
"aria-label": "Select all rows",
|
|
1418
|
+
checked: items.length > 0 && selected?.size === items.length,
|
|
1419
|
+
onCheckedChange: onToggleAll
|
|
1420
|
+
}
|
|
1421
|
+
)
|
|
1422
|
+
}
|
|
1206
1423
|
),
|
|
1207
|
-
|
|
1208
|
-
|
|
1424
|
+
activeColumns.map((col, colIndex) => {
|
|
1425
|
+
const sortIndex = sorts.findIndex((s) => s.key === col.key);
|
|
1426
|
+
const sortEntry = sortIndex >= 0 ? sorts[sortIndex] : null;
|
|
1427
|
+
const resizeWidth = colWidths[col.key];
|
|
1428
|
+
const pin = pinProps(col.key, true);
|
|
1429
|
+
const draggable = reorderableColumns && !pin;
|
|
1430
|
+
return /* @__PURE__ */ jsxs(
|
|
1431
|
+
TableHead,
|
|
1432
|
+
{
|
|
1433
|
+
className: cn(
|
|
1434
|
+
getCellClasses(col),
|
|
1435
|
+
headDensity,
|
|
1436
|
+
col.sortable && "select-none",
|
|
1437
|
+
"overflow-hidden",
|
|
1438
|
+
resizableColumns && "relative",
|
|
1439
|
+
pin?.className
|
|
1440
|
+
),
|
|
1441
|
+
style: {
|
|
1442
|
+
...resizeWidth ? { width: resizeWidth, minWidth: resizeWidth } : {},
|
|
1443
|
+
...pin?.style
|
|
1444
|
+
},
|
|
1445
|
+
draggable: draggable || void 0,
|
|
1446
|
+
onDragStart: draggable ? (e) => {
|
|
1447
|
+
setDragKey(col.key);
|
|
1448
|
+
e.dataTransfer.effectAllowed = "move";
|
|
1449
|
+
e.dataTransfer.setData("text/plain", col.key);
|
|
1450
|
+
} : void 0,
|
|
1451
|
+
onDragOver: draggable ? (e) => {
|
|
1452
|
+
if (dragKey && dragKey !== col.key) e.preventDefault();
|
|
1453
|
+
} : void 0,
|
|
1454
|
+
onDrop: draggable ? (e) => {
|
|
1455
|
+
e.preventDefault();
|
|
1456
|
+
if (dragKey && dragKey !== col.key) moveColumn(dragKey, { before: col.key });
|
|
1457
|
+
setDragKey(null);
|
|
1458
|
+
} : void 0,
|
|
1459
|
+
onDragEnd: draggable ? () => setDragKey(null) : void 0,
|
|
1460
|
+
"aria-sort": sortEntry && sortIndex === 0 ? sortEntry.dir === "asc" ? "ascending" : "descending" : void 0,
|
|
1461
|
+
children: [
|
|
1462
|
+
(() => {
|
|
1463
|
+
const label = /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-1", children: [
|
|
1464
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", title: col.header, children: col.header }),
|
|
1465
|
+
col.sortable && (sortEntry ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1466
|
+
sortEntry.dir === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { className: "h-3 w-3 shrink-0 text-foreground" }) : /* @__PURE__ */ jsx(ArrowDown, { className: "h-3 w-3 shrink-0 text-foreground" }),
|
|
1467
|
+
sorts.length > 1 && /* @__PURE__ */ jsx(
|
|
1468
|
+
"span",
|
|
1469
|
+
{
|
|
1470
|
+
"aria-hidden": "true",
|
|
1471
|
+
className: "text-[10px] text-muted-foreground font-mono",
|
|
1472
|
+
children: sortIndex + 1
|
|
1473
|
+
}
|
|
1474
|
+
)
|
|
1475
|
+
] }) : /* @__PURE__ */ jsx(ArrowUpDown, { className: "h-3 w-3 shrink-0 text-muted-foreground/50" }))
|
|
1476
|
+
] });
|
|
1477
|
+
return col.sortable ? /* @__PURE__ */ jsx(
|
|
1478
|
+
"button",
|
|
1479
|
+
{
|
|
1480
|
+
type: "button",
|
|
1481
|
+
className: "-mx-1 flex w-[calc(100%+0.5rem)] min-w-0 items-center rounded px-1 text-left cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
1482
|
+
onClick: (e) => handleSort(col.key, e.shiftKey),
|
|
1483
|
+
onKeyDown: (e) => {
|
|
1484
|
+
if (!draggable || !e.altKey) return;
|
|
1485
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
1486
|
+
const movable = activeColumns.filter((c) => !pinned[c.key]);
|
|
1487
|
+
const i = movable.findIndex((c) => c.key === col.key);
|
|
1488
|
+
const neighbour = movable[e.key === "ArrowLeft" ? i - 1 : i + 1];
|
|
1489
|
+
if (!neighbour) return;
|
|
1490
|
+
e.preventDefault();
|
|
1491
|
+
moveColumn(col.key, e.key === "ArrowLeft" ? { before: neighbour.key } : { after: neighbour.key });
|
|
1492
|
+
},
|
|
1493
|
+
"aria-label": sortEntry && sortIndex > 0 ? `${col.header}, sorted ${sortEntry.dir === "asc" ? "ascending" : "descending"}, sort ${sortIndex + 1}` : void 0,
|
|
1494
|
+
children: label
|
|
1495
|
+
}
|
|
1496
|
+
) : label;
|
|
1497
|
+
})(),
|
|
1498
|
+
resizableColumns && /* @__PURE__ */ jsx(
|
|
1499
|
+
ResizeHandle,
|
|
1500
|
+
{
|
|
1501
|
+
onResize: (delta) => handleResize(
|
|
1502
|
+
col.key,
|
|
1503
|
+
delta,
|
|
1504
|
+
col.minWidth ?? 60
|
|
1505
|
+
),
|
|
1506
|
+
onResizeEnd: handleResizeEnd
|
|
1507
|
+
}
|
|
1508
|
+
)
|
|
1509
|
+
]
|
|
1510
|
+
},
|
|
1511
|
+
col.key
|
|
1512
|
+
);
|
|
1513
|
+
}),
|
|
1514
|
+
renderActions && /* @__PURE__ */ jsx(
|
|
1515
|
+
TableHead,
|
|
1209
1516
|
{
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
value
|
|
1517
|
+
className: cn("w-10", headDensity, pinProps("__actions", true)?.className),
|
|
1518
|
+
style: pinProps("__actions", true)?.style
|
|
1213
1519
|
}
|
|
1214
|
-
)
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
renderActions && /* @__PURE__ */ jsx(
|
|
1520
|
+
)
|
|
1521
|
+
] })
|
|
1522
|
+
}
|
|
1523
|
+
),
|
|
1524
|
+
/* @__PURE__ */ jsx(TableBody, { ref: bodyRef, children: displayItems.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
|
|
1220
1525
|
TableCell,
|
|
1221
1526
|
{
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
children:
|
|
1527
|
+
colSpan: totalCols,
|
|
1528
|
+
className: "text-center py-12 text-muted-foreground",
|
|
1529
|
+
children: emptyMessage
|
|
1225
1530
|
}
|
|
1226
|
-
)
|
|
1531
|
+
) }) : virtualRows ? [
|
|
1532
|
+
/* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: virtualRows[0]?.start ?? 0 } }, "__top"),
|
|
1533
|
+
...virtualRows.map(
|
|
1534
|
+
(vr) => renderRow(displayItems[vr.index], vr.index, rowVirtualizer.measureElement)
|
|
1535
|
+
),
|
|
1536
|
+
/* @__PURE__ */ jsx(
|
|
1537
|
+
"tr",
|
|
1538
|
+
{
|
|
1539
|
+
"aria-hidden": true,
|
|
1540
|
+
style: {
|
|
1541
|
+
height: rowVirtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0)
|
|
1542
|
+
}
|
|
1543
|
+
},
|
|
1544
|
+
"__bottom"
|
|
1545
|
+
)
|
|
1546
|
+
] : displayItems.map((item, index) => renderRow(item, index)) })
|
|
1227
1547
|
]
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
] }) }),
|
|
1548
|
+
}
|
|
1549
|
+
)
|
|
1550
|
+
}
|
|
1551
|
+
),
|
|
1233
1552
|
pagination && /* @__PURE__ */ jsxs("div", { className: "sticky bottom-0 z-10 flex flex-col sm:flex-row items-center justify-between gap-2 px-4 py-3 border-t bg-muted/30 text-xs text-muted-foreground", children: [
|
|
1234
1553
|
/* @__PURE__ */ jsx("span", { children: totalItems === 0 ? "No records" : `Showing ${showingFrom}\u2013${showingTo} of ${totalItems}` }),
|
|
1235
1554
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
@@ -1477,6 +1796,7 @@ var statusColorMap = {
|
|
|
1477
1796
|
primary: { color: "text-primary", bgColor: "bg-primary/15" }
|
|
1478
1797
|
};
|
|
1479
1798
|
var DRAG_TYPE = "application/x-genesis-kanban-item";
|
|
1799
|
+
var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
1480
1800
|
function KanbanBoard({
|
|
1481
1801
|
columns,
|
|
1482
1802
|
renderCard,
|
|
@@ -1489,112 +1809,301 @@ function KanbanBoard({
|
|
|
1489
1809
|
}) {
|
|
1490
1810
|
const [dragging, setDragging] = useState(null);
|
|
1491
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();
|
|
1492
1817
|
const keyOf = (item, index) => getKey ? getKey(item) : String(index);
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1495
|
-
|
|
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));
|
|
1496
1848
|
};
|
|
1497
1849
|
const onDrop = (event, to) => {
|
|
1498
1850
|
event.preventDefault();
|
|
1499
1851
|
setOver(null);
|
|
1500
1852
|
const key = event.dataTransfer.getData(DRAG_TYPE) || dragging?.key;
|
|
1501
1853
|
setDragging(null);
|
|
1502
|
-
if (
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
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);
|
|
1508
1936
|
}
|
|
1509
1937
|
}
|
|
1510
1938
|
};
|
|
1511
|
-
const
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
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",
|
|
1574
2039
|
{
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
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 })
|
|
1578
2063
|
}
|
|
1579
2064
|
),
|
|
1580
|
-
/* @__PURE__ */ jsxs(
|
|
1581
|
-
/* @__PURE__ */ jsx(
|
|
1582
|
-
|
|
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
|
+
] })
|
|
1583
2088
|
] })
|
|
1584
|
-
]
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
)
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
)
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
|
|
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
|
+
] });
|
|
1598
2107
|
}
|
|
1599
2108
|
function DataList({
|
|
1600
2109
|
items,
|
|
@@ -2713,7 +3222,12 @@ function ResourceForm(props) {
|
|
|
2713
3222
|
steps,
|
|
2714
3223
|
columns = 1,
|
|
2715
3224
|
prefillFromUrl,
|
|
2716
|
-
autoPlaceholder = false
|
|
3225
|
+
autoPlaceholder = false,
|
|
3226
|
+
warnWhenUnsaved = false,
|
|
3227
|
+
onDirtyChange,
|
|
3228
|
+
mutationMode = "pessimistic",
|
|
3229
|
+
undoDelayMs,
|
|
3230
|
+
onUndoable
|
|
2717
3231
|
} = props;
|
|
2718
3232
|
const isMobile = useIsMobile();
|
|
2719
3233
|
const effectiveLayout = isMobile ? "sheet" : layoutProp;
|
|
@@ -2731,11 +3245,33 @@ function ResourceForm(props) {
|
|
|
2731
3245
|
onOpenChange(false);
|
|
2732
3246
|
onSuccess?.(data);
|
|
2733
3247
|
},
|
|
2734
|
-
|
|
3248
|
+
// The form is closed by the time an optimistic or undoable save fails,
|
|
3249
|
+
// so the error has to reach the user some other way.
|
|
3250
|
+
onError: onError ?? (mutationMode === "pessimistic" ? void 0 : showWriteErrorToast),
|
|
2735
3251
|
prefillFromUrl,
|
|
2736
|
-
autoPlaceholder
|
|
3252
|
+
autoPlaceholder,
|
|
3253
|
+
warnWhenUnsaved,
|
|
3254
|
+
// A closed dialog is not touched by a save that fails later.
|
|
3255
|
+
visible: open,
|
|
3256
|
+
mutationMode,
|
|
3257
|
+
undoDelayMs,
|
|
3258
|
+
onUndoable: onUndoable ?? showUndoToast
|
|
2737
3259
|
};
|
|
2738
|
-
const { form, resolvedFields, handleSave, isSaving, isLoading } = useResourceForm(hookOptions);
|
|
3260
|
+
const { form, resolvedFields, handleSave, isSaving, isLoading, isDirty, confirmLeave } = useResourceForm(hookOptions);
|
|
3261
|
+
const onDirtyChangeRef = useRef(onDirtyChange);
|
|
3262
|
+
useEffect(() => {
|
|
3263
|
+
onDirtyChangeRef.current = onDirtyChange;
|
|
3264
|
+
});
|
|
3265
|
+
useEffect(() => {
|
|
3266
|
+
onDirtyChangeRef.current?.(isDirty, confirmLeave);
|
|
3267
|
+
}, [isDirty, confirmLeave]);
|
|
3268
|
+
const requestOpenChange = useCallback(
|
|
3269
|
+
(next) => {
|
|
3270
|
+
if (!next && !confirmLeave()) return;
|
|
3271
|
+
onOpenChange(next);
|
|
3272
|
+
},
|
|
3273
|
+
[confirmLeave, onOpenChange]
|
|
3274
|
+
);
|
|
2739
3275
|
const bodyProps = {
|
|
2740
3276
|
form,
|
|
2741
3277
|
resolvedFields,
|
|
@@ -2743,14 +3279,14 @@ function ResourceForm(props) {
|
|
|
2743
3279
|
action,
|
|
2744
3280
|
isSaving,
|
|
2745
3281
|
isLoading,
|
|
2746
|
-
onOpenChange,
|
|
3282
|
+
onOpenChange: requestOpenChange,
|
|
2747
3283
|
handleSave,
|
|
2748
3284
|
mode,
|
|
2749
3285
|
steps,
|
|
2750
3286
|
columns
|
|
2751
3287
|
};
|
|
2752
3288
|
if (effectiveLayout === "dialog") {
|
|
2753
|
-
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-[560px]", children: [
|
|
3289
|
+
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: requestOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-[560px]", children: [
|
|
2754
3290
|
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
2755
3291
|
/* @__PURE__ */ jsx(DialogTitle, { children: formTitle }),
|
|
2756
3292
|
/* @__PURE__ */ jsx(DialogDescription, { className: "sr-only", children: formDescription })
|
|
@@ -2759,7 +3295,7 @@ function ResourceForm(props) {
|
|
|
2759
3295
|
] }) });
|
|
2760
3296
|
}
|
|
2761
3297
|
if (effectiveLayout === "sheet") {
|
|
2762
|
-
return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange, children: /* @__PURE__ */ jsxs(SheetContent, { side: "right", className: "sm:max-w-[480px] overflow-y-auto", children: [
|
|
3298
|
+
return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange: requestOpenChange, children: /* @__PURE__ */ jsxs(SheetContent, { side: "right", className: "sm:max-w-[480px] overflow-y-auto", children: [
|
|
2763
3299
|
/* @__PURE__ */ jsxs(SheetHeader, { children: [
|
|
2764
3300
|
/* @__PURE__ */ jsx(SheetTitle, { children: formTitle }),
|
|
2765
3301
|
/* @__PURE__ */ jsx(SheetDescription, { className: "sr-only", children: formDescription })
|
|
@@ -2778,7 +3314,8 @@ function ConfirmDeleteDialog({
|
|
|
2778
3314
|
onOpenChange,
|
|
2779
3315
|
onConfirm,
|
|
2780
3316
|
title = "Delete this record?",
|
|
2781
|
-
|
|
3317
|
+
kind = "permanent",
|
|
3318
|
+
description = kind === "recycle" ? "It moves to the recycle bin, where it can be restored." : "This can't be undone.",
|
|
2782
3319
|
confirmLabel = "Delete",
|
|
2783
3320
|
cancelLabel = "Cancel"
|
|
2784
3321
|
}) {
|
|
@@ -2847,499 +3384,131 @@ function ConfirmDeleteDialog({
|
|
|
2847
3384
|
}
|
|
2848
3385
|
function ResourcePage(props) {
|
|
2849
3386
|
const {
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
total: totalProp,
|
|
2853
|
-
pagination = "client",
|
|
2854
|
-
clientRowLimit = 1e3,
|
|
2855
|
-
title,
|
|
2856
|
-
subtitle,
|
|
2857
|
-
icon,
|
|
2858
|
-
columns: columnsProp,
|
|
2859
|
-
getKey: getKeyProp,
|
|
2860
|
-
detail = "panel",
|
|
2861
|
-
detailWidth = "md",
|
|
2862
|
-
titleField: titleFieldProp,
|
|
2863
|
-
subtitleField: subtitleFieldProp,
|
|
2864
|
-
getTitle,
|
|
2865
|
-
getSubtitle,
|
|
2866
|
-
renderDetail: renderDetailProp,
|
|
2867
|
-
detailFields,
|
|
2868
|
-
statusFilter,
|
|
2869
|
-
searchFields,
|
|
2870
|
-
searchPlaceholder,
|
|
2871
|
-
initialFilters,
|
|
2872
|
-
onFiltersChange,
|
|
2873
|
-
filters: filtersProp,
|
|
2874
|
-
page: pageProp,
|
|
2875
|
-
perPage: perPageProp,
|
|
2876
|
-
onPageChange,
|
|
2877
|
-
onPerPageChange,
|
|
2878
|
-
onSortChange,
|
|
2879
|
-
onSearchChange,
|
|
2880
|
-
loading: loadingProp,
|
|
2881
|
-
error: errorProp,
|
|
2882
|
-
errorMessage: errorMessage2,
|
|
2883
|
-
onRetry: onRetryProp,
|
|
2884
|
-
savedViews,
|
|
2885
|
-
onLoadView,
|
|
2886
|
-
onSaveView,
|
|
2887
|
-
onDeleteView,
|
|
3387
|
+
bulkActions,
|
|
3388
|
+
can,
|
|
2888
3389
|
canSaveViews,
|
|
3390
|
+
capabilities,
|
|
3391
|
+
className,
|
|
2889
3392
|
columnVisibility,
|
|
2890
3393
|
columnVisibilityKey,
|
|
2891
|
-
resizableColumns,
|
|
2892
|
-
resizeKey,
|
|
2893
|
-
stickyHeader,
|
|
2894
|
-
stickyFirstColumn,
|
|
2895
|
-
pageSizeOptions,
|
|
2896
|
-
onCreate,
|
|
2897
|
-
onEdit,
|
|
2898
|
-
onDelete,
|
|
2899
|
-
canEditItem,
|
|
2900
3394
|
createLabel = "New",
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
3395
|
+
detail = "panel",
|
|
3396
|
+
detailFields,
|
|
3397
|
+
detailWidth = "md",
|
|
3398
|
+
emptyDescription,
|
|
3399
|
+
emptyIcon,
|
|
3400
|
+
emptyMessage,
|
|
3401
|
+
emptyState,
|
|
3402
|
+
errorMessage: errorMessage2,
|
|
3403
|
+
filters: filtersProp,
|
|
2909
3404
|
formFields,
|
|
2910
|
-
|
|
3405
|
+
formLayout: formLayoutProp,
|
|
2911
3406
|
formMode,
|
|
2912
3407
|
formSteps,
|
|
2913
|
-
formLayout: formLayoutProp,
|
|
2914
|
-
bulkActions,
|
|
2915
|
-
renderCard: renderCardProp,
|
|
2916
|
-
openOnCardClick = false,
|
|
2917
|
-
renderTable: renderTableProp,
|
|
2918
3408
|
gridCols: gridColsProp,
|
|
2919
|
-
keyboardNavigation = true,
|
|
2920
|
-
capabilities,
|
|
2921
|
-
toolbar,
|
|
2922
|
-
emptyState,
|
|
2923
|
-
emptyIcon,
|
|
2924
|
-
emptyMessage,
|
|
2925
|
-
emptyDescription,
|
|
2926
3409
|
header: headerMode = "page",
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
3410
|
+
icon,
|
|
3411
|
+
keyboardNavigation = true,
|
|
3412
|
+
mutationMode: mutationModeProp,
|
|
3413
|
+
onCreate,
|
|
3414
|
+
onDelete,
|
|
3415
|
+
onDeleteView,
|
|
3416
|
+
onEdit,
|
|
3417
|
+
onLoadView,
|
|
3418
|
+
onPageChange,
|
|
3419
|
+
onRetry: onRetryProp,
|
|
3420
|
+
onSaveView,
|
|
3421
|
+
onSearchChange,
|
|
3422
|
+
onSortChange,
|
|
3423
|
+
openOnCardClick = false,
|
|
3424
|
+
pageSizeOptions,
|
|
3425
|
+
pagination = "client",
|
|
3426
|
+
renderCard: renderCardProp,
|
|
3427
|
+
renderDetail: renderDetailProp,
|
|
3428
|
+
renderForm: renderFormProp,
|
|
3429
|
+
renderTable: renderTableProp,
|
|
3430
|
+
resizableColumns,
|
|
3431
|
+
resizeKey,
|
|
3432
|
+
resource: resourceName,
|
|
3433
|
+
savedViews,
|
|
3434
|
+
searchPlaceholder,
|
|
3435
|
+
secondaryActions,
|
|
2932
3436
|
slotProps,
|
|
2933
|
-
|
|
3437
|
+
slots,
|
|
3438
|
+
stickyFirstColumn,
|
|
3439
|
+
stickyHeader,
|
|
3440
|
+
subtitle,
|
|
3441
|
+
title,
|
|
3442
|
+
toolbar
|
|
2934
3443
|
} = props;
|
|
2935
|
-
const isClientMode = !!resourceName;
|
|
2936
|
-
const resourceServerPaging = isClientMode && pagination === "server";
|
|
2937
|
-
const isServerControlled = resourceServerPaging || pageProp !== void 0 || perPageProp !== void 0 || onPageChange !== void 0 || onPerPageChange !== void 0 || onSortChange !== void 0 || onSearchChange !== void 0;
|
|
2938
|
-
const viewSettingsKey = viewSettingsKeyProp ?? resourceName ?? title.toLowerCase().replace(/\s+/g, "-");
|
|
2939
|
-
const { settings, updateSetting } = useViewSettings(viewSettingsKey);
|
|
2940
|
-
const serverSearchFields = resourceOptions?.searchFields ?? searchFields;
|
|
2941
|
-
const resourceHook = useResource(
|
|
2942
|
-
resourceName ?? "__noop__",
|
|
2943
|
-
isClientMode ? {
|
|
2944
|
-
...resourceOptions,
|
|
2945
|
-
// Client paging fetches everything it can in one request; server
|
|
2946
|
-
// paging starts at the user's stored page size. An explicit
|
|
2947
|
-
// `defaultPerPage` wins over both.
|
|
2948
|
-
defaultPerPage: resourceOptions?.defaultPerPage ?? (resourceServerPaging ? perPageProp ?? settings.pageSize : clientRowLimit),
|
|
2949
|
-
searchFields: resourceServerPaging ? serverSearchFields : resourceOptions?.searchFields,
|
|
2950
|
-
// Paging, searching and sorting change the query key; keep the
|
|
2951
|
-
// current page on screen while the next one loads.
|
|
2952
|
-
keepPreviousData: resourceOptions?.keepPreviousData ?? resourceServerPaging
|
|
2953
|
-
} : { enabled: false }
|
|
2954
|
-
);
|
|
2955
|
-
const effectivePageSize = resourceServerPaging ? resourceHook.params.pagination?.perPage ?? settings.pageSize : perPageProp ?? settings.pageSize;
|
|
2956
|
-
const displayData = isClientMode ? resourceHook.list.data?.data ?? [] : dataProp ?? [];
|
|
2957
|
-
const displayTotal = isClientMode ? resourceHook.list.data?.total ?? 0 : totalProp ?? 0;
|
|
2958
|
-
const isLoading = isClientMode ? resourceHook.list.isLoading : loadingProp ?? false;
|
|
2959
|
-
const hasError = isClientMode ? resourceHook.list.isError : errorProp ?? false;
|
|
2960
|
-
const page = useResourcePage({
|
|
2961
|
-
resource: resourceName,
|
|
2962
|
-
data: displayData,
|
|
2963
|
-
total: displayTotal,
|
|
2964
|
-
columns: columnsProp,
|
|
2965
|
-
statusFilter,
|
|
2966
|
-
searchFields,
|
|
2967
|
-
initialFilters,
|
|
2968
|
-
onFiltersChange,
|
|
2969
|
-
defaultView,
|
|
2970
|
-
detail,
|
|
2971
|
-
titleField: titleFieldProp,
|
|
2972
|
-
subtitleField: subtitleFieldProp,
|
|
2973
|
-
controlled: isServerControlled
|
|
2974
|
-
});
|
|
2975
3444
|
const {
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
const statusField = statusFilter ? typeof statusFilter === "string" ? statusFilter : statusFilter.field : void 0;
|
|
3008
|
-
const defaultFilters = resourceOptions?.defaultFilters;
|
|
3009
|
-
const pushedStatusRef = useRef("");
|
|
3010
|
-
useEffect(() => {
|
|
3011
|
-
if (!resourceServerPaging || !statusField) return;
|
|
3012
|
-
const key = JSON.stringify([page.statusValue, defaultFilters ?? null]);
|
|
3013
|
-
if (key === pushedStatusRef.current) return;
|
|
3014
|
-
pushedStatusRef.current = key;
|
|
3015
|
-
setResourceFilters([
|
|
3016
|
-
...defaultFilters ?? [],
|
|
3017
|
-
...page.statusValue.length > 0 ? [{ field: statusField, operator: "in", value: page.statusValue }] : []
|
|
3018
|
-
]);
|
|
3019
|
-
}, [
|
|
3020
|
-
resourceServerPaging,
|
|
3021
|
-
statusField,
|
|
3022
|
-
page.statusValue,
|
|
3023
|
-
defaultFilters,
|
|
3024
|
-
setResourceFilters
|
|
3025
|
-
]);
|
|
3026
|
-
const clientTruncated = isClientMode && !resourceServerPaging && !isLoading && !hasError && displayTotal > displayData.length;
|
|
3027
|
-
const warnedTruncatedRef = useRef(false);
|
|
3028
|
-
useEffect(() => {
|
|
3029
|
-
if (!clientTruncated || warnedTruncatedRef.current) return;
|
|
3030
|
-
warnedTruncatedRef.current = true;
|
|
3031
|
-
console.warn(
|
|
3032
|
-
`ResourcePage: "${resourceName}" has ${displayTotal} rows but only ${displayData.length} were fetched. Pass \`pagination="server"\` to page through all of them, or raise \`clientRowLimit\`.`
|
|
3033
|
-
);
|
|
3034
|
-
}, [clientTruncated, resourceName, displayTotal, displayData.length]);
|
|
3035
|
-
const actions = useMemo(() => {
|
|
3036
|
-
if (isClientMode) {
|
|
3037
|
-
return {
|
|
3038
|
-
update: resourceHook.update,
|
|
3039
|
-
remove: resourceHook.remove,
|
|
3040
|
-
refetch: () => resourceHook.list.refetch()
|
|
3041
|
-
};
|
|
3042
|
-
}
|
|
3043
|
-
return {
|
|
3044
|
-
update: async () => {
|
|
3045
|
-
throw new Error(
|
|
3046
|
-
"ResourceActions.update not available in server-data mode"
|
|
3047
|
-
);
|
|
3048
|
-
},
|
|
3049
|
-
remove: async () => {
|
|
3050
|
-
throw new Error(
|
|
3051
|
-
"ResourceActions.remove not available in server-data mode"
|
|
3052
|
-
);
|
|
3053
|
-
},
|
|
3054
|
-
refetch: () => {
|
|
3055
|
-
}
|
|
3056
|
-
};
|
|
3057
|
-
}, [
|
|
3058
|
-
isClientMode,
|
|
3059
|
-
resourceHook.update,
|
|
3060
|
-
resourceHook.remove,
|
|
3061
|
-
resourceHook.list
|
|
3062
|
-
]);
|
|
3063
|
-
const keyOf = useCallback(
|
|
3064
|
-
(item) => getKeyProp ? getKeyProp(item) : String(item.id ?? ""),
|
|
3065
|
-
[getKeyProp]
|
|
3066
|
-
);
|
|
3067
|
-
const selectedKey = page.selectedItem ? keyOf(page.selectedItem) : null;
|
|
3068
|
-
const selectedItem = !page.selectedItem ? null : selectedKey ? displayData.find((row) => keyOf(row) === selectedKey) ?? page.selectedItem : page.selectedItem;
|
|
3069
|
-
const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
|
|
3070
|
-
const handleToggleSelect = useCallback((id) => {
|
|
3071
|
-
setSelectedIds((prev) => {
|
|
3072
|
-
const next = new Set(prev);
|
|
3073
|
-
if (next.has(id)) next.delete(id);
|
|
3074
|
-
else next.add(id);
|
|
3075
|
-
return next;
|
|
3076
|
-
});
|
|
3077
|
-
}, []);
|
|
3078
|
-
const handleToggleAll = useCallback(() => {
|
|
3079
|
-
setSelectedIds((prev) => {
|
|
3080
|
-
if (prev.size === page.data.length) return /* @__PURE__ */ new Set();
|
|
3081
|
-
return new Set(page.data.map(keyOf));
|
|
3082
|
-
});
|
|
3083
|
-
}, [page.data, keyOf]);
|
|
3084
|
-
const handleClearSelection = useCallback(() => {
|
|
3085
|
-
setSelectedIds(/* @__PURE__ */ new Set());
|
|
3086
|
-
}, []);
|
|
3087
|
-
const [formOpen, setFormOpen] = useState(false);
|
|
3088
|
-
const [formAction, setFormAction] = useState("create");
|
|
3089
|
-
const [formItem, setFormItem] = useState(null);
|
|
3090
|
-
const useBuiltInCreate = allowCreate && !onCreate && isClientMode;
|
|
3091
|
-
const useBuiltInEdit = allowEdit && !onEdit && isClientMode;
|
|
3092
|
-
const handleCreate = useCallback(() => {
|
|
3093
|
-
if (useBuiltInCreate) {
|
|
3094
|
-
setFormAction("create");
|
|
3095
|
-
setFormItem(null);
|
|
3096
|
-
setFormOpen(true);
|
|
3097
|
-
} else if (onCreate) {
|
|
3098
|
-
onCreate();
|
|
3099
|
-
}
|
|
3100
|
-
}, [useBuiltInCreate, onCreate]);
|
|
3101
|
-
const handleEdit = useCallback(
|
|
3102
|
-
(item) => {
|
|
3103
|
-
if (canEdit && !canEdit(item)) return;
|
|
3104
|
-
if (canEditItem && !canEditItem(item)) return;
|
|
3105
|
-
if (useBuiltInEdit) {
|
|
3106
|
-
setFormAction("edit");
|
|
3107
|
-
setFormItem(item);
|
|
3108
|
-
setFormOpen(true);
|
|
3109
|
-
} else if (onEdit) {
|
|
3110
|
-
onEdit(item);
|
|
3111
|
-
}
|
|
3112
|
-
},
|
|
3113
|
-
[useBuiltInEdit, onEdit, canEdit, canEditItem]
|
|
3114
|
-
);
|
|
3115
|
-
const closeDetail = useCallback(() => {
|
|
3116
|
-
page.setDetailOpen(false);
|
|
3117
|
-
page.setSelectedItem(null);
|
|
3118
|
-
}, [page]);
|
|
3119
|
-
const useBuiltInDelete = !!allowDelete && !onDelete && isClientMode;
|
|
3120
|
-
const deleteConfirmed = confirmDelete === void 0 ? useBuiltInDelete : !!confirmDelete;
|
|
3121
|
-
const deleteCopy = typeof confirmDelete === "object" ? confirmDelete : void 0;
|
|
3122
|
-
const [pendingDelete, setPendingDelete] = useState(null);
|
|
3123
|
-
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
3124
|
-
const { remove: removeRecord } = resourceHook;
|
|
3125
|
-
const performDelete = useCallback(
|
|
3126
|
-
async (item) => {
|
|
3127
|
-
if (useBuiltInDelete) {
|
|
3128
|
-
await removeRecord(
|
|
3129
|
-
getKeyProp ? keyOf(item) : item.id
|
|
3130
|
-
);
|
|
3131
|
-
} else if (onDelete) {
|
|
3132
|
-
await onDelete(item);
|
|
3133
|
-
}
|
|
3134
|
-
closeDetail();
|
|
3135
|
-
onDeleted?.(item);
|
|
3136
|
-
},
|
|
3137
|
-
[
|
|
3138
|
-
useBuiltInDelete,
|
|
3139
|
-
removeRecord,
|
|
3140
|
-
getKeyProp,
|
|
3141
|
-
keyOf,
|
|
3142
|
-
onDelete,
|
|
3143
|
-
closeDetail,
|
|
3144
|
-
onDeleted
|
|
3145
|
-
]
|
|
3146
|
-
);
|
|
3147
|
-
const deleteHandler = !(useBuiltInDelete || onDelete) ? void 0 : deleteConfirmed ? (item) => {
|
|
3148
|
-
setPendingDelete(item);
|
|
3149
|
-
setDeleteOpen(true);
|
|
3150
|
-
} : useBuiltInDelete ? (item) => {
|
|
3151
|
-
performDelete(item).catch((err) => {
|
|
3152
|
-
console.error("ResourcePage: delete failed", err);
|
|
3153
|
-
});
|
|
3154
|
-
} : (
|
|
3155
|
-
// Unconfirmed `onDelete` keeps its original contract exactly:
|
|
3156
|
-
// called straight from the peek, nothing closed for it.
|
|
3157
|
-
(item) => {
|
|
3158
|
-
void onDelete?.(item);
|
|
3159
|
-
}
|
|
3160
|
-
);
|
|
3161
|
-
const mayChange = (item) => canEditItem ? canEditItem(item) : true;
|
|
3162
|
-
const selectedEditable = !!selectedItem && mayChange(selectedItem) && (canEdit ? canEdit(selectedItem) : true);
|
|
3163
|
-
const selectedDeletable = !!selectedItem && mayChange(selectedItem) && (canDelete ? canDelete(selectedItem) : true);
|
|
3164
|
-
const tableColumns = useMemo(() => {
|
|
3165
|
-
const base = isClientMode ? page.columns.map((col) => ({
|
|
3166
|
-
key: col.key,
|
|
3167
|
-
header: col.header ?? col.key,
|
|
3168
|
-
sortable: col.sortable ?? true,
|
|
3169
|
-
hideBelow: col.hideBelow,
|
|
3170
|
-
editable: col.editable,
|
|
3171
|
-
accessor: col.accessor,
|
|
3172
|
-
render: col.render ? (item, value) => col.render(item, value, actions) : void 0
|
|
3173
|
-
})) : page.tableColumns;
|
|
3174
|
-
if (!isServerControlled) return base;
|
|
3175
|
-
return base.map((col) => ({ ...col, sortable: false }));
|
|
3176
|
-
}, [
|
|
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,
|
|
3177
3476
|
isClientMode,
|
|
3477
|
+
isLoading,
|
|
3178
3478
|
isServerControlled,
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
const pagerPerPage = resourceServerPaging ? resourceHook.params.pagination?.perPage : perPageProp;
|
|
3185
|
-
const pagerTotal = resourceServerPaging ? displayTotal : totalProp;
|
|
3186
|
-
const pagerOnPageChange = resourceServerPaging ? resourceHook.setPage : onPageChange;
|
|
3187
|
-
const currentPage = pagerPage ?? 1;
|
|
3188
|
-
const handleSearchChange = useCallback(
|
|
3189
|
-
(value) => {
|
|
3190
|
-
page.filterConfig.onSearchChange(value);
|
|
3191
|
-
if (!isServerControlled) return;
|
|
3192
|
-
onSearchChange?.(value);
|
|
3193
|
-
if (currentPage !== 1 && !resourceServerPaging) onPageChange?.(1);
|
|
3194
|
-
},
|
|
3195
|
-
[
|
|
3196
|
-
page.filterConfig,
|
|
3197
|
-
isServerControlled,
|
|
3198
|
-
onSearchChange,
|
|
3199
|
-
onPageChange,
|
|
3200
|
-
resourceServerPaging,
|
|
3201
|
-
currentPage
|
|
3202
|
-
]
|
|
3203
|
-
);
|
|
3204
|
-
const handleSortSelect = useCallback(
|
|
3205
|
-
(value) => {
|
|
3206
|
-
page.filterConfig.onSortChange(value);
|
|
3207
|
-
if (!isServerControlled) return;
|
|
3208
|
-
const [field, order] = value.split(":");
|
|
3209
|
-
onSortChange?.(
|
|
3210
|
-
field && (order === "asc" || order === "desc") ? { field, order } : null
|
|
3211
|
-
);
|
|
3212
|
-
if (!resourceServerPaging) onPageChange?.(1);
|
|
3213
|
-
},
|
|
3214
|
-
[
|
|
3215
|
-
page.filterConfig,
|
|
3216
|
-
isServerControlled,
|
|
3217
|
-
onSortChange,
|
|
3218
|
-
onPageChange,
|
|
3219
|
-
resourceServerPaging
|
|
3220
|
-
]
|
|
3221
|
-
);
|
|
3222
|
-
const handleClearFilters = useCallback(() => {
|
|
3223
|
-
page.clearFilters();
|
|
3224
|
-
if (!isServerControlled) return;
|
|
3225
|
-
onSearchChange?.("");
|
|
3226
|
-
onSortChange?.(null);
|
|
3227
|
-
if (!resourceServerPaging) onPageChange?.(1);
|
|
3228
|
-
}, [
|
|
3479
|
+
keyOf,
|
|
3480
|
+
keyboardNav,
|
|
3481
|
+
linkMissing,
|
|
3482
|
+
linked,
|
|
3483
|
+
missingPagerProps,
|
|
3229
3484
|
page,
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
updateSetting("pageSize", size);
|
|
3258
|
-
onPerPageChange?.(size);
|
|
3259
|
-
if (resourceServerPaging) resourceHook.setPerPage(size);
|
|
3260
|
-
else if (isServerControlled) onPageChange?.(1);
|
|
3261
|
-
},
|
|
3262
|
-
[
|
|
3263
|
-
updateSetting,
|
|
3264
|
-
onPerPageChange,
|
|
3265
|
-
resourceServerPaging,
|
|
3266
|
-
resourceHook.setPerPage,
|
|
3267
|
-
isServerControlled,
|
|
3268
|
-
onPageChange
|
|
3269
|
-
]
|
|
3270
|
-
);
|
|
3271
|
-
const missingPerPage = isServerControlled && !!pagerOnPageChange && pagerPerPage === void 0;
|
|
3272
|
-
const missingTotal = isServerControlled && !!pagerOnPageChange && pagerTotal === void 0;
|
|
3273
|
-
const pagerMisconfigured = missingPerPage || missingTotal;
|
|
3274
|
-
const missingPagerProps = [
|
|
3275
|
-
...missingPerPage ? ["perPage"] : [],
|
|
3276
|
-
...missingTotal ? ["total"] : []
|
|
3277
|
-
];
|
|
3278
|
-
const loggedMissingPerPageRef = useRef(false);
|
|
3279
|
-
useEffect(() => {
|
|
3280
|
-
if (!pagerMisconfigured) return;
|
|
3281
|
-
if (loggedMissingPerPageRef.current) return;
|
|
3282
|
-
loggedMissingPerPageRef.current = true;
|
|
3283
|
-
console.error(
|
|
3284
|
-
`ResourcePage: ${missingPagerProps.map((name) => `\`${name}\``).join(" and ")} ${missingPagerProps.length > 1 ? "are" : "is"} required when \`onPageChange\` is supplied.`
|
|
3285
|
-
);
|
|
3286
|
-
}, [pagerMisconfigured]);
|
|
3287
|
-
const pagerPageSize = pagerPerPage ?? effectivePageSize;
|
|
3288
|
-
const totalPages = Math.max(
|
|
3289
|
-
1,
|
|
3290
|
-
Math.ceil(displayTotal / Math.max(1, pagerPageSize))
|
|
3291
|
-
);
|
|
3292
|
-
const rangeFrom = page.data.length === 0 ? 0 : (currentPage - 1) * pagerPageSize + 1;
|
|
3293
|
-
const rangeTo = rangeFrom === 0 ? 0 : rangeFrom + page.data.length - 1;
|
|
3294
|
-
const { setPage: setResourcePage } = resourceHook;
|
|
3295
|
-
const settledPage = !isLoading && !hasError && !resourceHook.list.isPlaceholderData;
|
|
3296
|
-
useEffect(() => {
|
|
3297
|
-
if (!resourceServerPaging || !settledPage) return;
|
|
3298
|
-
if (currentPage > totalPages) setResourcePage(totalPages);
|
|
3299
|
-
}, [resourceServerPaging, settledPage, currentPage, totalPages, setResourcePage]);
|
|
3300
|
-
const titleOf = (item) => getTitle ? getTitle(item) : String(item[page.titleField] ?? "");
|
|
3301
|
-
const detailTitle = selectedItem ? titleOf(selectedItem) : "";
|
|
3302
|
-
const detailSubtitle = !selectedItem ? void 0 : getSubtitle ? getSubtitle(selectedItem) : page.subtitleField ? String(
|
|
3303
|
-
selectedItem[page.subtitleField] ?? ""
|
|
3304
|
-
) : void 0;
|
|
3305
|
-
const handleRowClick = (item) => {
|
|
3306
|
-
if (detail === "route") {
|
|
3307
|
-
const id = item.id;
|
|
3308
|
-
if (resourceName && id) {
|
|
3309
|
-
onNavigate(`/${resourceName}/${id}`);
|
|
3310
|
-
}
|
|
3311
|
-
return;
|
|
3312
|
-
}
|
|
3313
|
-
page.handleRowClick(item);
|
|
3314
|
-
};
|
|
3315
|
-
const hasBulkSelect = !!bulkActions && bulkActions.length > 0;
|
|
3316
|
-
const overlayOpen = page.detailOpen || formOpen || deleteOpen;
|
|
3317
|
-
const keyboardNav = useKeyboardNavigation({
|
|
3318
|
-
itemCount: page.data.length,
|
|
3319
|
-
enabled: keyboardNavigation && page.viewMode !== "table",
|
|
3320
|
-
overlayOpen,
|
|
3321
|
-
onOpen: (index) => {
|
|
3322
|
-
const item = page.data[index];
|
|
3323
|
-
if (item) handleRowClick(item);
|
|
3324
|
-
},
|
|
3325
|
-
onToggleSelect: hasBulkSelect ? (index) => {
|
|
3326
|
-
const item = page.data[index];
|
|
3327
|
-
if (item) {
|
|
3328
|
-
handleToggleSelect(keyOf(item));
|
|
3329
|
-
}
|
|
3330
|
-
} : void 0,
|
|
3331
|
-
// Supplied only while there is an open detail to close — the hook
|
|
3332
|
-
// suppresses Backspace's default whenever this is present, so leaving it
|
|
3333
|
-
// wired up permanently would swallow the browser/app's Backspace on
|
|
3334
|
-
// every grid/list page (including detail="none" and detail="route",
|
|
3335
|
-
// which never open one). `detailOpen` is only ever true for the panel
|
|
3336
|
-
// and modal modes, so it is the precise precondition here.
|
|
3337
|
-
onBack: page.detailOpen ? () => {
|
|
3338
|
-
page.setDetailOpen(false);
|
|
3339
|
-
page.setSelectedItem(null);
|
|
3340
|
-
} : void 0
|
|
3341
|
-
});
|
|
3342
|
-
const showCreateButton = onCreate || useBuiltInCreate;
|
|
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);
|
|
3511
|
+
const showCreateButton = (onCreate || useBuiltInCreate) && allows("create");
|
|
3343
3512
|
const createButton = showCreateButton ? /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: handleCreate, children: [
|
|
3344
3513
|
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4 mr-1" }),
|
|
3345
3514
|
createLabel
|
|
@@ -3439,7 +3608,8 @@ function ResourcePage(props) {
|
|
|
3439
3608
|
isLoading,
|
|
3440
3609
|
hasError,
|
|
3441
3610
|
isEmpty,
|
|
3442
|
-
errorMessage: errorMessage2
|
|
3611
|
+
errorMessage: errorMessage2 ?? // A provider's read error carries a sentence users can be shown.
|
|
3612
|
+
(isClientMode && isDataReadError(resourceHook.list.error) ? resourceHook.list.error.message : void 0),
|
|
3443
3613
|
onRetry: isClientMode ? () => resourceHook.list.refetch() : onRetryProp,
|
|
3444
3614
|
viewMode: page.viewMode,
|
|
3445
3615
|
onViewModeChange: page.setViewMode,
|
|
@@ -3534,9 +3704,13 @@ function ResourcePage(props) {
|
|
|
3534
3704
|
resizeKey,
|
|
3535
3705
|
stickyHeader,
|
|
3536
3706
|
stickyFirstColumn,
|
|
3707
|
+
keyboardNavigation,
|
|
3537
3708
|
// The server already cut the page window; DataTable's own
|
|
3538
3709
|
// pagination would slice the slice.
|
|
3539
|
-
pagination: isServerControlled ? false : void 0
|
|
3710
|
+
pagination: isServerControlled ? false : void 0,
|
|
3711
|
+
// Client mode: the header sort is the page's sort, so it
|
|
3712
|
+
// reaches the URL and a link restores what the table shows.
|
|
3713
|
+
...isServerControlled ? {} : { sort: tableSort, onSortChange: handleTableSort }
|
|
3540
3714
|
},
|
|
3541
3715
|
slotProps?.table
|
|
3542
3716
|
),
|
|
@@ -3656,9 +3830,18 @@ function ResourcePage(props) {
|
|
|
3656
3830
|
(detail === "panel" || detail === "modal") && renderSlot(
|
|
3657
3831
|
DetailPanel,
|
|
3658
3832
|
slots?.detail,
|
|
3659
|
-
{
|
|
3833
|
+
linkMissing ? {
|
|
3834
|
+
// A linked record this user can't read, or that's gone.
|
|
3835
|
+
item: {},
|
|
3836
|
+
open: detailOpen,
|
|
3837
|
+
onClose: closeDetail,
|
|
3838
|
+
title: "Record not found",
|
|
3839
|
+
width: detailWidth,
|
|
3840
|
+
layout: detail === "modal" ? "dialog" : "sheet",
|
|
3841
|
+
children: () => /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", "data-testid": "resource-page-link-missing", children: isDataReadError(linked.error) && linked.error.outcome === "forbidden" ? linked.error.message : "This record doesn't exist, or you don't have access to it." })
|
|
3842
|
+
} : {
|
|
3660
3843
|
item: selectedItem,
|
|
3661
|
-
open:
|
|
3844
|
+
open: detailOpen,
|
|
3662
3845
|
onClose: closeDetail,
|
|
3663
3846
|
title: detailTitle,
|
|
3664
3847
|
subtitle: detailSubtitle,
|
|
@@ -3680,6 +3863,7 @@ function ResourcePage(props) {
|
|
|
3680
3863
|
layout: formLayoutProp ?? (formAction === "edit" ? "sheet" : "dialog"),
|
|
3681
3864
|
mode: formMode,
|
|
3682
3865
|
steps: formSteps,
|
|
3866
|
+
mutationMode: mutationModeProp,
|
|
3683
3867
|
item: formAction === "edit" ? formItem ?? void 0 : page.data.length > 0 ? Object.fromEntries(
|
|
3684
3868
|
Object.keys(page.data[0]).filter((k) => k !== "id").map((k) => [k, ""])
|
|
3685
3869
|
) : void 0,
|
|
@@ -3701,6 +3885,7 @@ function ResourcePage(props) {
|
|
|
3701
3885
|
open: deleteOpen,
|
|
3702
3886
|
onOpenChange: setDeleteOpen,
|
|
3703
3887
|
onConfirm: () => pendingDelete ? performDelete(pendingDelete) : void 0,
|
|
3888
|
+
kind: deleteKind,
|
|
3704
3889
|
title: typeof deleteCopy?.title === "function" ? pendingDelete ? deleteCopy.title(pendingDelete) : void 0 : deleteCopy?.title,
|
|
3705
3890
|
description: typeof deleteCopy?.description === "function" ? pendingDelete ? deleteCopy.description(pendingDelete) : void 0 : deleteCopy?.description,
|
|
3706
3891
|
confirmLabel: deleteCopy?.confirmLabel,
|