@ackplus/react-tanstack-data-table 1.1.12 → 1.1.15

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 +517 -237
  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 +37 -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 +647 -231
  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 +59 -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,15 @@ 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
+ DataRefreshOptions,
49
+ DataTableProps,
50
+ } from './types/data-table.types';
51
+ import { ColumnFilterState, ExportPhase, ExportProgressPayload, ExportStateChange, TableFiltersForFetch, TableState } from './types';
52
+ import { DataTableApi, DataTableExportApiOptions } from './types/data-table-api';
47
53
  import {
48
54
  createExpandingColumn,
49
55
  createSelectionColumn,
@@ -91,8 +97,10 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
91
97
  // Data management mode (MUI DataGrid style)
92
98
  dataMode = 'client',
93
99
  initialLoadData = true,
94
- onFetchData,
95
- onDataStateChange,
100
+ onFetchData, // callback to fetch data from the server need to with response { data: T[], total: number }
101
+ onFetchStateChange, // callback to fetch data from the server no need to resonce , this for filter data
102
+ onDataChange, // callback to change data
103
+ onDataStateChange, // callback to change data state
96
104
 
97
105
  // Selection props
98
106
  enableRowSelection = false,
@@ -144,12 +152,19 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
144
152
  enableSorting = true,
145
153
  sortingMode = 'client',
146
154
  onSortingChange,
155
+
156
+ //export props
147
157
  exportFilename = 'export',
158
+ exportConcurrency = 'cancelAndRestart',
159
+ exportChunkSize = 1000,
160
+ exportStrictTotalCheck = false,
161
+ exportSanitizeCSV = true,
148
162
  onExportProgress,
149
163
  onExportComplete,
150
164
  onExportError,
151
165
  onServerExport,
152
166
  onExportCancel,
167
+ onExportStateChange,
153
168
 
154
169
  // Styling props
155
170
  enableHover = true,
@@ -252,16 +267,32 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
252
267
  const [serverData, setServerData] = useState<T[] | null>(null);
253
268
  const [serverTotal, setServerTotal] = useState(0);
254
269
  const [exportController, setExportController] = useState<AbortController | null>(null);
270
+ const [exportProgress, setExportProgress] = useState<ExportProgressPayload>({});
271
+ const [exportPhase, setExportPhase] = useState<ExportPhase | null>(null);
272
+ const [queuedExportCount, setQueuedExportCount] = useState(0);
255
273
 
256
274
  // -------------------------------
257
275
  // Ref hooks (grouped together)
258
276
  // -------------------------------
259
277
  const tableContainerRef = useRef<HTMLDivElement>(null);
260
278
  const internalApiRef = useRef<DataTableApi<T>>(null);
279
+ const exportControllerRef = useRef<AbortController | null>(null);
280
+ const exportQueueRef = useRef<Promise<void>>(Promise.resolve());
281
+
282
+ const isExternallyControlledData = useMemo(
283
+ () => !onFetchData && (!!onDataChange || !!onFetchStateChange),
284
+ [onFetchData, onDataChange, onFetchStateChange]
285
+ );
261
286
 
262
287
  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]);
288
+ const tableData = useMemo(() => {
289
+ if (isExternallyControlledData) return data;
290
+ return serverData !== null ? serverData : data;
291
+ }, [isExternallyControlledData, serverData, data]);
292
+ const tableTotalRow = useMemo(
293
+ () => (isExternallyControlledData ? (totalRow || data.length) : (serverData !== null ? serverTotal : totalRow || data.length)),
294
+ [isExternallyControlledData, serverData, serverTotal, totalRow, data]
295
+ );
265
296
  const tableLoading = useMemo(() => onFetchData ? (loading || fetchLoading) : loading, [onFetchData, loading, fetchLoading]);
266
297
 
267
298
 
@@ -309,15 +340,11 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
309
340
  // -------------------------------
310
341
  // Callback hooks (grouped together)
311
342
  // -------------------------------
