@ackplus/react-tanstack-data-table 1.1.19 → 1.1.21
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/dist/lib/hooks/use-data-table-engine.d.ts.map +1 -1
- package/dist/lib/hooks/use-data-table-engine.js +54 -15
- package/dist/lib/types/data-table.types.d.ts +2 -1
- package/dist/lib/types/data-table.types.d.ts.map +1 -1
- package/package.json +3 -4
- package/src/index.ts +0 -75
- package/src/lib/components/data-table-view.tsx +0 -386
- package/src/lib/components/droupdown/menu-dropdown.tsx +0 -103
- package/src/lib/components/filters/filter-value-input.tsx +0 -225
- package/src/lib/components/filters/index.ts +0 -126
- package/src/lib/components/headers/draggable-header.tsx +0 -326
- package/src/lib/components/headers/index.ts +0 -6
- package/src/lib/components/headers/table-header.tsx +0 -175
- package/src/lib/components/index.ts +0 -21
- package/src/lib/components/pagination/data-table-pagination.tsx +0 -111
- package/src/lib/components/pagination/index.ts +0 -5
- package/src/lib/components/rows/data-table-row.tsx +0 -218
- package/src/lib/components/rows/empty-data-row.tsx +0 -69
- package/src/lib/components/rows/index.ts +0 -7
- package/src/lib/components/rows/loading-rows.tsx +0 -164
- package/src/lib/components/toolbar/bulk-actions-toolbar.tsx +0 -125
- package/src/lib/components/toolbar/column-filter-control.tsx +0 -432
- package/src/lib/components/toolbar/column-pinning-control.tsx +0 -275
- package/src/lib/components/toolbar/column-reset-control.tsx +0 -74
- package/src/lib/components/toolbar/column-visibility-control.tsx +0 -105
- package/src/lib/components/toolbar/data-table-toolbar.tsx +0 -257
- package/src/lib/components/toolbar/index.ts +0 -17
- package/src/lib/components/toolbar/table-export-control.tsx +0 -233
- package/src/lib/components/toolbar/table-refresh-control.tsx +0 -62
- package/src/lib/components/toolbar/table-search-control.tsx +0 -155
- package/src/lib/components/toolbar/table-size-control.tsx +0 -102
- package/src/lib/contexts/data-table-context.tsx +0 -126
- package/src/lib/data-table.tsx +0 -29
- package/src/lib/features/README.md +0 -161
- package/src/lib/features/column-filter.feature.ts +0 -493
- package/src/lib/features/index.ts +0 -23
- package/src/lib/features/selection.feature.ts +0 -322
- package/src/lib/hooks/index.ts +0 -2
- package/src/lib/hooks/use-data-table-engine.ts +0 -1516
- package/src/lib/icons/add-icon.tsx +0 -23
- package/src/lib/icons/csv-icon.tsx +0 -15
- package/src/lib/icons/delete-icon.tsx +0 -30
- package/src/lib/icons/excel-icon.tsx +0 -15
- package/src/lib/icons/index.ts +0 -7
- package/src/lib/icons/unpin-icon.tsx +0 -18
- package/src/lib/icons/view-comfortable-icon.tsx +0 -45
- package/src/lib/icons/view-compact-icon.tsx +0 -55
- package/src/lib/types/column.types.ts +0 -63
- package/src/lib/types/data-table-api.ts +0 -191
- package/src/lib/types/data-table.types.ts +0 -192
- package/src/lib/types/export.types.ts +0 -223
- package/src/lib/types/index.ts +0 -24
- package/src/lib/types/slots.types.ts +0 -342
- package/src/lib/types/table.types.ts +0 -88
- package/src/lib/utils/column-helpers.ts +0 -72
- package/src/lib/utils/debounced-fetch.utils.ts +0 -131
- package/src/lib/utils/export-utils.ts +0 -712
- package/src/lib/utils/index.ts +0 -27
- package/src/lib/utils/logger.ts +0 -203
- package/src/lib/utils/slot-helpers.tsx +0 -194
- package/src/lib/utils/special-columns.utils.ts +0 -101
- package/src/lib/utils/styling-helpers.ts +0 -126
- package/src/lib/utils/table-helpers.ts +0 -106
|
@@ -1,712 +0,0 @@
|
|
|
1
|
-
import { Table } from '@tanstack/react-table';
|
|
2
|
-
import * as XLSX from 'xlsx';
|
|
3
|
-
import { SelectionState } from '../features';
|
|
4
|
-
import { ExportPhase, ServerExportResult } from '../types/export.types';
|
|
5
|
-
|
|
6
|
-
// Local types for the utility functions (keep simpler for actual implementation)
|
|
7
|
-
export interface ExportOptions {
|
|
8
|
-
format: 'csv' | 'excel';
|
|
9
|
-
filename: string;
|
|
10
|
-
onProgress?: (progress: { processedRows?: number; totalRows?: number; percentage?: number }) => void;
|
|
11
|
-
onComplete?: (result: { success: boolean; filename: string; totalRows: number }) => void;
|
|
12
|
-
onError?: (error: { message: string; code: string }) => void;
|
|
13
|
-
onStateChange?: (state: {
|
|
14
|
-
phase: ExportPhase;
|
|
15
|
-
processedRows?: number;
|
|
16
|
-
totalRows?: number;
|
|
17
|
-
percentage?: number;
|
|
18
|
-
message?: string;
|
|
19
|
-
code?: string;
|
|
20
|
-
}) => void;
|
|
21
|
-
signal?: AbortSignal;
|
|
22
|
-
sanitizeCSV?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
export interface ServerExportOptions extends ExportOptions {
|
|
27
|
-
fetchData: (
|
|
28
|
-
filters?: any,
|
|
29
|
-
selection?: SelectionState,
|
|
30
|
-
signal?: AbortSignal
|
|
31
|
-
) => Promise<ServerExportResult<any>>;
|
|
32
|
-
currentFilters?: any;
|
|
33
|
-
selection?: SelectionState;
|
|
34
|
-
chunkSize?: number;
|
|
35
|
-
strictTotalCheck?: boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const EXPORT_CANCELLED_CODE = 'CANCELLED';
|
|
39
|
-
const DEFAULT_CHUNK_SIZE = 1000;
|
|
40
|
-
const MAX_SERVER_EXPORT_PAGES = 10000;
|
|
41
|
-
|
|
42
|
-
function createCancelledExportError(): Error & { code: string } {
|
|
43
|
-
const error = new Error('Export cancelled') as Error & { code: string };
|
|
44
|
-
error.name = 'AbortError';
|
|
45
|
-
error.code = EXPORT_CANCELLED_CODE;
|
|
46
|
-
return error;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function isCancelledError(error: unknown): boolean {
|
|
50
|
-
if (!(error instanceof Error)) return false;
|
|
51
|
-
return error.name === 'AbortError' || (error as any).code === EXPORT_CANCELLED_CODE;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function throwIfExportCancelled(signal?: AbortSignal): void {
|
|
55
|
-
if (signal?.aborted) {
|
|
56
|
-
throw createCancelledExportError();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function waitWithAbort(ms: number, signal?: AbortSignal): Promise<void> {
|
|
61
|
-
if (!signal) {
|
|
62
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return new Promise((resolve, reject) => {
|
|
66
|
-
const timer = setTimeout(() => {
|
|
67
|
-
signal.removeEventListener('abort', onAbort);
|
|
68
|
-
resolve();
|
|
69
|
-
}, ms);
|
|
70
|
-
|
|
71
|
-
const onAbort = () => {
|
|
72
|
-
clearTimeout(timer);
|
|
73
|
-
signal.removeEventListener('abort', onAbort);
|
|
74
|
-
reject(createCancelledExportError());
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function notifyState(
|
|
82
|
-
onStateChange: ExportOptions['onStateChange'],
|
|
83
|
-
state: {
|
|
84
|
-
phase: ExportPhase;
|
|
85
|
-
processedRows?: number;
|
|
86
|
-
totalRows?: number;
|
|
87
|
-
percentage?: number;
|
|
88
|
-
message?: string;
|
|
89
|
-
code?: string;
|
|
90
|
-
},
|
|
91
|
-
): void {
|
|
92
|
-
onStateChange?.(state);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function isServerExportDataResult(result: unknown): result is { data: any[]; total: number } {
|
|
96
|
-
return (
|
|
97
|
-
!!result
|
|
98
|
-
&& typeof result === 'object'
|
|
99
|
-
&& 'data' in (result as any)
|
|
100
|
-
&& Array.isArray((result as any).data)
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function isServerExportBlobResult(result: unknown): result is { blob: Blob; filename?: string; mimeType?: string; total?: number } {
|
|
105
|
-
return (
|
|
106
|
-
!!result
|
|
107
|
-
&& typeof result === 'object'
|
|
108
|
-
&& 'blob' in (result as any)
|
|
109
|
-
&& (result as any).blob instanceof Blob
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function isServerExportFileUrlResult(result: unknown): result is { fileUrl: string; filename?: string; mimeType?: string; total?: number } {
|
|
114
|
-
return (
|
|
115
|
-
!!result
|
|
116
|
-
&& typeof result === 'object'
|
|
117
|
-
&& typeof (result as any).fileUrl === 'string'
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function resolveExportHeader(columnDef: any, columnId: string): string {
|
|
122
|
-
const defaultHeader = typeof columnDef?.header === 'string' ? columnDef.header : columnId;
|
|
123
|
-
|
|
124
|
-
if (columnDef?.exportHeader === undefined || columnDef?.exportHeader === null) {
|
|
125
|
-
return defaultHeader;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (typeof columnDef.exportHeader === 'function') {
|
|
129
|
-
return String(
|
|
130
|
-
columnDef.exportHeader({
|
|
131
|
-
columnId,
|
|
132
|
-
defaultHeader,
|
|
133
|
-
columnDef,
|
|
134
|
-
}) ?? defaultHeader
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return String(columnDef.exportHeader);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function applyExportValueTransform(
|
|
142
|
-
columnDef: any,
|
|
143
|
-
value: any,
|
|
144
|
-
row: any,
|
|
145
|
-
rowIndex: number,
|
|
146
|
-
columnId: string,
|
|
147
|
-
) {
|
|
148
|
-
if (typeof columnDef?.exportValue === 'function') {
|
|
149
|
-
return columnDef.exportValue({
|
|
150
|
-
value,
|
|
151
|
-
row,
|
|
152
|
-
rowIndex,
|
|
153
|
-
columnId,
|
|
154
|
-
columnDef,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
return value;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function applyExportFormatTransform(
|
|
161
|
-
columnDef: any,
|
|
162
|
-
value: any,
|
|
163
|
-
row: any,
|
|
164
|
-
rowIndex: number,
|
|
165
|
-
columnId: string,
|
|
166
|
-
) {
|
|
167
|
-
const format = columnDef?.exportFormat;
|
|
168
|
-
if (!format || format === 'auto') {
|
|
169
|
-
return value;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (typeof format === 'function') {
|
|
173
|
-
return format({
|
|
174
|
-
value,
|
|
175
|
-
row,
|
|
176
|
-
rowIndex,
|
|
177
|
-
columnId,
|
|
178
|
-
columnDef,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (value === null || value === undefined) {
|
|
183
|
-
return '';
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
switch (format) {
|
|
187
|
-
case 'string':
|
|
188
|
-
return String(value);
|
|
189
|
-
case 'number':
|
|
190
|
-
return Number(value);
|
|
191
|
-
case 'boolean':
|
|
192
|
-
return Boolean(value);
|
|
193
|
-
case 'json':
|
|
194
|
-
return JSON.stringify(value);
|
|
195
|
-
case 'date':
|
|
196
|
-
if (value instanceof Date) return value.toISOString();
|
|
197
|
-
return String(value);
|
|
198
|
-
default:
|
|
199
|
-
return value;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function normalizeExportValue(value: any): any {
|
|
204
|
-
if (value === null || value === undefined) {
|
|
205
|
-
return '';
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (value instanceof Date) {
|
|
209
|
-
return value.toISOString();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (typeof value === 'object') {
|
|
213
|
-
return JSON.stringify(value);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return value;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Export data for client-side tables
|
|
221
|
-
* - If rows are selected, export only selected rows
|
|
222
|
-
* - Otherwise export all filtered/visible rows
|
|
223
|
-
* - Only export visible columns
|
|
224
|
-
*/
|
|
225
|
-
export async function exportClientData<TData>(
|
|
226
|
-
table: Table<TData>,
|
|
227
|
-
options: ExportOptions,
|
|
228
|
-
): Promise<void> {
|
|
229
|
-
const { format, filename, onProgress, onComplete, onError, onStateChange, signal, sanitizeCSV = true } = options;
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
throwIfExportCancelled(signal);
|
|
233
|
-
notifyState(onStateChange, { phase: 'starting' });
|
|
234
|
-
|
|
235
|
-
// Get selected rows if any are selected
|
|
236
|
-
// const selectedRowIds = Object.keys(table.getState().rowSelection).filter(
|
|
237
|
-
// key => table.getState().rowSelection[key]
|
|
238
|
-
// );
|
|
239
|
-
|
|
240
|
-
// const hasSelectedRows = selectedRowIds.length > 0;
|
|
241
|
-
|
|
242
|
-
// // Get the rows to export
|
|
243
|
-
// const rowsToExport = hasSelectedRows ? table.getSelectedRowModel().rows : table.getFilteredRowModel().rows;
|
|
244
|
-
|
|
245
|
-
const selectedRows = table.getSelectedRows ? table.getSelectedRows() : [];
|
|
246
|
-
const hasSelectedRows = selectedRows.length > 0;
|
|
247
|
-
const rowsToExport = hasSelectedRows ? selectedRows : table.getFilteredRowModel().rows;
|
|
248
|
-
|
|
249
|
-
// Prepare data for export - get all visible columns and their values, excluding hideInExport columns
|
|
250
|
-
const exportData: Record<string, any>[] = [];
|
|
251
|
-
const visibleColumns = table.getVisibleLeafColumns().filter((col) => col.columnDef.hideInExport !== true);
|
|
252
|
-
|
|
253
|
-
for (let index = 0; index < rowsToExport.length; index++) {
|
|
254
|
-
throwIfExportCancelled(signal);
|
|
255
|
-
const row = rowsToExport[index];
|
|
256
|
-
|
|
257
|
-
onProgress?.({
|
|
258
|
-
processedRows: index + 1,
|
|
259
|
-
totalRows: rowsToExport.length,
|
|
260
|
-
percentage: Math.round(((index + 1) / rowsToExport.length) * 100),
|
|
261
|
-
});
|
|
262
|
-
notifyState(onStateChange, {
|
|
263
|
-
phase: 'processing',
|
|
264
|
-
processedRows: index + 1,
|
|
265
|
-
totalRows: rowsToExport.length,
|
|
266
|
-
percentage: Math.round(((index + 1) / rowsToExport.length) * 100),
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
const rowData: Record<string, any> = {};
|
|
270
|
-
|
|
271
|
-
for (const column of visibleColumns) {
|
|
272
|
-
const columnDef = column.columnDef;
|
|
273
|
-
const header = resolveExportHeader(columnDef, column.id);
|
|
274
|
-
const cell = row.getVisibleCells().find((visibleCell) => visibleCell.column.id === column.id);
|
|
275
|
-
const baseValue = cell ? cell.getValue() : (row as any)?.original?.[column.id];
|
|
276
|
-
const transformedValue = applyExportFormatTransform(
|
|
277
|
-
columnDef,
|
|
278
|
-
applyExportValueTransform(columnDef, baseValue, row.original, index, column.id),
|
|
279
|
-
row.original,
|
|
280
|
-
index,
|
|
281
|
-
column.id,
|
|
282
|
-
);
|
|
283
|
-
rowData[header] = normalizeExportValue(transformedValue);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
exportData.push(rowData);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Export the data
|
|
290
|
-
notifyState(onStateChange, {
|
|
291
|
-
phase: 'downloading',
|
|
292
|
-
processedRows: exportData.length,
|
|
293
|
-
totalRows: exportData.length,
|
|
294
|
-
percentage: 100,
|
|
295
|
-
});
|
|
296
|
-
await exportToFile(exportData, format, filename, signal, sanitizeCSV);
|
|
297
|
-
|
|
298
|
-
onComplete?.({
|
|
299
|
-
success: true,
|
|
300
|
-
filename: `${filename}.${format === 'excel' ? 'xlsx' : 'csv'}`,
|
|
301
|
-
totalRows: exportData.length,
|
|
302
|
-
});
|
|
303
|
-
notifyState(onStateChange, {
|
|
304
|
-
phase: 'completed',
|
|
305
|
-
processedRows: exportData.length,
|
|
306
|
-
totalRows: exportData.length,
|
|
307
|
-
percentage: 100,
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
} catch (error) {
|
|
311
|
-
if (isCancelledError(error)) {
|
|
312
|
-
notifyState(onStateChange, { phase: 'cancelled', code: EXPORT_CANCELLED_CODE });
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
console.error('Client export failed:', error);
|
|
316
|
-
notifyState(onStateChange, {
|
|
317
|
-
phase: 'error',
|
|
318
|
-
message: error instanceof Error ? error.message : 'Export failed',
|
|
319
|
-
code: 'CLIENT_EXPORT_ERROR',
|
|
320
|
-
});
|
|
321
|
-
onError?.({
|
|
322
|
-
message: error instanceof Error ? error.message : 'Export failed',
|
|
323
|
-
code: 'CLIENT_EXPORT_ERROR',
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Export data for server-side tables
|
|
330
|
-
* - Fetch data using provided fetchData function
|
|
331
|
-
* - Pass selection information to server for filtering
|
|
332
|
-
* - Export all returned data (server handles selection/filtering)
|
|
333
|
-
*/
|
|
334
|
-
export async function exportServerData<TData>(
|
|
335
|
-
table: Table<TData>,
|
|
336
|
-
options: ServerExportOptions,
|
|
337
|
-
): Promise<void> {
|
|
338
|
-
const {
|
|
339
|
-
format,
|
|
340
|
-
filename,
|
|
341
|
-
fetchData,
|
|
342
|
-
currentFilters,
|
|
343
|
-
selection,
|
|
344
|
-
onProgress,
|
|
345
|
-
onComplete,
|
|
346
|
-
onError,
|
|
347
|
-
onStateChange,
|
|
348
|
-
signal,
|
|
349
|
-
chunkSize = DEFAULT_CHUNK_SIZE,
|
|
350
|
-
strictTotalCheck = false,
|
|
351
|
-
sanitizeCSV = true,
|
|
352
|
-
} = options;
|
|
353
|
-
|
|
354
|
-
try {
|
|
355
|
-
throwIfExportCancelled(signal);
|
|
356
|
-
notifyState(onStateChange, { phase: 'starting' });
|
|
357
|
-
|
|
358
|
-
// Initial progress
|
|
359
|
-
onProgress?.({ });
|
|
360
|
-
notifyState(onStateChange, { phase: 'fetching' });
|
|
361
|
-
|
|
362
|
-
// First, get total count to determine if we need chunking
|
|
363
|
-
const initialResponse = await fetchData({
|
|
364
|
-
...currentFilters,
|
|
365
|
-
pagination: { pageIndex: 0, pageSize: 1 }
|
|
366
|
-
}, selection, signal);
|
|
367
|
-
|
|
368
|
-
if (isServerExportBlobResult(initialResponse)) {
|
|
369
|
-
throwIfExportCancelled(signal);
|
|
370
|
-
const resolvedName = initialResponse.filename || `${filename}.${format === 'excel' ? 'xlsx' : 'csv'}`;
|
|
371
|
-
notifyState(onStateChange, { phase: 'downloading' });
|
|
372
|
-
downloadFile(
|
|
373
|
-
initialResponse.blob,
|
|
374
|
-
resolvedName,
|
|
375
|
-
initialResponse.mimeType || initialResponse.blob.type || 'application/octet-stream'
|
|
376
|
-
);
|
|
377
|
-
onComplete?.({
|
|
378
|
-
success: true,
|
|
379
|
-
filename: resolvedName,
|
|
380
|
-
totalRows: initialResponse.total ?? 0,
|
|
381
|
-
});
|
|
382
|
-
notifyState(onStateChange, { phase: 'completed' });
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (isServerExportFileUrlResult(initialResponse)) {
|
|
387
|
-
throwIfExportCancelled(signal);
|
|
388
|
-
const resolvedName = initialResponse.filename || `${filename}.${format === 'excel' ? 'xlsx' : 'csv'}`;
|
|
389
|
-
notifyState(onStateChange, { phase: 'downloading' });
|
|
390
|
-
await downloadFromUrl(
|
|
391
|
-
initialResponse.fileUrl,
|
|
392
|
-
resolvedName,
|
|
393
|
-
initialResponse.mimeType || 'application/octet-stream',
|
|
394
|
-
signal
|
|
395
|
-
);
|
|
396
|
-
onComplete?.({
|
|
397
|
-
success: true,
|
|
398
|
-
filename: resolvedName,
|
|
399
|
-
totalRows: initialResponse.total ?? 0,
|
|
400
|
-
});
|
|
401
|
-
notifyState(onStateChange, { phase: 'completed' });
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (!isServerExportDataResult(initialResponse)) {
|
|
406
|
-
throw new Error('Invalid data received from server');
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const totalRows = typeof initialResponse.total === 'number' ? initialResponse.total : initialResponse.data.length;
|
|
410
|
-
let allData: any[] = [];
|
|
411
|
-
const hasTotal = typeof totalRows === 'number' && totalRows >= 0;
|
|
412
|
-
|
|
413
|
-
for (let page = 0; page < MAX_SERVER_EXPORT_PAGES; page++) {
|
|
414
|
-
throwIfExportCancelled(signal);
|
|
415
|
-
|
|
416
|
-
const chunkFilters = {
|
|
417
|
-
...currentFilters,
|
|
418
|
-
pagination: {
|
|
419
|
-
pageIndex: page,
|
|
420
|
-
pageSize: chunkSize,
|
|
421
|
-
},
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
const chunkResponse = await fetchData(chunkFilters, selection, signal);
|
|
425
|
-
|
|
426
|
-
if (isServerExportBlobResult(chunkResponse)) {
|
|
427
|
-
throwIfExportCancelled(signal);
|
|
428
|
-
const resolvedName = chunkResponse.filename || `${filename}.${format === 'excel' ? 'xlsx' : 'csv'}`;
|
|
429
|
-
notifyState(onStateChange, { phase: 'downloading' });
|
|
430
|
-
downloadFile(
|
|
431
|
-
chunkResponse.blob,
|
|
432
|
-
resolvedName,
|
|
433
|
-
chunkResponse.mimeType || chunkResponse.blob.type || 'application/octet-stream'
|
|
434
|
-
);
|
|
435
|
-
onComplete?.({
|
|
436
|
-
success: true,
|
|
437
|
-
filename: resolvedName,
|
|
438
|
-
totalRows: chunkResponse.total ?? allData.length,
|
|
439
|
-
});
|
|
440
|
-
notifyState(onStateChange, { phase: 'completed' });
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (isServerExportFileUrlResult(chunkResponse)) {
|
|
445
|
-
throwIfExportCancelled(signal);
|
|
446
|
-
const resolvedName = chunkResponse.filename || `${filename}.${format === 'excel' ? 'xlsx' : 'csv'}`;
|
|
447
|
-
notifyState(onStateChange, { phase: 'downloading' });
|
|
448
|
-
await downloadFromUrl(
|
|
449
|
-
chunkResponse.fileUrl,
|
|
450
|
-
resolvedName,
|
|
451
|
-
chunkResponse.mimeType || 'application/octet-stream',
|
|
452
|
-
signal
|
|
453
|
-
);
|
|
454
|
-
onComplete?.({
|
|
455
|
-
success: true,
|
|
456
|
-
filename: resolvedName,
|
|
457
|
-
totalRows: chunkResponse.total ?? allData.length,
|
|
458
|
-
});
|
|
459
|
-
notifyState(onStateChange, { phase: 'completed' });
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (!isServerExportDataResult(chunkResponse)) {
|
|
464
|
-
throw new Error(`Failed to fetch chunk ${page + 1}`);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const chunkData = chunkResponse.data;
|
|
468
|
-
if (chunkData.length === 0) {
|
|
469
|
-
break;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
allData.push(...chunkData);
|
|
473
|
-
|
|
474
|
-
const percentage = hasTotal && totalRows > 0
|
|
475
|
-
? Math.min(100, Math.round((allData.length / totalRows) * 100))
|
|
476
|
-
: undefined;
|
|
477
|
-
onProgress?.({
|
|
478
|
-
processedRows: allData.length,
|
|
479
|
-
totalRows: hasTotal ? totalRows : undefined,
|
|
480
|
-
percentage,
|
|
481
|
-
});
|
|
482
|
-
notifyState(onStateChange, {
|
|
483
|
-
phase: 'fetching',
|
|
484
|
-
processedRows: allData.length,
|
|
485
|
-
totalRows: hasTotal ? totalRows : undefined,
|
|
486
|
-
percentage,
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
if (hasTotal) {
|
|
490
|
-
if (allData.length >= totalRows) {
|
|
491
|
-
break;
|
|
492
|
-
}
|
|
493
|
-
} else if (chunkData.length < chunkSize) {
|
|
494
|
-
break;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
await waitWithAbort(100, signal);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
if (hasTotal && allData.length > totalRows) {
|
|
501
|
-
allData = allData.slice(0, totalRows);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (hasTotal && strictTotalCheck && allData.length < totalRows) {
|
|
505
|
-
throw new Error(`Expected ${totalRows} rows for export but received ${allData.length}`);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
throwIfExportCancelled(signal);
|
|
509
|
-
|
|
510
|
-
// Get visible columns for proper headers and data processing, excluding hideInExport columns
|
|
511
|
-
const visibleColumns = table.getVisibleLeafColumns().filter(col => {
|
|
512
|
-
const columnDef = col.columnDef;
|
|
513
|
-
return col.getIsVisible() && columnDef.hideInExport !== true;
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
// Prepare data for export with proper column processing
|
|
517
|
-
const exportData: Record<string, any>[] = [];
|
|
518
|
-
|
|
519
|
-
for (let index = 0; index < allData.length; index++) {
|
|
520
|
-
throwIfExportCancelled(signal);
|
|
521
|
-
const rowData = allData[index];
|
|
522
|
-
|
|
523
|
-
const exportRow: Record<string, any> = {};
|
|
524
|
-
|
|
525
|
-
visibleColumns.forEach(column => {
|
|
526
|
-
const columnId = column.id;
|
|
527
|
-
const columnDef = column.columnDef;
|
|
528
|
-
const header = resolveExportHeader(columnDef, columnId);
|
|
529
|
-
|
|
530
|
-
// Get value from raw data
|
|
531
|
-
let value = rowData[columnId];
|
|
532
|
-
|
|
533
|
-
// Apply accessorFn if defined
|
|
534
|
-
if (column.accessorFn && typeof column.accessorFn === 'function') {
|
|
535
|
-
value = column.accessorFn(rowData, index);
|
|
536
|
-
}
|
|
537
|
-
value = applyExportValueTransform(columnDef, value, rowData, index, columnId);
|
|
538
|
-
value = applyExportFormatTransform(columnDef, value, rowData, index, columnId);
|
|
539
|
-
value = normalizeExportValue(value);
|
|
540
|
-
|
|
541
|
-
exportRow[header] = value;
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
exportData.push(exportRow);
|
|
545
|
-
if (allData.length > 0) {
|
|
546
|
-
notifyState(onStateChange, {
|
|
547
|
-
phase: 'processing',
|
|
548
|
-
processedRows: index + 1,
|
|
549
|
-
totalRows: allData.length,
|
|
550
|
-
percentage: Math.round(((index + 1) / allData.length) * 100),
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
notifyState(onStateChange, { phase: 'downloading' });
|
|
556
|
-
await exportToFile(exportData, format, filename, signal, sanitizeCSV);
|
|
557
|
-
|
|
558
|
-
onComplete?.({
|
|
559
|
-
success: true,
|
|
560
|
-
filename: `${filename}.${format === 'excel' ? 'xlsx' : 'csv'}`,
|
|
561
|
-
totalRows: exportData.length,
|
|
562
|
-
});
|
|
563
|
-
notifyState(onStateChange, {
|
|
564
|
-
phase: 'completed',
|
|
565
|
-
processedRows: exportData.length,
|
|
566
|
-
totalRows: exportData.length,
|
|
567
|
-
percentage: 100,
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
} catch (error) {
|
|
571
|
-
if (isCancelledError(error)) {
|
|
572
|
-
notifyState(onStateChange, { phase: 'cancelled', code: EXPORT_CANCELLED_CODE });
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
console.error('Server export failed:', error);
|
|
576
|
-
notifyState(onStateChange, {
|
|
577
|
-
phase: 'error',
|
|
578
|
-
message: error instanceof Error ? error.message : 'Export failed',
|
|
579
|
-
code: 'SERVER_EXPORT_ERROR',
|
|
580
|
-
});
|
|
581
|
-
onError?.({
|
|
582
|
-
message: error instanceof Error ? error.message : 'Export failed',
|
|
583
|
-
code: 'SERVER_EXPORT_ERROR',
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Export data to file (CSV or Excel)
|
|
590
|
-
*/
|
|
591
|
-
async function exportToFile(
|
|
592
|
-
data: Record<string, any>[],
|
|
593
|
-
format: 'csv' | 'excel',
|
|
594
|
-
filename: string,
|
|
595
|
-
signal?: AbortSignal,
|
|
596
|
-
sanitizeCSV: boolean = true,
|
|
597
|
-
): Promise<void> {
|
|
598
|
-
throwIfExportCancelled(signal);
|
|
599
|
-
|
|
600
|
-
if (data.length === 0) {
|
|
601
|
-
throw new Error('No data to export');
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
if (format === 'csv') {
|
|
605
|
-
const csv = convertToCSV(data, sanitizeCSV);
|
|
606
|
-
throwIfExportCancelled(signal);
|
|
607
|
-
downloadFile(csv, `${filename}.csv`, 'text/csv');
|
|
608
|
-
} else {
|
|
609
|
-
const workbook = XLSX.utils.book_new();
|
|
610
|
-
const worksheet = XLSX.utils.json_to_sheet(data);
|
|
611
|
-
XLSX.utils.book_append_sheet(workbook, worksheet, 'Data');
|
|
612
|
-
|
|
613
|
-
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
|
614
|
-
const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
|
615
|
-
throwIfExportCancelled(signal);
|
|
616
|
-
downloadFile(blob, `${filename}.xlsx`, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Convert data to CSV format
|
|
622
|
-
*/
|
|
623
|
-
function convertToCSV(data: Record<string, any>[], sanitizeCSV: boolean): string {
|
|
624
|
-
if (data.length === 0) return '';
|
|
625
|
-
|
|
626
|
-
const headers = Object.keys(data[0]);
|
|
627
|
-
const csvRows = [headers.join(',')];
|
|
628
|
-
|
|
629
|
-
for (const row of data) {
|
|
630
|
-
const values = headers.map(header => {
|
|
631
|
-
const rawValue = row[header] ?? '';
|
|
632
|
-
const normalizedValue = sanitizeCSV ? sanitizeCSVCellValue(rawValue) : rawValue;
|
|
633
|
-
const value = normalizedValue === null || normalizedValue === undefined
|
|
634
|
-
? ''
|
|
635
|
-
: String(normalizedValue);
|
|
636
|
-
// Escape quotes and wrap in quotes if contains comma or quote
|
|
637
|
-
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
|
638
|
-
return `"${value.replace(/"/g, '""')}"`;
|
|
639
|
-
}
|
|
640
|
-
return value;
|
|
641
|
-
});
|
|
642
|
-
csvRows.push(values.join(','));
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
return csvRows.join('\n');
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function sanitizeCSVCellValue(value: any): any {
|
|
649
|
-
if (typeof value !== 'string' || value.length === 0) {
|
|
650
|
-
return value;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
const firstCharacter = value[0];
|
|
654
|
-
if (firstCharacter === '=' || firstCharacter === '+' || firstCharacter === '-' || firstCharacter === '@') {
|
|
655
|
-
return `'${value}`;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
return value;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
async function downloadFromUrl(
|
|
662
|
-
url: string,
|
|
663
|
-
filename: string,
|
|
664
|
-
mimeType: string,
|
|
665
|
-
signal?: AbortSignal,
|
|
666
|
-
): Promise<void> {
|
|
667
|
-
throwIfExportCancelled(signal);
|
|
668
|
-
try {
|
|
669
|
-
const response = await fetch(url, { signal });
|
|
670
|
-
if (!response.ok) {
|
|
671
|
-
throw new Error(`Failed to download export file from URL (${response.status})`);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const blob = await response.blob();
|
|
675
|
-
throwIfExportCancelled(signal);
|
|
676
|
-
downloadFile(blob, filename, mimeType || blob.type || 'application/octet-stream');
|
|
677
|
-
return;
|
|
678
|
-
} catch (error) {
|
|
679
|
-
if (isCancelledError(error)) {
|
|
680
|
-
throw error;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// Fallback for URLs that block fetch due to CORS - browser handles URL directly.
|
|
684
|
-
const link = document.createElement('a');
|
|
685
|
-
link.href = url;
|
|
686
|
-
link.download = filename;
|
|
687
|
-
link.style.display = 'none';
|
|
688
|
-
|
|
689
|
-
document.body.appendChild(link);
|
|
690
|
-
link.click();
|
|
691
|
-
document.body.removeChild(link);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
/**
|
|
696
|
-
* Download file to user's device
|
|
697
|
-
*/
|
|
698
|
-
function downloadFile(content: string | Blob, filename: string, mimeType: string): void {
|
|
699
|
-
const blob = content instanceof Blob ? content : new Blob([content], { type: mimeType });
|
|
700
|
-
const url = URL.createObjectURL(blob);
|
|
701
|
-
|
|
702
|
-
const link = document.createElement('a');
|
|
703
|
-
link.href = url;
|
|
704
|
-
link.download = filename;
|
|
705
|
-
link.style.display = 'none';
|
|
706
|
-
|
|
707
|
-
document.body.appendChild(link);
|
|
708
|
-
link.click();
|
|
709
|
-
document.body.removeChild(link);
|
|
710
|
-
|
|
711
|
-
URL.revokeObjectURL(url);
|
|
712
|
-
}
|