@ackplus/react-tanstack-data-table 1.1.12 → 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 (51) 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/table-export-control.d.ts.map +1 -1
  8. package/dist/lib/components/toolbar/table-export-control.js +46 -12
  9. package/dist/lib/contexts/data-table-context.d.ts +7 -10
  10. package/dist/lib/contexts/data-table-context.d.ts.map +1 -1
  11. package/dist/lib/contexts/data-table-context.js +5 -1
  12. package/dist/lib/data-table.d.ts.map +1 -1
  13. package/dist/lib/data-table.js +561 -230
  14. package/dist/lib/features/column-filter.feature.js +38 -21
  15. package/dist/lib/features/selection.feature.d.ts.map +1 -1
  16. package/dist/lib/features/selection.feature.js +11 -3
  17. package/dist/lib/types/column.types.d.ts +19 -0
  18. package/dist/lib/types/column.types.d.ts.map +1 -1
  19. package/dist/lib/types/data-table-api.d.ts +24 -18
  20. package/dist/lib/types/data-table-api.d.ts.map +1 -1
  21. package/dist/lib/types/data-table.types.d.ts +36 -10
  22. package/dist/lib/types/data-table.types.d.ts.map +1 -1
  23. package/dist/lib/types/export.types.d.ts +57 -13
  24. package/dist/lib/types/export.types.d.ts.map +1 -1
  25. package/dist/lib/types/slots.types.d.ts +3 -1
  26. package/dist/lib/types/slots.types.d.ts.map +1 -1
  27. package/dist/lib/types/table.types.d.ts +1 -3
  28. package/dist/lib/types/table.types.d.ts.map +1 -1
  29. package/dist/lib/utils/debounced-fetch.utils.d.ts +8 -4
  30. package/dist/lib/utils/debounced-fetch.utils.d.ts.map +1 -1
  31. package/dist/lib/utils/debounced-fetch.utils.js +63 -14
  32. package/dist/lib/utils/export-utils.d.ts +14 -4
  33. package/dist/lib/utils/export-utils.d.ts.map +1 -1
  34. package/dist/lib/utils/export-utils.js +362 -66
  35. package/package.json +4 -2
  36. package/src/lib/components/droupdown/menu-dropdown.tsx +9 -3
  37. package/src/lib/components/filters/filter-value-input.tsx +2 -2
  38. package/src/lib/components/pagination/data-table-pagination.tsx +14 -2
  39. package/src/lib/components/toolbar/table-export-control.tsx +65 -9
  40. package/src/lib/contexts/data-table-context.tsx +16 -2
  41. package/src/lib/data-table.tsx +703 -222
  42. package/src/lib/features/column-filter.feature.ts +40 -19
  43. package/src/lib/features/selection.feature.ts +11 -5
  44. package/src/lib/types/column.types.ts +20 -1
  45. package/src/lib/types/data-table-api.ts +33 -15
  46. package/src/lib/types/data-table.types.ts +58 -3
  47. package/src/lib/types/export.types.ts +79 -10
  48. package/src/lib/types/slots.types.ts +3 -1
  49. package/src/lib/types/table.types.ts +1 -3
  50. package/src/lib/utils/debounced-fetch.utils.ts +90 -18
  51. package/src/lib/utils/export-utils.ts +496 -69
@@ -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,
@@ -252,16 +266,32 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
252
266
  const [serverData, setServerData] = useState<T[] | null>(null);
253
267
  const [serverTotal, setServerTotal] = useState(0);
254
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);
255
272
 
256
273
  // -------------------------------
257
274
  // Ref hooks (grouped together)
258
275
  // -------------------------------
259
276
  const tableContainerRef = useRef<HTMLDivElement>(null);
260
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
+ );
261
285
 
262
286
  const { debouncedFetch, isLoading: fetchLoading } = useDebouncedFetch(onFetchData);
263
- const tableData = useMemo(() => serverData ? serverData : data, [serverData, data]);
264
- 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
+ );
265
295
  const tableLoading = useMemo(() => onFetchData ? (loading || fetchLoading) : loading, [onFetchData, loading, fetchLoading]);
266
296
 
267
297
 
@@ -309,7 +339,10 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
309
339
  // -------------------------------