312
- const fetchData = useCallback(async (overrides: Partial<TableState> = {}, options?: { delay?: number }) => {
313
- if (!onFetchData) {
314
- if (logger.isLevelEnabled('debug')) {
315
- logger.debug('onFetchData not provided, skipping fetch', { overrides, columnFilter, sorting, pagination });
316
- }
317
- return;
318
- }
319
-
320
- const filters: TableFiltersForFetch = {
343
+ const fetchData = useCallback(async (
344
+ overrides: Partial<TableState> = {},
345
+ options?: { delay?: number; meta?: DataFetchMeta }
346
+ ) => {
347
+ const filters: Partial<TableFiltersForFetch> = {
321
348
  globalFilter,
322
349
  pagination,
323
350
  columnFilter,
@@ -325,15 +352,30 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
325
352
  ...overrides,
326
353
  };
327
354
 
328
- console.log('Fetching data', filters);
355
+ if(onFetchStateChange) {
356
+ onFetchStateChange(filters, options?.meta);
357
+ }
358
+ if (!onFetchData) {
359
+ if (logger.isLevelEnabled('debug')) {
360
+ logger.debug('onFetchData not provided, skipping fetch', { overrides, columnFilter, sorting, pagination });
361
+ }
362
+ return;
363
+ }
329
364
 
330
365
  if (logger.isLevelEnabled('info')) {
331
- logger.info('Requesting data', { filters });
366
+ logger.info('Requesting data', {
367
+ filters,
368
+ reason: options?.meta?.reason,
369
+ force: options?.meta?.force,
370
+ });
332
371
  }
333
372
 
334
373
  try {
335
374
  const delay = options?.delay ?? 300; // respects 0
336
- const result = await debouncedFetch(filters, delay);
375
+ const result = await debouncedFetch(filters, {
376
+ debounceDelay: delay,
377
+ meta: options?.meta,
378
+ });
337
379
 
338
380
  if (logger.isLevelEnabled('info')) {
339
381
  logger.info('Fetch resolved', {
@@ -342,7 +384,7 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
342
384
  });
343
385
  }
344
386
 
345
- if (result?.data && result?.total !== undefined) {
387
+ if (result && Array.isArray(result.data) && result.total !== undefined) {
346
388
  setServerData(result.data);
347
389
  setServerTotal(result.total);
348
390
  } else if (logger.isLevelEnabled('warn')) {
@@ -362,8 +404,28 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
362
404
  sorting,
363
405
  debouncedFetch,
364
406
  logger,
407
+ onFetchStateChange,
365
408
  ]);
366
409
 
410
+ const normalizeRefreshOptions = useCallback((
411
+ options?: boolean | DataRefreshOptions,
412
+ fallbackReason: string = 'refresh'
413
+ ) => {
414
+ if (typeof options === 'boolean') {
415
+ return {
416
+ resetPagination: options,
417
+ force: false,
418
+ reason: fallbackReason,
419
+ };
420
+ }
421
+
422
+ return {
423
+ resetPagination: options?.resetPagination ?? false,
424
+ force: options?.force ?? false,
425
+ reason: options?.reason ?? fallbackReason,
426
+ };
427
+ }, []);
428
+
367
429
 
368
430
  const handleSelectionStateChange = useCallback((updaterOrValue) => {
369
431
  setSelectionState((prevState) => {
@@ -635,11 +697,20 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
635
697
  // Effects (after callbacks)
636
698
  // -------------------------------
637
699
  useEffect(() => {
638
- if (initialLoadData && onFetchData) {
700
+ if (!isExternallyControlledData || serverData === null) return;
701
+ setServerData(null);
702
+ setServerTotal(0);
703
+ }, [isExternallyControlledData, serverData]);
704
+
705
+ useEffect(() => {
706
+ if (initialLoadData && (onFetchData || onFetchStateChange)) {
639
707
  if (logger.isLevelEnabled('info')) {
640
708
  logger.info('Initial data load triggered', { initialLoadData });
641
709
  }
642
- fetchData({});
710
+ fetchData({}, {
711
+ delay: 0,
712
+ meta: { reason: 'initial' },
713
+ });
643
714
  } else if (logger.isLevelEnabled('debug')) {
644
715
  logger.debug('Skipping initial data load', {
645
716
  initialLoadData,
@@ -670,13 +741,14 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
670
741
  if (!onDataStateChange) return;
671
742
 
672
743
  const live = table.getState();
744
+ const liveColumnFilter = live.columnFilter;
673
745
 
674
746
  // only keep what you persist/store
675
747
  const payload = {
676
748
  sorting: live.sorting,
677
749
  pagination: live.pagination,
678
750
  globalFilter: live.globalFilter,
679
- columnFilter: live.columnFilter,
751
+ columnFilter: liveColumnFilter,
680
752
  columnVisibility: live.columnVisibility,
681
753
  columnSizing: live.columnSizing,
682
754
  columnOrder: live.columnOrder,
@@ -708,8 +780,7 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
708
780
  const getResetState = useCallback((): Partial<TableState> => {
709
781
  const resetSorting = initialStateConfig.sorting || [];
710
782
  const resetGlobalFilter = initialStateConfig.globalFilter ?? '';
711
- const resetColumnFilter =
712
- initialStateConfig.columnFilter || { filters: [], logic: 'AND', pendingFilters: [], pendingLogic: 'AND' };
783
+ const resetColumnFilter = initialStateConfig.columnFilter;
713
784
 
714
785
  const resetPagination = enablePagination
715
786
  ? (initialStateConfig.pagination || { pageIndex: 0, pageSize: 10 })
@@ -723,6 +794,75 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
723
794
  };
724
795
  }, [initialStateConfig, enablePagination]);
725
796
 
797
+ const applyDataMutation = useCallback((
798
+ action: DataMutationAction,
799
+ updater: (rows: T[]) => T[],
800
+ details: Partial<Omit<DataMutationContext<T>, 'action' | 'previousData' | 'nextData'>> = {}
801
+ ) => {
802
+ const previousData = [...tableData];
803
+ const nextData = updater(previousData);
804
+
805
+ if (nextData === previousData) return nextData;
806
+
807
+ const nextTotal = Math.max(0, tableTotalRow + (nextData.length - previousData.length));
808
+
809
+ if (!isExternallyControlledData) {
810
+ setServerData(nextData);
811
+ setServerTotal(nextTotal);
812
+ }
813
+ onDataChange?.(nextData, {
814
+ action,
815
+ previousData,
816
+ nextData,
817
+ totalRow: nextTotal,
818
+ ...details,
819
+ });
820
+
821
+ if (logger.isLevelEnabled('debug')) {
822
+ logger.debug('Applied data mutation', {
823
+ action,
824
+ previousCount: previousData.length,
825
+ nextCount: nextData.length,
826
+ totalRow: nextTotal,
827
+ });
828
+ }
829
+
830
+ return nextData;
831
+ }, [isExternallyControlledData, logger, onDataChange, tableData, tableTotalRow]);
832
+
833
+ const triggerRefresh = useCallback(async (
834
+ options?: boolean | DataRefreshOptions,
835
+ fallbackReason: string = 'refresh'
836
+ ) => {
837
+ const normalizedOptions = normalizeRefreshOptions(options, fallbackReason);
838
+ const nextPagination = enablePagination
839
+ ? {
840
+ pageIndex: normalizedOptions.resetPagination ? 0 : pagination.pageIndex,
841
+ pageSize: pagination.pageSize,
842
+ }
843
+ : undefined;
844
+
845
+ const shouldUpdatePagination = !!nextPagination
846
+ && (nextPagination.pageIndex !== pagination.pageIndex || nextPagination.pageSize !== pagination.pageSize);
847
+
848
+ if (nextPagination && shouldUpdatePagination) {
849
+ setPagination(nextPagination);
850
+ onPaginationChange?.(nextPagination);
851
+ }
852
+
853
+ await fetchData(
854
+ nextPagination ? { pagination: nextPagination } : {},
855
+ {
856
+ delay: 0,
857
+ meta: {
858
+ reason: normalizedOptions.reason,
859
+ force: normalizedOptions.force,
860
+ },
861
+ }
862
+ );
863
+ return;
864
+ }, [normalizeRefreshOptions, enablePagination, pagination, onPaginationChange, fetchData]);
865
+
726
866
  const resetAllAndReload = useCallback(() => {
727
867
  const resetState = getResetState();
728
868
 
@@ -730,7 +870,10 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
730
870
  setGlobalFilter(resetState.globalFilter ?? '');
731
871
  setColumnFilter(resetState.columnFilter as any);
732
872
 
733
- if (resetState.pagination) setPagination(resetState.pagination);
873
+ if (resetState.pagination) {
874
+ setPagination(resetState.pagination);
875
+ onPaginationChange?.(resetState.pagination);
876
+ }
734
877
 
735
878
  setSelectionState(initialSelectionState);
736
879
  setExpanded({});
@@ -741,8 +884,120 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
741
884
  setColumnOrder(initialStateConfig.columnOrder || []);
742
885
  setColumnPinning(initialStateConfig.columnPinning || { left: [], right: [] });
743
886
 
744
- if (onFetchData) fetchData(resetState, { delay: 0 });
745
- }, [getResetState, initialSelectionState, initialStateConfig, onFetchData, fetchData]);
887
+ const resetOptions = normalizeRefreshOptions({
888
+ resetPagination: true,
889
+ force: true,
890
+ reason: 'reset',
891
+ }, 'reset');
892
+
893
+ void fetchData(resetState, {
894
+ delay: 0,
895
+ meta: {
896
+ reason: resetOptions.reason,
897
+ force: resetOptions.force,
898
+ },
899
+ });
900
+ }, [getResetState, initialSelectionState, initialStateConfig, onPaginationChange, normalizeRefreshOptions, fetchData]);
901
+
902
+ const setExportControllerSafely = useCallback((
903
+ value: AbortController | null | ((current: AbortController | null) => AbortController | null)
904
+ ) => {
905
+ setExportController((current) => {
906
+ const next = typeof value === 'function' ? (value as any)(current) : value;
907
+ exportControllerRef.current = next;
908
+ return next;
909
+ });
910
+ }, []);
911
+
912
+ const handleExportProgressInternal = useCallback((progress: ExportProgressPayload) => {
913
+ setExportProgress(progress || {});
914
+ onExportProgress?.(progress);
915
+ }, [onExportProgress]);
916
+
917
+ const handleExportStateChangeInternal = useCallback((state: ExportStateChange) => {
918
+ setExportPhase(state.phase);
919
+ if (
920
+ state.processedRows !== undefined
921
+ || state.totalRows !== undefined
922
+ || state.percentage !== undefined
923
+ ) {
924
+ setExportProgress({
925
+ processedRows: state.processedRows,
926
+ totalRows: state.totalRows,
927
+ percentage: state.percentage,
928
+ });
929
+ }
930
+ onExportStateChange?.(state);
931
+ }, [onExportStateChange]);
932
+
933
+ const runExportWithPolicy = useCallback(
934
+ async (
935
+ options: {
936
+ format: 'csv' | 'excel';
937
+ filename: string;
938
+ mode: 'client' | 'server';
939
+ execute: (controller: AbortController) => Promise<void>;
940
+ }
941
+ ) => {
942
+ const { format, filename, mode, execute } = options;
943
+
944
+ const startExecution = async () => {
945
+ const controller = new AbortController();
946
+ setExportProgress({});
947
+ setExportControllerSafely(controller);
948
+ try {
949
+ await execute(controller);
950
+ } finally {
951
+ setExportControllerSafely((current) => (current === controller ? null : current));
952
+ }
953
+ };
954
+
955
+ if (exportConcurrency === 'queue') {
956
+ setQueuedExportCount((prev) => prev + 1);
957
+ const runQueued = async (): Promise<void> => {
958
+ setQueuedExportCount((prev) => Math.max(0, prev - 1));
959
+ await startExecution();
960
+ };
961
+ const queuedPromise = exportQueueRef.current
962
+ .catch(() => undefined)
963
+ .then(runQueued);
964
+ exportQueueRef.current = queuedPromise;
965
+ return queuedPromise;
966
+ }
967
+
968
+ const activeController = exportControllerRef.current;
969
+ if (activeController) {
970
+ if (exportConcurrency === 'ignoreIfRunning') {
971
+ handleExportStateChangeInternal({
972
+ phase: 'error',
973
+ mode,
974
+ format,
975
+ filename,
976
+ message: 'An export is already running',
977
+ code: 'EXPORT_IN_PROGRESS',
978
+ endedAt: Date.now(),
979
+ });
980
+ onExportError?.({
981
+ message: 'An export is already running',
982
+ code: 'EXPORT_IN_PROGRESS',
983
+ });
984
+ return;
985
+ }
986
+
987
+ if (exportConcurrency === 'cancelAndRestart') {
988
+ activeController.abort();
989
+ }
990
+ }
991
+
992
+ await startExecution();
993
+ },
994
+ [
995
+ exportConcurrency,
996
+ handleExportStateChangeInternal,
997
+ onExportError,
998
+ setExportControllerSafely,
999
+ ]
1000
+ );
746
1001
 
747
1002
  const dataTableApi = useMemo(() => {
748
1003
  // helpers (avoid repeating boilerplate)
@@ -783,6 +1038,14 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
783
1038
  handleGlobalFilterChange(next);
784
1039
  };
785
1040
 
1041
+ const getRowIndexById = (rowsToSearch: T[], rowId: string) =>
1042
+ rowsToSearch.findIndex((row, index) => String(generateRowId(row, index, idKey)) === rowId);
1043
+
1044
+ const clampInsertIndex = (rowsToMutate: T[], insertIndex?: number) => {
1045
+ if (insertIndex === undefined) return rowsToMutate.length;
1046
+ return Math.max(0, Math.min(insertIndex, rowsToMutate.length));
1047
+ };
1048
+
786
1049
  return {
787
1050
  table: {
788
1051
  getTable: () => table,
@@ -1075,146 +1338,163 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1075
1338
  // Data Management (kept same, but ensure state changes go through handlers)
1076
1339
  // -------------------------------
1077
1340
  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
- }
1341
+ refresh: (options?: boolean | DataRefreshOptions) => {
1342
+ void triggerRefresh(options, 'refresh');
1098
1343
  },
1099
1344
 
1100
- reload: () => {
1101
- const allState = table.getState();
1102
- onDataStateChange?.(allState);
1103
- fetchData?.();
1104
- if (logger.isLevelEnabled("debug")) {
1105
- logger.info("Reloading data", allState);
1106
- }
1345
+ reload: (options: DataRefreshOptions = {}) => {
1346
+ void triggerRefresh(
1347
+ {
1348
+ ...options,
1349
+ resetPagination: options.resetPagination ?? false,
1350
+ reason: options.reason ?? 'reload',
1351
+ },
1352
+ 'reload'
1353
+ );
1107
1354
  },
1108
1355
 
1109
- resetAll: () => resetAllAndReload({ resetLayout: true }),
1356
+ resetAll: () => resetAllAndReload(),
1110
1357
 
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,
1358
+ getAllData: () => [...tableData],
1359
+ getRowData: (rowId: string) => {
1360
+ const rowIndex = getRowIndexById(tableData, rowId);
1361
+ return rowIndex === -1 ? undefined : tableData[rowIndex];
1362
+ },
1363
+ getRowByIndex: (index: number) => tableData[index],
1115
1364
 
1116
1365
  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);
1366
+ applyDataMutation('updateRow', (rowsToMutate) => {
1367
+ const rowIndex = getRowIndexById(rowsToMutate, rowId);
1368
+ if (rowIndex === -1) return rowsToMutate;
1369
+ const nextData = [...rowsToMutate];
1370
+ nextData[rowIndex] = { ...nextData[rowIndex], ...updates };
1371
+ return nextData;
1372
+ }, { rowId });
1122
1373
  },
1123
1374
 
1124
1375
  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
- }
1376
+ applyDataMutation('updateRowByIndex', (rowsToMutate) => {
1377
+ if (!rowsToMutate[index]) return rowsToMutate;
1378
+ const nextData = [...rowsToMutate];
1379
+ nextData[index] = { ...nextData[index], ...updates };
1380
+ return nextData;
1381
+ }, { index });
1131
1382
  },
1132
1383
 
1133
1384
  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);
1385
+ applyDataMutation('insertRow', (rowsToMutate) => {
1386
+ const nextData = [...rowsToMutate];
1387
+ nextData.splice(clampInsertIndex(nextData, index), 0, newRow);
1388
+ return nextData;
1389
+ }, { index });
1139
1390
  },
1140
1391
 
1141
1392
  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}`);
1393
+ applyDataMutation('deleteRow', (rowsToMutate) => {
1394
+ const rowIndex = getRowIndexById(rowsToMutate, rowId);
1395
+ if (rowIndex === -1) return rowsToMutate;
1396
+ const nextData = [...rowsToMutate];
1397
+ nextData.splice(rowIndex, 1);
1398
+ return nextData;
1399
+ }, { rowId });
1145
1400
  },
1146
1401
 
1147
1402
  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}`);
1403
+ applyDataMutation('deleteRowByIndex', (rowsToMutate) => {
1404
+ if (index < 0 || index >= rowsToMutate.length) return rowsToMutate;
1405
+ const nextData = [...rowsToMutate];
1406
+ nextData.splice(index, 1);
1407
+ return nextData;
1408
+ }, { index });
1152
1409
  },
1153
1410
 
1154
1411
  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);
1412
+ const currentSelection = table.getSelectionState?.() || selectionState;
1413
+ const selectedIds = new Set((currentSelection.ids || []).map((id) => String(id)));
1414
+ const loadedRowIds = tableData.map((row, index) => String(generateRowId(row, index, idKey)));
1415
+ const deletableRowIds = currentSelection.type === 'exclude'
1416
+ ? loadedRowIds.filter((rowId) => !selectedIds.has(rowId))
1417
+ : loadedRowIds.filter((rowId) => selectedIds.has(rowId));
1418
+
1419
+ if (deletableRowIds.length === 0) return;
1420
+ if (
1421
+ currentSelection.type === 'exclude'
1422
+ && table.getRowCount() > loadedRowIds.length
1423
+ && logger.isLevelEnabled('info')
1424
+ ) {
1425
+ logger.info('deleteSelectedRows in exclude mode removed currently loaded rows only', {
1426
+ removedRows: deletableRowIds.length,
1427
+ totalSelected: table.getSelectedCount?.(),
1428
+ });
1429
+ }
1162
1430
 
1163
- setServerData(newData);
1431
+ const deletableRowIdSet = new Set(deletableRowIds);
1432
+ applyDataMutation(
1433
+ 'deleteSelectedRows',
1434
+ (rowsToMutate) =>
1435
+ rowsToMutate.filter((row, index) => !deletableRowIdSet.has(String(generateRowId(row, index, idKey)))),
1436
+ { rowIds: deletableRowIds }
1437
+ );
1164
1438
  table.deselectAll?.();
1165
-
1166
- if (logger.isLevelEnabled("debug")) logger.debug("Deleting selected rows");
1167
1439
  },
1168
1440
 
1169
- replaceAllData: (newData: T[]) => setServerData?.(newData),
1441
+ replaceAllData: (newData: T[]) => {
1442
+ applyDataMutation('replaceAllData', () => [...newData]);
1443
+ },
1170
1444
 
1171
1445
  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 || []);
1446
+ const updateMap = new Map(updates.map((update) => [update.rowId, update.data]));
1447
+ applyDataMutation('updateMultipleRows', (rowsToMutate) =>
1448
+ rowsToMutate.map((row, index) => {
1449
+ const currentRowId = String(generateRowId(row, index, idKey));
1450
+ const updateData = updateMap.get(currentRowId);
1451
+ return updateData ? { ...row, ...updateData } : row;
1452
+ })
1453
+ );
1179
1454
  },
1180
1455
 
1181
1456
  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);
1457
+ applyDataMutation('insertMultipleRows', (rowsToMutate) => {
1458
+ const nextData = [...rowsToMutate];
1459
+ nextData.splice(clampInsertIndex(nextData, startIndex), 0, ...newRows);
1460
+ return nextData;
1461
+ }, { index: startIndex });
1186
1462
  },
1187
1463
 
1188
1464
  deleteMultipleRows: (rowIds: string[]) => {
1189
1465
  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);
1466
+ applyDataMutation(
1467
+ 'deleteMultipleRows',
1468
+ (rowsToMutate) =>
1469
+ rowsToMutate.filter((row, index) => !idsToDelete.has(String(generateRowId(row, index, idKey)))),
1470
+ { rowIds }
1471
+ );
1194
1472
  },
1195
1473
 
1196
1474
  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);
