@ackplus/react-tanstack-data-table 1.1.11 → 1.1.13

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 (61) hide show
  1. package/README.md +143 -11
  2. package/dist/lib/components/droupdown/menu-dropdown.d.ts.map +1 -1
  3. package/dist/lib/components/droupdown/menu-dropdown.js +8 -1
  4. package/dist/lib/components/filters/filter-value-input.js +2 -2
  5. package/dist/lib/components/pagination/data-table-pagination.d.ts.map +1 -1
  6. package/dist/lib/components/pagination/data-table-pagination.js +10 -1
  7. package/dist/lib/components/toolbar/data-table-toolbar.d.ts.map +1 -1
  8. package/dist/lib/components/toolbar/data-table-toolbar.js +5 -2
  9. package/dist/lib/components/toolbar/table-export-control.d.ts.map +1 -1
  10. package/dist/lib/components/toolbar/table-export-control.js +46 -12
  11. package/dist/lib/components/toolbar/table-refresh-control.d.ts +15 -0
  12. package/dist/lib/components/toolbar/table-refresh-control.d.ts.map +1 -0
  13. package/dist/lib/components/toolbar/table-refresh-control.js +61 -0
  14. package/dist/lib/contexts/data-table-context.d.ts +7 -10
  15. package/dist/lib/contexts/data-table-context.d.ts.map +1 -1
  16. package/dist/lib/contexts/data-table-context.js +5 -1
  17. package/dist/lib/data-table.d.ts.map +1 -1
  18. package/dist/lib/data-table.js +1110 -946
  19. package/dist/lib/features/column-filter.feature.js +38 -21
  20. package/dist/lib/features/selection.feature.d.ts.map +1 -1
  21. package/dist/lib/features/selection.feature.js +11 -3
  22. package/dist/lib/types/column.types.d.ts +19 -0
  23. package/dist/lib/types/column.types.d.ts.map +1 -1
  24. package/dist/lib/types/data-table-api.d.ts +25 -18
  25. package/dist/lib/types/data-table-api.d.ts.map +1 -1
  26. package/dist/lib/types/data-table.types.d.ts +37 -10
  27. package/dist/lib/types/data-table.types.d.ts.map +1 -1
  28. package/dist/lib/types/export.types.d.ts +57 -13
  29. package/dist/lib/types/export.types.d.ts.map +1 -1
  30. package/dist/lib/types/slots.types.d.ts +12 -1
  31. package/dist/lib/types/slots.types.d.ts.map +1 -1
  32. package/dist/lib/types/table.types.d.ts +1 -3
  33. package/dist/lib/types/table.types.d.ts.map +1 -1
  34. package/dist/lib/utils/debounced-fetch.utils.d.ts +8 -4
  35. package/dist/lib/utils/debounced-fetch.utils.d.ts.map +1 -1
  36. package/dist/lib/utils/debounced-fetch.utils.js +63 -14
  37. package/dist/lib/utils/export-utils.d.ts +14 -4
  38. package/dist/lib/utils/export-utils.d.ts.map +1 -1
  39. package/dist/lib/utils/export-utils.js +362 -66
  40. package/dist/lib/utils/slot-helpers.d.ts +1 -1
  41. package/dist/lib/utils/slot-helpers.d.ts.map +1 -1
  42. package/package.json +4 -2
  43. package/src/lib/components/droupdown/menu-dropdown.tsx +9 -3
  44. package/src/lib/components/filters/filter-value-input.tsx +2 -2
  45. package/src/lib/components/pagination/data-table-pagination.tsx +14 -2
  46. package/src/lib/components/toolbar/data-table-toolbar.tsx +15 -1
  47. package/src/lib/components/toolbar/table-export-control.tsx +65 -9
  48. package/src/lib/components/toolbar/table-refresh-control.tsx +58 -0
  49. package/src/lib/contexts/data-table-context.tsx +16 -2
  50. package/src/lib/data-table.tsx +1282 -932
  51. package/src/lib/features/column-filter.feature.ts +40 -19
  52. package/src/lib/features/selection.feature.ts +11 -5
  53. package/src/lib/types/column.types.ts +20 -1
  54. package/src/lib/types/data-table-api.ts +37 -15
  55. package/src/lib/types/data-table.types.ts +59 -3
  56. package/src/lib/types/export.types.ts +79 -10
  57. package/src/lib/types/slots.types.ts +11 -1
  58. package/src/lib/types/table.types.ts +1 -3
  59. package/src/lib/utils/debounced-fetch.utils.ts +90 -18
  60. package/src/lib/utils/export-utils.ts +496 -69
  61. package/src/lib/utils/slot-helpers.tsx +1 -1
@@ -41,9 +41,16 @@ import { TableHeader } from './components/headers';
41
41
  import { DataTablePagination } from './components/pagination';
42
42
  import { DataTableRow, LoadingRows, EmptyDataRow } from './components/rows';
43
43
  import { DataTableToolbar, BulkActionsToolbar } from './components/toolbar';
