@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
|
@@ -379,9 +379,16 @@ function evaluateFilterCondition(columnValue: any, operator: string, filterValue
|
|
|
379
379
|
|
|
380
380
|
// --- Date type logic ---
|
|
381
381
|
if (type === 'date') {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
382
|
+
if (operator === 'isEmpty') {
|
|
383
|
+
return columnValue === null || columnValue === undefined || columnValue === '';
|
|
384
|
+
}
|
|
385
|
+
if (operator === 'isNotEmpty') {
|
|
386
|
+
return columnValue !== null && columnValue !== undefined && columnValue !== '';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const mCol = columnValue ? toMoment(columnValue) : null;
|
|
390
|
+
const mFilter = filterValue ? toMoment(filterValue) : null;
|
|
391
|
+
if (!mCol || !mFilter || !mCol.isValid() || !mFilter.isValid()) return false;
|
|
385
392
|
switch (operator) {
|
|
386
393
|
case 'equals':
|
|
387
394
|
return mCol.isSame(mFilter, 'day');
|
|
@@ -391,10 +398,6 @@ function evaluateFilterCondition(columnValue: any, operator: string, filterValue
|
|
|
391
398
|
return mCol.isAfter(mFilter, 'day');
|
|
392
399
|
case 'before':
|
|
393
400
|
return mCol.isBefore(mFilter, 'day');
|
|
394
|
-
case 'isEmpty':
|
|
395
|
-
return !columnValue;
|
|
396
|
-
case 'isNotEmpty':
|
|
397
|
-
return !!columnValue;
|
|
398
401
|
default:
|
|
399
402
|
return true;
|
|
400
403
|
}
|
|
@@ -417,11 +420,16 @@ function evaluateFilterCondition(columnValue: any, operator: string, filterValue
|
|
|
417
420
|
// --- Select type logic (in, notIn, single select) ---
|
|
418
421
|
if (type === 'select') {
|
|
419
422
|
if (operator === 'in' || operator === 'notIn') {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
+
const values = Array.isArray(filterValue)
|
|
424
|
+
? filterValue
|
|
425
|
+
: [filterValue].filter((value) => value !== undefined && value !== null && value !== '');
|
|
426
|
+
|
|
427
|
+
if (values.length === 0) {
|
|
428
|
+
return operator === 'notIn';
|
|
423
429
|
}
|
|
424
|
-
|
|
430
|
+
|
|
431
|
+
if (operator === 'in') return values.includes(columnValue);
|
|
432
|
+
if (operator === 'notIn') return !values.includes(columnValue);
|
|
425
433
|
}
|
|
426
434
|
if (operator === 'equals' || operator === 'notEquals') {
|
|
427
435
|
return operator === 'equals'
|
|
@@ -431,6 +439,23 @@ function evaluateFilterCondition(columnValue: any, operator: string, filterValue
|
|
|
431
439
|
}
|
|
432
440
|
|
|
433
441
|
// --- Text/Number type logic ---
|
|
442
|
+
if (type === 'number') {
|
|
443
|
+
switch (operator) {
|
|
444
|
+
case 'equals':
|
|
445
|
+
return Number(columnValue) === Number(filterValue);
|
|
446
|
+
case 'notEquals':
|
|
447
|
+
return Number(columnValue) !== Number(filterValue);
|
|
448
|
+
case 'greaterThan':
|
|
449
|
+
return Number(columnValue) > Number(filterValue);
|
|
450
|
+
case 'greaterThanOrEqual':
|
|
451
|
+
return Number(columnValue) >= Number(filterValue);
|
|
452
|
+
case 'lessThan':
|
|
453
|
+
return Number(columnValue) < Number(filterValue);
|
|
454
|
+
case 'lessThanOrEqual':
|
|
455
|
+
return Number(columnValue) <= Number(filterValue);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
434
459
|
switch (operator) {
|
|
435
460
|
case 'contains':
|
|
436
461
|
return String(columnValue).toLowerCase().includes(String(filterValue).toLowerCase());
|
|
@@ -440,18 +465,14 @@ function evaluateFilterCondition(columnValue: any, operator: string, filterValue
|
|
|
440
465
|
return String(columnValue).toLowerCase().startsWith(String(filterValue).toLowerCase());
|
|
441
466
|
case 'endsWith':
|
|
442
467
|
return String(columnValue).toLowerCase().endsWith(String(filterValue).toLowerCase());
|
|
468
|
+
case 'equals':
|
|
469
|
+
return columnValue === filterValue;
|
|
470
|
+
case 'notEquals':
|
|
471
|
+
return columnValue !== filterValue;
|
|
443
472
|
case 'isEmpty':
|
|
444
473
|
return columnValue === null || columnValue === undefined || columnValue === '';
|
|
445
474
|
case 'isNotEmpty':
|
|
446
475
|
return columnValue !== null && columnValue !== undefined && columnValue !== '';
|
|
447
|
-
case 'greaterThan':
|
|
448
|
-
return Number(columnValue) > Number(filterValue);
|
|
449
|
-
case 'greaterThanOrEqual':
|
|
450
|
-
return Number(columnValue) >= Number(filterValue);
|
|
451
|
-
case 'lessThan':
|
|
452
|
-
return Number(columnValue) < Number(filterValue);
|
|
453
|
-
case 'lessThanOrEqual':
|
|
454
|
-
return Number(columnValue) <= Number(filterValue);
|
|
455
476
|
default:
|
|
456
477
|
return true;
|
|
457
478
|
}
|
|
@@ -101,6 +101,15 @@ export const SelectionFeature: TableFeature<any> = {
|
|
|
101
101
|
|
|
102
102
|
// Define the feature's table instance methods
|
|
103
103
|
createTable: <TData extends RowData>(table: Table<TData>): void => {
|
|
104
|
+
const getRowsForSelection = () => {
|
|
105
|
+
// In client mode with pagination we can inspect all loaded rows.
|
|
106
|
+
// In manual/server pagination only the loaded slice exists locally.
|
|
107
|
+
if (table.options.manualPagination) {
|
|
108
|
+
return table.getRowModel().rows;
|
|
109
|
+
}
|
|
110
|
+
return table.getPrePaginationRowModel?.().rows || table.getRowModel().rows;
|
|
111
|
+
};
|
|
112
|
+
|
|
104
113
|
table.setSelectionState = (updater) => {
|
|
105
114
|
if (!table.options.enableAdvanceSelection) return;
|
|
106
115
|
const safeUpdater: Updater<SelectionState> = (old) => {
|
|
@@ -284,17 +293,14 @@ export const SelectionFeature: TableFeature<any> = {
|
|
|
284
293
|
table.getSelectedRowIds = () => {
|
|
285
294
|
const state = table.getSelectionState();
|
|
286
295
|
if (state.type === 'exclude') {
|
|
287
|
-
|
|
288
|
-
'[SelectionFeature] getSelectedRowIds() is not accurate in exclude mode. Use getSelectionState() to interpret selection properly.'
|
|
289
|
-
);
|
|
290
|
-
return []; // Return empty to avoid misleading API
|
|
296
|
+
return table.getSelectedRows().map((row) => row.id);
|
|
291
297
|
}
|
|
292
298
|
return state.ids;
|
|
293
299
|
};
|
|
294
300
|
|
|
295
301
|
table.getSelectedRows = () => {
|
|
296
302
|
const state = table.getSelectionState();
|
|
297
|
-
const allRows =
|
|
303
|
+
const allRows = getRowsForSelection();
|
|
298
304
|
|
|
299
305
|
if (state.type === 'exclude') {
|
|
300
306
|
// Return all rows except excluded ones
|
|
@@ -24,6 +24,25 @@ declare module '@tanstack/react-table' {
|
|
|
24
24
|
align?: 'left' | 'center' | 'right';
|
|
25
25
|
filterable?: boolean;
|
|
26
26
|
hideInExport?: boolean;
|
|
27
|
+
exportHeader?: string | ((context: {
|
|
28
|
+
columnId: string;
|
|
29
|
+
defaultHeader: string;
|
|
30
|
+
columnDef: ColumnDefBase<TData, TValue>;
|
|
31
|
+
}) => string);
|
|
32
|
+
exportValue?: (context: {
|
|
33
|
+
value: any;
|
|
34
|
+
row: TData;
|
|
35
|
+
rowIndex: number;
|
|
36
|
+
columnId: string;
|
|
37
|
+
columnDef: ColumnDefBase<TData, TValue>;
|
|
38
|
+
}) => any;
|
|
39
|
+
exportFormat?: 'auto' | 'string' | 'number' | 'boolean' | 'json' | 'date' | ((context: {
|
|
40
|
+
value: any;
|
|
41
|
+
row: TData;
|
|
42
|
+
rowIndex: number;
|
|
43
|
+
columnId: string;
|
|
44
|
+
columnDef: ColumnDefBase<TData, TValue>;
|
|
45
|
+
}) => any);
|
|
27
46
|
wrapText?: boolean; // If true, text will wrap; if false, text will truncate with ellipsis (default: false)
|
|
28
47
|
editComponent?: React.ComponentType<{
|
|
29
48
|
value: any;
|
|
@@ -41,4 +60,4 @@ declare module '@tanstack/react-table' {
|
|
|
41
60
|
}
|
|
42
61
|
export type DataTableColumn<TData extends RowData, TValue = unknown> = ColumnDef<TData, TValue> & {
|
|
43
62
|
// All custom properties are now defined in the module augmentation above
|
|
44
|
-
}
|
|
63
|
+
}
|
|
@@ -2,6 +2,25 @@ import { ColumnPinningState, SortingState, ColumnOrderState, TableState, Row, Ta
|
|
|
2
2
|
|
|
3
3
|
import { ColumnFilterState } from './table.types';
|
|
4
4
|
import { SelectionState } from '../features';
|
|
5
|
+
import { ServerExportResult } from './export.types';
|
|
6
|
+
|
|
7
|
+
export interface DataRefreshApiOptions {
|
|
8
|
+
resetPagination?: boolean;
|
|
9
|
+
force?: boolean;
|
|
10
|
+
reason?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type DataRefreshApiInput = boolean | DataRefreshApiOptions;
|
|
14
|
+
|
|
15
|
+
export interface DataTableExportApiOptions {
|
|
16
|
+
filename?: string;
|
|
17
|
+
onlyVisibleColumns?: boolean;
|
|
18
|
+
onlySelectedRows?: boolean;
|
|
19
|
+
includeHeaders?: boolean;
|
|
20
|
+
chunkSize?: number;
|
|
21
|
+
strictTotalCheck?: boolean;
|
|
22
|
+
sanitizeCSV?: boolean;
|
|
23
|
+
}
|
|
5
24
|
|
|
6
25
|
export interface DataTableApi<T = any> {
|
|
7
26
|
// Column Management
|
|
@@ -87,6 +106,8 @@ export interface DataTableApi<T = any> {
|
|
|
87
106
|
// Selection state getters
|
|
88
107
|
getSelectionState: () => SelectionState; // Get selection state
|
|
89
108
|
getSelectedCount: () => number; // Get total selected count
|
|
109
|
+
// Returns selected rows that are currently loaded in the table instance.
|
|
110
|
+
// For server/manual pagination, use getSelectionState() for full selection intent.
|
|
90
111
|
getSelectedRows: () => Row<T>[]
|
|
91
112
|
// Selection state checks
|
|
92
113
|
isRowSelected: (rowId: string) => boolean;
|
|
@@ -95,9 +116,9 @@ export interface DataTableApi<T = any> {
|
|
|
95
116
|
// Data Management
|
|
96
117
|
data: {
|
|
97
118
|
// Refresh data with pagination reset
|
|
98
|
-
refresh: (
|
|
119
|
+
refresh: (options?: DataRefreshApiInput) => void;
|
|
99
120
|
// Reload data without all current states
|
|
100
|
-
reload: () => void;
|
|
121
|
+
reload: (options?: DataRefreshApiOptions) => void;
|
|
101
122
|
// Reset all data to initial state
|
|
102
123
|
resetAll: () => void;
|
|
103
124
|
|
|
@@ -148,24 +169,21 @@ export interface DataTableApi<T = any> {
|
|
|
148
169
|
|
|
149
170
|
// Simplified Export
|
|
150
171
|
export: {
|
|
151
|
-
exportCSV: (options?:
|
|
152
|
-
|
|
153
|
-
onlyVisibleColumns?: boolean;
|
|
154
|
-
onlySelectedRows?: boolean;
|
|
155
|
-
includeHeaders?: boolean;
|
|
156
|
-
}) => Promise<void>;
|
|
157
|
-
exportExcel: (options?: {
|
|
158
|
-
filename?: string;
|
|
159
|
-
onlyVisibleColumns?: boolean;
|
|
160
|
-
onlySelectedRows?: boolean;
|
|
161
|
-
includeHeaders?: boolean;
|
|
162
|
-
}) => Promise<void>;
|
|
172
|
+
exportCSV: (options?: DataTableExportApiOptions) => Promise<void>;
|
|
173
|
+
exportExcel: (options?: DataTableExportApiOptions) => Promise<void>;
|
|
163
174
|
exportServerData: (options: {
|
|
164
175
|
format: 'csv' | 'excel';
|
|
165
176
|
filename?: string;
|
|
166
|
-
fetchData: (
|
|
177
|
+
fetchData: (
|
|
178
|
+
filters?: Partial<TableState>,
|
|
179
|
+
selection?: SelectionState,
|
|
180
|
+
signal?: AbortSignal
|
|
181
|
+
) => Promise<ServerExportResult<T>>;
|
|
167
182
|
pageSize?: number;
|
|
168
183
|
includeHeaders?: boolean;
|
|
184
|
+
chunkSize?: number;
|
|
185
|
+
strictTotalCheck?: boolean;
|
|
186
|
+
sanitizeCSV?: boolean;
|
|
169
187
|
}) => Promise<void>;
|
|
170
188
|
isExporting: () => boolean;
|
|
171
189
|
cancelExport: () => void;
|
|
@@ -9,10 +9,54 @@ import { DataTableSlots, PartialSlotProps } from './slots.types';
|
|
|
9
9
|
import { DataTableSize } from '../utils/table-helpers';
|
|
10
10
|
import { SelectionState, SelectMode } from '../features';
|
|
11
11
|
import { DataTableLoggingOptions } from '../utils/logger';
|
|
12
|
+
import { ExportConcurrencyMode, ExportProgressPayload, ExportStateChange, ServerExportResult } from './export.types';
|
|
12
13
|
|
|
13
14
|
// Dynamic data management interfaces
|
|
14
15
|
// TableFilters now imported from types folder
|
|
15
16
|
|
|
17
|
+
export type DataRefreshReason = 'initial' | 'state-change' | 'refresh' | 'reload' | 'reset' | string;
|
|
18
|
+
|
|
19
|
+
export interface DataFetchMeta {
|
|
20
|
+
reason?: DataRefreshReason;
|
|
21
|
+
force?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DataRefreshOptions extends DataFetchMeta {
|
|
25
|
+
resetPagination?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DataRefreshContext {
|
|
29
|
+
filters: Partial<TableFilters>;
|
|
30
|
+
state: Partial<TableState>;
|
|
31
|
+
options: Required<Pick<DataRefreshOptions, 'resetPagination' | 'force'>> & {
|
|
32
|
+
reason: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type DataMutationAction =
|
|
37
|
+
| 'updateRow'
|
|
38
|
+
| 'updateRowByIndex'
|
|
39
|
+
| 'insertRow'
|
|
40
|
+
| 'deleteRow'
|
|
41
|
+
| 'deleteRowByIndex'
|
|
42
|
+
| 'deleteSelectedRows'
|
|
43
|
+
| 'replaceAllData'
|
|
44
|
+
| 'updateMultipleRows'
|
|
45
|
+
| 'insertMultipleRows'
|
|
46
|
+
| 'deleteMultipleRows'
|
|
47
|
+
| 'updateField'
|
|
48
|
+
| 'updateFieldByIndex';
|
|
49
|
+
|
|
50
|
+
export interface DataMutationContext<T> {
|
|
51
|
+
action: DataMutationAction;
|
|
52
|
+
previousData: T[];
|
|
53
|
+
nextData: T[];
|
|
54
|
+
rowId?: string;
|
|
55
|
+
index?: number;
|
|
56
|
+
rowIds?: string[];
|
|
57
|
+
totalRow?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
16
60
|
export interface DataTableProps<T> {
|
|
17
61
|
// Core data props
|
|
18
62
|
// columns: DataTableColumn<T>[] | AccessorKeyColumnDef <T, string>[];
|
|
@@ -28,16 +72,27 @@ export interface DataTableProps<T> {
|
|
|
28
72
|
initialState?: Partial<TableState>;
|
|
29
73
|
initialLoadData?: boolean; // Initial load data (default: true)
|
|
30
74
|
onDataStateChange?: (filters: Partial<TableState>) => void; // Callback when any filter/state changes
|
|
31
|
-
onFetchData?: (filters: Partial<TableFilters
|
|
75
|
+
onFetchData?: (filters: Partial<TableFilters>, meta?: DataFetchMeta) => Promise<{ data: T[]; total: number }>;
|
|
76
|
+
onRefreshData?: (context: DataRefreshContext) => void | Promise<void>;
|
|
77
|
+
onDataChange?: (nextData: T[], context: DataMutationContext<T>) => void;
|
|
32
78
|
|
|
33
79
|
// Simplified Export props
|
|
34
80
|
exportFilename?: string;
|
|
35
|
-
|
|
81
|
+
exportConcurrency?: ExportConcurrencyMode;
|
|
82
|
+
exportChunkSize?: number;
|
|
83
|
+
exportStrictTotalCheck?: boolean;
|
|
84
|
+
exportSanitizeCSV?: boolean;
|
|
85
|
+
onExportProgress?: (progress: ExportProgressPayload) => void;
|
|
36
86
|
onExportComplete?: (result: { success: boolean; filename: string; totalRows: number }) => void;
|
|
37
87
|
onExportError?: (error: { message: string; code: string }) => void;
|
|
88
|
+
onExportStateChange?: (state: ExportStateChange) => void;
|
|
38
89
|
|
|
39
90
|
// Server export callback - receives current table state/filters and selection data
|
|
40
|
-
onServerExport?: (
|
|
91
|
+
onServerExport?: (
|
|
92
|
+
filters?: Partial<TableState>,
|
|
93
|
+
selection?: SelectionState,
|
|
94
|
+
signal?: AbortSignal
|
|
95
|
+
) => Promise<ServerExportResult<any>>;
|
|
41
96
|
|
|
42
97
|
// Export cancellation callback - called when export is cancelled
|
|
43
98
|
onExportCancel?: () => void;
|
|
@@ -6,6 +6,64 @@
|
|
|
6
6
|
import { TableState } from './table.types';
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
export type ExportFormat = 'csv' | 'excel';
|
|
10
|
+
export type ExportConcurrencyMode = 'ignoreIfRunning' | 'cancelAndRestart' | 'queue';
|
|
11
|
+
export type ExportPhase =
|
|
12
|
+
| 'starting'
|
|
13
|
+
| 'fetching'
|
|
14
|
+
| 'processing'
|
|
15
|
+
| 'downloading'
|
|
16
|
+
| 'completed'
|
|
17
|
+
| 'cancelled'
|
|
18
|
+
| 'error';
|
|
19
|
+
|
|
20
|
+
export type ExportValueFormat = 'auto' | 'string' | 'number' | 'boolean' | 'json' | 'date';
|
|
21
|
+
|
|
22
|
+
export interface ExportStateChange {
|
|
23
|
+
phase: ExportPhase;
|
|
24
|
+
mode: 'client' | 'server';
|
|
25
|
+
format: ExportFormat;
|
|
26
|
+
filename: string;
|
|
27
|
+
processedRows?: number;
|
|
28
|
+
totalRows?: number;
|
|
29
|
+
percentage?: number;
|
|
30
|
+
message?: string;
|
|
31
|
+
code?: string;
|
|
32
|
+
startedAt?: number;
|
|
33
|
+
endedAt?: number;
|
|
34
|
+
queueLength?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ExportProgressPayload {
|
|
38
|
+
processedRows?: number;
|
|
39
|
+
totalRows?: number;
|
|
40
|
+
percentage?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ServerExportDataResult<T = any> {
|
|
44
|
+
data: T[];
|
|
45
|
+
total: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ServerExportBlobResult {
|
|
49
|
+
blob: Blob;
|
|
50
|
+
filename?: string;
|
|
51
|
+
mimeType?: string;
|
|
52
|
+
total?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ServerExportFileUrlResult {
|
|
56
|
+
fileUrl: string;
|
|
57
|
+
filename?: string;
|
|
58
|
+
mimeType?: string;
|
|
59
|
+
total?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type ServerExportResult<T = any> =
|
|
63
|
+
| ServerExportDataResult<T>
|
|
64
|
+
| ServerExportBlobResult
|
|
65
|
+
| ServerExportFileUrlResult;
|
|
66
|
+
|
|
9
67
|
/**
|
|
10
68
|
* Server export column configuration
|
|
11
69
|
*/
|
|
@@ -21,7 +79,7 @@ export interface ServerExportColumn<T = any> {
|
|
|
21
79
|
*/
|
|
22
80
|
export interface ExportOptions {
|
|
23
81
|
filename?: string;
|
|
24
|
-
format:
|
|
82
|
+
format: ExportFormat;
|
|
25
83
|
includeHeaders?: boolean;
|
|
26
84
|
onlyVisibleColumns?: boolean;
|
|
27
85
|
onlyFilteredData?: boolean;
|
|
@@ -37,11 +95,11 @@ export interface ExportOptions {
|
|
|
37
95
|
* Export progress information
|
|
38
96
|
*/
|
|
39
97
|
export interface ExportProgress {
|
|
40
|
-
processedRows
|
|
41
|
-
totalRows
|
|
42
|
-
percentage
|
|
43
|
-
currentChunk
|
|
44
|
-
totalChunks
|
|
98
|
+
processedRows?: number;
|
|
99
|
+
totalRows?: number;
|
|
100
|
+
percentage?: number;
|
|
101
|
+
currentChunk?: number;
|
|
102
|
+
totalChunks?: number;
|
|
45
103
|
estimatedTimeRemaining?: number;
|
|
46
104
|
}
|
|
47
105
|
|
|
@@ -62,7 +120,7 @@ export interface ExportResult {
|
|
|
62
120
|
*/
|
|
63
121
|
export interface ExportError {
|
|
64
122
|
message: string;
|
|
65
|
-
code: 'CANCELLED' | 'MEMORY_ERROR' | 'PROCESSING_ERROR' | 'UNKNOWN';
|
|
123
|
+
code: 'CANCELLED' | 'MEMORY_ERROR' | 'PROCESSING_ERROR' | 'UNKNOWN' | 'EXPORT_IN_PROGRESS';
|
|
66
124
|
details?: any;
|
|
67
125
|
}
|
|
68
126
|
|
|
@@ -71,13 +129,16 @@ export interface ExportError {
|
|
|
71
129
|
*/
|
|
72
130
|
export interface ExportConfig {
|
|
73
131
|
enabled: boolean;
|
|
74
|
-
formats:
|
|
132
|
+
formats: ExportFormat[];
|
|
75
133
|
filename?: string;
|
|
76
134
|
includeHeaders?: boolean;
|
|
77
135
|
onlyVisibleColumns?: boolean;
|
|
78
136
|
onlyFilteredData?: boolean;
|
|
79
137
|
// New configuration for large datasets
|
|
80
138
|
chunkSize?: number;
|
|
139
|
+
strictTotalCheck?: boolean;
|
|
140
|
+
sanitizeCSV?: boolean;
|
|
141
|
+
concurrency?: ExportConcurrencyMode;
|
|
81
142
|
enableProgressTracking?: boolean;
|
|
82
143
|
maxMemoryThreshold?: number; // MB
|
|
83
144
|
}
|
|
@@ -124,10 +185,13 @@ export interface PinnedColumnStyleOptions {
|
|
|
124
185
|
|
|
125
186
|
export interface SimpleExportOptions {
|
|
126
187
|
filename?: string;
|
|
127
|
-
format:
|
|
188
|
+
format: ExportFormat;
|
|
128
189
|
includeHeaders?: boolean;
|
|
129
190
|
onlyVisibleColumns?: boolean;
|
|
130
191
|
onlySelectedRows?: boolean;
|
|
192
|
+
chunkSize?: number;
|
|
193
|
+
strictTotalCheck?: boolean;
|
|
194
|
+
sanitizeCSV?: boolean;
|
|
131
195
|
}
|
|
132
196
|
|
|
133
197
|
/**
|
|
@@ -141,7 +205,11 @@ export interface SelectionExportData {
|
|
|
141
205
|
}
|
|
142
206
|
|
|
143
207
|
export interface ServerExportOptions extends SimpleExportOptions {
|
|
144
|
-
fetchData: (
|
|
208
|
+
fetchData: (
|
|
209
|
+
filters?: Partial<TableState>,
|
|
210
|
+
selection?: SelectionExportData,
|
|
211
|
+
signal?: AbortSignal
|
|
212
|
+
) => Promise<ServerExportResult<any>>;
|
|
145
213
|
currentFilters?: any; // Current table filters/state
|
|
146
214
|
pageSize?: number;
|
|
147
215
|
selection?: SelectionExportData;
|
|
@@ -151,4 +219,5 @@ export interface ExportCallbacks {
|
|
|
151
219
|
onProgress?: (progress: ExportProgress) => void;
|
|
152
220
|
onComplete?: (result: ExportResult) => void;
|
|
153
221
|
onError?: (error: ExportError) => void;
|
|
222
|
+
onStateChange?: (state: ExportStateChange) => void;
|
|
154
223
|
}
|
|
@@ -14,7 +14,7 @@ import { TableProps, TableContainerProps, BoxProps, ToolbarProps, TableRowProps,
|
|
|
14
14
|
import { Table, Row, Column } from '@tanstack/react-table';
|
|
15
15
|
import { ComponentType, ReactNode, HTMLAttributes, ComponentProps } from 'react';
|
|
16
16
|
|
|
17
|
-
import { DataTableColumn, TableFilters, ExportProgress, ExportResult, ExportError, ServerExportColumn } from './index';
|
|
17
|
+
import { DataTableColumn, TableFilters, ExportProgress, ExportResult, ExportError, ServerExportColumn, ExportStateChange } from './index';
|
|
18
18
|
import { DataTableSize } from '../utils/table-helpers';
|
|
19
19
|
import { DataTablePaginationProps } from "../components/pagination";
|
|
20
20
|
import { DataTableToolbarProps } from '../components/toolbar/data-table-toolbar';
|
|
@@ -83,6 +83,7 @@ export interface DataTableSlots<T = any> {
|
|
|
83
83
|
onExportComplete?: (result: ExportResult) => void;
|
|
84
84
|
onExportError?: (error: ExportError) => void;
|
|
85
85
|
onExportCancel?: () => void;
|
|
86
|
+
onExportStateChange?: (state: ExportStateChange) => void;
|
|
86
87
|
}>>;
|
|
87
88
|
|
|
88
89
|
header?: SlotComponent<EnhancedSlotProps<BaseSlotProps<T>, TableHeadProps & {
|
|
@@ -209,6 +210,7 @@ export interface DataTableSlots<T = any> {
|
|
|
209
210
|
onExportComplete?: (result: ExportResult) => void;
|
|
210
211
|
onExportError?: (error: ExportError) => void;
|
|
211
212
|
onExportCancel?: () => void;
|
|
213
|
+
onExportStateChange?: (state: ExportStateChange) => void;
|
|
212
214
|
}>>;
|
|
213
215
|
|
|
214
216
|
refreshButton?: SlotComponent<EnhancedSlotProps<BaseSlotProps<T>, ComponentProps<'button'> & {
|
|
@@ -49,12 +49,10 @@ export interface TableFilters {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export interface TableFiltersForFetch {
|
|
52
|
+
export interface TableFiltersForFetch extends Partial<TableFilters> {
|
|
53
53
|
search?: string;
|
|
54
54
|
page?: number;
|
|
55
55
|
pageSize?: number;
|
|
56
|
-
sorting?: SortingState;
|
|
57
|
-
columnFilter?: ColumnFilterState;
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
export interface ColumnFilterState {
|
|
@@ -1,53 +1,125 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { DataFetchMeta, TableFilters } from '../types';
|
|
4
4
|
|
|
5
5
|
const DEFAULT_DEBOUNCE_DELAY = 300;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
interface DebouncedFetchOptions {
|
|
8
|
+
debounceDelay?: number;
|
|
9
|
+
meta?: DataFetchMeta;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface useDebouncedFetchReturn<T extends Record<string, any>> {
|
|
13
|
+
debouncedFetch: (
|
|
14
|
+
filters: Partial<TableFilters>,
|
|
15
|
+
optionsOrDelay?: number | DebouncedFetchOptions
|
|
16
|
+
) => Promise<{ data: T[]; total: number } | null>;
|
|
9
17
|
isLoading: boolean;
|
|
10
18
|
}
|
|
11
19
|
|
|
20
|
+
interface PendingRequest<T extends Record<string, any>> {
|
|
21
|
+
id: number;
|
|
22
|
+
resolve: (value: { data: T[]; total: number } | null) => void;
|
|
23
|
+
reject: (reason?: unknown) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
export function useDebouncedFetch<T extends Record<string, any>>(
|
|
13
|
-
onFetchData: ((
|
|
27
|
+
onFetchData: ((
|
|
28
|
+
filters: Partial<TableFilters>,
|
|
29
|
+
meta?: DataFetchMeta
|
|
30
|
+
) => Promise<{ data: T[]; total: number }>) | undefined
|
|
14
31
|
): useDebouncedFetchReturn<T> {
|
|
15
32
|
const [isLoading, setIsLoading] = useState(false);
|
|
16
33
|
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
34
|
+
const pendingRequestRef = useRef<PendingRequest<T> | null>(null);
|
|
35
|
+
const latestRequestIdRef = useRef(0);
|
|
36
|
+
const activeRequestCountRef = useRef(0);
|
|
37
|
+
const isMountedRef = useRef(true);
|
|
17
38
|
|
|
18
|
-
const
|
|
39
|
+
const resetLoadingIfIdle = useCallback(() => {
|
|
40
|
+
if (!isMountedRef.current) return;
|
|
41
|
+
if (!debounceTimer.current && !pendingRequestRef.current && activeRequestCountRef.current === 0) {
|
|
42
|
+
setIsLoading(false);
|
|
43
|
+
}
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
const debouncedFetch = useCallback(async (
|
|
47
|
+
filters: Partial<TableFilters>,
|
|
48
|
+
optionsOrDelay: number | DebouncedFetchOptions = DEFAULT_DEBOUNCE_DELAY
|
|
49
|
+
) => {
|
|
19
50
|
if (!onFetchData) return null;
|
|
20
51
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
52
|
+
const options = typeof optionsOrDelay === 'number'
|
|
53
|
+
? { debounceDelay: optionsOrDelay }
|
|
54
|
+
: optionsOrDelay;
|
|
55
|
+
const debounceDelay = options.debounceDelay ?? DEFAULT_DEBOUNCE_DELAY;
|
|
56
|
+
const requestId = latestRequestIdRef.current + 1;
|
|
57
|
+
latestRequestIdRef.current = requestId;
|
|
58
|
+
|
|
59
|
+
// Clear existing timer and resolve pending debounced request.
|
|
24
60
|
if (debounceTimer.current) {
|
|
25
61
|
clearTimeout(debounceTimer.current);
|
|
62
|
+
debounceTimer.current = null;
|
|
26
63
|
}
|
|
64
|
+
if (pendingRequestRef.current) {
|
|
65
|
+
pendingRequestRef.current.resolve(null);
|
|
66
|
+
pendingRequestRef.current = null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setIsLoading(true);
|
|
70
|
+
|
|
71
|
+
return new Promise<{ data: T[]; total: number } | null>((resolve, reject) => {
|
|
72
|
+
pendingRequestRef.current = {
|
|
73
|
+
id: requestId,
|
|
74
|
+
resolve,
|
|
75
|
+
reject,
|
|
76
|
+
};
|
|
27
77
|
|
|
28
|
-
return new Promise<{ data: T[]; total: number } | null>((resolve) => {
|
|
29
78
|
debounceTimer.current = setTimeout(async () => {
|
|
30
|
-
|
|
79
|
+
const pendingRequest = pendingRequestRef.current;
|
|
80
|
+
if (!pendingRequest || pendingRequest.id !== requestId) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pendingRequestRef.current = null;
|
|
85
|
+
debounceTimer.current = null;
|
|
86
|
+
activeRequestCountRef.current += 1;
|
|
87
|
+
|
|
31
88
|
try {
|
|
32
|
-
const result = await onFetchData(filters);
|
|
33
|
-
|
|
89
|
+
const result = await onFetchData(filters, options.meta);
|
|
90
|
+
|
|
91
|
+
// Ignore stale responses if a newer request was queued.
|
|
92
|
+
if (requestId === latestRequestIdRef.current) {
|
|
93
|
+
resolve(result);
|
|
94
|
+
} else {
|
|
95
|
+
resolve(null);
|
|
96
|
+
}
|
|
34
97
|
} catch (error) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
98
|
+
if (requestId === latestRequestIdRef.current) {
|
|
99
|
+
reject(error);
|
|
100
|
+
} else {
|
|
101
|
+
resolve(null);
|
|
102
|
+
}
|
|
38
103
|
} finally {
|
|
39
|
-
|
|
104
|
+
activeRequestCountRef.current = Math.max(0, activeRequestCountRef.current - 1);
|
|
105
|
+
resetLoadingIfIdle();
|
|
40
106
|
}
|
|
41
107
|
}, debounceDelay);
|
|
42
108
|
});
|
|
43
|
-
}, [onFetchData]);
|
|
109
|
+
}, [onFetchData, resetLoadingIfIdle]);
|
|
44
110
|
|
|
45
111
|
// Cleanup timer on unmount
|
|
46
112
|
useEffect(() => {
|
|
47
|
-
|
|
113
|
+
isMountedRef.current = true;
|
|
48
114
|
return () => {
|
|
115
|
+
isMountedRef.current = false;
|
|
49
116
|
if (debounceTimer.current) {
|
|
50
117
|
clearTimeout(debounceTimer.current);
|
|
118
|
+
debounceTimer.current = null;
|
|
119
|
+
}
|
|
120
|
+
if (pendingRequestRef.current) {
|
|
121
|
+
pendingRequestRef.current.resolve(null);
|
|
122
|
+
pendingRequestRef.current = null;
|
|
51
123
|
}
|
|
52
124
|
};
|
|
53
125
|
}, []);
|