1475
+ applyDataMutation('updateField', (rowsToMutate) => {
1476
+ const rowIndex = getRowIndexById(rowsToMutate, rowId);
1477
+ if (rowIndex === -1) return rowsToMutate;
1478
+ const nextData = [...rowsToMutate];
1479
+ nextData[rowIndex] = { ...nextData[rowIndex], [fieldName]: value };
1480
+ return nextData;
1481
+ }, { rowId });
1201
1482
  },
1202
1483
 
1203
1484
  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
- }
1485
+ applyDataMutation('updateFieldByIndex', (rowsToMutate) => {
1486
+ if (!rowsToMutate[index]) return rowsToMutate;
1487
+ const nextData = [...rowsToMutate];
1488
+ nextData[index] = { ...nextData[index], [fieldName]: value };
1489
+ return nextData;
1490
+ }, { index });
1209
1491
  },
1210
1492
 
1211
- findRows: (predicate: (row: T) => boolean) =>
1212
- (table.getRowModel().rows || []).filter((row) => predicate(row.original)).map((row) => row.original),
1493
+ findRows: (predicate: (row: T) => boolean) => tableData.filter(predicate),
1213
1494
 
1214
- findRowIndex: (predicate: (row: T) => boolean) =>
1215
- (table.getRowModel().rows || []).findIndex((row) => predicate(row.original)),
1495
+ findRowIndex: (predicate: (row: T) => boolean) => tableData.findIndex(predicate),
1216
1496
 
