@marktiderman/genesis-ui 2.7.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/EXTENDING.md +40 -1
- package/SKILL.md +198 -9
- package/dist/{AppShell-CX0M-dfc.d.ts → AppShell-LXQykEqe.d.ts} +47 -5
- package/dist/{chunk-M62CHO3N.js → chunk-4C4ESW6O.js} +9 -4
- package/dist/chunk-4C4ESW6O.js.map +1 -0
- package/dist/chunk-DIV4BYXX.js +58 -0
- package/dist/chunk-DIV4BYXX.js.map +1 -0
- package/dist/chunk-FUVWKTPL.js +19 -0
- package/dist/chunk-FUVWKTPL.js.map +1 -0
- package/dist/{chunk-PLIYF6KI.js → chunk-MPI33I4K.js} +2 -2
- package/dist/{chunk-PLIYF6KI.js.map → chunk-MPI33I4K.js.map} +1 -1
- package/dist/{chunk-JA3ZKULS.js → chunk-OITWCIG2.js} +3 -3
- package/dist/chunk-OITWCIG2.js.map +1 -0
- package/dist/chunk-PKFDGGZV.js +65 -0
- package/dist/chunk-PKFDGGZV.js.map +1 -0
- package/dist/{chunk-IXGAEECT.js → chunk-RKPS7ERS.js} +9 -5
- package/dist/chunk-RKPS7ERS.js.map +1 -0
- package/dist/{chunk-TXOZMKNW.js → chunk-S7NXD37O.js} +27 -26
- package/dist/chunk-S7NXD37O.js.map +1 -0
- package/dist/{chunk-ZPPLUGLA.js → chunk-UYI3GLFJ.js} +162 -19
- package/dist/chunk-UYI3GLFJ.js.map +1 -0
- package/dist/{chunk-CDYZRZ6U.js → chunk-WGMEVX3Y.js} +2 -2
- package/dist/chunk-WGMEVX3Y.js.map +1 -0
- package/dist/chunk-ZDHWNQTR.js +552 -0
- package/dist/chunk-ZDHWNQTR.js.map +1 -0
- package/dist/components/data/index.d.ts +306 -19
- package/dist/components/data/index.js +1177 -284
- package/dist/components/data/index.js.map +1 -1
- package/dist/components/layout/form-page.d.ts +1 -1
- package/dist/components/layout/form-page.js +1 -1
- package/dist/components/layout/index.d.ts +2 -2
- package/dist/components/layout/index.js +8 -8
- package/dist/components/patterns/index.d.ts +1 -1
- package/dist/components/patterns/index.js +5 -5
- package/dist/components/patterns/sortable-list.js +1 -1
- package/dist/components/rich-text/rich-text-editor.d.ts +194 -0
- package/dist/components/rich-text/rich-text-editor.js +833 -0
- package/dist/components/rich-text/rich-text-editor.js.map +1 -0
- package/dist/components/rich-text/rich-text.d.ts +51 -0
- package/dist/components/rich-text/rich-text.js +4 -0
- package/dist/components/rich-text/rich-text.js.map +1 -0
- package/dist/components/ui/alert-dialog.d.ts +12 -1
- package/dist/components/ui/alert-dialog.js +1 -1
- package/dist/hooks/index.d.ts +4 -3
- package/dist/hooks/index.js +5 -4
- package/dist/hooks/use-data-filters.d.ts +5 -3
- package/dist/hooks/use-data-filters.js +2 -2
- package/dist/hooks/use-resource-page.d.ts +2 -1
- package/dist/hooks/use-resource-page.js +3 -2
- 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/hooks/use-view-preference.d.ts +17 -2
- package/dist/hooks/use-view-preference.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +32 -32
- package/dist/{navigation-DBwcZ6vb.d.ts → navigation-Bha0R3Fu.d.ts} +6 -3
- package/dist/rich-text-doc-C54fME2C.d.ts +180 -0
- package/dist/router/react-router.d.ts +47 -0
- package/dist/router/react-router.js +53 -0
- package/dist/router/react-router.js.map +1 -0
- package/dist/router/tanstack-router.d.ts +47 -0
- package/dist/router/tanstack-router.js +105 -0
- package/dist/router/tanstack-router.js.map +1 -0
- package/dist/{use-resource-page-Dd2VUv30.d.ts → use-resource-page-BDnnCssE.d.ts} +83 -5
- package/package.json +61 -14
- package/dist/chunk-CDYZRZ6U.js.map +0 -1
- package/dist/chunk-FTTNGGLA.js +0 -20
- package/dist/chunk-FTTNGGLA.js.map +0 -1
- package/dist/chunk-IXGAEECT.js.map +0 -1
- package/dist/chunk-JA3ZKULS.js.map +0 -1
- package/dist/chunk-M62CHO3N.js.map +0 -1
- package/dist/chunk-TXOZMKNW.js.map +0 -1
- package/dist/chunk-ZPPLUGLA.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { AppShell, PageHeader } from '../../chunk-
|
|
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
5
|
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '../../chunk-N2ZFNOOT.js';
|
|
@@ -7,45 +7,50 @@ import { Textarea } from '../../chunk-YETMISCE.js';
|
|
|
7
7
|
import { Separator } from '../../chunk-SXSCHWZU.js';
|
|
8
8
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '../../chunk-XCGZPFT7.js';
|
|
9
9
|
import { Skeleton } from '../../chunk-QFM4Z2SS.js';
|
|
10
|
+
import { toast } from '../../chunk-4RAZJ6LL.js';
|
|
10
11
|
import { Progress } from '../../chunk-W5Y3LG5K.js';
|
|
11
|
-
import { Input } from '../../chunk-QSXVL5UB.js';
|
|
12
|
-
import { Card, CardContent, CardHeader, CardTitle } from '../../chunk-FN3FZVUR.js';
|
|
13
12
|
import { Checkbox } from '../../chunk-SEBU6JCK.js';
|
|
13
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../chunk-FN3FZVUR.js';
|
|
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';
|
|
17
|
+
import { Input } from '../../chunk-QSXVL5UB.js';
|
|
14
18
|
import '../../chunk-B2GIBUQD.js';
|
|
15
19
|
import { Badge } from '../../chunk-NPNADZHM.js';
|
|
16
20
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../../chunk-FK42ZGGV.js';
|
|
17
|
-
import { Popover, PopoverTrigger, PopoverContent } from '../../chunk-22XB3Q6H.js';
|
|
18
21
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuSeparator, DropdownMenuItem, DropdownMenuLabel } from '../../chunk-QJ6P2DW6.js';
|
|
19
22
|
import { Switch } from '../../chunk-27ELYTGE.js';
|
|
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';
|
|
20
27
|
import { EmptyState } from '../../chunk-65SPT7EA.js';
|
|
21
28
|
export { EmptyState } from '../../chunk-65SPT7EA.js';
|
|
22
29
|
import { Button } from '../../chunk-NWSCYLPT.js';
|
|
23
|
-
import { PageLoading } from '../../chunk-YRYQXU3G.js';
|
|
24
|
-
import { Spinner } from '../../chunk-OVU3Z2MT.js';
|
|
25
30
|
import { FormField, FormLabel, FormError, FormDescription } from '../../chunk-R7QFP6EL.js';
|
|
26
31
|
import '../../chunk-F47U5KNW.js';
|
|
27
|
-
import {
|
|
28
|
-
import '../../chunk-
|
|
29
|
-
import '../../chunk-AVFZ4P4B.js';
|
|
30
|
-
import { titleCase, useResourcePage } from '../../chunk-IXGAEECT.js';
|
|
32
|
+
import { titleCase, useResourcePage } from '../../chunk-RKPS7ERS.js';
|
|
33
|
+
import { useResourceUrlState } from '../../chunk-DIV4BYXX.js';
|
|
31
34
|
export { useSavedViews } from '../../chunk-GCQFVMPI.js';
|
|
32
|
-
import '../../chunk-
|
|
35
|
+
import '../../chunk-PKFDGGZV.js';
|
|
33
36
|
import { useViewSettings } from '../../chunk-OZ7ZAYCS.js';
|
|
34
37
|
export { CommandPalette, useCommandPaletteHotkey } from '../../chunk-CNTWQSUT.js';
|
|
35
38
|
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator } from '../../chunk-SEFK4NZV.js';
|
|
36
39
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '../../chunk-7XMZVISM.js';
|
|
37
40
|
import { cn } from '../../chunk-NZ5WBA4R.js';
|
|
38
|
-
import { DefaultLink, defaultNavigate } from '../../chunk-
|
|
39
|
-
export { DefaultLink, defaultNavigate, isPathActive, resolveNavigation } from '../../chunk-
|
|
41
|
+
import { DefaultLink, defaultNavigate } from '../../chunk-WGMEVX3Y.js';
|
|
42
|
+
export { DefaultLink, defaultNavigate, isPathActive, resolveNavigation } from '../../chunk-WGMEVX3Y.js';
|
|
40
43
|
import { useKeyboardNavigation } from '../../chunk-YJ6MPS6U.js';
|
|
41
44
|
import { useIsMobile } from '../../chunk-VFO3I7PV.js';
|
|
42
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';
|
|
43
46
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
44
|
-
import { useState, useRef, useEffect, useCallback, useMemo, Fragment as Fragment$1 } from 'react';
|
|
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';
|
|
45
50
|
import { formatDistanceToNow, format } from 'date-fns';
|
|
46
51
|
import { useFieldArray, Controller, useWatch } from 'react-hook-form';
|
|
47
|
-
import { useResource, useWizard, useResourceForm, useOne, buildArrayItemDefaults } from '@marktiderman/genesis-core/hooks';
|
|
48
|
-
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';
|
|
49
54
|
import { useDropzone } from 'react-dropzone';
|
|
50
55
|
|
|
51
56
|
function DataPageShell({
|
|
@@ -59,6 +64,7 @@ function DataPageShell({
|
|
|
59
64
|
isEmpty = false,
|
|
60
65
|
emptyIcon,
|
|
61
66
|
emptyMessage,
|
|
67
|
+
emptyDescription,
|
|
62
68
|
emptyAction,
|
|
63
69
|
hasError = false,
|
|
64
70
|
errorMessage: errorMessage2,
|
|
@@ -89,11 +95,13 @@ function DataPageShell({
|
|
|
89
95
|
}) {
|
|
90
96
|
const entity = entityName || title.toLowerCase();
|
|
91
97
|
const collapseFilters = filtersCollapsible && filters != null && header !== null;
|
|
98
|
+
const toolbarOnly = header === "toolbar";
|
|
99
|
+
const baseHeaderOptions = toolbarOnly ? { showTitle: false, ...headerOptions } : headerOptions;
|
|
92
100
|
const mergedOptions = {
|
|
93
101
|
defaultDetailsOpen: defaultFiltersOpen,
|
|
94
102
|
detailsLabel: filtersLabel,
|
|
95
103
|
subtitleInDetails: collapseFilters ? true : void 0,
|
|
96
|
-
...
|
|
104
|
+
...baseHeaderOptions
|
|
97
105
|
};
|
|
98
106
|
const sharedHeaderProps = {
|
|
99
107
|
title,
|
|
@@ -114,7 +122,7 @@ function DataPageShell({
|
|
|
114
122
|
stats,
|
|
115
123
|
optionsStorageKey: headerOptionsStorageKey
|
|
116
124
|
};
|
|
117
|
-
const Header = header === null ? (
|
|
125
|
+
const Header = header === null ? () => null : header === "toolbar" || header === void 0 ? PageHeader : header;
|
|
118
126
|
return /* @__PURE__ */ jsxs("div", { className: cn("space-y-6", className), children: [
|
|
119
127
|
collapseFilters ? /* @__PURE__ */ jsx(
|
|
120
128
|
Header,
|
|
@@ -128,7 +136,7 @@ function DataPageShell({
|
|
|
128
136
|
options: mergedOptions
|
|
129
137
|
}
|
|
130
138
|
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
131
|
-
/* @__PURE__ */ jsx(Header, { ...sharedHeaderProps, options:
|
|
139
|
+
/* @__PURE__ */ jsx(Header, { ...sharedHeaderProps, options: baseHeaderOptions }),
|
|
132
140
|
filters
|
|
133
141
|
] }),
|
|
134
142
|
isLoading ? /* @__PURE__ */ jsx(
|
|
@@ -158,6 +166,7 @@ function DataPageShell({
|
|
|
158
166
|
{
|
|
159
167
|
icon: emptyIcon,
|
|
160
168
|
title: emptyMessage,
|
|
169
|
+
...emptyDescription ? { description: emptyDescription } : {},
|
|
161
170
|
action: emptyAction,
|
|
162
171
|
hasFilters: hasActiveFilters,
|
|
163
172
|
onClearFilters
|
|
@@ -540,6 +549,28 @@ function DataFilters({
|
|
|
540
549
|
] });
|
|
541
550
|
}
|
|
542
551
|
var DEFAULT_BATCH_SIZE = 48;
|
|
552
|
+
var NESTED_CONTROL = "a,button,input,select,textarea,label,[role=button],[role=link],[role=checkbox],[role=switch],[role=menuitem],[contenteditable=true]";
|
|
553
|
+
function activation(item, onItemClick, getItemLabel) {
|
|
554
|
+
if (!onItemClick) return {};
|
|
555
|
+
return {
|
|
556
|
+
role: "button",
|
|
557
|
+
tabIndex: 0,
|
|
558
|
+
"aria-label": getItemLabel?.(item),
|
|
559
|
+
onClick: (e) => {
|
|
560
|
+
const control = e.target.closest(NESTED_CONTROL);
|
|
561
|
+
if (control && control !== e.currentTarget) return;
|
|
562
|
+
onItemClick(item);
|
|
563
|
+
},
|
|
564
|
+
onKeyDown: (e) => {
|
|
565
|
+
if (e.target !== e.currentTarget) return;
|
|
566
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
567
|
+
e.preventDefault();
|
|
568
|
+
onItemClick(item);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
var ACTIVATABLE = "cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background";
|
|
543
574
|
function DataGrid({
|
|
544
575
|
items,
|
|
545
576
|
viewMode,
|
|
@@ -551,7 +582,9 @@ function DataGrid({
|
|
|
551
582
|
staggerAnimation = true,
|
|
552
583
|
getKey,
|
|
553
584
|
focusedIndex = -1,
|
|
554
|
-
keyboardContainerRef
|
|
585
|
+
keyboardContainerRef,
|
|
586
|
+
onItemClick,
|
|
587
|
+
getItemLabel
|
|
555
588
|
}) {
|
|
556
589
|
const [visibleCount, setVisibleCount] = useState(batchSize);
|
|
557
590
|
const sentinelRef = useRef(null);
|
|
@@ -587,8 +620,10 @@ function DataGrid({
|
|
|
587
620
|
"div",
|
|
588
621
|
{
|
|
589
622
|
"data-nav-item": true,
|
|
623
|
+
...activation(item, onItemClick, getItemLabel),
|
|
590
624
|
className: cn(
|
|
591
625
|
"px-4 py-3 transition-colors hover:bg-accent/50",
|
|
626
|
+
onItemClick && ACTIVATABLE,
|
|
592
627
|
focusedIndex === i && "ring-2 ring-inset ring-primary bg-accent/50"
|
|
593
628
|
),
|
|
594
629
|
children: renderListItem(item, i)
|
|
@@ -605,8 +640,10 @@ function DataGrid({
|
|
|
605
640
|
"div",
|
|
606
641
|
{
|
|
607
642
|
"data-nav-item": true,
|
|
643
|
+
...activation(item, onItemClick, getItemLabel),
|
|
608
644
|
className: cn(
|
|
609
645
|
staggerAnimation && "animate-fade-in",
|
|
646
|
+
onItemClick && cn(ACTIVATABLE, "h-full rounded-xl"),
|
|
610
647
|
focusedIndex === i && "ring-2 ring-primary ring-offset-2 ring-offset-background rounded-xl transition-shadow duration-150"
|
|
611
648
|
),
|
|
612
649
|
style: staggerAnimation ? {
|
|
@@ -637,9 +674,66 @@ var DENSITY_HEAD = {
|
|
|
637
674
|
comfortable: "h-10",
|
|
638
675
|
spacious: "h-12 py-3"
|
|
639
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
|
+
}
|
|
640
721
|
function getCellClasses(col) {
|
|
641
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" : "";
|
|
642
|
-
return cn(col.width, hide);
|
|
723
|
+
return cn(typeof col.width === "string" && !PX_WIDTH.test(col.width) ? col.width : void 0, hide);
|
|
724
|
+
}
|
|
725
|
+
var PX_WIDTH = /^\s*\d+(\.\d+)?(px)?\s*$/;
|
|
726
|
+
function pxWidth(width) {
|
|
727
|
+
if (typeof width === "number") return width > 0 ? width : void 0;
|
|
728
|
+
if (width && PX_WIDTH.test(width)) {
|
|
729
|
+
const px = parseFloat(width);
|
|
730
|
+
return px > 0 ? px : void 0;
|
|
731
|
+
}
|
|
732
|
+
return void 0;
|
|
733
|
+
}
|
|
734
|
+
function colHideClass(hideBelow) {
|
|
735
|
+
if (!hideBelow) return void 0;
|
|
736
|
+
return hideBelow === "sm" ? "hidden sm:table-column" : hideBelow === "md" ? "hidden md:table-column" : hideBelow === "lg" ? "hidden lg:table-column" : "hidden xl:table-column";
|
|
643
737
|
}
|
|
644
738
|
function errorMessage(error) {
|
|
645
739
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -842,29 +936,29 @@ function DataTable({
|
|
|
842
936
|
stickyHeader = true,
|
|
843
937
|
resizeKey,
|
|
844
938
|
stickyFirstColumn,
|
|
845
|
-
density = "comfortable"
|
|
939
|
+
density = "comfortable",
|
|
940
|
+
keyboardNavigation = true,
|
|
941
|
+
openColumn,
|
|
942
|
+
virtualize,
|
|
943
|
+
pinnedColumns,
|
|
944
|
+
reorderableColumns = true,
|
|
945
|
+
columnOrderKey,
|
|
946
|
+
sort: sortProp,
|
|
947
|
+
onSortChange
|
|
846
948
|
}) {
|
|
949
|
+
const rowKeys = keyboardNavigation && !!onRowClick;
|
|
950
|
+
const [activeRowKey, setActiveRowKey] = useState(null);
|
|
847
951
|
const cellDensity = DENSITY_CELL[density];
|
|
848
952
|
const headDensity = DENSITY_HEAD[density];
|
|
849
|
-
const [
|
|
953
|
+
const [ownSorts, setOwnSorts] = useState([]);
|
|
954
|
+
const sorts = sortProp ?? ownSorts;
|
|
850
955
|
const handleSort = useCallback(
|
|
851
956
|
(key, shiftKey) => {
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
const entry = prev[existing];
|
|
856
|
-
if (entry.dir === "asc") {
|
|
857
|
-
const next = [...prev];
|
|
858
|
-
next[existing] = { key, dir: "desc" };
|
|
859
|
-
return next;
|
|
860
|
-
}
|
|
861
|
-
return prev.filter((_, i) => i !== existing);
|
|
862
|
-
}
|
|
863
|
-
if (shiftKey) return [...prev, { key, dir: "asc" }];
|
|
864
|
-
return [{ key, dir: "asc" }];
|
|
865
|
-
});
|
|
957
|
+
const next = nextSort(sorts, key, shiftKey);
|
|
958
|
+
if (sortProp === void 0) setOwnSorts(next);
|
|
959
|
+
onSortChange?.(next);
|
|
866
960
|
},
|
|
867
|
-
[]
|
|
961
|
+
[sorts, sortProp, onSortChange]
|
|
868
962
|
);
|
|
869
963
|
const [visibleCols, setVisibleCols] = useState(
|
|
870
964
|
() => {
|
|
@@ -904,7 +998,7 @@ function DataTable({
|
|
|
904
998
|
}, [columnVisibilityKey]);
|
|
905
999
|
useEffect(() => {
|
|
906
1000
|
if (!visibilityHydrated || !columnVisibilityKey) return;
|
|
907
|
-
|
|
1001
|
+
storeSet(
|
|
908
1002
|
`dt-vis-${columnVisibilityKey}`,
|
|
909
1003
|
JSON.stringify(visibleCols)
|
|
910
1004
|
);
|
|
@@ -918,8 +1012,9 @@ function DataTable({
|
|
|
918
1012
|
defaults[c.key] = c.defaultVisible !== false;
|
|
919
1013
|
});
|
|
920
1014
|
setVisibleCols(defaults);
|
|
1015
|
+
setColOrder(null);
|
|
921
1016
|
if (columnVisibilityKey) {
|
|
922
|
-
|
|
1017
|
+
storeRemove(`dt-vis-${columnVisibilityKey}`);
|
|
923
1018
|
}
|
|
924
1019
|
}, [columns, columnVisibilityKey]);
|
|
925
1020
|
useEffect(() => {
|
|
@@ -935,7 +1030,54 @@ function DataTable({
|
|
|
935
1030
|
return changed ? updated : prev;
|
|
936
1031
|
});
|
|
937
1032
|
}, [columns]);
|
|
938
|
-
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
|
+
];
|
|
939
1081
|
const [colWidths, setColWidths] = useState(() => {
|
|
940
1082
|
if (resizeKey) {
|
|
941
1083
|
try {
|
|
@@ -946,10 +1088,8 @@ function DataTable({
|
|
|
946
1088
|
}
|
|
947
1089
|
const initial = {};
|
|
948
1090
|
columns.forEach((c) => {
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
if (!isNaN(px) && px > 0) initial[c.key] = px;
|
|
952
|
-
}
|
|
1091
|
+
const px = pxWidth(c.width);
|
|
1092
|
+
if (px) initial[c.key] = px;
|
|
953
1093
|
});
|
|
954
1094
|
return initial;
|
|
955
1095
|
});
|
|
@@ -964,46 +1104,232 @@ function DataTable({
|
|
|
964
1104
|
);
|
|
965
1105
|
const handleResizeEnd = useCallback(() => {
|
|
966
1106
|
if (resizeKey) {
|
|
967
|
-
|
|
1107
|
+
storeSet(`dt-resize-${resizeKey}`, JSON.stringify(colWidths));
|
|
968
1108
|
}
|
|
969
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
|
+
};
|
|
970
1137
|
const [currentPage, setCurrentPage] = useState(0);
|
|
971
1138
|
const [currentPageSize, setCurrentPageSize] = useState(initialPageSize);
|
|
972
1139
|
useEffect(() => {
|
|
973
1140
|
setCurrentPage(0);
|
|
974
1141
|
}, [items.length, currentPageSize]);
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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;
|
|
996
1174
|
const totalPages = pagination ? Math.max(1, Math.ceil(totalItems / currentPageSize)) : 1;
|
|
997
|
-
const displayItems =
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
+
};
|
|
1001
1204
|
const showingFrom = totalItems === 0 ? 0 : currentPage * currentPageSize + 1;
|
|
1002
1205
|
const showingTo = Math.min(
|
|
1003
1206
|
(currentPage + 1) * currentPageSize,
|
|
1004
1207
|
totalItems
|
|
1005
1208
|
);
|
|
1006
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
|
+
};
|
|
1007
1333
|
return /* @__PURE__ */ jsxs("div", { className: "border rounded-lg animate-fade-in flex flex-col", children: [
|
|
1008
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: [
|
|
1009
1335
|
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
@@ -1041,135 +1367,181 @@ function DataTable({
|
|
|
1041
1367
|
}
|
|
1042
1368
|
)
|
|
1043
1369
|
] }) }),
|
|
1044
|
-
/* @__PURE__ */ jsx(
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
{
|
|
1053
|
-
checked: items.length > 0 && selected?.size === items.length,
|
|
1054
|
-
onCheckedChange: onToggleAll
|
|
1055
|
-
}
|
|
1056
|
-
) }),
|
|
1057
|
-
activeColumns.map((col, colIndex) => {
|
|
1058
|
-
const sortIndex = sorts.findIndex((s) => s.key === col.key);
|
|
1059
|
-
const sortEntry = sortIndex >= 0 ? sorts[sortIndex] : null;
|
|
1060
|
-
const resizeWidth = colWidths[col.key];
|
|
1061
|
-
const isFirstCol = colIndex === 0 && stickyFirstColumn;
|
|
1062
|
-
return /* @__PURE__ */ jsxs(
|
|
1063
|
-
TableHead,
|
|
1064
|
-
{
|
|
1065
|
-
className: cn(
|
|
1066
|
-
getCellClasses(col),
|
|
1067
|
-
headDensity,
|
|
1068
|
-
col.sortable && "cursor-pointer select-none",
|
|
1069
|
-
resizableColumns && "relative",
|
|
1070
|
-
isFirstCol && "sticky left-0 bg-background",
|
|
1071
|
-
isFirstCol && stickyHeader ? "z-[15]" : isFirstCol ? "z-[5]" : void 0
|
|
1072
|
-
),
|
|
1073
|
-
style: resizeWidth ? { width: resizeWidth, minWidth: resizeWidth } : void 0,
|
|
1074
|
-
onClick: col.sortable ? (e) => handleSort(col.key, e.shiftKey) : void 0,
|
|
1075
|
-
children: [
|
|
1076
|
-
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
|
|
1077
|
-
col.header,
|
|
1078
|
-
col.sortable && (sortEntry ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1079
|
-
sortEntry.dir === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { className: "h-3 w-3 text-foreground" }) : /* @__PURE__ */ jsx(ArrowDown, { className: "h-3 w-3 text-foreground" }),
|
|
1080
|
-
sorts.length > 1 && /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: sortIndex + 1 })
|
|
1081
|
-
] }) : /* @__PURE__ */ jsx(ArrowUpDown, { className: "h-3 w-3 text-muted-foreground/50" }))
|
|
1082
|
-
] }),
|
|
1083
|
-
resizableColumns && /* @__PURE__ */ jsx(
|
|
1084
|
-
ResizeHandle,
|
|
1085
|
-
{
|
|
1086
|
-
onResize: (delta) => handleResize(
|
|
1087
|
-
col.key,
|
|
1088
|
-
delta,
|
|
1089
|
-
col.minWidth ?? 60
|
|
1090
|
-
),
|
|
1091
|
-
onResizeEnd: handleResizeEnd
|
|
1092
|
-
}
|
|
1093
|
-
)
|
|
1094
|
-
]
|
|
1095
|
-
},
|
|
1096
|
-
col.key
|
|
1097
|
-
);
|
|
1098
|
-
}),
|
|
1099
|
-
renderActions && /* @__PURE__ */ jsx(TableHead, { className: cn("w-10", headDensity) })
|
|
1100
|
-
] })
|
|
1101
|
-
}
|
|
1102
|
-
),
|
|
1103
|
-
/* @__PURE__ */ jsx(TableBody, { children: displayItems.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
|
|
1104
|
-
TableCell,
|
|
1105
|
-
{
|
|
1106
|
-
colSpan: totalCols,
|
|
1107
|
-
className: "text-center py-12 text-muted-foreground",
|
|
1108
|
-
children: emptyMessage
|
|
1109
|
-
}
|
|
1110
|
-
) }) : displayItems.map((item) => {
|
|
1111
|
-
const id = getKey(item);
|
|
1112
|
-
return /* @__PURE__ */ jsxs(
|
|
1113
|
-
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,
|
|
1114
1378
|
{
|
|
1115
|
-
|
|
1116
|
-
"
|
|
1117
|
-
onClick: onRowClick ? () => onRowClick(item) : void 0,
|
|
1379
|
+
style: { tableLayout: "fixed" },
|
|
1380
|
+
"aria-rowcount": virtualizing ? displayItems.length + 1 : void 0,
|
|
1118
1381
|
children: [
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
{
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
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",
|
|
1126
1388
|
{
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
{
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
+
}
|
|
1146
1416
|
),
|
|
1147
|
-
|
|
1148
|
-
|
|
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,
|
|
1149
1509
|
{
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
value
|
|
1510
|
+
className: cn("w-10", headDensity, pinProps("__actions", true)?.className),
|
|
1511
|
+
style: pinProps("__actions", true)?.style
|
|
1153
1512
|
}
|
|
1154
|
-
)
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
renderActions && /* @__PURE__ */ jsx(
|
|
1513
|
+
)
|
|
1514
|
+
] })
|
|
1515
|
+
}
|
|
1516
|
+
),
|
|
1517
|
+
/* @__PURE__ */ jsx(TableBody, { ref: bodyRef, children: displayItems.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
|
|
1160
1518
|
TableCell,
|
|
1161
1519
|
{
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
children:
|
|
1520
|
+
colSpan: totalCols,
|
|
1521
|
+
className: "text-center py-12 text-muted-foreground",
|
|
1522
|
+
children: emptyMessage
|
|
1165
1523
|
}
|
|
1166
|
-
)
|
|
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)) })
|
|
1167
1540
|
]
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
] }) }),
|
|
1541
|
+
}
|
|
1542
|
+
)
|
|
1543
|
+
}
|
|
1544
|
+
),
|
|
1173
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: [
|
|
1174
1546
|
/* @__PURE__ */ jsx("span", { children: totalItems === 0 ? "No records" : `Showing ${showingFrom}\u2013${showingTo} of ${totalItems}` }),
|
|
1175
1547
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
@@ -1750,11 +2122,48 @@ function DetailPanel({
|
|
|
1750
2122
|
}
|
|
1751
2123
|
);
|
|
1752
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
|
+
}
|
|
1753
2158
|
function RelationField({
|
|
1754
2159
|
field,
|
|
1755
2160
|
value,
|
|
1756
2161
|
onChange,
|
|
1757
|
-
disabled
|
|
2162
|
+
disabled,
|
|
2163
|
+
id,
|
|
2164
|
+
"aria-describedby": ariaDescribedBy,
|
|
2165
|
+
"aria-invalid": ariaInvalid,
|
|
2166
|
+
"aria-required": ariaRequired
|
|
1758
2167
|
}) {
|
|
1759
2168
|
const [open, setOpen] = useState(false);
|
|
1760
2169
|
const [createOpen, setCreateOpen] = useState(false);
|
|
@@ -1813,7 +2222,11 @@ function RelationField({
|
|
|
1813
2222
|
{
|
|
1814
2223
|
variant: "outline",
|
|
1815
2224
|
role: "combobox",
|
|
2225
|
+
id,
|
|
1816
2226
|
"aria-expanded": open,
|
|
2227
|
+
"aria-describedby": ariaDescribedBy,
|
|
2228
|
+
"aria-invalid": ariaInvalid,
|
|
2229
|
+
"aria-required": ariaRequired,
|
|
1817
2230
|
className: "w-full justify-between font-normal",
|
|
1818
2231
|
disabled: disabled || isLoading,
|
|
1819
2232
|
children: [
|
|
@@ -1924,7 +2337,12 @@ function FileUploadField({
|
|
|
1924
2337
|
field,
|
|
1925
2338
|
value,
|
|
1926
2339
|
onChange,
|
|
1927
|
-
disabled
|
|
2340
|
+
disabled,
|
|
2341
|
+
id,
|
|
2342
|
+
"aria-labelledby": ariaLabelledBy,
|
|
2343
|
+
"aria-describedby": ariaDescribedBy,
|
|
2344
|
+
"aria-invalid": ariaInvalid,
|
|
2345
|
+
"aria-required": ariaRequired
|
|
1928
2346
|
}) {
|
|
1929
2347
|
const multiple = field.multiple ?? false;
|
|
1930
2348
|
const maxSize = field.maxSize ?? DEFAULT_MAX_SIZE;
|
|
@@ -1997,7 +2415,16 @@ function FileUploadField({
|
|
|
1997
2415
|
/* @__PURE__ */ jsxs(
|
|
1998
2416
|
"div",
|
|
1999
2417
|
{
|
|
2000
|
-
...getRootProps(
|
|
2418
|
+
...getRootProps({
|
|
2419
|
+
// The focusable drop zone carries the field's name, description
|
|
2420
|
+
// and state; react-dropzone's default `role="presentation"` would
|
|
2421
|
+
// leave a keyboard user on an unnamed tab stop.
|
|
2422
|
+
role: "button",
|
|
2423
|
+
"aria-labelledby": ariaLabelledBy,
|
|
2424
|
+
"aria-describedby": ariaDescribedBy,
|
|
2425
|
+
"aria-invalid": ariaInvalid,
|
|
2426
|
+
"aria-disabled": disabled || void 0
|
|
2427
|
+
}),
|
|
2001
2428
|
className: cn(
|
|
2002
2429
|
"flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed px-6 py-8 text-center transition-colors",
|
|
2003
2430
|
isDragActive ? "border-primary bg-primary/5" : "border-muted-foreground/25 hover:border-muted-foreground/50",
|
|
@@ -2005,7 +2432,17 @@ function FileUploadField({
|
|
|
2005
2432
|
hasFiles && "py-4"
|
|
2006
2433
|
),
|
|
2007
2434
|
children: [
|
|
2008
|
-
/* @__PURE__ */ jsx(
|
|
2435
|
+
/* @__PURE__ */ jsx(
|
|
2436
|
+
"input",
|
|
2437
|
+
{
|
|
2438
|
+
...getInputProps({
|
|
2439
|
+
id,
|
|
2440
|
+
"aria-describedby": ariaDescribedBy,
|
|
2441
|
+
"aria-invalid": ariaInvalid,
|
|
2442
|
+
"aria-required": ariaRequired
|
|
2443
|
+
})
|
|
2444
|
+
}
|
|
2445
|
+
),
|
|
2009
2446
|
isImage ? /* @__PURE__ */ jsx(ImageIcon, { className: "mb-2 h-8 w-8 text-muted-foreground" }) : /* @__PURE__ */ jsx(Upload, { className: "mb-2 h-8 w-8 text-muted-foreground" }),
|
|
2010
2447
|
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: isDragActive ? "Drop files here..." : "Drag & drop or click to upload" }),
|
|
2011
2448
|
/* @__PURE__ */ jsxs("p", { className: "mt-1 text-xs text-muted-foreground/70", children: [
|
|
@@ -2182,6 +2619,11 @@ function ResourceFormFieldRenderer({
|
|
|
2182
2619
|
watchedValues
|
|
2183
2620
|
}) {
|
|
2184
2621
|
const label = field.label ?? titleCase(field.key);
|
|
2622
|
+
const baseId = useId();
|
|
2623
|
+
const inputId = `${baseId}-input`;
|
|
2624
|
+
const errorId = `${baseId}-error`;
|
|
2625
|
+
const descId = `${baseId}-desc`;
|
|
2626
|
+
const labelId = `${baseId}-label`;
|
|
2185
2627
|
const fieldType = field.type ?? "text";
|
|
2186
2628
|
const isReadOnly = action === "edit" && field.readOnlyOnEdit;
|
|
2187
2629
|
const isDisabled = isReadOnly || (watchedValues ? field.disabled?.(watchedValues) === true : false);
|
|
@@ -2213,14 +2655,20 @@ function ResourceFormFieldRenderer({
|
|
|
2213
2655
|
control,
|
|
2214
2656
|
render: ({ field: f, fieldState }) => {
|
|
2215
2657
|
const errorMessage2 = fieldState.error?.message;
|
|
2658
|
+
const describedBy = [field.description ? descId : null, errorMessage2 ? errorId : null].filter(Boolean).join(" ") || void 0;
|
|
2216
2659
|
const ariaProps = {
|
|
2660
|
+
id: inputId,
|
|
2217
2661
|
"aria-invalid": !!errorMessage2 || void 0,
|
|
2218
|
-
"aria-describedby":
|
|
2662
|
+
"aria-describedby": describedBy,
|
|
2219
2663
|
"aria-required": field.required || void 0
|
|
2220
2664
|
};
|
|
2221
2665
|
if (fieldType === "switch") {
|
|
2222
2666
|
return /* @__PURE__ */ jsxs(FormField, { className: "flex flex-row items-center justify-between rounded-lg border p-4", children: [
|
|
2223
|
-
/* @__PURE__ */
|
|
2667
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
|
|
2668
|
+
/* @__PURE__ */ jsx(FormLabel, { htmlFor: inputId, error: !!errorMessage2, children: label }),
|
|
2669
|
+
field.description && /* @__PURE__ */ jsx(FormDescription, { id: descId, children: field.description }),
|
|
2670
|
+
/* @__PURE__ */ jsx(FormError, { id: errorId, children: errorMessage2 })
|
|
2671
|
+
] }),
|
|
2224
2672
|
/* @__PURE__ */ jsx(
|
|
2225
2673
|
Switch,
|
|
2226
2674
|
{
|
|
@@ -2244,14 +2692,15 @@ function ResourceFormFieldRenderer({
|
|
|
2244
2692
|
}
|
|
2245
2693
|
),
|
|
2246
2694
|
/* @__PURE__ */ jsxs("div", { className: "space-y-1 leading-none", children: [
|
|
2247
|
-
/* @__PURE__ */ jsx(FormLabel, { error: !!errorMessage2, children: label }),
|
|
2248
|
-
/* @__PURE__ */ jsx(
|
|
2695
|
+
/* @__PURE__ */ jsx(FormLabel, { htmlFor: inputId, error: !!errorMessage2, children: label }),
|
|
2696
|
+
field.description && /* @__PURE__ */ jsx(FormDescription, { id: descId, children: field.description }),
|
|
2697
|
+
/* @__PURE__ */ jsx(FormError, { id: errorId, children: errorMessage2 })
|
|
2249
2698
|
] })
|
|
2250
2699
|
] });
|
|
2251
2700
|
}
|
|
2252
2701
|
if (fieldType === "select" && field.options) {
|
|
2253
2702
|
return /* @__PURE__ */ jsxs(FormField, { children: [
|
|
2254
|
-
/* @__PURE__ */ jsx(FormLabel, { error: !!errorMessage2, required: field.required, children: label }),
|
|
2703
|
+
/* @__PURE__ */ jsx(FormLabel, { htmlFor: inputId, error: !!errorMessage2, required: field.required, children: label }),
|
|
2255
2704
|
/* @__PURE__ */ jsxs(
|
|
2256
2705
|
Select,
|
|
2257
2706
|
{
|
|
@@ -2270,49 +2719,53 @@ function ResourceFormFieldRenderer({
|
|
|
2270
2719
|
]
|
|
2271
2720
|
}
|
|
2272
2721
|
),
|
|
2273
|
-
field.description && /* @__PURE__ */ jsx(FormDescription, { id:
|
|
2274
|
-
/* @__PURE__ */ jsx(FormError, { id:
|
|
2722
|
+
field.description && /* @__PURE__ */ jsx(FormDescription, { id: descId, children: field.description }),
|
|
2723
|
+
/* @__PURE__ */ jsx(FormError, { id: errorId, children: errorMessage2 })
|
|
2275
2724
|
] });
|
|
2276
2725
|
}
|
|
2277
2726
|
if (fieldType === "relation" && field.relation) {
|
|
2278
2727
|
return /* @__PURE__ */ jsxs(FormField, { children: [
|
|
2279
|
-
/* @__PURE__ */ jsx(FormLabel, { error: !!errorMessage2, required: field.required, children: label }),
|
|
2728
|
+
/* @__PURE__ */ jsx(FormLabel, { htmlFor: inputId, error: !!errorMessage2, required: field.required, children: label }),
|
|
2280
2729
|
/* @__PURE__ */ jsx(
|
|
2281
2730
|
RelationField,
|
|
2282
2731
|
{
|
|
2283
2732
|
field,
|
|
2284
2733
|
value: f.value != null ? String(f.value) : "",
|
|
2285
2734
|
onChange: f.onChange,
|
|
2286
|
-
disabled: isDisabled
|
|
2735
|
+
disabled: isDisabled,
|
|
2736
|
+
...ariaProps
|
|
2287
2737
|
}
|
|
2288
2738
|
),
|
|
2289
|
-
field.description && /* @__PURE__ */ jsx(FormDescription, { id:
|
|
2290
|
-
/* @__PURE__ */ jsx(FormError, { id:
|
|
2739
|
+
field.description && /* @__PURE__ */ jsx(FormDescription, { id: descId, children: field.description }),
|
|
2740
|
+
/* @__PURE__ */ jsx(FormError, { id: errorId, children: errorMessage2 })
|
|
2291
2741
|
] });
|
|
2292
2742
|
}
|
|
2293
2743
|
if (fieldType === "file" || fieldType === "image") {
|
|
2294
2744
|
return /* @__PURE__ */ jsxs(FormField, { children: [
|
|
2295
|
-
/* @__PURE__ */ jsx(FormLabel, { error: !!errorMessage2, required: field.required, children: label }),
|
|
2745
|
+
/* @__PURE__ */ jsx(FormLabel, { id: labelId, htmlFor: inputId, error: !!errorMessage2, required: field.required, children: label }),
|
|
2296
2746
|
/* @__PURE__ */ jsx(
|
|
2297
2747
|
FileUploadField,
|
|
2298
2748
|
{
|
|
2749
|
+
"aria-labelledby": labelId,
|
|
2299
2750
|
field,
|
|
2300
2751
|
value: f.value,
|
|
2301
2752
|
onChange: f.onChange,
|
|
2302
|
-
disabled: isDisabled
|
|
2753
|
+
disabled: isDisabled,
|
|
2754
|
+
...ariaProps
|
|
2303
2755
|
}
|
|
2304
2756
|
),
|
|
2305
|
-
field.description && /* @__PURE__ */ jsx(FormDescription, { id:
|
|
2306
|
-
/* @__PURE__ */ jsx(FormError, { id:
|
|
2757
|
+
field.description && /* @__PURE__ */ jsx(FormDescription, { id: descId, children: field.description }),
|
|
2758
|
+
/* @__PURE__ */ jsx(FormError, { id: errorId, children: errorMessage2 })
|
|
2307
2759
|
] });
|
|
2308
2760
|
}
|
|
2309
2761
|
if (fieldType === "textarea") {
|
|
2310
2762
|
return /* @__PURE__ */ jsxs(FormField, { children: [
|
|
2311
|
-
/* @__PURE__ */ jsx(FormLabel, { error: !!errorMessage2, required: field.required, children: label }),
|
|
2763
|
+
/* @__PURE__ */ jsx(FormLabel, { htmlFor: inputId, error: !!errorMessage2, required: field.required, children: label }),
|
|
2312
2764
|
/* @__PURE__ */ jsx(
|
|
2313
2765
|
Textarea,
|
|
2314
2766
|
{
|
|
2315
2767
|
placeholder: field.placeholder,
|
|
2768
|
+
maxLength: field.maxLength,
|
|
2316
2769
|
rows: field.rows ?? 3,
|
|
2317
2770
|
value: f.value != null ? String(f.value) : "",
|
|
2318
2771
|
onChange: (e) => f.onChange(e.target.value),
|
|
@@ -2321,13 +2774,13 @@ function ResourceFormFieldRenderer({
|
|
|
2321
2774
|
...ariaProps
|
|
2322
2775
|
}
|
|
2323
2776
|
),
|
|
2324
|
-
field.description && /* @__PURE__ */ jsx(FormDescription, { id:
|
|
2325
|
-
/* @__PURE__ */ jsx(FormError, { id:
|
|
2777
|
+
field.description && /* @__PURE__ */ jsx(FormDescription, { id: descId, children: field.description }),
|
|
2778
|
+
/* @__PURE__ */ jsx(FormError, { id: errorId, children: errorMessage2 })
|
|
2326
2779
|
] });
|
|
2327
2780
|
}
|
|
2328
2781
|
if (fieldType === "number") {
|
|
2329
2782
|
return /* @__PURE__ */ jsxs(FormField, { children: [
|
|
2330
|
-
/* @__PURE__ */ jsx(FormLabel, { error: !!errorMessage2, required: field.required, children: label }),
|
|
2783
|
+
/* @__PURE__ */ jsx(FormLabel, { htmlFor: inputId, error: !!errorMessage2, required: field.required, children: label }),
|
|
2331
2784
|
/* @__PURE__ */ jsx(
|
|
2332
2785
|
Input,
|
|
2333
2786
|
{
|
|
@@ -2345,17 +2798,18 @@ function ResourceFormFieldRenderer({
|
|
|
2345
2798
|
...ariaProps
|
|
2346
2799
|
}
|
|
2347
2800
|
),
|
|
2348
|
-
field.description && /* @__PURE__ */ jsx(FormDescription, { id:
|
|
2349
|
-
/* @__PURE__ */ jsx(FormError, { id:
|
|
2801
|
+
field.description && /* @__PURE__ */ jsx(FormDescription, { id: descId, children: field.description }),
|
|
2802
|
+
/* @__PURE__ */ jsx(FormError, { id: errorId, children: errorMessage2 })
|
|
2350
2803
|
] });
|
|
2351
2804
|
}
|
|
2352
2805
|
return /* @__PURE__ */ jsxs(FormField, { children: [
|
|
2353
|
-
/* @__PURE__ */ jsx(FormLabel, { error: !!errorMessage2, required: field.required, children: label }),
|
|
2806
|
+
/* @__PURE__ */ jsx(FormLabel, { htmlFor: inputId, error: !!errorMessage2, required: field.required, children: label }),
|
|
2354
2807
|
/* @__PURE__ */ jsx(
|
|
2355
2808
|
Input,
|
|
2356
2809
|
{
|
|
2357
2810
|
type: fieldType,
|
|
2358
2811
|
placeholder: field.placeholder,
|
|
2812
|
+
maxLength: field.maxLength,
|
|
2359
2813
|
value: f.value != null ? String(f.value) : "",
|
|
2360
2814
|
onChange: (e) => f.onChange(e.target.value),
|
|
2361
2815
|
disabled: isDisabled,
|
|
@@ -2363,8 +2817,8 @@ function ResourceFormFieldRenderer({
|
|
|
2363
2817
|
...ariaProps
|
|
2364
2818
|
}
|
|
2365
2819
|
),
|
|
2366
|
-
field.description && /* @__PURE__ */ jsx(FormDescription, { id:
|
|
2367
|
-
/* @__PURE__ */ jsx(FormError, { id:
|
|
2820
|
+
field.description && /* @__PURE__ */ jsx(FormDescription, { id: descId, children: field.description }),
|
|
2821
|
+
/* @__PURE__ */ jsx(FormError, { id: errorId, children: errorMessage2 })
|
|
2368
2822
|
] });
|
|
2369
2823
|
}
|
|
2370
2824
|
}
|
|
@@ -2476,6 +2930,19 @@ function FormBody({
|
|
|
2476
2930
|
control: form.control,
|
|
2477
2931
|
disabled: !hasConditionalFields
|
|
2478
2932
|
});
|
|
2933
|
+
const rootError = form.formState.errors.root?.message;
|
|
2934
|
+
const rootErrorBanner = rootError ? /* @__PURE__ */ jsxs(
|
|
2935
|
+
"div",
|
|
2936
|
+
{
|
|
2937
|
+
role: "alert",
|
|
2938
|
+
"data-testid": "resource-form-error",
|
|
2939
|
+
className: "flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive",
|
|
2940
|
+
children: [
|
|
2941
|
+
/* @__PURE__ */ jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0", "aria-hidden": "true" }),
|
|
2942
|
+
/* @__PURE__ */ jsx("span", { children: rootError })
|
|
2943
|
+
]
|
|
2944
|
+
}
|
|
2945
|
+
) : null;
|
|
2479
2946
|
const onInvalid = (errors) => {
|
|
2480
2947
|
const firstErrorKey = Object.keys(errors)[0];
|
|
2481
2948
|
if (firstErrorKey) form.setFocus(firstErrorKey);
|
|
@@ -2484,18 +2951,21 @@ function FormBody({
|
|
|
2484
2951
|
return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-8", children: /* @__PURE__ */ jsx(Spinner, {}) });
|
|
2485
2952
|
}
|
|
2486
2953
|
if (mode === "wizard" && steps) {
|
|
2487
|
-
return /* @__PURE__ */
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2954
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
2955
|
+
rootErrorBanner,
|
|
2956
|
+
/* @__PURE__ */ jsx(
|
|
2957
|
+
WizardForm,
|
|
2958
|
+
{
|
|
2959
|
+
steps,
|
|
2960
|
+
form,
|
|
2961
|
+
fields: resolvedFields,
|
|
2962
|
+
onSubmit: handleSave,
|
|
2963
|
+
isSaving,
|
|
2964
|
+
action,
|
|
2965
|
+
onInvalid
|
|
2966
|
+
}
|
|
2967
|
+
)
|
|
2968
|
+
] });
|
|
2499
2969
|
}
|
|
2500
2970
|
const visibleFields = resolvedFields.filter(
|
|
2501
2971
|
(field) => field.visible?.(watchedValues) !== false
|
|
@@ -2535,6 +3005,7 @@ function FormBody({
|
|
|
2535
3005
|
return /* @__PURE__ */ jsxs(
|
|
2536
3006
|
"form",
|
|
2537
3007
|
{
|
|
3008
|
+
noValidate: true,
|
|
2538
3009
|
onSubmit: (e) => {
|
|
2539
3010
|
e.preventDefault();
|
|
2540
3011
|
void form.handleSubmit(handleSave, onInvalid)(e);
|
|
@@ -2548,6 +3019,7 @@ function FormBody({
|
|
|
2548
3019
|
] }),
|
|
2549
3020
|
renderFields(group.fields)
|
|
2550
3021
|
] }, group.name ?? "__default")) }) }),
|
|
3022
|
+
rootErrorBanner,
|
|
2551
3023
|
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
2552
3024
|
/* @__PURE__ */ jsx(
|
|
2553
3025
|
Button,
|
|
@@ -2584,7 +3056,14 @@ function ResourceForm(props) {
|
|
|
2584
3056
|
layout: layoutProp = "dialog",
|
|
2585
3057
|
mode = "standard",
|
|
2586
3058
|
steps,
|
|
2587
|
-
columns = 1
|
|
3059
|
+
columns = 1,
|
|
3060
|
+
prefillFromUrl,
|
|
3061
|
+
autoPlaceholder = false,
|
|
3062
|
+
warnWhenUnsaved = false,
|
|
3063
|
+
onDirtyChange,
|
|
3064
|
+
mutationMode = "pessimistic",
|
|
3065
|
+
undoDelayMs,
|
|
3066
|
+
onUndoable
|
|
2588
3067
|
} = props;
|
|
2589
3068
|
const isMobile = useIsMobile();
|
|
2590
3069
|
const effectiveLayout = isMobile ? "sheet" : layoutProp;
|
|
@@ -2602,9 +3081,31 @@ function ResourceForm(props) {
|
|
|
2602
3081
|
onOpenChange(false);
|
|
2603
3082
|
onSuccess?.(data);
|
|
2604
3083
|
},
|
|
2605
|
-
|
|
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),
|
|
3087
|
+
prefillFromUrl,
|
|
3088
|
+
autoPlaceholder,
|
|
3089
|
+
warnWhenUnsaved,
|
|
3090
|
+
mutationMode,
|
|
3091
|
+
undoDelayMs,
|
|
3092
|
+
onUndoable: onUndoable ?? showUndoToast
|
|
2606
3093
|
};
|
|
2607
|
-
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
|
+
);
|
|
2608
3109
|
const bodyProps = {
|
|
2609
3110
|
form,
|
|
2610
3111
|
resolvedFields,
|
|
@@ -2612,14 +3113,14 @@ function ResourceForm(props) {
|
|
|
2612
3113
|
action,
|
|
2613
3114
|
isSaving,
|
|
2614
3115
|
isLoading,
|
|
2615
|
-
onOpenChange,
|
|
3116
|
+
onOpenChange: requestOpenChange,
|
|
2616
3117
|
handleSave,
|
|
2617
3118
|
mode,
|
|
2618
3119
|
steps,
|
|
2619
3120
|
columns
|
|
2620
3121
|
};
|
|
2621
3122
|
if (effectiveLayout === "dialog") {
|
|
2622
|
-
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: [
|
|
2623
3124
|
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
2624
3125
|
/* @__PURE__ */ jsx(DialogTitle, { children: formTitle }),
|
|
2625
3126
|
/* @__PURE__ */ jsx(DialogDescription, { className: "sr-only", children: formDescription })
|
|
@@ -2628,7 +3129,7 @@ function ResourceForm(props) {
|
|
|
2628
3129
|
] }) });
|
|
2629
3130
|
}
|
|
2630
3131
|
if (effectiveLayout === "sheet") {
|
|
2631
|
-
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: [
|
|
2632
3133
|
/* @__PURE__ */ jsxs(SheetHeader, { children: [
|
|
2633
3134
|
/* @__PURE__ */ jsx(SheetTitle, { children: formTitle }),
|
|
2634
3135
|
/* @__PURE__ */ jsx(SheetDescription, { className: "sr-only", children: formDescription })
|
|
@@ -2642,11 +3143,86 @@ function ResourceForm(props) {
|
|
|
2642
3143
|
/* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx(FormBody, { ...bodyProps }) })
|
|
2643
3144
|
] });
|
|
2644
3145
|
}
|
|
3146
|
+
function ConfirmDeleteDialog({
|
|
3147
|
+
open,
|
|
3148
|
+
onOpenChange,
|
|
3149
|
+
onConfirm,
|
|
3150
|
+
title = "Delete this record?",
|
|
3151
|
+
kind = "permanent",
|
|
3152
|
+
description = kind === "recycle" ? "It moves to the recycle bin, where it can be restored." : "This can't be undone.",
|
|
3153
|
+
confirmLabel = "Delete",
|
|
3154
|
+
cancelLabel = "Cancel"
|
|
3155
|
+
}) {
|
|
3156
|
+
const [pending, setPending] = useState(false);
|
|
3157
|
+
const [error, setError] = useState(null);
|
|
3158
|
+
useEffect(() => {
|
|
3159
|
+
if (open) setError(null);
|
|
3160
|
+
}, [open]);
|
|
3161
|
+
const confirm = async () => {
|
|
3162
|
+
setPending(true);
|
|
3163
|
+
setError(null);
|
|
3164
|
+
try {
|
|
3165
|
+
await onConfirm();
|
|
3166
|
+
onOpenChange(false);
|
|
3167
|
+
} catch (err) {
|
|
3168
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
3169
|
+
} finally {
|
|
3170
|
+
setPending(false);
|
|
3171
|
+
}
|
|
3172
|
+
};
|
|
3173
|
+
return /* @__PURE__ */ jsx(
|
|
3174
|
+
AlertDialog,
|
|
3175
|
+
{
|
|
3176
|
+
open,
|
|
3177
|
+
onOpenChange: (next) => {
|
|
3178
|
+
if (!pending) onOpenChange(next);
|
|
3179
|
+
},
|
|
3180
|
+
children: /* @__PURE__ */ jsxs(AlertDialogContent, { "data-testid": "confirm-delete-dialog", children: [
|
|
3181
|
+
/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [
|
|
3182
|
+
/* @__PURE__ */ jsx(AlertDialogTitle, { children: title }),
|
|
3183
|
+
/* @__PURE__ */ jsx(AlertDialogDescription, { children: description })
|
|
3184
|
+
] }),
|
|
3185
|
+
error ? /* @__PURE__ */ jsxs(
|
|
3186
|
+
"div",
|
|
3187
|
+
{
|
|
3188
|
+
role: "alert",
|
|
3189
|
+
"data-testid": "confirm-delete-error",
|
|
3190
|
+
className: "flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive",
|
|
3191
|
+
children: [
|
|
3192
|
+
/* @__PURE__ */ jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0", "aria-hidden": "true" }),
|
|
3193
|
+
/* @__PURE__ */ jsx("span", { children: error })
|
|
3194
|
+
]
|
|
3195
|
+
}
|
|
3196
|
+
) : null,
|
|
3197
|
+
/* @__PURE__ */ jsxs(AlertDialogFooter, { children: [
|
|
3198
|
+
/* @__PURE__ */ jsx(AlertDialogCancel, { disabled: pending, "data-testid": "confirm-delete-cancel", children: cancelLabel }),
|
|
3199
|
+
/* @__PURE__ */ jsxs(
|
|
3200
|
+
AlertDialogAction,
|
|
3201
|
+
{
|
|
3202
|
+
disabled: pending,
|
|
3203
|
+
"data-testid": "confirm-delete-confirm",
|
|
3204
|
+
onClick: (e) => {
|
|
3205
|
+
e.preventDefault();
|
|
3206
|
+
void confirm();
|
|
3207
|
+
},
|
|
3208
|
+
children: [
|
|
3209
|
+
pending ? /* @__PURE__ */ jsx(Spinner, { size: "sm", className: "mr-2" }) : null,
|
|
3210
|
+
confirmLabel
|
|
3211
|
+
]
|
|
3212
|
+
}
|
|
3213
|
+
)
|
|
3214
|
+
] })
|
|
3215
|
+
] })
|
|
3216
|
+
}
|
|
3217
|
+
);
|
|
3218
|
+
}
|
|
2645
3219
|
function ResourcePage(props) {
|
|
2646
3220
|
const {
|
|
2647
3221
|
resource: resourceName,
|
|
2648
3222
|
data: dataProp,
|
|
2649
3223
|
total: totalProp,
|
|
3224
|
+
pagination = "client",
|
|
3225
|
+
clientRowLimit = 1e3,
|
|
2650
3226
|
title,
|
|
2651
3227
|
subtitle,
|
|
2652
3228
|
icon,
|
|
@@ -2656,6 +3232,8 @@ function ResourcePage(props) {
|
|
|
2656
3232
|
detailWidth = "md",
|
|
2657
3233
|
titleField: titleFieldProp,
|
|
2658
3234
|
subtitleField: subtitleFieldProp,
|
|
3235
|
+
getTitle,
|
|
3236
|
+
getSubtitle,
|
|
2659
3237
|
renderDetail: renderDetailProp,
|
|
2660
3238
|
detailFields,
|
|
2661
3239
|
statusFilter,
|
|
@@ -2689,7 +3267,14 @@ function ResourcePage(props) {
|
|
|
2689
3267
|
onCreate,
|
|
2690
3268
|
onEdit,
|
|
2691
3269
|
onDelete,
|
|
3270
|
+
can,
|
|
3271
|
+
canEditItem,
|
|
2692
3272
|
createLabel = "New",
|
|
3273
|
+
canEdit,
|
|
3274
|
+
canDelete,
|
|
3275
|
+
allowDelete,
|
|
3276
|
+
confirmDelete,
|
|
3277
|
+
onDeleted,
|
|
2693
3278
|
secondaryActions,
|
|
2694
3279
|
allowCreate,
|
|
2695
3280
|
allowEdit,
|
|
@@ -2698,15 +3283,22 @@ function ResourcePage(props) {
|
|
|
2698
3283
|
formMode,
|
|
2699
3284
|
formSteps,
|
|
2700
3285
|
formLayout: formLayoutProp,
|
|
3286
|
+
mutationMode: mutationModeProp,
|
|
2701
3287
|
bulkActions,
|
|
2702
3288
|
renderCard: renderCardProp,
|
|
3289
|
+
openOnCardClick = false,
|
|
2703
3290
|
renderTable: renderTableProp,
|
|
2704
3291
|
gridCols: gridColsProp,
|
|
2705
3292
|
keyboardNavigation = true,
|
|
2706
3293
|
capabilities,
|
|
2707
3294
|
toolbar,
|
|
2708
3295
|
emptyState,
|
|
3296
|
+
emptyIcon,
|
|
3297
|
+
emptyMessage,
|
|
3298
|
+
emptyDescription,
|
|
3299
|
+
header: headerMode = "page",
|
|
2709
3300
|
resourceOptions,
|
|
3301
|
+
urlState: urlStateProp,
|
|
2710
3302
|
defaultView = "table",
|
|
2711
3303
|
onNavigate = defaultNavigate,
|
|
2712
3304
|
viewSettingsKey: viewSettingsKeyProp,
|
|
@@ -2715,14 +3307,46 @@ function ResourcePage(props) {
|
|
|
2715
3307
|
className
|
|
2716
3308
|
} = props;
|
|
2717
3309
|
const isClientMode = !!resourceName;
|
|
2718
|
-
const
|
|
3310
|
+
const resourceServerPaging = isClientMode && pagination === "server";
|
|
3311
|
+
const url = useResourceUrlState(
|
|
3312
|
+
urlStateProp ? "setSearchParams" in urlStateProp ? { searchParams: urlStateProp } : urlStateProp : void 0
|
|
3313
|
+
);
|
|
3314
|
+
const [urlSeed] = useState(() => {
|
|
3315
|
+
if (!url) return null;
|
|
3316
|
+
const sortable = !url.sort || !columnsProp ? true : columnsProp.some(
|
|
3317
|
+
(col) => typeof col === "string" ? col === url.sort.field : col.key === url.sort.field && col.sortable !== false
|
|
3318
|
+
);
|
|
3319
|
+
return { q: url.q, sort: sortable ? url.sort : null, page: url.page };
|
|
3320
|
+
});
|
|
3321
|
+
const urlPeek = detail === "panel" || detail === "modal" ? url : void 0;
|
|
3322
|
+
const urlSortKey = urlSeed?.sort ? `${urlSeed.sort.field}:${urlSeed.sort.order}` : "";
|
|
3323
|
+
const isServerControlled = resourceServerPaging || pageProp !== void 0 || perPageProp !== void 0 || onPageChange !== void 0 || onPerPageChange !== void 0 || onSortChange !== void 0 || onSearchChange !== void 0;
|
|
2719
3324
|
const viewSettingsKey = viewSettingsKeyProp ?? resourceName ?? title.toLowerCase().replace(/\s+/g, "-");
|
|
2720
3325
|
const { settings, updateSetting } = useViewSettings(viewSettingsKey);
|
|
2721
|
-
const
|
|
3326
|
+
const serverSearchFields = resourceOptions?.searchFields ?? searchFields;
|
|
2722
3327
|
const resourceHook = useResource(
|
|
2723
3328
|
resourceName ?? "__noop__",
|
|
2724
|
-
isClientMode ?
|
|
3329
|
+
isClientMode ? {
|
|
3330
|
+
...resourceOptions,
|
|
3331
|
+
// Client paging fetches everything it can in one request; server
|
|
3332
|
+
// paging starts at the user's stored page size. An explicit
|
|
3333
|
+
// `defaultPerPage` wins over both.
|
|
3334
|
+
defaultPerPage: resourceOptions?.defaultPerPage ?? (resourceServerPaging ? perPageProp ?? settings.pageSize : clientRowLimit),
|
|
3335
|
+
searchFields: resourceServerPaging ? serverSearchFields : resourceOptions?.searchFields,
|
|
3336
|
+
// Paging, searching and sorting change the query key; keep the
|
|
3337
|
+
// current page on screen while the next one loads.
|
|
3338
|
+
keepPreviousData: resourceOptions?.keepPreviousData ?? resourceServerPaging,
|
|
3339
|
+
// Start where the URL says, so restoring a link doesn't push a
|
|
3340
|
+
// search or sort that resets the page to 1.
|
|
3341
|
+
...resourceServerPaging && urlSeed ? {
|
|
3342
|
+
defaultSearch: urlSeed.q,
|
|
3343
|
+
...urlSeed.page ? { defaultPage: urlSeed.page } : {},
|
|
3344
|
+
...urlSeed.sort ? { defaultSort: urlSeed.sort } : {}
|
|
3345
|
+
} : {},
|
|
3346
|
+
...writeModeOptions(resourceOptions, mutationModeProp)
|
|
3347
|
+
} : { enabled: false }
|
|
2725
3348
|
);
|
|
3349
|
+
const effectivePageSize = resourceServerPaging ? resourceHook.params.pagination?.perPage ?? settings.pageSize : perPageProp ?? settings.pageSize;
|
|
2726
3350
|
const displayData = isClientMode ? resourceHook.list.data?.data ?? [] : dataProp ?? [];
|
|
2727
3351
|
const displayTotal = isClientMode ? resourceHook.list.data?.total ?? 0 : totalProp ?? 0;
|
|
2728
3352
|
const isLoading = isClientMode ? resourceHook.list.isLoading : loadingProp ?? false;
|
|
@@ -2734,14 +3358,80 @@ function ResourcePage(props) {
|
|
|
2734
3358
|
columns: columnsProp,
|
|
2735
3359
|
statusFilter,
|
|
2736
3360
|
searchFields,
|
|
2737
|
-
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),
|
|
2738
3365
|
onFiltersChange,
|
|
2739
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,
|
|
2740
3370
|
detail,
|
|
2741
3371
|
titleField: titleFieldProp,
|
|
2742
3372
|
subtitleField: subtitleFieldProp,
|
|
2743
3373
|
controlled: isServerControlled
|
|
2744
3374
|
});
|
|
3375
|
+
const {
|
|
3376
|
+
setSearch: setResourceSearch,
|
|
3377
|
+
setSort: setResourceSort,
|
|
3378
|
+
setFilters: setResourceFilters
|
|
3379
|
+
} = resourceHook;
|
|
3380
|
+
const pushedSearchRef = useRef(resourceServerPaging ? urlSeed?.q ?? "" : "");
|
|
3381
|
+
const searchEffectRanRef = useRef(false);
|
|
3382
|
+
useEffect(() => {
|
|
3383
|
+
if (!resourceServerPaging) return;
|
|
3384
|
+
const firstRun = !searchEffectRanRef.current;
|
|
3385
|
+
searchEffectRanRef.current = true;
|
|
3386
|
+
if (page.search === pushedSearchRef.current) return;
|
|
3387
|
+
const push = () => {
|
|
3388
|
+
pushedSearchRef.current = page.search;
|
|
3389
|
+
setResourceSearch(page.search);
|
|
3390
|
+
};
|
|
3391
|
+
if (firstRun || page.search === "") {
|
|
3392
|
+
push();
|
|
3393
|
+
return;
|
|
3394
|
+
}
|
|
3395
|
+
const timer = setTimeout(push, 300);
|
|
3396
|
+
return () => clearTimeout(timer);
|
|
3397
|
+
}, [resourceServerPaging, page.search, setResourceSearch]);
|
|
3398
|
+
const defaultSort = resourceOptions?.defaultSort;
|
|
3399
|
+
const pushedSortRef = useRef(resourceServerPaging ? urlSortKey : "");
|
|
3400
|
+
useEffect(() => {
|
|
3401
|
+
if (!resourceServerPaging) return;
|
|
3402
|
+
const key = page.sort ? `${page.sort.field}:${page.sort.order}` : "";
|
|
3403
|
+
if (key === pushedSortRef.current) return;
|
|
3404
|
+
pushedSortRef.current = key;
|
|
3405
|
+
setResourceSort(page.sort ?? defaultSort);
|
|
3406
|
+
}, [resourceServerPaging, page.sort, defaultSort, setResourceSort]);
|
|
3407
|
+
const statusField = statusFilter ? typeof statusFilter === "string" ? statusFilter : statusFilter.field : void 0;
|
|
3408
|
+
const defaultFilters = resourceOptions?.defaultFilters;
|
|
3409
|
+
const pushedStatusRef = useRef("");
|
|
3410
|
+
useEffect(() => {
|
|
3411
|
+
if (!resourceServerPaging || !statusField) return;
|
|
3412
|
+
const key = JSON.stringify([page.statusValue, defaultFilters ?? null]);
|
|
3413
|
+
if (key === pushedStatusRef.current) return;
|
|
3414
|
+
pushedStatusRef.current = key;
|
|
3415
|
+
setResourceFilters([
|
|
3416
|
+
...defaultFilters ?? [],
|
|
3417
|
+
...page.statusValue.length > 0 ? [{ field: statusField, operator: "in", value: page.statusValue }] : []
|
|
3418
|
+
]);
|
|
3419
|
+
}, [
|
|
3420
|
+
resourceServerPaging,
|
|
3421
|
+
statusField,
|
|
3422
|
+
page.statusValue,
|
|
3423
|
+
defaultFilters,
|
|
3424
|
+
setResourceFilters
|
|
3425
|
+
]);
|
|
3426
|
+
const clientTruncated = isClientMode && !resourceServerPaging && !isLoading && !hasError && displayTotal > displayData.length;
|
|
3427
|
+
const warnedTruncatedRef = useRef(false);
|
|
3428
|
+
useEffect(() => {
|
|
3429
|
+
if (!clientTruncated || warnedTruncatedRef.current) return;
|
|
3430
|
+
warnedTruncatedRef.current = true;
|
|
3431
|
+
console.warn(
|
|
3432
|
+
`ResourcePage: "${resourceName}" has ${displayTotal} rows but only ${displayData.length} were fetched. Pass \`pagination="server"\` to page through all of them, or raise \`clientRowLimit\`.`
|
|
3433
|
+
);
|
|
3434
|
+
}, [clientTruncated, resourceName, displayTotal, displayData.length]);
|
|
2745
3435
|
const actions = useMemo(() => {
|
|
2746
3436
|
if (isClientMode) {
|
|
2747
3437
|
return {
|
|
@@ -2774,6 +3464,17 @@ function ResourcePage(props) {
|
|
|
2774
3464
|
(item) => getKeyProp ? getKeyProp(item) : String(item.id ?? ""),
|
|
2775
3465
|
[getKeyProp]
|
|
2776
3466
|
);
|
|
3467
|
+
const snapshotKey = page.selectedItem ? keyOf(page.selectedItem) : null;
|
|
3468
|
+
const selectedKey = urlPeek ? urlPeek.detailId : snapshotKey;
|
|
3469
|
+
const snapshot = page.selectedItem && snapshotKey === selectedKey ? page.selectedItem : null;
|
|
3470
|
+
const rowOrSnapshot = !selectedKey ? snapshot : displayData.find((row) => keyOf(row) === selectedKey) ?? snapshot;
|
|
3471
|
+
const linkedId = urlPeek && selectedKey && !rowOrSnapshot ? selectedKey : null;
|
|
3472
|
+
const linked = useOne(resourceName ?? "__noop__", linkedId, {
|
|
3473
|
+
enabled: isClientMode && !!linkedId
|
|
3474
|
+
});
|
|
3475
|
+
const selectedItem = rowOrSnapshot ?? (linkedId && linked.data ? linked.data : null);
|
|
3476
|
+
const linkMissing = !!linkedId && !selectedItem && !isLoading && (isClientMode ? linked.isError : true);
|
|
3477
|
+
const detailOpen = urlPeek ? selectedItem !== null || linkMissing : page.detailOpen;
|
|
2777
3478
|
const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
|
|
2778
3479
|
const handleToggleSelect = useCallback((id) => {
|
|
2779
3480
|
setSelectedIds((prev) => {
|
|
@@ -2806,8 +3507,20 @@ function ResourcePage(props) {
|
|
|
2806
3507
|
onCreate();
|
|
2807
3508
|
}
|
|
2808
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
|
+
);
|
|
2809
3521
|
const handleEdit = useCallback(
|
|
2810
3522
|
(item) => {
|
|
3523
|
+
if (!allows("edit", item)) return;
|
|
2811
3524
|
if (useBuiltInEdit) {
|
|
2812
3525
|
setFormAction("edit");
|
|
2813
3526
|
setFormItem(item);
|
|
@@ -2816,14 +3529,83 @@ function ResourcePage(props) {
|
|
|
2816
3529
|
onEdit(item);
|
|
2817
3530
|
}
|
|
2818
3531
|
},
|
|
2819
|
-
[useBuiltInEdit, onEdit]
|
|
3532
|
+
[useBuiltInEdit, onEdit, allows]
|
|
3533
|
+
);
|
|
3534
|
+
const closeDetail = useCallback(() => {
|
|
3535
|
+
page.setDetailOpen(false);
|
|
3536
|
+
page.setSelectedItem(null);
|
|
3537
|
+
urlPeek?.setDetailId(null);
|
|
3538
|
+
}, [page, urlPeek]);
|
|
3539
|
+
const useBuiltInDelete = !!allowDelete && !onDelete && isClientMode;
|
|
3540
|
+
const deleteConfirmed = confirmDelete === void 0 ? useBuiltInDelete : !!confirmDelete;
|
|
3541
|
+
const deleteCopy = typeof confirmDelete === "object" ? confirmDelete : void 0;
|
|
3542
|
+
const [pendingDelete, setPendingDelete] = useState(null);
|
|
3543
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
3544
|
+
const { remove: removeRecord } = resourceHook;
|
|
3545
|
+
const { data: providerCaps } = useCapabilities(resourceName, {
|
|
3546
|
+
enabled: useBuiltInDelete && deleteConfirmed
|
|
3547
|
+
});
|
|
3548
|
+
const deleteKind = providerCaps?.softDelete ? "recycle" : "permanent";
|
|
3549
|
+
const performDelete = useCallback(
|
|
3550
|
+
async (item) => {
|
|
3551
|
+
if (useBuiltInDelete) {
|
|
3552
|
+
await removeRecord(
|
|
3553
|
+
getKeyProp ? keyOf(item) : item.id
|
|
3554
|
+
);
|
|
3555
|
+
} else if (onDelete) {
|
|
3556
|
+
await onDelete(item);
|
|
3557
|
+
}
|
|
3558
|
+
closeDetail();
|
|
3559
|
+
onDeleted?.(item);
|
|
3560
|
+
},
|
|
3561
|
+
[
|
|
3562
|
+
useBuiltInDelete,
|
|
3563
|
+
removeRecord,
|
|
3564
|
+
getKeyProp,
|
|
3565
|
+
keyOf,
|
|
3566
|
+
onDelete,
|
|
3567
|
+
closeDetail,
|
|
3568
|
+
onDeleted
|
|
3569
|
+
]
|
|
2820
3570
|
);
|
|
3571
|
+
const deleteHandler = !(useBuiltInDelete || onDelete) ? void 0 : deleteConfirmed ? (item) => {
|
|
3572
|
+
setPendingDelete(item);
|
|
3573
|
+
setDeleteOpen(true);
|
|
3574
|
+
} : useBuiltInDelete ? (item) => {
|
|
3575
|
+
performDelete(item).catch((err) => {
|
|
3576
|
+
if (!resourceOptions?.onError) {
|
|
3577
|
+
toast.error(err instanceof Error ? err.message : "The delete failed.");
|
|
3578
|
+
}
|
|
3579
|
+
});
|
|
3580
|
+
} : (
|
|
3581
|
+
// Unconfirmed `onDelete` keeps its original contract exactly:
|
|
3582
|
+
// called straight from the peek, nothing closed for it.
|
|
3583
|
+
(item) => {
|
|
3584
|
+
const report = (err) => {
|
|
3585
|
+
if (resourceOptions?.onError) {
|
|
3586
|
+
resourceOptions.onError(err instanceof Error ? err : new Error(String(err)));
|
|
3587
|
+
} else {
|
|
3588
|
+
toast.error(err instanceof Error ? err.message : "The delete failed.");
|
|
3589
|
+
}
|
|
3590
|
+
};
|
|
3591
|
+
try {
|
|
3592
|
+
Promise.resolve(onDelete?.(item)).catch(report);
|
|
3593
|
+
} catch (err) {
|
|
3594
|
+
report(err);
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
);
|
|
3598
|
+
const selectedEditable = !!selectedItem && allows("edit", selectedItem);
|
|
3599
|
+
const selectedDeletable = !!selectedItem && allows("delete", selectedItem);
|
|
2821
3600
|
const tableColumns = useMemo(() => {
|
|
2822
3601
|
const base = isClientMode ? page.columns.map((col) => ({
|
|
2823
3602
|
key: col.key,
|
|
2824
3603
|
header: col.header ?? col.key,
|
|
2825
3604
|
sortable: col.sortable ?? true,
|
|
2826
3605
|
hideBelow: col.hideBelow,
|
|
3606
|
+
width: col.width,
|
|
3607
|
+
minWidth: col.minWidth,
|
|
3608
|
+
defaultVisible: col.defaultVisible,
|
|
2827
3609
|
editable: col.editable,
|
|
2828
3610
|
accessor: col.accessor,
|
|
2829
3611
|
render: col.render ? (item, value) => col.render(item, value, actions) : void 0
|
|
@@ -2837,19 +3619,49 @@ function ResourcePage(props) {
|
|
|
2837
3619
|
page.tableColumns,
|
|
2838
3620
|
actions
|
|
2839
3621
|
]);
|
|
2840
|
-
const
|
|
3622
|
+
const [tableTieBreaks, setTableTieBreaks] = useState(null);
|
|
3623
|
+
const pageSort = page.sort;
|
|
3624
|
+
const setPageSort = page.setSort;
|
|
3625
|
+
const tableSort = useMemo(() => {
|
|
3626
|
+
if (!pageSort) return [];
|
|
3627
|
+
const primary = { key: pageSort.field, dir: pageSort.order };
|
|
3628
|
+
const rest = tableTieBreaks && tableTieBreaks.primary === `${primary.key}:${primary.dir}` ? tableTieBreaks.rest : [];
|
|
3629
|
+
return [primary, ...rest];
|
|
3630
|
+
}, [pageSort, tableTieBreaks]);
|
|
3631
|
+
const handleTableSort = useCallback(
|
|
3632
|
+
(next) => {
|
|
3633
|
+
const [primary, ...rest] = next;
|
|
3634
|
+
setTableTieBreaks(primary ? { primary: `${primary.key}:${primary.dir}`, rest } : null);
|
|
3635
|
+
setPageSort(primary ? { field: primary.key, order: primary.dir } : null);
|
|
3636
|
+
},
|
|
3637
|
+
[setPageSort]
|
|
3638
|
+
);
|
|
3639
|
+
const pagerPage = resourceServerPaging ? resourceHook.params.pagination?.page ?? 1 : pageProp;
|
|
3640
|
+
const pagerPerPage = resourceServerPaging ? resourceHook.params.pagination?.perPage : perPageProp;
|
|
3641
|
+
const pagerTotal = resourceServerPaging ? displayTotal : totalProp;
|
|
3642
|
+
const pagerOnPageChange = resourceServerPaging ? resourceHook.setPage : onPageChange;
|
|
3643
|
+
const currentPage = pagerPage ?? 1;
|
|
3644
|
+
const writeUrl = url?.write;
|
|
3645
|
+
useEffect(() => {
|
|
3646
|
+
writeUrl?.({
|
|
3647
|
+
q: page.search,
|
|
3648
|
+
sort: page.sort,
|
|
3649
|
+
...resourceServerPaging ? { page: currentPage } : {}
|
|
3650
|
+
});
|
|
3651
|
+
}, [writeUrl, page.search, page.sort, currentPage, resourceServerPaging]);
|
|
2841
3652
|
const handleSearchChange = useCallback(
|
|
2842
3653
|
(value) => {
|
|
2843
3654
|
page.filterConfig.onSearchChange(value);
|
|
2844
3655
|
if (!isServerControlled) return;
|
|
2845
3656
|
onSearchChange?.(value);
|
|
2846
|
-
if (currentPage !== 1) onPageChange?.(1);
|
|
3657
|
+
if (currentPage !== 1 && !resourceServerPaging) onPageChange?.(1);
|
|
2847
3658
|
},
|
|
2848
3659
|
[
|
|
2849
3660
|
page.filterConfig,
|
|
2850
3661
|
isServerControlled,
|
|
2851
3662
|
onSearchChange,
|
|
2852
3663
|
onPageChange,
|
|
3664
|
+
resourceServerPaging,
|
|
2853
3665
|
currentPage
|
|
2854
3666
|
]
|
|
2855
3667
|
);
|
|
@@ -2861,24 +3673,43 @@ function ResourcePage(props) {
|
|
|
2861
3673
|
onSortChange?.(
|
|
2862
3674
|
field && (order === "asc" || order === "desc") ? { field, order } : null
|
|
2863
3675
|
);
|
|
2864
|
-
onPageChange?.(1);
|
|
3676
|
+
if (!resourceServerPaging) onPageChange?.(1);
|
|
2865
3677
|
},
|
|
2866
|
-
[
|
|
3678
|
+
[
|
|
3679
|
+
page.filterConfig,
|
|
3680
|
+
isServerControlled,
|
|
3681
|
+
onSortChange,
|
|
3682
|
+
onPageChange,
|
|
3683
|
+
resourceServerPaging
|
|
3684
|
+
]
|
|
2867
3685
|
);
|
|
2868
3686
|
const handleClearFilters = useCallback(() => {
|
|
2869
3687
|
page.clearFilters();
|
|
2870
3688
|
if (!isServerControlled) return;
|
|
2871
3689
|
onSearchChange?.("");
|
|
2872
3690
|
onSortChange?.(null);
|
|
2873
|
-
onPageChange?.(1);
|
|
2874
|
-
}, [
|
|
3691
|
+
if (!resourceServerPaging) onPageChange?.(1);
|
|
3692
|
+
}, [
|
|
3693
|
+
page,
|
|
3694
|
+
isServerControlled,
|
|
3695
|
+
onSearchChange,
|
|
3696
|
+
onSortChange,
|
|
3697
|
+
onPageChange,
|
|
3698
|
+
resourceServerPaging
|
|
3699
|
+
]);
|
|
2875
3700
|
const handleStatusChange = useCallback(
|
|
2876
3701
|
(values) => {
|
|
2877
3702
|
page.filterConfig.statusChips?.onChange(values);
|
|
2878
3703
|
if (!isServerControlled) return;
|
|
2879
|
-
if (currentPage !== 1) onPageChange?.(1);
|
|
3704
|
+
if (currentPage !== 1 && !resourceServerPaging) onPageChange?.(1);
|
|
2880
3705
|
},
|
|
2881
|
-
[
|
|
3706
|
+
[
|
|
3707
|
+
page.filterConfig,
|
|
3708
|
+
isServerControlled,
|
|
3709
|
+
onPageChange,
|
|
3710
|
+
resourceServerPaging,
|
|
3711
|
+
currentPage
|
|
3712
|
+
]
|
|
2882
3713
|
);
|
|
2883
3714
|
const statusChipsConfig = useMemo(() => {
|
|
2884
3715
|
const base = page.filterConfig.statusChips;
|
|
@@ -2889,12 +3720,20 @@ function ResourcePage(props) {
|
|
|
2889
3720
|
(size) => {
|
|
2890
3721
|
updateSetting("pageSize", size);
|
|
2891
3722
|
onPerPageChange?.(size);
|
|
2892
|
-
if (
|
|
3723
|
+
if (resourceServerPaging) resourceHook.setPerPage(size);
|
|
3724
|
+
else if (isServerControlled) onPageChange?.(1);
|
|
2893
3725
|
},
|
|
2894
|
-
[
|
|
3726
|
+
[
|
|
3727
|
+
updateSetting,
|
|
3728
|
+
onPerPageChange,
|
|
3729
|
+
resourceServerPaging,
|
|
3730
|
+
resourceHook.setPerPage,
|
|
3731
|
+
isServerControlled,
|
|
3732
|
+
onPageChange
|
|
3733
|
+
]
|
|
2895
3734
|
);
|
|
2896
|
-
const missingPerPage = isServerControlled && !!
|
|
2897
|
-
const missingTotal = isServerControlled && !!
|
|
3735
|
+
const missingPerPage = isServerControlled && !!pagerOnPageChange && pagerPerPage === void 0;
|
|
3736
|
+
const missingTotal = isServerControlled && !!pagerOnPageChange && pagerTotal === void 0;
|
|
2898
3737
|
const pagerMisconfigured = missingPerPage || missingTotal;
|
|
2899
3738
|
const missingPagerProps = [
|
|
2900
3739
|
...missingPerPage ? ["perPage"] : [],
|
|
@@ -2909,18 +3748,23 @@ function ResourcePage(props) {
|
|
|
2909
3748
|
`ResourcePage: ${missingPagerProps.map((name) => `\`${name}\``).join(" and ")} ${missingPagerProps.length > 1 ? "are" : "is"} required when \`onPageChange\` is supplied.`
|
|
2910
3749
|
);
|
|
2911
3750
|
}, [pagerMisconfigured]);
|
|
2912
|
-
const pagerPageSize =
|
|
3751
|
+
const pagerPageSize = pagerPerPage ?? effectivePageSize;
|
|
2913
3752
|
const totalPages = Math.max(
|
|
2914
3753
|
1,
|
|
2915
3754
|
Math.ceil(displayTotal / Math.max(1, pagerPageSize))
|
|
2916
3755
|
);
|
|
2917
3756
|
const rangeFrom = page.data.length === 0 ? 0 : (currentPage - 1) * pagerPageSize + 1;
|
|
2918
3757
|
const rangeTo = rangeFrom === 0 ? 0 : rangeFrom + page.data.length - 1;
|
|
2919
|
-
const
|
|
2920
|
-
|
|
2921
|
-
)
|
|
2922
|
-
|
|
2923
|
-
|
|
3758
|
+
const { setPage: setResourcePage } = resourceHook;
|
|
3759
|
+
const settledPage = !isLoading && !hasError && !resourceHook.list.isPlaceholderData;
|
|
3760
|
+
useEffect(() => {
|
|
3761
|
+
if (!resourceServerPaging || !settledPage) return;
|
|
3762
|
+
if (currentPage > totalPages) setResourcePage(totalPages);
|
|
3763
|
+
}, [resourceServerPaging, settledPage, currentPage, totalPages, setResourcePage]);
|
|
3764
|
+
const titleOf = (item) => getTitle ? getTitle(item) : String(item[page.titleField] ?? "");
|
|
3765
|
+
const detailTitle = selectedItem ? titleOf(selectedItem) : "";
|
|
3766
|
+
const detailSubtitle = !selectedItem ? void 0 : getSubtitle ? getSubtitle(selectedItem) : page.subtitleField ? String(
|
|
3767
|
+
selectedItem[page.subtitleField] ?? ""
|
|
2924
3768
|
) : void 0;
|
|
2925
3769
|
const handleRowClick = (item) => {
|
|
2926
3770
|
if (detail === "route") {
|
|
@@ -2931,9 +3775,10 @@ function ResourcePage(props) {
|
|
|
2931
3775
|
return;
|
|
2932
3776
|
}
|
|
2933
3777
|
page.handleRowClick(item);
|
|
3778
|
+
urlPeek?.setDetailId(keyOf(item));
|
|
2934
3779
|
};
|
|
2935
3780
|
const hasBulkSelect = !!bulkActions && bulkActions.length > 0;
|
|
2936
|
-
const overlayOpen =
|
|
3781
|
+
const overlayOpen = detailOpen || formOpen || deleteOpen;
|
|
2937
3782
|
const keyboardNav = useKeyboardNavigation({
|
|
2938
3783
|
itemCount: page.data.length,
|
|
2939
3784
|
enabled: keyboardNavigation && page.viewMode !== "table",
|
|
@@ -2954,12 +3799,9 @@ function ResourcePage(props) {
|
|
|
2954
3799
|
// every grid/list page (including detail="none" and detail="route",
|
|
2955
3800
|
// which never open one). `detailOpen` is only ever true for the panel
|
|
2956
3801
|
// and modal modes, so it is the precise precondition here.
|
|
2957
|
-
onBack:
|
|
2958
|
-
page.setDetailOpen(false);
|
|
2959
|
-
page.setSelectedItem(null);
|
|
2960
|
-
} : void 0
|
|
3802
|
+
onBack: detailOpen ? closeDetail : void 0
|
|
2961
3803
|
});
|
|
2962
|
-
const showCreateButton = onCreate || useBuiltInCreate;
|
|
3804
|
+
const showCreateButton = (onCreate || useBuiltInCreate) && allows("create");
|
|
2963
3805
|
const createButton = showCreateButton ? /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: handleCreate, children: [
|
|
2964
3806
|
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4 mr-1" }),
|
|
2965
3807
|
createLabel
|
|
@@ -3059,7 +3901,8 @@ function ResourcePage(props) {
|
|
|
3059
3901
|
isLoading,
|
|
3060
3902
|
hasError,
|
|
3061
3903
|
isEmpty,
|
|
3062
|
-
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),
|
|
3063
3906
|
onRetry: isClientMode ? () => resourceHook.list.refetch() : onRetryProp,
|
|
3064
3907
|
viewMode: page.viewMode,
|
|
3065
3908
|
onViewModeChange: page.setViewMode,
|
|
@@ -3072,6 +3915,10 @@ function ResourcePage(props) {
|
|
|
3072
3915
|
hasActiveFilters: page.hasActiveFilters,
|
|
3073
3916
|
onClearFilters: handleClearFilters,
|
|
3074
3917
|
emptyAction: emptyState,
|
|
3918
|
+
emptyIcon,
|
|
3919
|
+
emptyMessage,
|
|
3920
|
+
emptyDescription,
|
|
3921
|
+
...headerMode === "toolbar" ? { header: "toolbar" } : {},
|
|
3075
3922
|
filters: renderSlot(
|
|
3076
3923
|
DataFilters,
|
|
3077
3924
|
slots?.filters,
|
|
@@ -3090,7 +3937,7 @@ function ResourcePage(props) {
|
|
|
3090
3937
|
onDeleteView,
|
|
3091
3938
|
canSaveViews,
|
|
3092
3939
|
onClearAll: page.hasActiveFilters ? handleClearFilters : void 0,
|
|
3093
|
-
showSearch: capabilities?.search !== false,
|
|
3940
|
+
showSearch: capabilities?.search !== false && !(resourceServerPaging && !serverSearchFields?.length),
|
|
3094
3941
|
showSort: capabilities?.sort !== false
|
|
3095
3942
|
},
|
|
3096
3943
|
slotProps?.filters
|
|
@@ -3150,18 +3997,42 @@ function ResourcePage(props) {
|
|
|
3150
3997
|
resizeKey,
|
|
3151
3998
|
stickyHeader,
|
|
3152
3999
|
stickyFirstColumn,
|
|
4000
|
+
keyboardNavigation,
|
|
3153
4001
|
// The server already cut the page window; DataTable's own
|
|
3154
4002
|
// pagination would slice the slice.
|
|
3155
|
-
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 }
|
|
3156
4007
|
},
|
|
3157
4008
|
slotProps?.table
|
|
3158
4009
|
),
|
|
4010
|
+
...openOnCardClick && renderCardProp && detail !== "none" ? {
|
|
4011
|
+
onItemClick: handleRowClick,
|
|
4012
|
+
getItemLabel: (item) => `Open ${titleOf(item)}`.trim()
|
|
4013
|
+
} : {},
|
|
3159
4014
|
renderCard: (item, index) => renderCardProp ? renderCardProp(item, index, cardActionsFor(item)) : renderDefaultCard(item),
|
|
3160
4015
|
renderListItem: (item, index) => renderCardProp ? renderCardProp(item, index, cardActionsFor(item)) : renderDefaultListItem(item)
|
|
3161
4016
|
},
|
|
3162
4017
|
slotProps?.grid
|
|
3163
4018
|
),
|
|
3164
|
-
|
|
4019
|
+
clientTruncated && /* @__PURE__ */ jsxs(
|
|
4020
|
+
"p",
|
|
4021
|
+
{
|
|
4022
|
+
role: "status",
|
|
4023
|
+
className: "px-3 py-2 text-sm text-muted-foreground",
|
|
4024
|
+
"data-testid": "resource-page-truncated",
|
|
4025
|
+
children: [
|
|
4026
|
+
"Showing the first ",
|
|
4027
|
+
displayData.length.toLocaleString(),
|
|
4028
|
+
" of",
|
|
4029
|
+
" ",
|
|
4030
|
+
displayTotal.toLocaleString(),
|
|
4031
|
+
" records."
|
|
4032
|
+
]
|
|
4033
|
+
}
|
|
4034
|
+
),
|
|
4035
|
+
isServerControlled && pagerOnPageChange && pagerMisconfigured && /* @__PURE__ */ jsxs(
|
|
3165
4036
|
"div",
|
|
3166
4037
|
{
|
|
3167
4038
|
className: "flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive",
|
|
@@ -3186,10 +4057,11 @@ function ResourcePage(props) {
|
|
|
3186
4057
|
]
|
|
3187
4058
|
}
|
|
3188
4059
|
),
|
|
3189
|
-
isServerControlled &&
|
|
4060
|
+
isServerControlled && pagerOnPageChange && !pagerMisconfigured && /* @__PURE__ */ jsxs(
|
|
3190
4061
|
"nav",
|
|
3191
4062
|
{
|
|
3192
4063
|
"aria-label": "Pagination",
|
|
4064
|
+
"aria-busy": resourceServerPaging && resourceHook.list.isPlaceholderData || void 0,
|
|
3193
4065
|
className: "flex flex-col sm:flex-row items-center justify-between gap-2 px-3 py-2 text-sm",
|
|
3194
4066
|
"data-testid": "resource-page-pagination",
|
|
3195
4067
|
children: [
|
|
@@ -3209,7 +4081,7 @@ function ResourcePage(props) {
|
|
|
3209
4081
|
variant: "outline",
|
|
3210
4082
|
size: "sm",
|
|
3211
4083
|
disabled: currentPage <= 1,
|
|
3212
|
-
onClick: () =>
|
|
4084
|
+
onClick: () => pagerOnPageChange(currentPage - 1),
|
|
3213
4085
|
testID: "resource-page-prev",
|
|
3214
4086
|
children: [
|
|
3215
4087
|
/* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4 mr-1", "aria-hidden": "true" }),
|
|
@@ -3236,7 +4108,7 @@ function ResourcePage(props) {
|
|
|
3236
4108
|
variant: "outline",
|
|
3237
4109
|
size: "sm",
|
|
3238
4110
|
disabled: currentPage >= totalPages,
|
|
3239
|
-
onClick: () =>
|
|
4111
|
+
onClick: () => pagerOnPageChange(currentPage + 1),
|
|
3240
4112
|
testID: "resource-page-next",
|
|
3241
4113
|
children: [
|
|
3242
4114
|
"Next",
|
|
@@ -3251,17 +4123,23 @@ function ResourcePage(props) {
|
|
|
3251
4123
|
(detail === "panel" || detail === "modal") && renderSlot(
|
|
3252
4124
|
DetailPanel,
|
|
3253
4125
|
slots?.detail,
|
|
3254
|
-
{
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
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
|
+
} : {
|
|
4136
|
+
item: selectedItem,
|
|
4137
|
+
open: detailOpen,
|
|
4138
|
+
onClose: closeDetail,
|
|
3261
4139
|
title: detailTitle,
|
|
3262
4140
|
subtitle: detailSubtitle,
|
|
3263
|
-
onEdit: onEdit ?? (useBuiltInEdit ? handleEdit : void 0),
|
|
3264
|
-
onDelete,
|
|
4141
|
+
onEdit: selectedEditable ? onEdit ?? (useBuiltInEdit ? handleEdit : void 0) : void 0,
|
|
4142
|
+
onDelete: selectedDeletable ? deleteHandler : void 0,
|
|
3265
4143
|
fields: detailFields,
|
|
3266
4144
|
width: detailWidth,
|
|
3267
4145
|
layout: detail === "modal" ? "dialog" : "sheet",
|
|
@@ -3278,6 +4156,7 @@ function ResourcePage(props) {
|
|
|
3278
4156
|
layout: formLayoutProp ?? (formAction === "edit" ? "sheet" : "dialog"),
|
|
3279
4157
|
mode: formMode,
|
|
3280
4158
|
steps: formSteps,
|
|
4159
|
+
mutationMode: mutationModeProp,
|
|
3281
4160
|
item: formAction === "edit" ? formItem ?? void 0 : page.data.length > 0 ? Object.fromEntries(
|
|
3282
4161
|
Object.keys(page.data[0]).filter((k) => k !== "id").map((k) => [k, ""])
|
|
3283
4162
|
) : void 0,
|
|
@@ -3287,11 +4166,25 @@ function ResourcePage(props) {
|
|
|
3287
4166
|
listData: page.data,
|
|
3288
4167
|
renderForm: renderFormProp,
|
|
3289
4168
|
onSuccess: () => {
|
|
3290
|
-
|
|
3291
|
-
page.setSelectedItem(null);
|
|
4169
|
+
if (formAction === "create") closeDetail();
|
|
3292
4170
|
}
|
|
3293
4171
|
},
|
|
3294
4172
|
slotProps?.form
|
|
4173
|
+
),
|
|
4174
|
+
deleteConfirmed && (useBuiltInDelete || !!onDelete) && renderSlot(
|
|
4175
|
+
ConfirmDeleteDialog,
|
|
4176
|
+
slots?.deleteDialog,
|
|
4177
|
+
{
|
|
4178
|
+
open: deleteOpen,
|
|
4179
|
+
onOpenChange: setDeleteOpen,
|
|
4180
|
+
onConfirm: () => pendingDelete ? performDelete(pendingDelete) : void 0,
|
|
4181
|
+
kind: deleteKind,
|
|
4182
|
+
title: typeof deleteCopy?.title === "function" ? pendingDelete ? deleteCopy.title(pendingDelete) : void 0 : deleteCopy?.title,
|
|
4183
|
+
description: typeof deleteCopy?.description === "function" ? pendingDelete ? deleteCopy.description(pendingDelete) : void 0 : deleteCopy?.description,
|
|
4184
|
+
confirmLabel: deleteCopy?.confirmLabel,
|
|
4185
|
+
cancelLabel: deleteCopy?.cancelLabel
|
|
4186
|
+
},
|
|
4187
|
+
slotProps?.deleteDialog
|
|
3295
4188
|
)
|
|
3296
4189
|
]
|
|
3297
4190
|
}
|
|
@@ -3371,6 +4264,6 @@ function ResourceDetailPage({
|
|
|
3371
4264
|
var AppShell2 = AppShell;
|
|
3372
4265
|
var PageHeader2 = PageHeader;
|
|
3373
4266
|
|
|
3374
|
-
export { AppShell2 as AppShell, BadgeCell, CurrencyCell, DataBulkBar, DataFilters, DataGrid, DataList, DataPageShell, DataTable, DateCell, DetailPanel, FieldArrayRenderer, FileUploadField, FilterCombobox, KanbanBoard, LinkCell, NumberCell, PageHeader2 as PageHeader, RelationField, ResourceDetailPage, ResourceForm, ResourcePage, StatCard, ThumbnailCell, WizardForm };
|
|
4267
|
+
export { AppShell2 as AppShell, BadgeCell, ConfirmDeleteDialog, CurrencyCell, DataBulkBar, DataFilters, DataGrid, DataList, DataPageShell, DataTable, DateCell, DetailPanel, FieldArrayRenderer, FileUploadField, FilterCombobox, KanbanBoard, LinkCell, NumberCell, PageHeader2 as PageHeader, RelationField, ResourceDetailPage, ResourceForm, ResourcePage, StatCard, ThumbnailCell, WizardForm };
|
|
3375
4268
|
//# sourceMappingURL=index.js.map
|
|
3376
4269
|
//# sourceMappingURL=index.js.map
|