310
340
  // Callback hooks (grouped together)
311
341
  // -------------------------------
312
- 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
+ ) => {
313
346
  if (!onFetchData) {
314
347
  if (logger.isLevelEnabled('debug')) {
315
348
  logger.debug('onFetchData not provided, skipping fetch', { overrides, columnFilter, sorting, pagination });
@@ -317,7 +350,7 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
317
350
  return;
318
351
  }
319
352
 
320
- const filters: TableFiltersForFetch = {
353
+ const filters: Partial<TableFiltersForFetch> = {
321
354
  globalFilter,
322
355
  pagination,
323
356
  columnFilter,
@@ -325,15 +358,20 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
325
358
  ...overrides,
326
359
  };
327
360
 
328
- console.log('Fetching data', filters);
329
-
330
361
  if (logger.isLevelEnabled('info')) {
331
- logger.info('Requesting data', { filters });
362
+ logger.info('Requesting data', {
363
+ filters,
364
+ reason: options?.meta?.reason,
365
+ force: options?.meta?.force,
366
+ });
332
367
  }
333
368
 
334
369
  try {
335
370
  const delay = options?.delay ?? 300; // respects 0
336
- const result = await debouncedFetch(filters, delay);
371
+ const result = await debouncedFetch(filters, {
372
+ debounceDelay: delay,
373
+ meta: options?.meta,
374
+ });
337
375
 
338
376
  if (logger.isLevelEnabled('info')) {
339
377
  logger.info('Fetch resolved', {
@@ -342,7 +380,7 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
342
380
  });
343
381
  }
344
382
 
345
- if (result?.data && result?.total !== undefined) {
383
+ if (result && Array.isArray(result.data) && result.total !== undefined) {
346
384
  setServerData(result.data);
347
385
  setServerTotal(result.total);
348
386
  } else if (logger.isLevelEnabled('warn')) {
@@ -364,6 +402,25 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
364
402
  logger,
365
403
  ]);
366
404
 
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
+ };
415
+ }
416
+
417
+ return {
418
+ resetPagination: options?.resetPagination ?? false,
419
+ force: options?.force ?? false,
420
+ reason: options?.reason ?? fallbackReason,
421
+ };
422
+ }, []);
423
+
367
424
 
