@marktiderman/genesis-ui 2.8.0 → 2.9.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 +58 -4
- package/dist/chunk-DIV4BYXX.js +58 -0
- package/dist/chunk-DIV4BYXX.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 +88 -25
- package/dist/components/data/index.js +702 -224
- package/dist/components/data/index.js.map +1 -1
- package/dist/components/layout/index.js +7 -7
- package/dist/hooks/index.d.ts +2 -1
- package/dist/hooks/index.js +2 -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 +19 -19
- package/dist/{use-resource-page-D4byuwAs.d.ts → use-resource-page-BDnnCssE.d.ts} +63 -2
- package/package.json +4 -8
- package/dist/chunk-DWCAC2E7.js.map +0 -1
|
@@ -2,33 +2,35 @@
|
|
|
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 {
|
|
10
|
+
import { toast } from '../../chunk-4RAZJ6LL.js';
|
|
10
11
|
import { Progress } from '../../chunk-W5Y3LG5K.js';
|
|
11
|
-
import { Card, CardContent, CardHeader, CardTitle } from '../../chunk-FN3FZVUR.js';
|
|
12
12
|
import { Checkbox } from '../../chunk-SEBU6JCK.js';
|
|
13
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../chunk-FN3FZVUR.js';
|
|
13
14
|
import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from '../../chunk-4C4ESW6O.js';
|
|
15
|
+
import { PageLoading } from '../../chunk-YRYQXU3G.js';
|
|
16
|
+
import { Spinner } from '../../chunk-OVU3Z2MT.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 { titleCase, useResourcePage } from '../../chunk-
|
|
32
|
+
import { titleCase, useResourcePage } from '../../chunk-RKPS7ERS.js';
|
|
33
|
+
import { useResourceUrlState } from '../../chunk-DIV4BYXX.js';
|
|
32
34
|
export { useSavedViews } from '../../chunk-GCQFVMPI.js';
|
|
33
35
|
import '../../chunk-PKFDGGZV.js';
|
|
34
36
|
import { useViewSettings } from '../../chunk-OZ7ZAYCS.js';
|
|
@@ -43,10 +45,12 @@ import { useIsMobile } from '../../chunk-VFO3I7PV.js';
|
|
|
43
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';
|
|
44
46
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
45
47
|
import { useState, useRef, useEffect, useCallback, useMemo, useId, Fragment as Fragment$1 } from 'react';
|
|
48
|
+
import { useReactTable, getPaginationRowModel, getSortedRowModel, getCoreRowModel } from '@tanstack/react-table';
|
|
49
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
46
50
|
import { formatDistanceToNow, format } from 'date-fns';
|
|
47
51
|
import { useFieldArray, Controller, useWatch } from 'react-hook-form';
|
|
48
|
-
import { useResource, useWizard, useResourceForm, useOne, buildArrayItemDefaults } from '@marktiderman/genesis-core/hooks';
|
|
49
|
-
import { useDataProvider } from '@marktiderman/genesis-core/provider';
|
|
52
|
+
import { useResource, useWizard, useResourceForm, useOne, useCapabilities, buildArrayItemDefaults } from '@marktiderman/genesis-core/hooks';
|
|
53
|
+
import { useDataProvider, isDataReadError } from '@marktiderman/genesis-core/provider';
|
|
50
54
|
import { useDropzone } from 'react-dropzone';
|
|
51
55
|
|
|
52
56
|
function DataPageShell({
|
|
@@ -670,6 +674,50 @@ var DENSITY_HEAD = {
|
|
|
670
674
|
comfortable: "h-10",
|
|
671
675
|
spacious: "h-12 py-3"
|
|
672
676
|
};
|
|
677
|
+
function storeSet(key, value) {
|
|
678
|
+
try {
|
|
679
|
+
localStorage.setItem(key, value);
|
|
680
|
+
} catch {
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
function storeRemove(key) {
|
|
684
|
+
try {
|
|
685
|
+
localStorage.removeItem(key);
|
|
686
|
+
} catch {
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
var ROW_HEIGHT = { compact: 33, comfortable: 41, spacious: 49 };
|
|
690
|
+
var PINNED_WIDTH = 160;
|
|
691
|
+
function compareValues(a, b) {
|
|
692
|
+
const emptyA = a == null || a === "";
|
|
693
|
+
const emptyB = b == null || b === "";
|
|
694
|
+
if (emptyA || emptyB) return emptyA === emptyB ? 0 : emptyA ? -1 : 1;
|
|
695
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
696
|
+
if (typeof a === "bigint" && typeof b === "bigint") return a < b ? -1 : a > b ? 1 : 0;
|
|
697
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
|
|
698
|
+
if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
|
|
699
|
+
return String(a).localeCompare(String(b), void 0, { numeric: true, sensitivity: "base" });
|
|
700
|
+
}
|
|
701
|
+
var ROW_OPEN = "data-row-open";
|
|
702
|
+
function focusRowAt(body, index) {
|
|
703
|
+
const target = body?.querySelector(`[${ROW_OPEN}="${index}"]`);
|
|
704
|
+
target?.focus();
|
|
705
|
+
return !!target;
|
|
706
|
+
}
|
|
707
|
+
function nextSort(prev, key, shiftKey) {
|
|
708
|
+
const existing = prev.findIndex((s) => s.key === key);
|
|
709
|
+
if (existing >= 0) {
|
|
710
|
+
const entry = prev[existing];
|
|
711
|
+
if (entry.dir === "asc") {
|
|
712
|
+
const next = [...prev];
|
|
713
|
+
next[existing] = { key, dir: "desc" };
|
|
714
|
+
return next;
|
|
715
|
+
}
|
|
716
|
+
return prev.filter((_, i) => i !== existing);
|
|
717
|
+
}
|
|
718
|
+
if (shiftKey) return [...prev, { key, dir: "asc" }];
|
|
719
|
+
return [{ key, dir: "asc" }];
|
|
720
|
+
}
|
|
673
721
|
function getCellClasses(col) {
|
|
674
722
|
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
723
|
return cn(typeof col.width === "string" && !PX_WIDTH.test(col.width) ? col.width : void 0, hide);
|
|
@@ -888,29 +936,29 @@ function DataTable({
|
|
|
888
936
|
stickyHeader = true,
|
|
889
937
|
resizeKey,
|
|
890
938
|
stickyFirstColumn,
|
|
891
|
-
density = "comfortable"
|
|
939
|
+
density = "comfortable",
|
|
940
|
+
keyboardNavigation = true,
|
|
941
|
+
openColumn,
|
|
942
|
+
virtualize,
|
|
943
|
+
pinnedColumns,
|
|
944
|
+
reorderableColumns = true,
|
|
945
|
+
columnOrderKey,
|
|
946
|
+
sort: sortProp,
|
|
947
|
+
onSortChange
|
|
892
948
|
}) {
|
|
949
|
+
const rowKeys = keyboardNavigation && !!onRowClick;
|
|
950
|
+
const [activeRowKey, setActiveRowKey] = useState(null);
|
|
893
951
|
const cellDensity = DENSITY_CELL[density];
|
|
894
952
|
const headDensity = DENSITY_HEAD[density];
|
|
895
|
-
const [
|
|
953
|
+
const [ownSorts, setOwnSorts] = useState([]);
|
|
954
|
+
const sorts = sortProp ?? ownSorts;
|
|
896
955
|
const handleSort = useCallback(
|
|
897
956
|
(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
|
-
});
|
|
957
|
+
const next = nextSort(sorts, key, shiftKey);
|
|
958
|
+
if (sortProp === void 0) setOwnSorts(next);
|
|
959
|
+
onSortChange?.(next);
|
|
912
960
|
},
|
|
913
|
-
[]
|
|
961
|
+
[sorts, sortProp, onSortChange]
|
|
914
962
|
);
|
|
915
963
|
const [visibleCols, setVisibleCols] = useState(
|
|
916
964
|
() => {
|
|
@@ -950,7 +998,7 @@ function DataTable({
|
|
|
950
998
|
}, [columnVisibilityKey]);
|
|
951
999
|
useEffect(() => {
|
|
952
1000
|
if (!visibilityHydrated || !columnVisibilityKey) return;
|
|
953
|
-
|
|
1001
|
+
storeSet(
|
|
954
1002
|
`dt-vis-${columnVisibilityKey}`,
|
|
955
1003
|
JSON.stringify(visibleCols)
|
|
956
1004
|
);
|
|
@@ -964,8 +1012,9 @@ function DataTable({
|
|
|
964
1012
|
defaults[c.key] = c.defaultVisible !== false;
|
|
965
1013
|
});
|
|
966
1014
|
setVisibleCols(defaults);
|
|
1015
|
+
setColOrder(null);
|
|
967
1016
|
if (columnVisibilityKey) {
|
|
968
|
-
|
|
1017
|
+
storeRemove(`dt-vis-${columnVisibilityKey}`);
|
|
969
1018
|
}
|
|
970
1019
|
}, [columns, columnVisibilityKey]);
|
|
971
1020
|
useEffect(() => {
|
|
@@ -981,7 +1030,54 @@ function DataTable({
|
|
|
981
1030
|
return changed ? updated : prev;
|
|
982
1031
|
});
|
|
983
1032
|
}, [columns]);
|
|
984
|
-
const
|
|
1033
|
+
const [colOrder, setColOrder] = useState(null);
|
|
1034
|
+
const [orderHydrated, setOrderHydrated] = useState(false);
|
|
1035
|
+
useEffect(() => {
|
|
1036
|
+
if (columnOrderKey) {
|
|
1037
|
+
try {
|
|
1038
|
+
const saved = localStorage.getItem(`dt-order-${columnOrderKey}`);
|
|
1039
|
+
const parsed = saved ? JSON.parse(saved) : null;
|
|
1040
|
+
if (Array.isArray(parsed) && parsed.every((k) => typeof k === "string")) {
|
|
1041
|
+
setColOrder(parsed);
|
|
1042
|
+
}
|
|
1043
|
+
} catch {
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
setOrderHydrated(true);
|
|
1047
|
+
}, [columnOrderKey]);
|
|
1048
|
+
useEffect(() => {
|
|
1049
|
+
if (!orderHydrated || !columnOrderKey) return;
|
|
1050
|
+
if (colOrder) storeSet(`dt-order-${columnOrderKey}`, JSON.stringify(colOrder));
|
|
1051
|
+
else storeRemove(`dt-order-${columnOrderKey}`);
|
|
1052
|
+
}, [colOrder, columnOrderKey, orderHydrated]);
|
|
1053
|
+
const orderedColumns = useMemo(() => {
|
|
1054
|
+
if (!colOrder) return columns;
|
|
1055
|
+
const rank = new Map(colOrder.map((k, i) => [k, i]));
|
|
1056
|
+
return [...columns].sort(
|
|
1057
|
+
(a, b) => (rank.get(a.key) ?? colOrder.length) - (rank.get(b.key) ?? colOrder.length)
|
|
1058
|
+
);
|
|
1059
|
+
}, [columns, colOrder]);
|
|
1060
|
+
const moveColumn = useCallback(
|
|
1061
|
+
(key, to) => {
|
|
1062
|
+
const keys = orderedColumns.map((c) => c.key);
|
|
1063
|
+
const from = keys.indexOf(key);
|
|
1064
|
+
if (from < 0) return;
|
|
1065
|
+
keys.splice(from, 1);
|
|
1066
|
+
const at = "before" in to ? Math.max(0, keys.indexOf(to.before)) : keys.indexOf(to.after) + 1;
|
|
1067
|
+
keys.splice(at, 0, key);
|
|
1068
|
+
setColOrder(keys);
|
|
1069
|
+
},
|
|
1070
|
+
[orderedColumns]
|
|
1071
|
+
);
|
|
1072
|
+
const [dragKey, setDragKey] = useState(null);
|
|
1073
|
+
const visibleColumns = columnVisibility ? orderedColumns.filter((c) => visibleCols[c.key] !== false) : orderedColumns;
|
|
1074
|
+
const leftKeys = pinnedColumns?.left ?? (stickyFirstColumn && visibleColumns[0] ? [visibleColumns[0].key] : []);
|
|
1075
|
+
const rightKeys = pinnedColumns?.right ?? [];
|
|
1076
|
+
const activeColumns = [
|
|
1077
|
+
...visibleColumns.filter((c) => leftKeys.includes(c.key)),
|
|
1078
|
+
...visibleColumns.filter((c) => !leftKeys.includes(c.key) && !rightKeys.includes(c.key)),
|
|
1079
|
+
...visibleColumns.filter((c) => rightKeys.includes(c.key))
|
|
1080
|
+
];
|
|
985
1081
|
const [colWidths, setColWidths] = useState(() => {
|
|
986
1082
|
if (resizeKey) {
|
|
987
1083
|
try {
|
|
@@ -1008,46 +1104,232 @@ function DataTable({
|
|
|
1008
1104
|
);
|
|
1009
1105
|
const handleResizeEnd = useCallback(() => {
|
|
1010
1106
|
if (resizeKey) {
|
|
1011
|
-
|
|
1107
|
+
storeSet(`dt-resize-${resizeKey}`, JSON.stringify(colWidths));
|
|
1012
1108
|
}
|
|
1013
1109
|
}, [resizeKey, colWidths]);
|
|
1110
|
+
const pinWidth = (key) => colWidths[key] ?? PINNED_WIDTH;
|
|
1111
|
+
const pinned = {};
|
|
1112
|
+
{
|
|
1113
|
+
let left = selectable ? 40 : 0;
|
|
1114
|
+
for (const c of activeColumns) {
|
|
1115
|
+
if (!leftKeys.includes(c.key)) continue;
|
|
1116
|
+
pinned[c.key] = { side: "left", offset: left };
|
|
1117
|
+
left += pinWidth(c.key);
|
|
1118
|
+
}
|
|
1119
|
+
let right = renderActions ? 40 : 0;
|
|
1120
|
+
for (const c of [...activeColumns].reverse()) {
|
|
1121
|
+
if (!rightKeys.includes(c.key)) continue;
|
|
1122
|
+
pinned[c.key] = { side: "right", offset: right };
|
|
1123
|
+
right += pinWidth(c.key);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
const pinProps = (key, header) => {
|
|
1127
|
+
const z = header ? stickyHeader ? "z-[15]" : "z-[5]" : "z-[1]";
|
|
1128
|
+
if (key === "__select") {
|
|
1129
|
+
return leftKeys.length ? { className: cn("sticky left-0 bg-background", z), style: { left: 0 } } : null;
|
|
1130
|
+
}
|
|
1131
|
+
if (key === "__actions") {
|
|
1132
|
+
return rightKeys.length ? { className: cn("sticky right-0 bg-background", z), style: { right: 0 } } : null;
|
|
1133
|
+
}
|
|
1134
|
+
const p = pinned[key];
|
|
1135
|
+
return p ? { className: cn("sticky bg-background", z), style: { [p.side]: p.offset } } : null;
|
|
1136
|
+
};
|
|
1014
1137
|
const [currentPage, setCurrentPage] = useState(0);
|
|
1015
1138
|
const [currentPageSize, setCurrentPageSize] = useState(initialPageSize);
|
|
1016
1139
|
useEffect(() => {
|
|
1017
1140
|
setCurrentPage(0);
|
|
1018
1141
|
}, [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
|
-
|
|
1142
|
+
const sorting = useMemo(
|
|
1143
|
+
() => sorts.map((s) => ({ id: s.key, desc: s.dir === "desc" })),
|
|
1144
|
+
[sorts]
|
|
1145
|
+
);
|
|
1146
|
+
const tanstackColumns = useMemo(
|
|
1147
|
+
() => columns.map((col) => ({
|
|
1148
|
+
id: col.key,
|
|
1149
|
+
// Blanks read as undefined here, so `sortUndefined` keeps them last
|
|
1150
|
+
// whichever way the column sorts.
|
|
1151
|
+
accessorFn: (row) => {
|
|
1152
|
+
const v = col.accessor ? col.accessor(row) : row[col.key];
|
|
1153
|
+
return v == null || v === "" ? void 0 : v;
|
|
1154
|
+
},
|
|
1155
|
+
sortUndefined: "last",
|
|
1156
|
+
sortingFn: (a, b, id) => compareValues(a.getValue(id), b.getValue(id))
|
|
1157
|
+
})),
|
|
1158
|
+
[columns]
|
|
1159
|
+
);
|
|
1160
|
+
const table = useReactTable({
|
|
1161
|
+
data: items,
|
|
1162
|
+
columns: tanstackColumns,
|
|
1163
|
+
getRowId: (row) => getKey(row),
|
|
1164
|
+
state: {
|
|
1165
|
+
sorting,
|
|
1166
|
+
pagination: { pageIndex: currentPage, pageSize: currentPageSize }
|
|
1167
|
+
},
|
|
1168
|
+
enableMultiSort: true,
|
|
1169
|
+
getCoreRowModel: getCoreRowModel(),
|
|
1170
|
+
getSortedRowModel: getSortedRowModel(),
|
|
1171
|
+
...pagination ? { getPaginationRowModel: getPaginationRowModel() } : {}
|
|
1172
|
+
});
|
|
1173
|
+
const totalItems = table.getPrePaginationRowModel().rows.length;
|
|
1040
1174
|
const totalPages = pagination ? Math.max(1, Math.ceil(totalItems / currentPageSize)) : 1;
|
|
1041
|
-
const displayItems =
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1175
|
+
const displayItems = table.getRowModel().rows.map((row) => row.original);
|
|
1176
|
+
const scrollRef = useRef(null);
|
|
1177
|
+
const bodyRef = useRef(null);
|
|
1178
|
+
const virtualizing = !!virtualize && !pagination;
|
|
1179
|
+
const virtualOptions = typeof virtualize === "object" ? virtualize : {};
|
|
1180
|
+
const viewportHeight = virtualOptions.height ?? 600;
|
|
1181
|
+
const rowVirtualizer = useVirtualizer({
|
|
1182
|
+
count: virtualizing ? displayItems.length : 0,
|
|
1183
|
+
getScrollElement: () => scrollRef.current,
|
|
1184
|
+
estimateSize: () => virtualOptions.estimateRowHeight ?? ROW_HEIGHT[density],
|
|
1185
|
+
overscan: 10,
|
|
1186
|
+
initialRect: { width: 0, height: viewportHeight }
|
|
1187
|
+
});
|
|
1188
|
+
const virtualRows = virtualizing ? rowVirtualizer.getVirtualItems() : null;
|
|
1189
|
+
const focusRow = (index) => {
|
|
1190
|
+
const last = displayItems.length - 1;
|
|
1191
|
+
const target = Math.max(0, Math.min(last, index));
|
|
1192
|
+
if (focusRowAt(bodyRef.current, target)) return;
|
|
1193
|
+
if (!virtualizing) return;
|
|
1194
|
+
rowVirtualizer.scrollToIndex(target);
|
|
1195
|
+
requestAnimationFrame(() => focusRowAt(bodyRef.current, target));
|
|
1196
|
+
};
|
|
1197
|
+
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;
|
|
1199
|
+
const rowLabel = (item, id) => {
|
|
1200
|
+
const col = activeColumns.find((c) => c.key === (openKey ?? activeColumns[0]?.key));
|
|
1201
|
+
const value = col ? (col.accessor ?? ((it) => it[col.key]))(item) : void 0;
|
|
1202
|
+
return typeof value === "string" || typeof value === "number" ? String(value) : id;
|
|
1203
|
+
};
|
|
1045
1204
|
const showingFrom = totalItems === 0 ? 0 : currentPage * currentPageSize + 1;
|
|
1046
1205
|
const showingTo = Math.min(
|
|
1047
1206
|
(currentPage + 1) * currentPageSize,
|
|
1048
1207
|
totalItems
|
|
1049
1208
|
);
|
|
1050
1209
|
const totalCols = activeColumns.length + (selectable ? 1 : 0) + (renderActions ? 1 : 0);
|
|
1210
|
+
const renderRow = (item, index, measure) => {
|
|
1211
|
+
const id = getKey(item);
|
|
1212
|
+
return /* @__PURE__ */ jsxs(
|
|
1213
|
+
TableRow,
|
|
1214
|
+
{
|
|
1215
|
+
ref: measure,
|
|
1216
|
+
"data-index": index,
|
|
1217
|
+
"aria-rowindex": virtualizing ? index + 2 : void 0,
|
|
1218
|
+
className: cn(
|
|
1219
|
+
onRowClick && "cursor-pointer",
|
|
1220
|
+
"focus-within:bg-muted/40"
|
|
1221
|
+
),
|
|
1222
|
+
"data-state": selected?.has(id) ? "selected" : void 0,
|
|
1223
|
+
onClick: onRowClick ? () => onRowClick(item) : void 0,
|
|
1224
|
+
children: [
|
|
1225
|
+
selectable && /* @__PURE__ */ jsx(
|
|
1226
|
+
TableCell,
|
|
1227
|
+
{
|
|
1228
|
+
className: cn(cellDensity, pinProps("__select", false)?.className),
|
|
1229
|
+
style: pinProps("__select", false)?.style,
|
|
1230
|
+
onClick: (e) => e.stopPropagation(),
|
|
1231
|
+
children: /* @__PURE__ */ jsx(
|
|
1232
|
+
Checkbox,
|
|
1233
|
+
{
|
|
1234
|
+
"aria-label": `Select ${rowLabel(item, id)}`,
|
|
1235
|
+
checked: selected?.has(id),
|
|
1236
|
+
onCheckedChange: () => onToggleSelect?.(id)
|
|
1237
|
+
}
|
|
1238
|
+
)
|
|
1239
|
+
}
|
|
1240
|
+
),
|
|
1241
|
+
activeColumns.map((col, colIndex) => {
|
|
1242
|
+
const acc = col.accessor || ((it) => it[col.key]);
|
|
1243
|
+
const value = acc(item);
|
|
1244
|
+
const pin = pinProps(col.key, false);
|
|
1245
|
+
return /* @__PURE__ */ jsx(
|
|
1246
|
+
TableCell,
|
|
1247
|
+
{
|
|
1248
|
+
className: cn(
|
|
1249
|
+
getCellClasses(
|
|
1250
|
+
col
|
|
1251
|
+
),
|
|
1252
|
+
cellDensity,
|
|
1253
|
+
pin?.className
|
|
1254
|
+
),
|
|
1255
|
+
style: pin?.style,
|
|
1256
|
+
children: (() => {
|
|
1257
|
+
const content = col.editable ? /* @__PURE__ */ jsx(
|
|
1258
|
+
EditableCell,
|
|
1259
|
+
{
|
|
1260
|
+
item,
|
|
1261
|
+
column: col,
|
|
1262
|
+
value
|
|
1263
|
+
}
|
|
1264
|
+
) : col.render ? col.render(item, value) : /* @__PURE__ */ jsx("span", { className: "truncate block max-w-[300px]", children: String(value ?? "\u2014") });
|
|
1265
|
+
if (col.key !== openKey) return content;
|
|
1266
|
+
const openProps = {
|
|
1267
|
+
type: "button",
|
|
1268
|
+
[ROW_OPEN]: String(index),
|
|
1269
|
+
tabIndex: id === tabStopKey ? 0 : -1,
|
|
1270
|
+
onFocus: () => setActiveRowKey(id),
|
|
1271
|
+
onClick: (e) => {
|
|
1272
|
+
e.stopPropagation();
|
|
1273
|
+
onRowClick?.(item);
|
|
1274
|
+
},
|
|
1275
|
+
onKeyDown: (e) => {
|
|
1276
|
+
if (e.target !== e.currentTarget) return;
|
|
1277
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
1278
|
+
const move = (to) => {
|
|
1279
|
+
e.preventDefault();
|
|
1280
|
+
focusRow(to);
|
|
1281
|
+
};
|
|
1282
|
+
if (e.key === "ArrowDown" || e.key === "j") move(index + 1);
|
|
1283
|
+
else if (e.key === "ArrowUp" || e.key === "k") move(index - 1);
|
|
1284
|
+
else if (e.key === "Home") move(0);
|
|
1285
|
+
else if (e.key === "End") move(displayItems.length - 1);
|
|
1286
|
+
else if (e.key === "x" && selectable) {
|
|
1287
|
+
e.preventDefault();
|
|
1288
|
+
onToggleSelect?.(id);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
if (!col.editable && !col.render) {
|
|
1293
|
+
return /* @__PURE__ */ jsx(
|
|
1294
|
+
"button",
|
|
1295
|
+
{
|
|
1296
|
+
...openProps,
|
|
1297
|
+
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",
|
|
1298
|
+
children: content
|
|
1299
|
+
}
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1", children: [
|
|
1303
|
+
/* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: content }),
|
|
1304
|
+
/* @__PURE__ */ jsx(
|
|
1305
|
+
"button",
|
|
1306
|
+
{
|
|
1307
|
+
...openProps,
|
|
1308
|
+
"aria-label": `Open ${rowLabel(item, id)}`,
|
|
1309
|
+
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",
|
|
1310
|
+
children: "Open"
|
|
1311
|
+
}
|
|
1312
|
+
)
|
|
1313
|
+
] });
|
|
1314
|
+
})()
|
|
1315
|
+
},
|
|
1316
|
+
col.key
|
|
1317
|
+
);
|
|
1318
|
+
}),
|
|
1319
|
+
renderActions && /* @__PURE__ */ jsx(
|
|
1320
|
+
TableCell,
|
|
1321
|
+
{
|
|
1322
|
+
className: cn(cellDensity, pinProps("__actions", false)?.className),
|
|
1323
|
+
style: pinProps("__actions", false)?.style,
|
|
1324
|
+
onClick: (e) => e.stopPropagation(),
|
|
1325
|
+
children: renderActions(item)
|
|
1326
|
+
}
|
|
1327
|
+
)
|
|
1328
|
+
]
|
|
1329
|
+
},
|
|
1330
|
+
id
|
|
1331
|
+
);
|
|
1332
|
+
};
|
|
1051
1333
|
return /* @__PURE__ */ jsxs("div", { className: "border rounded-lg animate-fade-in flex flex-col", children: [
|
|
1052
1334
|
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
1335
|
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
@@ -1085,151 +1367,181 @@ function DataTable({
|
|
|
1085
1367
|
}
|
|
1086
1368
|
)
|
|
1087
1369
|
] }) }),
|
|
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,
|
|
1370
|
+
/* @__PURE__ */ jsx(
|
|
1371
|
+
"div",
|
|
1372
|
+
{
|
|
1373
|
+
ref: scrollRef,
|
|
1374
|
+
className: cn("overflow-x-auto", virtualizing && "overflow-y-auto"),
|
|
1375
|
+
style: virtualizing ? { maxHeight: viewportHeight } : void 0,
|
|
1376
|
+
children: /* @__PURE__ */ jsxs(
|
|
1377
|
+
Table,
|
|
1174
1378
|
{
|
|
1175
|
-
|
|
1176
|
-
"
|
|
1177
|
-
onClick: onRowClick ? () => onRowClick(item) : void 0,
|
|
1379
|
+
style: { tableLayout: "fixed" },
|
|
1380
|
+
"aria-rowcount": virtualizing ? displayItems.length + 1 : void 0,
|
|
1178
1381
|
children: [
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
{
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
Checkbox,
|
|
1382
|
+
/* @__PURE__ */ jsxs("colgroup", { children: [
|
|
1383
|
+
selectable && /* @__PURE__ */ jsx("col", { style: { width: 40 } }),
|
|
1384
|
+
activeColumns.map((col) => {
|
|
1385
|
+
const w = colWidths[col.key] ?? (pinned[col.key] ? PINNED_WIDTH : void 0);
|
|
1386
|
+
return /* @__PURE__ */ jsx(
|
|
1387
|
+
"col",
|
|
1186
1388
|
{
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
{
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1389
|
+
className: colHideClass(col.hideBelow),
|
|
1390
|
+
style: w ? { width: w } : void 0
|
|
1391
|
+
},
|
|
1392
|
+
col.key
|
|
1393
|
+
);
|
|
1394
|
+
}),
|
|
1395
|
+
renderActions && /* @__PURE__ */ jsx("col", { style: { width: 40 } })
|
|
1396
|
+
] }),
|
|
1397
|
+
/* @__PURE__ */ jsx(
|
|
1398
|
+
TableHeader,
|
|
1399
|
+
{
|
|
1400
|
+
className: stickyHeader ? "sticky top-0 z-10 bg-background" : "",
|
|
1401
|
+
children: /* @__PURE__ */ jsxs(TableRow, { "aria-rowindex": virtualizing ? 1 : void 0, children: [
|
|
1402
|
+
selectable && /* @__PURE__ */ jsx(
|
|
1403
|
+
TableHead,
|
|
1404
|
+
{
|
|
1405
|
+
className: cn("w-10", headDensity, pinProps("__select", true)?.className),
|
|
1406
|
+
style: pinProps("__select", true)?.style,
|
|
1407
|
+
children: /* @__PURE__ */ jsx(
|
|
1408
|
+
Checkbox,
|
|
1409
|
+
{
|
|
1410
|
+
"aria-label": "Select all rows",
|
|
1411
|
+
checked: items.length > 0 && selected?.size === items.length,
|
|
1412
|
+
onCheckedChange: onToggleAll
|
|
1413
|
+
}
|
|
1414
|
+
)
|
|
1415
|
+
}
|
|
1206
1416
|
),
|
|
1207
|
-
|
|
1208
|
-
|
|
1417
|
+
activeColumns.map((col, colIndex) => {
|
|
1418
|
+
const sortIndex = sorts.findIndex((s) => s.key === col.key);
|
|
1419
|
+
const sortEntry = sortIndex >= 0 ? sorts[sortIndex] : null;
|
|
1420
|
+
const resizeWidth = colWidths[col.key];
|
|
1421
|
+
const pin = pinProps(col.key, true);
|
|
1422
|
+
const draggable = reorderableColumns && !pin;
|
|
1423
|
+
return /* @__PURE__ */ jsxs(
|
|
1424
|
+
TableHead,
|
|
1425
|
+
{
|
|
1426
|
+
className: cn(
|
|
1427
|
+
getCellClasses(col),
|
|
1428
|
+
headDensity,
|
|
1429
|
+
col.sortable && "select-none",
|
|
1430
|
+
"overflow-hidden",
|
|
1431
|
+
resizableColumns && "relative",
|
|
1432
|
+
pin?.className
|
|
1433
|
+
),
|
|
1434
|
+
style: {
|
|
1435
|
+
...resizeWidth ? { width: resizeWidth, minWidth: resizeWidth } : {},
|
|
1436
|
+
...pin?.style
|
|
1437
|
+
},
|
|
1438
|
+
draggable: draggable || void 0,
|
|
1439
|
+
onDragStart: draggable ? (e) => {
|
|
1440
|
+
setDragKey(col.key);
|
|
1441
|
+
e.dataTransfer.effectAllowed = "move";
|
|
1442
|
+
e.dataTransfer.setData("text/plain", col.key);
|
|
1443
|
+
} : void 0,
|
|
1444
|
+
onDragOver: draggable ? (e) => {
|
|
1445
|
+
if (dragKey && dragKey !== col.key) e.preventDefault();
|
|
1446
|
+
} : void 0,
|
|
1447
|
+
onDrop: draggable ? (e) => {
|
|
1448
|
+
e.preventDefault();
|
|
1449
|
+
if (dragKey && dragKey !== col.key) moveColumn(dragKey, { before: col.key });
|
|
1450
|
+
setDragKey(null);
|
|
1451
|
+
} : void 0,
|
|
1452
|
+
onDragEnd: draggable ? () => setDragKey(null) : void 0,
|
|
1453
|
+
"aria-sort": sortEntry && sortIndex === 0 ? sortEntry.dir === "asc" ? "ascending" : "descending" : void 0,
|
|
1454
|
+
children: [
|
|
1455
|
+
(() => {
|
|
1456
|
+
const label = /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-1", children: [
|
|
1457
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", title: col.header, children: col.header }),
|
|
1458
|
+
col.sortable && (sortEntry ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1459
|
+
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" }),
|
|
1460
|
+
sorts.length > 1 && /* @__PURE__ */ jsx(
|
|
1461
|
+
"span",
|
|
1462
|
+
{
|
|
1463
|
+
"aria-hidden": "true",
|
|
1464
|
+
className: "text-[10px] text-muted-foreground font-mono",
|
|
1465
|
+
children: sortIndex + 1
|
|
1466
|
+
}
|
|
1467
|
+
)
|
|
1468
|
+
] }) : /* @__PURE__ */ jsx(ArrowUpDown, { className: "h-3 w-3 shrink-0 text-muted-foreground/50" }))
|
|
1469
|
+
] });
|
|
1470
|
+
return col.sortable ? /* @__PURE__ */ jsx(
|
|
1471
|
+
"button",
|
|
1472
|
+
{
|
|
1473
|
+
type: "button",
|
|
1474
|
+
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",
|
|
1475
|
+
onClick: (e) => handleSort(col.key, e.shiftKey),
|
|
1476
|
+
onKeyDown: (e) => {
|
|
1477
|
+
if (!draggable || !e.altKey) return;
|
|
1478
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
1479
|
+
const movable = activeColumns.filter((c) => !pinned[c.key]);
|
|
1480
|
+
const i = movable.findIndex((c) => c.key === col.key);
|
|
1481
|
+
const neighbour = movable[e.key === "ArrowLeft" ? i - 1 : i + 1];
|
|
1482
|
+
if (!neighbour) return;
|
|
1483
|
+
e.preventDefault();
|
|
1484
|
+
moveColumn(col.key, e.key === "ArrowLeft" ? { before: neighbour.key } : { after: neighbour.key });
|
|
1485
|
+
},
|
|
1486
|
+
"aria-label": sortEntry && sortIndex > 0 ? `${col.header}, sorted ${sortEntry.dir === "asc" ? "ascending" : "descending"}, sort ${sortIndex + 1}` : void 0,
|
|
1487
|
+
children: label
|
|
1488
|
+
}
|
|
1489
|
+
) : label;
|
|
1490
|
+
})(),
|
|
1491
|
+
resizableColumns && /* @__PURE__ */ jsx(
|
|
1492
|
+
ResizeHandle,
|
|
1493
|
+
{
|
|
1494
|
+
onResize: (delta) => handleResize(
|
|
1495
|
+
col.key,
|
|
1496
|
+
delta,
|
|
1497
|
+
col.minWidth ?? 60
|
|
1498
|
+
),
|
|
1499
|
+
onResizeEnd: handleResizeEnd
|
|
1500
|
+
}
|
|
1501
|
+
)
|
|
1502
|
+
]
|
|
1503
|
+
},
|
|
1504
|
+
col.key
|
|
1505
|
+
);
|
|
1506
|
+
}),
|
|
1507
|
+
renderActions && /* @__PURE__ */ jsx(
|
|
1508
|
+
TableHead,
|
|
1209
1509
|
{
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
value
|
|
1510
|
+
className: cn("w-10", headDensity, pinProps("__actions", true)?.className),
|
|
1511
|
+
style: pinProps("__actions", true)?.style
|
|
1213
1512
|
}
|
|
1214
|
-
)
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
renderActions && /* @__PURE__ */ jsx(
|
|
1513
|
+
)
|
|
1514
|
+
] })
|
|
1515
|
+
}
|
|
1516
|
+
),
|
|
1517
|
+
/* @__PURE__ */ jsx(TableBody, { ref: bodyRef, children: displayItems.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
|
|
1220
1518
|
TableCell,
|
|
1221
1519
|
{
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
children:
|
|
1520
|
+
colSpan: totalCols,
|
|
1521
|
+
className: "text-center py-12 text-muted-foreground",
|
|
1522
|
+
children: emptyMessage
|
|
1225
1523
|
}
|
|
1226
|
-
)
|
|
1524
|
+
) }) : virtualRows ? [
|
|
1525
|
+
/* @__PURE__ */ jsx("tr", { "aria-hidden": true, style: { height: virtualRows[0]?.start ?? 0 } }, "__top"),
|
|
1526
|
+
...virtualRows.map(
|
|
1527
|
+
(vr) => renderRow(displayItems[vr.index], vr.index, rowVirtualizer.measureElement)
|
|
1528
|
+
),
|
|
1529
|
+
/* @__PURE__ */ jsx(
|
|
1530
|
+
"tr",
|
|
1531
|
+
{
|
|
1532
|
+
"aria-hidden": true,
|
|
1533
|
+
style: {
|
|
1534
|
+
height: rowVirtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0)
|
|
1535
|
+
}
|
|
1536
|
+
},
|
|
1537
|
+
"__bottom"
|
|
1538
|
+
)
|
|
1539
|
+
] : displayItems.map((item, index) => renderRow(item, index)) })
|
|
1227
1540
|
]
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
] }) }),
|
|
1541
|
+
}
|
|
1542
|
+
)
|
|
1543
|
+
}
|
|
1544
|
+
),
|
|
1233
1545
|
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
1546
|
/* @__PURE__ */ jsx("span", { children: totalItems === 0 ? "No records" : `Showing ${showingFrom}\u2013${showingTo} of ${totalItems}` }),
|
|
1235
1547
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
@@ -1810,6 +2122,39 @@ function DetailPanel({
|
|
|
1810
2122
|
}
|
|
1811
2123
|
);
|
|
1812
2124
|
}
|
|
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
|
+
}
|
|
1813
2158
|
function RelationField({
|
|
1814
2159
|
field,
|
|
1815
2160
|
value,
|
|
@@ -2713,7 +3058,12 @@ function ResourceForm(props) {
|
|
|
2713
3058
|
steps,
|
|
2714
3059
|
columns = 1,
|
|
2715
3060
|
prefillFromUrl,
|
|
2716
|
-
autoPlaceholder = false
|
|
3061
|
+
autoPlaceholder = false,
|
|
3062
|
+
warnWhenUnsaved = false,
|
|
3063
|
+
onDirtyChange,
|
|
3064
|
+
mutationMode = "pessimistic",
|
|
3065
|
+
undoDelayMs,
|
|
3066
|
+
onUndoable
|
|
2717
3067
|
} = props;
|
|
2718
3068
|
const isMobile = useIsMobile();
|
|
2719
3069
|
const effectiveLayout = isMobile ? "sheet" : layoutProp;
|
|
@@ -2731,11 +3081,31 @@ function ResourceForm(props) {
|
|
|
2731
3081
|
onOpenChange(false);
|
|
2732
3082
|
onSuccess?.(data);
|
|
2733
3083
|
},
|
|
2734
|
-
|
|
3084
|
+
// The form is closed by the time an optimistic or undoable save fails,
|
|
3085
|
+
// so the error has to reach the user some other way.
|
|
3086
|
+
onError: onError ?? (mutationMode === "pessimistic" ? void 0 : showWriteErrorToast),
|
|
2735
3087
|
prefillFromUrl,
|
|
2736
|
-
autoPlaceholder
|
|
3088
|
+
autoPlaceholder,
|
|
3089
|
+
warnWhenUnsaved,
|
|
3090
|
+
mutationMode,
|
|
3091
|
+
undoDelayMs,
|
|
3092
|
+
onUndoable: onUndoable ?? showUndoToast
|
|
2737
3093
|
};
|
|
2738
|
-
const { form, resolvedFields, handleSave, isSaving, isLoading } = useResourceForm(hookOptions);
|
|
3094
|
+
const { form, resolvedFields, handleSave, isSaving, isLoading, isDirty, confirmLeave } = useResourceForm(hookOptions);
|
|
3095
|
+
const onDirtyChangeRef = useRef(onDirtyChange);
|
|
3096
|
+
useEffect(() => {
|
|
3097
|
+
onDirtyChangeRef.current = onDirtyChange;
|
|
3098
|
+
});
|
|
3099
|
+
useEffect(() => {
|
|
3100
|
+
onDirtyChangeRef.current?.(isDirty, confirmLeave);
|
|
3101
|
+
}, [isDirty, confirmLeave]);
|
|
3102
|
+
const requestOpenChange = useCallback(
|
|
3103
|
+
(next) => {
|
|
3104
|
+
if (!next && !confirmLeave()) return;
|
|
3105
|
+
onOpenChange(next);
|
|
3106
|
+
},
|
|
3107
|
+
[confirmLeave, onOpenChange]
|
|
3108
|
+
);
|
|
2739
3109
|
const bodyProps = {
|
|
2740
3110
|
form,
|
|
2741
3111
|
resolvedFields,
|
|
@@ -2743,14 +3113,14 @@ function ResourceForm(props) {
|
|
|
2743
3113
|
action,
|
|
2744
3114
|
isSaving,
|
|
2745
3115
|
isLoading,
|
|
2746
|
-
onOpenChange,
|
|
3116
|
+
onOpenChange: requestOpenChange,
|
|
2747
3117
|
handleSave,
|
|
2748
3118
|
mode,
|
|
2749
3119
|
steps,
|
|
2750
3120
|
columns
|
|
2751
3121
|
};
|
|
2752
3122
|
if (effectiveLayout === "dialog") {
|
|
2753
|
-
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-[560px]", children: [
|
|
3123
|
+
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: requestOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-[560px]", children: [
|
|
2754
3124
|
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
2755
3125
|
/* @__PURE__ */ jsx(DialogTitle, { children: formTitle }),
|
|
2756
3126
|
/* @__PURE__ */ jsx(DialogDescription, { className: "sr-only", children: formDescription })
|
|
@@ -2759,7 +3129,7 @@ function ResourceForm(props) {
|
|
|
2759
3129
|
] }) });
|
|
2760
3130
|
}
|
|
2761
3131
|
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: [
|
|
3132
|
+
return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange: requestOpenChange, children: /* @__PURE__ */ jsxs(SheetContent, { side: "right", className: "sm:max-w-[480px] overflow-y-auto", children: [
|
|
2763
3133
|
/* @__PURE__ */ jsxs(SheetHeader, { children: [
|
|
2764
3134
|
/* @__PURE__ */ jsx(SheetTitle, { children: formTitle }),
|
|
2765
3135
|
/* @__PURE__ */ jsx(SheetDescription, { className: "sr-only", children: formDescription })
|
|
@@ -2778,7 +3148,8 @@ function ConfirmDeleteDialog({
|
|
|
2778
3148
|
onOpenChange,
|
|
2779
3149
|
onConfirm,
|
|
2780
3150
|
title = "Delete this record?",
|
|
2781
|
-
|
|
3151
|
+
kind = "permanent",
|
|
3152
|
+
description = kind === "recycle" ? "It moves to the recycle bin, where it can be restored." : "This can't be undone.",
|
|
2782
3153
|
confirmLabel = "Delete",
|
|
2783
3154
|
cancelLabel = "Cancel"
|
|
2784
3155
|
}) {
|
|
@@ -2896,6 +3267,7 @@ function ResourcePage(props) {
|
|
|
2896
3267
|
onCreate,
|
|
2897
3268
|
onEdit,
|
|
2898
3269
|
onDelete,
|
|
3270
|
+
can,
|
|
2899
3271
|
canEditItem,
|
|
2900
3272
|
createLabel = "New",
|
|
2901
3273
|
canEdit,
|
|
@@ -2911,6 +3283,7 @@ function ResourcePage(props) {
|
|
|
2911
3283
|
formMode,
|
|
2912
3284
|
formSteps,
|
|
2913
3285
|
formLayout: formLayoutProp,
|
|
3286
|
+
mutationMode: mutationModeProp,
|
|
2914
3287
|
bulkActions,
|
|
2915
3288
|
renderCard: renderCardProp,
|
|
2916
3289
|
openOnCardClick = false,
|
|
@@ -2925,6 +3298,7 @@ function ResourcePage(props) {
|
|
|
2925
3298
|
emptyDescription,
|
|
2926
3299
|
header: headerMode = "page",
|
|
2927
3300
|
resourceOptions,
|
|
3301
|
+
urlState: urlStateProp,
|
|
2928
3302
|
defaultView = "table",
|
|
2929
3303
|
onNavigate = defaultNavigate,
|
|
2930
3304
|
viewSettingsKey: viewSettingsKeyProp,
|
|
@@ -2934,6 +3308,18 @@ function ResourcePage(props) {
|
|
|
2934
3308
|
} = props;
|
|
2935
3309
|
const isClientMode = !!resourceName;
|
|
2936
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}` : "";
|
|
2937
3323
|
const isServerControlled = resourceServerPaging || pageProp !== void 0 || perPageProp !== void 0 || onPageChange !== void 0 || onPerPageChange !== void 0 || onSortChange !== void 0 || onSearchChange !== void 0;
|
|
2938
3324
|
const viewSettingsKey = viewSettingsKeyProp ?? resourceName ?? title.toLowerCase().replace(/\s+/g, "-");
|
|
2939
3325
|
const { settings, updateSetting } = useViewSettings(viewSettingsKey);
|
|
@@ -2949,7 +3335,15 @@ function ResourcePage(props) {
|
|
|
2949
3335
|
searchFields: resourceServerPaging ? serverSearchFields : resourceOptions?.searchFields,
|
|
2950
3336
|
// Paging, searching and sorting change the query key; keep the
|
|
2951
3337
|
// current page on screen while the next one loads.
|
|
2952
|
-
keepPreviousData: resourceOptions?.keepPreviousData ?? resourceServerPaging
|
|
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)
|
|
2953
3347
|
} : { enabled: false }
|
|
2954
3348
|
);
|
|
2955
3349
|
const effectivePageSize = resourceServerPaging ? resourceHook.params.pagination?.perPage ?? settings.pageSize : perPageProp ?? settings.pageSize;
|
|
@@ -2964,9 +3358,15 @@ function ResourcePage(props) {
|
|
|
2964
3358
|
columns: columnsProp,
|
|
2965
3359
|
statusFilter,
|
|
2966
3360
|
searchFields,
|
|
2967
|
-
initialFilters
|
|
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),
|
|
2968
3365
|
onFiltersChange,
|
|
2969
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,
|
|
2970
3370
|
detail,
|
|
2971
3371
|
titleField: titleFieldProp,
|
|
2972
3372
|
subtitleField: subtitleFieldProp,
|
|
@@ -2977,7 +3377,7 @@ function ResourcePage(props) {
|
|
|
2977
3377
|
setSort: setResourceSort,
|
|
2978
3378
|
setFilters: setResourceFilters
|
|
2979
3379
|
} = resourceHook;
|
|
2980
|
-
const pushedSearchRef = useRef("");
|
|
3380
|
+
const pushedSearchRef = useRef(resourceServerPaging ? urlSeed?.q ?? "" : "");
|
|
2981
3381
|
const searchEffectRanRef = useRef(false);
|
|
2982
3382
|
useEffect(() => {
|
|
2983
3383
|
if (!resourceServerPaging) return;
|
|
@@ -2996,7 +3396,7 @@ function ResourcePage(props) {
|
|
|
2996
3396
|
return () => clearTimeout(timer);
|
|
2997
3397
|
}, [resourceServerPaging, page.search, setResourceSearch]);
|
|
2998
3398
|
const defaultSort = resourceOptions?.defaultSort;
|
|
2999
|
-
const pushedSortRef = useRef("");
|
|
3399
|
+
const pushedSortRef = useRef(resourceServerPaging ? urlSortKey : "");
|
|
3000
3400
|
useEffect(() => {
|
|
3001
3401
|
if (!resourceServerPaging) return;
|
|
3002
3402
|
const key = page.sort ? `${page.sort.field}:${page.sort.order}` : "";
|
|
@@ -3064,8 +3464,17 @@ function ResourcePage(props) {
|
|
|
3064
3464
|
(item) => getKeyProp ? getKeyProp(item) : String(item.id ?? ""),
|
|
3065
3465
|
[getKeyProp]
|
|
3066
3466
|
);
|
|
3067
|
-
const
|
|
3068
|
-
const
|
|
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;
|
|
3069
3478
|
const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
|
|
3070
3479
|
const handleToggleSelect = useCallback((id) => {
|
|
3071
3480
|
setSelectedIds((prev) => {
|
|
@@ -3098,10 +3507,20 @@ function ResourcePage(props) {
|
|
|
3098
3507
|
onCreate();
|
|
3099
3508
|
}
|
|
3100
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
|
+
);
|
|
3101
3521
|
const handleEdit = useCallback(
|
|
3102
3522
|
(item) => {
|
|
3103
|
-
if (
|
|
3104
|
-
if (canEditItem && !canEditItem(item)) return;
|
|
3523
|
+
if (!allows("edit", item)) return;
|
|
3105
3524
|
if (useBuiltInEdit) {
|
|
3106
3525
|
setFormAction("edit");
|
|
3107
3526
|
setFormItem(item);
|
|
@@ -3110,18 +3529,23 @@ function ResourcePage(props) {
|
|
|
3110
3529
|
onEdit(item);
|
|
3111
3530
|
}
|
|
3112
3531
|
},
|
|
3113
|
-
[useBuiltInEdit, onEdit,
|
|
3532
|
+
[useBuiltInEdit, onEdit, allows]
|
|
3114
3533
|
);
|
|
3115
3534
|
const closeDetail = useCallback(() => {
|
|
3116
3535
|
page.setDetailOpen(false);
|
|
3117
3536
|
page.setSelectedItem(null);
|
|
3118
|
-
|
|
3537
|
+
urlPeek?.setDetailId(null);
|
|
3538
|
+
}, [page, urlPeek]);
|
|
3119
3539
|
const useBuiltInDelete = !!allowDelete && !onDelete && isClientMode;
|
|
3120
3540
|
const deleteConfirmed = confirmDelete === void 0 ? useBuiltInDelete : !!confirmDelete;
|
|
3121
3541
|
const deleteCopy = typeof confirmDelete === "object" ? confirmDelete : void 0;
|
|
3122
3542
|
const [pendingDelete, setPendingDelete] = useState(null);
|
|
3123
3543
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
3124
3544
|
const { remove: removeRecord } = resourceHook;
|
|
3545
|
+
const { data: providerCaps } = useCapabilities(resourceName, {
|
|
3546
|
+
enabled: useBuiltInDelete && deleteConfirmed
|
|
3547
|
+
});
|
|
3548
|
+
const deleteKind = providerCaps?.softDelete ? "recycle" : "permanent";
|
|
3125
3549
|
const performDelete = useCallback(
|
|
3126
3550
|
async (item) => {
|
|
3127
3551
|
if (useBuiltInDelete) {
|
|
@@ -3149,24 +3573,39 @@ function ResourcePage(props) {
|
|
|
3149
3573
|
setDeleteOpen(true);
|
|
3150
3574
|
} : useBuiltInDelete ? (item) => {
|
|
3151
3575
|
performDelete(item).catch((err) => {
|
|
3152
|
-
|
|
3576
|
+
if (!resourceOptions?.onError) {
|
|
3577
|
+
toast.error(err instanceof Error ? err.message : "The delete failed.");
|
|
3578
|
+
}
|
|
3153
3579
|
});
|
|
3154
3580
|
} : (
|
|
3155
3581
|
// Unconfirmed `onDelete` keeps its original contract exactly:
|
|
3156
3582
|
// called straight from the peek, nothing closed for it.
|
|
3157
3583
|
(item) => {
|
|
3158
|
-
|
|
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
|
+
}
|
|
3159
3596
|
}
|
|
3160
3597
|
);
|
|
3161
|
-
const
|
|
3162
|
-
const
|
|
3163
|
-
const selectedDeletable = !!selectedItem && mayChange(selectedItem) && (canDelete ? canDelete(selectedItem) : true);
|
|
3598
|
+
const selectedEditable = !!selectedItem && allows("edit", selectedItem);
|
|
3599
|
+
const selectedDeletable = !!selectedItem && allows("delete", selectedItem);
|
|
3164
3600
|
const tableColumns = useMemo(() => {
|
|
3165
3601
|
const base = isClientMode ? page.columns.map((col) => ({
|
|
3166
3602
|
key: col.key,
|
|
3167
3603
|
header: col.header ?? col.key,
|
|
3168
3604
|
sortable: col.sortable ?? true,
|
|
3169
3605
|
hideBelow: col.hideBelow,
|
|
3606
|
+
width: col.width,
|
|
3607
|
+
minWidth: col.minWidth,
|
|
3608
|
+
defaultVisible: col.defaultVisible,
|
|
3170
3609
|
editable: col.editable,
|
|
3171
3610
|
accessor: col.accessor,
|
|
3172
3611
|
render: col.render ? (item, value) => col.render(item, value, actions) : void 0
|
|
@@ -3180,11 +3619,36 @@ function ResourcePage(props) {
|
|
|
3180
3619
|
page.tableColumns,
|
|
3181
3620
|
actions
|
|
3182
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
|
+
);
|
|
3183
3639
|
const pagerPage = resourceServerPaging ? resourceHook.params.pagination?.page ?? 1 : pageProp;
|
|
3184
3640
|
const pagerPerPage = resourceServerPaging ? resourceHook.params.pagination?.perPage : perPageProp;
|
|
3185
3641
|
const pagerTotal = resourceServerPaging ? displayTotal : totalProp;
|
|
3186
3642
|
const pagerOnPageChange = resourceServerPaging ? resourceHook.setPage : onPageChange;
|
|
3187
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]);
|
|
3188
3652
|
const handleSearchChange = useCallback(
|
|
3189
3653
|
(value) => {
|
|
3190
3654
|
page.filterConfig.onSearchChange(value);
|
|
@@ -3311,9 +3775,10 @@ function ResourcePage(props) {
|
|
|
3311
3775
|
return;
|
|
3312
3776
|
}
|
|
3313
3777
|
page.handleRowClick(item);
|
|
3778
|
+
urlPeek?.setDetailId(keyOf(item));
|
|
3314
3779
|
};
|
|
3315
3780
|
const hasBulkSelect = !!bulkActions && bulkActions.length > 0;
|
|
3316
|
-
const overlayOpen =
|
|
3781
|
+
const overlayOpen = detailOpen || formOpen || deleteOpen;
|
|
3317
3782
|
const keyboardNav = useKeyboardNavigation({
|
|
3318
3783
|
itemCount: page.data.length,
|
|
3319
3784
|
enabled: keyboardNavigation && page.viewMode !== "table",
|
|
@@ -3334,12 +3799,9 @@ function ResourcePage(props) {
|
|
|
3334
3799
|
// every grid/list page (including detail="none" and detail="route",
|
|
3335
3800
|
// which never open one). `detailOpen` is only ever true for the panel
|
|
3336
3801
|
// and modal modes, so it is the precise precondition here.
|
|
3337
|
-
onBack:
|
|
3338
|
-
page.setDetailOpen(false);
|
|
3339
|
-
page.setSelectedItem(null);
|
|
3340
|
-
} : void 0
|
|
3802
|
+
onBack: detailOpen ? closeDetail : void 0
|
|
3341
3803
|
});
|
|
3342
|
-
const showCreateButton = onCreate || useBuiltInCreate;
|
|
3804
|
+
const showCreateButton = (onCreate || useBuiltInCreate) && allows("create");
|
|
3343
3805
|
const createButton = showCreateButton ? /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: handleCreate, children: [
|
|
3344
3806
|
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4 mr-1" }),
|
|
3345
3807
|
createLabel
|
|
@@ -3439,7 +3901,8 @@ function ResourcePage(props) {
|
|
|
3439
3901
|
isLoading,
|
|
3440
3902
|
hasError,
|
|
3441
3903
|
isEmpty,
|
|
3442
|
-
errorMessage: errorMessage2
|
|
3904
|
+
errorMessage: errorMessage2 ?? // A provider's read error carries a sentence users can be shown.
|
|
3905
|
+
(isClientMode && isDataReadError(resourceHook.list.error) ? resourceHook.list.error.message : void 0),
|
|
3443
3906
|
onRetry: isClientMode ? () => resourceHook.list.refetch() : onRetryProp,
|
|
3444
3907
|
viewMode: page.viewMode,
|
|
3445
3908
|
onViewModeChange: page.setViewMode,
|
|
@@ -3534,9 +3997,13 @@ function ResourcePage(props) {
|
|
|
3534
3997
|
resizeKey,
|
|
3535
3998
|
stickyHeader,
|
|
3536
3999
|
stickyFirstColumn,
|
|
4000
|
+
keyboardNavigation,
|
|
3537
4001
|
// The server already cut the page window; DataTable's own
|
|
3538
4002
|
// pagination would slice the slice.
|
|
3539
|
-
pagination: isServerControlled ? false : void 0
|
|
4003
|
+
pagination: isServerControlled ? false : void 0,
|
|
4004
|
+
// Client mode: the header sort is the page's sort, so it
|
|
4005
|
+
// reaches the URL and a link restores what the table shows.
|
|
4006
|
+
...isServerControlled ? {} : { sort: tableSort, onSortChange: handleTableSort }
|
|
3540
4007
|
},
|
|
3541
4008
|
slotProps?.table
|
|
3542
4009
|
),
|
|
@@ -3656,9 +4123,18 @@ function ResourcePage(props) {
|
|
|
3656
4123
|
(detail === "panel" || detail === "modal") && renderSlot(
|
|
3657
4124
|
DetailPanel,
|
|
3658
4125
|
slots?.detail,
|
|
3659
|
-
{
|
|
4126
|
+
linkMissing ? {
|
|
4127
|
+
// A linked record this user can't read, or that's gone.
|
|
4128
|
+
item: {},
|
|
4129
|
+
open: detailOpen,
|
|
4130
|
+
onClose: closeDetail,
|
|
4131
|
+
title: "Record not found",
|
|
4132
|
+
width: detailWidth,
|
|
4133
|
+
layout: detail === "modal" ? "dialog" : "sheet",
|
|
4134
|
+
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." })
|
|
4135
|
+
} : {
|
|
3660
4136
|
item: selectedItem,
|
|
3661
|
-
open:
|
|
4137
|
+
open: detailOpen,
|
|
3662
4138
|
onClose: closeDetail,
|
|
3663
4139
|
title: detailTitle,
|
|
3664
4140
|
subtitle: detailSubtitle,
|
|
@@ -3680,6 +4156,7 @@ function ResourcePage(props) {
|
|
|
3680
4156
|
layout: formLayoutProp ?? (formAction === "edit" ? "sheet" : "dialog"),
|
|
3681
4157
|
mode: formMode,
|
|
3682
4158
|
steps: formSteps,
|
|
4159
|
+
mutationMode: mutationModeProp,
|
|
3683
4160
|
item: formAction === "edit" ? formItem ?? void 0 : page.data.length > 0 ? Object.fromEntries(
|
|
3684
4161
|
Object.keys(page.data[0]).filter((k) => k !== "id").map((k) => [k, ""])
|
|
3685
4162
|
) : void 0,
|
|
@@ -3701,6 +4178,7 @@ function ResourcePage(props) {
|
|
|
3701
4178
|
open: deleteOpen,
|
|
3702
4179
|
onOpenChange: setDeleteOpen,
|
|
3703
4180
|
onConfirm: () => pendingDelete ? performDelete(pendingDelete) : void 0,
|
|
4181
|
+
kind: deleteKind,
|
|
3704
4182
|
title: typeof deleteCopy?.title === "function" ? pendingDelete ? deleteCopy.title(pendingDelete) : void 0 : deleteCopy?.title,
|
|
3705
4183
|
description: typeof deleteCopy?.description === "function" ? pendingDelete ? deleteCopy.description(pendingDelete) : void 0 : deleteCopy?.description,
|
|
3706
4184
|
confirmLabel: deleteCopy?.confirmLabel,
|