@ackplus/react-tanstack-data-table 1.1.20 → 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.
Files changed (61) hide show
  1. package/dist/lib/hooks/use-data-table-engine.d.ts.map +1 -1
  2. package/dist/lib/hooks/use-data-table-engine.js +4 -0
  3. package/package.json +3 -4
  4. package/src/index.ts +0 -75
  5. package/src/lib/components/data-table-view.tsx +0 -386
  6. package/src/lib/components/droupdown/menu-dropdown.tsx +0 -103
  7. package/src/lib/components/filters/filter-value-input.tsx +0 -225
  8. package/src/lib/components/filters/index.ts +0 -126
  9. package/src/lib/components/headers/draggable-header.tsx +0 -326
  10. package/src/lib/components/headers/index.ts +0 -6
  11. package/src/lib/components/headers/table-header.tsx +0 -175
  12. package/src/lib/components/index.ts +0 -21
  13. package/src/lib/components/pagination/data-table-pagination.tsx +0 -111
  14. package/src/lib/components/pagination/index.ts +0 -5
  15. package/src/lib/components/rows/data-table-row.tsx +0 -218
  16. package/src/lib/components/rows/empty-data-row.tsx +0 -69
  17. package/src/lib/components/rows/index.ts +0 -7
  18. package/src/lib/components/rows/loading-rows.tsx +0 -164
  19. package/src/lib/components/toolbar/bulk-actions-toolbar.tsx +0 -125
  20. package/src/lib/components/toolbar/column-filter-control.tsx +0 -432
  21. package/src/lib/components/toolbar/column-pinning-control.tsx +0 -275
  22. package/src/lib/components/toolbar/column-reset-control.tsx +0 -74
  23. package/src/lib/components/toolbar/column-visibility-control.tsx +0 -105
  24. package/src/lib/components/toolbar/data-table-toolbar.tsx +0 -257
  25. package/src/lib/components/toolbar/index.ts +0 -17
  26. package/src/lib/components/toolbar/table-export-control.tsx +0 -233
  27. package/src/lib/components/toolbar/table-refresh-control.tsx +0 -62
  28. package/src/lib/components/toolbar/table-search-control.tsx +0 -155
  29. package/src/lib/components/toolbar/table-size-control.tsx +0 -102
  30. package/src/lib/contexts/data-table-context.tsx +0 -126
  31. package/src/lib/data-table.tsx +0 -29
  32. package/src/lib/features/README.md +0 -161
  33. package/src/lib/features/column-filter.feature.ts +0 -493
  34. package/src/lib/features/index.ts +0 -23
  35. package/src/lib/features/selection.feature.ts +0 -322
  36. package/src/lib/hooks/index.ts +0 -2
  37. package/src/lib/hooks/use-data-table-engine.ts +0 -1552
  38. package/src/lib/icons/add-icon.tsx +0 -23
  39. package/src/lib/icons/csv-icon.tsx +0 -15
  40. package/src/lib/icons/delete-icon.tsx +0 -30
  41. package/src/lib/icons/excel-icon.tsx +0 -15
  42. package/src/lib/icons/index.ts +0 -7
  43. package/src/lib/icons/unpin-icon.tsx +0 -18
  44. package/src/lib/icons/view-comfortable-icon.tsx +0 -45
  45. package/src/lib/icons/view-compact-icon.tsx +0 -55
  46. package/src/lib/types/column.types.ts +0 -63
  47. package/src/lib/types/data-table-api.ts +0 -191
  48. package/src/lib/types/data-table.types.ts +0 -193
  49. package/src/lib/types/export.types.ts +0 -223
  50. package/src/lib/types/index.ts +0 -24
  51. package/src/lib/types/slots.types.ts +0 -342
  52. package/src/lib/types/table.types.ts +0 -88
  53. package/src/lib/utils/column-helpers.ts +0 -72
  54. package/src/lib/utils/debounced-fetch.utils.ts +0 -131
  55. package/src/lib/utils/export-utils.ts +0 -712
  56. package/src/lib/utils/index.ts +0 -27
  57. package/src/lib/utils/logger.ts +0 -203
  58. package/src/lib/utils/slot-helpers.tsx +0 -194
  59. package/src/lib/utils/special-columns.utils.ts +0 -101
  60. package/src/lib/utils/styling-helpers.ts +0 -126
  61. 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
- }