@ackplus/react-tanstack-data-table 1.1.16 → 1.1.17

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.
Files changed (31) hide show
  1. package/README.md +0 -8
  2. package/dist/index.d.ts +4 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +5 -1
  5. package/dist/lib/components/data-table-view.d.ts +7 -0
  6. package/dist/lib/components/data-table-view.d.ts.map +1 -0
  7. package/dist/lib/components/data-table-view.js +151 -0
  8. package/dist/lib/components/toolbar/data-table-toolbar.d.ts.map +1 -1
  9. package/dist/lib/components/toolbar/data-table-toolbar.js +14 -1
  10. package/dist/lib/components/toolbar/table-export-control.d.ts.map +1 -1
  11. package/dist/lib/components/toolbar/table-export-control.js +0 -2
  12. package/dist/lib/data-table.d.ts +0 -3
  13. package/dist/lib/data-table.d.ts.map +1 -1
  14. package/dist/lib/data-table.js +9 -1638
  15. package/dist/lib/hooks/index.d.ts +3 -0
  16. package/dist/lib/hooks/index.d.ts.map +1 -0
  17. package/dist/lib/hooks/index.js +5 -0
  18. package/dist/lib/hooks/use-data-table-engine.d.ts +104 -0
  19. package/dist/lib/hooks/use-data-table-engine.d.ts.map +1 -0
  20. package/dist/lib/hooks/use-data-table-engine.js +961 -0
  21. package/dist/lib/types/data-table.types.d.ts +0 -1
  22. package/dist/lib/types/data-table.types.d.ts.map +1 -1
  23. package/package.json +1 -1
  24. package/src/index.ts +4 -0
  25. package/src/lib/components/data-table-view.tsx +386 -0
  26. package/src/lib/components/toolbar/data-table-toolbar.tsx +15 -1
  27. package/src/lib/components/toolbar/table-export-control.tsx +0 -2
  28. package/src/lib/data-table.tsx +17 -2183
  29. package/src/lib/hooks/index.ts +2 -0
  30. package/src/lib/hooks/use-data-table-engine.ts +1282 -0
  31. package/src/lib/types/data-table.types.ts +0 -1