1217
- getDataCount: () => (table.getRowModel().rows || []).length || 0,
1497
+ getDataCount: () => tableData.length,
1218
1498
  getFilteredDataCount: () => table.getFilteredRowModel().rows.length,
1219
1499
  },
1220
1500
 
@@ -1229,7 +1509,7 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1229
1509
  applySorting(initialStateConfig.sorting || []);
1230
1510
  applyGlobalFilter(initialStateConfig.globalFilter ?? "");
1231
1511
  },
1232
- resetAll: () => resetAllAndReload({ resetLayout: true }),
1512
+ resetAll: () => resetAllAndReload(),
1233
1513
  saveLayout: () => ({
1234
1514
  columnVisibility: table.getState().columnVisibility,
1235
1515
  columnSizing: table.getState().columnSizing,
@@ -1267,99 +1547,200 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1267
1547
  // Export (unchanged mostly)
1268
1548
  // -------------------------------
1269
1549
  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,
1550
+ exportCSV: async (options: DataTableExportApiOptions = {}) => {
1551
+ const {
1552
+ filename = exportFilename,
1553
+ chunkSize = exportChunkSize,
1554
+ strictTotalCheck = exportStrictTotalCheck,
1555
+ sanitizeCSV = exportSanitizeCSV,
1556
+ } = options;
1557
+ const mode: 'client' | 'server' = dataMode === "server" && !!onServerExport ? 'server' : 'client';
1558
+
1559
+ await runExportWithPolicy({
1560
+ format: 'csv',
1561
+ filename,
1562
+ mode,
1563
+ execute: async (controller) => {
1564
+ const toStateChange = (state: {
1565
+ phase: ExportPhase;
1566
+ processedRows?: number;
1567
+ totalRows?: number;
1568
+ percentage?: number;
1569
+ message?: string;
1570
+ code?: string;
1571
+ }) => {
1572
+ const isFinalPhase = state.phase === 'completed' || state.phase === 'cancelled' || state.phase === 'error';
1573
+ handleExportStateChangeInternal({
1574
+ phase: state.phase,
1575
+ mode,
1576
+ format: 'csv',
1577
+ filename,
1578
+ processedRows: state.processedRows,
1579
+ totalRows: state.totalRows,
1580
+ percentage: state.percentage,
1581
+ message: state.message,
1582
+ code: state.code,
1583
+ startedAt: state.phase === 'starting' ? Date.now() : undefined,
1584
+ endedAt: isFinalPhase ? Date.now() : undefined,
1585
+ queueLength: queuedExportCount,
1586
+ });
1587
+ if (state.phase === 'cancelled') {
1588
+ onExportCancel?.();
1589
+ }
1283
1590
  };
1284
1591
 
1285
- if (logger.isLevelEnabled("debug")) logger.debug("Server export CSV", { currentFilters });
1592
+ if (mode === 'server' && onServerExport) {
1593
+ const currentFilters = {
1594
+ globalFilter: table.getState().globalFilter,
1595
+ columnFilter: table.getState().columnFilter,
1596
+ sorting: table.getState().sorting,
1597
+ pagination: table.getState().pagination,
1598
+ };
1599
+
1600
+ if (logger.isLevelEnabled("debug")) logger.debug("Server export CSV", { currentFilters });
1601
+
1602
+ await exportServerData(table, {
1603
+ format: "csv",
1604
+ filename,
1605
+ fetchData: (filters: any, selection: any, signal?: AbortSignal) =>
1606
+ onServerExport(filters, selection, signal),
1607
+ currentFilters,
1608
+ selection: table.getSelectionState?.(),
1609
+ onProgress: handleExportProgressInternal,
1610
+ onComplete: onExportComplete,
1611
+ onError: onExportError,
1612
+ onStateChange: toStateChange,
1613
+ signal: controller.signal,
1614
+ chunkSize,
1615
+ strictTotalCheck,
1616
+ sanitizeCSV,
1617
+ });
1618
+ return;
1619
+ }
1286
1620
 
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
1621
  await exportClientData(table, {
1299
1622
  format: "csv",
1300
1623
  filename,
1301
- onProgress: onExportProgress,
1624
+ onProgress: handleExportProgressInternal,
1302
1625
  onComplete: onExportComplete,
1303
1626
  onError: onExportError,
1627
+ onStateChange: toStateChange,
1628
+ signal: controller.signal,
1629
+ sanitizeCSV,
1304
1630
  });
1305
1631
 
1306
1632
  if (logger.isLevelEnabled("debug")) logger.debug("Client export CSV", filename);
1307
1633
  }
1308
- } catch (error: any) {
1309
- onExportError?.({ message: error.message || "Export failed", code: "EXPORT_ERROR" });
1310
- } finally {
1311
- setExportController?.(null);
1312
- }
1634
+ });
1313
1635
  },
1314
1636
 
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,
1637
+ exportExcel: async (options: DataTableExportApiOptions = {}) => {
1638
+ const {
1639
+ filename = exportFilename,
1640
+ chunkSize = exportChunkSize,
1641
+ strictTotalCheck = exportStrictTotalCheck,
1642
+ sanitizeCSV = exportSanitizeCSV,
1643
+ } = options;
1644
+ const mode: 'client' | 'server' = dataMode === "server" && !!onServerExport ? 'server' : 'client';
1645
+
1646
+ await runExportWithPolicy({
1647
+ format: 'excel',
1648
+ filename,
1649
+ mode,
1650
+ execute: async (controller) => {
1651
+ const toStateChange = (state: {
1652
+ phase: ExportPhase;
1653
+ processedRows?: number;
1654
+ totalRows?: number;
1655
+ percentage?: number;
1656
+ message?: string;
1657
+ code?: string;
1658
+ }) => {
1659
+ const isFinalPhase = state.phase === 'completed' || state.phase === 'cancelled' || state.phase === 'error';
1660
+ handleExportStateChangeInternal({
1661
+ phase: state.phase,
1662
+ mode,
1663
+ format: 'excel',
1664
+ filename,
1665
+ processedRows: state.processedRows,
1666
+ totalRows: state.totalRows,
1667
+ percentage: state.percentage,
1668
+ message: state.message,
1669
+ code: state.code,
1670
+ startedAt: state.phase === 'starting' ? Date.now() : undefined,
1671
+ endedAt: isFinalPhase ? Date.now() : undefined,
1672
+ queueLength: queuedExportCount,
1673
+ });
1674
+ if (state.phase === 'cancelled') {
1675
+ onExportCancel?.();
1676
+ }
1328
1677
  };
1329
1678
 
1330
- if (logger.isLevelEnabled("debug")) logger.debug("Server export Excel", { currentFilters });
1679
+ if (mode === 'server' && onServerExport) {
1680
+ const currentFilters = {
1681
+ globalFilter: table.getState().globalFilter,
1682
+ columnFilter: table.getState().columnFilter,
1683
+ sorting: table.getState().sorting,
1684
+ pagination: table.getState().pagination,
1685
+ };
1686
+
1687
+ if (logger.isLevelEnabled("debug")) logger.debug("Server export Excel", { currentFilters });
1688
+
1689
+ await exportServerData(table, {
1690
+ format: "excel",
1691
+ filename,
1692
+ fetchData: (filters: any, selection: any, signal?: AbortSignal) =>
1693
+ onServerExport(filters, selection, signal),
1694
+ currentFilters,
1695
+ selection: table.getSelectionState?.(),
1696
+ onProgress: handleExportProgressInternal,
1697
+ onComplete: onExportComplete,
1698
+ onError: onExportError,
1699
+ onStateChange: toStateChange,
1700
+ signal: controller.signal,
1701
+ chunkSize,
1702
+ strictTotalCheck,
1703
+ sanitizeCSV,
1704
+ });
1705
+ return;
1706
+ }
1331
1707
 
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
1708
  await exportClientData(table, {
1344
1709
  format: "excel",
1345
1710
  filename,
1346
- onProgress: onExportProgress,
1711
+ onProgress: handleExportProgressInternal,
1347
1712
  onComplete: onExportComplete,
1348
1713
  onError: onExportError,
1714
+ onStateChange: toStateChange,
1715
+ signal: controller.signal,
1716
+ sanitizeCSV,
1349
1717
  });
1350
1718
 
1351
1719
  if (logger.isLevelEnabled("debug")) logger.debug("Client export Excel", filename);
1352
1720
  }
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
- }
1721
+ });
1359
1722
  },