368
425
  const handleSelectionStateChange = useCallback((updaterOrValue) => {
369
426
  setSelectionState((prevState) => {
@@ -634,12 +691,21 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
634
691
  // -------------------------------
635
692
  // Effects (after callbacks)
636
693
  // -------------------------------
694
+ useEffect(() => {
695
+ if (!isExternallyControlledData || serverData === null) return;
696
+ setServerData(null);
697
+ setServerTotal(0);
698
+ }, [isExternallyControlledData, serverData]);
699
+
637
700
  useEffect(() => {
638
701
  if (initialLoadData && onFetchData) {
639
702
  if (logger.isLevelEnabled('info')) {
640
703
  logger.info('Initial data load triggered', { initialLoadData });
641
704
  }
642
- fetchData({});
705
+ fetchData({}, {
706
+ delay: 0,
707
+ meta: { reason: 'initial' },
708
+ });
643
709
  } else if (logger.isLevelEnabled('debug')) {
644
710
  logger.debug('Skipping initial data load', {
645
711
  initialLoadData,
@@ -670,13 +736,14 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
670
736
  if (!onDataStateChange) return;
671
737
 
672
738
  const live = table.getState();
739
+ const liveColumnFilter = live.columnFilter;
673
740
 
674
741
  // only keep what you persist/store
675
742
  const payload = {
676
743
  sorting: live.sorting,
677
744
  pagination: live.pagination,
678
745
  globalFilter: live.globalFilter,
679
- columnFilter: live.columnFilter,
746
+ columnFilter: liveColumnFilter,
680
747
  columnVisibility: live.columnVisibility,
681
748
  columnSizing: live.columnSizing,
682
749
  columnOrder: live.columnOrder,
@@ -708,8 +775,7 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
708
775
  const getResetState = useCallback((): Partial<TableState> => {
709
776
  const resetSorting = initialStateConfig.sorting || [];
710
777
  const resetGlobalFilter = initialStateConfig.globalFilter ?? '';
711
- const resetColumnFilter =
712
- initialStateConfig.columnFilter || { filters: [], logic: 'AND', pendingFilters: [], pendingLogic: 'AND' };
778
+ const resetColumnFilter = initialStateConfig.columnFilter;
713
779
 
714
780
  const resetPagination = enablePagination
715
781
  ? (initialStateConfig.pagination || { pageIndex: 0, pageSize: 10 })
@@ -723,6 +789,126 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
723
789
  };
724
790
  }, [initialStateConfig, enablePagination]);
725
791
 
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);
799
+
800
+ if (nextData === previousData) return nextData;
801
+
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
+ },
892
+ }
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
+
726
912
  const resetAllAndReload = useCallback(() => {
727
913
  const resetState = getResetState();
728
914
 
@@ -730,7 +916,10 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
730
916
  setGlobalFilter(resetState.globalFilter ?? '');
731
917
  setColumnFilter(resetState.columnFilter as any);
732
918
 
733
- if (resetState.pagination) setPagination(resetState.pagination);
919
+ if (resetState.pagination) {
920
+ setPagination(resetState.pagination);
921
+ onPaginationChange?.(resetState.pagination);
922
+ }
734
923
 
735
924
  setSelectionState(initialSelectionState);
736
925
  setExpanded({});
@@ -741,8 +930,139 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
741
930
  setColumnOrder(initialStateConfig.columnOrder || []);
742
931
  setColumnPinning(initialStateConfig.columnPinning || { left: [], right: [] });
743
932
 
744
- if (onFetchData) fetchData(resetState, { delay: 0 });
745
- }, [getResetState, initialSelectionState, initialStateConfig, onFetchData, fetchData]);
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));
1017
+ }
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;
1050
+ }
1051
+
1052
+ if (exportConcurrency === 'cancelAndRestart') {
1053
+ activeController.abort();
1054
+ }
1055
+ }
1056
+
1057
+ await startExecution();
1058
+ },
1059
+ [
1060
+ exportConcurrency,
1061
+ handleExportStateChangeInternal,
1062
+ onExportError,
1063
+ setExportControllerSafely,
1064
+ ]
1065
+ );
746
1066
 
