@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.
- 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 +517 -237
- 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 +37 -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 +647 -231
- 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 +59 -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,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 {
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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(() =>
|
|
264
|
-
|
|
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 (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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', {
|
|
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,
|
|
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
|
|
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 (
|
|
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:
|
|
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)
|
|
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
|
-
|
|
745
|
-
|
|
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: (
|
|
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
|
-
}
|
|
1341
|
+
refresh: (options?: boolean | DataRefreshOptions) => {
|
|
1342
|
+
void triggerRefresh(options, 'refresh');
|
|
1098
1343
|
},
|
|
1099
1344
|
|
|
1100
|
-
reload: () => {
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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(
|
|
1356
|
+
resetAll: () => resetAllAndReload(),
|
|
1110
1357
|
|
|
1111
|
-
getAllData: () =>
|
|
1112
|
-
getRowData: (rowId: string) =>
|
|
1113
|
-
|
|
1114
|
-
|
|
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
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
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
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
.filter((
|
|
1161
|
-
|
|
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
|
-
|
|
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[]) =>
|
|
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((
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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: () =>
|
|
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(
|
|
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:
|
|
1271
|
-
const {
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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 (
|
|
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:
|
|
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
|
-
}
|
|
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:
|
|
1316
|
-
const {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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 (
|
|
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:
|
|
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
|
-
}
|
|
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:
|
|
1362
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1404
|
-
|
|
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
|
-
|
|
1425
|
-
|
|
1831
|
+
triggerRefresh,
|
|
1832
|
+
applyDataMutation,
|
|
1833
|
+
tableData,
|
|
1834
|
+
selectionState,
|
|
1426
1835
|
// export
|
|
1427
1836
|
exportFilename,
|
|
1428
|
-
|
|
1837
|
+
exportChunkSize,
|
|
1838
|
+
exportStrictTotalCheck,
|
|
1839
|
+
exportSanitizeCSV,
|
|
1429
1840
|
onExportComplete,
|
|
1430
1841
|
onExportError,
|
|
1842
|
+
onExportCancel,
|
|
1431
1843
|
onServerExport,
|
|
1432
|
-
|
|
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
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
onExportCancel();
|
|
1579
|
-
}
|
|
1990
|
+
const activeController = exportControllerRef.current;
|
|
1991
|
+
if (activeController) {
|
|
1992
|
+
activeController.abort();
|
|
1993
|
+
setExportControllerSafely((current) => (current === activeController ? null : current));
|
|
1580
1994
|
}
|
|
1581
|
-
}, [
|
|
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}
|