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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +143 -11
  2. package/dist/lib/components/droupdown/menu-dropdown.d.ts.map +1 -1
  3. package/dist/lib/components/droupdown/menu-dropdown.js +8 -1
  4. package/dist/lib/components/filters/filter-value-input.js +2 -2
  5. package/dist/lib/components/pagination/data-table-pagination.d.ts.map +1 -1
  6. package/dist/lib/components/pagination/data-table-pagination.js +10 -1
  7. package/dist/lib/components/toolbar/table-export-control.d.ts.map +1 -1
  8. package/dist/lib/components/toolbar/table-export-control.js +46 -12
  9. package/dist/lib/contexts/data-table-context.d.ts +7 -10
  10. package/dist/lib/contexts/data-table-context.d.ts.map +1 -1
  11. package/dist/lib/contexts/data-table-context.js +5 -1
  12. package/dist/lib/data-table.d.ts.map +1 -1
  13. package/dist/lib/data-table.js +517 -237
  14. package/dist/lib/features/column-filter.feature.js +38 -21
  15. package/dist/lib/features/selection.feature.d.ts.map +1 -1
  16. package/dist/lib/features/selection.feature.js +11 -3
  17. package/dist/lib/types/column.types.d.ts +19 -0
  18. package/dist/lib/types/column.types.d.ts.map +1 -1
  19. package/dist/lib/types/data-table-api.d.ts +24 -18
  20. package/dist/lib/types/data-table-api.d.ts.map +1 -1
  21. package/dist/lib/types/data-table.types.d.ts +37 -10
  22. package/dist/lib/types/data-table.types.d.ts.map +1 -1
  23. package/dist/lib/types/export.types.d.ts +57 -13
  24. package/dist/lib/types/export.types.d.ts.map +1 -1
  25. package/dist/lib/types/slots.types.d.ts +3 -1
  26. package/dist/lib/types/slots.types.d.ts.map +1 -1
  27. package/dist/lib/types/table.types.d.ts +1 -3
  28. package/dist/lib/types/table.types.d.ts.map +1 -1
  29. package/dist/lib/utils/debounced-fetch.utils.d.ts +8 -4
  30. package/dist/lib/utils/debounced-fetch.utils.d.ts.map +1 -1
  31. package/dist/lib/utils/debounced-fetch.utils.js +63 -14
  32. package/dist/lib/utils/export-utils.d.ts +14 -4
  33. package/dist/lib/utils/export-utils.d.ts.map +1 -1
  34. package/dist/lib/utils/export-utils.js +362 -66
  35. package/package.json +4 -2
  36. package/src/lib/components/droupdown/menu-dropdown.tsx +9 -3
  37. package/src/lib/components/filters/filter-value-input.tsx +2 -2
  38. package/src/lib/components/pagination/data-table-pagination.tsx +14 -2
  39. package/src/lib/components/toolbar/table-export-control.tsx +65 -9
  40. package/src/lib/contexts/data-table-context.tsx +16 -2
  41. package/src/lib/data-table.tsx +647 -231
  42. package/src/lib/features/column-filter.feature.ts +40 -19
  43. package/src/lib/features/selection.feature.ts +11 -5
  44. package/src/lib/types/column.types.ts +20 -1
  45. package/src/lib/types/data-table-api.ts +33 -15
  46. package/src/lib/types/data-table.types.ts +59 -3
  47. package/src/lib/types/export.types.ts +79 -10
  48. package/src/lib/types/slots.types.ts +3 -1
  49. package/src/lib/types/table.types.ts +1 -3
  50. package/src/lib/utils/debounced-fetch.utils.ts +90 -18
  51. package/src/lib/utils/export-utils.ts +496 -69