747
1067
  const dataTableApi = useMemo(() => {
748
1068
  // helpers (avoid repeating boilerplate)
@@ -783,6 +1103,14 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
783
1103
  handleGlobalFilterChange(next);
784
1104
  };
785
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
+
786
1114
  return {
787
1115
  table: {
788
1116
  getTable: () => table,
@@ -1075,146 +1403,163 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1075
1403
  // Data Management (kept same, but ensure state changes go through handlers)
1076
1404
  // -------------------------------
1077
1405
  data: {
1078
- refresh: (resetPagination = false) => {
1079
- const allState = table.getState();
1080
- const current = allState.pagination;
1081
-
1082
- const nextPagination = {
1083
- pageIndex: resetPagination ? 0 : current?.pageIndex ?? 0,
1084
- pageSize: current?.pageSize ?? initialStateConfig.pagination?.pageSize ?? 10,
1085
- };
1086
-
1087
- // must go through handler so server fetch triggers
1088
- applyPagination(nextPagination);
1089
-
1090
- // emit persisted state (your emitTableState effect will also do it)
1091
- onDataStateChange?.({ ...allState, pagination: nextPagination });
1092
-
1093
- fetchData?.({ pagination: nextPagination });
1094
-
1095
- if (logger.isLevelEnabled("debug")) {
1096
- logger.debug("Refreshing data", { nextPagination, allState });
1097
- }
1406
+ refresh: (options?: boolean | DataRefreshOptions) => {
1407
+ void triggerRefresh(options, 'refresh');
1098
1408
  },
1099
1409
 
1100
- reload: () => {
1101
- const allState = table.getState();
1102
- onDataStateChange?.(allState);
1103
- fetchData?.();
1104
- if (logger.isLevelEnabled("debug")) {
1105
- logger.info("Reloading data", allState);
1106
- }
1410
+ reload: (options: DataRefreshOptions = {}) => {
1411
+ void triggerRefresh(
1412
+ {
1413
+ ...options,
1414
+ resetPagination: options.resetPagination ?? false,
1415
+ reason: options.reason ?? 'reload',
1416
+ },
1417
+ 'reload'
1418
+ );
1107
1419
  },
1108
1420
 
1109
- resetAll: () => resetAllAndReload({ resetLayout: true }),
1421
+ resetAll: () => resetAllAndReload(),
1110
1422
 
1111
- getAllData: () => table.getRowModel().rows?.map((row) => row.original) || [],
1112
- getRowData: (rowId: string) =>
1113
- table.getRowModel().rows?.find((row) => String(row.original[idKey]) === rowId)?.original,
1114
- getRowByIndex: (index: number) => table.getRowModel().rows?.[index]?.original,
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],
1115
1429
 
1116
1430
  updateRow: (rowId: string, updates: Partial<T>) => {
1117
- const newData = table.getRowModel().rows?.map((row) =>
1118
- String(row.original[idKey]) === rowId ? { ...row.original, ...updates } : row.original
1119
- );
1120
- setServerData?.(newData || []);
1121
- if (logger.isLevelEnabled("debug")) logger.debug(`Updating row ${rowId}`, updates);
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 });
1122
1438
  },
1123
1439
 
1124
1440
  updateRowByIndex: (index: number, updates: Partial<T>) => {
1125
- const newData = table.getRowModel().rows?.map((row) => row.original) || [];
1126
- if (newData[index]) {
1127
- newData[index] = { ...newData[index]!, ...updates };
1128
- setServerData(newData);
1129
- if (logger.isLevelEnabled("debug")) logger.debug(`Updating row by index ${index}`, updates);
1130
- }
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 });
1131
1447
  },
1132
1448
 
1133
1449
  insertRow: (newRow: T, index?: number) => {
1134
- const newData = table.getRowModel().rows?.map((row) => row.original) || [];
1135
- if (index !== undefined) newData.splice(index, 0, newRow);
1136
- else newData.push(newRow);
1137
- setServerData(newData || []);
1138
- if (logger.isLevelEnabled("debug")) logger.debug("Inserting row", newRow);
1450
+ applyDataMutation('insertRow', (rowsToMutate) => {
1451
+ const nextData = [...rowsToMutate];
1452
+ nextData.splice(clampInsertIndex(nextData, index), 0, newRow);
1453
+ return nextData;
1454
+ }, { index });
1139
1455
  },
1140
1456
 
1141
1457
  deleteRow: (rowId: string) => {
1142
- const newData = (table.getRowModel().rows || []).filter((row) => String(row.original[idKey]) !== rowId);
1143
- setServerData?.(newData.map((r) => r.original) || []);
1144
- if (logger.isLevelEnabled("debug")) logger.debug(`Deleting row ${rowId}`);
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 });
1145
1465
  },
1146
1466
 
1147
1467
  deleteRowByIndex: (index: number) => {
1148
- const newData = (table.getRowModel().rows || []).map((row) => row.original);
1149
- newData.splice(index, 1);
1150
- setServerData(newData);
1151
- if (logger.isLevelEnabled("debug")) logger.debug(`Deleting row by index ${index}`);
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 });
1152
1474
  },
1153
1475
 
1154
1476
  deleteSelectedRows: () => {
1155
- const selectedRows = table.getSelectedRows?.() || [];
1156
- if (selectedRows.length === 0) return;
1157
-
1158
- const selectedIds = new Set(selectedRows.map((row) => String(row.original[idKey])));
1159
- const newData = (table.getRowModel().rows || [])
1160
- .filter((row) => !selectedIds.has(String(row.original[idKey])))
1161
- .map((row) => row.original);
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
+ }
1162
1495
 
1163
- setServerData(newData);
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
+ );
1164
1503
  table.deselectAll?.();
1165
-
1166
- if (logger.isLevelEnabled("debug")) logger.debug("Deleting selected rows");
1167
1504
  },
1168
1505
 
1169
- replaceAllData: (newData: T[]) => setServerData?.(newData),
1506
+ replaceAllData: (newData: T[]) => {
1507
+ applyDataMutation('replaceAllData', () => [...newData]);
1508
+ },
1170
1509
 