1360
1723
 
1361
- exportServerData: async (options: any) => {
1362
- const { format, filename = exportFilename, fetchData: fetchFn = onServerExport } = options;
1724
+ exportServerData: async (options: {
1725
+ format: 'csv' | 'excel';
1726
+ filename?: string;
1727
+ fetchData?: (
1728
+ filters?: Partial<TableState>,
1729
+ selection?: SelectionState,
1730
+ signal?: AbortSignal
1731
+ ) => Promise<any>;
1732
+ chunkSize?: number;
1733
+ strictTotalCheck?: boolean;
1734
+ sanitizeCSV?: boolean;
1735
+ }) => {
1736
+ const {
1737
+ format,
1738
+ filename = exportFilename,
1739
+ fetchData: fetchFn = onServerExport,
1740
+ chunkSize = exportChunkSize,
1741
+ strictTotalCheck = exportStrictTotalCheck,
1742
+ sanitizeCSV = exportSanitizeCSV,
1743
+ } = options;
1363
1744
 
1364
1745
  if (!fetchFn) {
1365
1746
  onExportError?.({ message: "No server export function provided", code: "NO_SERVER_EXPORT" });
@@ -1367,41 +1748,67 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1367
1748
  return;
1368
1749
  }
1369
1750
 
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
- }
1751
+ await runExportWithPolicy({
1752
+ format,
1753
+ filename,
1754
+ mode: 'server',
1755
+ execute: async (controller) => {
1756
+ const currentFilters = {
1757
+ globalFilter: table.getState().globalFilter,
1758
+ columnFilter: table.getState().columnFilter,
1759
+ sorting: table.getState().sorting,
1760
+ pagination: table.getState().pagination,
1761
+ };
1762
+
1763
+ if (logger.isLevelEnabled("debug")) logger.debug("Server export data", { currentFilters });
1764
+
1765
+ await exportServerData(table, {
1766
+ format,
1767
+ filename,
1768
+ fetchData: (filters: any, selection: any, signal?: AbortSignal) =>
1769
+ fetchFn(filters, selection, signal),
1770
+ currentFilters,
1771
+ selection: table.getSelectionState?.(),
1772
+ onProgress: handleExportProgressInternal,
1773
+ onComplete: onExportComplete,
1774
+ onError: onExportError,
1775
+ onStateChange: (state) => {
1776
+ const isFinalPhase = state.phase === 'completed' || state.phase === 'cancelled' || state.phase === 'error';
1777
+ handleExportStateChangeInternal({
1778
+ phase: state.phase,
1779
+ mode: 'server',
1780
+ format,
1781
+ filename,
1782
+ processedRows: state.processedRows,
1783
+ totalRows: state.totalRows,
1784
+ percentage: state.percentage,
1785
+ message: state.message,
1786
+ code: state.code,
1787
+ startedAt: state.phase === 'starting' ? Date.now() : undefined,
1788
+ endedAt: isFinalPhase ? Date.now() : undefined,
1789
+ queueLength: queuedExportCount,
1790
+ });
1791
+ if (state.phase === 'cancelled') {
1792
+ onExportCancel?.();
1793
+ }
1794
+ },
1795
+ signal: controller.signal,
1796
+ chunkSize,
1797
+ strictTotalCheck,
1798
+ sanitizeCSV,
1799
+ });
1800
+ }
1801
+ });
1399
1802
  },
