@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.
- package/README.md +143 -11
- package/dist/lib/components/droupdown/menu-dropdown.d.ts.map +1 -1
- package/dist/lib/components/droupdown/menu-dropdown.js +8 -1
- package/dist/lib/components/filters/filter-value-input.js +2 -2
- package/dist/lib/components/pagination/data-table-pagination.d.ts.map +1 -1
- package/dist/lib/components/pagination/data-table-pagination.js +10 -1
- package/dist/lib/components/toolbar/table-export-control.d.ts.map +1 -1
- package/dist/lib/components/toolbar/table-export-control.js +46 -12
- package/dist/lib/contexts/data-table-context.d.ts +7 -10
- package/dist/lib/contexts/data-table-context.d.ts.map +1 -1
- package/dist/lib/contexts/data-table-context.js +5 -1
- package/dist/lib/data-table.d.ts.map +1 -1
- package/dist/lib/data-table.js +561 -230
- package/dist/lib/features/column-filter.feature.js +38 -21
- package/dist/lib/features/selection.feature.d.ts.map +1 -1
- package/dist/lib/features/selection.feature.js +11 -3
- package/dist/lib/types/column.types.d.ts +19 -0
- package/dist/lib/types/column.types.d.ts.map +1 -1
- package/dist/lib/types/data-table-api.d.ts +24 -18
- package/dist/lib/types/data-table-api.d.ts.map +1 -1
- package/dist/lib/types/data-table.types.d.ts +36 -10
- package/dist/lib/types/data-table.types.d.ts.map +1 -1
- package/dist/lib/types/export.types.d.ts +57 -13
- package/dist/lib/types/export.types.d.ts.map +1 -1
- package/dist/lib/types/slots.types.d.ts +3 -1
- package/dist/lib/types/slots.types.d.ts.map +1 -1
- package/dist/lib/types/table.types.d.ts +1 -3
- package/dist/lib/types/table.types.d.ts.map +1 -1
- package/dist/lib/utils/debounced-fetch.utils.d.ts +8 -4
- package/dist/lib/utils/debounced-fetch.utils.d.ts.map +1 -1
- package/dist/lib/utils/debounced-fetch.utils.js +63 -14
- package/dist/lib/utils/export-utils.d.ts +14 -4
- package/dist/lib/utils/export-utils.d.ts.map +1 -1
- package/dist/lib/utils/export-utils.js +362 -66
- package/package.json +4 -2
- package/src/lib/components/droupdown/menu-dropdown.tsx +9 -3
- package/src/lib/components/filters/filter-value-input.tsx +2 -2
- package/src/lib/components/pagination/data-table-pagination.tsx +14 -2
- package/src/lib/components/toolbar/table-export-control.tsx +65 -9
- package/src/lib/contexts/data-table-context.tsx +16 -2
- package/src/lib/data-table.tsx +703 -222
- package/src/lib/features/column-filter.feature.ts +40 -19
- package/src/lib/features/selection.feature.ts +11 -5
- package/src/lib/types/column.types.ts +20 -1
- package/src/lib/types/data-table-api.ts +33 -15
- package/src/lib/types/data-table.types.ts +58 -3
- package/src/lib/types/export.types.ts +79 -10
- package/src/lib/types/slots.types.ts +3 -1
- package/src/lib/types/table.types.ts +1 -3
- package/src/lib/utils/debounced-fetch.utils.ts +90 -18
- package/src/lib/utils/export-utils.ts +496 -69
package/src/lib/data-table.tsx
CHANGED
|
@@ -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 {
|
|
45
|
-
|
|
46
|
-
|
|
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(() =>
|
|
264
|
-
|
|
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 (
|
|
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', {
|
|
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,
|
|
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
|
|
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:
|
|
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)
|
|
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
|
-
|
|
745
|
-
|
|
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: (
|
|
1079
|
-
|
|
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
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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(
|
|
1421
|
+
resetAll: () => resetAllAndReload(),
|
|
1110
1422
|
|
|
1111
|
-
getAllData: () =>
|
|
1112
|
-
getRowData: (rowId: string) =>
|
|
1113
|
-
|
|
1114
|
-
|
|
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
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
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
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
.filter((
|
|
1161
|
-
|
|
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
|
-
|
|
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[]) =>
|
|
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((
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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: () =>
|
|
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(
|
|
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:
|
|
1271
|
-
const {
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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 (
|
|
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:
|
|
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
|
-
}
|
|
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:
|
|
1316
|
-
const {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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 (
|
|
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:
|
|
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
|
-
}
|
|
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:
|
|
1362
|
-
|
|
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
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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
|
-
|
|
1404
|
-
|
|
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
|
-
|
|
1425
|
-
|
|
1896
|
+
triggerRefresh,
|
|
1897
|
+
applyDataMutation,
|
|
1898
|
+
tableData,
|
|
1899
|
+
selectionState,
|
|
1426
1900
|
// export
|
|
1427
1901
|
exportFilename,
|
|
1428
|
-
|
|
1902
|
+
exportChunkSize,
|
|
1903
|
+
exportStrictTotalCheck,
|
|
1904
|
+
exportSanitizeCSV,
|
|
1429
1905
|
onExportComplete,
|
|
1430
1906
|
onExportError,
|
|
1907
|
+
onExportCancel,
|
|
1431
1908
|
onServerExport,
|
|
1432
|
-
|
|
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
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
onExportCancel();
|
|
1579
|
-
}
|
|
2055
|
+
const activeController = exportControllerRef.current;
|
|
2056
|
+
if (activeController) {
|
|
2057
|
+
activeController.abort();
|
|
2058
|
+
setExportControllerSafely((current) => (current === activeController ? null : current));
|
|
1580
2059
|
}
|
|
1581
|
-
}, [
|
|
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}
|