1171
1510
  updateMultipleRows: (updates: Array<{ rowId: string; data: Partial<T> }>) => {
1172
- const updateMap = new Map(updates.map((u) => [u.rowId, u.data]));
1173
- const newData = (table.getRowModel().rows || []).map((row) => {
1174
- const rowId = String(row.original[idKey]);
1175
- const updateData = updateMap.get(rowId);
1176
- return updateData ? { ...row.original, ...updateData } : row.original;
1177
- });
1178
- setServerData(newData || []);
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
+ );
1179
1519
  },
1180
1520
 
1181
1521
  insertMultipleRows: (newRows: T[], startIndex?: number) => {
1182
- const newData = (table.getRowModel().rows || []).map((row) => row.original);
1183
- if (startIndex !== undefined) newData.splice(startIndex, 0, ...newRows);
1184
- else newData.push(...newRows);
1185
- setServerData?.(newData);
1522
+ applyDataMutation('insertMultipleRows', (rowsToMutate) => {
1523
+ const nextData = [...rowsToMutate];
1524
+ nextData.splice(clampInsertIndex(nextData, startIndex), 0, ...newRows);
1525
+ return nextData;
1526
+ }, { index: startIndex });
1186
1527
  },
1187
1528
 
1188
1529
  deleteMultipleRows: (rowIds: string[]) => {
1189
1530
  const idsToDelete = new Set(rowIds);
1190
- const newData = (table.getRowModel().rows || [])
1191
- .filter((row) => !idsToDelete.has(String(row.original[idKey])))
1192
- .map((row) => row.original);
1193
- setServerData(newData);
1531
+ applyDataMutation(
1532
+ 'deleteMultipleRows',
1533
+ (rowsToMutate) =>
1534
+ rowsToMutate.filter((row, index) => !idsToDelete.has(String(generateRowId(row, index, idKey)))),
1535
+ { rowIds }
1536
+ );
1194
1537
  },
1195
1538
 
1196
1539
  updateField: (rowId: string, fieldName: keyof T, value: any) => {
1197
- const newData = (table.getRowModel().rows || []).map((row) =>
1198
- String(row.original[idKey]) === rowId ? { ...row.original, [fieldName]: value } : row.original
1199
- );
1200
- setServerData?.(newData);
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 });
1201
1547
  },
1202
1548
 
1203
1549
  updateFieldByIndex: (index: number, fieldName: keyof T, value: any) => {
1204
- const newData = (table.getRowModel().rows || []).map((row) => row.original);
1205
- if (newData[index]) {
1206
- newData[index] = { ...newData[index], [fieldName]: value };
1207
- setServerData?.(newData);
1208
- }
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 });
1209
1556
  },
1210
1557
 
1211
- findRows: (predicate: (row: T) => boolean) =>
1212
- (table.getRowModel().rows || []).filter((row) => predicate(row.original)).map((row) => row.original),
1558
+ findRows: (predicate: (row: T) => boolean) => tableData.filter(predicate),
1213
1559
 
1214
- findRowIndex: (predicate: (row: T) => boolean) =>
1215
- (table.getRowModel().rows || []).findIndex((row) => predicate(row.original)),
1560
+ findRowIndex: (predicate: (row: T) => boolean) => tableData.findIndex(predicate),
1216
1561
 
1217
- getDataCount: () => (table.getRowModel().rows || []).length || 0,
1562
+ getDataCount: () => tableData.length,
1218
1563
  getFilteredDataCount: () => table.getFilteredRowModel().rows.length,
1219
1564
  },
1220
1565
 
@@ -1229,7 +1574,7 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1229
1574
  applySorting(initialStateConfig.sorting || []);
1230
1575
  applyGlobalFilter(initialStateConfig.globalFilter ?? "");
1231
1576
  },