1400
1803
 
1401
1804
  isExporting: () => isExporting || false,
1402
1805
  cancelExport: () => {
1403
- exportController?.abort();
1404
- setExportController?.(null);
1806
+ const activeController = exportControllerRef.current;
1807
+ if (!activeController) {
1808
+ return;
1809
+ }
1810
+ activeController.abort();
1811
+ setExportControllerSafely((current) => (current === activeController ? null : current));
1405
1812
  if (logger.isLevelEnabled("debug")) logger.debug("Export cancelled");
1406
1813
  },
1407
1814
  },
@@ -1421,17 +1828,26 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1421
1828
  initialStateConfig,
1422
1829
  enablePagination,
1423
1830
  idKey,
1424
- onDataStateChange,
1425
- fetchData,
1831
+ triggerRefresh,
1832
+ applyDataMutation,
1833
+ tableData,
1834
+ selectionState,
1426
1835
  // export
1427
1836
  exportFilename,
1428
- onExportProgress,
1837
+ exportChunkSize,
1838
+ exportStrictTotalCheck,
1839
+ exportSanitizeCSV,
1429
1840
  onExportComplete,
1430
1841
  onExportError,
1842
+ onExportCancel,
1431
1843
  onServerExport,
1432
- exportController,
1844
+ queuedExportCount,
1433
1845
  isExporting,