@@ -379,9 +379,16 @@ function evaluateFilterCondition(columnValue: any, operator: string, filterValue
379
379
 
380
380
  // --- Date type logic ---
381
381
  if (type === 'date') {
382
- const mCol = toMoment(columnValue);
383
- const mFilter = toMoment(filterValue);
384
- if (!mCol || !mFilter) return false;
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
- if (Array.isArray(filterValue)) {
421
- if (operator === 'in') return filterValue.includes(columnValue);
422
- if (operator === 'notIn') return !filterValue.includes(columnValue);
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
- return false;
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
- console.warn(
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 = table.getRowModel().rows;
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: (resetPagination?: boolean) => void;
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
- filename?: string;
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: (filters?: Partial<TableState>) => Promise<{ data: T[]; total: number }>;
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,28 @@ 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>) => Promise<{ data: T[]; total: number }>;
75
+ onFetchData?: (filters: Partial<TableFilters>, meta?: DataFetchMeta) => Promise<{ data: T[]; total: number }>;
76
+ onFetchStateChange?: (filters: Partial<TableState>, meta?: DataFetchMeta) => void;
77
+ onRefreshData?: (context: DataRefreshContext) => void | Promise<void>;
78
+ onDataChange?: (nextData: T[], context: DataMutationContext<T>) => void;
32
79
 
33
80
  // Simplified Export props
34
81
  exportFilename?: string;
35
- onExportProgress?: (progress: { processedRows?: number; totalRows?: number; percentage?: number }) => void;
82
+ exportConcurrency?: ExportConcurrencyMode;
83
+ exportChunkSize?: number;
84
+ exportStrictTotalCheck?: boolean;
85
+ exportSanitizeCSV?: boolean;
86
+ onExportProgress?: (progress: ExportProgressPayload) => void;
36
87
  onExportComplete?: (result: { success: boolean; filename: string; totalRows: number }) => void;
37
88
  onExportError?: (error: { message: string; code: string }) => void;
89
+ onExportStateChange?: (state: ExportStateChange) => void;
38
90
 
39
91
  // Server export callback - receives current table state/filters and selection data
40
- onServerExport?: (filters?: Partial<TableState>, selection?: SelectionState) => Promise<{ data: any[]; total: number }>;
92
+ onServerExport?: (
93
+ filters?: Partial<TableState>,
94
+ selection?: SelectionState,
95
+ signal?: AbortSignal
96
+ ) => Promise<ServerExportResult<any>>;
41
97
 
42
98
  // Export cancellation callback - called when export is cancelled
43
99
  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: 'csv' | 'excel';
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: number;
41
- totalRows: number;
42
- percentage: number;
43
- currentChunk: number;
44
- totalChunks: number;
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: ('csv' | 'excel')[];
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: 'csv' | 'excel';
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: (filters?: Partial<TableState>, selection?: SelectionExportData) => Promise<{ data: any[]; total: number }>;
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 { TableFiltersForFetch } from '../types';
3
+ import { DataFetchMeta, TableFilters } from '../types';
4
4
 
5
5
  const DEFAULT_DEBOUNCE_DELAY = 300;
6
6
 
7
- interface useDebouncedFetchReturn<T extends Record<string, any>> {
8
- debouncedFetch: (filters: TableFiltersForFetch, debounceDelay?: number) => Promise<{ data: T[]; total: number }>;
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: ((filters: TableFiltersForFetch) => Promise<{ data: T[]; total: number }>) | undefined
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 debouncedFetch = useCallback(async (filters: TableFiltersForFetch, debounceDelay = DEFAULT_DEBOUNCE_DELAY) => {
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
- // Create a unique key for the current fetch parameters
22
- // const currentParams = JSON.stringify(filters);
23
- // Clear existing timer
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
- setIsLoading(true);
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
- resolve(result);
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
- // Handle fetch error silently or could be passed to onError callback
36
- console.error('Error fetching data:', error);
37
- resolve(null);
98
+ if (requestId === latestRequestIdRef.current) {
99
+ reject(error);
100
+ } else {
101
+ resolve(null);
102
+ }
38
103
  } finally {
39
- setIsLoading(false);
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
- // Fetch data when dependencies change
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
  }, []);