44
- import { DataTableProps } from './types/data-table.types';
45
- import { ColumnFilterState, TableFiltersForFetch, TableState } from './types';
46
- import { DataTableApi } from './types/data-table-api';
44
+ import {
45
+ DataFetchMeta,
46
+ DataMutationAction,
47
+ DataMutationContext,
48
+ DataRefreshContext,
49
+ DataRefreshOptions,
50
+ DataTableProps,
51
+ } from './types/data-table.types';
52
+ import { ColumnFilterState, ExportPhase, ExportProgressPayload, ExportStateChange, TableFiltersForFetch, TableState } from './types';
53
+ import { DataTableApi, DataTableExportApiOptions } from './types/data-table-api';
47
54
  import {
48
55
  createExpandingColumn,
49
56
  createSelectionColumn,
@@ -92,6 +99,8 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
92
99
  dataMode = 'client',
93
100
  initialLoadData = true,
94
101
  onFetchData,
102
+ onRefreshData,
103
+ onDataChange,
95
104
  onDataStateChange,
96
105
 
97
106
  // Selection props
@@ -145,11 +154,16 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
145
154
  sortingMode = 'client',
146
155
  onSortingChange,
147
156
  exportFilename = 'export',
157
+ exportConcurrency = 'cancelAndRestart',
158
+ exportChunkSize = 1000,
159
+ exportStrictTotalCheck = false,
160
+ exportSanitizeCSV = true,
148
161
  onExportProgress,
149
162
  onExportComplete,
150
163
  onExportError,
151
164
  onServerExport,
152
165
  onExportCancel,
166
+ onExportStateChange,
153
167
 
154
168
  // Styling props
155
169
  enableHover = true,
@@ -170,6 +184,7 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
170
184
  enableTableSizeControl = true,
171
185
  enableExport = false,
172
186
  enableReset = true,
187
+ enableRefresh = false,
173
188
 
174
189
  // Loading and empty states
175
190
  loading = false,
@@ -251,16 +266,32 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
251
266
  const [serverData, setServerData] = useState<T[] | null>(null);
252
267
  const [serverTotal, setServerTotal] = useState(0);
253
268
  const [exportController, setExportController] = useState<AbortController | null>(null);
269
+ const [exportProgress, setExportProgress] = useState<ExportProgressPayload>({});
270
+ const [exportPhase, setExportPhase] = useState<ExportPhase | null>(null);
271
+ const [queuedExportCount, setQueuedExportCount] = useState(0);
254
272
 
255
273
  // -------------------------------
256
274
  // Ref hooks (grouped together)
257
275
  // -------------------------------
258
276
  const tableContainerRef = useRef<HTMLDivElement>(null);
259
277
  const internalApiRef = useRef<DataTableApi<T>>(null);
278
+ const exportControllerRef = useRef<AbortController | null>(null);
279
+ const exportQueueRef = useRef<Promise<void>>(Promise.resolve());
280
+
281
+ const isExternallyControlledData = useMemo(
282
+ () => !onFetchData && (!!onDataChange || !!onRefreshData),
283
+ [onFetchData, onDataChange, onRefreshData]
284
+ );
260
285
 
261
286
  const { debouncedFetch, isLoading: fetchLoading } = useDebouncedFetch(onFetchData);
262
- const tableData = useMemo(() => serverData ? serverData : data, [serverData, data]);
263
- const tableTotalRow = useMemo(() => serverData ? serverTotal : totalRow || data.length, [serverData, serverTotal, totalRow, data]);
287
+ const tableData = useMemo(() => {
288
+ if (isExternallyControlledData) return data;
289
+ return serverData !== null ? serverData : data;
290
+ }, [isExternallyControlledData, serverData, data]);
291
+ const tableTotalRow = useMemo(
292
+ () => (isExternallyControlledData ? (totalRow || data.length) : (serverData !== null ? serverTotal : totalRow || data.length)),
293
+ [isExternallyControlledData, serverData, serverTotal, totalRow, data]
294
+ );
264
295
  const tableLoading = useMemo(() => onFetchData ? (loading || fetchLoading) : loading, [onFetchData, loading, fetchLoading]);
265
296
 
266
297
 
@@ -308,7 +339,10 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
308
339
  // -------------------------------
309
340
  // Callback hooks (grouped together)
310
341
  // -------------------------------
311
- const fetchData = useCallback(async (overrides: Partial<TableState> = {}, options?: { delay?: number }) => {
342
+ const fetchData = useCallback(async (
343
+ overrides: Partial<TableState> = {},
344
+ options?: { delay?: number; meta?: DataFetchMeta }
345
+ ) => {
312
346
  if (!onFetchData) {
313
347
  if (logger.isLevelEnabled('debug')) {
314
348
  logger.debug('onFetchData not provided, skipping fetch', { overrides, columnFilter, sorting, pagination });
@@ -316,7 +350,7 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
316
350
  return;
317
351
  }
318
352
 
319
- const filters: TableFiltersForFetch = {
353
+ const filters: Partial<TableFiltersForFetch> = {
320
354
  globalFilter,
321
355
  pagination,
322
356
  columnFilter,
@@ -325,11 +359,19 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
325
359
  };
326
360
 
327
361
  if (logger.isLevelEnabled('info')) {
328
- logger.info('Requesting data', { filters });
362
+ logger.info('Requesting data', {
363
+ filters,
364
+ reason: options?.meta?.reason,
365
+ force: options?.meta?.force,
366
+ });
329
367
  }
330
368
 
331
369
  try {
332
- const result = await debouncedFetch(filters, options === undefined ? 0 : options.delay || 300);
370
+ const delay = options?.delay ?? 300; // respects 0
371
+ const result = await debouncedFetch(filters, {
372
+ debounceDelay: delay,
373
+ meta: options?.meta,
374
+ });
333
375
 
334
376
  if (logger.isLevelEnabled('info')) {
335
377
  logger.info('Fetch resolved', {
@@ -338,7 +380,7 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
338
380
  });
339
381
  }
340
382
 
341
- if (result?.data && result?.total !== undefined) {
383
+ if (result && Array.isArray(result.data) && result.total !== undefined) {
342
384
  setServerData(result.data);
343
385
  setServerTotal(result.total);
344
386
  } else if (logger.isLevelEnabled('warn')) {
@@ -360,76 +402,42 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
360
402
  logger,
361
403
  ]);
362
404
 
363
-
364
- const tableStateChange = useCallback((overrides: Partial<TableState> = {}) => {
365
- if (!onDataStateChange) {
366
- if (logger.isLevelEnabled('debug')) {
367
- logger.debug('No onDataStateChange handler registered; skipping state update notification', { overrides });
368
- }
369
- return;
405
+ const normalizeRefreshOptions = useCallback((
406
+ options?: boolean | DataRefreshOptions,
407
+ fallbackReason: string = 'refresh'
408
+ ) => {
409
+ if (typeof options === 'boolean') {
410
+ return {
411
+ resetPagination: options,
412
+ force: false,
413
+ reason: fallbackReason,
414
+ };
370
415
  }
371
416
 
372
- const currentState: Partial<TableState> = {
373
- globalFilter,
374
- columnFilter,
375
- sorting,
376
- pagination,
377
- columnOrder,
378
- columnPinning,
379
- columnVisibility,
380
- columnSizing,
381
- ...overrides,
417
+ return {
418
+ resetPagination: options?.resetPagination ?? false,
419
+ force: options?.force ?? false,
420
+ reason: options?.reason ?? fallbackReason,
382
421
  };
383
-
384
- if (logger.isLevelEnabled('debug')) {
385
- logger.debug('Emitting tableStateChange', currentState);
386
- }
387
-
388
- onDataStateChange?.(currentState);
389
- }, [
390
- onDataStateChange,
391
- globalFilter,
392
- columnFilter,
393
- sorting,
394
- pagination,
395
- columnOrder,
396
- columnPinning,
397
- columnVisibility,
398
- columnSizing,
399
- logger,
400
- ]);
422
+ }, []);
401
423
 
402
424
 
403
425
  const handleSelectionStateChange = useCallback((updaterOrValue) => {
404
426
  setSelectionState((prevState) => {
405
- const newSelectionState = typeof updaterOrValue === 'function'
406
- ? updaterOrValue(prevState)
407
- : updaterOrValue;
408
- setTimeout(() => {
409
- if (onSelectionChange) {
410
- onSelectionChange(newSelectionState);
411
- }
412
- if (onDataStateChange) {
413
- tableStateChange({ selectionState: newSelectionState });
414
- }
415
- }, 0);
416
- return newSelectionState;
427
+ const next =
428
+ typeof updaterOrValue === 'function' ? updaterOrValue(prevState) : updaterOrValue;
429
+ onSelectionChange?.(next);
430
+ return next;
417
431
  });
418
- }, [onSelectionChange, onDataStateChange, tableStateChange]);
432
+ }, [onSelectionChange]);
419
433
 
420
434
  const handleColumnFilterStateChange = useCallback((filterState: ColumnFilterState) => {
421
435
  if (!filterState || typeof filterState !== 'object') return;
422
436
 
423
437
  setColumnFilter(filterState);
424
-
425
- if (onColumnFiltersChange) {
426
- setTimeout(() => onColumnFiltersChange(filterState), 0);
427
- }
428
-
429
- if (onDataStateChange) {
430
- setTimeout(() => tableStateChange({ columnFilter: filterState }), 0);
431
- }
432
- }, [onColumnFiltersChange, onDataStateChange, tableStateChange]);
438
+ onColumnFiltersChange?.(filterState);
439
+ return filterState;
440
+ }, [onColumnFiltersChange]);
433
441
 
434
442
 
435
443
  const resetPageToFirst = useCallback(() => {
@@ -445,43 +453,20 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
445
453
  return newPagination;
446
454
  }, [pagination, logger, onPaginationChange]);
447
455
 
448
-
449
- const handleSortingChange = useCallback((updaterOrValue: any) => {
450
- let newSorting = typeof updaterOrValue === 'function'
451
- ? updaterOrValue(sorting)
452
- : updaterOrValue;
453
- newSorting = newSorting.filter((sort: any) => sort.id);
454
- setSorting(newSorting);
455
- onSortingChange?.(newSorting);
456
456
 
457
- if (logger.isLevelEnabled('debug')) {
458
- logger.debug('Sorting change applied', {
459
- sorting: newSorting,
460
- serverMode: isServerMode,
461
- serverSorting: isServerSorting,
462
- });
463
- }
457
+ const handleSortingChange = useCallback((updaterOrValue: any) => {
464
458
 
465
- if (isServerMode || isServerSorting) {
466
- const pagination = resetPageToFirst();
467
- if (logger.isLevelEnabled('debug')) {
468
- logger.debug('Sorting change triggered server fetch', { pagination, sorting: newSorting });
459
+ setSorting((prev) => {
460
+ const next = typeof updaterOrValue === 'function' ? updaterOrValue(prev) : updaterOrValue;
461
+ const cleaned = next.filter((s: any) => s?.id);
462
+ onSortingChange?.(cleaned);
463
+ const nextPagination = resetPageToFirst();
464
+ if (isServerMode || isServerSorting) {
465
+ fetchData({ sorting: cleaned, pagination: nextPagination }, { delay: 0 });
469
466
  }
470
- tableStateChange({ sorting: newSorting, pagination });
471
- fetchData({
472
- sorting: newSorting,
473
- pagination,
474
- });
475
- } else if (onDataStateChange) {
476
- const pagination = resetPageToFirst();
477
- setTimeout(() => {
478
- if (logger.isLevelEnabled('debug')) {
479
- logger.debug('Sorting change notified client state change', { pagination, sorting: newSorting });
480
- }
481
- tableStateChange({ sorting: newSorting, pagination });
482
- }, 0);
483
- }
484
- }, [sorting, onSortingChange, logger, isServerMode, isServerSorting, onDataStateChange, resetPageToFirst, tableStateChange, fetchData]);
467
+ return cleaned;
468
+ });
469
+ }, [onSortingChange, isServerMode, isServerSorting, resetPageToFirst, fetchData]);
485
470
 
486
471
  const handleColumnOrderChange = useCallback((updatedColumnOrder: Updater<ColumnOrderState>) => {
487
472
  const newColumnOrder = typeof updatedColumnOrder === 'function'
@@ -493,144 +478,64 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
493
478
  }
494
479
  }, [onColumnDragEnd, columnOrder]);
495
480
 
496
- const handleColumnPinningChange = useCallback((updatedColumnPinning: Updater<ColumnPinningState>) => {
497
- const newColumnPinning = typeof updatedColumnPinning === 'function'
498
- ? updatedColumnPinning(columnPinning)
499
- : updatedColumnPinning;
500
- setColumnPinning(newColumnPinning);
501
- if (onColumnPinningChange) {
502
- onColumnPinningChange(newColumnPinning);
503
- }
504
- }, [onColumnPinningChange, columnPinning]);
481
+ const handleColumnPinningChange = useCallback(
482
+ (updater: Updater<ColumnPinningState>) => {
483
+ setColumnPinning((prev) => {
484
+ const next = typeof updater === "function" ? updater(prev) : updater;
485
+ // keep direct callback here (optional)
486
+ onColumnPinningChange?.(next);
487
+ return next;
488
+ });
489
+ },
490
+ [onColumnPinningChange]
491
+ );
505
492
 
506
493
  // Column visibility change handler - same pattern as column order
507
494
  const handleColumnVisibilityChange = useCallback((updater: any) => {
508
- const newVisibility = typeof updater === 'function'
509
- ? updater(columnVisibility)
510
- : updater;
511
- setColumnVisibility(newVisibility);
512
-
513
- if (onColumnVisibilityChange) {
514
- setTimeout(() => {
515
- onColumnVisibilityChange(newVisibility);
516
- }, 0);
517
- }
518
-
519
- if (onDataStateChange) {
520
- setTimeout(() => {
521
- tableStateChange({ columnVisibility: newVisibility });
522
- }, 0);
523
- }
524
- }, [onColumnVisibilityChange, onDataStateChange, tableStateChange, columnVisibility]);
495
+ setColumnVisibility((prev) => {
496
+ const next = typeof updater === 'function' ? updater(prev) : updater;
497
+ onColumnVisibilityChange?.(next);
498
+ return next;
499
+ });
500
+ }, [onColumnVisibilityChange]);
525
501
 
526
502
  // Column sizing change handler - same pattern as column order
527
503
  const handleColumnSizingChange = useCallback((updater: any) => {
528
- const newSizing = typeof updater === 'function'
529
- ? updater(columnSizing)
530
- : updater;
531
- setColumnSizing(newSizing);
532
-
533
- if (onColumnSizingChange) {
534
- setTimeout(() => {
535
- onColumnSizingChange(newSizing);
536
- }, 0);
537
- }
538
-
539
- if (onDataStateChange) {
540
- setTimeout(() => {
541
- tableStateChange({ columnSizing: newSizing });
542
- }, 0);
543
- }
544
- }, [onColumnSizingChange, onDataStateChange, tableStateChange, columnSizing]);
504
+ setColumnSizing((prev) => {
505
+ const next = typeof updater === 'function' ? updater(prev) : updater;
506
+ onColumnSizingChange?.(next);
507
+ return next;
508
+ });
509
+ }, [onColumnSizingChange]);
545
510
 
546
511
  const handlePaginationChange = useCallback((updater: any) => {
547
- const newPagination = typeof updater === 'function' ? updater(pagination) : updater;
548
- if (logger.isLevelEnabled('debug')) {
549
- logger.debug('Pagination change requested', {
550
- previous: pagination,
551
- next: newPagination,
552
- serverSide: isServerMode || isServerPagination,
553
- });
554
- }
555
-
556
- // Update pagination state
557
- setPagination(newPagination);
558
- onPaginationChange?.(newPagination);
559
-
560
- if (logger.isLevelEnabled('debug')) {
561
- logger.debug('Pagination state updated', newPagination);
562
- }
563
-
564
- // Notify state change and fetch data if needed
565
- if (isServerMode || isServerPagination) {
566
- setTimeout(() => {
567
- if (logger.isLevelEnabled('debug')) {
568
- logger.debug('Notifying server-side pagination change', newPagination);
569
- }
570
- tableStateChange({ pagination: newPagination });
571
- fetchData({ pagination: newPagination });
572
- }, 0);
573
- } else if (onDataStateChange) {
574
- setTimeout(() => {
575
- if (logger.isLevelEnabled('debug')) {
576
- logger.debug('Notifying client-side pagination change', newPagination);
577
- }
578
- tableStateChange({ pagination: newPagination });
579
- }, 0);
580
- }
581
- }, [
582
- pagination,
583
- isServerMode,
584
- isServerPagination,
585
- onDataStateChange,
586
- fetchData,
587
- tableStateChange,
588
- logger,
589
- onPaginationChange,
590
- ]);
512
+ setPagination((prev) => {
513
+ const next = typeof updater === 'function' ? updater(prev) : updater;
514
+ onPaginationChange?.(next);
515
+ if (isServerMode || isServerPagination) {
516
+ fetchData({ pagination: next }, { delay: 0 });
517
+ }
518
+ return next;
519
+ });
520
+ }, [isServerMode, isServerPagination, fetchData, onPaginationChange]);
591
521
 
592
522
 
593
523
 
594
524
  const handleGlobalFilterChange = useCallback((updaterOrValue: any) => {
595
- const newFilter = typeof updaterOrValue === 'function'
596
- ? updaterOrValue(globalFilter)
597
- : updaterOrValue;
598
- setGlobalFilter(newFilter);
525
+ setGlobalFilter((prev) => {
526
+ const next = typeof updaterOrValue === 'function' ? updaterOrValue(prev) : updaterOrValue;
599
527
 
600
- if (logger.isLevelEnabled('debug')) {
601
- logger.debug('Global filter change applied', {
602
- value: newFilter,
603
- serverMode: isServerMode,
604
- serverFiltering: isServerFiltering,
605
- });
606
- }
528
+ onGlobalFilterChange?.(next);
607
529
 
608
- if (isServerMode || isServerFiltering) {
609
- const pagination = resetPageToFirst();
610
- setTimeout(() => {
611
- if (logger.isLevelEnabled('debug')) {
612
- logger.debug('Global filter change triggering server fetch', {
613
- pagination,
614
- value: newFilter,
615
- });
616
- }
617
- tableStateChange({ globalFilter: newFilter, pagination });
618
- fetchData({ globalFilter: newFilter, pagination });
619
- }, 0);
620
- } else if (onDataStateChange) {
621
- const pagination = resetPageToFirst();
622
- setTimeout(() => {
623
- if (logger.isLevelEnabled('debug')) {
624
- logger.debug('Global filter change notifying client listeners', {
625
- pagination,
626
- value: newFilter,
627
- });
628
- }
629
- tableStateChange({ globalFilter: newFilter, pagination });
630
- }, 0);
631
- }
632
- onGlobalFilterChange?.(newFilter);
633
- }, [globalFilter, logger, isServerMode, isServerFiltering, onDataStateChange, onGlobalFilterChange, resetPageToFirst, tableStateChange, fetchData]);
530
+ if (isServerMode || isServerFiltering) {
531
+ const nextPagination = { pageIndex: 0, pageSize: pagination.pageSize };
532
+ setPagination(nextPagination);
533
+ fetchData({ globalFilter: next, pagination: nextPagination }, { delay: 0 });
534
+ }
535
+
536
+ return next;
537
+ });
538
+ }, [isServerMode, isServerFiltering, onGlobalFilterChange, fetchData, pagination.pageSize]);
634
539
 
635
540
  const onColumnFilterChangeHandler = useCallback((updater: any) => {
636
541
  const currentState = columnFilter;
@@ -648,24 +553,15 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
648
553
 
649
554
  const onColumnFilterApplyHandler = useCallback((appliedState: ColumnFilterState) => {
650
555
  const pagination = resetPageToFirst();
651
-
652
556
  if (isServerFiltering) {
653
- tableStateChange({
654
- columnFilter: appliedState,
655
- pagination,
656
- });
657
557
  fetchData({
658
558
  columnFilter: appliedState,
659
559
  pagination,
660
560
  });
661
- } else if (onDataStateChange) {
662
- setTimeout(() => tableStateChange({ columnFilter: appliedState, pagination }), 0);
663
561
  }
664
562
 
665
- setTimeout(() => {
666
- onColumnFiltersChange?.(appliedState);
667
- }, 0);
668
- }, [resetPageToFirst, isServerFiltering, onDataStateChange, tableStateChange, fetchData, onColumnFiltersChange]);
563
+ onColumnFiltersChange?.(appliedState);
564
+ }, [resetPageToFirst, isServerFiltering, fetchData, onColumnFiltersChange]);
669
565
 
670
566
  // -------------------------------
671
567
  // Table creation (after callbacks/memo)
@@ -795,12 +691,21 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
795
691
  // -------------------------------
796
692
  // Effects (after callbacks)
797
693
  // -------------------------------