1434
1846
  dataMode,
1847
+ handleExportProgressInternal,
1848
+ handleExportStateChangeInternal,
1849
+ runExportWithPolicy,
1850
+ setExportControllerSafely,
1435
1851
  logger,
1436
1852
  resetAllAndReload,
1437
1853
  ]);
@@ -1571,14 +1987,12 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1571
1987
  // Export cancel callback
1572
1988
  // -------------------------------
1573
1989
  const handleCancelExport = useCallback(() => {
1574
- if (exportController) {
1575
- exportController.abort();
1576
- setExportController(null);
1577
- if (onExportCancel) {
1578
- onExportCancel();
1579
- }
1990
+ const activeController = exportControllerRef.current;
1991
+ if (activeController) {
1992
+ activeController.abort();
1993
+ setExportControllerSafely((current) => (current === activeController ? null : current));
1580
1994
  }
1581
- }, [exportController, onExportCancel]);
1995
+ }, [setExportControllerSafely]);
1582
1996
 
1583
1997
  // -------------------------------
1584
1998
  // Slot components
@@ -1658,6 +2072,8 @@ export const DataTable = forwardRef<DataTableApi<any>, DataTableProps<any>>(func
1658
2072
  slotProps={slotProps}
1659
2073
  isExporting={isExporting}
1660
2074
  exportController={exportController}
2075
+ exportPhase={exportPhase}
2076
+ exportProgress={exportProgress}
1661
2077
  onCancelExport={handleCancelExport}
1662
2078
  exportFilename={exportFilename}
1663
2079
  onExportProgress={onExportProgress}