@@ -0,0 +1,1282 @@
1
+ import {
2
+ getCoreRowModel,
3
+ getPaginationRowModel,
4
+ getSortedRowModel,
5
+ PaginationState,
6
+ useReactTable,
7
+ type ColumnOrderState,
8
+ type ColumnPinningState,
9
+ type SortingState,
10
+ type Updater,
11
+ } from "@tanstack/react-table";
12
+ import { useVirtualizer } from "@tanstack/react-virtual";
13
+ import { useTheme } from "@mui/material/styles";
14
+ import { useMemo, useReducer, useState, useRef, useCallback, useEffect, RefObject, CSSProperties } from "react";
15
+
16
+ // your types
17
+ import type {
18
+ ColumnFilterState,
19
+ DataFetchMeta,
20
+ DataRefreshOptions,
21
+ DataTableProps,
22
+ ExportPhase,
23
+ ExportProgressPayload,
24
+ ExportStateChange,
25
+ TableFiltersForFetch,
26
+ TableState,
27
+ } from "../types";
28
+ import type { DataTableApi, DataTableExportApiOptions } from "../types/data-table-api";
29
+
30
+ // your features / utils
31
+ import { ColumnFilterFeature, getCombinedFilteredRowModel } from "../features/column-filter.feature";
32
+ import { SelectionFeature, SelectionState } from "../features";
33
+ import { createExpandingColumn, createSelectionColumn } from "../utils/special-columns.utils";
34
+ import {
35
+ exportClientData,
36
+ exportServerData,
37
+ generateRowId,
38
+ withIdsDeep,
39
+ type DataTableSize,
40
+ } from "../utils";
41
+ import { useDebouncedFetch } from "../utils/debounced-fetch.utils";
42
+
43
+ const DEFAULT_INITIAL_STATE = {
44
+ sorting: [] as SortingState,
45
+ pagination: { pageIndex: 0, pageSize: 10 },
46
+ selectionState: { ids: [], type: "include" } as SelectionState,
47
+ globalFilter: "",
48
+ expanded: {} as Record<string, boolean>,
49
+ columnOrder: [] as ColumnOrderState,
50
+ columnPinning: { left: [], right: [] } as ColumnPinningState,
51
+ columnVisibility: {} as Record<string, boolean>,
52
+ columnSizing: {} as Record<string, number>,
53
+ columnFilter: {
54
+ filters: [],
55
+ logic: "AND",
56
+ pendingFilters: [],
57
+ pendingLogic: "AND",
58
+ } as ColumnFilterState,
59
+ };
60
+
61
+ type EngineUIState = {
62
+ sorting: SortingState;
63
+ pagination: { pageIndex: number; pageSize: number };
64
+ globalFilter: string;
65
+ selectionState: SelectionState;
66
+ columnFilter: ColumnFilterState;
67
+ expanded: Record<string, boolean>;
68
+ tableSize: DataTableSize;
69
+ columnOrder: ColumnOrderState;
70
+ columnPinning: ColumnPinningState;
71
+ columnVisibility: Record<string, boolean>;
72
+ columnSizing: Record<string, number>;
73
+ };
74
+ type EngineAction =
75
+ | { type: "SET_SORTING_RESET_PAGE"; payload: SortingState }
76
+ | { type: "SET_PAGINATION"; payload: { pageIndex: number; pageSize: number } }
77
+ | { type: "SET_GLOBAL_FILTER_RESET_PAGE"; payload: string }
78
+ | { type: "SET_SELECTION"; payload: SelectionState }
79
+ | { type: "SET_COLUMN_FILTER"; payload: ColumnFilterState }
80
+ | { type: "SET_COLUMN_FILTER_RESET_PAGE"; payload: ColumnFilterState }
81
+ | { type: "SET_EXPANDED"; payload: Record<string, boolean> }
82
+ | { type: "SET_TABLE_SIZE"; payload: DataTableSize }
83
+ | { type: "SET_COLUMN_ORDER"; payload: ColumnOrderState }
84
+ | { type: "SET_COLUMN_PINNING"; payload: ColumnPinningState }
85
+ | { type: "SET_COLUMN_VISIBILITY"; payload: Record<string, boolean> }
86
+ | { type: "SET_COLUMN_SIZING"; payload: Record<string, number> }
87
+ | { type: "RESET_ALL"; payload: Partial<EngineUIState> } // payload = computed reset state
88
+ | { type: "RESTORE_LAYOUT"; payload: Partial<EngineUIState> };
89
+
90
+
91
+ function uiReducer(state: EngineUIState, action: EngineAction): EngineUIState {
92
+ switch (action.type) {
93
+ case "SET_SORTING_RESET_PAGE":
94
+ return { ...state, sorting: action.payload, pagination: { pageIndex: 0, pageSize: state.pagination.pageSize } };
95
+ case "SET_PAGINATION":
96
+ return { ...state, pagination: action.payload };
97
+ case "SET_GLOBAL_FILTER_RESET_PAGE":
98
+ return { ...state, globalFilter: action.payload, pagination: { pageIndex: 0, pageSize: state.pagination.pageSize } };
99
+ case "SET_SELECTION":
100
+ return { ...state, selectionState: action.payload };
101
+ case "SET_COLUMN_FILTER":
102
+ return { ...state, columnFilter: action.payload };
103
+ case "SET_COLUMN_FILTER_RESET_PAGE":
104
+ return { ...state, columnFilter: action.payload, pagination: { pageIndex: 0, pageSize: state.pagination.pageSize } };
105
+ case "SET_EXPANDED":
106
+ return { ...state, expanded: action.payload };
107
+ case "SET_TABLE_SIZE":
108
+ return { ...state, tableSize: action.payload };
109
+ case "SET_COLUMN_ORDER":
110
+ return { ...state, columnOrder: action.payload };
111
+ case "SET_COLUMN_PINNING":
112
+ return { ...state, columnPinning: action.payload };
113
+ case "SET_COLUMN_VISIBILITY":
114
+ return { ...state, columnVisibility: action.payload };
115
+ case "SET_COLUMN_SIZING":
116
+ return { ...state, columnSizing: action.payload };
117
+ case "RESTORE_LAYOUT":
118
+ return { ...state, ...action.payload };
119
+ case "RESET_ALL":
120
+ return { ...state, ...action.payload };
121
+ default:
122
+ return state;
123
+ }
124
+ }
125
+
126
+ function useLatestRef<T>(value: T) {
127
+ const ref = useRef(value);
128
+ useEffect(() => {
129
+ ref.current = value;
130
+ }, [value]);
131
+ return ref;
132
+ }
133
+
134
+ function useEvent<T extends (...args: any[]) => any>(fn: T): T {
135
+ const fnRef = useLatestRef(fn);
136
+ return useCallback(((...args: any[]) => fnRef.current(...args)) as T, [fnRef]) as T;
137
+ }
138
+
139
+
140
+ export interface EngineResult<T = any> {
141
+ table: ReturnType<typeof useReactTable<T>>;
142
+ refs: {
143
+ tableContainerRef: RefObject<HTMLDivElement>;
144
+ apiRef: RefObject<DataTableApi<T> | null>;
145
+ exportControllerRef: RefObject<AbortController | null>;
146
+ };
147
+ derived: {
148
+ isServerMode: boolean;
149
+ isServerPagination: boolean;
150
+ isServerFiltering: boolean;
151
+ isServerSorting: boolean;
152
+ tableData: T[];
153
+ tableTotalRow: number;
154
+ tableLoading: boolean;
155
+ rows: ReturnType<ReturnType<typeof useReactTable<T>>["getRowModel"]>["rows"];
156
+ visibleLeafColumns: ReturnType<typeof useReactTable<T>>["getVisibleLeafColumns"];
157
+ useFixedLayout: boolean;
158
+ tableStyle: CSSProperties;
159
+ isExporting: boolean;
160
+ exportPhase: ExportPhase | null;
161
+ exportProgress: ExportProgressPayload;
162
+ isSomeRowsSelected: boolean;
163
+ selectedRowCount: number;
164
+ };
165
+ state: EngineUIState;
166
+ actions: {
167
+ fetchData: (overrides?: Partial<TableState>, options?: { delay?: number; meta?: DataFetchMeta }) => Promise<any>;
168
+ handleSortingChange: (updaterOrValue: any) => void;
169
+ handlePaginationChange: (updater: any) => void;
170
+ handleGlobalFilterChange: (updaterOrValue: any) => void;
171
+ handleColumnFilterChangeHandler: (updater: any, isApply?: boolean) => void;
172
+ handleColumnOrderChange: (updatedColumnOrder: Updater<ColumnOrderState>) => void;
173
+ handleColumnPinningChange: (updater: Updater<ColumnPinningState>) => void;
174
+ handleColumnVisibilityChange: (updater: any) => void;
175
+ handleColumnSizingChange: (updater: any) => void;
176
+ handleColumnReorder: (draggedColumnId: string, targetColumnId: string) => void;
177
+ resetAllAndReload: () => void;
178
+ triggerRefresh: (options?: boolean | DataRefreshOptions, fallbackReason?: string) => Promise<void>;
179
+ setTableSize: (size: DataTableSize) => void;
180
+ handleCancelExport: () => void;
181
+ renderRowModel: { rowVirtualizer: ReturnType<typeof useVirtualizer> };
182
+ };
183
+ api: DataTableApi<T>;
184
+ providerProps: {
185
+ table: ReturnType<typeof useReactTable<T>>;
186
+ apiRef: RefObject<DataTableApi<T> | null>;
187
+ dataMode: "client" | "server";
188
+ tableSize: DataTableSize;
189
+ onTableSizeChange: (size: DataTableSize) => void;
190
+ columnFilter: ColumnFilterState;
191
+ onChangeColumnFilter: (filter: ColumnFilterState) => void;
192
+ slots: Record<string, any>;
193
+ slotProps: Record<string, any>;
194
+ isExporting: boolean;
195
+ exportController: AbortController | null;
196
+ exportPhase: ExportPhase | null;
197
+ exportProgress: ExportProgressPayload;
198
+ onCancelExport: () => void;
199
+ exportFilename: string;
200
+ onExportProgress?: (progress: ExportProgressPayload) => void;
201
+ onExportComplete?: (result: { success: boolean; filename: string; totalRows: number }) => void;
202
+ onExportError?: (error: { message: string; code: string }) => void;
203
+ onServerExport?: (filters?: Partial<any>, selection?: SelectionState, signal?: AbortSignal) => Promise<any>;
204
+ };
205
+ }
206
+
207
+ export function useDataTableEngine<T extends Record<string, any>>(
208
+ props: DataTableProps<T>
209
+ ): EngineResult<T> {
210
+ const {
211
+ initialState,
212
+ columns,
213
+ data = [],
214
+ totalRow = 0,
215
+ idKey = "id" as keyof T,
216
+
217
+ dataMode = "client",
218
+ initialLoadData = true,
219
+ onFetchData,
220
+ onFetchStateChange,
221
+ onDataStateChange,
222
+
223
+ enableRowSelection = false,
224
+ enableMultiRowSelection = true,
225
+ selectMode = "page",
226
+ isRowSelectable,
227
+ onSelectionChange,
228
+ enableBulkActions = false,
229
+
230
+ enableColumnResizing = false,
231
+ columnResizeMode = "onChange",
232
+ onColumnSizingChange,
233
+
234
+ enableColumnDragging = false,
235
+ onColumnDragEnd,
236
+
237
+ enableColumnPinning = false,
238
+ onColumnPinningChange,
239
+
240
+ onColumnVisibilityChange,
241
+ enableColumnVisibility = true,
242
+
243
+ enableExpanding = false,
244
+ getRowCanExpand,
245
+
246
+ enablePagination = false,
247
+ paginationMode = "client",
248
+
249
+ enableGlobalFilter = true,
250
+
251
+ enableColumnFilter = false,
252
+ filterMode = "client",
253
+
254
+ enableSorting = true,
255
+ sortingMode = "client",
256
+ onSortingChange,
257
+
258
+ exportFilename = "export",
259
+ exportConcurrency = "cancelAndRestart",
260
+ exportChunkSize = 1000,
261
+ exportStrictTotalCheck = false,
262
+ exportSanitizeCSV = true,
263
+ onExportProgress,
264
+ onExportComplete,
265
+ onExportError,
266
+ onServerExport,
267
+ onExportCancel,
268
+ onExportStateChange,
269
+
270
+ fitToScreen = true,
271
+ tableSize: initialTableSize = "medium",
272
+ enableVirtualization = false,
273
+ estimateRowHeight = 52,
274
+
275
+ loading = false,
276
+
277
+ onColumnFiltersChange,
278
+ onPaginationChange,
279
+ onGlobalFilterChange,
280
+
281
+ slots = {},
282
+ slotProps = {},
283
+ } = props;
284
+
285
+ const theme = useTheme();
286
+
287
+ const isServerMode = dataMode === "server";
288
+ const isServerPagination = paginationMode === "server" || isServerMode;
289
+ const isServerFiltering = filterMode === "server" || isServerMode;
290
+ const isServerSorting = sortingMode === "server" || isServerMode;
291
+
292
+ // --- initial config (memo)
293
+ const initialStateConfig = useMemo(() => {
294
+ const config = { ...DEFAULT_INITIAL_STATE, ...initialState };
295
+ return config;
296
+ }, [initialState]);
297
+
298
+ const initialUIState: EngineUIState = useMemo(
299
+ () => ({
300
+ sorting: initialStateConfig.sorting ?? DEFAULT_INITIAL_STATE.sorting,
301
+ pagination: initialStateConfig.pagination ?? DEFAULT_INITIAL_STATE.pagination,
302
+ globalFilter: initialStateConfig.globalFilter ?? DEFAULT_INITIAL_STATE.globalFilter,
303
+ selectionState: initialStateConfig.selectionState ?? DEFAULT_INITIAL_STATE.selectionState,
304
+ columnFilter: initialStateConfig.columnFilter ?? DEFAULT_INITIAL_STATE.columnFilter,
305
+ expanded: initialStateConfig.expanded ?? {},
306
+ tableSize: (initialTableSize || "medium") as DataTableSize,
307
+ columnOrder: initialStateConfig.columnOrder ?? DEFAULT_INITIAL_STATE.columnOrder,
308
+ columnPinning: initialStateConfig.columnPinning ?? DEFAULT_INITIAL_STATE.columnPinning,
309
+ columnVisibility: initialStateConfig.columnVisibility ?? DEFAULT_INITIAL_STATE.columnVisibility,
310
+ columnSizing: initialStateConfig.columnSizing ?? DEFAULT_INITIAL_STATE.columnSizing,
311
+ }),
312
+ [initialStateConfig, initialTableSize]
313
+ );
314
+
315
+ // --- UI state (reducer)
316
+ const [ui, dispatch] = useReducer(uiReducer, initialUIState);
317
+
318
+ // --- server data state (UI-affecting)
319
+ const [serverData, setServerData] = useState<T[] | null>(null);
320
+ const [serverTotal, setServerTotal] = useState<number>(0);
321
+
322
+ // --- export UI state
323
+ const [exportPhase, setExportPhase] = useState<ExportPhase | null>(null);
324
+ const [exportProgress, setExportProgress] = useState<ExportProgressPayload>({});
325
+ const [exportController, setExportController] = useState<AbortController | null>(null);
326
+ const [queuedExportCount, setQueuedExportCount] = useState(0);
327
+
328
+ // --- refs (no-render control)
329
+ const tableContainerRef = useRef<HTMLDivElement>(null);
330
+ const apiRef = useRef<DataTableApi<T> | null>(null);
331
+ const exportControllerRef = useRef<AbortController | null>(null);
332
+ const exportQueueRef = useRef<Promise<void>>(Promise.resolve());
333
+ const lastSentRef = useRef<string>("");
334
+
335
+ // --- latest refs (prevent stale closures in stable API)
336
+ const uiRef = useLatestRef(ui);
337
+ const dataRef = useLatestRef(data);;
338
+ const serverDataRef = useLatestRef(serverData);
339
+ const nextFetchDelayRef = useRef<number>(0);
340
+
341
+ // callbacks refs (super important)
342
+ const onFetchDataRef = useLatestRef(onFetchData);
343
+ const onFetchStateChangeRef = useLatestRef(onFetchStateChange);
344
+ const onDataStateChangeRef = useLatestRef(onDataStateChange);
345
+
346
+ const onSortingChangeRef = useLatestRef(onSortingChange);
347
+ const onPaginationChangeRef = useLatestRef(onPaginationChange);
348
+ const onGlobalFilterChangeRef = useLatestRef(onGlobalFilterChange);
349
+ const onColumnFiltersChangeRef = useLatestRef(onColumnFiltersChange);
350
+
351
+ const onColumnDragEndRef = useLatestRef(onColumnDragEnd);
352
+ const onColumnPinningChangeRef = useLatestRef(onColumnPinningChange);
353
+ const onColumnVisibilityChangeRef = useLatestRef(onColumnVisibilityChange);
354
+ const onColumnSizingChangeRef = useLatestRef(onColumnSizingChange);
355
+ const onSelectionChangeRef = useLatestRef(onSelectionChange);
356
+
357
+ const onExportProgressRef = useLatestRef(onExportProgress);
358
+ const onExportCompleteRef = useLatestRef(onExportComplete);
359
+ const onExportErrorRef = useLatestRef(onExportError);
360
+ const onExportCancelRef = useLatestRef(onExportCancel);
361
+ const onExportStateChangeRef = useLatestRef(onExportStateChange);
362
+ const onServerExportRef = useLatestRef(onServerExport);
363
+
364
+
365
+ // --- debounced fetch helper (can stay as-is)
366
+ const fetchHandler = useEvent((filters: any, opts: any) => onFetchDataRef.current?.(filters, opts));
367
+ const { debouncedFetch, isLoading: fetchLoading } = useDebouncedFetch(fetchHandler);
368
+
369
+ const tableData = useMemo(() => {
370
+ return serverData !== null ? serverData : data;
371
+ }, [serverData, data]);
372
+
373
+ const tableTotalRow = useMemo(() => {
374
+ return serverData !== null ? serverTotal : totalRow || data.length;
375
+ }, [serverData, serverTotal, totalRow, data]);
376
+
377
+ const tableLoading = useMemo(() => {
378
+ return onFetchData ? loading || fetchLoading : loading;
379
+ }, [onFetchData, loading, fetchLoading]);
380
+
381
+
382
+ // --- columns enhancement
383
+ const enhancedColumns = useMemo(() => {
384
+ let cols = [...columns];
385
+ if (enableExpanding) {
386
+ cols = [
387
+ createExpandingColumn<T>({
388
+ ...(slotProps?.expandColumn && typeof slotProps.expandColumn === "object"
389
+ ? slotProps.expandColumn
390
+ : {}),
391
+ }),
392
+ ...cols,
393
+ ];
394
+ }
395
+ if (enableRowSelection) {
396
+ cols = [
397
+ createSelectionColumn<T>({
398
+ ...(slotProps?.selectionColumn && typeof slotProps.selectionColumn === "object"
399
+ ? slotProps.selectionColumn
400
+ : {}),
401
+ multiSelect: enableMultiRowSelection,
402
+ }),
403
+ ...cols,
404
+ ];
405
+ }
406
+ return withIdsDeep(cols);
407
+ }, [
408
+ columns,
409
+ enableExpanding,
410
+ enableRowSelection,
411
+ enableMultiRowSelection,
412
+ slotProps?.expandColumn,
413
+ slotProps?.selectionColumn,
414
+ ]);
415
+
416
+ // --- fetchData: useEvent so it's stable but reads latest state refs
417
+ const fetchData = useEvent(async (overrides: Partial<TableState> = {}, options?: { delay?: number; meta?: DataFetchMeta }) => {
418
+ const s = uiRef.current;
419
+
420
+ const filters: Partial<TableFiltersForFetch> = {
421
+ globalFilter: s.globalFilter,
422
+ pagination: s.pagination,
423
+ columnFilter: s.columnFilter,
424
+ sorting: s.sorting,
425
+ ...overrides,
426
+ };
427
+
428
+ onFetchStateChangeRef.current?.(filters, options?.meta);
429
+
430
+ const handler = onFetchDataRef.current;
431
+ if (!handler) return;
432
+
433
+ const delay = options?.delay ?? 0;
434
+ const result = await debouncedFetch(filters, { debounceDelay: delay, meta: options?.meta });
435
+
436
+ if (result && Array.isArray(result.data) && result.total !== undefined) {
437
+ setServerData(result.data);
438
+ setServerTotal(result.total);
439
+ }
440
+
441
+ return result;
442
+ });
443
+
444
+ // --- derived selection counts
445
+ const isSomeRowsSelected = useMemo(() => {
446
+ if (!enableBulkActions || !enableRowSelection) return false;
447
+ if (ui.selectionState.type === "exclude") return ui.selectionState.ids.length < tableTotalRow;
448
+ return ui.selectionState.ids.length > 0;
449
+ }, [enableBulkActions, enableRowSelection, ui.selectionState, tableTotalRow]);
450
+
451
+ const selectedRowCount = useMemo(() => {
452
+ if (!enableBulkActions || !enableRowSelection) return 0;
453
+ if (ui.selectionState.type === "exclude") return tableTotalRow - ui.selectionState.ids.length;
454
+ return ui.selectionState.ids.length;
455
+ }, [enableBulkActions, enableRowSelection, ui.selectionState, tableTotalRow]);
456
+
457
+
458
+
459
+ // --- TanStack Table
460
+ const table = useReactTable({
461
+ _features: [ColumnFilterFeature, SelectionFeature],
462
+ data: tableData,
463
+ columns: enhancedColumns,
464
+ initialState: initialStateConfig,
465
+ state: {
466
+ ...(enableSorting ? { sorting: ui.sorting } : {}),
467
+ ...(enablePagination ? { pagination: ui.pagination } : {}),
468
+ ...(enableGlobalFilter ? { globalFilter: ui.globalFilter } : {}),
469
+ ...(enableExpanding ? { expanded: ui.expanded } : {}),
470
+ ...(enableColumnDragging ? { columnOrder: ui.columnOrder } : {}),
471
+ ...(enableColumnPinning ? { columnPinning: ui.columnPinning } : {}),
472
+ ...(enableColumnVisibility ? { columnVisibility: ui.columnVisibility } : {}),
473
+ ...(enableColumnResizing ? { columnSizing: ui.columnSizing } : {}),
474
+ ...(enableColumnFilter ? { columnFilter: ui.columnFilter } : {}),
475
+ ...(enableRowSelection ? { selectionState: ui.selectionState } : {}),
476
+ },
477
+
478
+ selectMode,
479
+ enableAdvanceSelection: !!enableRowSelection,
480
+ isRowSelectable: isRowSelectable as any,
481
+
482
+ ...(enableRowSelection
483
+ ? {
484
+ onSelectionStateChange: (updaterOrValue: any) => {
485
+ dispatch({
486
+ type: "SET_SELECTION",
487
+ payload:
488
+ typeof updaterOrValue === "function"
489
+ ? updaterOrValue(uiRef.current.selectionState)
490
+ : updaterOrValue,
491
+ });
492
+ },
493
+ }
494
+ : {}),
495
+
496
+ enableAdvanceColumnFilter: enableColumnFilter,
497
+ onColumnFilterChange: (updater: any) => {
498
+ const next = typeof updater === "function" ? updater(uiRef.current.columnFilter) : updater;
499
+ dispatch({ type: "SET_COLUMN_FILTER", payload: next });
500
+ },
501
+ onColumnFilterApply: (state: ColumnFilterState) => {
502
+ dispatch({ type: "SET_COLUMN_FILTER_RESET_PAGE", payload: state });
503
+ },
504
+
505
+ ...(enableSorting
506
+ ? {
507
+ onSortingChange: (updaterOrValue: any) => {
508
+ const prev = uiRef.current.sorting;
509
+ const next = typeof updaterOrValue === "function" ? updaterOrValue(prev) : updaterOrValue;
510
+ const cleaned = (next || []).filter((s: any) => s?.id);
511
+ onSortingChangeRef.current?.(cleaned);
512
+ dispatch({ type: "SET_SORTING_RESET_PAGE", payload: cleaned });
513
+ },
514
+ }
515
+ : {}),
516
+
517
+ ...(enablePagination
518
+ ? {
519
+ onPaginationChange: (updater: any) => {
520
+ const prev = uiRef.current.pagination;
521
+ const next = typeof updater === "function" ? updater(prev) : updater;
522
+ onPaginationChangeRef.current?.(next);
523
+ dispatch({ type: "SET_PAGINATION", payload: next });
524
+ },
525
+ }
526
+ : {}),
527
+
528
+ ...(enableGlobalFilter
529
+ ? {
530
+ onGlobalFilterChange: (updaterOrValue: any) => {
531
+ const prev = uiRef.current.globalFilter;
532
+ const next = typeof updaterOrValue === "function" ? updaterOrValue(prev) : updaterOrValue;
533
+ onGlobalFilterChangeRef.current?.(next);
534
+ nextFetchDelayRef.current = 400;
535
+ dispatch({ type: "SET_GLOBAL_FILTER_RESET_PAGE", payload: next });
536
+ },
537
+ }
538
+ : {}),
539
+
540
+ ...(enableExpanding ? { onExpandedChange: (u: any) => dispatch({ type: "SET_EXPANDED", payload: typeof u === "function" ? u(uiRef.current.expanded) : u }) } : {}),
541
+ ...(enableColumnDragging ? { onColumnOrderChange: (u: any) => dispatch({ type: "SET_COLUMN_ORDER", payload: typeof u === "function" ? u(uiRef.current.columnOrder) : u }) } : {}),
542
+ ...(enableColumnPinning ? { onColumnPinningChange: (u: any) => dispatch({ type: "SET_COLUMN_PINNING", payload: typeof u === "function" ? u(uiRef.current.columnPinning) : u }) } : {}),
543
+ ...(enableColumnVisibility ? { onColumnVisibilityChange: (u: any) => dispatch({ type: "SET_COLUMN_VISIBILITY", payload: typeof u === "function" ? u(uiRef.current.columnVisibility) : u }) } : {}),
544
+ ...(enableColumnResizing ? { onColumnSizingChange: (u: any) => dispatch({ type: "SET_COLUMN_SIZING", payload: typeof u === "function" ? u(uiRef.current.columnSizing) : u }) } : {}),
545
+
546
+ getCoreRowModel: getCoreRowModel(),
547
+ ...(enableSorting ? { getSortedRowModel: getSortedRowModel() } : {}),
548
+ ...(enableColumnFilter || enableGlobalFilter ? { getFilteredRowModel: getCombinedFilteredRowModel<T>() } : {}),
549
+ ...(enablePagination && !isServerPagination ? { getPaginationRowModel: getPaginationRowModel() } : {}),
550
+
551
+ enableSorting,
552
+ manualSorting: isServerSorting,
553
+ manualFiltering: isServerFiltering,
554
+
555
+ enableColumnResizing,
556
+ columnResizeMode,
557
+ columnResizeDirection: theme.direction,
558
+
559
+ enableColumnPinning,
560
+ ...(enableExpanding ? { getRowCanExpand: getRowCanExpand as any } : {}),
561
+
562
+ manualPagination: isServerPagination,
563
+ autoResetPageIndex: false,
564
+
565
+ rowCount: enablePagination ? (tableTotalRow ?? tableData.length) : tableData.length,
566
+ getRowId: (row: any, index: number) => generateRowId(row, index, idKey),
567
+ });
568
+
569
+ // --- layout sizing
570
+ const allLeafColumns = table.getAllLeafColumns();
571
+ const hasExplicitSizing = allLeafColumns.some((col) => {
572
+ const { size, minSize, maxSize } = col.columnDef;
573
+ return size !== undefined || minSize !== undefined || maxSize !== undefined;
574
+ });
575
+ const useFixedLayout = fitToScreen || enableColumnResizing || hasExplicitSizing;
576
+ const tableTotalSize = table.getTotalSize();
577
+ const tableWidth = fitToScreen ? "100%" : useFixedLayout ? tableTotalSize : "100%";
578
+
579
+ const tableStyle: CSSProperties = {
580
+ width: tableWidth,
581
+ minWidth: fitToScreen ? tableTotalSize : undefined,
582
+ tableLayout: useFixedLayout ? "fixed" : "auto",
583
+ };
584
+
585
+ // --- virtualization
586
+ const rows = table.getRowModel().rows;
587
+ const rowVirtualizer = useVirtualizer({
588
+ count: rows.length,
589
+ getScrollElement: () => tableContainerRef.current,
590
+ estimateSize: () => estimateRowHeight,
591
+ overscan: 10,
592
+ enabled: enableVirtualization && !enablePagination && rows.length > 0,
593
+ });
594
+
595
+
596
+ const serverKey = useMemo(() => {
597
+ if (!(isServerMode || isServerPagination || isServerFiltering || isServerSorting)) return null;
598
+
599
+ return JSON.stringify({
600
+ sorting: ui.sorting,
601
+ pagination: ui.pagination,
602
+ globalFilter: ui.globalFilter,
603
+ columnFilter: { filters: ui.columnFilter.filters, logic: ui.columnFilter.logic }, // only applied
604
+ });
605
+ }, [isServerMode, isServerPagination, isServerFiltering, isServerSorting, ui.sorting, ui.pagination, ui.globalFilter, ui.columnFilter]);
606
+ const serverKeyRef = useLatestRef(serverKey);
607
+ const lastServerKeyRef = useRef<string | null>(null);
608
+
609
+ // --- initial fetch
610
+ useEffect(() => {
611
+ if (!initialLoadData) return;
612
+ // If we're in server mode, mark current serverKey as already handled
613
+ // so the serverKey effect doesn't immediately fetch again.
614
+ if (serverKeyRef.current) {
615
+ lastServerKeyRef.current = serverKeyRef.current;
616
+ }
617
+ if (onFetchData || onFetchStateChange) {
618
+ void fetchData({}, { delay: 0, meta: { reason: "initial" } });
619
+ }
620
+ // eslint-disable-next-line react-hooks/exhaustive-deps
621
+ }, []);
622
+
623
+ useEffect(() => {
624
+ if (!serverKey) return;
625
+ if (serverKey === lastServerKeyRef.current) return;
626
+ lastServerKeyRef.current = serverKey;
627
+
628
+ const delay = nextFetchDelayRef.current ?? 0;
629
+ nextFetchDelayRef.current = 0; // reset after using
630
+
631
+ void fetchData({}, { delay, meta: { reason: "stateChange" } });
632
+ }, [serverKey, fetchData]);
633
+
634
+ // columnFilter apply handler stays explicit (button), but you can also auto-fetch on change if needed
635
+ const handleColumnFilterChangeHandler = useCallback(
636
+ (updater: any, isApply = false) => {
637
+ const prev = uiRef.current.columnFilter;
638
+ const next = typeof updater === "function" ? updater(prev) : updater;
639
+ if (isApply) {
640
+ nextFetchDelayRef.current = 0;
641
+ dispatch({ type: "SET_COLUMN_FILTER_RESET_PAGE", payload: next });
642
+ } else {
643
+ dispatch({ type: "SET_COLUMN_FILTER", payload: next });
644
+ }
645
+ onColumnFiltersChangeRef.current?.(next, isApply);
646
+ },
647
+ [onColumnFiltersChangeRef, uiRef]
648
+ );
649
+
650
+ // --- emit table state (dedupe)
651
+ useEffect(() => {
652
+ const cb = onDataStateChangeRef.current;
653
+ if (!cb) return;
654
+
655
+ const live = table.getState();
656
+ const payload = {
657
+ sorting: live.sorting,
658
+ pagination: live.pagination,
659
+ globalFilter: live.globalFilter,
660
+ columnFilter: live.columnFilter,
661
+ columnVisibility: live.columnVisibility,
662
+ columnSizing: live.columnSizing,
663
+ columnOrder: live.columnOrder,
664
+ columnPinning: live.columnPinning,
665
+ };
666
+ const key = JSON.stringify(payload);
667
+ if (key === lastSentRef.current) return;
668
+ lastSentRef.current = key;
669
+ cb(payload);
670
+ }, [table, ui.sorting, ui.pagination, ui.globalFilter, ui.columnFilter, ui.columnVisibility, ui.columnSizing, ui.columnOrder, ui.columnPinning, onDataStateChangeRef]);
671
+
672
+ // --- helpers
673
+ const resetPageToFirst = useCallback(() => {
674
+ return { pageIndex: 0, pageSize: uiRef.current.pagination.pageSize };
675
+ }, [uiRef]);
676
+
677
+ const normalizeRefreshOptions = useCallback(
678
+ (options?: boolean | DataRefreshOptions, fallbackReason: string = "refresh") => {
679
+ if (typeof options === "boolean") return { resetPagination: options, force: false, reason: fallbackReason };
680
+ return {
681
+ resetPagination: options?.resetPagination ?? false,
682
+ force: options?.force ?? false,
683
+ reason: options?.reason ?? fallbackReason,
684
+ };
685
+ },
686
+ []
687
+ );
688
+
689
+ const triggerRefresh = useCallback(
690
+ async (options?: boolean | DataRefreshOptions, fallbackReason: string = "refresh") => {
691
+ const n = normalizeRefreshOptions(options, fallbackReason);
692
+ const current = uiRef.current.pagination;
693
+ const nextPagination = enablePagination
694
+ ? { pageIndex: n.resetPagination ? 0 : current.pageIndex, pageSize: current.pageSize }
695
+ : undefined;
696
+
697
+ if (nextPagination) {
698
+ nextFetchDelayRef.current = 0;
699
+ dispatch({ type: "SET_PAGINATION", payload: nextPagination });
700
+ onPaginationChangeRef.current?.(nextPagination);
701
+ }
702
+ const paginationChanged = !!nextPagination &&
703
+ (nextPagination.pageIndex !== current.pageIndex || nextPagination.pageSize !== current.pageSize);
704
+
705
+ if (!paginationChanged) {
706
+ await fetchData({}, { delay: 0, meta: { reason: n.reason, force: n.force } });
707
+ }
708
+ },
709
+ [enablePagination, fetchData, normalizeRefreshOptions, onPaginationChangeRef, uiRef]
710
+ );
711
+
712
+ const getResetPayload = useCallback((): Partial<EngineUIState> => {
713
+ const resetSorting = initialStateConfig.sorting || [];
714
+ const resetGlobalFilter = initialStateConfig.globalFilter ?? "";
715
+ const resetColumnFilter = (initialStateConfig.columnFilter ?? DEFAULT_INITIAL_STATE.columnFilter) as ColumnFilterState;
716
+
717
+ const resetPagination = enablePagination
718
+ ? (initialStateConfig.pagination || { pageIndex: 0, pageSize: 10 })
719
+ : uiRef.current.pagination;
720
+
721
+ return {
722
+ sorting: resetSorting,
723
+ globalFilter: resetGlobalFilter,
724
+ columnFilter: resetColumnFilter,
725
+ ...(enablePagination ? { pagination: resetPagination } : {}),
726
+ selectionState: initialStateConfig.selectionState ?? DEFAULT_INITIAL_STATE.selectionState,
727
+ expanded: {},
728
+ columnVisibility: initialStateConfig.columnVisibility || {},
729
+ columnSizing: initialStateConfig.columnSizing || {},
730
+ columnOrder: initialStateConfig.columnOrder || [],
731
+ columnPinning: initialStateConfig.columnPinning || { left: [], right: [] },
732
+ };
733
+ }, [enablePagination, initialStateConfig]);
734
+
735
+ const resetAllAndReload = useCallback(() => {
736
+ const payload = getResetPayload();
737
+ dispatch({ type: "RESET_ALL", payload });
738
+ void fetchData(
739
+ {
740
+ sorting: payload.sorting as any,
741
+ globalFilter: payload.globalFilter as any,
742
+ columnFilter: payload.columnFilter as any,
743
+ ...(enablePagination ? { pagination: payload.pagination as any } : {}),
744
+ },
745
+ { delay: 0, meta: { reason: "reset", force: true } }
746
+ );
747
+ }, [enablePagination, fetchData, getResetPayload]);
748
+
749
+ // --- export (refs + small UI state)
750
+ const setExportControllerSafely = useCallback(
751
+ (value: AbortController | null | ((current: AbortController | null) => AbortController | null)) => {
752
+ setExportController((current) => {
753
+ const next = typeof value === "function" ? (value as any)(current) : value;
754
+ exportControllerRef.current = next;
755
+ return next;
756
+ });
757
+ },
758
+ []
759
+ );
760
+
761
+ const handleExportProgressInternal = useCallback((p: ExportProgressPayload) => {
762
+ setExportProgress(p || {});
763
+ onExportProgressRef.current?.(p);
764
+ }, []);
765
+
766
+ const handleExportStateChangeInternal = useCallback((s: ExportStateChange) => {
767
+ setExportPhase(s.phase);
768
+ if (s.processedRows !== undefined || s.totalRows !== undefined || s.percentage !== undefined) {
769
+ setExportProgress({
770
+ processedRows: s.processedRows,
771
+ totalRows: s.totalRows,
772
+ percentage: s.percentage,
773
+ });
774
+ }
775
+ onExportStateChangeRef.current?.(s);
776
+ }, []);
777
+
778
+ const runExportWithPolicy = useCallback(
779
+ async (options: { format: "csv" | "excel"; filename: string; mode: "client" | "server"; execute: (controller: AbortController) => Promise<void> }) => {
780
+ const { format, filename, mode, execute } = options;
781
+
782
+ const startExecution = async () => {
783
+ const controller = new AbortController();
784
+ setExportProgress({});
785
+ setExportControllerSafely(controller);
786
+ try {
787
+ await execute(controller);
788
+ } finally {
789
+ setExportControllerSafely((cur) => (cur === controller ? null : cur));
790
+ }
791
+ };
792
+
793
+ if (exportConcurrency === "queue") {
794
+ setQueuedExportCount((p) => p + 1);
795
+ const runQueued = async () => {
796
+ setQueuedExportCount((p) => Math.max(0, p - 1));
797
+ await startExecution();
798
+ };
799
+ const queuedPromise = exportQueueRef.current.catch(() => undefined).then(runQueued);
800
+ exportQueueRef.current = queuedPromise;
801
+ return queuedPromise;
802
+ }
803
+
804
+ const active = exportControllerRef.current;
805
+ if (active) {
806
+ if (exportConcurrency === "ignoreIfRunning") {
807
+ handleExportStateChangeInternal({
808
+ phase: "error",
809
+ mode,
810
+ format,
811
+ filename,
812
+ message: "An export is already running",
813
+ code: "EXPORT_IN_PROGRESS",
814
+ endedAt: Date.now(),
815
+ } as any);
816
+ onExportErrorRef.current?.({ message: "An export is already running", code: "EXPORT_IN_PROGRESS" });
817
+ return;
818
+ }
819
+ if (exportConcurrency === "cancelAndRestart") active.abort();
820
+ }
821
+
822
+ await startExecution();
823
+ },
824
+ [exportConcurrency, handleExportStateChangeInternal, onExportErrorRef, setExportControllerSafely]
825
+ );
826
+
827
+ const handleCancelExport = useCallback(() => {
828
+ const active = exportControllerRef.current;
829
+ if (!active) return;
830
+ active.abort();
831
+ setExportControllerSafely((cur) => (cur === active ? null : cur));
832
+ onExportCancelRef.current?.();
833
+ }, [onExportCancelRef, setExportControllerSafely]);
834
+
835
+ const isExporting = exportController !== null;
836
+
837
+ // --- stable API (created once)
838
+ if (!apiRef.current) {
839
+ apiRef.current = {} as DataTableApi<T>;
840
+
841
+ // IMPORTANT: do NOT capture `table/ui/data` here. Always read from refs inside methods.
842
+ (apiRef.current as any).table = { getTable: () => table }; // will be updated below via tableRef
843
+ // We'll overwrite getTable below with a ref-backed function.
844
+ }
845
+
846
+ // table ref so API always returns latest table instance
847
+ const tableRef = useLatestRef(table);
848
+
849
+
850
+ useEffect(() => {
851
+ const api = apiRef.current!;
852
+ api.table = { getTable: () => tableRef.current } as any;
853
+
854
+ // --- state getters
855
+ api.state = {
856
+ getTableState: () => tableRef.current.getState(),
857
+ getCurrentFilters: () => tableRef.current.getState().columnFilter,
858
+ getCurrentSorting: () => tableRef.current.getState().sorting,
859
+ getCurrentPagination: () => tableRef.current.getState().pagination,
860
+ getCurrentSelection: () => uiRef.current.selectionState,
861
+ getGlobalFilter: () => tableRef.current.getState().globalFilter,
862
+ };
863
+
864
+ // --- data
865
+ api.data = {
866
+ refresh: (options?: boolean | DataRefreshOptions) => void triggerRefresh(options, "refresh"),
867
+ reload: (options: DataRefreshOptions = {}) => void triggerRefresh({ ...options, reason: options.reason ?? "reload" }, "reload"),
868
+ resetAll: () => resetAllAndReload(),
869
+
870
+ getAllData: () => {
871
+ const sData = serverDataRef.current;
872
+ const base = sData !== null ? sData : dataRef.current;
873
+ return [...base];
874
+ },
875
+ getDataCount: () => {
876
+ const sData = serverDataRef.current;
877
+ const base = sData !== null ? sData : dataRef.current;
878
+ return base.length;
879
+ },
880
+ getFilteredDataCount: () => tableRef.current.getFilteredRowModel().rows.length,
881
+ } as any;
882
+
883
+ // --- sorting/pagination/filtering - dispatch + callbacks + server fetch policies
884
+ api.sorting = {
885
+ setSorting: (next: SortingState) => {
886
+ const cleaned = (next || []).filter((s: any) => s?.id);
887
+ onSortingChangeRef.current?.(cleaned);
888
+ nextFetchDelayRef.current = 0;
889
+ dispatch({ type: "SET_SORTING_RESET_PAGE", payload: cleaned });
890
+ },
891
+ clearSorting: () => {
892
+ onSortingChangeRef.current?.([]);
893
+ nextFetchDelayRef.current = 0;
894
+ dispatch({ type: "SET_SORTING_RESET_PAGE", payload: [] });
895
+ },
896
+ resetSorting: () => {
897
+ const next = (initialStateConfig.sorting || []) as SortingState;
898
+ onSortingChangeRef.current?.(next);
899
+ nextFetchDelayRef.current = 0;
900
+ dispatch({ type: "SET_SORTING_RESET_PAGE", payload: next });
901
+ },
902
+ } as any;
903
+
904
+ api.pagination = {
905
+ goToPage: (pageIndex: number) => {
906
+ const prev = uiRef.current.pagination;
907
+ const next = { ...prev, pageIndex };
908
+ onPaginationChangeRef.current?.(next);
909
+ nextFetchDelayRef.current = 0;
910
+ dispatch({ type: "SET_PAGINATION", payload: next });
911
+ },
912
+ setPageSize: (pageSize: number) => {
913
+ const next = { pageIndex: 0, pageSize };
914
+ onPaginationChangeRef.current?.(next);
915
+ nextFetchDelayRef.current = 0;
916
+ dispatch({ type: "SET_PAGINATION", payload: next });
917
+ },
918
+ resetPagination: () => {
919
+ const next = (initialStateConfig.pagination || { pageIndex: 0, pageSize: 10 }) as any;
920
+ onPaginationChangeRef.current?.(next);
921
+ nextFetchDelayRef.current = 0;
922
+ dispatch({ type: "SET_PAGINATION", payload: next });
923
+ },
924
+ } as any;
925
+
926
+ api.filtering = {
927
+ setGlobalFilter: (filter: string) => {
928
+ onGlobalFilterChangeRef.current?.(filter);
929
+ nextFetchDelayRef.current = 400;
930
+ dispatch({ type: "SET_GLOBAL_FILTER_RESET_PAGE", payload: filter });
931
+ },
932
+ clearGlobalFilter: () => {
933
+ onGlobalFilterChangeRef.current?.("");
934
+ nextFetchDelayRef.current = 400;
935
+ dispatch({ type: "SET_GLOBAL_FILTER_RESET_PAGE", payload: "" });
936
+ },
937
+ setColumnFilters: (filters: ColumnFilterState, isApply = false) => handleColumnFilterChangeHandler(filters, isApply),
938
+ } as any;
939
+
940
+ api.columnVisibility = {
941
+ showColumn: (id: string) => dispatch({ type: "SET_COLUMN_VISIBILITY", payload: { ...uiRef.current.columnVisibility, [id]: true } }),
942
+ hideColumn: (id: string) => dispatch({ type: "SET_COLUMN_VISIBILITY", payload: { ...uiRef.current.columnVisibility, [id]: false } }),
943
+ resetColumnVisibility: () => dispatch({ type: "SET_COLUMN_VISIBILITY", payload: initialStateConfig.columnVisibility || {} }),
944
+ } as any;
945
+
946
+ api.columnOrdering = {
947
+ setColumnOrder: (next: ColumnOrderState) => {
948
+ dispatch({ type: "SET_COLUMN_ORDER", payload: next });
949
+ onColumnDragEndRef.current?.(next);
950
+ },
951
+ resetColumnOrder: () => dispatch({ type: "SET_COLUMN_ORDER", payload: initialStateConfig.columnOrder || [] }),
952
+ } as any;
953
+
954
+ api.columnPinning = {
955
+ setPinning: (next: ColumnPinningState) => {
956
+ dispatch({ type: "SET_COLUMN_PINNING", payload: next });
957
+ onColumnPinningChangeRef.current?.(next);
958
+ },
959
+ resetColumnPinning: () => dispatch({ type: "SET_COLUMN_PINNING", payload: initialStateConfig.columnPinning || { left: [], right: [] } }),
960
+ } as any;
961
+
962
+ api.columnResizing = {
963
+ resetColumnSizing: () => dispatch({ type: "SET_COLUMN_SIZING", payload: initialStateConfig.columnSizing || {} }),
964
+ } as any;
965
+
966
+ api.selection = {
967
+ getSelectionState: () => tableRef.current.getSelectionState?.() || ({ ids: [], type: "include" } as const),
968
+ getSelectedRows: () => tableRef.current.getSelectedRows(),
969
+ getSelectedCount: () => tableRef.current.getSelectedCount(),
970
+ isRowSelected: (rowId: string) => tableRef.current.getIsRowSelected(rowId) || false,
971
+ // keep using your table extension methods if you have them:
972
+ selectAll: () => tableRef.current.selectAll?.(),
973
+ deselectAll: () => tableRef.current.deselectAll?.(),
974
+ } as any;
975
+
976
+ // --- export API (use your existing exportClientData/exportServerData)
977
+ api.export = {
978
+ exportCSV: async (options: DataTableExportApiOptions = {}) => {
979
+ const fn = options.filename ?? exportFilename;
980
+ const chunkSize = options.chunkSize ?? exportChunkSize;
981
+ const strictTotalCheck = options.strictTotalCheck ?? exportStrictTotalCheck;
982
+ const sanitizeCSV = options.sanitizeCSV ?? exportSanitizeCSV;
983
+
984
+ const mode: "client" | "server" = dataMode === "server" && !!onServerExportRef.current ? "server" : "client";
985
+
986
+ await runExportWithPolicy({
987
+ format: "csv",
988
+ filename: fn,
989
+ mode,
990
+ execute: async (controller) => {
991
+ // TODO: keep your state-change event mapping (starting/progress/completed/cancel/error)
992
+ if (mode === "server" && onServerExportRef.current) {
993
+ await exportServerData(tableRef.current, {
994
+ format: "csv",
995
+ filename: fn,
996
+ fetchData: (filters: any, selection: any, signal?: AbortSignal) =>
997
+ onServerExportRef.current?.(filters, selection, signal),
998
+ currentFilters: {
999
+ globalFilter: tableRef.current.getState().globalFilter,
1000
+ columnFilter: tableRef.current.getState().columnFilter,
1001
+ sorting: tableRef.current.getState().sorting,
1002
+ pagination: tableRef.current.getState().pagination,
1003
+ },
1004
+ selection: tableRef.current.getSelectionState?.(),
1005
+ onProgress: handleExportProgressInternal,
1006
+ onComplete: onExportCompleteRef.current,
1007
+ onError: onExportErrorRef.current,
1008
+ onStateChange: (s: any) => handleExportStateChangeInternal({ ...s, mode, format: "csv", filename: fn, queueLength: queuedExportCount } as any),
1009
+ signal: controller.signal,
1010
+ chunkSize,
1011
+ strictTotalCheck,
1012
+ sanitizeCSV,
1013
+ });
1014
+ return;
1015
+ }
1016
+
1017
+ await exportClientData(tableRef.current, {
1018
+ format: "csv",
1019
+ filename: fn,
1020
+ onProgress: handleExportProgressInternal,
1021
+ onComplete: onExportCompleteRef.current,
1022
+ onError: onExportErrorRef.current,
1023
+ onStateChange: (s: any) => handleExportStateChangeInternal({ ...s, mode, format: "csv", filename: fn, queueLength: queuedExportCount } as any),
1024
+ signal: controller.signal,
1025
+ sanitizeCSV,
1026
+ });
1027
+ },
1028
+ });
1029
+ },
1030
+
1031
+ exportExcel: async (options: DataTableExportApiOptions = {}) => {
1032
+ const fn = options.filename ?? exportFilename;
1033
+ const chunkSize = options.chunkSize ?? exportChunkSize;
1034
+ const strictTotalCheck = options.strictTotalCheck ?? exportStrictTotalCheck;
1035
+ const sanitizeCSV = options.sanitizeCSV ?? exportSanitizeCSV;
1036
+
1037
+ const mode: "client" | "server" = dataMode === "server" && !!onServerExportRef.current ? "server" : "client";
1038
+
1039
+ await runExportWithPolicy({
1040
+ format: "excel",
1041
+ filename: fn,
1042
+ mode,
1043
+ execute: async (controller) => {
1044
+ // TODO: keep your state-change event mapping (starting/progress/completed/cancel/error)
1045
+ if (mode === "server" && onServerExportRef.current) {
1046
+ await exportServerData(tableRef.current, {
1047
+ format: "excel",
1048
+ filename: fn,
1049
+ fetchData: (filters: any, selection: any, signal?: AbortSignal) =>
1050
+ onServerExportRef.current?.(filters, selection, signal),
1051
+ currentFilters: {
1052
+ globalFilter: tableRef.current.getState().globalFilter,
1053
+ columnFilter: tableRef.current.getState().columnFilter,
1054
+ sorting: tableRef.current.getState().sorting,
1055
+ pagination: tableRef.current.getState().pagination,
1056
+ },
1057
+ selection: tableRef.current.getSelectionState?.(),
1058
+ onProgress: handleExportProgressInternal,
1059
+ onComplete: onExportCompleteRef.current,
1060
+ onError: onExportErrorRef.current,
1061
+ onStateChange: (s: any) => handleExportStateChangeInternal({ ...s, mode, format: "csv", filename: fn, queueLength: queuedExportCount } as any),
1062
+ signal: controller.signal,
1063
+ chunkSize,
1064
+ strictTotalCheck,
1065
+ sanitizeCSV,
1066
+ });
1067
+ return;
1068
+ }
1069
+
1070
+ await exportClientData(tableRef.current, {
1071
+ format: "excel",
1072
+ filename: fn,
1073
+ onProgress: handleExportProgressInternal,
1074
+ onComplete: onExportCompleteRef.current,
1075
+ onError: onExportErrorRef.current,
1076
+ onStateChange: (s: any) => handleExportStateChangeInternal({ ...s, mode, format: "csv", filename: fn, queueLength: queuedExportCount } as any),
1077
+ signal: controller.signal,
1078
+ sanitizeCSV,
1079
+ });
1080
+ },
1081
+ });
1082
+ },
1083
+
1084
+ isExporting: () => exportControllerRef.current != null,
1085
+ cancelExport: () => handleCancelExport(),
1086
+ } as any;
1087
+ }, [dataMode, exportChunkSize, exportFilename, exportSanitizeCSV, exportStrictTotalCheck, fetchData, handleCancelExport, handleColumnFilterChangeHandler, handleExportProgressInternal, handleExportStateChangeInternal, initialStateConfig, isServerMode, isServerPagination, isServerSorting, resetAllAndReload, resetPageToFirst, runExportWithPolicy, triggerRefresh, queuedExportCount, tableRef, uiRef, serverDataRef, dataRef, onSortingChangeRef, onPaginationChangeRef, onGlobalFilterChangeRef, onColumnDragEndRef, onColumnPinningChangeRef, onServerExportRef, onExportCompleteRef, onExportErrorRef]);
1088
+
1089
+ // --- imperative handlers (used by TanStack callbacks above or view)
1090
+ const handleSortingChange = useCallback(
1091
+ (updaterOrValue: Updater<SortingState>) => {
1092
+ const prev = uiRef.current.sorting;
1093
+ const next = typeof updaterOrValue === "function" ? updaterOrValue(prev) : updaterOrValue;
1094
+ const cleaned = (next || []).filter((s: any) => s?.id);
1095
+ onSortingChangeRef.current?.(cleaned);
1096
+ nextFetchDelayRef.current = 0;
1097
+ dispatch({ type: "SET_SORTING_RESET_PAGE", payload: cleaned });
1098
+ },
1099
+ [onSortingChangeRef, uiRef]
1100
+ );
1101
+
1102
+ const handlePaginationChange = useCallback(
1103
+ (updater: Updater<PaginationState>) => {
1104
+ const prev = uiRef.current.pagination;
1105
+ const next = typeof updater === "function" ? updater(prev) : updater;
1106
+ onPaginationChangeRef.current?.(next);
1107
+ nextFetchDelayRef.current = 0;
1108
+ dispatch({ type: "SET_PAGINATION", payload: next });
1109
+ },
1110
+ [onPaginationChangeRef, uiRef]
1111
+ );
1112
+
1113
+ const handleGlobalFilterChange = useCallback(
1114
+ (updaterOrValue: Updater<string>) => {
1115
+ const prev = uiRef.current.globalFilter;
1116
+ const next = typeof updaterOrValue === "function" ? updaterOrValue(prev) : updaterOrValue;
1117
+ onGlobalFilterChangeRef.current?.(next);
1118
+ nextFetchDelayRef.current = 400;
1119
+ dispatch({ type: "SET_GLOBAL_FILTER_RESET_PAGE", payload: next });
1120
+ },
1121
+ [onGlobalFilterChangeRef, uiRef]
1122
+ );
1123
+
1124
+ const handleColumnOrderChange = useCallback(
1125
+ (updated: Updater<ColumnOrderState>) => {
1126
+ const prev = uiRef.current.columnOrder;
1127
+ const next = typeof updated === "function" ? updated(prev) : updated;
1128
+ dispatch({ type: "SET_COLUMN_ORDER", payload: next });
1129
+ onColumnDragEndRef.current?.(next);
1130
+ },
1131
+ [onColumnDragEndRef, uiRef]
1132
+ );
1133
+
1134
+ const handleColumnPinningChange = useCallback(
1135
+ (updated: Updater<ColumnPinningState>) => {
1136
+ const prev = uiRef.current.columnPinning;
1137
+ const next = typeof updated === "function" ? updated(prev) : updated;
1138
+ dispatch({ type: "SET_COLUMN_PINNING", payload: next });
1139
+ onColumnPinningChangeRef.current?.(next);
1140
+ },
1141
+ [onColumnPinningChangeRef, uiRef]
1142
+ );
1143
+
1144
+ const handleColumnVisibilityChange = useCallback(
1145
+ (updated: any) => {
1146
+ const prev = uiRef.current.columnVisibility;
1147
+ const next = typeof updated === "function" ? updated(prev) : updated;
1148
+ dispatch({ type: "SET_COLUMN_VISIBILITY", payload: next });
1149
+ onColumnVisibilityChangeRef.current?.(next);
1150
+ },
1151
+ [onColumnVisibilityChangeRef, uiRef]
1152
+ );
1153
+
1154
+ const handleColumnSizingChange = useCallback(
1155
+ (updated: any) => {
1156
+ const prev = uiRef.current.columnSizing;
1157
+ const next = typeof updated === "function" ? updated(prev) : updated;
1158
+ dispatch({ type: "SET_COLUMN_SIZING", payload: next });
1159
+ onColumnSizingChangeRef.current?.(next);
1160
+ },
1161
+ [onColumnSizingChangeRef, uiRef]
1162
+ );
1163
+
1164
+ const handleColumnReorder = useCallback(
1165
+ (draggedId: string, targetId: string) => {
1166
+ const currentOrder =
1167
+ uiRef.current.columnOrder.length > 0
1168
+ ? uiRef.current.columnOrder
1169
+ : enhancedColumns.map((c: any, idx: number) => c.id ?? c.accessorKey ?? `column_${idx}`);
1170
+
1171
+ const from = currentOrder.indexOf(draggedId);
1172
+ const to = currentOrder.indexOf(targetId);
1173
+ if (from === -1 || to === -1) return;
1174
+
1175
+ const next = [...currentOrder];
1176
+ next.splice(from, 1);
1177
+ next.splice(to, 0, draggedId);
1178
+
1179
+ handleColumnOrderChange(next);
1180
+ },
1181
+ [enhancedColumns, handleColumnOrderChange, uiRef]
1182
+ );
1183
+
1184
+ // optional: selection callback
1185
+ useEffect(() => {
1186
+ onSelectionChangeRef.current?.(ui.selectionState);
1187
+ }, [onSelectionChangeRef, ui.selectionState]);
1188
+
1189
+
1190
+
1191
+ // --- provider props
1192
+ const providerProps = useMemo(
1193
+ () => ({
1194
+ table,
1195
+ apiRef,
1196
+ dataMode,
1197
+ tableSize: ui.tableSize,
1198
+ onTableSizeChange: (size: DataTableSize) => dispatch({ type: "SET_TABLE_SIZE", payload: size }),
1199
+ columnFilter: ui.columnFilter,
1200
+ onChangeColumnFilter: (f: ColumnFilterState) => handleColumnFilterChangeHandler(f, false),
1201
+ slots,
1202
+ slotProps,
1203
+ isExporting,
1204
+ exportController,
1205
+ exportPhase,
1206
+ exportProgress,
1207
+ onCancelExport: handleCancelExport,
1208
+ exportFilename,
1209
+ onExportProgress,
1210
+ onExportComplete,
1211
+ onExportError,
1212
+ onServerExport,
1213
+ }),
1214
+ [
1215
+ table,
1216
+ dataMode,
1217
+ ui.tableSize,
1218
+ ui.columnFilter,
1219
+ slots,
1220
+ slotProps,
1221
+ isExporting,
1222
+ exportController,
1223
+ exportPhase,
1224
+ exportProgress,
1225
+ handleCancelExport,
1226
+ exportFilename,
1227
+ onExportProgress,
1228
+ onExportComplete,
1229
+ onExportError,
1230
+ onServerExport,
1231
+ handleColumnFilterChangeHandler,
1232
+ ]
1233
+ );
1234
+
1235
+
1236
+ return {
1237
+ table,
1238
+ refs: {
1239
+ tableContainerRef,
1240
+ apiRef,
1241
+ exportControllerRef,
1242
+ },
1243
+ derived: {
1244
+ isServerMode,
1245
+ isServerPagination,
1246
+ isServerFiltering,
1247
+ isServerSorting,
1248
+ tableData,
1249
+ tableTotalRow,
1250
+ tableLoading,
1251
+ rows,
1252
+ visibleLeafColumns: table.getVisibleLeafColumns,
1253
+ useFixedLayout,
1254
+ tableStyle,
1255
+ isExporting,
1256
+ exportPhase,
1257
+ exportProgress,
1258
+ isSomeRowsSelected,
1259
+ selectedRowCount,
1260
+ },
1261
+ state: ui,
1262
+ actions: {
1263
+ fetchData,
1264
+ handleSortingChange,
1265
+ handlePaginationChange,
1266
+ handleGlobalFilterChange,
1267
+ handleColumnFilterChangeHandler,
1268
+ handleColumnOrderChange,
1269
+ handleColumnPinningChange,
1270
+ handleColumnVisibilityChange,
1271
+ handleColumnSizingChange,
1272
+ handleColumnReorder,
1273
+ resetAllAndReload,
1274
+ triggerRefresh,
1275
+ setTableSize: (size: DataTableSize) => dispatch({ type: "SET_TABLE_SIZE", payload: size }),
1276
+ handleCancelExport,
1277
+ renderRowModel: { rowVirtualizer },
1278
+ },
1279
+ api: apiRef.current!,
1280
+ providerProps,
1281
+ };
1282
+ }