694
+ useEffect(() => {
695
+ if (!isExternallyControlledData || serverData === null) return;
696
+ setServerData(null);
697
+ setServerTotal(0);
698
+ }, [isExternallyControlledData, serverData]);
699
+
798
700
  useEffect(() => {
799
701
  if (initialLoadData && onFetchData) {
800
702
  if (logger.isLevelEnabled('info')) {
801
703
  logger.info('Initial data load triggered', { initialLoadData });
802
704
  }
803
- fetchData({});
705
+ fetchData({}, {
706
+ delay: 0,
707
+ meta: { reason: 'initial' },
708
+ });
804
709
  } else if (logger.isLevelEnabled('debug')) {
805
710
  logger.debug('Skipping initial data load', {
806
711
  initialLoadData,
@@ -825,501 +730,852 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
825
730
  }, [enableColumnDragging, enhancedColumns, columnOrder.length]);
826
731
 
827
732
 
828
- const dataTableApi = useMemo(() => ({
829
- table: {
830
- getTable: () => table,
831
- },
832
- // Column Management
833
- columnVisibility: {
834
- showColumn: (columnId: string) => {
835
- table.getColumn(columnId)?.toggleVisibility(true);
836
- },
837
- hideColumn: (columnId: string) => {
838
- table.getColumn(columnId)?.toggleVisibility(false);
839
- },
840
- toggleColumn: (columnId: string) => {
841
- table.getColumn(columnId)?.toggleVisibility();
842
- },
843
- showAllColumns: () => {
844
- table.toggleAllColumnsVisible(true);
845
- },
846
- hideAllColumns: () => {
847
- table.toggleAllColumnsVisible(false);
848
- },
849
- resetColumnVisibility: () => {
850
- const initialVisibility = initialStateConfig.columnVisibility || {};
851
- table.setColumnVisibility(initialVisibility);
852
- // Manually trigger handler to ensure callbacks are called
853
- handleColumnVisibilityChange(initialVisibility);
854
- },
855
- },
733
+ const lastSentRef = useRef<string>("");
856
734
 
857
- // Column Ordering
858
- columnOrdering: {
859
- setColumnOrder: (columnOrder: ColumnOrderState) => {
860
- table.setColumnOrder(columnOrder);
861
- },
862
- moveColumn: (columnId: string, toIndex: number) => {
863
- const currentOrder = table.getState().columnOrder || [];
864
- const currentIndex = currentOrder.indexOf(columnId);
865
- if (currentIndex === -1) return;
735
+ const emitTableState = useCallback(() => {
736
+ if (!onDataStateChange) return;
866
737
 
867
- const newOrder = [...currentOrder];
868
- newOrder.splice(currentIndex, 1);
869
- newOrder.splice(toIndex, 0, columnId);
738
+ const live = table.getState();
739
+ const liveColumnFilter = live.columnFilter;
870
740
 
871
- table.setColumnOrder(newOrder);
872
- },
873
- resetColumnOrder: () => {
874
- const initialOrder = enhancedColumns.map((col, index) => {
875
- if (col.id) return col.id;
876
- const anyCol = col as any;
877
- if (anyCol.accessorKey && typeof anyCol.accessorKey === 'string') {
878
- return anyCol.accessorKey;
879
- }
880
- return `column_${index}`;
881
- });
882
- table.setColumnOrder(initialOrder);
883
- // Manually trigger handler to ensure callbacks are called
884
- handleColumnOrderChange(initialOrder);
885
- },
886
- },
741
+ // only keep what you persist/store
742
+ const payload = {
743
+ sorting: live.sorting,
744
+ pagination: live.pagination,
745
+ globalFilter: live.globalFilter,
746
+ columnFilter: liveColumnFilter,
747
+ columnVisibility: live.columnVisibility,
748
+ columnSizing: live.columnSizing,
749
+ columnOrder: live.columnOrder,
750
+ columnPinning: live.columnPinning,
751
+ };
887
752
 
888
- // Column Pinning
889
- columnPinning: {
890
- pinColumnLeft: (columnId: string) => {
891
- const currentPinning = table.getState().columnPinning;
892
- const newPinning = { ...currentPinning };
753
+ const key = JSON.stringify(payload);
754
+ if (key === lastSentRef.current) return;
893
755
 
894
- // Remove from right if exists
895
- newPinning.right = (newPinning.right || []).filter(id => id !== columnId);
896
- // Add to left if not exists
897
- newPinning.left = [...(newPinning.left || []).filter(id => id !== columnId), columnId];
756
+ lastSentRef.current = key;
757
+ onDataStateChange(payload);
758
+ }, [onDataStateChange, table]);
898
759
 
899
- table.setColumnPinning(newPinning);
900
- },
901
- pinColumnRight: (columnId: string) => {
902
- const currentPinning = table.getState().columnPinning;
903
- const newPinning = { ...currentPinning };
760
+ useEffect(() => {
761
+ emitTableState();
762
+ }, [
763
+ emitTableState,
764
+ sorting,
765
+ pagination,
766
+ globalFilter,
767
+ columnFilter,
768
+ columnVisibility,
769
+ columnSizing,
770
+ columnOrder,
771
+ columnPinning,
772
+ ]);
904
773
 
905
- // Remove from left if exists
906
- newPinning.left = (newPinning.left || []).filter(id => id !== columnId);
907
- // Add to right if not exists - prepend to beginning (appears rightmost to leftmost)
908
- // First column pinned appears rightmost, second appears to its left, etc.
909
- newPinning.right = [columnId, ...(newPinning.right || []).filter(id => id !== columnId)];
910
774
 
911
- table.setColumnPinning(newPinning);
912
- },
913
- unpinColumn: (columnId: string) => {
914
- const currentPinning = table.getState().columnPinning;
915
- const newPinning = {
916
- left: (currentPinning.left || []).filter(id => id !== columnId),
917
- right: (currentPinning.right || []).filter(id => id !== columnId),
918
- };
775
+ const getResetState = useCallback((): Partial<TableState> => {
776
+ const resetSorting = initialStateConfig.sorting || [];
777
+ const resetGlobalFilter = initialStateConfig.globalFilter ?? '';
778
+ const resetColumnFilter = initialStateConfig.columnFilter;
919
779
 
920
- table.setColumnPinning(newPinning);
921
- },
922
- setPinning: (pinning: ColumnPinningState) => {
923
- table.setColumnPinning(pinning);
924
- },
925
- resetColumnPinning: () => {
926
- const initialPinning = initialStateConfig.columnPinning || { left: [], right: [] };
927
- table.setColumnPinning(initialPinning);
928
- // Manually trigger handler to ensure callbacks are called
929
- handleColumnPinningChange(initialPinning);
930
- },
931
- },
780
+ const resetPagination = enablePagination
781
+ ? (initialStateConfig.pagination || { pageIndex: 0, pageSize: 10 })
782
+ : undefined;
932
783
 
933
- // Column Resizing
934
- columnResizing: {
935
- resizeColumn: (columnId: string, width: number) => {
936
- // Use table's setColumnSizing method
937
- const currentSizing = table.getState().columnSizing;
938
- table.setColumnSizing({
939
- ...currentSizing,
940
- [columnId]: width,
941
- });
942
- },
943
- autoSizeColumn: (columnId: string) => {
944
- // TanStack doesn't have built-in auto-size, so reset to default
945
- table.getColumn(columnId)?.resetSize();
946
- },
947
- autoSizeAllColumns: () => {
948
- const initialSizing = initialStateConfig.columnSizing || {};
949
- table.setColumnSizing(initialSizing);
950
- // Manually trigger handler to ensure callbacks are called
951
- handleColumnSizingChange(initialSizing);
952
- },
953
- resetColumnSizing: () => {
954
- const initialSizing = initialStateConfig.columnSizing || {};
955
- table.setColumnSizing(initialSizing);
956
- // Manually trigger handler to ensure callbacks are called
957
- handleColumnSizingChange(initialSizing);
958
- },
959
- },
784
+ return {
785
+ sorting: resetSorting,
786
+ globalFilter: resetGlobalFilter,
787
+ columnFilter: resetColumnFilter,
788
+ ...(resetPagination ? { pagination: resetPagination } : {}),
789
+ };
790
+ }, [initialStateConfig, enablePagination]);
960
791
 
961
- // Filtering
962
- filtering: {
963
- setGlobalFilter: (filter: string) => {
964
- table.setGlobalFilter(filter);
965
- },
966
- clearGlobalFilter: () => {
967
- table.setGlobalFilter('');
968
- },
969
- setColumnFilters: (filters: ColumnFilterState) => {
970
- handleColumnFilterStateChange(filters);
971
- },
972
- addColumnFilter: (columnId: string, operator: string, value: any) => {
973
- const newFilter = {
974
- id: `filter_${Date.now()}`,
975
- columnId,
976
- operator,
977
- value,
978
- };
979
- const columnFilter = table.getState().columnFilter;
980
-
981
- const currentFilters = columnFilter.filters || [];
982
- const newFilters = [...currentFilters, newFilter];
983
- handleColumnFilterStateChange({
984
- filters: newFilters,
985
- logic: columnFilter.logic,
986
- pendingFilters: columnFilter.pendingFilters || [],
987
- pendingLogic: columnFilter.pendingLogic || 'AND',
988
- });
989
- if (logger.isLevelEnabled('debug')) {
990
- logger.debug(`Adding column filter ${columnId} ${operator} ${value}`, newFilters);
991
- }
992
- },
993
- removeColumnFilter: (filterId: string) => {
994
- const columnFilter = table.getState().columnFilter;
995
- const currentFilters = columnFilter.filters || [];
996
- const newFilters = currentFilters.filter((f: any) => f.id !== filterId);
997
- handleColumnFilterStateChange({
998
- filters: newFilters,
999
- logic: columnFilter.logic,
1000
- pendingFilters: columnFilter.pendingFilters || [],
1001
- pendingLogic: columnFilter.pendingLogic || 'AND',
1002
- });
1003
- if (logger.isLevelEnabled('debug')) {
1004
- logger.debug(`Removing column filter ${filterId}`, newFilters);
1005
- }
1006
- },
1007
- clearAllFilters: () => {
1008
- table.setGlobalFilter('');
1009
- handleColumnFilterStateChange({
1010
- filters: [],
1011
- logic: 'AND',
1012
- pendingFilters: [],
1013
- pendingLogic: 'AND',
1014
- });
1015
- },
1016
- resetFilters: () => {
1017
- handleColumnFilterStateChange({
1018
- filters: [],
1019
- logic: 'AND',
1020
- pendingFilters: [],
1021
- pendingLogic: 'AND',
1022
- });
1023
- if (logger.isLevelEnabled('debug')) {
1024
- logger.debug('Resetting filters');
1025
- }
1026
- },
1027
- },
792
+ const applyDataMutation = useCallback((
793
+ action: DataMutationAction,
794
+ updater: (rows: T[]) => T[],
795
+ details: Partial<Omit<DataMutationContext<T>, 'action' | 'previousData' | 'nextData'>> = {}
796
+ ) => {
797
+ const previousData = [...tableData];
798
+ const nextData = updater(previousData);
1028
799
 
1029
- // Sorting
1030
- sorting: {
1031
- setSorting: (sortingState: SortingState) => {
1032
- table.setSorting(sortingState);
1033
- if (logger.isLevelEnabled('debug')) {
1034
- logger.debug(`Setting sorting`, sortingState);
1035
- }
1036
- },
1037
- sortColumn: (columnId: string, direction: 'asc' | 'desc' | false) => {
1038
- const column = table.getColumn(columnId);
1039
- if (!column) return;
1040
-
1041
- if (direction === false) {
1042
- column.clearSorting();
1043
- } else {
1044
- column.toggleSorting(direction === 'desc');
1045
- }
1046
- },
1047
- clearSorting: () => {
1048
- table.setSorting([]);
1049
- // Manually trigger handler to ensure callbacks are called
1050
- handleSortingChange([]);
1051
- },
1052
- resetSorting: () => {
1053
- const initialSorting = initialStateConfig.sorting || [];
1054
- table.setSorting(initialSorting);
1055
- // Manually trigger handler to ensure callbacks are called
1056
- handleSortingChange(initialSorting);
1057
- },
1058
- },
800
+ if (nextData === previousData) return nextData;
1059
801
 
1060
- // Pagination
1061
- pagination: {
1062
- goToPage: (pageIndex: number) => {
1063
- table.setPageIndex(pageIndex);
1064
- if (logger.isLevelEnabled('debug')) {
1065
- logger.debug(`Going to page ${pageIndex}`);
1066
- }
1067
- },
1068
- nextPage: () => {
1069
- table.nextPage();
1070
- if (logger.isLevelEnabled('debug')) {
1071
- logger.debug('Next page');
1072
- }
1073
- },
1074
- previousPage: () => {
1075
- table.previousPage();
1076
- if (logger.isLevelEnabled('debug')) {
1077
- logger.debug('Previous page');
802
+ const nextTotal = Math.max(0, tableTotalRow + (nextData.length - previousData.length));
803
+
804
+ if (!isExternallyControlledData) {
805
+ setServerData(nextData);
806
+ setServerTotal(nextTotal);
807
+ }
808
+ onDataChange?.(nextData, {
809
+ action,
810
+ previousData,
811
+ nextData,
812
+ totalRow: nextTotal,
813
+ ...details,
814
+ });
815
+
816
+ if (logger.isLevelEnabled('debug')) {
817
+ logger.debug('Applied data mutation', {
818
+ action,
819
+ previousCount: previousData.length,
820
+ nextCount: nextData.length,
821
+ totalRow: nextTotal,
822
+ });
823
+ }
824
+
825
+ return nextData;
826
+ }, [isExternallyControlledData, logger, onDataChange, tableData, tableTotalRow]);
827
+
828
+ const buildRefreshContext = useCallback((
829
+ options: ReturnType<typeof normalizeRefreshOptions>,
830
+ paginationOverride?: { pageIndex: number; pageSize: number }
831
+ ): DataRefreshContext => {
832
+ const state = table.getState();
833
+ const nextPagination = paginationOverride || state.pagination || pagination;
834
+
835
+ return {
836
+ filters: {
837
+ globalFilter,
838
+ pagination: nextPagination,
839
+ columnFilter,
840
+ sorting,
841
+ },
842
+ state: {
843
+ sorting,
844
+ pagination: nextPagination,
845
+ globalFilter,
846
+ columnFilter,
847
+ columnVisibility: state.columnVisibility,
848
+ columnSizing: state.columnSizing,
849
+ columnOrder: state.columnOrder,
850
+ columnPinning: state.columnPinning,
851
+ },
852
+ options,
853
+ };
854
+ }, [table, pagination, globalFilter, columnFilter, sorting]);
855
+
856
+ const triggerRefresh = useCallback(async (
857
+ options?: boolean | DataRefreshOptions,
858
+ fallbackReason: string = 'refresh'
859
+ ) => {
860
+ const normalizedOptions = normalizeRefreshOptions(options, fallbackReason);
861
+ const nextPagination = enablePagination
862
+ ? {
863
+ pageIndex: normalizedOptions.resetPagination ? 0 : pagination.pageIndex,
864
+ pageSize: pagination.pageSize,
865
+ }
866
+ : undefined;
867
+
868
+ const shouldUpdatePagination = !!nextPagination
869
+ && (nextPagination.pageIndex !== pagination.pageIndex || nextPagination.pageSize !== pagination.pageSize);
870
+
871
+ if (nextPagination && shouldUpdatePagination) {
872
+ setPagination(nextPagination);
873
+ onPaginationChange?.(nextPagination);
874
+ }
875
+
876
+ const refreshContext = buildRefreshContext(normalizedOptions, nextPagination);
877
+
878
+ if (onRefreshData) {
879
+ await onRefreshData(refreshContext);
880
+ return;
881
+ }
882
+
883
+ if (onFetchData) {
884
+ await fetchData(
885
+ nextPagination ? { pagination: nextPagination } : {},
886
+ {
887
+ delay: 0,
888
+ meta: {
889
+ reason: normalizedOptions.reason,
890
+ force: normalizedOptions.force,
891
+ },
1078
892
  }
1079
- },
1080
- setPageSize: (pageSize: number) => {
1081
- table.setPageSize(pageSize);
1082
- if (logger.isLevelEnabled('debug')) {
1083
- logger.debug(`Setting page size to ${pageSize}`);
893
+ );
894
+ return;
895
+ }
896
+
897
+ if (logger.isLevelEnabled('debug')) {
898
+ logger.debug('Refresh skipped because no refresh handler is configured', refreshContext);
899
+ }
900
+ }, [
901
+ normalizeRefreshOptions,
902
+ enablePagination,
903
+ pagination,
904
+ onPaginationChange,
905
+ buildRefreshContext,
906
+ onRefreshData,
907
+ onFetchData,
908
+ fetchData,
909
+ logger,
910
+ ]);
911
+
912
+ const resetAllAndReload = useCallback(() => {
913
+ const resetState = getResetState();
914
+
915
+ setSorting(resetState.sorting || []);
916
+ setGlobalFilter(resetState.globalFilter ?? '');
917
+ setColumnFilter(resetState.columnFilter as any);
918
+
919
+ if (resetState.pagination) {
920
+ setPagination(resetState.pagination);
921
+ onPaginationChange?.(resetState.pagination);
922
+ }
923
+
924
+ setSelectionState(initialSelectionState);
925
+ setExpanded({});
926
+
927
+ // layout state
928
+ setColumnVisibility(initialStateConfig.columnVisibility || {});
929
+ setColumnSizing(initialStateConfig.columnSizing || {});
930
+ setColumnOrder(initialStateConfig.columnOrder || []);
931
+ setColumnPinning(initialStateConfig.columnPinning || { left: [], right: [] });
932
+
933
+ const resetOptions = normalizeRefreshOptions({
934
+ resetPagination: true,
935
+ force: true,
936
+ reason: 'reset',
937
+ }, 'reset');
938
+
939
+ const refreshContext = buildRefreshContext(resetOptions, resetState.pagination);
940
+
941
+ if (onRefreshData) {
942
+ void onRefreshData(refreshContext);
943
+ return;
944
+ }
945
+
946
+ if (onFetchData) {
947
+ void fetchData(resetState, {
948
+ delay: 0,
949
+ meta: {
950
+ reason: resetOptions.reason,
951
+ force: resetOptions.force,
952
+ },
953
+ });
954
+ }
955
+ }, [
956
+ getResetState,
957
+ initialSelectionState,
958
+ initialStateConfig,
959
+ onPaginationChange,
960
+ normalizeRefreshOptions,
961
+ buildRefreshContext,
962
+ onRefreshData,
963
+ onFetchData,
964
+ fetchData,
965
+ ]);
966
+
967
+ const setExportControllerSafely = useCallback((
968
+ value: AbortController | null | ((current: AbortController | null) => AbortController | null)
969
+ ) => {
970
+ setExportController((current) => {
971
+ const next = typeof value === 'function' ? (value as any)(current) : value;
972
+ exportControllerRef.current = next;
973
+ return next;
974
+ });
975
+ }, []);
976
+
977
+ const handleExportProgressInternal = useCallback((progress: ExportProgressPayload) => {
978
+ setExportProgress(progress || {});
979
+ onExportProgress?.(progress);
980
+ }, [onExportProgress]);
981
+
982
+ const handleExportStateChangeInternal = useCallback((state: ExportStateChange) => {
983
+ setExportPhase(state.phase);
984
+ if (
985
+ state.processedRows !== undefined
986
+ || state.totalRows !== undefined
987
+ || state.percentage !== undefined
988
+ ) {
989
+ setExportProgress({
990
+ processedRows: state.processedRows,
991
+ totalRows: state.totalRows,
992
+ percentage: state.percentage,
993
+ });
994
+ }
995
+ onExportStateChange?.(state);
996
+ }, [onExportStateChange]);
997
+
998
+ const runExportWithPolicy = useCallback(
999
+ async (
1000
+ options: {
1001
+ format: 'csv' | 'excel';
1002
+ filename: string;
1003
+ mode: 'client' | 'server';
1004
+ execute: (controller: AbortController) => Promise<void>;
1005
+ }
1006
+ ) => {
1007
+ const { format, filename, mode, execute } = options;
1008
+
1009
+ const startExecution = async () => {
1010
+ const controller = new AbortController();
1011
+ setExportProgress({});
1012
+ setExportControllerSafely(controller);
1013
+ try {
1014
+ await execute(controller);
1015
+ } finally {
1016
+ setExportControllerSafely((current) => (current === controller ? null : current));
1084
1017
  }
1085
- },
1086
- goToFirstPage: () => {
1087
- table.setPageIndex(0);
1088
- if (logger.isLevelEnabled('debug')) {
1089
- logger.debug('Going to first page');
1018
+ };
1019
+
1020
+ if (exportConcurrency === 'queue') {
1021
+ setQueuedExportCount((prev) => prev + 1);
1022
+ const runQueued = async (): Promise<void> => {
1023
+ setQueuedExportCount((prev) => Math.max(0, prev - 1));
1024
+ await startExecution();
1025
+ };
1026
+ const queuedPromise = exportQueueRef.current
1027
+ .catch(() => undefined)
1028
+ .then(runQueued);
1029
+ exportQueueRef.current = queuedPromise;
1030
+ return queuedPromise;
1031
+ }
1032
+
1033
+ const activeController = exportControllerRef.current;
1034
+ if (activeController) {
1035
+ if (exportConcurrency === 'ignoreIfRunning') {
1036
+ handleExportStateChangeInternal({
1037
+ phase: 'error',
1038
+ mode,
1039
+ format,
1040
+ filename,
1041
+ message: 'An export is already running',
1042
+ code: 'EXPORT_IN_PROGRESS',
1043
+ endedAt: Date.now(),
1044
+ });
1045
+ onExportError?.({
1046
+ message: 'An export is already running',
1047
+ code: 'EXPORT_IN_PROGRESS',
1048
+ });
1049
+ return;
1090
1050
  }
1091
- },
1092
- goToLastPage: () => {
1093
- const pageCount = table.getPageCount();
1094
- if (pageCount > 0) {
1095
- table.setPageIndex(pageCount - 1);
1096
- if (logger.isLevelEnabled('debug')) {
1097
- logger.debug(`Going to last page ${pageCount - 1}`);
1098
- }
1051
+
1052
+ if (exportConcurrency === 'cancelAndRestart') {
1053
+ activeController.abort();
1099
1054
  }
1100
- },
1101
- resetPagination: () => {
1102
- const initialPagination = initialStateConfig.pagination || { pageIndex: 0, pageSize: 10 };
1103
- table.setPagination(initialPagination);
1104
- // Manually trigger handler to ensure callbacks are called
1105
- handlePaginationChange(initialPagination);
1106
- },
1107
- },
1055
+ }
1108
1056
 
1109
- // Access via table methods: table.selectRow(), table.getIsRowSelected(), etc.
1110
- selection: {
1111
- selectRow: (rowId: string) => table.selectRow?.(rowId),
1112
- deselectRow: (rowId: string) => table.deselectRow?.(rowId),
1113
- toggleRowSelection: (rowId: string) => table.toggleRowSelected?.(rowId),
1114
- selectAll: () => table.selectAll?.(),
1115
- deselectAll: () => table.deselectAll?.(),
1116
- toggleSelectAll: () => table.toggleAllRowsSelected?.(),
1117
- getSelectionState: () => table.getSelectionState?.() || { ids: [], type: 'include' as const },
1118
- getSelectedRows: () => table.getSelectedRows(),
1119
- getSelectedCount: () => table.getSelectedCount(),
1120
- isRowSelected: (rowId) => table.getIsRowSelected(rowId) || false,
1057
+ await startExecution();
1121
1058
  },
1059
+ [
1060
+ exportConcurrency,
1061
+ handleExportStateChangeInternal,
1062
+ onExportError,
1063
+ setExportControllerSafely,
1064
+ ]
1065
+ );
1122
1066
 
1123
- // Data Management
1124
- data: {
1125
- refresh: (resetPagination = false) => {
1126
- const filters = table.getState();
1127
- const pagination = {
1128
- pageIndex: resetPagination ? 0 : initialStateConfig.pagination?.pageIndex || 0,
1129
- pageSize: filters.pagination?.pageSize || initialStateConfig.pagination?.pageSize || 10,
1130
- };
1131
- const allState = table.getState();
1132
- setPagination(pagination);
1133
- onDataStateChange?.({ ...allState, pagination });
1134
- fetchData?.({ pagination });
1135
- if (logger.isLevelEnabled('debug')) {
1136
- logger.debug('Refreshing data using Ref', { pagination, allState });
1137
- }
1138
- },
1139
- reload: () => {
1140
- const allState = table.getState();
1067
+ const dataTableApi = useMemo(() => {
1068
+ // helpers (avoid repeating boilerplate)
1069
+ const buildInitialOrder = () =>
1070
+ enhancedColumns.map((col, index) => {
1071
+ if ((col as any).id) return (col as any).id as string;
1072
+ const anyCol = col as any;
1073
+ if (anyCol.accessorKey && typeof anyCol.accessorKey === "string") return anyCol.accessorKey;
1074
+ return `column_${index}`;
1075
+ });
1141
1076
 
1142
- onDataStateChange?.(allState);
1143
- fetchData?.({});
1144
- if (logger.isLevelEnabled('debug')) {
1145
- logger.info('Reloading data', allState);
1146
- }
1147
- },
1148
- // Data CRUD operations
1149
- getAllData: () => {
1150
- return table.getRowModel().rows?.map(row => row.original) || [];
1151
- },
1152
- getRowData: (rowId: string) => {
1153
- return table.getRowModel().rows?.find(row => String(row.original[idKey]) === rowId)?.original;
1154
- },
1155
- getRowByIndex: (index: number) => {
1156
- return table.getRowModel().rows?.[index]?.original;
1157
- },
1158
- updateRow: (rowId: string, updates: Partial<T>) => {
1159
- const newData = table.getRowModel().rows?.map(row => String(row.original[idKey]) === rowId
1160
- ? {
1161
- ...row.original,
1162
- ...updates,
1163
- }
1164
- : row.original);
1165
- setServerData?.(newData || []);
1166
- if (logger.isLevelEnabled('debug')) {
1167
- logger.debug(`Updating row ${rowId}`, updates);
1168
- }
1169
- },
1170
- updateRowByIndex: (index: number, updates: Partial<T>) => {
1171
- const newData = table.getRowModel().rows?.map(row => row.original);
1172
- if (newData?.[index]) {
1173
- newData[index] = {
1174
- ...newData[index]!,
1175
- ...updates,
1176
- };
1177
- setServerData(newData);
1178
- if (logger.isLevelEnabled('debug')) {
1179
- logger.debug(`Updating row by index ${index}`, updates);
1180
- }
1181
- }
1182
- },
1183
- insertRow: (newRow: T, index?: number) => {
1184
- const newData = table.getRowModel().rows?.map(row => row.original) || [];
1185
- if (index !== undefined) {
1186
- newData.splice(index, 0, newRow);
1187
- } else {
1188
- newData.push(newRow);
1189
- }
1190
- setServerData(newData || []);
1191
- if (logger.isLevelEnabled('debug')) {
1192
- logger.debug(`Inserting row`, newRow);
1193
- }
1194
- },
1195
- deleteRow: (rowId: string) => {
1196
- const newData = (table.getRowModel().rows || [])?.filter(row => String(row.original[idKey]) !== rowId);
1197
- setServerData?.(newData?.map(row => row.original) || []);
1198
- if (logger.isLevelEnabled('debug')) {
1199
- logger.debug(`Deleting row ${rowId}`);
1200
- }
1201
- },
1202
- deleteRowByIndex: (index: number) => {
1203
- const newData = (table.getRowModel().rows || [])?.map(row => row.original);
1204
- newData.splice(index, 1);
1205
- setServerData(newData);
1206
- if (logger.isLevelEnabled('debug')) {
1207
- logger.debug(`Deleting row by index ${index}`);
1208
- }
1209
- },
1210
- deleteSelectedRows: () => {
1211
- const selectedRows = table.getSelectedRows?.() || [];
1212
- if (selectedRows.length === 0) return;
1077
+ const applyColumnOrder = (next: ColumnOrderState) => {
1078
+ // handleColumnOrderChange supports both Updater<ColumnOrderState> and array in your impl
1079
+ handleColumnOrderChange(next as any);
1080
+ };
1213
1081
 
1214
- const selectedIds = new Set(selectedRows.map(row => String(row.original[idKey])));
1215
- const newData = (table.getRowModel().rows || [])?.filter(row => !selectedIds.has(String(row.original[idKey])));
1082
+ const applyPinning = (next: ColumnPinningState) => {
1083
+ handleColumnPinningChange(next as any);
1084
+ };
1216
1085
 
1217
- setServerData(newData?.map(row => row.original) || []);
1218
- table.deselectAll?.();
1086
+ const applyVisibility = (next: Record<string, boolean>) => {
1087
+ handleColumnVisibilityChange(next as any);
1088
+ };
1219
1089
 
1220
- if (logger.isLevelEnabled('debug')) {
1221
- logger.debug('Deleting selected rows');
1222
- }
1223
- },
1224
- replaceAllData: (newData: T[]) => {
1225
- setServerData?.(newData);
1226
- },
1090
+ const applySizing = (next: Record<string, number>) => {
1091
+ handleColumnSizingChange(next as any);
1092
+ };
1227
1093
 
1228
- // Bulk operations
1229
- updateMultipleRows: (updates: Array<{ rowId: string; data: Partial<T> }>) => {
1230
- const updateMap = new Map(updates.map(u => [u.rowId, u.data]));
1231
- const newData = (table.getRowModel().rows || [])?.map(row => {
1232
- const rowId = String(row.original[idKey]);
1233
- const updateData = updateMap.get(rowId);
1234
- return updateData ? {
1235
- ...row.original,
1236
- ...updateData,
1237
- } : row.original;
1238
- });
1239
- setServerData(newData || []);
1240
- },
1241
- insertMultipleRows: (newRows: T[], startIndex?: number) => {
1242
- const newData = (table.getRowModel().rows || [])?.map(row => row.original);
1243
- if (startIndex !== undefined) {
1244
- newData.splice(startIndex, 0, ...newRows);
1245
- } else {
1246
- newData.push(...newRows);
1247
- }
1248
- setServerData?.(newData);
1249
- },
1250
- deleteMultipleRows: (rowIds: string[]) => {
1251
- const idsToDelete = new Set(rowIds);
1252
- const newData = (table.getRowModel().rows || [])?.filter(row => !idsToDelete.has(String(row.original[idKey])))?.map(row => row.original);
1253
- setServerData(newData);
1254
- },
1094
+ const applyPagination = (next: any) => {
1095
+ handlePaginationChange(next);
1096
+ };
1255
1097
 
1256
- // Field-specific updates
1257
- updateField: (rowId: string, fieldName: keyof T, value: any) => {
1258
- const newData = (table.getRowModel().rows || [])?.map(row => String(row.original[idKey]) === rowId
1259
- ? {
1260
- ...row.original,
1261
- [fieldName]: value,
1262
- }
1263
- : row.original);
1264
- setServerData?.(newData);
1265
- },
1266
- updateFieldByIndex: (index: number, fieldName: keyof T, value: any) => {
1267
- const newData = (table.getRowModel().rows || [])?.map(row => row.original);
1268
- if (newData[index]) {
1269
- newData[index] = {
1270
- ...newData[index],
1271
- [fieldName]: value,
1098
+ const applySorting = (next: any) => {
1099
+ handleSortingChange(next);
1100
+ };
1101
+
1102
+ const applyGlobalFilter = (next: any) => {
1103
+ handleGlobalFilterChange(next);
1104
+ };
1105
+
1106
+ const getRowIndexById = (rowsToSearch: T[], rowId: string) =>
1107
+ rowsToSearch.findIndex((row, index) => String(generateRowId(row, index, idKey)) === rowId);
1108
+
1109
+ const clampInsertIndex = (rowsToMutate: T[], insertIndex?: number) => {
1110
+ if (insertIndex === undefined) return rowsToMutate.length;
1111
+ return Math.max(0, Math.min(insertIndex, rowsToMutate.length));
1112
+ };
1113
+
1114
+ return {
1115
+ table: {
1116
+ getTable: () => table,
1117
+ },
1118
+
1119
+ // -------------------------------
1120
+ // Column Management
1121
+ // -------------------------------
1122
+ columnVisibility: {
1123
+ showColumn: (columnId: string) => {
1124
+ applyVisibility({ ...table.getState().columnVisibility, [columnId]: true });
1125
+ },
1126
+ hideColumn: (columnId: string) => {
1127
+ applyVisibility({ ...table.getState().columnVisibility, [columnId]: false });
1128
+ },
1129
+ toggleColumn: (columnId: string) => {
1130
+ const curr = table.getState().columnVisibility?.[columnId] ?? true;
1131
+ applyVisibility({ ...table.getState().columnVisibility, [columnId]: !curr });
1132
+ },
1133
+ showAllColumns: () => {
1134
+ // set all known columns true
1135
+ const all: Record<string, boolean> = {};
1136
+ table.getAllLeafColumns().forEach((c) => (all[c.id] = true));
1137
+ applyVisibility(all);
1138
+ },
1139
+ hideAllColumns: () => {
1140
+ const all: Record<string, boolean> = {};
1141
+ table.getAllLeafColumns().forEach((c) => (all[c.id] = false));
1142
+ applyVisibility(all);
1143
+ },
1144
+ resetColumnVisibility: () => {
1145
+ const initialVisibility = initialStateConfig.columnVisibility || {};
1146
+ applyVisibility(initialVisibility);
1147
+ },
1148
+ },
1149
+
1150
+ // -------------------------------
1151
+ // Column Ordering
1152
+ // -------------------------------
1153
+ columnOrdering: {
1154
+ setColumnOrder: (nextOrder: ColumnOrderState) => {
1155
+ applyColumnOrder(nextOrder);
1156
+ },
1157
+ moveColumn: (columnId: string, toIndex: number) => {
1158
+ const currentOrder =
1159
+ (table.getState().columnOrder?.length ? table.getState().columnOrder : buildInitialOrder()) || [];
1160
+ const fromIndex = currentOrder.indexOf(columnId);
1161
+ if (fromIndex === -1) return;
1162
+
1163
+ const next = [...currentOrder];
1164
+ next.splice(fromIndex, 1);
1165
+ next.splice(toIndex, 0, columnId);
1166
+
1167
+ applyColumnOrder(next);
1168
+ },
1169
+ resetColumnOrder: () => {
1170
+ applyColumnOrder(buildInitialOrder());
1171
+ },
1172
+ },
1173
+
1174
+ // -------------------------------
1175
+ // Column Pinning
1176
+ // -------------------------------
1177
+ columnPinning: {
1178
+ pinColumnLeft: (columnId: string) => {
1179
+ const current = table.getState().columnPinning || { left: [], right: [] };
1180
+ const next: ColumnPinningState = {
1181
+ left: [...(current.left || []).filter((id) => id !== columnId), columnId],
1182
+ right: (current.right || []).filter((id) => id !== columnId),
1183
+ };
1184
+ applyPinning(next);
1185
+ },
1186
+ pinColumnRight: (columnId: string) => {
1187
+ const current = table.getState().columnPinning || { left: [], right: [] };
1188
+ const next: ColumnPinningState = {
1189
+ left: (current.left || []).filter((id) => id !== columnId),
1190
+ // keep your "prepend" behavior
1191
+ right: [columnId, ...(current.right || []).filter((id) => id !== columnId)],
1192
+ };
1193
+ applyPinning(next);
1194
+ },
1195
+ unpinColumn: (columnId: string) => {
1196
+ const current = table.getState().columnPinning || { left: [], right: [] };
1197
+ const next: ColumnPinningState = {
1198
+ left: (current.left || []).filter((id) => id !== columnId),
1199
+ right: (current.right || []).filter((id) => id !== columnId),
1200
+ };
1201
+ applyPinning(next);
1202
+ },
1203
+ setPinning: (pinning: ColumnPinningState) => {
1204
+ applyPinning(pinning);
1205
+ },
1206
+ resetColumnPinning: () => {
1207
+ const initialPinning = initialStateConfig.columnPinning || { left: [], right: [] };
1208
+ applyPinning(initialPinning);
1209
+ },
1210
+ },
1211
+
1212
+ // -------------------------------
1213
+ // Column Resizing
1214
+ // -------------------------------
1215
+ columnResizing: {
1216
+ resizeColumn: (columnId: string, width: number) => {
1217
+ const currentSizing = table.getState().columnSizing || {};
1218
+ applySizing({ ...currentSizing, [columnId]: width });
1219
+ },
1220
+ autoSizeColumn: (columnId: string) => {
1221
+ // safe to call tanstack helper; it will feed into onColumnSizingChange if wired,
1222
+ // but since you're controlled, we still prefer to update through handler:
1223
+ const col = table.getColumn(columnId);
1224
+ if (!col) return;
1225
+
1226
+ col.resetSize();
1227
+ // after resetSize, read state and emit via handler so controlled stays synced
1228
+ applySizing({ ...(table.getState().columnSizing || {}) });
1229
+ },
1230
+ autoSizeAllColumns: () => {
1231
+ const initialSizing = initialStateConfig.columnSizing || {};
1232
+ applySizing(initialSizing);
1233
+ },
1234
+ resetColumnSizing: () => {
1235
+ const initialSizing = initialStateConfig.columnSizing || {};
1236
+ applySizing(initialSizing);
1237
+ },
1238
+ },
1239
+
1240
+ // -------------------------------
1241
+ // Filtering
1242
+ // -------------------------------
1243
+ filtering: {
1244
+ setGlobalFilter: (filter: string) => {
1245
+ applyGlobalFilter(filter);
1246
+ },
1247
+ clearGlobalFilter: () => {
1248
+ applyGlobalFilter("");
1249
+ },
1250
+ setColumnFilters: (filters: ColumnFilterState) => {
1251
+ handleColumnFilterStateChange(filters);
1252
+ },
1253
+ addColumnFilter: (columnId: string, operator: string, value: any) => {
1254
+ const newFilter = {
1255
+ id: `filter_${Date.now()}`,
1256
+ columnId,
1257
+ operator,
1258
+ value,
1272
1259
  };
1273
- setServerData?.(newData);
1274
- }
1275
- },
1276
1260
 
1277
- // Data queries
1278
- findRows: (predicate: (row: T) => boolean) => {
1279
- return (table.getRowModel().rows || [])?.filter(row => predicate(row.original))?.map(row => row.original);
1280
- },
1281
- findRowIndex: (predicate: (row: T) => boolean) => {
1282
- return (table.getRowModel().rows || [])?.findIndex(row => predicate(row.original));
1283
- },
1284
- getDataCount: () => {
1285
- return (table.getRowModel().rows || [])?.length || 0;
1286
- },
1287
- getFilteredDataCount: () => {
1288
- return table.getFilteredRowModel().rows.length;
1289
- },
1290
- },
1261
+ const current = table.getState().columnFilter;
1262
+ const currentFilters = current?.filters || [];
1263
+ const nextFilters = [...currentFilters, newFilter];
1291
1264
 
1292
- // Layout Management
1293
- layout: {
1294
- resetLayout: () => {
1295
- table.resetColumnSizing();
1296
- table.resetColumnVisibility();
1297
- table.resetSorting();
1298
- table.resetGlobalFilter();
1299
- },
1300
- resetAll: () => {
1301
- // Reset everything to initial state
1302
- table.resetColumnSizing();
1303
- table.resetColumnVisibility();
1304
- table.resetSorting();
1305
- table.resetGlobalFilter();
1306
- table.resetColumnOrder();
1307
- table.resetExpanded();
1308
- handleSelectionStateChange(initialSelectionState);
1309
- table.resetColumnPinning();
1310
-
1311
- handleColumnFilterStateChange(initialStateConfig.columnFilter || { filters: [], logic: 'AND', pendingFilters: [], pendingLogic: 'AND' });
1312
-
1313
- if (enablePagination) {
1314
- table.setPagination(initialStateConfig.pagination || { pageIndex: 0, pageSize: 10 });
1315
- }
1265
+ handleColumnFilterStateChange({
1266
+ filters: nextFilters,
1267
+ logic: current?.logic,
1268
+ pendingFilters: current?.pendingFilters || [],
1269
+ pendingLogic: current?.pendingLogic || "AND",
1270
+ });
1316
1271
 
1317
- if (enableColumnPinning) {
1318
- table.setColumnPinning(initialStateConfig.columnPinning || { left: [], right: [] });
1319
- }
1320
- },
1321
- saveLayout: () => {
1322
- return {
1272
+ if (logger.isLevelEnabled("debug")) {
1273
+ logger.debug(`Adding column filter ${columnId} ${operator} ${value}`, nextFilters);
1274
+ }
1275
+ },
1276
+ removeColumnFilter: (filterId: string) => {
1277
+ const current = table.getState().columnFilter;
1278
+ const currentFilters = current?.filters || [];
1279
+ const nextFilters = currentFilters.filter((f: any) => f.id !== filterId);
1280
+
1281
+ handleColumnFilterStateChange({
1282
+ filters: nextFilters,
1283
+ logic: current?.logic,
1284
+ pendingFilters: current?.pendingFilters || [],
1285
+ pendingLogic: current?.pendingLogic || "AND",
1286
+ });
1287
+
1288
+ if (logger.isLevelEnabled("debug")) {
1289
+ logger.debug(`Removing column filter ${filterId}`, nextFilters);
1290
+ }
1291
+ },
1292
+ clearAllFilters: () => {
1293
+ applyGlobalFilter("");
1294
+ handleColumnFilterStateChange({
1295
+ filters: [],
1296
+ logic: "AND",
1297
+ pendingFilters: [],
1298
+ pendingLogic: "AND",
1299
+ });
1300
+ },
1301
+ resetFilters: () => {
1302
+ handleColumnFilterStateChange({
1303
+ filters: [],
1304
+ logic: "AND",
1305
+ pendingFilters: [],
1306
+ pendingLogic: "AND",
1307
+ });
1308
+
1309
+ if (logger.isLevelEnabled("debug")) {
1310
+ logger.debug("Resetting filters");
1311
+ }
1312
+ },
1313
+ },
1314
+
1315
+ // -------------------------------
1316
+ // Sorting
1317
+ // -------------------------------
1318
+ sorting: {
1319
+ setSorting: (sortingState: SortingState) => {
1320
+ applySorting(sortingState);
1321
+ if (logger.isLevelEnabled("debug")) logger.debug("Setting sorting", sortingState);
1322
+ },
1323
+
1324
+ // NOTE: toggleSorting is okay, but can become "one behind" in controlled server mode.
1325
+ // So we implement deterministic sorting through handler.
1326
+ sortColumn: (columnId: string, direction: "asc" | "desc" | false) => {
1327
+ const current = table.getState().sorting || [];
1328
+ const filtered = current.filter((s: any) => s.id !== columnId);
1329
+
1330
+ if (direction === false) {
1331
+ applySorting(filtered);
1332
+ return;
1333
+ }
1334
+
1335
+ applySorting([{ id: columnId, desc: direction === "desc" }, ...filtered]);
1336
+ },
1337
+
1338
+ clearSorting: () => {
1339
+ applySorting([]);
1340
+ },
1341
+ resetSorting: () => {
1342
+ const initialSorting = initialStateConfig.sorting || [];
1343
+ applySorting(initialSorting);
1344
+ },
1345
+ },
1346
+
1347
+ // -------------------------------
1348
+ // Pagination
1349
+ // -------------------------------
1350
+ pagination: {
1351
+ goToPage: (pageIndex: number) => {
1352
+ applyPagination((prev: any) => ({ ...prev, pageIndex }));
1353
+ if (logger.isLevelEnabled("debug")) logger.debug(`Going to page ${pageIndex}`);
1354
+ },
1355
+ nextPage: () => {
1356
+ applyPagination((prev: any) => ({ ...prev, pageIndex: (prev?.pageIndex ?? 0) + 1 }));
1357
+ if (logger.isLevelEnabled("debug")) logger.debug("Next page");
1358
+ },
1359
+ previousPage: () => {
1360
+ applyPagination((prev: any) => ({ ...prev, pageIndex: Math.max(0, (prev?.pageIndex ?? 0) - 1) }));
1361
+ if (logger.isLevelEnabled("debug")) logger.debug("Previous page");
1362
+ },
1363
+ setPageSize: (pageSize: number) => {
1364
+ // usually want pageIndex reset
1365
+ applyPagination(() => ({ pageIndex: 0, pageSize }));
1366
+ if (logger.isLevelEnabled("debug")) logger.debug(`Setting page size to ${pageSize}`);
1367
+ },
1368
+ goToFirstPage: () => {
1369
+ applyPagination((prev: any) => ({ ...prev, pageIndex: 0 }));
1370
+ if (logger.isLevelEnabled("debug")) logger.debug("Going to first page");
1371
+ },
1372
+ goToLastPage: () => {
1373
+ // pageCount can be derived; keep safe fallback
1374
+ const pageCount = table.getPageCount?.() ?? 0;
1375
+ if (pageCount > 0) {
1376
+ applyPagination((prev: any) => ({ ...prev, pageIndex: pageCount - 1 }));
1377
+ if (logger.isLevelEnabled("debug")) logger.debug(`Going to last page ${pageCount - 1}`);
1378
+ }
1379
+ },
1380
+ resetPagination: () => {
1381
+ const initialPagination = initialStateConfig.pagination || { pageIndex: 0, pageSize: 10 };
1382
+ applyPagination(initialPagination);
1383
+ },
1384
+ },
1385
+
1386
+ // -------------------------------
1387
+ // Selection
1388
+ // -------------------------------
1389
+ selection: {
1390
+ selectRow: (rowId: string) => table.selectRow?.(rowId),
1391
+ deselectRow: (rowId: string) => table.deselectRow?.(rowId),
1392
+ toggleRowSelection: (rowId: string) => table.toggleRowSelected?.(rowId),
1393
+ selectAll: () => table.selectAll?.(),
1394
+ deselectAll: () => table.deselectAll?.(),
1395
+ toggleSelectAll: () => table.toggleAllRowsSelected?.(),
1396
+ getSelectionState: () => table.getSelectionState?.() || ({ ids: [], type: "include" } as const),
1397
+ getSelectedRows: () => table.getSelectedRows(),
1398
+ getSelectedCount: () => table.getSelectedCount(),
1399
+ isRowSelected: (rowId: string) => table.getIsRowSelected(rowId) || false,
1400
+ },
1401
+
1402
+ // -------------------------------
1403
+ // Data Management (kept same, but ensure state changes go through handlers)
1404
+ // -------------------------------
1405
+ data: {
1406
+ refresh: (options?: boolean | DataRefreshOptions) => {
1407
+ void triggerRefresh(options, 'refresh');
1408
+ },
1409
+
1410
+ reload: (options: DataRefreshOptions = {}) => {
1411
+ void triggerRefresh(
1412
+ {
1413
+ ...options,
1414
+ resetPagination: options.resetPagination ?? false,
1415
+ reason: options.reason ?? 'reload',
1416
+ },
1417
+ 'reload'
1418
+ );
1419
+ },
1420
+
1421
+ resetAll: () => resetAllAndReload(),
1422
+
1423
+ getAllData: () => [...tableData],
1424
+ getRowData: (rowId: string) => {
1425
+ const rowIndex = getRowIndexById(tableData, rowId);
1426
+ return rowIndex === -1 ? undefined : tableData[rowIndex];
1427
+ },
1428
+ getRowByIndex: (index: number) => tableData[index],
1429
+
1430
+ updateRow: (rowId: string, updates: Partial<T>) => {
1431
+ applyDataMutation('updateRow', (rowsToMutate) => {
1432
+ const rowIndex = getRowIndexById(rowsToMutate, rowId);
1433
+ if (rowIndex === -1) return rowsToMutate;
1434
+ const nextData = [...rowsToMutate];
1435
+ nextData[rowIndex] = { ...nextData[rowIndex], ...updates };
1436
+ return nextData;
1437
+ }, { rowId });
1438
+ },
1439
+
1440
+ updateRowByIndex: (index: number, updates: Partial<T>) => {
1441
+ applyDataMutation('updateRowByIndex', (rowsToMutate) => {
1442
+ if (!rowsToMutate[index]) return rowsToMutate;
1443
+ const nextData = [...rowsToMutate];
1444
+ nextData[index] = { ...nextData[index], ...updates };
1445
+ return nextData;
1446
+ }, { index });
1447
+ },
1448
+
1449
+ insertRow: (newRow: T, index?: number) => {
1450
+ applyDataMutation('insertRow', (rowsToMutate) => {
1451
+ const nextData = [...rowsToMutate];
1452
+ nextData.splice(clampInsertIndex(nextData, index), 0, newRow);
1453
+ return nextData;
1454
+ }, { index });
1455
+ },
1456
+
1457
+ deleteRow: (rowId: string) => {
1458
+ applyDataMutation('deleteRow', (rowsToMutate) => {
1459
+ const rowIndex = getRowIndexById(rowsToMutate, rowId);
1460
+ if (rowIndex === -1) return rowsToMutate;
1461
+ const nextData = [...rowsToMutate];
1462
+ nextData.splice(rowIndex, 1);
1463
+ return nextData;
1464
+ }, { rowId });
1465
+ },
1466
+
1467
+ deleteRowByIndex: (index: number) => {
1468
+ applyDataMutation('deleteRowByIndex', (rowsToMutate) => {
1469
+ if (index < 0 || index >= rowsToMutate.length) return rowsToMutate;
1470
+ const nextData = [...rowsToMutate];
1471
+ nextData.splice(index, 1);
1472
+ return nextData;
1473
+ }, { index });
1474
+ },
1475
+
1476
+ deleteSelectedRows: () => {
1477
+ const currentSelection = table.getSelectionState?.() || selectionState;
1478
+ const selectedIds = new Set((currentSelection.ids || []).map((id) => String(id)));
1479
+ const loadedRowIds = tableData.map((row, index) => String(generateRowId(row, index, idKey)));
1480
+ const deletableRowIds = currentSelection.type === 'exclude'
1481
+ ? loadedRowIds.filter((rowId) => !selectedIds.has(rowId))
1482
+ : loadedRowIds.filter((rowId) => selectedIds.has(rowId));
1483
+
1484
+ if (deletableRowIds.length === 0) return;
1485
+ if (
1486
+ currentSelection.type === 'exclude'
1487
+ && table.getRowCount() > loadedRowIds.length
1488
+ && logger.isLevelEnabled('info')
1489
+ ) {
1490
+ logger.info('deleteSelectedRows in exclude mode removed currently loaded rows only', {
1491
+ removedRows: deletableRowIds.length,
1492
+ totalSelected: table.getSelectedCount?.(),
1493
+ });
1494
+ }
1495
+
1496
+ const deletableRowIdSet = new Set(deletableRowIds);
1497
+ applyDataMutation(
1498
+ 'deleteSelectedRows',
1499
+ (rowsToMutate) =>
1500
+ rowsToMutate.filter((row, index) => !deletableRowIdSet.has(String(generateRowId(row, index, idKey)))),
1501
+ { rowIds: deletableRowIds }
1502
+ );
1503
+ table.deselectAll?.();
1504
+ },
1505
+
1506
+ replaceAllData: (newData: T[]) => {
1507
+ applyDataMutation('replaceAllData', () => [...newData]);
1508
+ },
1509
+
1510
+ updateMultipleRows: (updates: Array<{ rowId: string; data: Partial<T> }>) => {
1511
+ const updateMap = new Map(updates.map((update) => [update.rowId, update.data]));
1512
+ applyDataMutation('updateMultipleRows', (rowsToMutate) =>
1513
+ rowsToMutate.map((row, index) => {
1514
+ const currentRowId = String(generateRowId(row, index, idKey));
1515
+ const updateData = updateMap.get(currentRowId);
1516
+ return updateData ? { ...row, ...updateData } : row;
1517
+ })
1518
+ );
1519
+ },
1520
+
1521
+ insertMultipleRows: (newRows: T[], startIndex?: number) => {
1522
+ applyDataMutation('insertMultipleRows', (rowsToMutate) => {
1523
+ const nextData = [...rowsToMutate];
1524
+ nextData.splice(clampInsertIndex(nextData, startIndex), 0, ...newRows);
1525
+ return nextData;
1526
+ }, { index: startIndex });
1527
+ },
1528
+
1529
+ deleteMultipleRows: (rowIds: string[]) => {
1530
+ const idsToDelete = new Set(rowIds);
1531
+ applyDataMutation(
1532
+ 'deleteMultipleRows',
1533
+ (rowsToMutate) =>
1534
+ rowsToMutate.filter((row, index) => !idsToDelete.has(String(generateRowId(row, index, idKey)))),
1535
+ { rowIds }
1536
+ );
1537
+ },
1538
+
1539
+ updateField: (rowId: string, fieldName: keyof T, value: any) => {
1540
+ applyDataMutation('updateField', (rowsToMutate) => {
1541
+ const rowIndex = getRowIndexById(rowsToMutate, rowId);
1542
+ if (rowIndex === -1) return rowsToMutate;
1543
+ const nextData = [...rowsToMutate];
1544
+ nextData[rowIndex] = { ...nextData[rowIndex], [fieldName]: value };
1545
+ return nextData;
1546
+ }, { rowId });
1547
+ },
1548
+
1549
+ updateFieldByIndex: (index: number, fieldName: keyof T, value: any) => {
1550
+ applyDataMutation('updateFieldByIndex', (rowsToMutate) => {
1551
+ if (!rowsToMutate[index]) return rowsToMutate;
1552
+ const nextData = [...rowsToMutate];
1553
+ nextData[index] = { ...nextData[index], [fieldName]: value };
1554
+ return nextData;
1555
+ }, { index });
1556
+ },
1557
+
1558
+ findRows: (predicate: (row: T) => boolean) => tableData.filter(predicate),
1559
+
1560
+ findRowIndex: (predicate: (row: T) => boolean) => tableData.findIndex(predicate),
1561
+
1562
+ getDataCount: () => tableData.length,
1563
+ getFilteredDataCount: () => table.getFilteredRowModel().rows.length,
1564
+ },
1565
+
1566
+ // -------------------------------
1567
+ // Layout Management
1568
+ // -------------------------------
1569
+ layout: {
1570
+ resetLayout: () => {
1571
+ // go through handlers so controlled state updates + emit works
1572
+ applySizing(initialStateConfig.columnSizing || {});
1573
+ applyVisibility(initialStateConfig.columnVisibility || {});
1574
+ applySorting(initialStateConfig.sorting || []);
1575
+ applyGlobalFilter(initialStateConfig.globalFilter ?? "");
1576
+ },
1577
+ resetAll: () => resetAllAndReload(),
1578
+ saveLayout: () => ({
1323
1579
  columnVisibility: table.getState().columnVisibility,
1324
1580
  columnSizing: table.getState().columnSizing,
1325
1581
  columnOrder: table.getState().columnOrder,
@@ -1328,251 +1584,337 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1328
1584
  pagination: table.getState().pagination,
1329
1585
  globalFilter: table.getState().globalFilter,
1330
1586
  columnFilter: table.getState().columnFilter,
1331
- };
1332
- },
1333
- restoreLayout: (layout: Partial<TableState>) => {
1334
- if (layout.columnVisibility) {
1335
- table.setColumnVisibility(layout.columnVisibility);
1336
- }
1337
- if (layout.columnSizing) {
1338
- table.setColumnSizing(layout.columnSizing);
1339
- }
1340
- if (layout.columnOrder) {
1341
- table.setColumnOrder(layout.columnOrder);
1342
- }
1343
- if (layout.columnPinning) {
1344
- table.setColumnPinning(layout.columnPinning);
1345
- }
1346
- if (layout.sorting) {
1347
- table.setSorting(layout.sorting);
1348
- }
1349
- if (layout.pagination && enablePagination) {
1350
- table.setPagination(layout.pagination);
1351
- }
1352
- if (layout.globalFilter !== undefined) {
1353
- table.setGlobalFilter(layout.globalFilter);
1354
- }
1355
- if (layout.columnFilter) {
1356
- handleColumnFilterStateChange(layout.columnFilter);
1357
- }
1358
- },
1359
- },
1360
-
1361
- // Table State
1362
- state: {
1363
- getTableState: () => {
1364
- return table.getState();
1365
- },
1366
- getCurrentFilters: () => {
1367
- return table.getState().columnFilter;
1368
- },
1369
- getCurrentSorting: () => {
1370
- return table.getState().sorting;
1371
- },
1372
- getCurrentPagination: () => {
1373
- return table.getState().pagination;
1374
- },
1375
- // Backward compatibility: expose the raw selection array expected by older consumers
1376
- getCurrentSelection: () => {
1377
- return table.getSelectionState?.();
1378
- },
1379
- },
1380
-
1381
- // Simplified Export
1382
- export: {
1383
- exportCSV: async (options: any = {}) => {
1384
- const { filename = exportFilename, } = options;
1385
-
1386
- try {
1387
- // Create abort controller for this export
1388
- const controller = new AbortController();
1389
- setExportController?.(controller);
1390
-
1391
- if (dataMode === 'server' && onServerExport) {
1392
- // Server export with selection data
1393
- const currentFilters = {
1394
- globalFilter: table.getState().globalFilter,
1395
- columnFilter: table.getState().columnFilter,
1396
- sorting: table.getState().sorting,
1397
- pagination: table.getState().pagination,
1398
- };
1399
- if (logger.isLevelEnabled('debug')) {
1400
- logger.debug('Server export CSV', { currentFilters });
1401
- }
1402
- await exportServerData(table, {
1403
- format: 'csv',
1404
- filename,
1405
- fetchData: (filters, selection) => onServerExport(filters, selection),
1406
- currentFilters,
1407
- selection: table.getSelectionState?.(),
1408
- onProgress: onExportProgress,
1409
- onComplete: onExportComplete,
1410
- onError: onExportError,
1411
- });
1412
- } else {
1413
- // Client export - auto-detect selected rows if not specified
1414
- await exportClientData(table, {
1415
- format: 'csv',
1416
- filename,
1417
- onProgress: onExportProgress,
1418
- onComplete: onExportComplete,
1419
- onError: onExportError,
1420
- });
1421
- if (logger.isLevelEnabled('debug')) {
1422
- logger.debug('Client export CSV', filename);
1587
+ }),
1588
+ restoreLayout: (layout: Partial<TableState>) => {
1589
+ if (layout.columnVisibility) applyVisibility(layout.columnVisibility as any);
1590
+ if (layout.columnSizing) applySizing(layout.columnSizing as any);
1591
+ if (layout.columnOrder) applyColumnOrder(layout.columnOrder as any);
1592
+ if (layout.columnPinning) applyPinning(layout.columnPinning as any);
1593
+ if (layout.sorting) applySorting(layout.sorting as any);
1594
+ if (layout.pagination && enablePagination) applyPagination(layout.pagination as any);
1595
+ if (layout.globalFilter !== undefined) applyGlobalFilter(layout.globalFilter);
1596
+ if (layout.columnFilter) handleColumnFilterStateChange(layout.columnFilter as any);
1597
+ },
1598
+ },
1599
+
1600
+ // -------------------------------
1601
+ // Table State
1602
+ // -------------------------------
1603
+ state: {
1604
+ getTableState: () => table.getState(),
1605
+ getCurrentFilters: () => table.getState().columnFilter,
1606
+ getCurrentSorting: () => table.getState().sorting,
1607
+ getCurrentPagination: () => table.getState().pagination,
1608
+ getCurrentSelection: () => table.getSelectionState?.(),
1609
+ },
1610
+
1611
+ // -------------------------------
1612
+ // Export (unchanged mostly)
1613
+ // -------------------------------
1614
+ export: {
1615
+ exportCSV: async (options: DataTableExportApiOptions = {}) => {
1616
+ const {
1617
+ filename = exportFilename,
1618
+ chunkSize = exportChunkSize,
1619
+ strictTotalCheck = exportStrictTotalCheck,
1620
+ sanitizeCSV = exportSanitizeCSV,
1621
+ } = options;
1622
+ const mode: 'client' | 'server' = dataMode === "server" && !!onServerExport ? 'server' : 'client';
1623
+
1624
+ await runExportWithPolicy({
1625
+ format: 'csv',
1626
+ filename,
1627
+ mode,
1628
+ execute: async (controller) => {
1629
+ const toStateChange = (state: {
1630
+ phase: ExportPhase;
1631
+ processedRows?: number;
1632
+ totalRows?: number;
1633
+ percentage?: number;
1634
+ message?: string;
1635
+ code?: string;
1636
+ }) => {
1637
+ const isFinalPhase = state.phase === 'completed' || state.phase === 'cancelled' || state.phase === 'error';
1638
+ handleExportStateChangeInternal({
1639
+ phase: state.phase,
1640
+ mode,
1641
+ format: 'csv',
1642
+ filename,
1643
+ processedRows: state.processedRows,
1644
+ totalRows: state.totalRows,
1645
+ percentage: state.percentage,
1646
+ message: state.message,
1647
+ code: state.code,
1648
+ startedAt: state.phase === 'starting' ? Date.now() : undefined,
1649
+ endedAt: isFinalPhase ? Date.now() : undefined,
1650
+ queueLength: queuedExportCount,
1651
+ });
1652
+ if (state.phase === 'cancelled') {
1653
+ onExportCancel?.();
1654
+ }
1655
+ };
1656
+
1657
+ if (mode === 'server' && onServerExport) {
1658
+ const currentFilters = {
1659
+ globalFilter: table.getState().globalFilter,
1660
+ columnFilter: table.getState().columnFilter,
1661
+ sorting: table.getState().sorting,
1662
+ pagination: table.getState().pagination,
1663
+ };
1664
+
1665
+ if (logger.isLevelEnabled("debug")) logger.debug("Server export CSV", { currentFilters });
1666
+
1667
+ await exportServerData(table, {
1668
+ format: "csv",
1669
+ filename,
1670
+ fetchData: (filters: any, selection: any, signal?: AbortSignal) =>
1671
+ onServerExport(filters, selection, signal),
1672
+ currentFilters,
1673
+ selection: table.getSelectionState?.(),
1674
+ onProgress: handleExportProgressInternal,
1675
+ onComplete: onExportComplete,
1676
+ onError: onExportError,
1677
+ onStateChange: toStateChange,
1678
+ signal: controller.signal,
1679
+ chunkSize,
1680
+ strictTotalCheck,
1681
+ sanitizeCSV,
1682
+ });
1683
+ return;
1684
+ }
1685
+
1686
+ await exportClientData(table, {
1687
+ format: "csv",
1688
+ filename,
1689
+ onProgress: handleExportProgressInternal,
1690
+ onComplete: onExportComplete,
1691
+ onError: onExportError,
1692
+ onStateChange: toStateChange,
1693
+ signal: controller.signal,
1694
+ sanitizeCSV,
1695
+ });
1696
+
1697
+ if (logger.isLevelEnabled("debug")) logger.debug("Client export CSV", filename);
1423
1698
  }
1424
- }
1425
- } catch (error: any) {
1426
- onExportError?.({
1427
- message: error.message || 'Export failed',
1428
- code: 'EXPORT_ERROR',
1429
1699
  });
1430
- } finally {
1431
- setExportController?.(null);
1432
- }
1433
- },
1434
- exportExcel: async (options: any = {}) => {
1435
- const { filename = exportFilename } = options;
1436
-
1437
- try {
1438
- // Create abort controller for this export
1439
- const controller = new AbortController();
1440
- setExportController?.(controller);
1441
-
1442
- if (dataMode === 'server' && onServerExport) {
1443
- // Server export with selection data
1444
- const currentFilters = {
1445
- globalFilter: table.getState().globalFilter,
1446
- columnFilter: table.getState().columnFilter,
1447
- sorting: table.getState().sorting,
1448
- pagination: table.getState().pagination,
1449
- };
1450
-
1451
- if (logger.isLevelEnabled('debug')) {
1452
- logger.debug('Server export Excel', { currentFilters });
1453
- }
1454
- await exportServerData(table, {
1455
- format: 'excel',
1456
- filename,
1457
- fetchData: (filters, selection) => onServerExport(filters, selection),
1458
- currentFilters,
1459
- selection: table.getSelectionState?.(),
1460
- onProgress: onExportProgress,
1461
- onComplete: onExportComplete,
1462
- onError: onExportError,
1463
- });
1464
- } else {
1465
- // Client export - auto-detect selected rows if not specified
1466
- await exportClientData(table, {
1467
- format: 'excel',
1468
- filename,
1469
- onProgress: onExportProgress,
1470
- onComplete: onExportComplete,
1471
- onError: onExportError,
1472
- });
1473
- if (logger.isLevelEnabled('debug')) {
1474
- logger.debug('Client export Excel', filename);
1700
+ },
1701
+
1702
+ exportExcel: async (options: DataTableExportApiOptions = {}) => {
1703
+ const {
1704
+ filename = exportFilename,
1705
+ chunkSize = exportChunkSize,
1706
+ strictTotalCheck = exportStrictTotalCheck,
1707
+ sanitizeCSV = exportSanitizeCSV,
1708
+ } = options;
1709
+ const mode: 'client' | 'server' = dataMode === "server" && !!onServerExport ? 'server' : 'client';
1710
+
1711
+ await runExportWithPolicy({
1712
+ format: 'excel',
1713
+ filename,
1714
+ mode,
1715
+ execute: async (controller) => {
1716
+ const toStateChange = (state: {
1717
+ phase: ExportPhase;
1718
+ processedRows?: number;
1719
+ totalRows?: number;
1720
+ percentage?: number;
1721
+ message?: string;
1722
+ code?: string;
1723
+ }) => {
1724
+ const isFinalPhase = state.phase === 'completed' || state.phase === 'cancelled' || state.phase === 'error';
1725
+ handleExportStateChangeInternal({
1726
+ phase: state.phase,
1727
+ mode,
1728
+ format: 'excel',
1729
+ filename,
1730
+ processedRows: state.processedRows,
1731
+ totalRows: state.totalRows,
1732
+ percentage: state.percentage,
1733
+ message: state.message,
1734
+ code: state.code,
1735
+ startedAt: state.phase === 'starting' ? Date.now() : undefined,
1736
+ endedAt: isFinalPhase ? Date.now() : undefined,
1737
+ queueLength: queuedExportCount,
1738
+ });
1739
+ if (state.phase === 'cancelled') {
1740
+ onExportCancel?.();
1741
+ }
1742
+ };
1743
+
1744
+ if (mode === 'server' && onServerExport) {
1745
+ const currentFilters = {
1746
+ globalFilter: table.getState().globalFilter,
1747
+ columnFilter: table.getState().columnFilter,
1748
+ sorting: table.getState().sorting,
1749
+ pagination: table.getState().pagination,
1750
+ };
1751
+
1752
+ if (logger.isLevelEnabled("debug")) logger.debug("Server export Excel", { currentFilters });
1753
+
1754
+ await exportServerData(table, {
1755
+ format: "excel",
1756
+ filename,
1757
+ fetchData: (filters: any, selection: any, signal?: AbortSignal) =>
1758
+ onServerExport(filters, selection, signal),
1759
+ currentFilters,
1760
+ selection: table.getSelectionState?.(),
1761
+ onProgress: handleExportProgressInternal,
1762
+ onComplete: onExportComplete,
1763
+ onError: onExportError,
1764
+ onStateChange: toStateChange,
1765
+ signal: controller.signal,
1766
+ chunkSize,
1767
+ strictTotalCheck,
1768
+ sanitizeCSV,
1769
+ });
1770
+ return;
1771
+ }
1772
+
1773
+ await exportClientData(table, {
1774
+ format: "excel",
1775
+ filename,
1776
+ onProgress: handleExportProgressInternal,
1777
+ onComplete: onExportComplete,
1778
+ onError: onExportError,
1779
+ onStateChange: toStateChange,
1780
+ signal: controller.signal,
1781
+ sanitizeCSV,
1782
+ });
1783
+
1784
+ if (logger.isLevelEnabled("debug")) logger.debug("Client export Excel", filename);
1475
1785
  }
1476
- }
1477
- } catch (error: any) {
1478
- onExportError?.({
1479
- message: error.message || 'Export failed',
1480
- code: 'EXPORT_ERROR',
1481
- });
1482
- if (logger.isLevelEnabled('debug')) {
1483
- logger.debug('Server export Excel failed', error);
1484
- }
1485
- } finally {
1486
- setExportController?.(null);
1487
- }
1488
- },
1489
- exportServerData: async (options) => {
1490
- const {
1491
- format,
1492
- filename = exportFilename,
1493
- fetchData = onServerExport,
1494
- } = options;
1495
-
1496
- if (!fetchData) {
1497
- onExportError?.({
1498
- message: 'No server export function provided',
1499
- code: 'NO_SERVER_EXPORT',
1500
1786
  });
1501
- if (logger.isLevelEnabled('debug')) {
1502
- logger.debug('Server export data failed', 'No server export function provided');
1787
+ },
1788
+
1789
+ exportServerData: async (options: {
1790
+ format: 'csv' | 'excel';
1791
+ filename?: string;
1792
+ fetchData?: (
1793
+ filters?: Partial<TableState>,
1794
+ selection?: SelectionState,
1795
+ signal?: AbortSignal
1796
+ ) => Promise<any>;
1797
+ chunkSize?: number;
1798
+ strictTotalCheck?: boolean;
1799
+ sanitizeCSV?: boolean;
1800
+ }) => {
1801
+ const {
1802
+ format,
1803
+ filename = exportFilename,
1804
+ fetchData: fetchFn = onServerExport,
1805
+ chunkSize = exportChunkSize,
1806
+ strictTotalCheck = exportStrictTotalCheck,
1807
+ sanitizeCSV = exportSanitizeCSV,
1808
+ } = options;
1809
+
1810
+ if (!fetchFn) {
1811
+ onExportError?.({ message: "No server export function provided", code: "NO_SERVER_EXPORT" });
1812
+ if (logger.isLevelEnabled("debug")) logger.debug("Server export data failed", "No server export function provided");
1813
+ return;
1503
1814
  }
1504
- return;
1505
- }
1506
1815
 
1507
- try {
1508
- // Create abort controller for this export
1509
- const controller = new AbortController();
1510
- setExportController?.(controller);
1511
-
1512
- const currentFilters = {
1513
- globalFilter: table.getState().globalFilter,
1514
- columnFilter: table.getState().columnFilter,
1515
- sorting: table.getState().sorting,
1516
- pagination: table.getState().pagination,
1517
- };
1518
- if (logger.isLevelEnabled('debug')) {
1519
- logger.debug('Server export data', { currentFilters });
1520
- }
1521
- await exportServerData(table, {
1816
+ await runExportWithPolicy({
1522
1817
  format,
1523
1818
  filename,
1524
- fetchData: (filters, selection) => fetchData(filters, selection),
1525
- currentFilters,
1526
- selection: table.getSelectionState?.(),
1527
- onProgress: onExportProgress,
1528
- onComplete: onExportComplete,
1529
- onError: onExportError,
1530
- });
1531
- } catch (error: any) {
1532
- onExportError?.({
1533
- message: error.message || 'Export failed',
1534
- code: 'EXPORT_ERROR',
1819
+ mode: 'server',
1820
+ execute: async (controller) => {
1821
+ const currentFilters = {
1822
+ globalFilter: table.getState().globalFilter,
1823
+ columnFilter: table.getState().columnFilter,
1824
+ sorting: table.getState().sorting,
1825
+ pagination: table.getState().pagination,
1826
+ };
1827
+
1828
+ if (logger.isLevelEnabled("debug")) logger.debug("Server export data", { currentFilters });
1829
+
1830
+ await exportServerData(table, {
1831
+ format,
1832
+ filename,
1833
+ fetchData: (filters: any, selection: any, signal?: AbortSignal) =>
1834
+ fetchFn(filters, selection, signal),
1835
+ currentFilters,
1836
+ selection: table.getSelectionState?.(),
1837
+ onProgress: handleExportProgressInternal,
1838
+ onComplete: onExportComplete,
1839
+ onError: onExportError,
1840
+ onStateChange: (state) => {
1841
+ const isFinalPhase = state.phase === 'completed' || state.phase === 'cancelled' || state.phase === 'error';
1842
+ handleExportStateChangeInternal({
1843
+ phase: state.phase,
1844
+ mode: 'server',
1845
+ format,
1846
+ filename,
1847
+ processedRows: state.processedRows,
1848
+ totalRows: state.totalRows,
1849
+ percentage: state.percentage,
1850
+ message: state.message,
1851
+ code: state.code,
1852
+ startedAt: state.phase === 'starting' ? Date.now() : undefined,
1853
+ endedAt: isFinalPhase ? Date.now() : undefined,
1854
+ queueLength: queuedExportCount,
1855
+ });
1856
+ if (state.phase === 'cancelled') {
1857
+ onExportCancel?.();
1858
+ }
1859
+ },
1860
+ signal: controller.signal,
1861
+ chunkSize,
1862
+ strictTotalCheck,
1863
+ sanitizeCSV,
1864
+ });
1865
+ }
1535
1866
  });
1536
- if (logger.isLevelEnabled('debug')) {
1537
- logger.debug('Server export data failed', error);
1867
+ },
1868
+
1869
+ isExporting: () => isExporting || false,
1870
+ cancelExport: () => {
1871
+ const activeController = exportControllerRef.current;
1872
+ if (!activeController) {
1873
+ return;
1538
1874
  }
1539
- } finally {
1540
- setExportController?.(null);
1541
- }
1875
+ activeController.abort();
1876
+ setExportControllerSafely((current) => (current === activeController ? null : current));
1877
+ if (logger.isLevelEnabled("debug")) logger.debug("Export cancelled");
1878
+ },
1542
1879
  },
1543
- // Export state
1544
- isExporting: () => isExporting || false,
1545
- cancelExport: () => {
1546
- exportController?.abort();
1547
- setExportController?.(null);
1548
- if (logger.isLevelEnabled('debug')) {
1549
- logger.debug('Export cancelled');
1550
- }
1551
- },
1552
- },
1880
+ };
1553
1881
  // eslint-disable-next-line react-hooks/exhaustive-deps
1554
- }), [
1882
+ }, [
1555
1883
  table,
1556
1884
  enhancedColumns,
1885
+ handleColumnOrderChange,
1886
+ handleColumnPinningChange,
1887
+ handleColumnVisibilityChange,
1888
+ handleColumnSizingChange,
1889
+ handlePaginationChange,
1890
+ handleSortingChange,
1891
+ handleGlobalFilterChange,
1557
1892
  handleColumnFilterStateChange,
1558
- idKey,
1559
- onDataStateChange,
1560
- fetchData,
1561
- enableColumnPinning,
1893
+ initialStateConfig,
1562
1894
  enablePagination,
1563
- // Export dependencies
1895
+ idKey,
1896
+ triggerRefresh,
1897
+ applyDataMutation,
1898
+ tableData,
1899
+ selectionState,
1900
+ // export
1564
1901
  exportFilename,
1565
- onExportProgress,
1902
+ exportChunkSize,
1903
+ exportStrictTotalCheck,
1904
+ exportSanitizeCSV,
1566
1905
  onExportComplete,
1567
1906
  onExportError,
1907
+ onExportCancel,
1568
1908
  onServerExport,
1569
- exportController,
1570
- setExportController,
1909
+ queuedExportCount,
1571
1910
  isExporting,
1572
1911
  dataMode,
1573
- selectMode,
1574
- onSelectionChange,
1575
- // Note: custom selection removed from dependency array
1912
+ handleExportProgressInternal,
1913
+ handleExportStateChangeInternal,
1914
+ runExportWithPolicy,
1915
+ setExportControllerSafely,
1916
+ logger,
1917
+ resetAllAndReload,
1576
1918
  ]);
1577
1919
 
1578
1920
  internalApiRef.current = dataTableApi;
@@ -1710,14 +2052,12 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1710
2052
  // Export cancel callback
1711
2053
  // -------------------------------
1712
2054
  const handleCancelExport = useCallback(() => {
1713
- if (exportController) {
1714
- exportController.abort();
1715
- setExportController(null);
1716
- if (onExportCancel) {
1717
- onExportCancel();
1718
- }
2055
+ const activeController = exportControllerRef.current;
2056
+ if (activeController) {
2057
+ activeController.abort();
2058
+ setExportControllerSafely((current) => (current === activeController ? null : current));
1719
2059
  }
1720
- }, [exportController, onExportCancel]);
2060
+ }, [setExportControllerSafely]);
1721
2061
 
1722
2062
  // -------------------------------
1723
2063
  // Slot components
@@ -1797,6 +2137,8 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1797
2137
  slotProps={slotProps}
1798
2138
  isExporting={isExporting}
1799
2139
  exportController={exportController}
2140
+ exportPhase={exportPhase}
2141
+ exportProgress={exportProgress}
1800
2142
  onCancelExport={handleCancelExport}
1801
2143
  exportFilename={exportFilename}
1802
2144
  onExportProgress={onExportProgress}
@@ -1818,7 +2160,15 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1818
2160
  enableReset={enableReset}
1819
2161
  enableTableSizeControl={enableTableSizeControl}
1820
2162
  enableColumnPinning={enableColumnPinning}
2163
+ enableRefresh={enableRefresh}
1821
2164
  {...toolbarSlotProps}
2165
+ refreshButtonProps={{
2166
+ loading: tableLoading, // disable while fetching
2167
+ showSpinnerWhileLoading: false,
2168
+ onRefresh: () => internalApiRef.current?.data?.refresh?.(true),
2169
+ ...toolbarSlotProps.refreshButtonProps,
2170
+ }}
2171
+
1822
2172
  />
1823
2173
  ) : null}
1824
2174