1232
- resetAll: () => resetAllAndReload({ resetLayout: true }),
1577
+ resetAll: () => resetAllAndReload(),
1233
1578
  saveLayout: () => ({
1234
1579
  columnVisibility: table.getState().columnVisibility,
1235
1580
  columnSizing: table.getState().columnSizing,
@@ -1267,99 +1612,200 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1267
1612
  // Export (unchanged mostly)
1268
1613
  // -------------------------------
1269
1614
  export: {
1270
- exportCSV: async (options: any = {}) => {
1271
- const { filename = exportFilename } = options;
1272
-
1273
- try {
1274
- const controller = new AbortController();
1275
- setExportController?.(controller);
1276
-
1277
- if (dataMode === "server" && onServerExport) {
1278
- const currentFilters = {
1279
- globalFilter: table.getState().globalFilter,
1280
- columnFilter: table.getState().columnFilter,
1281
- sorting: table.getState().sorting,
1282
- pagination: table.getState().pagination,
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
+ }
1283
1655
  };
1284
1656
 
1285
- if (logger.isLevelEnabled("debug")) logger.debug("Server export CSV", { currentFilters });
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
+ }
1286
1685
 
1287
- await exportServerData(table, {
1288
- format: "csv",
1289
- filename,
1290
- fetchData: (filters: any, selection: any) => onServerExport(filters, selection),
1291
- currentFilters,
1292
- selection: table.getSelectionState?.(),
1293
- onProgress: onExportProgress,
1294
- onComplete: onExportComplete,
1295
- onError: onExportError,
1296
- });
1297
- } else {
1298
1686
  await exportClientData(table, {
1299
1687
  format: "csv",
1300
1688
  filename,
1301
- onProgress: onExportProgress,
1689
+ onProgress: handleExportProgressInternal,
1302
1690
  onComplete: onExportComplete,
1303
1691
  onError: onExportError,
1692
+ onStateChange: toStateChange,
1693
+ signal: controller.signal,
1694
+ sanitizeCSV,
1304
1695
  });
1305
1696
 
1306
1697
  if (logger.isLevelEnabled("debug")) logger.debug("Client export CSV", filename);
1307
1698
  }
1308
- } catch (error: any) {
1309
- onExportError?.({ message: error.message || "Export failed", code: "EXPORT_ERROR" });
1310
- } finally {
1311
- setExportController?.(null);
1312
- }
1699
+ });
1313
1700
  },
1314
1701
 
1315
- exportExcel: async (options: any = {}) => {
1316
- const { filename = exportFilename } = options;
1317
-
1318
- try {
1319
- const controller = new AbortController();
1320
- setExportController?.(controller);
1321
-
1322
- if (dataMode === "server" && onServerExport) {
1323
- const currentFilters = {
1324
- globalFilter: table.getState().globalFilter,
1325
- columnFilter: table.getState().columnFilter,
1326
- sorting: table.getState().sorting,
1327
- pagination: table.getState().pagination,
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
+ }
1328
1742
  };
1329
1743
 
1330
- if (logger.isLevelEnabled("debug")) logger.debug("Server export Excel", { currentFilters });
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
+ }
1331
1772
 
1332
- await exportServerData(table, {
1333
- format: "excel",
1334
- filename,
1335
- fetchData: (filters: any, selection: any) => onServerExport(filters, selection),
1336
- currentFilters,
1337
- selection: table.getSelectionState?.(),
1338
- onProgress: onExportProgress,
1339
- onComplete: onExportComplete,
1340
- onError: onExportError,
1341
- });
1342
- } else {
1343
1773
  await exportClientData(table, {
1344
1774
  format: "excel",
1345
1775
  filename,
1346
- onProgress: onExportProgress,
1776
+ onProgress: handleExportProgressInternal,
1347
1777
  onComplete: onExportComplete,
1348
1778
  onError: onExportError,
1779
+ onStateChange: toStateChange,
1780
+ signal: controller.signal,
1781
+ sanitizeCSV,
1349
1782
  });
1350
1783
 
1351
1784
  if (logger.isLevelEnabled("debug")) logger.debug("Client export Excel", filename);
1352
1785
  }
1353
- } catch (error: any) {
1354
- onExportError?.({ message: error.message || "Export failed", code: "EXPORT_ERROR" });
1355
- if (logger.isLevelEnabled("debug")) logger.debug("Server export Excel failed", error);
1356
- } finally {
1357
- setExportController?.(null);
1358
- }
1786
+ });
1359
1787
  },
1360
1788
 
