@stackframe/dashboard-ui-components 2.8.89 → 2.8.91
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/dist/components/button.d.ts +4 -4
- package/dist/components/data-grid/data-grid-sizing.d.ts +6 -5
- package/dist/components/data-grid/data-grid-sizing.d.ts.map +1 -1
- package/dist/components/data-grid/data-grid-sizing.js +9 -28
- package/dist/components/data-grid/data-grid-sizing.js.map +1 -1
- package/dist/components/data-grid/data-grid.d.ts +17 -237
- package/dist/components/data-grid/data-grid.d.ts.map +1 -1
- package/dist/components/data-grid/data-grid.js +377 -523
- package/dist/components/data-grid/data-grid.js.map +1 -1
- package/dist/components/data-grid/data-grid.test.js +82 -0
- package/dist/components/data-grid/data-grid.test.js.map +1 -1
- package/dist/components/data-grid/index.d.ts +4 -3
- package/dist/components/data-grid/index.js +17 -58
- package/dist/components/data-grid/state.d.ts +4 -61
- package/dist/components/data-grid/state.d.ts.map +1 -1
- package/dist/components/data-grid/state.js +13 -160
- package/dist/components/data-grid/state.js.map +1 -1
- package/dist/components/data-grid/types.d.ts +9 -4
- package/dist/components/data-grid/types.d.ts.map +1 -1
- package/dist/components/data-grid/use-url-state.d.ts +38 -0
- package/dist/components/data-grid/use-url-state.d.ts.map +1 -0
- package/dist/components/data-grid/use-url-state.js +214 -0
- package/dist/components/data-grid/use-url-state.js.map +1 -0
- package/dist/components/data-grid/use-url-state.test.d.ts +1 -0
- package/dist/components/data-grid/use-url-state.test.js +91 -0
- package/dist/components/data-grid/use-url-state.test.js.map +1 -0
- package/dist/components/dialog.d.ts +67 -0
- package/dist/components/dialog.d.ts.map +1 -0
- package/dist/components/dialog.js +94 -0
- package/dist/components/dialog.js.map +1 -0
- package/dist/dashboard-ui-components.global.js +10648 -6394
- package/dist/dashboard-ui-components.global.js.map +4 -4
- package/dist/esm/components/button.d.ts +4 -4
- package/dist/esm/components/data-grid/data-grid-sizing.d.ts +6 -5
- package/dist/esm/components/data-grid/data-grid-sizing.d.ts.map +1 -1
- package/dist/esm/components/data-grid/data-grid-sizing.js +7 -26
- package/dist/esm/components/data-grid/data-grid-sizing.js.map +1 -1
- package/dist/esm/components/data-grid/data-grid.d.ts +17 -237
- package/dist/esm/components/data-grid/data-grid.d.ts.map +1 -1
- package/dist/esm/components/data-grid/data-grid.js +380 -526
- package/dist/esm/components/data-grid/data-grid.js.map +1 -1
- package/dist/esm/components/data-grid/data-grid.test.js +82 -0
- package/dist/esm/components/data-grid/data-grid.test.js.map +1 -1
- package/dist/esm/components/data-grid/index.d.ts +4 -3
- package/dist/esm/components/data-grid/index.js +4 -3
- package/dist/esm/components/data-grid/state.d.ts +4 -61
- package/dist/esm/components/data-grid/state.d.ts.map +1 -1
- package/dist/esm/components/data-grid/state.js +15 -150
- package/dist/esm/components/data-grid/state.js.map +1 -1
- package/dist/esm/components/data-grid/types.d.ts +9 -4
- package/dist/esm/components/data-grid/types.d.ts.map +1 -1
- package/dist/esm/components/data-grid/use-url-state.d.ts +38 -0
- package/dist/esm/components/data-grid/use-url-state.d.ts.map +1 -0
- package/dist/esm/components/data-grid/use-url-state.js +212 -0
- package/dist/esm/components/data-grid/use-url-state.js.map +1 -0
- package/dist/esm/components/data-grid/use-url-state.test.d.ts +1 -0
- package/dist/esm/components/data-grid/use-url-state.test.js +91 -0
- package/dist/esm/components/data-grid/use-url-state.test.js.map +1 -0
- package/dist/esm/components/dialog.d.ts +67 -0
- package/dist/esm/components/dialog.d.ts.map +1 -0
- package/dist/esm/components/dialog.js +86 -0
- package/dist/esm/components/dialog.js.map +1 -0
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.js +2 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +37 -0
- package/package.json +4 -3
|
@@ -3,83 +3,17 @@
|
|
|
3
3
|
import { ArrowDown, ArrowUp, CaretDown, CaretUp, CheckSquare, MinusSquare, Square } from "@phosphor-icons/react";
|
|
4
4
|
import { cn } from "@stackframe/stack-ui";
|
|
5
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
|
7
|
-
import {
|
|
6
|
+
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { exportToCsv, formatGridDate, resolveColumnValue } from "./state.js";
|
|
8
8
|
import { resolveDataGridStrings } from "./strings.js";
|
|
9
9
|
import { throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
|
10
|
+
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
|
10
11
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
11
12
|
import { DesignSkeleton } from "../skeleton.js";
|
|
12
|
-
import {
|
|
13
|
+
import { DEFAULT_COL_WIDTH, clampColumnWidth, getEffectiveMaxWidth, getEffectiveMinWidth } from "./data-grid-sizing.js";
|
|
13
14
|
import { DataGridToolbar } from "./data-grid-toolbar.js";
|
|
14
15
|
|
|
15
16
|
//#region src/components/data-grid/data-grid.tsx
|
|
16
|
-
function ResizeHandle({ onResize, onResizeEnd }) {
|
|
17
|
-
const startXRef = useRef(0);
|
|
18
|
-
const rafRef = useRef(0);
|
|
19
|
-
const latestDeltaRef = useRef(0);
|
|
20
|
-
const callbacksRef = useRef({
|
|
21
|
-
onResize,
|
|
22
|
-
onResizeEnd
|
|
23
|
-
});
|
|
24
|
-
callbacksRef.current = {
|
|
25
|
-
onResize,
|
|
26
|
-
onResizeEnd
|
|
27
|
-
};
|
|
28
|
-
const onPointerDown = useCallback((e) => {
|
|
29
|
-
e.preventDefault();
|
|
30
|
-
e.stopPropagation();
|
|
31
|
-
startXRef.current = e.clientX;
|
|
32
|
-
latestDeltaRef.current = 0;
|
|
33
|
-
const el = e.currentTarget;
|
|
34
|
-
el.setPointerCapture(e.pointerId);
|
|
35
|
-
let finished = false;
|
|
36
|
-
const onMove = (ev) => {
|
|
37
|
-
latestDeltaRef.current = ev.clientX - startXRef.current;
|
|
38
|
-
if (rafRef.current !== 0) return;
|
|
39
|
-
rafRef.current = requestAnimationFrame(() => {
|
|
40
|
-
rafRef.current = 0;
|
|
41
|
-
callbacksRef.current.onResize(latestDeltaRef.current);
|
|
42
|
-
});
|
|
43
|
-
};
|
|
44
|
-
const finish = () => {
|
|
45
|
-
if (finished) return;
|
|
46
|
-
finished = true;
|
|
47
|
-
if (rafRef.current !== 0) {
|
|
48
|
-
cancelAnimationFrame(rafRef.current);
|
|
49
|
-
rafRef.current = 0;
|
|
50
|
-
callbacksRef.current.onResize(latestDeltaRef.current);
|
|
51
|
-
}
|
|
52
|
-
el.removeEventListener("pointermove", onMove);
|
|
53
|
-
el.removeEventListener("pointerup", finish);
|
|
54
|
-
el.removeEventListener("pointercancel", finish);
|
|
55
|
-
el.removeEventListener("lostpointercapture", finish);
|
|
56
|
-
if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
|
|
57
|
-
callbacksRef.current.onResizeEnd();
|
|
58
|
-
};
|
|
59
|
-
el.addEventListener("pointermove", onMove);
|
|
60
|
-
el.addEventListener("pointerup", finish);
|
|
61
|
-
el.addEventListener("pointercancel", finish);
|
|
62
|
-
el.addEventListener("lostpointercapture", finish);
|
|
63
|
-
}, []);
|
|
64
|
-
return /* @__PURE__ */ jsx("div", {
|
|
65
|
-
className: cn("absolute right-0 top-0 bottom-0 z-10 w-[5px] cursor-col-resize touch-none", "group-hover/header:bg-foreground/[0.06] hover:!bg-blue-500/30", "transition-colors duration-100"),
|
|
66
|
-
onClick: (e) => {
|
|
67
|
-
e.preventDefault();
|
|
68
|
-
e.stopPropagation();
|
|
69
|
-
},
|
|
70
|
-
onPointerDown
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
function getNearestVerticalScrollElement(element) {
|
|
74
|
-
let current = element?.parentElement ?? null;
|
|
75
|
-
while (current) {
|
|
76
|
-
const style = window.getComputedStyle(current);
|
|
77
|
-
const overflowY = style.overflowY === "visible" ? style.overflow : style.overflowY;
|
|
78
|
-
if ((overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") && current.scrollHeight > current.clientHeight + 1) return current;
|
|
79
|
-
current = current.parentElement;
|
|
80
|
-
}
|
|
81
|
-
return window;
|
|
82
|
-
}
|
|
83
17
|
function getEventTargetElement(target) {
|
|
84
18
|
if (target instanceof Element) return target;
|
|
85
19
|
if (target instanceof Node) return target.parentElement;
|
|
@@ -101,41 +35,122 @@ function isDataGridInteractiveRowClickTarget(target) {
|
|
|
101
35
|
function shouldIgnoreRowClick(event) {
|
|
102
36
|
return event.defaultPrevented || isDataGridInteractiveRowClickTarget(event.target);
|
|
103
37
|
}
|
|
104
|
-
function
|
|
38
|
+
function toTanstackSorting(sorting) {
|
|
39
|
+
return sorting.map((s) => ({
|
|
40
|
+
id: s.columnId,
|
|
41
|
+
desc: s.direction === "desc"
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
function fromTanstackSorting(sorting) {
|
|
45
|
+
return sorting.map((s) => ({
|
|
46
|
+
columnId: s.id,
|
|
47
|
+
direction: s.desc ? "desc" : "asc"
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
function toTanstackRowSelection(ids) {
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const id of ids) out[id] = true;
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function resolveUpdater(updater, current) {
|
|
56
|
+
return typeof updater === "function" ? updater(current) : updater;
|
|
57
|
+
}
|
|
58
|
+
function distributeFlexWidths(sizes, visibleColumns, available) {
|
|
59
|
+
const flexCols = visibleColumns.filter((c) => c.flex != null && c.flex > 0);
|
|
60
|
+
if (flexCols.length === 0 || available <= 0) return;
|
|
61
|
+
const totalFlex = flexCols.reduce((acc, c) => acc + (c.flex ?? 0), 0);
|
|
62
|
+
let remaining = available;
|
|
63
|
+
flexCols.forEach((col, i) => {
|
|
64
|
+
const share = i === flexCols.length - 1 ? remaining : Math.floor(available * ((col.flex ?? 0) / totalFlex));
|
|
65
|
+
const max = col.maxWidth ?? Infinity;
|
|
66
|
+
const add = Math.max(0, Math.min(share, max - sizes[col.id]));
|
|
67
|
+
sizes[col.id] += add;
|
|
68
|
+
remaining -= add;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function selectSingle(current, rowId) {
|
|
72
|
+
const isSelected = current.selectedIds.has(rowId);
|
|
73
|
+
return {
|
|
74
|
+
selectedIds: isSelected ? /* @__PURE__ */ new Set() : new Set([rowId]),
|
|
75
|
+
anchorId: isSelected ? null : rowId
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function selectRange(current, rowId, allRowIds, additive) {
|
|
79
|
+
if (current.anchorId == null) return null;
|
|
80
|
+
const anchorIdx = allRowIds.indexOf(current.anchorId);
|
|
81
|
+
const currentIdx = allRowIds.indexOf(rowId);
|
|
82
|
+
if (anchorIdx < 0 || currentIdx < 0) return null;
|
|
83
|
+
const start = Math.min(anchorIdx, currentIdx);
|
|
84
|
+
const end = Math.max(anchorIdx, currentIdx);
|
|
85
|
+
const next = additive ? new Set(current.selectedIds) : /* @__PURE__ */ new Set();
|
|
86
|
+
for (let i = start; i <= end; i++) next.add(allRowIds[i]);
|
|
87
|
+
return {
|
|
88
|
+
selectedIds: next,
|
|
89
|
+
anchorId: current.anchorId
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function selectToggle(current, rowId) {
|
|
93
|
+
const next = new Set(current.selectedIds);
|
|
94
|
+
if (next.has(rowId)) next.delete(rowId);
|
|
95
|
+
else next.add(rowId);
|
|
96
|
+
return {
|
|
97
|
+
selectedIds: next,
|
|
98
|
+
anchorId: rowId
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function nextSelection(input) {
|
|
102
|
+
const { current, rowId, mode, modifiers, allRowIds } = input;
|
|
103
|
+
if (mode === "single") return selectSingle(current, rowId);
|
|
104
|
+
if (modifiers.shift) {
|
|
105
|
+
const range = selectRange(current, rowId, allRowIds, modifiers.ctrl);
|
|
106
|
+
if (range != null) return range;
|
|
107
|
+
}
|
|
108
|
+
if (modifiers.ctrl) return selectToggle(current, rowId);
|
|
109
|
+
return {
|
|
110
|
+
selectedIds: new Set([rowId]),
|
|
111
|
+
anchorId: rowId
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function HeaderCell({ header, col, resizable }) {
|
|
115
|
+
const sorted = header.column.getIsSorted();
|
|
116
|
+
const sortIndex = header.column.getSortIndex();
|
|
117
|
+
const totalSorts = header.column.getCanMultiSort() ? header.getContext().table.getState().sorting.length : 0;
|
|
105
118
|
const ctx = {
|
|
106
119
|
columnId: col.id,
|
|
107
120
|
columnDef: col,
|
|
108
|
-
isSorted,
|
|
109
|
-
sortIndex
|
|
121
|
+
isSorted: sorted === false ? false : sorted,
|
|
122
|
+
sortIndex: totalSorts > 1 && sortIndex >= 0 ? sortIndex + 1 : null
|
|
110
123
|
};
|
|
111
124
|
const label = typeof col.header === "function" ? col.header(ctx) : col.header;
|
|
112
|
-
const sortable =
|
|
125
|
+
const sortable = header.column.getCanSort();
|
|
126
|
+
const canResize = resizable && header.column.getCanResize();
|
|
127
|
+
const isResizing = header.column.getIsResizing();
|
|
113
128
|
return /* @__PURE__ */ jsxs("div", {
|
|
114
129
|
className: cn("group/header relative flex items-center gap-1.5 px-3 select-none bg-transparent overflow-hidden", "border-r border-black/[0.04] dark:border-white/[0.04] last:border-r-0", sortable && "cursor-pointer"),
|
|
115
|
-
style:
|
|
130
|
+
style: { width: `calc(var(--col-${col.id}-size) * 1px)` },
|
|
116
131
|
"data-col-id": col.id,
|
|
117
|
-
onClick:
|
|
132
|
+
onClick: sortable ? header.column.getToggleSortingHandler() : void 0,
|
|
118
133
|
role: "columnheader",
|
|
119
|
-
"aria-sort":
|
|
134
|
+
"aria-sort": sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : "none",
|
|
120
135
|
children: [
|
|
121
136
|
/* @__PURE__ */ jsx("span", {
|
|
122
137
|
className: cn("flex-1 min-w-0 truncate text-xs font-semibold uppercase tracking-wider text-muted-foreground", col.align === "center" && "text-center", col.align === "right" && "text-right"),
|
|
123
138
|
children: label
|
|
124
139
|
}),
|
|
125
|
-
|
|
140
|
+
sorted && /* @__PURE__ */ jsxs("span", {
|
|
126
141
|
className: "flex items-center gap-0.5 text-foreground/60",
|
|
127
|
-
children: [
|
|
142
|
+
children: [sorted === "asc" ? /* @__PURE__ */ jsx(ArrowUp, {
|
|
128
143
|
className: "h-3 w-3",
|
|
129
144
|
weight: "bold"
|
|
130
145
|
}) : /* @__PURE__ */ jsx(ArrowDown, {
|
|
131
146
|
className: "h-3 w-3",
|
|
132
147
|
weight: "bold"
|
|
133
|
-
}), sortIndex != null && /* @__PURE__ */ jsx("span", {
|
|
148
|
+
}), ctx.sortIndex != null && /* @__PURE__ */ jsx("span", {
|
|
134
149
|
className: "text-[10px] font-medium tabular-nums",
|
|
135
|
-
children: sortIndex
|
|
150
|
+
children: ctx.sortIndex
|
|
136
151
|
})]
|
|
137
152
|
}),
|
|
138
|
-
!
|
|
153
|
+
!sorted && sortable && /* @__PURE__ */ jsxs("span", {
|
|
139
154
|
className: "hidden group-hover/header:flex items-center text-foreground/20",
|
|
140
155
|
children: [/* @__PURE__ */ jsx(CaretUp, {
|
|
141
156
|
className: "h-2.5 w-2.5 -mb-[1px]",
|
|
@@ -145,9 +160,14 @@ function HeaderCell({ col, isSorted, sortIndex, resizable, onSort, onResize, onR
|
|
|
145
160
|
weight: "bold"
|
|
146
161
|
})]
|
|
147
162
|
}),
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
canResize && /* @__PURE__ */ jsx("div", {
|
|
164
|
+
onMouseDown: header.getResizeHandler(),
|
|
165
|
+
onTouchStart: header.getResizeHandler(),
|
|
166
|
+
onClick: (e) => {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
e.stopPropagation();
|
|
169
|
+
},
|
|
170
|
+
className: cn("absolute right-0 top-0 bottom-0 z-10 w-[5px] cursor-col-resize touch-none", "group-hover/header:bg-foreground/[0.06] hover:!bg-blue-500/30", "transition-colors duration-100", isResizing && "bg-blue-500/40")
|
|
151
171
|
})
|
|
152
172
|
]
|
|
153
173
|
});
|
|
@@ -172,7 +192,7 @@ function DataCell({ col, row, rowId, rowIndex, isSelected, dateDisplay }) {
|
|
|
172
192
|
const isWrap = col.cellOverflow === "wrap";
|
|
173
193
|
return /* @__PURE__ */ jsx("div", {
|
|
174
194
|
className: cn("flex px-3 bg-transparent overflow-hidden", "border-r border-black/[0.04] dark:border-white/[0.04] last:border-r-0", "text-sm text-foreground", isWrap ? "items-start py-2" : "items-center", col.align === "center" && "justify-center", col.align === "right" && "justify-end", hasCellClick && "cursor-pointer"),
|
|
175
|
-
style:
|
|
195
|
+
style: { width: `calc(var(--col-${col.id}-size) * 1px)` },
|
|
176
196
|
"data-col-id": col.id,
|
|
177
197
|
role: "gridcell",
|
|
178
198
|
onClick: col.onCellClick ? (e) => {
|
|
@@ -207,10 +227,6 @@ function formatCellValue(value) {
|
|
|
207
227
|
children: String(value)
|
|
208
228
|
});
|
|
209
229
|
}
|
|
210
|
-
/** Built-in date cell — mirrors what `formatGridDate` returns but wraps
|
|
211
|
-
* the display in a `<span>` with a `title` tooltip showing the absolute
|
|
212
|
-
* datetime. Only used when the column has `type: "date" | "dateTime"`
|
|
213
|
-
* and no custom `renderCell`. */
|
|
214
230
|
function renderDateCell(value, dateDisplay, col) {
|
|
215
231
|
const { display, tooltip } = formatGridDate(value, dateDisplay, {
|
|
216
232
|
parseValue: col.parseValue,
|
|
@@ -242,7 +258,7 @@ function SkeletonRow({ columns, height, showCheckbox }) {
|
|
|
242
258
|
children: /* @__PURE__ */ jsx(DesignSkeleton, { className: "h-4 w-4 rounded" })
|
|
243
259
|
}), columns.map((col) => /* @__PURE__ */ jsx("div", {
|
|
244
260
|
className: "flex items-center px-3 border-r border-black/[0.04] dark:border-white/[0.04] last:border-r-0",
|
|
245
|
-
style:
|
|
261
|
+
style: { width: `calc(var(--col-${col.id}-size) * 1px)` },
|
|
246
262
|
children: /* @__PURE__ */ jsx(DesignSkeleton, {
|
|
247
263
|
className: "h-3.5 rounded-md",
|
|
248
264
|
style: { width: `${40 + hashStringToInt(col.id) % 40}%` }
|
|
@@ -250,7 +266,7 @@ function SkeletonRow({ columns, height, showCheckbox }) {
|
|
|
250
266
|
}, col.id))]
|
|
251
267
|
});
|
|
252
268
|
}
|
|
253
|
-
function SelectionCheckbox({ checked, indeterminate, onChange, ariaLabel }) {
|
|
269
|
+
function SelectionCheckbox({ checked, indeterminate, onChange, ariaLabel, title }) {
|
|
254
270
|
const Icon = indeterminate ? MinusSquare : checked ? CheckSquare : Square;
|
|
255
271
|
return /* @__PURE__ */ jsx("button", {
|
|
256
272
|
className: cn("flex items-center justify-center w-full h-full", "hover:bg-foreground/[0.04] transition-colors duration-75", checked || indeterminate ? "text-blue-600 dark:text-blue-400" : "text-muted-foreground/40 hover:text-muted-foreground/60"),
|
|
@@ -259,6 +275,7 @@ function SelectionCheckbox({ checked, indeterminate, onChange, ariaLabel }) {
|
|
|
259
275
|
onChange(e);
|
|
260
276
|
},
|
|
261
277
|
"aria-label": ariaLabel,
|
|
278
|
+
title: title ?? ariaLabel,
|
|
262
279
|
role: "checkbox",
|
|
263
280
|
"aria-checked": indeterminate ? "mixed" : checked,
|
|
264
281
|
children: /* @__PURE__ */ jsx(Icon, {
|
|
@@ -373,396 +390,292 @@ function DefaultFooter({ ctx, pagination, onChange }) {
|
|
|
373
390
|
});
|
|
374
391
|
}
|
|
375
392
|
/**
|
|
376
|
-
* Interactive table
|
|
377
|
-
* and
|
|
378
|
-
*
|
|
379
|
-
*
|
|
393
|
+
* Interactive table built on TanStack Table v8. Sorting, column sizing,
|
|
394
|
+
* visibility, ordering, and pinning are owned by the table instance; we
|
|
395
|
+
* layer virtualization, sticky toolbar/header/footer, infinite scroll,
|
|
396
|
+
* quick search, CSV export, and date-format toggling on top.
|
|
380
397
|
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
* paginate your data directly — you own that, but `useDataSource` does
|
|
385
|
-
* it for you. The `rows` prop is always the already-processed slice to
|
|
386
|
-
* show. The grid tracks user intent in `state` (sort model, quick
|
|
387
|
-
* search text, page index). You feed that state into `useDataSource`,
|
|
388
|
-
* and its output goes back in as `rows`.
|
|
389
|
-
*
|
|
390
|
-
* `useDataSource` IS the processor. Given your full dataset and the
|
|
391
|
-
* grid's state, it returns the searched + sorted + paginated rows
|
|
392
|
-
* ready to pass to DataGrid. This is the ONLY correct pattern for
|
|
393
|
-
* client-side data — do NOT pass a raw array to `rows`.
|
|
394
|
-
*
|
|
395
|
-
* ## Search (client vs async)
|
|
396
|
-
*
|
|
397
|
-
* - **Client mode** (`useDataSource` with `data`): a case-insensitive
|
|
398
|
-
* substring match across every column is applied automatically.
|
|
399
|
-
* Override the matcher with `matchRow` for fuzzy / weighted search,
|
|
400
|
-
* or disable by passing `matchRow: () => true`.
|
|
401
|
-
* - **Async mode** (`useDataSource` with `dataSource`): `state.quickSearch`
|
|
402
|
-
* is forwarded to the generator as `params.quickSearch`. Same
|
|
403
|
-
* mechanism as `params.sorting` — a change triggers a refetch, and
|
|
404
|
-
* the generator is the "matching logic" (typically a WHERE / ILIKE
|
|
405
|
-
* clause in the backend query). The grid does NO client-side
|
|
406
|
-
* filtering in async mode.
|
|
407
|
-
*
|
|
408
|
-
* ## The canonical pattern
|
|
398
|
+
* The grid is display-only — it does not fetch or page data itself. Pair
|
|
399
|
+
* with `useDataSource` for client- or server-side data and pass the
|
|
400
|
+
* already-processed slice through `rows`.
|
|
409
401
|
*
|
|
410
402
|
* ```tsx
|
|
411
|
-
*
|
|
412
|
-
* const
|
|
413
|
-
* { id: "name", header: "Name", accessor: "name", width: 180, type: "string" },
|
|
414
|
-
* { id: "email", header: "Email", accessor: "email", width: 240, type: "string" },
|
|
415
|
-
* { id: "role", header: "Role", accessor: "role", width: 120, type: "singleSelect",
|
|
416
|
-
* valueOptions: [{ value: "admin", label: "Admin" }, { value: "member", label: "Member" }] },
|
|
417
|
-
* { id: "signUps", header: "Sign-ups", accessor: "signUps", width: 120, type: "number", align: "right",
|
|
418
|
-
* renderCell: ({ value }) => <span className="tabular-nums">{Number(value).toLocaleString()}</span> },
|
|
419
|
-
* ], []);
|
|
420
|
-
*
|
|
421
|
-
* // 2. Grid state — one hook, initialized from the columns. NEVER build the state object by hand.
|
|
422
|
-
* const [gridState, setGridState] = React.useState(() => createDefaultDataGridState(columns));
|
|
423
|
-
*
|
|
424
|
-
* // 3. Data source — wires your raw array through the grid state. ALWAYS call this
|
|
425
|
-
* // hook unconditionally at the top level (no if/return before it).
|
|
403
|
+
* const columns = useMemo(() => [...], []);
|
|
404
|
+
* const [gridState, setGridState] = useState(() => createDefaultDataGridState(columns));
|
|
426
405
|
* const gridData = useDataSource({
|
|
427
|
-
* data: users,
|
|
428
|
-
* columns,
|
|
429
|
-
* getRowId: (row) => row.id,
|
|
406
|
+
* data: users, columns, getRowId: (r) => r.id,
|
|
430
407
|
* sorting: gridState.sorting,
|
|
431
408
|
* quickSearch: gridState.quickSearch,
|
|
432
409
|
* pagination: gridState.pagination,
|
|
433
|
-
* paginationMode: "client",
|
|
410
|
+
* paginationMode: "client",
|
|
434
411
|
* });
|
|
435
412
|
*
|
|
436
|
-
* // 4. Render — `rows` comes from gridData.rows, NOT from your raw array.
|
|
437
413
|
* <DataGrid
|
|
438
414
|
* columns={columns}
|
|
439
415
|
* rows={gridData.rows}
|
|
440
|
-
* getRowId={(
|
|
416
|
+
* getRowId={(r) => r.id}
|
|
441
417
|
* totalRowCount={gridData.totalRowCount}
|
|
442
418
|
* isLoading={gridData.isLoading}
|
|
443
419
|
* state={gridState}
|
|
444
420
|
* onChange={setGridState}
|
|
445
|
-
* selectionMode="none" // "none" | "single" | "multiple"
|
|
446
|
-
* maxHeight={480}
|
|
447
421
|
* />
|
|
448
422
|
* ```
|
|
449
423
|
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
* `rows` — the grid won't search, sort, or paginate it.
|
|
456
|
-
* 3. Columns must be stable across renders. Define them outside the
|
|
457
|
-
* component or wrap in `React.useMemo`. A fresh columns array every
|
|
458
|
-
* render will reset sorting state.
|
|
459
|
-
* 4. Initialize grid state with `createDefaultDataGridState(columns)`.
|
|
460
|
-
* Do NOT spell out the state object manually — you will miss fields
|
|
461
|
-
* and crash.
|
|
462
|
-
* 5. `onChange` takes a `SetStateAction` (the setter you got from
|
|
463
|
-
* `useState`). Pass `setGridState` directly. Do NOT wrap it unless
|
|
464
|
-
* you know exactly what you're doing.
|
|
465
|
-
* 6. Call `useDataSource` ONCE per grid, at the top level, before any
|
|
466
|
-
* early return. It contains hooks.
|
|
467
|
-
* 7. `renderCell` is a PURE function of its context. NEVER call React
|
|
468
|
-
* hooks inside it (no `useState`, `useMemo`, `useEffect`, nothing).
|
|
469
|
-
* If you need derived data per row, compute it BEFORE the render —
|
|
470
|
-
* e.g. build a `Map<rowId, sparklineData>` in a `useMemo` and look
|
|
471
|
-
* it up in `renderCell`.
|
|
472
|
-
* 8. `toolbar` accepts `false` (hide it) or a render function
|
|
473
|
-
* `(ctx) => ReactNode`. Anything else — `true`, `undefined`, a state
|
|
474
|
-
* variable — will either show the default toolbar or crash. If you
|
|
475
|
-
* just want the default toolbar, omit the prop entirely.
|
|
476
|
-
* 9. The toolbar's search input writes to `state.quickSearch`. That
|
|
477
|
-
* value is consumed by `useDataSource` — client mode filters
|
|
478
|
-
* client-side, async mode forwards to the generator. Do NOT wire
|
|
479
|
-
* a separate "controlled" search prop, everything flows through
|
|
480
|
-
* grid state.
|
|
481
|
-
*
|
|
482
|
-
* ## renderCell — what you can and cannot do inside it
|
|
483
|
-
*
|
|
484
|
-
* ```tsx
|
|
485
|
-
* // OK — pure rendering from ctx:
|
|
486
|
-
* renderCell: ({ value }) => <span className="tabular-nums">{Number(value).toLocaleString()}</span>
|
|
487
|
-
* renderCell: ({ row }) => <Badge variant={row.active ? "default" : "outline"}>{row.status}</Badge>
|
|
488
|
-
*
|
|
489
|
-
* // OK — looking up pre-computed data by row id:
|
|
490
|
-
* // BEFORE the return, in the component body:
|
|
491
|
-
* const sparklinesById = React.useMemo(() => {
|
|
492
|
-
* const m = new Map();
|
|
493
|
-
* for (const u of users) {
|
|
494
|
-
* m.set(u.id, u.recentActivity.map((n, i) => ({ ts: i, values: { primary: n } })));
|
|
495
|
-
* }
|
|
496
|
-
* return m;
|
|
497
|
-
* }, [users]);
|
|
498
|
-
* // Then inside the column def:
|
|
499
|
-
* renderCell: ({ rowId }) => <MiniSparkline data={sparklinesById.get(rowId) ?? []} />
|
|
500
|
-
*
|
|
501
|
-
* // NOT OK — hooks inside renderCell:
|
|
502
|
-
* renderCell: ({ row }) => {
|
|
503
|
-
* const [hovered, setHovered] = React.useState(false); // ← crashes the grid
|
|
504
|
-
* const data = React.useMemo(() => ..., []); // ← crashes the grid
|
|
505
|
-
* return ...;
|
|
506
|
-
* }
|
|
507
|
-
*
|
|
508
|
-
* // NOT OK — embedding AnalyticsChart (or any other controlled, stateful chart) per row:
|
|
509
|
-
* // AnalyticsChart owns its own state, tooltips, zoom, and virtualized data
|
|
510
|
-
* // pipeline. Instantiating one per row is expensive and fights the grid's
|
|
511
|
-
* // virtualizer. Don't do it.
|
|
512
|
-
* ```
|
|
513
|
-
*
|
|
514
|
-
* ## Sparklines and mini-charts in cells — use raw Recharts
|
|
515
|
-
*
|
|
516
|
-
* If you want a tiny chart (sparkline, micro bar chart, trend line) inside
|
|
517
|
-
* a cell, drop down to raw `Recharts.*` components — they are lightweight
|
|
518
|
-
* and stateless, so they render cleanly per row without owning any state.
|
|
519
|
-
* Read pre-computed points off the row (or off a `Map<rowId, points>` you
|
|
520
|
-
* built in a `useMemo` above) and pass them directly to the Recharts
|
|
521
|
-
* primitive. Do NOT wrap them in `DesignChartContainer` or
|
|
522
|
-
* `DesignChartCard` inside a cell — those add chrome meant for full-size
|
|
523
|
-
* charts.
|
|
524
|
-
*
|
|
525
|
-
* ```tsx
|
|
526
|
-
* // OK — raw Recharts sparkline per row:
|
|
527
|
-
* renderCell: ({ rowId }) => {
|
|
528
|
-
* const points = sparklinesById.get(rowId) ?? [];
|
|
529
|
-
* return (
|
|
530
|
-
* <Recharts.ResponsiveContainer width="100%" height={28}>
|
|
531
|
-
* <Recharts.LineChart data={points} margin={{ top: 2, right: 2, bottom: 2, left: 2 }}>
|
|
532
|
-
* <Recharts.Line type="monotone" dataKey="v" stroke="currentColor" strokeWidth={1.5} dot={false} isAnimationActive={false} />
|
|
533
|
-
* </Recharts.LineChart>
|
|
534
|
-
* </Recharts.ResponsiveContainer>
|
|
535
|
-
* );
|
|
536
|
-
* }
|
|
537
|
-
* ```
|
|
538
|
-
*
|
|
539
|
-
* Keep in-cell Recharts configs minimal: no axes, no tooltips, no animation
|
|
540
|
-
* (`isAnimationActive={false}`), tight margins, fixed height. The goal is a
|
|
541
|
-
* visual summary, not an interactive chart.
|
|
542
|
-
*
|
|
543
|
-
* ## State shape (from `createDefaultDataGridState`)
|
|
544
|
-
*
|
|
545
|
-
* ```ts
|
|
546
|
-
* {
|
|
547
|
-
* sorting: [], // { columnId, direction: "asc" | "desc" }[]
|
|
548
|
-
* quickSearch: "", // search input text
|
|
549
|
-
* dateDisplay: "relative", // "relative" | "absolute"
|
|
550
|
-
* columnVisibility: {}, columnWidths: {...},
|
|
551
|
-
* columnPinning: { left: [], right: [] }, columnOrder: [...],
|
|
552
|
-
* pagination: { pageIndex: 0, pageSize: 50 },
|
|
553
|
-
* selection: { selectedIds: new Set(), anchorId: null },
|
|
554
|
-
* }
|
|
555
|
-
* ```
|
|
556
|
-
*
|
|
557
|
-
* Everything is updated through `setGridState` — the toolbar, header,
|
|
558
|
-
* and footer all call it for you. You do not need to wire any of this
|
|
559
|
-
* manually.
|
|
560
|
-
*
|
|
561
|
-
* ## Cell overflow and dynamic row heights
|
|
562
|
-
*
|
|
563
|
-
* By default every cell truncates its content with an ellipsis
|
|
564
|
-
* (`cellOverflow: "truncate"`). For columns whose content should wrap
|
|
565
|
-
* — badge lists, multi-line text, permission chips — set
|
|
566
|
-
* `cellOverflow: "wrap"` on the column definition.
|
|
567
|
-
*
|
|
568
|
-
* To let rows grow to fit their tallest cell, set `rowHeight="auto"`
|
|
569
|
-
* on the grid. The virtualizer will measure each row after render and
|
|
570
|
-
* adjust scroll positions accordingly. Pair with `estimatedRowHeight`
|
|
571
|
-
* (default 44) for better scroll-position estimates before measurement.
|
|
572
|
-
*
|
|
573
|
-
* ```tsx
|
|
574
|
-
* // Columns: UUIDs truncate, auth-method badges wrap
|
|
575
|
-
* const columns = [
|
|
576
|
-
* { id: "userId", header: "User ID", width: 130 }, // default truncate
|
|
577
|
-
* { id: "auth", header: "Auth methods", width: 150, cellOverflow: "wrap",
|
|
578
|
-
* renderCell: ({ row }) => (
|
|
579
|
-
* <div className="flex flex-wrap gap-1">
|
|
580
|
-
* {row.authTypes.map((t) => <Badge key={t}>{t}</Badge>)}
|
|
581
|
-
* </div>
|
|
582
|
-
* ),
|
|
583
|
-
* },
|
|
584
|
-
* ];
|
|
585
|
-
*
|
|
586
|
-
* <DataGrid columns={columns} rowHeight="auto" estimatedRowHeight={48} ... />
|
|
587
|
-
* ```
|
|
588
|
-
*
|
|
589
|
-
* With a fixed numeric `rowHeight` (the default), `cellOverflow: "wrap"`
|
|
590
|
-
* still lets content wrap within the row, but anything exceeding the
|
|
591
|
-
* fixed height is clipped. This is useful when you want controlled
|
|
592
|
-
* wrapping without variable row heights.
|
|
593
|
-
*
|
|
594
|
-
* ## Height and scrolling
|
|
595
|
-
*
|
|
596
|
-
* DataGrid is NOT a card. It has no border, rounded corners, or shadow of
|
|
597
|
-
* its own. Wrap it in whatever chrome you want — a `DesignCard`, a section,
|
|
598
|
-
* or just raw layout. The grid itself fills its parent's height via
|
|
599
|
-
* `h-full`.
|
|
600
|
-
*
|
|
601
|
-
* How the grid gets its height (pick ONE):
|
|
602
|
-
* 1. Bounded parent — put the grid inside a flex/grid container with a
|
|
603
|
-
* definite height (e.g. `flex-1 min-h-0` inside a page-filling flex
|
|
604
|
-
* column). The grid stretches to that height and scrolls its body.
|
|
605
|
-
* 2. `maxHeight` prop — pass a number (pixels) or CSS string
|
|
606
|
-
* (`"480px"`, `"60vh"`, `"100%"`). The grid caps at that size and
|
|
607
|
-
* scrolls its body.
|
|
608
|
-
* 3. Unbounded — omit `maxHeight` and let the parent grow freely. The
|
|
609
|
-
* grid renders at its full content height and the page scrolls. Fine
|
|
610
|
-
* for small lists; bad UX for thousands of rows.
|
|
611
|
-
*
|
|
612
|
-
* The toolbar, header, and footer are always `shrink-0`; only the body
|
|
613
|
-
* scrolls. You do NOT need to subtract toolbar/footer heights from
|
|
614
|
-
* `maxHeight` — the grid's internal flex layout handles that.
|
|
615
|
-
*
|
|
616
|
-
* ## When to use what
|
|
617
|
-
*
|
|
618
|
-
* - Simple static list, < 20 rows, no interaction → use a plain table component instead.
|
|
619
|
-
* - Interactive table, sortable + searchable, any size → `DataGrid` +
|
|
620
|
-
* `useDataSource` with `paginationMode: "client"`.
|
|
621
|
-
* - Infinite scroll over a huge dataset you fetch in pages → `dataSource` async
|
|
622
|
-
* generator + `paginationMode: "infinite"`. Only reach for this if you actually
|
|
623
|
-
* need pagination over a remote source. For anything that fits in memory,
|
|
624
|
-
* `"client"` is simpler and faster.
|
|
625
|
-
*
|
|
626
|
-
* ## Features you get for free
|
|
627
|
-
*
|
|
628
|
-
* Quick search, sortable columns (shift-click for multi-sort), column
|
|
629
|
-
* visibility toggle, column resize, CSV export, virtualized rendering
|
|
630
|
-
* for 10k+ rows, keyboard navigation, and a relative/absolute date
|
|
631
|
-
* toggle for `date` / `dateTime` columns.
|
|
424
|
+
* Iron rules:
|
|
425
|
+
* - `rows` is always `gridData.rows`, never your raw array.
|
|
426
|
+
* - Columns must be stable (define outside the component or wrap in `useMemo`).
|
|
427
|
+
* - Initialize state with `createDefaultDataGridState(columns)`.
|
|
428
|
+
* - `renderCell` must be a pure function — no React hooks inside.
|
|
632
429
|
*/
|
|
633
430
|
function DataGrid(props) {
|
|
634
|
-
const { columns: allColumns, rows, getRowId, totalRowCount, isLoading = false, isRefetching = false, hasMore = false, isLoadingMore = false, onLoadMore, state, onChange, paginationMode = "paginated", selectionMode = "none", resizable = true, rowHeight: rowHeightProp = 44, estimatedRowHeight: estimatedRowHeightProp, headerHeight = 44, overscan = 5, maxHeight, fillHeight = true, stickyTop, toolbar, toolbarExtra, emptyState, loadingState, footer, footerExtra, exportFilename = "export", strings: stringsOverride, className, onRowClick, onRowDoubleClick, onSelectionChange, onSortChange } = props;
|
|
431
|
+
const { columns: allColumns, rows, getRowId, totalRowCount, isLoading = false, isRefetching = false, hasMore = false, isLoadingMore = false, onLoadMore, state, onChange, paginationMode = "paginated", selectionMode = "none", resizable = true, rowHeight: rowHeightProp = 44, estimatedRowHeight: estimatedRowHeightProp, headerHeight = 44, overscan = 5, maxHeight, fillHeight = true, stickyTop, toolbar, toolbarExtra, emptyState, loadingState, footer, footerExtra, exportFilename = "export", strings: stringsOverride, className, onRowClick, onRowDoubleClick, onSelectionChange, onSortChange, onColumnResize, onColumnVisibilityChange } = props;
|
|
635
432
|
const isDynamicRowHeight = rowHeightProp === "auto";
|
|
636
433
|
const fixedRowHeight = isDynamicRowHeight ? void 0 : rowHeightProp;
|
|
637
434
|
const estimatedRowHeight = estimatedRowHeightProp ?? fixedRowHeight ?? 44;
|
|
638
435
|
const strings = useMemo(() => resolveDataGridStrings(stringsOverride), [stringsOverride]);
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
436
|
+
const tableColumns = useMemo(() => allColumns.map((col) => ({
|
|
437
|
+
id: col.id,
|
|
438
|
+
accessorFn: (row) => resolveColumnValue(col, row),
|
|
439
|
+
header: typeof col.header === "string" ? col.header : col.id,
|
|
440
|
+
size: col.width ?? DEFAULT_COL_WIDTH,
|
|
441
|
+
minSize: getEffectiveMinWidth(col),
|
|
442
|
+
maxSize: getEffectiveMaxWidth(col),
|
|
443
|
+
enableSorting: col.sortable !== false,
|
|
444
|
+
enableHiding: col.hideable !== false,
|
|
445
|
+
enableResizing: col.resizable !== false,
|
|
446
|
+
enableMultiSort: true
|
|
447
|
+
})), [allColumns]);
|
|
448
|
+
const tanstackSorting = useMemo(() => toTanstackSorting(state.sorting), [state.sorting]);
|
|
449
|
+
const tanstackRowSelection = useMemo(() => toTanstackRowSelection(state.selection.selectedIds), [state.selection.selectedIds]);
|
|
450
|
+
const tanstackColumnPinning = useMemo(() => ({
|
|
451
|
+
left: [...state.columnPinning.left],
|
|
452
|
+
right: [...state.columnPinning.right]
|
|
453
|
+
}), [state.columnPinning]);
|
|
454
|
+
const tanstackColumnOrder = useMemo(() => [...state.columnOrder], [state.columnOrder]);
|
|
455
|
+
const allColumnsRef = useRef(allColumns);
|
|
456
|
+
allColumnsRef.current = allColumns;
|
|
457
|
+
const handleSortingChange = useCallback((updater) => {
|
|
458
|
+
const ours = fromTanstackSorting(resolveUpdater(updater, toTanstackSorting(state.sorting))).map((s) => ({ ...s }));
|
|
459
|
+
onChange((s) => ({
|
|
460
|
+
...s,
|
|
461
|
+
sorting: ours,
|
|
462
|
+
pagination: {
|
|
463
|
+
...s.pagination,
|
|
464
|
+
pageIndex: 0
|
|
465
|
+
}
|
|
466
|
+
}));
|
|
467
|
+
onSortChange?.(ours);
|
|
468
|
+
}, [
|
|
469
|
+
onChange,
|
|
470
|
+
onSortChange,
|
|
471
|
+
state.sorting
|
|
643
472
|
]);
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
473
|
+
const handleColumnSizingChange = useCallback((updater) => {
|
|
474
|
+
const next = resolveUpdater(updater, state.columnWidths);
|
|
475
|
+
const clamped = {};
|
|
476
|
+
for (const [id, w] of Object.entries(next)) {
|
|
477
|
+
const col = allColumnsRef.current.find((c) => c.id === id);
|
|
478
|
+
clamped[id] = col ? clampColumnWidth(col, w) : w;
|
|
479
|
+
}
|
|
480
|
+
onChange((s) => ({
|
|
481
|
+
...s,
|
|
482
|
+
columnWidths: clamped
|
|
483
|
+
}));
|
|
484
|
+
if (onColumnResize) {
|
|
485
|
+
for (const [id, w] of Object.entries(clamped)) if (state.columnWidths[id] !== w) onColumnResize(id, w);
|
|
652
486
|
}
|
|
653
|
-
return {
|
|
654
|
-
widths,
|
|
655
|
-
totalWidth
|
|
656
|
-
};
|
|
657
487
|
}, [
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
488
|
+
onChange,
|
|
489
|
+
onColumnResize,
|
|
490
|
+
state.columnWidths
|
|
661
491
|
]);
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
const gridRef = useRef(null);
|
|
665
|
-
const handleSort = useCallback((columnId, multi) => {
|
|
666
|
-
const next = toggleSort(state.sorting, columnId, multi);
|
|
492
|
+
const handleVisibilityChange = useCallback((updater) => {
|
|
493
|
+
const next = resolveUpdater(updater, state.columnVisibility);
|
|
667
494
|
onChange((s) => ({
|
|
668
495
|
...s,
|
|
669
|
-
|
|
496
|
+
columnVisibility: next
|
|
670
497
|
}));
|
|
671
|
-
|
|
498
|
+
onColumnVisibilityChange?.(next);
|
|
672
499
|
}, [
|
|
673
500
|
onChange,
|
|
674
|
-
|
|
675
|
-
state.
|
|
501
|
+
onColumnVisibilityChange,
|
|
502
|
+
state.columnVisibility
|
|
676
503
|
]);
|
|
677
|
-
const
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
504
|
+
const handleColumnOrderChange = useCallback((updater) => {
|
|
505
|
+
const next = resolveUpdater(updater, [...state.columnOrder]);
|
|
506
|
+
onChange((s) => ({
|
|
507
|
+
...s,
|
|
508
|
+
columnOrder: next
|
|
509
|
+
}));
|
|
510
|
+
}, [onChange, state.columnOrder]);
|
|
511
|
+
const handleColumnPinningChange = useCallback((updater) => {
|
|
512
|
+
const next = resolveUpdater(updater, {
|
|
513
|
+
left: [...state.columnPinning.left],
|
|
514
|
+
right: [...state.columnPinning.right]
|
|
515
|
+
});
|
|
516
|
+
onChange((s) => ({
|
|
517
|
+
...s,
|
|
518
|
+
columnPinning: {
|
|
519
|
+
left: next.left ?? [],
|
|
520
|
+
right: next.right ?? []
|
|
521
|
+
}
|
|
522
|
+
}));
|
|
523
|
+
}, [onChange, state.columnPinning]);
|
|
524
|
+
const table = useReactTable({
|
|
525
|
+
data: rows,
|
|
526
|
+
columns: tableColumns,
|
|
527
|
+
getRowId: (row) => getRowId(row),
|
|
528
|
+
getCoreRowModel: getCoreRowModel(),
|
|
529
|
+
state: {
|
|
530
|
+
sorting: tanstackSorting,
|
|
531
|
+
columnVisibility: state.columnVisibility,
|
|
532
|
+
columnSizing: state.columnWidths,
|
|
533
|
+
columnOrder: tanstackColumnOrder,
|
|
534
|
+
columnPinning: tanstackColumnPinning,
|
|
535
|
+
rowSelection: tanstackRowSelection
|
|
536
|
+
},
|
|
537
|
+
onSortingChange: handleSortingChange,
|
|
538
|
+
onColumnSizingChange: handleColumnSizingChange,
|
|
539
|
+
onColumnVisibilityChange: handleVisibilityChange,
|
|
540
|
+
onColumnOrderChange: handleColumnOrderChange,
|
|
541
|
+
onColumnPinningChange: handleColumnPinningChange,
|
|
542
|
+
columnResizeMode: "onEnd",
|
|
543
|
+
enableRowSelection: selectionMode !== "none",
|
|
544
|
+
enableMultiRowSelection: selectionMode === "multiple",
|
|
545
|
+
enableColumnResizing: resizable,
|
|
546
|
+
manualSorting: true,
|
|
547
|
+
manualPagination: true,
|
|
548
|
+
manualFiltering: true
|
|
549
|
+
});
|
|
550
|
+
const visibleColumns = useMemo(() => {
|
|
551
|
+
const colMap = new Map(allColumns.map((c) => [c.id, c]));
|
|
552
|
+
return table.getVisibleLeafColumns().map((c) => colMap.get(c.id)).filter(Boolean);
|
|
692
553
|
}, [
|
|
693
554
|
allColumns,
|
|
694
|
-
|
|
695
|
-
|
|
555
|
+
table,
|
|
556
|
+
state.columnOrder,
|
|
557
|
+
state.columnVisibility
|
|
696
558
|
]);
|
|
559
|
+
const rowIds = useMemo(() => rows.map(getRowId), [rows, getRowId]);
|
|
560
|
+
const [containerWidth, setContainerWidth] = useState(0);
|
|
697
561
|
useLayoutEffect(() => {
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
562
|
+
const grid = gridRef.current;
|
|
563
|
+
const scroller = scrollContainerRef.current;
|
|
564
|
+
if (!grid) return;
|
|
565
|
+
const update = () => {
|
|
566
|
+
const w = scroller?.clientWidth ?? grid.clientWidth;
|
|
567
|
+
if (w > 0) setContainerWidth(w);
|
|
568
|
+
};
|
|
569
|
+
update();
|
|
570
|
+
const observer = new ResizeObserver(update);
|
|
571
|
+
observer.observe(grid);
|
|
572
|
+
if (scroller) observer.observe(scroller);
|
|
573
|
+
return () => observer.disconnect();
|
|
574
|
+
}, []);
|
|
575
|
+
const columnSizingInfo = table.getState().columnSizingInfo;
|
|
576
|
+
const columnSizes = useMemo(() => {
|
|
577
|
+
const sizes = {};
|
|
578
|
+
let baseTotal = selectionMode !== "none" ? 44 : 0;
|
|
579
|
+
const resizingId = columnSizingInfo.isResizingColumn || null;
|
|
580
|
+
const deltaOffset = columnSizingInfo.deltaOffset ?? 0;
|
|
581
|
+
for (const col of visibleColumns) {
|
|
582
|
+
const baseSize = table.getColumn(col.id)?.getSize() ?? col.width ?? DEFAULT_COL_WIDTH;
|
|
583
|
+
const liveSize = resizingId === col.id ? clampColumnWidth(col, baseSize + deltaOffset) : baseSize;
|
|
584
|
+
sizes[col.id] = liveSize;
|
|
585
|
+
baseTotal += liveSize;
|
|
586
|
+
}
|
|
587
|
+
distributeFlexWidths(sizes, visibleColumns, containerWidth - baseTotal);
|
|
588
|
+
return sizes;
|
|
589
|
+
}, [
|
|
590
|
+
visibleColumns,
|
|
591
|
+
table,
|
|
592
|
+
columnSizingInfo,
|
|
593
|
+
state.columnWidths,
|
|
594
|
+
containerWidth,
|
|
595
|
+
selectionMode
|
|
596
|
+
]);
|
|
597
|
+
const totalContentWidth = useMemo(() => {
|
|
598
|
+
let total = selectionMode !== "none" ? 44 : 0;
|
|
599
|
+
for (const col of visibleColumns) total += columnSizes[col.id] ?? 0;
|
|
600
|
+
return total;
|
|
601
|
+
}, [
|
|
602
|
+
visibleColumns,
|
|
603
|
+
columnSizes,
|
|
604
|
+
selectionMode
|
|
605
|
+
]);
|
|
606
|
+
const cssVars = useMemo(() => {
|
|
607
|
+
const vars = { "--grid-total-w": `${totalContentWidth}px` };
|
|
608
|
+
for (const col of visibleColumns) vars[`--col-${col.id}-size`] = columnSizes[col.id];
|
|
609
|
+
return vars;
|
|
610
|
+
}, [
|
|
611
|
+
visibleColumns,
|
|
612
|
+
columnSizes,
|
|
613
|
+
totalContentWidth
|
|
614
|
+
]);
|
|
615
|
+
const fireSelection = useCallback((next) => {
|
|
705
616
|
onChange((s) => ({
|
|
706
617
|
...s,
|
|
707
|
-
|
|
708
|
-
...s.columnWidths,
|
|
709
|
-
[r.columnId]: r.latestWidth
|
|
710
|
-
}
|
|
618
|
+
selection: next
|
|
711
619
|
}));
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const next = toggleRowSelection(state.selection, rowId, selectionMode, event.shiftKey, event.metaKey || event.ctrlKey, rowIds);
|
|
716
|
-
onChange((s) => ({
|
|
717
|
-
...s,
|
|
718
|
-
selection: next
|
|
719
|
-
}));
|
|
720
|
-
if (onSelectionChange) {
|
|
721
|
-
const selectedRows = rows.filter((r) => next.selectedIds.has(getRowId(r)));
|
|
722
|
-
onSelectionChange(next.selectedIds, selectedRows);
|
|
723
|
-
}
|
|
620
|
+
if (onSelectionChange) {
|
|
621
|
+
const idSet = next.selectedIds;
|
|
622
|
+
onSelectionChange(idSet, rows.filter((r) => idSet.has(getRowId(r))));
|
|
724
623
|
}
|
|
725
|
-
onRowClick?.(row, rowId, event);
|
|
726
624
|
}, [
|
|
727
|
-
selectionMode,
|
|
728
625
|
onChange,
|
|
729
|
-
onRowClick,
|
|
730
626
|
onSelectionChange,
|
|
731
|
-
rowIds,
|
|
732
627
|
rows,
|
|
733
|
-
getRowId
|
|
734
|
-
state.selection
|
|
628
|
+
getRowId
|
|
735
629
|
]);
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
630
|
+
const handleRowClick = useCallback((row, rowId, event) => {
|
|
631
|
+
if (selectionMode !== "none") fireSelection(nextSelection({
|
|
632
|
+
current: state.selection,
|
|
633
|
+
rowId,
|
|
634
|
+
mode: selectionMode,
|
|
635
|
+
modifiers: {
|
|
636
|
+
shift: event.shiftKey,
|
|
637
|
+
ctrl: event.metaKey || event.ctrlKey
|
|
638
|
+
},
|
|
639
|
+
allRowIds: rowIds
|
|
746
640
|
}));
|
|
747
|
-
|
|
641
|
+
onRowClick?.(row, rowId, event);
|
|
748
642
|
}, [
|
|
749
|
-
|
|
643
|
+
selectionMode,
|
|
644
|
+
state.selection,
|
|
750
645
|
rowIds,
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
646
|
+
fireSelection,
|
|
647
|
+
onRowClick
|
|
648
|
+
]);
|
|
649
|
+
const handleSelectAll = useCallback(() => {
|
|
650
|
+
fireSelection(rowIds.length > 0 && rowIds.every((id) => state.selection.selectedIds.has(id)) ? {
|
|
651
|
+
selectedIds: /* @__PURE__ */ new Set(),
|
|
652
|
+
anchorId: null
|
|
653
|
+
} : {
|
|
654
|
+
selectedIds: new Set(rowIds),
|
|
655
|
+
anchorId: null
|
|
656
|
+
});
|
|
657
|
+
}, [
|
|
658
|
+
rowIds,
|
|
659
|
+
state.selection.selectedIds,
|
|
660
|
+
fireSelection
|
|
754
661
|
]);
|
|
755
662
|
const handleExportCsv = useCallback(() => {
|
|
663
|
+
if (typeof window !== "undefined" && rows.length > 0) {
|
|
664
|
+
const totalSuffix = totalRowCount != null && totalRowCount > rows.length ? ` of ${totalRowCount} total — load more rows first to include them` : "";
|
|
665
|
+
if (!window.confirm(`Export ${rows.length.toLocaleString()} loaded row${rows.length === 1 ? "" : "s"}${totalSuffix}?`)) return;
|
|
666
|
+
}
|
|
756
667
|
exportToCsv(rows, visibleColumns, exportFilename);
|
|
757
668
|
}, [
|
|
758
669
|
rows,
|
|
759
670
|
visibleColumns,
|
|
760
|
-
exportFilename
|
|
671
|
+
exportFilename,
|
|
672
|
+
totalRowCount
|
|
761
673
|
]);
|
|
762
674
|
const scrollContainerRef = useRef(null);
|
|
763
675
|
const headerScrollRef = useRef(null);
|
|
764
676
|
const stickyChromeRef = useRef(null);
|
|
765
677
|
const rowsClipRef = useRef(null);
|
|
678
|
+
const gridRef = useRef(null);
|
|
766
679
|
const measureElementFn = useCallback((el) => el.getBoundingClientRect().height, []);
|
|
767
680
|
const rowVirtualizer = useVirtualizer({
|
|
768
681
|
count: rows.length,
|
|
@@ -775,104 +688,33 @@ function DataGrid(props) {
|
|
|
775
688
|
},
|
|
776
689
|
...isDynamicRowHeight ? { measureElement: measureElementFn } : {}
|
|
777
690
|
});
|
|
778
|
-
useLayoutEffect(() => {
|
|
779
|
-
const grid = gridRef.current;
|
|
780
|
-
const stickyEl = stickyChromeRef.current;
|
|
781
|
-
if (!grid || !stickyEl) return;
|
|
782
|
-
const parseRgba = (raw) => {
|
|
783
|
-
const rgbaMatch = raw.match(/rgba?\(\s*([\d.]+),\s*([\d.]+),\s*([\d.]+)(?:,\s*([\d.]+))?\s*\)/);
|
|
784
|
-
if (!rgbaMatch) return null;
|
|
785
|
-
const alphaRaw = rgbaMatch[4];
|
|
786
|
-
return [
|
|
787
|
-
Number(rgbaMatch[1]),
|
|
788
|
-
Number(rgbaMatch[2]),
|
|
789
|
-
Number(rgbaMatch[3]),
|
|
790
|
-
alphaRaw === void 0 ? 1 : Number(alphaRaw)
|
|
791
|
-
];
|
|
792
|
-
};
|
|
793
|
-
const blendOver = (base, top) => {
|
|
794
|
-
const [tr, tg, tb, ta] = top;
|
|
795
|
-
const [br, bg, bb, ba] = base;
|
|
796
|
-
const outA = ta + ba * (1 - ta);
|
|
797
|
-
if (outA === 0) return [
|
|
798
|
-
0,
|
|
799
|
-
0,
|
|
800
|
-
0,
|
|
801
|
-
0
|
|
802
|
-
];
|
|
803
|
-
return [
|
|
804
|
-
(tr * ta + br * ba * (1 - ta)) / outA,
|
|
805
|
-
(tg * ta + bg * ba * (1 - ta)) / outA,
|
|
806
|
-
(tb * ta + bb * ba * (1 - ta)) / outA,
|
|
807
|
-
outA
|
|
808
|
-
];
|
|
809
|
-
};
|
|
810
|
-
const detect = () => {
|
|
811
|
-
const layers = [];
|
|
812
|
-
let ancestor = grid.parentElement;
|
|
813
|
-
while (ancestor) {
|
|
814
|
-
const parsed = parseRgba(getComputedStyle(ancestor).backgroundColor);
|
|
815
|
-
if (parsed && parsed[3] > 0) {
|
|
816
|
-
layers.push(parsed);
|
|
817
|
-
if (parsed[3] >= 1) break;
|
|
818
|
-
}
|
|
819
|
-
ancestor = ancestor.parentElement;
|
|
820
|
-
}
|
|
821
|
-
if (layers.length === 0) {
|
|
822
|
-
stickyEl.style.backgroundColor = "";
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
let result = layers[layers.length - 1];
|
|
826
|
-
for (let i = layers.length - 2; i >= 0; i--) result = blendOver(result, layers[i]);
|
|
827
|
-
const [r, g, b] = result;
|
|
828
|
-
stickyEl.style.backgroundColor = `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`;
|
|
829
|
-
};
|
|
830
|
-
detect();
|
|
831
|
-
const observer = new MutationObserver(detect);
|
|
832
|
-
observer.observe(document.documentElement, {
|
|
833
|
-
attributes: true,
|
|
834
|
-
attributeFilter: ["class"]
|
|
835
|
-
});
|
|
836
|
-
return () => observer.disconnect();
|
|
837
|
-
}, []);
|
|
838
691
|
useLayoutEffect(() => {
|
|
839
692
|
const gridEl = gridRef.current;
|
|
840
693
|
const stickyEl = stickyChromeRef.current;
|
|
841
694
|
const bodyEl = scrollContainerRef.current;
|
|
842
695
|
const clipEl = rowsClipRef.current;
|
|
843
696
|
if (!gridEl || !stickyEl || !bodyEl || !clipEl) return;
|
|
844
|
-
const verticalScrollEl = fillHeight ? bodyEl : getNearestVerticalScrollElement(gridEl);
|
|
845
|
-
let extraObservedScrollEl = null;
|
|
846
|
-
if (verticalScrollEl instanceof HTMLElement && verticalScrollEl !== bodyEl) extraObservedScrollEl = verticalScrollEl;
|
|
847
697
|
const updateClip = () => {
|
|
848
698
|
const stickyRect = stickyEl.getBoundingClientRect();
|
|
849
699
|
const clipRect = clipEl.getBoundingClientRect();
|
|
850
700
|
const overlap = Math.max(0, stickyRect.bottom - clipRect.top);
|
|
851
|
-
|
|
852
|
-
const maskValue = overlap > 0 ? `linear-gradient(to bottom, transparent 0px, transparent ${overlap}px, black ${overlap}px, black 100%)` : "";
|
|
853
|
-
clipEl.style.clipPath = clipValue;
|
|
854
|
-
clipEl.style.setProperty("-webkit-clip-path", clipValue);
|
|
855
|
-
clipEl.style.maskImage = maskValue;
|
|
856
|
-
clipEl.style.setProperty("-webkit-mask-image", maskValue);
|
|
701
|
+
clipEl.style.setProperty("--data-grid-sticky-overlap", `${overlap}px`);
|
|
857
702
|
};
|
|
858
703
|
updateClip();
|
|
859
704
|
bodyEl.addEventListener("scroll", updateClip);
|
|
860
|
-
|
|
861
|
-
else if (extraObservedScrollEl) extraObservedScrollEl.addEventListener("scroll", updateClip);
|
|
705
|
+
window.addEventListener("scroll", updateClip, true);
|
|
862
706
|
window.addEventListener("resize", updateClip);
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
if (extraObservedScrollEl) ro.observe(extraObservedScrollEl);
|
|
707
|
+
const observer = new ResizeObserver(updateClip);
|
|
708
|
+
observer.observe(gridEl);
|
|
709
|
+
observer.observe(stickyEl);
|
|
710
|
+
observer.observe(bodyEl);
|
|
868
711
|
return () => {
|
|
869
712
|
bodyEl.removeEventListener("scroll", updateClip);
|
|
870
|
-
|
|
871
|
-
else if (extraObservedScrollEl) extraObservedScrollEl.removeEventListener("scroll", updateClip);
|
|
713
|
+
window.removeEventListener("scroll", updateClip, true);
|
|
872
714
|
window.removeEventListener("resize", updateClip);
|
|
873
|
-
|
|
715
|
+
observer.disconnect();
|
|
874
716
|
};
|
|
875
|
-
}, [
|
|
717
|
+
}, []);
|
|
876
718
|
const handleBodyScroll = useCallback(() => {
|
|
877
719
|
const body = scrollContainerRef.current;
|
|
878
720
|
const header = headerScrollRef.current;
|
|
@@ -913,21 +755,28 @@ function DataGrid(props) {
|
|
|
913
755
|
const allSelected = rowIds.length > 0 && rowIds.every((id) => state.selection.selectedIds.has(id));
|
|
914
756
|
const someSelected = !allSelected && rowIds.some((id) => state.selection.selectedIds.has(id));
|
|
915
757
|
const infiniteScrollRootRef = paginationMode === "infinite" && (fillHeight || maxHeight != null) ? scrollContainerRef : void 0;
|
|
758
|
+
const headers = useMemo(() => table.getHeaderGroups()[0]?.headers.filter((h) => visibleColumns.some((c) => c.id === h.column.id)) ?? [], [table, visibleColumns]);
|
|
759
|
+
const headerByColId = useMemo(() => {
|
|
760
|
+
const m = /* @__PURE__ */ new Map();
|
|
761
|
+
for (const h of headers) m.set(h.column.id, h);
|
|
762
|
+
return m;
|
|
763
|
+
}, [headers]);
|
|
764
|
+
const isBounded = fillHeight || maxHeight != null;
|
|
916
765
|
return /* @__PURE__ */ jsxs("div", {
|
|
917
766
|
ref: gridRef,
|
|
918
|
-
className: cn("flex w-full min-w-0 max-w-full flex-col bg-transparent rounded-[calc(var(--radius)*2)]", fillHeight ? "min-h-0 h-full" : "min-h-0 h-auto", className),
|
|
767
|
+
className: cn("isolate flex w-full min-w-0 max-w-full flex-col bg-transparent rounded-[calc(var(--radius)*2)]", fillHeight ? "min-h-0 h-full" : "min-h-0 h-auto", isBounded && "overflow-hidden", className),
|
|
919
768
|
style: maxHeight != null ? {
|
|
920
|
-
...
|
|
769
|
+
...cssVars,
|
|
921
770
|
maxHeight
|
|
922
|
-
} :
|
|
771
|
+
} : cssVars,
|
|
923
772
|
role: "grid",
|
|
924
773
|
"aria-rowcount": totalRowCount ?? rows.length,
|
|
925
774
|
"aria-colcount": visibleColumns.length,
|
|
926
775
|
children: [
|
|
927
776
|
/* @__PURE__ */ jsxs("div", {
|
|
928
777
|
ref: stickyChromeRef,
|
|
929
|
-
className: "sticky z-
|
|
930
|
-
style: { top: stickyTop ?? "var(--data-grid-sticky-top, 0px)" },
|
|
778
|
+
className: "sticky z-30 w-full min-w-0 shrink-0 overflow-visible rounded-t-[calc(var(--radius)*2)] bg-white/90 dark:bg-background/60 backdrop-blur-xl",
|
|
779
|
+
style: { top: stickyTop ?? (maxHeight != null ? 0 : "var(--data-grid-sticky-top, 0px)") },
|
|
931
780
|
children: [toolbar !== false && /* @__PURE__ */ jsx("div", {
|
|
932
781
|
className: "relative bg-transparent",
|
|
933
782
|
children: toolbar ? toolbar(toolbarCtx) : /* @__PURE__ */ jsx(DataGridToolbar, {
|
|
@@ -946,7 +795,7 @@ function DataGrid(props) {
|
|
|
946
795
|
className: "flex",
|
|
947
796
|
style: {
|
|
948
797
|
height: headerHeight,
|
|
949
|
-
minWidth:
|
|
798
|
+
minWidth: totalContentWidth
|
|
950
799
|
},
|
|
951
800
|
role: "row",
|
|
952
801
|
children: [selectionMode !== "none" && /* @__PURE__ */ jsx("div", {
|
|
@@ -956,30 +805,37 @@ function DataGrid(props) {
|
|
|
956
805
|
checked: allSelected,
|
|
957
806
|
indeterminate: someSelected,
|
|
958
807
|
onChange: handleSelectAll,
|
|
959
|
-
ariaLabel: "Select all rows"
|
|
808
|
+
ariaLabel: "Select all rows on this page",
|
|
809
|
+
title: "Select all rows on this page"
|
|
960
810
|
})
|
|
961
|
-
}), visibleColumns.map((col) =>
|
|
962
|
-
col
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
}
|
|
811
|
+
}), visibleColumns.map((col) => {
|
|
812
|
+
const header = headerByColId.get(col.id);
|
|
813
|
+
if (!header) return null;
|
|
814
|
+
return /* @__PURE__ */ jsx(HeaderCell, {
|
|
815
|
+
header,
|
|
816
|
+
col,
|
|
817
|
+
resizable
|
|
818
|
+
}, col.id);
|
|
819
|
+
})]
|
|
970
820
|
})
|
|
971
821
|
})]
|
|
972
822
|
})]
|
|
973
823
|
}),
|
|
974
824
|
/* @__PURE__ */ jsx("div", {
|
|
975
825
|
ref: scrollContainerRef,
|
|
976
|
-
className: cn("w-full min-w-0 overflow-auto bg-transparent",
|
|
826
|
+
className: cn("relative z-0 w-full min-w-0 overflow-auto bg-transparent", isBounded ? "min-h-0 flex-1" : "flex-none", "[&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar]:h-1.5", "[&::-webkit-scrollbar-track]:bg-transparent", "[&::-webkit-scrollbar-thumb]:bg-foreground/[0.08] [&::-webkit-scrollbar-thumb]:rounded-full", "[&::-webkit-scrollbar-thumb]:hover:bg-foreground/[0.15]"),
|
|
977
827
|
onScroll: handleBodyScroll,
|
|
978
828
|
children: /* @__PURE__ */ jsxs("div", {
|
|
979
829
|
ref: rowsClipRef,
|
|
830
|
+
"data-data-grid-rows-clip": "",
|
|
831
|
+
className: "relative z-0",
|
|
832
|
+
style: {
|
|
833
|
+
minWidth: totalContentWidth,
|
|
834
|
+
clipPath: "inset(var(--data-grid-sticky-overlap, 0px) 0 0 0)"
|
|
835
|
+
},
|
|
980
836
|
children: [
|
|
981
837
|
isLoading && /* @__PURE__ */ jsx("div", {
|
|
982
|
-
style: { minWidth:
|
|
838
|
+
style: { minWidth: totalContentWidth },
|
|
983
839
|
children: loadingState ?? Array.from({ length: 8 }).map((_, i) => /* @__PURE__ */ jsx(SkeletonRow, {
|
|
984
840
|
columns: visibleColumns,
|
|
985
841
|
height: estimatedRowHeight,
|
|
@@ -988,14 +844,14 @@ function DataGrid(props) {
|
|
|
988
844
|
}),
|
|
989
845
|
!isLoading && rows.length === 0 && /* @__PURE__ */ jsx("div", {
|
|
990
846
|
className: "flex items-center justify-center py-16 text-sm text-muted-foreground",
|
|
991
|
-
style: { minWidth:
|
|
847
|
+
style: { minWidth: totalContentWidth },
|
|
992
848
|
children: emptyState ?? strings.noData
|
|
993
849
|
}),
|
|
994
850
|
!isLoading && rows.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
995
851
|
style: {
|
|
996
852
|
height: rowVirtualizer.getTotalSize(),
|
|
997
853
|
width: "100%",
|
|
998
|
-
minWidth:
|
|
854
|
+
minWidth: totalContentWidth,
|
|
999
855
|
position: "relative"
|
|
1000
856
|
},
|
|
1001
857
|
children: rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
|
@@ -1012,12 +868,10 @@ function DataGrid(props) {
|
|
|
1012
868
|
transform: `translateY(${virtualRow.start}px)`
|
|
1013
869
|
},
|
|
1014
870
|
onClick: (e) => {
|
|
1015
|
-
if (shouldIgnoreRowClick(e))
|
|
1016
|
-
handleRowClick(row, rowId, e);
|
|
871
|
+
if (!shouldIgnoreRowClick(e)) handleRowClick(row, rowId, e);
|
|
1017
872
|
},
|
|
1018
873
|
onDoubleClick: (e) => {
|
|
1019
|
-
if (shouldIgnoreRowClick(e))
|
|
1020
|
-
onRowDoubleClick?.(row, rowId, e);
|
|
874
|
+
if (!shouldIgnoreRowClick(e)) onRowDoubleClick?.(row, rowId, e);
|
|
1021
875
|
},
|
|
1022
876
|
role: "row",
|
|
1023
877
|
"aria-rowindex": virtualRow.index + 2,
|
|
@@ -1029,7 +883,7 @@ function DataGrid(props) {
|
|
|
1029
883
|
style: { width: 44 },
|
|
1030
884
|
children: /* @__PURE__ */ jsx(SelectionCheckbox, {
|
|
1031
885
|
checked: isSelected,
|
|
1032
|
-
onChange: (event) =>
|
|
886
|
+
onChange: (event) => handleRowClick(row, rowId, event),
|
|
1033
887
|
ariaLabel: `Select row ${rowId}`
|
|
1034
888
|
})
|
|
1035
889
|
}), visibleColumns.map((col) => /* @__PURE__ */ jsx(DataCell, {
|
|
@@ -1053,7 +907,7 @@ function DataGrid(props) {
|
|
|
1053
907
|
})
|
|
1054
908
|
}),
|
|
1055
909
|
footer !== false && /* @__PURE__ */ jsxs("div", {
|
|
1056
|
-
className: "
|
|
910
|
+
className: "sticky bottom-0 z-30 shrink-0 overflow-hidden rounded-b-[calc(var(--radius)*2)] bg-white/90 dark:bg-background/60 backdrop-blur-xl",
|
|
1057
911
|
children: [footer ? footer(footerCtx) : /* @__PURE__ */ jsx(DefaultFooter, {
|
|
1058
912
|
ctx: footerCtx,
|
|
1059
913
|
pagination: paginationMode,
|