@dynostack/react-grid 0.1.0 → 0.1.3
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/README.md +138 -3
- package/dist/index.cjs +228 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -4
- package/dist/index.d.ts +41 -4
- package/dist/index.js +229 -68
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ A single `<DataTable />` component that gives you ag-grid–level functionality
|
|
|
40
40
|
- [Tailwind setup](#tailwind-setup)
|
|
41
41
|
- [Theme tokens](#theme-tokens)
|
|
42
42
|
- [Quick start](#quick-start)
|
|
43
|
+
- [Data fetching](#data-fetching)
|
|
43
44
|
- [Theming](#theming)
|
|
44
45
|
- [Density](#density)
|
|
45
46
|
- [Feature flags](#feature-flags)
|
|
@@ -167,6 +168,84 @@ That's it. You now have sort + filter + edit + add + delete + export + pin + res
|
|
|
167
168
|
|
|
168
169
|
---
|
|
169
170
|
|
|
171
|
+
## Data fetching
|
|
172
|
+
|
|
173
|
+
`DataTable` supports two data ownership models.
|
|
174
|
+
|
|
175
|
+
### 1. Controlled data from your page
|
|
176
|
+
|
|
177
|
+
Use this when your app already owns fetching with TanStack Query, SWR, Redux,
|
|
178
|
+
loader functions, or custom hooks. The table receives rows and loading flags as
|
|
179
|
+
props, and your app owns error/toast behavior.
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
const usersQuery = useQuery({
|
|
183
|
+
queryKey: ["users"],
|
|
184
|
+
queryFn: fetchUsers,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
<DataTable<User>
|
|
188
|
+
data={usersQuery.data ?? []}
|
|
189
|
+
columns={columns}
|
|
190
|
+
isLoading={usersQuery.isLoading}
|
|
191
|
+
isFetching={usersQuery.isFetching}
|
|
192
|
+
onRefresh={() => usersQuery.refetch()}
|
|
193
|
+
totalRecords={usersQuery.data?.length ?? 0}
|
|
194
|
+
/>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
For blocking load errors, render your own page-level error state or pass an empty
|
|
198
|
+
array. For background errors, show a toast from your query/mutation callbacks.
|
|
199
|
+
|
|
200
|
+
### 2. Internal fetching with `dataSource`
|
|
201
|
+
|
|
202
|
+
Use this when you want the table to own fetch/loading/error/refresh state.
|
|
203
|
+
`fetchRows` receives the current table state and can return either an array or
|
|
204
|
+
`{ rows, totalRecords }`.
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
<DataTable<User>
|
|
208
|
+
columns={columns}
|
|
209
|
+
dataSource={{
|
|
210
|
+
fetchRows: async ({ pageIndex, pageSize, sorting, columnFilters, globalFilter }) => {
|
|
211
|
+
const res = await fetch("/api/users", {
|
|
212
|
+
method: "POST",
|
|
213
|
+
headers: { "content-type": "application/json" },
|
|
214
|
+
body: JSON.stringify({
|
|
215
|
+
pageIndex,
|
|
216
|
+
pageSize,
|
|
217
|
+
sorting,
|
|
218
|
+
columnFilters,
|
|
219
|
+
q: globalFilter,
|
|
220
|
+
}),
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
if (!res.ok) throw new Error("Failed to load users")
|
|
224
|
+
return res.json() as Promise<{ rows: User[]; totalRecords: number }>
|
|
225
|
+
},
|
|
226
|
+
mode: "server",
|
|
227
|
+
onError: (error, context) => {
|
|
228
|
+
toast.error(context.message)
|
|
229
|
+
console.error(error)
|
|
230
|
+
},
|
|
231
|
+
}}
|
|
232
|
+
/>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Internal mode behavior:
|
|
236
|
+
|
|
237
|
+
- Initial load shows the table skeleton.
|
|
238
|
+
- Initial load failure shows an inline `Could not load rows` state with `Retry`.
|
|
239
|
+
- Refresh failure keeps the last successful rows visible and calls `onError`.
|
|
240
|
+
- `mode: "client"` expects the full row array and lets the table sort/filter/page in memory.
|
|
241
|
+
- `mode: "server"` expects the current page and uses `totalRecords` for pagination.
|
|
242
|
+
|
|
243
|
+
Keep using `onCellEdit`, `onRowSave`, `onAddRow`, and `onBulkDelete` for mutations.
|
|
244
|
+
The table does not assume your write API; this lets you choose optimistic updates,
|
|
245
|
+
rollback, toast notifications, and validation.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
170
249
|
## Theming
|
|
171
250
|
|
|
172
251
|
All shadcn tokens are supported plus `radius` and `fontFamily`. Anything you omit falls through to the consumer's `:root`.
|
|
@@ -454,7 +533,11 @@ Mark a column non-exportable via `meta.exportable: false`.
|
|
|
454
533
|
|
|
455
534
|
## Server-side data
|
|
456
535
|
|
|
457
|
-
|
|
536
|
+
You can do server-side data in either mode.
|
|
537
|
+
|
|
538
|
+
### Controlled server-side data
|
|
539
|
+
|
|
540
|
+
Own the API call in your page and pass the result into the table:
|
|
458
541
|
|
|
459
542
|
```tsx
|
|
460
543
|
const [q, setQ] = useState("")
|
|
@@ -474,7 +557,38 @@ const usersQuery = useQuery({
|
|
|
474
557
|
/>
|
|
475
558
|
```
|
|
476
559
|
|
|
477
|
-
Pair with TanStack Query's pagination/cursor utilities
|
|
560
|
+
Pair this with TanStack Query's pagination/cursor utilities when you want query
|
|
561
|
+
caching and mutation orchestration outside the grid.
|
|
562
|
+
|
|
563
|
+
### Built-in server-side data
|
|
564
|
+
|
|
565
|
+
Let the grid call your API by setting `dataSource.mode` to `"server"`:
|
|
566
|
+
|
|
567
|
+
```tsx
|
|
568
|
+
<DataTable<User>
|
|
569
|
+
columns={columns}
|
|
570
|
+
dataSource={{
|
|
571
|
+
mode: "server",
|
|
572
|
+
fetchRows: async (state) => {
|
|
573
|
+
const res = await fetch("/api/users/grid", {
|
|
574
|
+
method: "POST",
|
|
575
|
+
headers: { "content-type": "application/json" },
|
|
576
|
+
body: JSON.stringify(state),
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
if (!res.ok) throw new Error("Users request failed")
|
|
580
|
+
return res.json() as Promise<{ rows: User[]; totalRecords: number }>
|
|
581
|
+
},
|
|
582
|
+
onError: (_error, context) => {
|
|
583
|
+
toast.error(context.message)
|
|
584
|
+
},
|
|
585
|
+
}}
|
|
586
|
+
/>
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
`state` contains `pageIndex`, `pageSize`, `sorting`, `columnFilters`, and
|
|
590
|
+
`globalFilter`. In server mode the table assumes the API already applied those
|
|
591
|
+
operations and only renders the returned page.
|
|
478
592
|
|
|
479
593
|
---
|
|
480
594
|
|
|
@@ -482,8 +596,9 @@ Pair with TanStack Query's pagination/cursor utilities for cursor-based grids.
|
|
|
482
596
|
|
|
483
597
|
| Prop | Type | Default | Description |
|
|
484
598
|
| ------------------------- | ---------------------------------------------------------- | ---------------------- | ------------------------------------------------------ |
|
|
485
|
-
| `data` | `TData[]` |
|
|
599
|
+
| `data` | `TData[]` | `[]` | Controlled row data. Use this when fetching outside the table. |
|
|
486
600
|
| `columns` | `ColumnDef<TData>[]` | — | TanStack column definitions. |
|
|
601
|
+
| `dataSource` | `DataTableDataSource<TData>` | — | Optional internal fetcher for client/server data loading. |
|
|
487
602
|
| `isLoading` | `boolean` | `false` | Initial skeleton state. |
|
|
488
603
|
| `isFetching` | `boolean` | `false` | Background-refresh indicator. |
|
|
489
604
|
| `onRefresh` | `() => void` | — | Refresh button handler. |
|
|
@@ -514,6 +629,26 @@ Pair with TanStack Query's pagination/cursor utilities for cursor-based grids.
|
|
|
514
629
|
|
|
515
630
|
`TData` must extend `{ id: string \| number }`.
|
|
516
631
|
|
|
632
|
+
```ts
|
|
633
|
+
type DataTableDataSource<TData> = {
|
|
634
|
+
fetchRows: (params: {
|
|
635
|
+
pageIndex: number
|
|
636
|
+
pageSize: number
|
|
637
|
+
sorting: SortingState
|
|
638
|
+
columnFilters: ColumnFiltersState
|
|
639
|
+
globalFilter: string
|
|
640
|
+
}) => Promise<TData[] | { rows: TData[]; totalRecords?: number }>
|
|
641
|
+
mode?: "client" | "server"
|
|
642
|
+
enabled?: boolean
|
|
643
|
+
initialData?: TData[]
|
|
644
|
+
deps?: readonly unknown[]
|
|
645
|
+
onError?: (
|
|
646
|
+
error: unknown,
|
|
647
|
+
context: { type: "load" | "refresh"; message: string }
|
|
648
|
+
) => void
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
517
652
|
---
|
|
518
653
|
|
|
519
654
|
## Compatibility
|
package/dist/index.cjs
CHANGED
|
@@ -8,6 +8,7 @@ var tailwindMerge = require('tailwind-merge');
|
|
|
8
8
|
var radixUi = require('radix-ui');
|
|
9
9
|
var jsxRuntime = require('react/jsx-runtime');
|
|
10
10
|
var classVarianceAuthority = require('class-variance-authority');
|
|
11
|
+
var reactDom = require('react-dom');
|
|
11
12
|
|
|
12
13
|
function cn(...inputs) {
|
|
13
14
|
return tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
@@ -1717,14 +1718,34 @@ function DataTableRowActions({
|
|
|
1717
1718
|
onCancelEdit
|
|
1718
1719
|
}) {
|
|
1719
1720
|
const [open, setOpen] = react.useState(false);
|
|
1721
|
+
const [menuPosition, setMenuPosition] = react.useState({ top: 0, left: 0 });
|
|
1722
|
+
const triggerRef = react.useRef(null);
|
|
1720
1723
|
const menuRef = react.useRef(null);
|
|
1724
|
+
react.useLayoutEffect(() => {
|
|
1725
|
+
if (!open || !triggerRef.current) return;
|
|
1726
|
+
const rect = triggerRef.current.getBoundingClientRect();
|
|
1727
|
+
setMenuPosition({
|
|
1728
|
+
top: rect.bottom + 4,
|
|
1729
|
+
left: Math.max(8, rect.right - 160)
|
|
1730
|
+
});
|
|
1731
|
+
}, [open]);
|
|
1721
1732
|
react.useEffect(() => {
|
|
1722
1733
|
if (!open) return;
|
|
1723
1734
|
const onPointerDown = (event) => {
|
|
1724
|
-
|
|
1735
|
+
const target = event.target;
|
|
1736
|
+
if (!menuRef.current?.contains(target) && !triggerRef.current?.contains(target)) {
|
|
1737
|
+
setOpen(false);
|
|
1738
|
+
}
|
|
1725
1739
|
};
|
|
1740
|
+
const onDismiss = () => setOpen(false);
|
|
1726
1741
|
document.addEventListener("pointerdown", onPointerDown);
|
|
1727
|
-
|
|
1742
|
+
window.addEventListener("scroll", onDismiss, true);
|
|
1743
|
+
window.addEventListener("resize", onDismiss);
|
|
1744
|
+
return () => {
|
|
1745
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
1746
|
+
window.removeEventListener("scroll", onDismiss, true);
|
|
1747
|
+
window.removeEventListener("resize", onDismiss);
|
|
1748
|
+
};
|
|
1728
1749
|
}, [open]);
|
|
1729
1750
|
if (isEditing) {
|
|
1730
1751
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-1", children: [
|
|
@@ -1755,6 +1776,7 @@ function DataTableRowActions({
|
|
|
1755
1776
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1756
1777
|
Button,
|
|
1757
1778
|
{
|
|
1779
|
+
ref: triggerRef,
|
|
1758
1780
|
variant: "ghost",
|
|
1759
1781
|
size: "icon-xs",
|
|
1760
1782
|
"aria-label": "Open row actions",
|
|
@@ -1763,41 +1785,52 @@ function DataTableRowActions({
|
|
|
1763
1785
|
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MoreHorizontalIcon, {})
|
|
1764
1786
|
}
|
|
1765
1787
|
),
|
|
1766
|
-
open &&
|
|
1767
|
-
|
|
1768
|
-
|
|
1788
|
+
open && reactDom.createPortal(
|
|
1789
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1790
|
+
"div",
|
|
1769
1791
|
{
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
onAction?.(a, row);
|
|
1774
|
-
},
|
|
1792
|
+
ref: menuRef,
|
|
1793
|
+
className: "fixed z-[1000] w-40 rounded-md border bg-popover p-1 text-popover-foreground shadow-lg",
|
|
1794
|
+
style: { top: menuPosition.top, left: menuPosition.left },
|
|
1775
1795
|
children: [
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
c.
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
+
actions.map((a) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1797
|
+
RowActionButton,
|
|
1798
|
+
{
|
|
1799
|
+
danger: a === "delete",
|
|
1800
|
+
onClick: () => {
|
|
1801
|
+
setOpen(false);
|
|
1802
|
+
onAction?.(a, row);
|
|
1803
|
+
},
|
|
1804
|
+
children: [
|
|
1805
|
+
builtinIcons[a],
|
|
1806
|
+
" ",
|
|
1807
|
+
builtinLabels[a]
|
|
1808
|
+
]
|
|
1809
|
+
},
|
|
1810
|
+
a
|
|
1811
|
+
)),
|
|
1812
|
+
customActions.length > 0 && actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "-mx-1 my-1 h-px bg-border" }),
|
|
1813
|
+
customActions.filter((c) => c.show ? c.show(row) : true).map((c) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1814
|
+
RowActionButton,
|
|
1815
|
+
{
|
|
1816
|
+
danger: c.danger,
|
|
1817
|
+
onClick: () => {
|
|
1818
|
+
setOpen(false);
|
|
1819
|
+
onAction?.(c.id, row);
|
|
1820
|
+
},
|
|
1821
|
+
children: [
|
|
1822
|
+
c.icon,
|
|
1823
|
+
" ",
|
|
1824
|
+
c.label
|
|
1825
|
+
]
|
|
1826
|
+
},
|
|
1827
|
+
c.id
|
|
1828
|
+
))
|
|
1796
1829
|
]
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1830
|
+
}
|
|
1831
|
+
),
|
|
1832
|
+
document.body
|
|
1833
|
+
)
|
|
1801
1834
|
] });
|
|
1802
1835
|
}
|
|
1803
1836
|
function RowActionButton({
|
|
@@ -2166,6 +2199,8 @@ var DEFAULT_LABELS = {
|
|
|
2166
2199
|
total: "Total",
|
|
2167
2200
|
noData: "No data.",
|
|
2168
2201
|
noResults: "No rows match the current filters.",
|
|
2202
|
+
loadError: "Could not load rows.",
|
|
2203
|
+
retry: "Retry",
|
|
2169
2204
|
refreshing: "Refreshing",
|
|
2170
2205
|
rowsPerPage: "Rows per page",
|
|
2171
2206
|
page: "Page",
|
|
@@ -2179,6 +2214,7 @@ var DENSITY_CLASS = {
|
|
|
2179
2214
|
function DataTable({
|
|
2180
2215
|
data,
|
|
2181
2216
|
columns,
|
|
2217
|
+
dataSource,
|
|
2182
2218
|
isLoading,
|
|
2183
2219
|
isFetching,
|
|
2184
2220
|
onRefresh,
|
|
@@ -2210,7 +2246,7 @@ function DataTable({
|
|
|
2210
2246
|
const labels = { ...DEFAULT_LABELS, ...labelsProp };
|
|
2211
2247
|
const f = {
|
|
2212
2248
|
search: features?.search ?? true,
|
|
2213
|
-
refresh: features?.refresh ?? !!onRefresh,
|
|
2249
|
+
refresh: features?.refresh ?? !!(onRefresh || dataSource),
|
|
2214
2250
|
columnVisibility: features?.columnVisibility ?? true,
|
|
2215
2251
|
export: features?.export ?? true,
|
|
2216
2252
|
addRow: features?.addRow ?? !!onAddRow,
|
|
@@ -2254,14 +2290,80 @@ function DataTable({
|
|
|
2254
2290
|
pageIndex: 0,
|
|
2255
2291
|
pageSize: initialPageSize
|
|
2256
2292
|
});
|
|
2293
|
+
const [sourceRows, setSourceRows] = react.useState(
|
|
2294
|
+
() => dataSource?.initialData ?? []
|
|
2295
|
+
);
|
|
2296
|
+
const [sourceTotalRecords, setSourceTotalRecords] = react.useState(void 0);
|
|
2297
|
+
const [sourceError, setSourceError] = react.useState(null);
|
|
2298
|
+
const [sourceLoading, setSourceLoading] = react.useState(false);
|
|
2299
|
+
const [sourceFetching, setSourceFetching] = react.useState(false);
|
|
2300
|
+
const [sourceRefreshKey, setSourceRefreshKey] = react.useState(0);
|
|
2257
2301
|
const [editingCell, setEditingCell] = react.useState(null);
|
|
2258
2302
|
const [editingRowId, setEditingRowId] = react.useState(null);
|
|
2259
2303
|
const [rowDraft, setRowDraft] = react.useState({});
|
|
2304
|
+
const dataSourceEnabled = !!dataSource && dataSource.enabled !== false;
|
|
2305
|
+
const dataSourceMode = dataSource?.mode ?? "client";
|
|
2306
|
+
const isServerSideDataSource = dataSourceEnabled && dataSourceMode === "server";
|
|
2307
|
+
const dataSourceDeps = dataSource?.deps ?? [];
|
|
2308
|
+
react.useEffect(() => {
|
|
2309
|
+
if (!dataSourceEnabled || !dataSource) return;
|
|
2310
|
+
let cancelled = false;
|
|
2311
|
+
const isInitialLoad = sourceRows.length === 0;
|
|
2312
|
+
setSourceError(null);
|
|
2313
|
+
setSourceLoading(isInitialLoad);
|
|
2314
|
+
setSourceFetching(!isInitialLoad);
|
|
2315
|
+
dataSource.fetchRows({
|
|
2316
|
+
pageIndex: pagination.pageIndex,
|
|
2317
|
+
pageSize: pagination.pageSize,
|
|
2318
|
+
sorting,
|
|
2319
|
+
columnFilters,
|
|
2320
|
+
globalFilter
|
|
2321
|
+
}).then((result) => {
|
|
2322
|
+
if (cancelled) return;
|
|
2323
|
+
const rows2 = Array.isArray(result) ? result : result.rows;
|
|
2324
|
+
setSourceRows(rows2);
|
|
2325
|
+
setSourceTotalRecords(
|
|
2326
|
+
Array.isArray(result) ? rows2.length : result.totalRecords ?? rows2.length
|
|
2327
|
+
);
|
|
2328
|
+
}).catch((error) => {
|
|
2329
|
+
if (cancelled) return;
|
|
2330
|
+
setSourceError(error);
|
|
2331
|
+
dataSource.onError?.(error, {
|
|
2332
|
+
type: isInitialLoad ? "load" : "refresh",
|
|
2333
|
+
message: isInitialLoad ? labels.loadError : labels.refreshing
|
|
2334
|
+
});
|
|
2335
|
+
}).finally(() => {
|
|
2336
|
+
if (cancelled) return;
|
|
2337
|
+
setSourceLoading(false);
|
|
2338
|
+
setSourceFetching(false);
|
|
2339
|
+
});
|
|
2340
|
+
return () => {
|
|
2341
|
+
cancelled = true;
|
|
2342
|
+
};
|
|
2343
|
+
}, [
|
|
2344
|
+
dataSourceEnabled,
|
|
2345
|
+
dataSource,
|
|
2346
|
+
sourceRefreshKey,
|
|
2347
|
+
pagination.pageIndex,
|
|
2348
|
+
pagination.pageSize,
|
|
2349
|
+
sorting,
|
|
2350
|
+
columnFilters,
|
|
2351
|
+
globalFilter,
|
|
2352
|
+
labels.loadError,
|
|
2353
|
+
labels.refreshing,
|
|
2354
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2355
|
+
...dataSourceDeps
|
|
2356
|
+
]);
|
|
2357
|
+
const baseData = dataSourceEnabled ? sourceRows : data ?? [];
|
|
2358
|
+
const effectiveIsLoading = dataSourceEnabled ? sourceLoading : isLoading;
|
|
2359
|
+
const effectiveIsFetching = dataSourceEnabled ? sourceFetching : isFetching;
|
|
2360
|
+
const effectiveOnRefresh = dataSourceEnabled ? () => setSourceRefreshKey((key) => key + 1) : onRefresh;
|
|
2260
2361
|
const [extraRows, setExtraRows] = react.useState([]);
|
|
2261
2362
|
const mergedData = react.useMemo(
|
|
2262
|
-
() => extraRows.length ? [...extraRows, ...
|
|
2263
|
-
[extraRows,
|
|
2363
|
+
() => extraRows.length ? [...extraRows, ...baseData] : baseData,
|
|
2364
|
+
[extraRows, baseData]
|
|
2264
2365
|
);
|
|
2366
|
+
const effectiveTotalRecords = totalRecords ?? (dataSourceEnabled ? sourceTotalRecords ?? mergedData.length : mergedData.length);
|
|
2265
2367
|
const augmentedColumns = react.useMemo(() => {
|
|
2266
2368
|
const wrap = (col) => {
|
|
2267
2369
|
const meta = col.meta;
|
|
@@ -2411,6 +2513,10 @@ function DataTable({
|
|
|
2411
2513
|
enableRowSelection: true,
|
|
2412
2514
|
enableExpanding: !!(renderSubRow || getSubRows),
|
|
2413
2515
|
enablePinning: f.pinning,
|
|
2516
|
+
manualPagination: isServerSideDataSource,
|
|
2517
|
+
manualSorting: isServerSideDataSource,
|
|
2518
|
+
manualFiltering: isServerSideDataSource,
|
|
2519
|
+
pageCount: isServerSideDataSource ? Math.max(1, Math.ceil(effectiveTotalRecords / pagination.pageSize)) : void 0,
|
|
2414
2520
|
getRowId: (row) => String(row.id),
|
|
2415
2521
|
getSubRows,
|
|
2416
2522
|
getRowCanExpand: (row) => {
|
|
@@ -2435,27 +2541,56 @@ function DataTable({
|
|
|
2435
2541
|
getExpandedRowModel: reactTable.getExpandedRowModel(),
|
|
2436
2542
|
getPaginationRowModel: reactTable.getPaginationRowModel()
|
|
2437
2543
|
});
|
|
2438
|
-
const
|
|
2439
|
-
const
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2544
|
+
const reorderState = react.useRef(null);
|
|
2545
|
+
const [activeReorderId, setActiveReorderId] = react.useState(null);
|
|
2546
|
+
const [dragOverColId, setDragOverColId] = react.useState(null);
|
|
2547
|
+
const isInteractiveTarget = (target) => {
|
|
2548
|
+
const el = target;
|
|
2549
|
+
if (!el || !el.closest) return false;
|
|
2550
|
+
return !!el.closest(
|
|
2551
|
+
'button, a, input, select, textarea, [role="menuitem"], [data-no-drag]'
|
|
2552
|
+
);
|
|
2443
2553
|
};
|
|
2444
|
-
const
|
|
2445
|
-
e.
|
|
2446
|
-
e.
|
|
2554
|
+
const onHeaderPointerDown = (id) => (e) => {
|
|
2555
|
+
if (e.button !== 0) return;
|
|
2556
|
+
if (isInteractiveTarget(e.target)) return;
|
|
2557
|
+
reorderState.current = { x: e.clientX, y: e.clientY, id };
|
|
2447
2558
|
};
|
|
2448
|
-
const
|
|
2449
|
-
const
|
|
2450
|
-
|
|
2451
|
-
if (!
|
|
2559
|
+
const onHeaderPointerMove = (e) => {
|
|
2560
|
+
const start = reorderState.current;
|
|
2561
|
+
if (!start) return;
|
|
2562
|
+
if (!activeReorderId) {
|
|
2563
|
+
const dx = e.clientX - start.x;
|
|
2564
|
+
const dy = e.clientY - start.y;
|
|
2565
|
+
if (Math.hypot(dx, dy) < 6) return;
|
|
2566
|
+
setActiveReorderId(start.id);
|
|
2567
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
2568
|
+
}
|
|
2569
|
+
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
2570
|
+
const th = el ? el.closest("th[data-col-id]") : null;
|
|
2571
|
+
const overId = th?.dataset.colId ?? null;
|
|
2572
|
+
const next = overId && overId !== start.id ? overId : null;
|
|
2573
|
+
if (next !== dragOverColId) setDragOverColId(next);
|
|
2574
|
+
};
|
|
2575
|
+
const onHeaderPointerUp = (e) => {
|
|
2576
|
+
const start = reorderState.current;
|
|
2577
|
+
reorderState.current = null;
|
|
2578
|
+
if (!activeReorderId) return;
|
|
2579
|
+
try {
|
|
2580
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
2581
|
+
} catch {
|
|
2582
|
+
}
|
|
2583
|
+
const target = dragOverColId;
|
|
2584
|
+
setActiveReorderId(null);
|
|
2585
|
+
setDragOverColId(null);
|
|
2586
|
+
if (!start || !target || target === start.id) return;
|
|
2452
2587
|
setColumnOrder((order) => {
|
|
2453
2588
|
const next = [...order];
|
|
2454
|
-
const from = next.indexOf(
|
|
2455
|
-
const to = next.indexOf(
|
|
2589
|
+
const from = next.indexOf(start.id);
|
|
2590
|
+
const to = next.indexOf(target);
|
|
2456
2591
|
if (from === -1 || to === -1) return order;
|
|
2457
2592
|
next.splice(from, 1);
|
|
2458
|
-
next.splice(to, 0,
|
|
2593
|
+
next.splice(to, 0, start.id);
|
|
2459
2594
|
return next;
|
|
2460
2595
|
});
|
|
2461
2596
|
};
|
|
@@ -2512,9 +2647,9 @@ function DataTable({
|
|
|
2512
2647
|
DataTableToolbar,
|
|
2513
2648
|
{
|
|
2514
2649
|
table,
|
|
2515
|
-
totalRecords:
|
|
2516
|
-
isFetching,
|
|
2517
|
-
onRefresh: f.refresh ?
|
|
2650
|
+
totalRecords: effectiveTotalRecords,
|
|
2651
|
+
isFetching: effectiveIsFetching,
|
|
2652
|
+
onRefresh: f.refresh ? effectiveOnRefresh : void 0,
|
|
2518
2653
|
onAddRow: f.addRow ? handleAddRow : void 0,
|
|
2519
2654
|
onBulkDelete,
|
|
2520
2655
|
exportFileName,
|
|
@@ -2542,32 +2677,38 @@ function DataTable({
|
|
|
2542
2677
|
const meta = header.column.columnDef.meta;
|
|
2543
2678
|
const isUtility = header.column.id === "__select" || header.column.id === "__expand" || header.column.id === "__actions";
|
|
2544
2679
|
const isPinned = !!header.column.getIsPinned();
|
|
2680
|
+
const canReorder = !isUtility && f.reordering;
|
|
2681
|
+
const isDropTarget = canReorder && dragOverColId === header.column.id;
|
|
2682
|
+
const isBeingDragged = canReorder && activeReorderId === header.column.id;
|
|
2545
2683
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2546
2684
|
"th",
|
|
2547
2685
|
{
|
|
2548
2686
|
colSpan: header.colSpan,
|
|
2687
|
+
"data-col-id": header.column.id,
|
|
2688
|
+
onPointerDown: canReorder ? onHeaderPointerDown(header.column.id) : void 0,
|
|
2689
|
+
onPointerMove: canReorder ? onHeaderPointerMove : void 0,
|
|
2690
|
+
onPointerUp: canReorder ? onHeaderPointerUp : void 0,
|
|
2691
|
+
onPointerCancel: canReorder ? onHeaderPointerUp : void 0,
|
|
2549
2692
|
style: {
|
|
2550
2693
|
width: header.getSize(),
|
|
2551
2694
|
...getPinningStyles(header.column),
|
|
2552
2695
|
...isPinned ? { zIndex: 2 } : {}
|
|
2553
2696
|
},
|
|
2554
2697
|
className: cn(
|
|
2555
|
-
"group/th relative h-9 px-2 text-left align-middle text-xs font-medium text-muted-foreground",
|
|
2698
|
+
"group/th relative h-9 px-2 text-left align-middle text-xs font-medium text-muted-foreground select-none",
|
|
2699
|
+
canReorder && "cursor-grab active:cursor-grabbing",
|
|
2556
2700
|
isPinned && "bg-card",
|
|
2701
|
+
isBeingDragged && "opacity-50",
|
|
2702
|
+
isDropTarget && "bg-primary/10 ring-2 ring-inset ring-primary/40",
|
|
2557
2703
|
meta?.headerClassName
|
|
2558
2704
|
),
|
|
2559
2705
|
children: [
|
|
2560
2706
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
2561
|
-
|
|
2562
|
-
|
|
2707
|
+
canReorder && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2708
|
+
lucideReact.GripVerticalIcon,
|
|
2563
2709
|
{
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
onDragOver: onHeaderDragOver,
|
|
2567
|
-
onDrop: () => onHeaderDrop(header.column.id),
|
|
2568
|
-
className: "-ml-1 cursor-grab rounded p-0.5 text-muted-foreground/40 opacity-0 transition group-hover/th:opacity-100 hover:bg-muted hover:text-foreground active:cursor-grabbing",
|
|
2569
|
-
title: "Drag to reorder",
|
|
2570
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.GripVerticalIcon, { className: "size-3" })
|
|
2710
|
+
className: "-ml-1 size-3 shrink-0 text-muted-foreground/40 opacity-0 transition group-hover/th:opacity-100",
|
|
2711
|
+
"aria-hidden": "true"
|
|
2571
2712
|
}
|
|
2572
2713
|
),
|
|
2573
2714
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0 flex-1", children: header.isPlaceholder ? null : reactTable.flexRender(header.column.columnDef.header, header.getContext()) })
|
|
@@ -2575,6 +2716,8 @@ function DataTable({
|
|
|
2575
2716
|
header.column.getCanResize() && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2576
2717
|
"span",
|
|
2577
2718
|
{
|
|
2719
|
+
"data-no-drag": true,
|
|
2720
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
2578
2721
|
onMouseDown: header.getResizeHandler(),
|
|
2579
2722
|
onTouchStart: header.getResizeHandler(),
|
|
2580
2723
|
onDoubleClick: () => header.column.resetSize(),
|
|
@@ -2589,7 +2732,25 @@ function DataTable({
|
|
|
2589
2732
|
header.id
|
|
2590
2733
|
);
|
|
2591
2734
|
}) }, hg.id)) }),
|
|
2592
|
-
/* @__PURE__ */ jsxRuntime.jsx("tbody", { children:
|
|
2735
|
+
/* @__PURE__ */ jsxRuntime.jsx("tbody", { children: effectiveIsLoading ? renderSkeleton(visibleLeafCount, pagination.pageSize) : sourceError && rows.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2736
|
+
"td",
|
|
2737
|
+
{
|
|
2738
|
+
colSpan: visibleLeafCount,
|
|
2739
|
+
className: "h-32 text-center text-sm text-muted-foreground",
|
|
2740
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-2", children: [
|
|
2741
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: labels.loadError }),
|
|
2742
|
+
effectiveOnRefresh && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2743
|
+
"button",
|
|
2744
|
+
{
|
|
2745
|
+
type: "button",
|
|
2746
|
+
onClick: effectiveOnRefresh,
|
|
2747
|
+
className: "rounded-md border px-2 py-1 text-xs text-foreground hover:bg-muted",
|
|
2748
|
+
children: labels.retry
|
|
2749
|
+
}
|
|
2750
|
+
)
|
|
2751
|
+
] })
|
|
2752
|
+
}
|
|
2753
|
+
) }) : rows.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2593
2754
|
"td",
|
|
2594
2755
|
{
|
|
2595
2756
|
colSpan: visibleLeafCount,
|
|
@@ -2608,7 +2769,7 @@ function DataTable({
|
|
|
2608
2769
|
]
|
|
2609
2770
|
}
|
|
2610
2771
|
),
|
|
2611
|
-
|
|
2772
|
+
effectiveIsFetching && !effectiveIsLoading && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pointer-events-none absolute top-1.5 right-2 flex items-center gap-1.5 rounded-md bg-background/80 px-2 py-1 text-xs text-muted-foreground shadow-sm backdrop-blur", children: [
|
|
2612
2773
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2Icon, { className: "size-3 animate-spin" }),
|
|
2613
2774
|
" ",
|
|
2614
2775
|
labels.refreshing
|