@datum-cloud/datum-ui 1.0.0 → 1.1.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/dist/components/features/data-table/core/filter-engine.d.ts +5 -2
- package/dist/components/features/data-table/core/filter-engine.d.ts.map +1 -1
- package/dist/components/features/data-table/index.d.ts +4 -0
- package/dist/components/features/data-table/index.d.ts.map +1 -1
- package/dist/components/features/grouped-table/components/grouped-skeleton.d.ts +7 -0
- package/dist/components/features/grouped-table/components/grouped-skeleton.d.ts.map +1 -0
- package/dist/components/features/grouped-table/components/grouped-toolbar.d.ts +9 -0
- package/dist/components/features/grouped-table/components/grouped-toolbar.d.ts.map +1 -0
- package/dist/components/features/grouped-table/grouped-table.d.ts +3 -0
- package/dist/components/features/grouped-table/grouped-table.d.ts.map +1 -0
- package/dist/components/features/grouped-table/index.d.ts +3 -0
- package/dist/components/features/grouped-table/index.d.ts.map +1 -0
- package/dist/components/features/grouped-table/lib/bucket-rows.d.ts +14 -0
- package/dist/components/features/grouped-table/lib/bucket-rows.d.ts.map +1 -0
- package/dist/components/features/grouped-table/lib/compose-columns.d.ts +11 -0
- package/dist/components/features/grouped-table/lib/compose-columns.d.ts.map +1 -0
- package/dist/components/features/grouped-table/lib/sort-rows.d.ts +7 -0
- package/dist/components/features/grouped-table/lib/sort-rows.d.ts.map +1 -0
- package/dist/components/features/grouped-table/lib/use-controllable-state.d.ts +8 -0
- package/dist/components/features/grouped-table/lib/use-controllable-state.d.ts.map +1 -0
- package/dist/components/features/grouped-table/types.d.ts +57 -0
- package/dist/components/features/grouped-table/types.d.ts.map +1 -0
- package/dist/components/features/grouped-table/use-grouped-expansion.d.ts +10 -0
- package/dist/components/features/grouped-table/use-grouped-expansion.d.ts.map +1 -0
- package/dist/data-table/index.mjs +2 -1588
- package/dist/data-table-BTIxzB7O.mjs +1588 -0
- package/dist/grouped-table/index.mjs +339 -0
- package/package.json +8 -3
|
@@ -0,0 +1,1588 @@
|
|
|
1
|
+
import { t as cn } from "./cn-dlASUkDY.mjs";
|
|
2
|
+
import { t as Badge } from "./badge-DO2_XsXD.mjs";
|
|
3
|
+
import { t as Button } from "./button-B_N7A8gv.mjs";
|
|
4
|
+
import { t as Checkbox } from "./checkbox-BZr9bkke.mjs";
|
|
5
|
+
import { t as Input } from "./input-DBzgl-pN.mjs";
|
|
6
|
+
import { t as ResponsiveDropdown } from "./responsive-dropdown-BhvBzT_1.mjs";
|
|
7
|
+
import { i as SelectItem, l as SelectTrigger, n as SelectContent, t as Select, u as SelectValue } from "./select-DNnPDWSW.mjs";
|
|
8
|
+
import { t as Skeleton } from "./skeleton-CGU89HPB.mjs";
|
|
9
|
+
import { c as TableRow, i as TableCell, n as TableBody, o as TableHead, s as TableHeader, t as Table } from "./table-BR3mwU8X.mjs";
|
|
10
|
+
import { t as Autocomplete } from "./autocomplete-Bg187x6x.mjs";
|
|
11
|
+
import { t as CalendarDatePicker } from "./calendar-date-picker-DAVXW7Jg.mjs";
|
|
12
|
+
import { t as ActionRow } from "./action-row-BhMyMSep.mjs";
|
|
13
|
+
import { t as MultiSelect } from "./multi-select-C9ocz07C.mjs";
|
|
14
|
+
import { ArrowDown, ArrowUp, ArrowUpDown, ChevronLeft, ChevronRight, MoreHorizontal, X } from "lucide-react";
|
|
15
|
+
import { createContext, memo, use, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
16
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
17
|
+
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
|
18
|
+
import { flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
|
|
19
|
+
//#region src/components/features/data-table/constants.ts
|
|
20
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
21
|
+
const DEFAULT_PAGE_SIZES = [
|
|
22
|
+
10,
|
|
23
|
+
20,
|
|
24
|
+
30,
|
|
25
|
+
50
|
|
26
|
+
];
|
|
27
|
+
const DEFAULT_DEBOUNCE_MS = 300;
|
|
28
|
+
const DEFAULT_LOADING_ROWS = 5;
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/components/features/data-table/adapters/nuqs-adapter.ts
|
|
31
|
+
/**
|
|
32
|
+
* Serialize SortingState to URL-friendly string.
|
|
33
|
+
* Format: "name" (asc), "-name" (desc), comma-separated for multi-sort.
|
|
34
|
+
* Example: "-department,name" → [{id:"department",desc:true},{id:"name",desc:false}]
|
|
35
|
+
*/
|
|
36
|
+
function serializeSorting(sorting) {
|
|
37
|
+
if (sorting.length === 0) return "";
|
|
38
|
+
return sorting.map((s) => s.desc ? `-${s.id}` : s.id).join(",");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse URL sort string back to SortingState.
|
|
42
|
+
*/
|
|
43
|
+
function parseSorting(value) {
|
|
44
|
+
if (!value) return [];
|
|
45
|
+
return value.split(",").filter(Boolean).map((part) => {
|
|
46
|
+
if (part.startsWith("-")) return {
|
|
47
|
+
id: part.slice(1),
|
|
48
|
+
desc: true
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
id: part,
|
|
52
|
+
desc: false
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const coreSearchParams = {
|
|
57
|
+
sort: parseAsString.withDefault(""),
|
|
58
|
+
q: parseAsString.withDefault(""),
|
|
59
|
+
page: parseAsInteger.withDefault(0),
|
|
60
|
+
size: parseAsInteger.withDefault(20)
|
|
61
|
+
};
|
|
62
|
+
const EMPTY_FILTER_PARSERS_PLACEHOLDER = { _dt: parseAsString.withDefault("") };
|
|
63
|
+
/**
|
|
64
|
+
* Hook that creates a StateAdapter backed by nuqs URL query state.
|
|
65
|
+
*
|
|
66
|
+
* URL format:
|
|
67
|
+
* - `?sort=name` (asc) or `?sort=-name` (desc), comma-separated for multi-sort
|
|
68
|
+
* - `?q=search` for search text
|
|
69
|
+
* - `?page=0&size=20` for pagination
|
|
70
|
+
* - Custom filter keys as declared in options
|
|
71
|
+
*
|
|
72
|
+
* Requires `nuqs` to be installed in the consumer app.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```tsx
|
|
76
|
+
* const stateAdapter = useNuqsAdapter({
|
|
77
|
+
* filters: {
|
|
78
|
+
* status: parseAsString.withDefault(''),
|
|
79
|
+
* department: parseAsArrayOf(parseAsString).withDefault([]),
|
|
80
|
+
* },
|
|
81
|
+
* })
|
|
82
|
+
* const tableState = useDataTableClient({ data, columns, stateAdapter })
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
function useNuqsAdapter(options = {}) {
|
|
86
|
+
const { filters: filterParsers } = options;
|
|
87
|
+
const [coreState, setCoreState] = useQueryStates(coreSearchParams);
|
|
88
|
+
const hasFilters = filterParsers != null && Object.keys(filterParsers).length > 0;
|
|
89
|
+
const [filterState, setFilterState] = useQueryStates(hasFilters ? filterParsers : EMPTY_FILTER_PARSERS_PLACEHOLDER);
|
|
90
|
+
return useMemo(() => ({
|
|
91
|
+
read: () => ({
|
|
92
|
+
sorting: parseSorting(coreState.sort),
|
|
93
|
+
search: coreState.q,
|
|
94
|
+
pageIndex: coreState.page,
|
|
95
|
+
pageSize: coreState.size,
|
|
96
|
+
...hasFilters ? { filters: filterState } : {}
|
|
97
|
+
}),
|
|
98
|
+
write: (state) => {
|
|
99
|
+
setCoreState({
|
|
100
|
+
sort: serializeSorting(state.sorting),
|
|
101
|
+
q: state.search,
|
|
102
|
+
page: state.pageIndex ?? 0,
|
|
103
|
+
size: state.pageSize ?? 20
|
|
104
|
+
});
|
|
105
|
+
if (hasFilters && filterParsers) {
|
|
106
|
+
const update = {};
|
|
107
|
+
for (const key of Object.keys(filterParsers)) update[key] = state.filters?.[key] ?? null;
|
|
108
|
+
setFilterState(update);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}), [
|
|
112
|
+
coreState,
|
|
113
|
+
filterState,
|
|
114
|
+
hasFilters,
|
|
115
|
+
setCoreState,
|
|
116
|
+
setFilterState,
|
|
117
|
+
filterParsers
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/components/features/data-table/columns/selection-column.tsx
|
|
122
|
+
const SELECTION_COLUMN_ID = "select";
|
|
123
|
+
function createSelectionColumn(options = {}) {
|
|
124
|
+
const { className, headerClassName, renderHeader, renderCell } = options;
|
|
125
|
+
return {
|
|
126
|
+
id: SELECTION_COLUMN_ID,
|
|
127
|
+
size: 40,
|
|
128
|
+
enableSorting: false,
|
|
129
|
+
enableHiding: false,
|
|
130
|
+
header: renderHeader ?? (({ table }) => /* @__PURE__ */ jsx(Checkbox, {
|
|
131
|
+
checked: table.getIsAllPageRowsSelected() || table.getIsSomePageRowsSelected() && "indeterminate",
|
|
132
|
+
onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value),
|
|
133
|
+
"aria-label": "Select all",
|
|
134
|
+
className: headerClassName
|
|
135
|
+
})),
|
|
136
|
+
cell: renderCell ?? (({ row }) => /* @__PURE__ */ jsx(Checkbox, {
|
|
137
|
+
checked: row.getIsSelected(),
|
|
138
|
+
onCheckedChange: (value) => row.toggleSelected(!!value),
|
|
139
|
+
"aria-label": "Select row",
|
|
140
|
+
className
|
|
141
|
+
}))
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function hasSelectionColumn(columns) {
|
|
145
|
+
return columns.some((col) => "id" in col && col.id === SELECTION_COLUMN_ID);
|
|
146
|
+
}
|
|
147
|
+
function withSelectionColumn(columns, options = {}) {
|
|
148
|
+
if (hasSelectionColumn(columns)) return columns;
|
|
149
|
+
return [createSelectionColumn(options), ...columns];
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/components/features/data-table/components/column-header.tsx
|
|
153
|
+
function DataTableColumnHeader({ column, title, className }) {
|
|
154
|
+
if (!column.getCanSort()) return /* @__PURE__ */ jsx("div", {
|
|
155
|
+
className: cn(className),
|
|
156
|
+
"data-slot": "dt-column-header",
|
|
157
|
+
children: title
|
|
158
|
+
});
|
|
159
|
+
const sorted = column.getIsSorted();
|
|
160
|
+
return /* @__PURE__ */ jsx("div", {
|
|
161
|
+
className: cn("flex items-center gap-2", className),
|
|
162
|
+
"data-slot": "dt-column-header",
|
|
163
|
+
children: /* @__PURE__ */ jsxs("button", {
|
|
164
|
+
type: "button",
|
|
165
|
+
className: "flex items-center gap-1 hover:text-foreground -ml-3 h-8 px-3 cursor-pointer",
|
|
166
|
+
onClick: column.getToggleSortingHandler(),
|
|
167
|
+
"aria-label": `Sort by ${title}${sorted === "asc" ? ", sorted ascending" : sorted === "desc" ? ", sorted descending" : ""}`,
|
|
168
|
+
children: [/* @__PURE__ */ jsx("span", { children: title }), sorted === "desc" ? /* @__PURE__ */ jsx(ArrowDown, { className: "size-4" }) : sorted === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { className: "size-4" }) : /* @__PURE__ */ jsx(ArrowUpDown, { className: "size-4" })]
|
|
169
|
+
})
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/components/features/data-table/components/row-actions.tsx
|
|
174
|
+
function DataTableRowActions({ row, actions, isLoading = false, className, responsive = true, sheetTitle = "Actions" }) {
|
|
175
|
+
const [open, setOpen] = useState(false);
|
|
176
|
+
const data = row.original;
|
|
177
|
+
const visibleActions = actions.filter((action) => {
|
|
178
|
+
if (action.hidden === void 0) return true;
|
|
179
|
+
return typeof action.hidden === "function" ? !action.hidden(data) : !action.hidden;
|
|
180
|
+
});
|
|
181
|
+
if (visibleActions.length === 0) return null;
|
|
182
|
+
return /* @__PURE__ */ jsx(ResponsiveDropdown, {
|
|
183
|
+
open,
|
|
184
|
+
onOpenChange: setOpen,
|
|
185
|
+
trigger: /* @__PURE__ */ jsxs(Button, {
|
|
186
|
+
theme: "borderless",
|
|
187
|
+
size: "small",
|
|
188
|
+
className,
|
|
189
|
+
disabled: isLoading,
|
|
190
|
+
"data-slot": "dt-row-actions",
|
|
191
|
+
onClick: () => setOpen(!open),
|
|
192
|
+
children: [/* @__PURE__ */ jsx(MoreHorizontal, { className: "size-4" }), /* @__PURE__ */ jsx("span", {
|
|
193
|
+
className: "sr-only",
|
|
194
|
+
children: "Open menu"
|
|
195
|
+
})]
|
|
196
|
+
}),
|
|
197
|
+
sheetTitle,
|
|
198
|
+
align: "end",
|
|
199
|
+
responsive,
|
|
200
|
+
children: visibleActions.map((action) => /* @__PURE__ */ jsx(ActionRow, {
|
|
201
|
+
action,
|
|
202
|
+
data,
|
|
203
|
+
onSelect: () => setOpen(false)
|
|
204
|
+
}, action.label))
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/components/features/data-table/core/filter-engine.ts
|
|
209
|
+
/**
|
|
210
|
+
* Resolve a dot-path on an object (e.g. "status.registrationApproval").
|
|
211
|
+
* Falls back to a flat key lookup when the path has no dots.
|
|
212
|
+
*/
|
|
213
|
+
function resolvePath(obj, path) {
|
|
214
|
+
if (obj == null) return void 0;
|
|
215
|
+
const record = obj;
|
|
216
|
+
if (!path.includes(".")) return record[path];
|
|
217
|
+
return path.split(".").reduce((acc, key) => acc != null ? acc[key] : void 0, record);
|
|
218
|
+
}
|
|
219
|
+
const FILTER_STRATEGIES = {
|
|
220
|
+
"checkbox": (cellValue, filterValue) => {
|
|
221
|
+
if (filterValue == null) return true;
|
|
222
|
+
if (Array.isArray(filterValue) && filterValue.length === 0) return true;
|
|
223
|
+
if (!Array.isArray(filterValue)) return cellValue === filterValue;
|
|
224
|
+
if (Array.isArray(cellValue)) return cellValue.some((v) => filterValue.includes(v));
|
|
225
|
+
return filterValue.includes(cellValue);
|
|
226
|
+
},
|
|
227
|
+
"select": (cellValue, filterValue) => {
|
|
228
|
+
if (filterValue == null || filterValue === "") return true;
|
|
229
|
+
return cellValue === filterValue;
|
|
230
|
+
},
|
|
231
|
+
"date-gte": (cellValue, filterValue) => {
|
|
232
|
+
if (!filterValue) return true;
|
|
233
|
+
const cell = new Date(cellValue);
|
|
234
|
+
const filter = new Date(filterValue);
|
|
235
|
+
if (Number.isNaN(cell.getTime()) || Number.isNaN(filter.getTime())) return true;
|
|
236
|
+
return cell >= filter;
|
|
237
|
+
},
|
|
238
|
+
"date-lte": (cellValue, filterValue) => {
|
|
239
|
+
if (!filterValue) return true;
|
|
240
|
+
const cell = new Date(cellValue);
|
|
241
|
+
const filter = new Date(filterValue);
|
|
242
|
+
if (Number.isNaN(cell.getTime()) || Number.isNaN(filter.getTime())) return true;
|
|
243
|
+
return cell <= filter;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
function resolveStrategy(strategy) {
|
|
247
|
+
if (!strategy) return void 0;
|
|
248
|
+
if (typeof strategy === "function") return strategy;
|
|
249
|
+
return FILTER_STRATEGIES[strategy];
|
|
250
|
+
}
|
|
251
|
+
/** True when a row matches a free-text query (custom fn → columns → all values). */
|
|
252
|
+
function rowMatchesSearch(row, search, config) {
|
|
253
|
+
if (!search || search.length === 0) return true;
|
|
254
|
+
if (config.searchFn) return config.searchFn(row, search);
|
|
255
|
+
const query = search.toLowerCase();
|
|
256
|
+
if (config.searchableColumns && config.searchableColumns.length > 0) return config.searchableColumns.some((col) => {
|
|
257
|
+
const cellValue = resolvePath(row, col);
|
|
258
|
+
return cellValue != null && String(cellValue).toLowerCase().includes(query);
|
|
259
|
+
});
|
|
260
|
+
return Object.values(row).some((val) => val != null && String(val).toLowerCase().includes(query));
|
|
261
|
+
}
|
|
262
|
+
function applyFilters(data, filters, search, registeredFilters, customFilterFns, searchConfig) {
|
|
263
|
+
const hasFilters = Object.keys(filters).length > 0;
|
|
264
|
+
const hasSearch = search.length > 0;
|
|
265
|
+
if (!hasFilters && !hasSearch) return data;
|
|
266
|
+
return data.filter((row) => {
|
|
267
|
+
if (hasFilters) for (const [column, value] of Object.entries(filters)) {
|
|
268
|
+
const fn = customFilterFns[column] ?? resolveStrategy(registeredFilters.get(column));
|
|
269
|
+
if (!fn) {
|
|
270
|
+
console.warn(`[DataTable] No filter strategy registered for column "${column}". Filter ignored.`);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (!fn(resolvePath(row, column), value)) return false;
|
|
274
|
+
}
|
|
275
|
+
if (hasSearch && !rowMatchesSearch(row, search, searchConfig)) return false;
|
|
276
|
+
return true;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region src/components/features/data-table/core/store.ts
|
|
281
|
+
function createDataTableStore(options) {
|
|
282
|
+
let registeredFilters = /* @__PURE__ */ new Map();
|
|
283
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
284
|
+
function computeFilteredData(s) {
|
|
285
|
+
if (s.mode === "server") return s.data;
|
|
286
|
+
return applyFilters(s.data, s.filters, s.search, registeredFilters, options.filterFns ?? {}, {
|
|
287
|
+
searchFn: options.searchFn,
|
|
288
|
+
searchableColumns: options.searchableColumns
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
let state = {
|
|
292
|
+
data: options.data,
|
|
293
|
+
filteredData: options.data,
|
|
294
|
+
sorting: options.defaultSort ?? [],
|
|
295
|
+
filters: options.defaultFilters ?? {},
|
|
296
|
+
search: "",
|
|
297
|
+
rowSelection: {},
|
|
298
|
+
pageIndex: 0,
|
|
299
|
+
pageSize: options.pageSize ?? 20,
|
|
300
|
+
columnCount: options.columnCount ?? 0,
|
|
301
|
+
mode: options.mode,
|
|
302
|
+
isLoading: options.isLoading ?? false,
|
|
303
|
+
error: null,
|
|
304
|
+
inlineContents: [],
|
|
305
|
+
_version: 0
|
|
306
|
+
};
|
|
307
|
+
if (options.defaultFilters && Object.keys(options.defaultFilters).length > 0) state = {
|
|
308
|
+
...state,
|
|
309
|
+
filteredData: computeFilteredData(state)
|
|
310
|
+
};
|
|
311
|
+
function notify() {
|
|
312
|
+
for (const listener of listeners) listener();
|
|
313
|
+
}
|
|
314
|
+
function setState(next) {
|
|
315
|
+
state = {
|
|
316
|
+
...next,
|
|
317
|
+
_version: state._version + 1
|
|
318
|
+
};
|
|
319
|
+
notify();
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
getSnapshot: () => state,
|
|
323
|
+
subscribe: (listener) => {
|
|
324
|
+
listeners.add(listener);
|
|
325
|
+
return () => listeners.delete(listener);
|
|
326
|
+
},
|
|
327
|
+
setData: (data) => {
|
|
328
|
+
const next = {
|
|
329
|
+
...state,
|
|
330
|
+
data,
|
|
331
|
+
pageIndex: 0,
|
|
332
|
+
rowSelection: {}
|
|
333
|
+
};
|
|
334
|
+
setState({
|
|
335
|
+
...next,
|
|
336
|
+
filteredData: computeFilteredData(next)
|
|
337
|
+
});
|
|
338
|
+
},
|
|
339
|
+
setServerData: (data) => {
|
|
340
|
+
setState({
|
|
341
|
+
...state,
|
|
342
|
+
data,
|
|
343
|
+
filteredData: data
|
|
344
|
+
});
|
|
345
|
+
},
|
|
346
|
+
setSorting: (sorting) => {
|
|
347
|
+
setState({
|
|
348
|
+
...state,
|
|
349
|
+
sorting,
|
|
350
|
+
rowSelection: {}
|
|
351
|
+
});
|
|
352
|
+
},
|
|
353
|
+
setFilter: (key, value) => {
|
|
354
|
+
const next = {
|
|
355
|
+
...state,
|
|
356
|
+
filters: {
|
|
357
|
+
...state.filters,
|
|
358
|
+
[key]: value
|
|
359
|
+
},
|
|
360
|
+
rowSelection: {},
|
|
361
|
+
pageIndex: 0
|
|
362
|
+
};
|
|
363
|
+
setState({
|
|
364
|
+
...next,
|
|
365
|
+
filteredData: computeFilteredData(next)
|
|
366
|
+
});
|
|
367
|
+
},
|
|
368
|
+
clearFilter: (key) => {
|
|
369
|
+
const filters = Object.fromEntries(Object.entries(state.filters).filter(([k]) => k !== key));
|
|
370
|
+
const next = {
|
|
371
|
+
...state,
|
|
372
|
+
filters,
|
|
373
|
+
rowSelection: {},
|
|
374
|
+
pageIndex: 0
|
|
375
|
+
};
|
|
376
|
+
setState({
|
|
377
|
+
...next,
|
|
378
|
+
filteredData: computeFilteredData(next)
|
|
379
|
+
});
|
|
380
|
+
},
|
|
381
|
+
clearAllFilters: () => {
|
|
382
|
+
const next = {
|
|
383
|
+
...state,
|
|
384
|
+
filters: {},
|
|
385
|
+
rowSelection: {},
|
|
386
|
+
pageIndex: 0
|
|
387
|
+
};
|
|
388
|
+
setState({
|
|
389
|
+
...next,
|
|
390
|
+
filteredData: computeFilteredData(next)
|
|
391
|
+
});
|
|
392
|
+
},
|
|
393
|
+
setSearch: (search) => {
|
|
394
|
+
const next = {
|
|
395
|
+
...state,
|
|
396
|
+
search,
|
|
397
|
+
rowSelection: {},
|
|
398
|
+
pageIndex: 0
|
|
399
|
+
};
|
|
400
|
+
setState({
|
|
401
|
+
...next,
|
|
402
|
+
filteredData: computeFilteredData(next)
|
|
403
|
+
});
|
|
404
|
+
},
|
|
405
|
+
clearSearch: () => {
|
|
406
|
+
const next = {
|
|
407
|
+
...state,
|
|
408
|
+
search: "",
|
|
409
|
+
rowSelection: {},
|
|
410
|
+
pageIndex: 0
|
|
411
|
+
};
|
|
412
|
+
setState({
|
|
413
|
+
...next,
|
|
414
|
+
filteredData: computeFilteredData(next)
|
|
415
|
+
});
|
|
416
|
+
},
|
|
417
|
+
setRowSelection: (rowSelection) => {
|
|
418
|
+
setState({
|
|
419
|
+
...state,
|
|
420
|
+
rowSelection
|
|
421
|
+
});
|
|
422
|
+
},
|
|
423
|
+
setPageIndex: (pageIndex) => {
|
|
424
|
+
if (!Number.isFinite(pageIndex) || pageIndex < 0) return;
|
|
425
|
+
setState({
|
|
426
|
+
...state,
|
|
427
|
+
pageIndex: Math.floor(pageIndex),
|
|
428
|
+
rowSelection: {}
|
|
429
|
+
});
|
|
430
|
+
},
|
|
431
|
+
setPageSize: (pageSize) => {
|
|
432
|
+
if (!Number.isFinite(pageSize) || pageSize < 1) return;
|
|
433
|
+
setState({
|
|
434
|
+
...state,
|
|
435
|
+
pageSize: Math.floor(pageSize),
|
|
436
|
+
pageIndex: 0,
|
|
437
|
+
rowSelection: {}
|
|
438
|
+
});
|
|
439
|
+
},
|
|
440
|
+
setPagination: (pageIndex, pageSize) => {
|
|
441
|
+
const safeIndex = Number.isFinite(pageIndex) ? Math.max(0, Math.floor(pageIndex)) : state.pageIndex;
|
|
442
|
+
const safeSize = Number.isFinite(pageSize) ? Math.max(1, Math.floor(pageSize)) : state.pageSize;
|
|
443
|
+
setState({
|
|
444
|
+
...state,
|
|
445
|
+
pageIndex: safeIndex,
|
|
446
|
+
pageSize: safeSize,
|
|
447
|
+
rowSelection: {}
|
|
448
|
+
});
|
|
449
|
+
},
|
|
450
|
+
setLoading: (isLoading) => {
|
|
451
|
+
setState({
|
|
452
|
+
...state,
|
|
453
|
+
isLoading
|
|
454
|
+
});
|
|
455
|
+
},
|
|
456
|
+
setError: (error) => {
|
|
457
|
+
setState({
|
|
458
|
+
...state,
|
|
459
|
+
error
|
|
460
|
+
});
|
|
461
|
+
},
|
|
462
|
+
registerFilter: (column, strategy) => {
|
|
463
|
+
const next = new Map(registeredFilters);
|
|
464
|
+
next.set(column, strategy);
|
|
465
|
+
registeredFilters = next;
|
|
466
|
+
const filteredData = computeFilteredData(state);
|
|
467
|
+
setState({
|
|
468
|
+
...state,
|
|
469
|
+
filteredData
|
|
470
|
+
});
|
|
471
|
+
},
|
|
472
|
+
unregisterFilter: (column) => {
|
|
473
|
+
const next = new Map(registeredFilters);
|
|
474
|
+
next.delete(column);
|
|
475
|
+
registeredFilters = next;
|
|
476
|
+
if (column in state.filters) {
|
|
477
|
+
const filteredData = computeFilteredData(state);
|
|
478
|
+
setState({
|
|
479
|
+
...state,
|
|
480
|
+
filteredData
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
registerInlineContent: (entry) => {
|
|
485
|
+
const existing = state.inlineContents.findIndex((e) => e.id === entry.id);
|
|
486
|
+
const inlineContents = existing >= 0 ? state.inlineContents.map((e, i) => i === existing ? entry : e) : [...state.inlineContents, entry];
|
|
487
|
+
setState({
|
|
488
|
+
...state,
|
|
489
|
+
inlineContents
|
|
490
|
+
});
|
|
491
|
+
},
|
|
492
|
+
unregisterInlineContent: (id) => {
|
|
493
|
+
const inlineContents = state.inlineContents.filter((e) => e.id !== id);
|
|
494
|
+
setState({
|
|
495
|
+
...state,
|
|
496
|
+
inlineContents
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
//#endregion
|
|
502
|
+
//#region src/components/features/data-table/core/data-table-context.tsx
|
|
503
|
+
const DataTableStoreContext = createContext(null);
|
|
504
|
+
const TableInstanceContext = createContext(null);
|
|
505
|
+
/**
|
|
506
|
+
* Monotonic counter that increments on every store mutation.
|
|
507
|
+
* Table-dependent hooks consume this context to force re-renders
|
|
508
|
+
* through React's {children} composition boundary, since the
|
|
509
|
+
* mutable table singleton (stable ref) cannot trigger context updates.
|
|
510
|
+
*/
|
|
511
|
+
const DataTableRenderKeyContext = createContext(0);
|
|
512
|
+
/**
|
|
513
|
+
* Forces a re-render when the store changes. Used by table-dependent hooks.
|
|
514
|
+
* Uses useContext (not use()) because use() does not reliably register
|
|
515
|
+
* context subscriptions in SSR/hydration scenarios (React Router SSR),
|
|
516
|
+
* preventing re-renders when the context value changes.
|
|
517
|
+
*/
|
|
518
|
+
function useRenderKey() {
|
|
519
|
+
return useContext(DataTableRenderKeyContext);
|
|
520
|
+
}
|
|
521
|
+
createContext(null);
|
|
522
|
+
function useDataTableStore() {
|
|
523
|
+
const store = use(DataTableStoreContext);
|
|
524
|
+
if (!store) throw new Error("useDataTableStore must be used within a <DataTable.Client> or <DataTable.Server> provider");
|
|
525
|
+
return store;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Returns the table instance or null if not yet available.
|
|
529
|
+
* Used by hooks that need to handle the null-table window during SSR.
|
|
530
|
+
*/
|
|
531
|
+
function useTableInstanceOrNull() {
|
|
532
|
+
return use(TableInstanceContext);
|
|
533
|
+
}
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/components/features/data-table/hooks/use-selectors.ts
|
|
536
|
+
function shallowEqual(a, b) {
|
|
537
|
+
const keysA = Object.keys(a);
|
|
538
|
+
const keysB = Object.keys(b);
|
|
539
|
+
if (keysA.length !== keysB.length) return false;
|
|
540
|
+
for (const key of keysA) {
|
|
541
|
+
const va = a[key];
|
|
542
|
+
const vb = b[key];
|
|
543
|
+
if (va === vb) continue;
|
|
544
|
+
if (Array.isArray(va) && Array.isArray(vb)) {
|
|
545
|
+
if (va.length !== vb.length) return false;
|
|
546
|
+
for (let i = 0; i < va.length; i++) if (va[i] !== vb[i]) return false;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
function useSliceSelector(selector) {
|
|
554
|
+
const store = useDataTableStore();
|
|
555
|
+
const cachedRef = useRef(null);
|
|
556
|
+
const getSnapshot = useCallback(() => {
|
|
557
|
+
const next = selector(store.getSnapshot());
|
|
558
|
+
if (cachedRef.current && shallowEqual(cachedRef.current, next)) return cachedRef.current;
|
|
559
|
+
cachedRef.current = next;
|
|
560
|
+
return next;
|
|
561
|
+
}, [store, selector]);
|
|
562
|
+
return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
|
|
563
|
+
}
|
|
564
|
+
function useDataTableFilters() {
|
|
565
|
+
const store = useDataTableStore();
|
|
566
|
+
return useSliceSelector(useCallback((state) => ({
|
|
567
|
+
filters: state.filters,
|
|
568
|
+
setFilter: store.setFilter,
|
|
569
|
+
clearFilter: store.clearFilter,
|
|
570
|
+
clearAllFilters: store.clearAllFilters,
|
|
571
|
+
registerFilter: store.registerFilter,
|
|
572
|
+
unregisterFilter: store.unregisterFilter
|
|
573
|
+
}), [store]));
|
|
574
|
+
}
|
|
575
|
+
function useDataTableSearch() {
|
|
576
|
+
const store = useDataTableStore();
|
|
577
|
+
return useSliceSelector(useCallback((state) => ({
|
|
578
|
+
search: state.search,
|
|
579
|
+
setSearch: store.setSearch,
|
|
580
|
+
clearSearch: store.clearSearch
|
|
581
|
+
}), [store]));
|
|
582
|
+
}
|
|
583
|
+
function useDataTableSorting() {
|
|
584
|
+
const store = useDataTableStore();
|
|
585
|
+
return useSliceSelector(useCallback((state) => ({
|
|
586
|
+
sorting: state.sorting,
|
|
587
|
+
setSorting: store.setSorting
|
|
588
|
+
}), [store]));
|
|
589
|
+
}
|
|
590
|
+
function useDataTableSelection() {
|
|
591
|
+
useRenderKey();
|
|
592
|
+
const store = useDataTableStore();
|
|
593
|
+
const table = useTableInstanceOrNull();
|
|
594
|
+
return {
|
|
595
|
+
rowSelection: store.getSnapshot().rowSelection,
|
|
596
|
+
setRowSelection: store.setRowSelection,
|
|
597
|
+
selectedRows: table ? table.getFilteredSelectedRowModel().rows.map((r) => r.original) : []
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function useDataTablePagination() {
|
|
601
|
+
useRenderKey();
|
|
602
|
+
const store = useDataTableStore();
|
|
603
|
+
const table = useTableInstanceOrNull();
|
|
604
|
+
const state = store.getSnapshot();
|
|
605
|
+
if (!table) return {
|
|
606
|
+
canNextPage: false,
|
|
607
|
+
canPrevPage: false,
|
|
608
|
+
nextPage: () => {},
|
|
609
|
+
prevPage: () => {},
|
|
610
|
+
pageIndex: state.pageIndex,
|
|
611
|
+
pageCount: 0,
|
|
612
|
+
setPageIndex: store.setPageIndex,
|
|
613
|
+
pageSize: state.pageSize,
|
|
614
|
+
setPageSize: store.setPageSize,
|
|
615
|
+
totalRows: 0
|
|
616
|
+
};
|
|
617
|
+
return {
|
|
618
|
+
canNextPage: table.getCanNextPage(),
|
|
619
|
+
canPrevPage: table.getCanPreviousPage(),
|
|
620
|
+
nextPage: () => table.nextPage(),
|
|
621
|
+
prevPage: () => table.previousPage(),
|
|
622
|
+
pageIndex: state.pageIndex,
|
|
623
|
+
pageCount: table.getPageCount(),
|
|
624
|
+
setPageIndex: store.setPageIndex,
|
|
625
|
+
pageSize: state.pageSize,
|
|
626
|
+
setPageSize: store.setPageSize,
|
|
627
|
+
totalRows: state.filteredData.length
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
function useDataTableRows() {
|
|
631
|
+
useRenderKey();
|
|
632
|
+
const table = useTableInstanceOrNull();
|
|
633
|
+
if (!table) return {
|
|
634
|
+
rows: [],
|
|
635
|
+
headerGroups: [],
|
|
636
|
+
totalColumns: 0
|
|
637
|
+
};
|
|
638
|
+
return {
|
|
639
|
+
rows: table.getRowModel().rows,
|
|
640
|
+
headerGroups: table.getHeaderGroups(),
|
|
641
|
+
totalColumns: table.getAllColumns().length
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function useDataTableLoading() {
|
|
645
|
+
return useSliceSelector(useCallback((state) => ({
|
|
646
|
+
isLoading: state.isLoading,
|
|
647
|
+
error: state.error,
|
|
648
|
+
columnCount: state.columnCount
|
|
649
|
+
}), []));
|
|
650
|
+
}
|
|
651
|
+
function useDataTableInlineContents() {
|
|
652
|
+
const store = useDataTableStore();
|
|
653
|
+
return useSliceSelector(useCallback((state) => ({
|
|
654
|
+
inlineContents: state.inlineContents,
|
|
655
|
+
registerInlineContent: store.registerInlineContent,
|
|
656
|
+
unregisterInlineContent: store.unregisterInlineContent
|
|
657
|
+
}), [store]));
|
|
658
|
+
}
|
|
659
|
+
//#endregion
|
|
660
|
+
//#region src/components/features/data-table/components/active-filters.tsx
|
|
661
|
+
function formatValue(column, value, formatters) {
|
|
662
|
+
const fn = formatters?.[column];
|
|
663
|
+
if (fn) return fn(value);
|
|
664
|
+
return String(value);
|
|
665
|
+
}
|
|
666
|
+
function FilterGroup({ label, children, className }) {
|
|
667
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
668
|
+
className: cn("flex items-center gap-2 rounded-md border px-2 py-1", className),
|
|
669
|
+
"data-slot": "dt-filter-group",
|
|
670
|
+
"data-testid": "dt-filter-group",
|
|
671
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
672
|
+
className: "text-muted-foreground border-r pr-2 text-xs",
|
|
673
|
+
children: label
|
|
674
|
+
}), children]
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
const EMPTY_LABELS = {};
|
|
678
|
+
function ActiveFiltersInner({ label = "Selected Filters", excludeFilters, filterLabels = EMPTY_LABELS, formatFilterValue: formatters, clearAll = "icon", clearAllLabel = "Clear all", className, groupClassName, badgeClassName }) {
|
|
679
|
+
const { filters, setFilter, clearFilter, clearAllFilters } = useDataTableFilters();
|
|
680
|
+
const { search, clearSearch } = useDataTableSearch();
|
|
681
|
+
const excludeSet = useMemo(() => new Set(excludeFilters ?? []), [excludeFilters]);
|
|
682
|
+
const activeFilterEntries = Object.entries(filters).filter(([key, value]) => !excludeSet.has(key) && value != null && value !== "" && !(Array.isArray(value) && value.length === 0));
|
|
683
|
+
const showSearch = search.length > 0 && !excludeSet.has("search");
|
|
684
|
+
const hasFilters = activeFilterEntries.length > 0;
|
|
685
|
+
if (!showSearch && !hasFilters) return null;
|
|
686
|
+
const totalGroups = activeFilterEntries.length + (showSearch ? 1 : 0);
|
|
687
|
+
const removeArrayItem = (column, items, item) => {
|
|
688
|
+
const remaining = items.filter((v) => v !== item);
|
|
689
|
+
if (remaining.length > 0) setFilter(column, remaining);
|
|
690
|
+
else clearFilter(column);
|
|
691
|
+
};
|
|
692
|
+
const handleClearAll = () => {
|
|
693
|
+
clearAllFilters();
|
|
694
|
+
if (search.length > 0) clearSearch();
|
|
695
|
+
};
|
|
696
|
+
const badgeCn = cn("flex items-center gap-1.5 px-2 py-0.5 text-xs", badgeClassName);
|
|
697
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
698
|
+
className: cn("flex flex-wrap items-center gap-2", className),
|
|
699
|
+
"data-slot": "dt-active-filters",
|
|
700
|
+
"data-testid": "dt-active-filters",
|
|
701
|
+
children: [
|
|
702
|
+
label !== null && /* @__PURE__ */ jsx("span", {
|
|
703
|
+
className: "text-sm text-muted-foreground",
|
|
704
|
+
"data-slot": "dt-active-filters-label",
|
|
705
|
+
children: label
|
|
706
|
+
}),
|
|
707
|
+
showSearch && /* @__PURE__ */ jsx(FilterGroup, {
|
|
708
|
+
label: "Search",
|
|
709
|
+
className: groupClassName,
|
|
710
|
+
children: /* @__PURE__ */ jsxs(Badge, {
|
|
711
|
+
type: "muted",
|
|
712
|
+
theme: "solid",
|
|
713
|
+
className: badgeCn,
|
|
714
|
+
children: [/* @__PURE__ */ jsx("span", { children: search }), /* @__PURE__ */ jsx(Button, {
|
|
715
|
+
theme: "borderless",
|
|
716
|
+
size: "small",
|
|
717
|
+
"aria-label": "Clear search",
|
|
718
|
+
className: "h-auto p-0 text-muted-foreground hover:text-foreground",
|
|
719
|
+
onClick: clearSearch,
|
|
720
|
+
children: /* @__PURE__ */ jsx(X, {
|
|
721
|
+
className: "size-2.5",
|
|
722
|
+
"aria-hidden": "true"
|
|
723
|
+
})
|
|
724
|
+
})]
|
|
725
|
+
})
|
|
726
|
+
}),
|
|
727
|
+
activeFilterEntries.map(([column, value]) => {
|
|
728
|
+
const groupLabel = filterLabels[column] ?? column;
|
|
729
|
+
if (Array.isArray(value)) return /* @__PURE__ */ jsx(FilterGroup, {
|
|
730
|
+
label: groupLabel,
|
|
731
|
+
className: groupClassName,
|
|
732
|
+
children: value.map((item) => /* @__PURE__ */ jsxs(Badge, {
|
|
733
|
+
type: "muted",
|
|
734
|
+
theme: "solid",
|
|
735
|
+
className: badgeCn,
|
|
736
|
+
children: [/* @__PURE__ */ jsx("span", { children: formatValue(column, item, formatters) }), /* @__PURE__ */ jsx(Button, {
|
|
737
|
+
theme: "borderless",
|
|
738
|
+
size: "small",
|
|
739
|
+
"aria-label": `Remove ${formatValue(column, item, formatters)} from ${groupLabel}`,
|
|
740
|
+
className: "h-auto p-0 text-muted-foreground hover:text-foreground",
|
|
741
|
+
onClick: () => removeArrayItem(column, value, item),
|
|
742
|
+
children: /* @__PURE__ */ jsx(X, {
|
|
743
|
+
className: "size-2.5",
|
|
744
|
+
"aria-hidden": "true"
|
|
745
|
+
})
|
|
746
|
+
})]
|
|
747
|
+
}, item))
|
|
748
|
+
}, column);
|
|
749
|
+
return /* @__PURE__ */ jsx(FilterGroup, {
|
|
750
|
+
label: groupLabel,
|
|
751
|
+
className: groupClassName,
|
|
752
|
+
children: /* @__PURE__ */ jsxs(Badge, {
|
|
753
|
+
type: "muted",
|
|
754
|
+
theme: "solid",
|
|
755
|
+
className: badgeCn,
|
|
756
|
+
children: [/* @__PURE__ */ jsx("span", { children: formatValue(column, value, formatters) }), /* @__PURE__ */ jsx(Button, {
|
|
757
|
+
theme: "borderless",
|
|
758
|
+
size: "small",
|
|
759
|
+
"aria-label": `Clear ${groupLabel} filter`,
|
|
760
|
+
className: "h-auto p-0 text-muted-foreground hover:text-foreground",
|
|
761
|
+
onClick: () => clearFilter(column),
|
|
762
|
+
children: /* @__PURE__ */ jsx(X, {
|
|
763
|
+
className: "size-2.5",
|
|
764
|
+
"aria-hidden": "true"
|
|
765
|
+
})
|
|
766
|
+
})]
|
|
767
|
+
})
|
|
768
|
+
}, column);
|
|
769
|
+
}),
|
|
770
|
+
totalGroups > 1 && /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
771
|
+
clearAll === "icon" && /* @__PURE__ */ jsx(Button, {
|
|
772
|
+
theme: "borderless",
|
|
773
|
+
size: "small",
|
|
774
|
+
"aria-label": clearAllLabel,
|
|
775
|
+
title: clearAllLabel,
|
|
776
|
+
className: "h-auto p-1 text-muted-foreground hover:text-foreground",
|
|
777
|
+
"data-slot": "dt-clear-all-filters",
|
|
778
|
+
"data-testid": "dt-clear-all-filters",
|
|
779
|
+
onClick: handleClearAll,
|
|
780
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-4" })
|
|
781
|
+
}),
|
|
782
|
+
clearAll === "button" && /* @__PURE__ */ jsxs(Button, {
|
|
783
|
+
theme: "outline",
|
|
784
|
+
size: "small",
|
|
785
|
+
className: "h-auto px-2 py-1 text-xs",
|
|
786
|
+
"data-slot": "dt-clear-all-filters",
|
|
787
|
+
"data-testid": "dt-clear-all-filters",
|
|
788
|
+
onClick: handleClearAll,
|
|
789
|
+
children: [/* @__PURE__ */ jsx(X, { className: "size-3 mr-1" }), clearAllLabel]
|
|
790
|
+
}),
|
|
791
|
+
clearAll === "text" && /* @__PURE__ */ jsx(Button, {
|
|
792
|
+
theme: "borderless",
|
|
793
|
+
size: "small",
|
|
794
|
+
className: "h-auto px-1 py-0.5 text-xs text-muted-foreground hover:text-foreground",
|
|
795
|
+
"data-slot": "dt-clear-all-filters",
|
|
796
|
+
"data-testid": "dt-clear-all-filters",
|
|
797
|
+
onClick: handleClearAll,
|
|
798
|
+
children: clearAllLabel
|
|
799
|
+
})
|
|
800
|
+
] })
|
|
801
|
+
]
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
const DataTableActiveFilters = memo(ActiveFiltersInner);
|
|
805
|
+
//#endregion
|
|
806
|
+
//#region src/components/features/data-table/components/bulk-actions.tsx
|
|
807
|
+
function DataTableBulkActions({ children, className }) {
|
|
808
|
+
const { selectedRows } = useDataTableSelection();
|
|
809
|
+
if (selectedRows.length === 0) return null;
|
|
810
|
+
return /* @__PURE__ */ jsx("div", {
|
|
811
|
+
"data-slot": "dt-bulk-actions",
|
|
812
|
+
className,
|
|
813
|
+
children: children(selectedRows)
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
//#endregion
|
|
817
|
+
//#region src/components/features/data-table/components/content.tsx
|
|
818
|
+
function resolveClassName(value, item) {
|
|
819
|
+
if (typeof value === "function") return value(item);
|
|
820
|
+
return value;
|
|
821
|
+
}
|
|
822
|
+
function renderInlineContentRow(entry, colSpan, rows) {
|
|
823
|
+
return /* @__PURE__ */ jsx(TableRow, {
|
|
824
|
+
"data-slot": "dt-inline-content",
|
|
825
|
+
"data-position": entry.position,
|
|
826
|
+
className: cn("transition-all duration-200", entry.className),
|
|
827
|
+
children: /* @__PURE__ */ jsx(TableCell, {
|
|
828
|
+
colSpan,
|
|
829
|
+
children: entry.render({
|
|
830
|
+
onClose: entry.onClose,
|
|
831
|
+
rowData: entry.position === "row" ? rows.find((r) => r.id === entry.rowId)?.original ?? null : null
|
|
832
|
+
})
|
|
833
|
+
})
|
|
834
|
+
}, entry.id);
|
|
835
|
+
}
|
|
836
|
+
function DataTableContent({ emptyMessage, className, tableClassName, headerClassName, headerRowClassName, headerCellClassName, bodyClassName, rowClassName, cellClassName }) {
|
|
837
|
+
const { rows, headerGroups, totalColumns } = useDataTableRows();
|
|
838
|
+
const { isLoading, columnCount } = useDataTableLoading();
|
|
839
|
+
const { pageSize } = useDataTablePagination();
|
|
840
|
+
const { inlineContents } = useDataTableInlineContents();
|
|
841
|
+
const openInlineContents = useMemo(() => inlineContents.filter((e) => e.open), [inlineContents]);
|
|
842
|
+
const colSpan = totalColumns;
|
|
843
|
+
const skeletonColumns = totalColumns || columnCount || 5;
|
|
844
|
+
return /* @__PURE__ */ jsx("div", {
|
|
845
|
+
className: cn("datum-ui-data-table", className),
|
|
846
|
+
"data-slot": "dt",
|
|
847
|
+
style: { overflowX: "auto" },
|
|
848
|
+
children: /* @__PURE__ */ jsxs(Table, {
|
|
849
|
+
className: cn(tableClassName),
|
|
850
|
+
"data-slot": "dt-table",
|
|
851
|
+
children: [/* @__PURE__ */ jsx(TableHeader, {
|
|
852
|
+
className: cn(headerClassName),
|
|
853
|
+
"data-slot": "dt-header",
|
|
854
|
+
children: headerGroups.map((headerGroup) => /* @__PURE__ */ jsx(TableRow, {
|
|
855
|
+
className: cn(headerRowClassName),
|
|
856
|
+
"data-slot": "dt-header-row",
|
|
857
|
+
children: headerGroup.headers.map((header) => /* @__PURE__ */ jsx(TableHead, {
|
|
858
|
+
className: cn(headerCellClassName),
|
|
859
|
+
"data-slot": "dt-header-cell",
|
|
860
|
+
children: header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())
|
|
861
|
+
}, header.id))
|
|
862
|
+
}, headerGroup.id))
|
|
863
|
+
}), /* @__PURE__ */ jsxs(TableBody, {
|
|
864
|
+
className: cn(bodyClassName),
|
|
865
|
+
"data-slot": "dt-body",
|
|
866
|
+
children: [openInlineContents.filter((e) => e.position === "top").map((entry) => renderInlineContentRow(entry, colSpan, rows)), rows.length > 0 ? rows.map((row) => {
|
|
867
|
+
const rowEntry = openInlineContents.find((e) => e.position === "row" && e.rowId === row.id);
|
|
868
|
+
if (rowEntry) return renderInlineContentRow(rowEntry, colSpan, rows);
|
|
869
|
+
return /* @__PURE__ */ jsx(TableRow, {
|
|
870
|
+
className: cn(resolveClassName(rowClassName, row)),
|
|
871
|
+
style: { transitionProperty: "none" },
|
|
872
|
+
"data-slot": "dt-row",
|
|
873
|
+
"data-state": row.getIsSelected() ? "selected" : void 0,
|
|
874
|
+
children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(TableCell, {
|
|
875
|
+
className: cn(resolveClassName(cellClassName, cell)),
|
|
876
|
+
"data-slot": "dt-cell",
|
|
877
|
+
children: flexRender(cell.column.columnDef.cell, cell.getContext())
|
|
878
|
+
}, cell.id))
|
|
879
|
+
}, row.id);
|
|
880
|
+
}) : isLoading ? Array.from({ length: pageSize }, (_, i) => /* @__PURE__ */ jsx(TableRow, {
|
|
881
|
+
"data-slot": "dt-skeleton-row",
|
|
882
|
+
children: Array.from({ length: skeletonColumns }, (_, j) => /* @__PURE__ */ jsx(TableCell, {
|
|
883
|
+
"data-slot": "dt-skeleton-cell",
|
|
884
|
+
children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" })
|
|
885
|
+
}, j))
|
|
886
|
+
}, i)) : /* @__PURE__ */ jsx(TableRow, {
|
|
887
|
+
"data-slot": "dt-row",
|
|
888
|
+
children: /* @__PURE__ */ jsx(TableCell, {
|
|
889
|
+
colSpan,
|
|
890
|
+
className: "h-24 text-center",
|
|
891
|
+
"data-slot": "dt-empty",
|
|
892
|
+
children: emptyMessage ?? "No results."
|
|
893
|
+
})
|
|
894
|
+
})]
|
|
895
|
+
})]
|
|
896
|
+
})
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
//#endregion
|
|
900
|
+
//#region src/components/features/data-table/components/inline-content.tsx
|
|
901
|
+
function DataTableInlineContent({ position, rowId, open, onClose, className, children }) {
|
|
902
|
+
const id = useId();
|
|
903
|
+
const { registerInlineContent, unregisterInlineContent } = useDataTableInlineContents();
|
|
904
|
+
const initialRender = useRef(true);
|
|
905
|
+
useEffect(() => {
|
|
906
|
+
registerInlineContent({
|
|
907
|
+
id,
|
|
908
|
+
position,
|
|
909
|
+
rowId,
|
|
910
|
+
open,
|
|
911
|
+
onClose,
|
|
912
|
+
className,
|
|
913
|
+
render: children
|
|
914
|
+
});
|
|
915
|
+
return () => {
|
|
916
|
+
unregisterInlineContent(id);
|
|
917
|
+
};
|
|
918
|
+
}, [
|
|
919
|
+
id,
|
|
920
|
+
registerInlineContent,
|
|
921
|
+
unregisterInlineContent
|
|
922
|
+
]);
|
|
923
|
+
useEffect(() => {
|
|
924
|
+
if (initialRender.current) {
|
|
925
|
+
initialRender.current = false;
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
registerInlineContent({
|
|
929
|
+
id,
|
|
930
|
+
position,
|
|
931
|
+
rowId,
|
|
932
|
+
open,
|
|
933
|
+
onClose,
|
|
934
|
+
className,
|
|
935
|
+
render: children
|
|
936
|
+
});
|
|
937
|
+
}, [
|
|
938
|
+
id,
|
|
939
|
+
position,
|
|
940
|
+
rowId,
|
|
941
|
+
open,
|
|
942
|
+
onClose,
|
|
943
|
+
className,
|
|
944
|
+
children,
|
|
945
|
+
registerInlineContent
|
|
946
|
+
]);
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
//#endregion
|
|
950
|
+
//#region src/components/features/data-table/components/loading.tsx
|
|
951
|
+
function DataTableLoading({ rows = 5, columns = 4, className }) {
|
|
952
|
+
return /* @__PURE__ */ jsx("div", {
|
|
953
|
+
className,
|
|
954
|
+
"data-slot": "dt-loading",
|
|
955
|
+
style: { overflowX: "auto" },
|
|
956
|
+
children: /* @__PURE__ */ jsxs(Table, { children: [/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsx(TableRow, { children: Array.from({ length: columns }, (_, i) => /* @__PURE__ */ jsx(TableHead, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" }) }, i)) }) }), /* @__PURE__ */ jsx(TableBody, { children: Array.from({ length: rows }, (_, rowIndex) => /* @__PURE__ */ jsx(TableRow, { children: Array.from({ length: columns }, (_, colIndex) => /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }) }, colIndex)) }, rowIndex)) })] })
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
//#endregion
|
|
960
|
+
//#region src/components/features/data-table/components/pagination.tsx
|
|
961
|
+
/**
|
|
962
|
+
* Generates page numbers with ellipsis for large page counts.
|
|
963
|
+
* Shows up to 7 items: first, last, current +/- 1 neighbor, and ellipsis gaps.
|
|
964
|
+
*/
|
|
965
|
+
function getPageNumbers(currentPage, totalPages) {
|
|
966
|
+
if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
967
|
+
const pages = [1];
|
|
968
|
+
const current = currentPage + 1;
|
|
969
|
+
if (current <= 4) {
|
|
970
|
+
for (let i = 2; i <= 5; i++) pages.push(i);
|
|
971
|
+
pages.push("...");
|
|
972
|
+
pages.push(totalPages);
|
|
973
|
+
} else if (current >= totalPages - 3) {
|
|
974
|
+
pages.push("...");
|
|
975
|
+
for (let i = totalPages - 4; i <= totalPages; i++) pages.push(i);
|
|
976
|
+
} else {
|
|
977
|
+
pages.push("...");
|
|
978
|
+
for (let i = current - 1; i <= current + 1; i++) pages.push(i);
|
|
979
|
+
pages.push("...");
|
|
980
|
+
pages.push(totalPages);
|
|
981
|
+
}
|
|
982
|
+
return pages;
|
|
983
|
+
}
|
|
984
|
+
function DataTablePagination({ pageSizes = DEFAULT_PAGE_SIZES, className }) {
|
|
985
|
+
const { canNextPage, canPrevPage, nextPage, prevPage, pageIndex, pageCount, setPageIndex, pageSize, setPageSize, totalRows } = useDataTablePagination();
|
|
986
|
+
const isClientMode = pageCount > 0;
|
|
987
|
+
const startRow = pageIndex * pageSize + 1;
|
|
988
|
+
const endRow = Math.min((pageIndex + 1) * pageSize, totalRows);
|
|
989
|
+
const pageNumbers = useMemo(() => getPageNumbers(pageIndex, pageCount), [pageIndex, pageCount]);
|
|
990
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
991
|
+
className: cn("flex flex-col-reverse items-center justify-between gap-4 px-2 py-4 sm:flex-row", className),
|
|
992
|
+
"data-slot": "dt-pagination",
|
|
993
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
994
|
+
className: "flex items-center gap-4",
|
|
995
|
+
children: [isClientMode && totalRows > 0 && /* @__PURE__ */ jsxs("span", {
|
|
996
|
+
className: "text-sm text-muted-foreground whitespace-nowrap",
|
|
997
|
+
children: [
|
|
998
|
+
"Showing",
|
|
999
|
+
" ",
|
|
1000
|
+
startRow,
|
|
1001
|
+
" ",
|
|
1002
|
+
"to",
|
|
1003
|
+
" ",
|
|
1004
|
+
endRow,
|
|
1005
|
+
" ",
|
|
1006
|
+
"of",
|
|
1007
|
+
" ",
|
|
1008
|
+
totalRows,
|
|
1009
|
+
" ",
|
|
1010
|
+
"rows"
|
|
1011
|
+
]
|
|
1012
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1013
|
+
className: "flex items-center gap-2",
|
|
1014
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1015
|
+
className: "text-sm text-muted-foreground whitespace-nowrap",
|
|
1016
|
+
children: "Rows per page"
|
|
1017
|
+
}), /* @__PURE__ */ jsxs(Select, {
|
|
1018
|
+
value: String(pageSize),
|
|
1019
|
+
onValueChange: (value) => setPageSize(Number(value)),
|
|
1020
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
1021
|
+
className: "h-8 w-[70px]",
|
|
1022
|
+
children: /* @__PURE__ */ jsx(SelectValue, { placeholder: String(pageSize) })
|
|
1023
|
+
}), /* @__PURE__ */ jsx(SelectContent, {
|
|
1024
|
+
side: "top",
|
|
1025
|
+
children: pageSizes.map((size) => /* @__PURE__ */ jsx(SelectItem, {
|
|
1026
|
+
value: String(size),
|
|
1027
|
+
children: size
|
|
1028
|
+
}, size))
|
|
1029
|
+
})]
|
|
1030
|
+
})]
|
|
1031
|
+
})]
|
|
1032
|
+
}), /* @__PURE__ */ jsxs("nav", {
|
|
1033
|
+
"aria-label": "Table pagination",
|
|
1034
|
+
className: "flex items-center gap-1",
|
|
1035
|
+
children: [
|
|
1036
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1037
|
+
theme: "outline",
|
|
1038
|
+
size: "icon",
|
|
1039
|
+
className: "size-8",
|
|
1040
|
+
onClick: prevPage,
|
|
1041
|
+
disabled: !canPrevPage,
|
|
1042
|
+
"aria-label": "Previous page",
|
|
1043
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4" })
|
|
1044
|
+
}),
|
|
1045
|
+
isClientMode && pageCount > 1 ? pageNumbers.map((page, index) => {
|
|
1046
|
+
if (page === "...") return /* @__PURE__ */ jsx("span", {
|
|
1047
|
+
className: "px-2 text-sm text-muted-foreground",
|
|
1048
|
+
children: "..."
|
|
1049
|
+
}, `ellipsis-${index}`);
|
|
1050
|
+
const isActive = page === pageIndex + 1;
|
|
1051
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
1052
|
+
theme: isActive ? "solid" : "outline",
|
|
1053
|
+
size: "small",
|
|
1054
|
+
className: cn("h-8 min-w-8 px-2", isActive && "pointer-events-none font-semibold"),
|
|
1055
|
+
onClick: () => setPageIndex(page - 1),
|
|
1056
|
+
"aria-disabled": isActive || void 0,
|
|
1057
|
+
"aria-label": `Page ${page}`,
|
|
1058
|
+
"aria-current": isActive ? "page" : void 0,
|
|
1059
|
+
children: page
|
|
1060
|
+
}, page);
|
|
1061
|
+
}) : !isClientMode && /* @__PURE__ */ jsxs("span", {
|
|
1062
|
+
className: "px-2 text-sm text-muted-foreground",
|
|
1063
|
+
children: [
|
|
1064
|
+
"Page",
|
|
1065
|
+
" ",
|
|
1066
|
+
pageIndex + 1
|
|
1067
|
+
]
|
|
1068
|
+
}),
|
|
1069
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1070
|
+
theme: "outline",
|
|
1071
|
+
size: "icon",
|
|
1072
|
+
className: "size-8",
|
|
1073
|
+
onClick: nextPage,
|
|
1074
|
+
disabled: !canNextPage,
|
|
1075
|
+
"aria-label": "Next page",
|
|
1076
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4" })
|
|
1077
|
+
})
|
|
1078
|
+
]
|
|
1079
|
+
})]
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
//#endregion
|
|
1083
|
+
//#region src/components/features/data-table/components/search.tsx
|
|
1084
|
+
function DataTableSearch({ placeholder = "Search...", debounceMs = 300, className, disabled }) {
|
|
1085
|
+
const { search, setSearch } = useDataTableSearch();
|
|
1086
|
+
const [inputValue, setInputValue] = useState(search);
|
|
1087
|
+
useEffect(() => {
|
|
1088
|
+
setInputValue(search);
|
|
1089
|
+
}, [search]);
|
|
1090
|
+
useEffect(() => {
|
|
1091
|
+
const timer = setTimeout(() => {
|
|
1092
|
+
if (inputValue !== search) setSearch(inputValue);
|
|
1093
|
+
}, debounceMs);
|
|
1094
|
+
return () => clearTimeout(timer);
|
|
1095
|
+
}, [
|
|
1096
|
+
inputValue,
|
|
1097
|
+
debounceMs,
|
|
1098
|
+
search,
|
|
1099
|
+
setSearch
|
|
1100
|
+
]);
|
|
1101
|
+
return /* @__PURE__ */ jsx(Input, {
|
|
1102
|
+
placeholder,
|
|
1103
|
+
value: inputValue,
|
|
1104
|
+
onChange: (e) => setInputValue(e.target.value),
|
|
1105
|
+
className,
|
|
1106
|
+
disabled,
|
|
1107
|
+
"aria-label": placeholder,
|
|
1108
|
+
"data-slot": "dt-search"
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
//#endregion
|
|
1112
|
+
//#region src/components/features/data-table/hooks/use-data-table-client.ts
|
|
1113
|
+
/**
|
|
1114
|
+
* Creates a TanStack Table instance from an existing store.
|
|
1115
|
+
* Does NOT create the store or sync data — the caller is responsible for that.
|
|
1116
|
+
*/
|
|
1117
|
+
function useClientTable(store, options) {
|
|
1118
|
+
const { columns, getRowId, enableRowSelection = false, stateAdapter } = options;
|
|
1119
|
+
const resolvedColumns = useMemo(() => enableRowSelection ? withSelectionColumn(columns, typeof enableRowSelection === "object" ? enableRowSelection : {}) : columns, [columns, enableRowSelection]);
|
|
1120
|
+
const { filteredData, sorting, rowSelection, pageIndex, pageSize: storePageSize, filters, search } = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
1121
|
+
const table = useReactTable({
|
|
1122
|
+
data: filteredData,
|
|
1123
|
+
columns: resolvedColumns,
|
|
1124
|
+
state: {
|
|
1125
|
+
sorting,
|
|
1126
|
+
rowSelection,
|
|
1127
|
+
pagination: {
|
|
1128
|
+
pageIndex,
|
|
1129
|
+
pageSize: storePageSize
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
1132
|
+
onSortingChange: (updater) => {
|
|
1133
|
+
const next = typeof updater === "function" ? updater(sorting) : updater;
|
|
1134
|
+
store.setSorting(next);
|
|
1135
|
+
},
|
|
1136
|
+
onRowSelectionChange: (updater) => {
|
|
1137
|
+
const next = typeof updater === "function" ? updater(rowSelection) : updater;
|
|
1138
|
+
store.setRowSelection(next);
|
|
1139
|
+
},
|
|
1140
|
+
onPaginationChange: (updater) => {
|
|
1141
|
+
const next = typeof updater === "function" ? updater({
|
|
1142
|
+
pageIndex,
|
|
1143
|
+
pageSize: storePageSize
|
|
1144
|
+
}) : updater;
|
|
1145
|
+
store.setPagination(next.pageIndex, next.pageSize);
|
|
1146
|
+
},
|
|
1147
|
+
getCoreRowModel: getCoreRowModel(),
|
|
1148
|
+
getSortedRowModel: getSortedRowModel(),
|
|
1149
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
1150
|
+
getRowId,
|
|
1151
|
+
enableRowSelection: !!enableRowSelection
|
|
1152
|
+
});
|
|
1153
|
+
const hydratedRef = useRef(false);
|
|
1154
|
+
useEffect(() => {
|
|
1155
|
+
if (stateAdapter && !hydratedRef.current) {
|
|
1156
|
+
hydratedRef.current = true;
|
|
1157
|
+
const persisted = stateAdapter.read();
|
|
1158
|
+
if (persisted.sorting && persisted.sorting.length > 0) store.setSorting(persisted.sorting);
|
|
1159
|
+
if (persisted.filters) {
|
|
1160
|
+
for (const [key, value] of Object.entries(persisted.filters)) if (value != null) store.setFilter(key, value);
|
|
1161
|
+
}
|
|
1162
|
+
if (persisted.search) store.setSearch(persisted.search);
|
|
1163
|
+
if (persisted.pageIndex != null && persisted.pageIndex > 0) store.setPageIndex(persisted.pageIndex);
|
|
1164
|
+
if (persisted.pageSize != null) store.setPageSize(persisted.pageSize);
|
|
1165
|
+
}
|
|
1166
|
+
}, []);
|
|
1167
|
+
const isFirstWrite = useRef(true);
|
|
1168
|
+
useEffect(() => {
|
|
1169
|
+
if (!stateAdapter) return;
|
|
1170
|
+
if (isFirstWrite.current) {
|
|
1171
|
+
isFirstWrite.current = false;
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
stateAdapter.write({
|
|
1175
|
+
sorting,
|
|
1176
|
+
filters,
|
|
1177
|
+
search,
|
|
1178
|
+
pageIndex,
|
|
1179
|
+
pageSize: storePageSize
|
|
1180
|
+
});
|
|
1181
|
+
}, [
|
|
1182
|
+
sorting,
|
|
1183
|
+
filters,
|
|
1184
|
+
search,
|
|
1185
|
+
pageIndex,
|
|
1186
|
+
storePageSize,
|
|
1187
|
+
stateAdapter
|
|
1188
|
+
]);
|
|
1189
|
+
return { table };
|
|
1190
|
+
}
|
|
1191
|
+
//#endregion
|
|
1192
|
+
//#region src/components/features/data-table/core/client-provider.tsx
|
|
1193
|
+
/**
|
|
1194
|
+
* Inner component that calls useClientTable.
|
|
1195
|
+
* Only rendered after hydration (gated by tableReady).
|
|
1196
|
+
*/
|
|
1197
|
+
function ClientProviderInner({ store, className, children, ...options }) {
|
|
1198
|
+
const { table } = useClientTable(store, options);
|
|
1199
|
+
return /* @__PURE__ */ jsx(TableInstanceContext, {
|
|
1200
|
+
value: table,
|
|
1201
|
+
children: /* @__PURE__ */ jsx(DataTableRenderKeyContext, {
|
|
1202
|
+
value: store.getSnapshot()._version,
|
|
1203
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1204
|
+
className,
|
|
1205
|
+
children
|
|
1206
|
+
})
|
|
1207
|
+
})
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
function ClientProvider(props) {
|
|
1211
|
+
const { data, columns, loading, pageSize, getRowId, enableRowSelection, defaultSort, defaultFilters, searchableColumns, searchFn, filterFns, stateAdapter, className, children } = props;
|
|
1212
|
+
const store = useMemo(() => createDataTableStore({
|
|
1213
|
+
data,
|
|
1214
|
+
mode: "client",
|
|
1215
|
+
isLoading: true,
|
|
1216
|
+
defaultSort,
|
|
1217
|
+
defaultFilters,
|
|
1218
|
+
pageSize,
|
|
1219
|
+
columnCount: columns.length,
|
|
1220
|
+
searchableColumns,
|
|
1221
|
+
searchFn,
|
|
1222
|
+
filterFns
|
|
1223
|
+
}), []);
|
|
1224
|
+
const isInitialRender = useRef(true);
|
|
1225
|
+
useEffect(() => {
|
|
1226
|
+
if (isInitialRender.current) {
|
|
1227
|
+
isInitialRender.current = false;
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
store.setData(data);
|
|
1231
|
+
}, [data, store]);
|
|
1232
|
+
const isPageSizeInitial = useRef(true);
|
|
1233
|
+
useEffect(() => {
|
|
1234
|
+
if (isPageSizeInitial.current) {
|
|
1235
|
+
isPageSizeInitial.current = false;
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (pageSize != null) store.setPageSize(pageSize);
|
|
1239
|
+
}, [pageSize, store]);
|
|
1240
|
+
const [tableReady, setTableReady] = useState(false);
|
|
1241
|
+
useEffect(() => setTableReady(true), []);
|
|
1242
|
+
useEffect(() => {
|
|
1243
|
+
if (!tableReady) return;
|
|
1244
|
+
store.setLoading(loading ?? false);
|
|
1245
|
+
}, [
|
|
1246
|
+
loading,
|
|
1247
|
+
tableReady,
|
|
1248
|
+
store
|
|
1249
|
+
]);
|
|
1250
|
+
return /* @__PURE__ */ jsx(DataTableStoreContext, {
|
|
1251
|
+
value: store,
|
|
1252
|
+
children: tableReady ? /* @__PURE__ */ jsx(ClientProviderInner, {
|
|
1253
|
+
store,
|
|
1254
|
+
columns,
|
|
1255
|
+
getRowId,
|
|
1256
|
+
enableRowSelection,
|
|
1257
|
+
stateAdapter,
|
|
1258
|
+
className,
|
|
1259
|
+
children
|
|
1260
|
+
}) : /* @__PURE__ */ jsx(TableInstanceContext, {
|
|
1261
|
+
value: null,
|
|
1262
|
+
children: /* @__PURE__ */ jsx(DataTableRenderKeyContext, {
|
|
1263
|
+
value: 0,
|
|
1264
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1265
|
+
className,
|
|
1266
|
+
children
|
|
1267
|
+
})
|
|
1268
|
+
})
|
|
1269
|
+
})
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
//#endregion
|
|
1273
|
+
//#region src/components/features/data-table/hooks/use-data-table-server.ts
|
|
1274
|
+
/**
|
|
1275
|
+
* Creates a TanStack Table instance from an existing store.
|
|
1276
|
+
* Handles fetch-on-query-change, cursor pagination, and state adapter sync.
|
|
1277
|
+
* Does NOT create the store — the caller is responsible for that.
|
|
1278
|
+
*/
|
|
1279
|
+
function useServerTable(store, options) {
|
|
1280
|
+
const { columns, fetchFn, transform, getRowId, enableRowSelection = false, stateAdapter } = options;
|
|
1281
|
+
const fetchRef = useRef(fetchFn);
|
|
1282
|
+
const transformRef = useRef(transform);
|
|
1283
|
+
useEffect(() => {
|
|
1284
|
+
fetchRef.current = fetchFn;
|
|
1285
|
+
}, [fetchFn]);
|
|
1286
|
+
useEffect(() => {
|
|
1287
|
+
transformRef.current = transform;
|
|
1288
|
+
}, [transform]);
|
|
1289
|
+
const cursorMapRef = useRef(/* @__PURE__ */ new Map());
|
|
1290
|
+
const hasNextPageRef = useRef(false);
|
|
1291
|
+
const { sorting, filters, search, rowSelection, pageSize, pageIndex } = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
1292
|
+
const prevQueryRef = useRef({
|
|
1293
|
+
sorting,
|
|
1294
|
+
filters,
|
|
1295
|
+
search,
|
|
1296
|
+
pageSize
|
|
1297
|
+
});
|
|
1298
|
+
useEffect(() => {
|
|
1299
|
+
const prev = prevQueryRef.current;
|
|
1300
|
+
if (prev.sorting !== sorting || prev.filters !== filters || prev.search !== search || prev.pageSize !== pageSize) {
|
|
1301
|
+
cursorMapRef.current = /* @__PURE__ */ new Map();
|
|
1302
|
+
hasNextPageRef.current = false;
|
|
1303
|
+
if (pageIndex !== 0) {
|
|
1304
|
+
prevQueryRef.current = {
|
|
1305
|
+
sorting,
|
|
1306
|
+
filters,
|
|
1307
|
+
search,
|
|
1308
|
+
pageSize
|
|
1309
|
+
};
|
|
1310
|
+
store.setPageIndex(0);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
prevQueryRef.current = {
|
|
1315
|
+
sorting,
|
|
1316
|
+
filters,
|
|
1317
|
+
search,
|
|
1318
|
+
pageSize
|
|
1319
|
+
};
|
|
1320
|
+
let cancelled = false;
|
|
1321
|
+
store.setLoading(true);
|
|
1322
|
+
const cursor = cursorMapRef.current.get(pageIndex);
|
|
1323
|
+
fetchRef.current({
|
|
1324
|
+
sorting,
|
|
1325
|
+
filters,
|
|
1326
|
+
search,
|
|
1327
|
+
cursor,
|
|
1328
|
+
limit: pageSize
|
|
1329
|
+
}).then((response) => {
|
|
1330
|
+
if (cancelled) return;
|
|
1331
|
+
const result = transformRef.current(response);
|
|
1332
|
+
store.setServerData(result.data);
|
|
1333
|
+
store.setError(null);
|
|
1334
|
+
if (result.cursor) cursorMapRef.current.set(pageIndex + 1, result.cursor);
|
|
1335
|
+
hasNextPageRef.current = result.hasNextPage;
|
|
1336
|
+
}).catch((error) => {
|
|
1337
|
+
if (cancelled) return;
|
|
1338
|
+
store.setServerData([]);
|
|
1339
|
+
store.setError(error instanceof Error ? error : new Error(String(error)));
|
|
1340
|
+
hasNextPageRef.current = false;
|
|
1341
|
+
}).finally(() => {
|
|
1342
|
+
if (!cancelled) store.setLoading(false);
|
|
1343
|
+
});
|
|
1344
|
+
return () => {
|
|
1345
|
+
cancelled = true;
|
|
1346
|
+
};
|
|
1347
|
+
}, [
|
|
1348
|
+
sorting,
|
|
1349
|
+
filters,
|
|
1350
|
+
search,
|
|
1351
|
+
pageSize,
|
|
1352
|
+
pageIndex,
|
|
1353
|
+
store
|
|
1354
|
+
]);
|
|
1355
|
+
const resolvedColumns = useMemo(() => enableRowSelection ? withSelectionColumn(columns, typeof enableRowSelection === "object" ? enableRowSelection : {}) : columns, [columns, enableRowSelection]);
|
|
1356
|
+
const table = useReactTable({
|
|
1357
|
+
data: store.getSnapshot().data,
|
|
1358
|
+
columns: resolvedColumns,
|
|
1359
|
+
state: {
|
|
1360
|
+
sorting,
|
|
1361
|
+
rowSelection,
|
|
1362
|
+
pagination: {
|
|
1363
|
+
pageIndex,
|
|
1364
|
+
pageSize
|
|
1365
|
+
}
|
|
1366
|
+
},
|
|
1367
|
+
manualPagination: true,
|
|
1368
|
+
manualSorting: true,
|
|
1369
|
+
manualFiltering: true,
|
|
1370
|
+
pageCount: hasNextPageRef.current ? pageIndex + 2 : pageIndex + 1,
|
|
1371
|
+
getCoreRowModel: getCoreRowModel(),
|
|
1372
|
+
getRowId,
|
|
1373
|
+
enableRowSelection: !!enableRowSelection,
|
|
1374
|
+
onSortingChange: (updater) => {
|
|
1375
|
+
const next = typeof updater === "function" ? updater(sorting) : updater;
|
|
1376
|
+
store.setSorting(next);
|
|
1377
|
+
},
|
|
1378
|
+
onRowSelectionChange: (updater) => {
|
|
1379
|
+
const next = typeof updater === "function" ? updater(rowSelection) : updater;
|
|
1380
|
+
store.setRowSelection(next);
|
|
1381
|
+
},
|
|
1382
|
+
onPaginationChange: (updater) => {
|
|
1383
|
+
const next = typeof updater === "function" ? updater({
|
|
1384
|
+
pageIndex,
|
|
1385
|
+
pageSize
|
|
1386
|
+
}) : updater;
|
|
1387
|
+
store.setPagination(next.pageIndex, next.pageSize);
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
const stateAdapterRef = useRef(stateAdapter);
|
|
1391
|
+
useEffect(() => {
|
|
1392
|
+
stateAdapterRef.current = stateAdapter;
|
|
1393
|
+
}, [stateAdapter]);
|
|
1394
|
+
useEffect(() => {
|
|
1395
|
+
stateAdapterRef.current?.write({
|
|
1396
|
+
sorting,
|
|
1397
|
+
filters,
|
|
1398
|
+
search,
|
|
1399
|
+
pageSize
|
|
1400
|
+
});
|
|
1401
|
+
}, [
|
|
1402
|
+
sorting,
|
|
1403
|
+
filters,
|
|
1404
|
+
search,
|
|
1405
|
+
pageSize
|
|
1406
|
+
]);
|
|
1407
|
+
return { table };
|
|
1408
|
+
}
|
|
1409
|
+
//#endregion
|
|
1410
|
+
//#region src/components/features/data-table/core/server-provider.tsx
|
|
1411
|
+
/**
|
|
1412
|
+
* Inner component that calls useServerTable.
|
|
1413
|
+
* Only rendered after hydration (gated by tableReady).
|
|
1414
|
+
*/
|
|
1415
|
+
function ServerProviderInner({ store, className, children, ...options }) {
|
|
1416
|
+
const { table } = useServerTable(store, options);
|
|
1417
|
+
return /* @__PURE__ */ jsx(TableInstanceContext, {
|
|
1418
|
+
value: table,
|
|
1419
|
+
children: /* @__PURE__ */ jsx(DataTableRenderKeyContext, {
|
|
1420
|
+
value: store.getSnapshot()._version,
|
|
1421
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1422
|
+
className,
|
|
1423
|
+
children
|
|
1424
|
+
})
|
|
1425
|
+
})
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
function ServerProvider(props) {
|
|
1429
|
+
const { columns, fetchFn, transform, limit = 20, getRowId, enableRowSelection, defaultSort, defaultFilters, stateAdapter, className, children } = props;
|
|
1430
|
+
const store = useMemo(() => createDataTableStore({
|
|
1431
|
+
data: [],
|
|
1432
|
+
mode: "server",
|
|
1433
|
+
isLoading: true,
|
|
1434
|
+
defaultSort,
|
|
1435
|
+
defaultFilters,
|
|
1436
|
+
pageSize: limit,
|
|
1437
|
+
columnCount: columns.length
|
|
1438
|
+
}), []);
|
|
1439
|
+
const [tableReady, setTableReady] = useState(false);
|
|
1440
|
+
useEffect(() => setTableReady(true), []);
|
|
1441
|
+
return /* @__PURE__ */ jsx(DataTableStoreContext, {
|
|
1442
|
+
value: store,
|
|
1443
|
+
children: tableReady ? /* @__PURE__ */ jsx(ServerProviderInner, {
|
|
1444
|
+
store,
|
|
1445
|
+
columns,
|
|
1446
|
+
fetchFn,
|
|
1447
|
+
transform,
|
|
1448
|
+
limit,
|
|
1449
|
+
getRowId,
|
|
1450
|
+
enableRowSelection,
|
|
1451
|
+
stateAdapter,
|
|
1452
|
+
className,
|
|
1453
|
+
children
|
|
1454
|
+
}) : /* @__PURE__ */ jsx(TableInstanceContext, {
|
|
1455
|
+
value: null,
|
|
1456
|
+
children: /* @__PURE__ */ jsx(DataTableRenderKeyContext, {
|
|
1457
|
+
value: 0,
|
|
1458
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1459
|
+
className,
|
|
1460
|
+
children
|
|
1461
|
+
})
|
|
1462
|
+
})
|
|
1463
|
+
})
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
//#endregion
|
|
1467
|
+
//#region src/components/features/data-table/filters/checkbox-filter.tsx
|
|
1468
|
+
function CheckboxFilter({ column, label, options, className, disabled, responsive, sheetTitle, modal }) {
|
|
1469
|
+
const { filters, setFilter, clearFilter, registerFilter, unregisterFilter } = useDataTableFilters();
|
|
1470
|
+
useEffect(() => {
|
|
1471
|
+
registerFilter(column, "checkbox");
|
|
1472
|
+
return () => unregisterFilter(column);
|
|
1473
|
+
}, [
|
|
1474
|
+
column,
|
|
1475
|
+
registerFilter,
|
|
1476
|
+
unregisterFilter
|
|
1477
|
+
]);
|
|
1478
|
+
return /* @__PURE__ */ jsx(MultiSelect, {
|
|
1479
|
+
options,
|
|
1480
|
+
value: filters[column] ?? [],
|
|
1481
|
+
onValueChange: (next) => {
|
|
1482
|
+
if (next.length > 0) setFilter(column, next);
|
|
1483
|
+
else clearFilter(column);
|
|
1484
|
+
},
|
|
1485
|
+
placeholder: label,
|
|
1486
|
+
sheetTitle: sheetTitle ?? label,
|
|
1487
|
+
responsive,
|
|
1488
|
+
modalPopover: modal,
|
|
1489
|
+
maxCount: 2,
|
|
1490
|
+
showClearButton: true,
|
|
1491
|
+
showSelectAll: false,
|
|
1492
|
+
disabled,
|
|
1493
|
+
className
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
//#endregion
|
|
1497
|
+
//#region src/components/features/data-table/filters/date-picker-filter.tsx
|
|
1498
|
+
function DatePickerFilter({ column, label, className, datePickerPopoverClassName, disableFuture, disablePast, minDate, maxDate, disabled }) {
|
|
1499
|
+
const { filters, setFilter, clearFilter, registerFilter, unregisterFilter } = useDataTableFilters();
|
|
1500
|
+
const rawValue = filters[column];
|
|
1501
|
+
useEffect(() => {
|
|
1502
|
+
registerFilter(column, "date-gte");
|
|
1503
|
+
return () => unregisterFilter(column);
|
|
1504
|
+
}, [
|
|
1505
|
+
column,
|
|
1506
|
+
registerFilter,
|
|
1507
|
+
unregisterFilter
|
|
1508
|
+
]);
|
|
1509
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1510
|
+
"data-slot": "dt-filter",
|
|
1511
|
+
children: /* @__PURE__ */ jsx(CalendarDatePicker, {
|
|
1512
|
+
date: useMemo(() => {
|
|
1513
|
+
const date = rawValue ? new Date(rawValue) : void 0;
|
|
1514
|
+
return {
|
|
1515
|
+
from: date,
|
|
1516
|
+
to: date
|
|
1517
|
+
};
|
|
1518
|
+
}, [rawValue]),
|
|
1519
|
+
numberOfMonths: 1,
|
|
1520
|
+
closeOnSelect: true,
|
|
1521
|
+
placeholder: label,
|
|
1522
|
+
triggerClassName: cn("h-10", className),
|
|
1523
|
+
variant: "outline",
|
|
1524
|
+
disabled,
|
|
1525
|
+
disableFuture,
|
|
1526
|
+
disablePast,
|
|
1527
|
+
minDate,
|
|
1528
|
+
maxDate,
|
|
1529
|
+
popoverClassName: datePickerPopoverClassName,
|
|
1530
|
+
onDateSelect: (range) => {
|
|
1531
|
+
if (range?.from) setFilter(column, range.from.toISOString());
|
|
1532
|
+
else clearFilter(column);
|
|
1533
|
+
}
|
|
1534
|
+
})
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
//#endregion
|
|
1538
|
+
//#region src/components/features/data-table/filters/select-filter.tsx
|
|
1539
|
+
function SelectFilter({ column, label, options, placeholder, searchable = true, className, selectPopoverClassName, disabled, responsive, sheetTitle, modal }) {
|
|
1540
|
+
const { filters, setFilter, clearFilter, registerFilter, unregisterFilter } = useDataTableFilters();
|
|
1541
|
+
const value = filters[column];
|
|
1542
|
+
useEffect(() => {
|
|
1543
|
+
registerFilter(column, "select");
|
|
1544
|
+
return () => unregisterFilter(column);
|
|
1545
|
+
}, [
|
|
1546
|
+
column,
|
|
1547
|
+
registerFilter,
|
|
1548
|
+
unregisterFilter
|
|
1549
|
+
]);
|
|
1550
|
+
return /* @__PURE__ */ jsx(Autocomplete, {
|
|
1551
|
+
options,
|
|
1552
|
+
value,
|
|
1553
|
+
onValueChange: (next) => {
|
|
1554
|
+
if (next === "" || next === void 0) clearFilter(column);
|
|
1555
|
+
else setFilter(column, next);
|
|
1556
|
+
},
|
|
1557
|
+
placeholder: placeholder ?? label,
|
|
1558
|
+
searchPlaceholder: `Search ${label.toLowerCase()}...`,
|
|
1559
|
+
disableSearch: !searchable,
|
|
1560
|
+
sheetTitle: sheetTitle ?? label,
|
|
1561
|
+
responsive,
|
|
1562
|
+
modal,
|
|
1563
|
+
disabled,
|
|
1564
|
+
className,
|
|
1565
|
+
contentClassName: selectPopoverClassName,
|
|
1566
|
+
triggerClassName: "h-10"
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
//#endregion
|
|
1570
|
+
//#region src/components/features/data-table/data-table.tsx
|
|
1571
|
+
const DataTable = {
|
|
1572
|
+
Client: ClientProvider,
|
|
1573
|
+
Server: ServerProvider,
|
|
1574
|
+
ActiveFilters: DataTableActiveFilters,
|
|
1575
|
+
Content: DataTableContent,
|
|
1576
|
+
InlineContent: DataTableInlineContent,
|
|
1577
|
+
ColumnHeader: DataTableColumnHeader,
|
|
1578
|
+
Pagination: DataTablePagination,
|
|
1579
|
+
Search: DataTableSearch,
|
|
1580
|
+
RowActions: DataTableRowActions,
|
|
1581
|
+
BulkActions: DataTableBulkActions,
|
|
1582
|
+
Loading: DataTableLoading,
|
|
1583
|
+
SelectFilter,
|
|
1584
|
+
CheckboxFilter,
|
|
1585
|
+
DatePickerFilter
|
|
1586
|
+
};
|
|
1587
|
+
//#endregion
|
|
1588
|
+
export { DEFAULT_DEBOUNCE_MS as _, useDataTablePagination as a, DEFAULT_PAGE_SIZES as b, useDataTableSelection as c, resolvePath as d, rowMatchesSearch as f, useNuqsAdapter as g, createSelectionColumn as h, useDataTableLoading as i, useDataTableSorting as l, DataTableColumnHeader as m, useDataTableFilters as n, useDataTableRows as o, DataTableRowActions as p, useDataTableInlineContents as r, useDataTableSearch as s, DataTable as t, createDataTableStore as u, DEFAULT_LOADING_ROWS as v, DEFAULT_PAGE_SIZE as y };
|