1361
- exportServerData: async (options: any) => {
1362
- const { format, filename = exportFilename, fetchData: fetchFn = onServerExport } = options;
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;
1363
1809
 
1364
1810
  if (!fetchFn) {
1365
1811
  onExportError?.({ message: "No server export function provided", code: "NO_SERVER_EXPORT" });
@@ -1367,41 +1813,67 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1367
1813
  return;
1368
1814
  }
1369
1815
 
1370
- try {
1371
- const controller = new AbortController();
1372
- setExportController?.(controller);
1373
-
1374
- const currentFilters = {
1375
- globalFilter: table.getState().globalFilter,
1376
- columnFilter: table.getState().columnFilter,
1377
- sorting: table.getState().sorting,
1378
- pagination: table.getState().pagination,
1379
- };
1380
-
1381
- if (logger.isLevelEnabled("debug")) logger.debug("Server export data", { currentFilters });
1382
-
1383
- await exportServerData(table, {
1384
- format,
1385
- filename,
1386
- fetchData: (filters: any, selection: any) => fetchFn(filters, selection),
1387
- currentFilters,
1388
- selection: table.getSelectionState?.(),
1389
- onProgress: onExportProgress,
1390
- onComplete: onExportComplete,
1391
- onError: onExportError,
1392
- });
1393
- } catch (error: any) {
1394
- onExportError?.({ message: error.message || "Export failed", code: "EXPORT_ERROR" });
1395
- if (logger.isLevelEnabled("debug")) logger.debug("Server export data failed", error);
1396
- } finally {
1397
- setExportController?.(null);
1398
- }
1816
+ await runExportWithPolicy({
1817
+ format,
1818
+ filename,
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
+ }
1866
+ });
1399
1867
  },
1400
1868
 
1401
1869
  isExporting: () => isExporting || false,
1402
1870
  cancelExport: () => {
1403
- exportController?.abort();
1404
- setExportController?.(null);
1871
+ const activeController = exportControllerRef.current;
1872
+ if (!activeController) {
1873
+ return;
1874
+ }
1875
+ activeController.abort();
1876
+ setExportControllerSafely((current) => (current === activeController ? null : current));
1405
1877
  if (logger.isLevelEnabled("debug")) logger.debug("Export cancelled");
1406
1878
  },
1407
1879
  },
@@ -1421,17 +1893,26 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1421
1893
  initialStateConfig,
1422
1894
  enablePagination,
1423
1895
  idKey,
1424
- onDataStateChange,
1425
- fetchData,
1896
+ triggerRefresh,
1897
+ applyDataMutation,
1898
+ tableData,
1899
+ selectionState,
1426
1900
  // export
1427
1901
  exportFilename,
1428
- onExportProgress,
1902
+ exportChunkSize,
1903
+ exportStrictTotalCheck,
1904
+ exportSanitizeCSV,
1429
1905
  onExportComplete,
1430
1906
  onExportError,
1907
+ onExportCancel,
1431
1908
  onServerExport,
1432
- exportController,
1909
+ queuedExportCount,
1433
1910
  isExporting,
1434
1911
  dataMode,
1912
+ handleExportProgressInternal,
1913
+ handleExportStateChangeInternal,
1914
+ runExportWithPolicy,
1915
+ setExportControllerSafely,
1435
1916
  logger,
1436
1917
  resetAllAndReload,
1437
1918
  ]);
@@ -1571,14 +2052,12 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1571
2052
  // Export cancel callback
1572
2053
  // -------------------------------
1573
2054
  const handleCancelExport = useCallback(() => {
1574
- if (exportController) {
1575
- exportController.abort();
1576
- setExportController(null);
1577
- if (onExportCancel) {
1578
- onExportCancel();
1579
- }
2055
+ const activeController = exportControllerRef.current;
2056
+ if (activeController) {
2057
+ activeController.abort();
2058
+ setExportControllerSafely((current) => (current === activeController ? null : current));
1580
2059
  }
1581
- }, [exportController, onExportCancel]);
2060
+ }, [setExportControllerSafely]);
1582
2061
 
1583
2062
  // -------------------------------
1584
2063
  // Slot components
@@ -1658,6 +2137,8 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1658
2137
  slotProps={slotProps}
1659
2138
  isExporting={isExporting}
1660
2139
  exportController={exportController}
2140
+ exportPhase={exportPhase}
2141
+ exportProgress={exportProgress}
1661
2142
  onCancelExport={handleCancelExport}
1662
2143
  exportFilename={exportFilename}
1663
2144
  onExportProgress={onExportProgress}