@ackplus/react-tanstack-data-table 1.1.11 → 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.
Files changed (61) 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/data-table-toolbar.d.ts.map +1 -1
  8. package/dist/lib/components/toolbar/data-table-toolbar.js +5 -2
  9. package/dist/lib/components/toolbar/table-export-control.d.ts.map +1 -1
  10. package/dist/lib/components/toolbar/table-export-control.js +46 -12
  11. package/dist/lib/components/toolbar/table-refresh-control.d.ts +15 -0
  12. package/dist/lib/components/toolbar/table-refresh-control.d.ts.map +1 -0
  13. package/dist/lib/components/toolbar/table-refresh-control.js +61 -0
  14. package/dist/lib/contexts/data-table-context.d.ts +7 -10
  15. package/dist/lib/contexts/data-table-context.d.ts.map +1 -1
  16. package/dist/lib/contexts/data-table-context.js +5 -1
  17. package/dist/lib/data-table.d.ts.map +1 -1
  18. package/dist/lib/data-table.js +1110 -946
  19. package/dist/lib/features/column-filter.feature.js +38 -21
  20. package/dist/lib/features/selection.feature.d.ts.map +1 -1
  21. package/dist/lib/features/selection.feature.js +11 -3
  22. package/dist/lib/types/column.types.d.ts +19 -0
  23. package/dist/lib/types/column.types.d.ts.map +1 -1
  24. package/dist/lib/types/data-table-api.d.ts +25 -18
  25. package/dist/lib/types/data-table-api.d.ts.map +1 -1
  26. package/dist/lib/types/data-table.types.d.ts +37 -10
  27. package/dist/lib/types/data-table.types.d.ts.map +1 -1
  28. package/dist/lib/types/export.types.d.ts +57 -13
  29. package/dist/lib/types/export.types.d.ts.map +1 -1
  30. package/dist/lib/types/slots.types.d.ts +12 -1
  31. package/dist/lib/types/slots.types.d.ts.map +1 -1
  32. package/dist/lib/types/table.types.d.ts +1 -3
  33. package/dist/lib/types/table.types.d.ts.map +1 -1
  34. package/dist/lib/utils/debounced-fetch.utils.d.ts +8 -4
  35. package/dist/lib/utils/debounced-fetch.utils.d.ts.map +1 -1
  36. package/dist/lib/utils/debounced-fetch.utils.js +63 -14
  37. package/dist/lib/utils/export-utils.d.ts +14 -4
  38. package/dist/lib/utils/export-utils.d.ts.map +1 -1
  39. package/dist/lib/utils/export-utils.js +362 -66
  40. package/dist/lib/utils/slot-helpers.d.ts +1 -1
  41. package/dist/lib/utils/slot-helpers.d.ts.map +1 -1
  42. package/package.json +4 -2
  43. package/src/lib/components/droupdown/menu-dropdown.tsx +9 -3
  44. package/src/lib/components/filters/filter-value-input.tsx +2 -2
  45. package/src/lib/components/pagination/data-table-pagination.tsx +14 -2
  46. package/src/lib/components/toolbar/data-table-toolbar.tsx +15 -1
  47. package/src/lib/components/toolbar/table-export-control.tsx +65 -9
  48. package/src/lib/components/toolbar/table-refresh-control.tsx +58 -0
  49. package/src/lib/contexts/data-table-context.tsx +16 -2
  50. package/src/lib/data-table.tsx +1282 -932
  51. package/src/lib/features/column-filter.feature.ts +40 -19
  52. package/src/lib/features/selection.feature.ts +11 -5
  53. package/src/lib/types/column.types.ts +20 -1
  54. package/src/lib/types/data-table-api.ts +37 -15
  55. package/src/lib/types/data-table.types.ts +59 -3
  56. package/src/lib/types/export.types.ts +79 -10
  57. package/src/lib/types/slots.types.ts +11 -1
  58. package/src/lib/types/table.types.ts +1 -3
  59. package/src/lib/utils/debounced-fetch.utils.ts +90 -18
  60. package/src/lib/utils/export-utils.ts +496 -69
  61. package/src/lib/utils/slot-helpers.tsx +1 -1
@@ -36,6 +36,135 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.exportClientData = exportClientData;
37
37
  exports.exportServerData = exportServerData;
38
38
  const XLSX = __importStar(require("xlsx"));
39
+ const EXPORT_CANCELLED_CODE = 'CANCELLED';
40
+ const DEFAULT_CHUNK_SIZE = 1000;
41
+ const MAX_SERVER_EXPORT_PAGES = 10000;
42
+ function createCancelledExportError() {
43
+ const error = new Error('Export cancelled');
44
+ error.name = 'AbortError';
45
+ error.code = EXPORT_CANCELLED_CODE;
46
+ return error;
47
+ }
48
+ function isCancelledError(error) {
49
+ if (!(error instanceof Error))
50
+ return false;
51
+ return error.name === 'AbortError' || error.code === EXPORT_CANCELLED_CODE;
52
+ }
53
+ function throwIfExportCancelled(signal) {
54
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
55
+ throw createCancelledExportError();
56
+ }
57
+ }
58
+ function waitWithAbort(ms, signal) {
59
+ if (!signal) {
60
+ return new Promise((resolve) => setTimeout(resolve, ms));
61
+ }
62
+ return new Promise((resolve, reject) => {
63
+ const timer = setTimeout(() => {
64
+ signal.removeEventListener('abort', onAbort);
65
+ resolve();
66
+ }, ms);
67
+ const onAbort = () => {
68
+ clearTimeout(timer);
69
+ signal.removeEventListener('abort', onAbort);
70
+ reject(createCancelledExportError());
71
+ };
72
+ signal.addEventListener('abort', onAbort, { once: true });
73
+ });
74
+ }
75
+ function notifyState(onStateChange, state) {
76
+ onStateChange === null || onStateChange === void 0 ? void 0 : onStateChange(state);
77
+ }
78
+ function isServerExportDataResult(result) {
79
+ return (!!result
80
+ && typeof result === 'object'
81
+ && 'data' in result
82
+ && Array.isArray(result.data));
83
+ }
84
+ function isServerExportBlobResult(result) {
85
+ return (!!result
86
+ && typeof result === 'object'
87
+ && 'blob' in result
88
+ && result.blob instanceof Blob);
89
+ }
90
+ function isServerExportFileUrlResult(result) {
91
+ return (!!result
92
+ && typeof result === 'object'
93
+ && typeof result.fileUrl === 'string');
94
+ }
95
+ function resolveExportHeader(columnDef, columnId) {
96
+ var _a;
97
+ const defaultHeader = typeof (columnDef === null || columnDef === void 0 ? void 0 : columnDef.header) === 'string' ? columnDef.header : columnId;
98
+ if ((columnDef === null || columnDef === void 0 ? void 0 : columnDef.exportHeader) === undefined || (columnDef === null || columnDef === void 0 ? void 0 : columnDef.exportHeader) === null) {
99
+ return defaultHeader;
100
+ }
101
+ if (typeof columnDef.exportHeader === 'function') {
102
+ return String((_a = columnDef.exportHeader({
103
+ columnId,
104
+ defaultHeader,
105
+ columnDef,
106
+ })) !== null && _a !== void 0 ? _a : defaultHeader);
107
+ }
108
+ return String(columnDef.exportHeader);
109
+ }
110
+ function applyExportValueTransform(columnDef, value, row, rowIndex, columnId) {
111
+ if (typeof (columnDef === null || columnDef === void 0 ? void 0 : columnDef.exportValue) === 'function') {
112
+ return columnDef.exportValue({
113
+ value,
114
+ row,
115
+ rowIndex,
116
+ columnId,
117
+ columnDef,
118
+ });
119
+ }
120
+ return value;
121
+ }
122
+ function applyExportFormatTransform(columnDef, value, row, rowIndex, columnId) {
123
+ const format = columnDef === null || columnDef === void 0 ? void 0 : columnDef.exportFormat;
124
+ if (!format || format === 'auto') {
125
+ return value;
126
+ }
127
+ if (typeof format === 'function') {
128
+ return format({
129
+ value,
130
+ row,
131
+ rowIndex,
132
+ columnId,
133
+ columnDef,
134
+ });
135
+ }
136
+ if (value === null || value === undefined) {
137
+ return '';
138
+ }
139
+ switch (format) {
140
+ case 'string':
141
+ return String(value);
142
+ case 'number':
143
+ return Number(value);
144
+ case 'boolean':
145
+ return Boolean(value);
146
+ case 'json':
147
+ return JSON.stringify(value);
148
+ case 'date':
149
+ if (value instanceof Date)
150
+ return value.toISOString();
151
+ return String(value);
152
+ default:
153
+ return value;
154
+ }
155
+ }
156
+ function normalizeExportValue(value) {
157
+ if (value === null || value === undefined) {
158
+ return '';
159
+ }
160
+ if (value instanceof Date) {
161
+ return value.toISOString();
162
+ }
163
+ if (typeof value === 'object') {
164
+ return JSON.stringify(value);
165
+ }
166
+ return value;
167
+ }
39
168
  /**
40
169
  * Export data for client-side tables
41
170
  * - If rows are selected, export only selected rows
@@ -43,8 +172,11 @@ const XLSX = __importStar(require("xlsx"));
43
172
  * - Only export visible columns
44
173
  */
45
174
  async function exportClientData(table, options) {
46
- const { format, filename, onProgress, onComplete, onError } = options;
175
+ var _a;
176
+ const { format, filename, onProgress, onComplete, onError, onStateChange, signal, sanitizeCSV = true } = options;
47
177
  try {
178
+ throwIfExportCancelled(signal);
179
+ notifyState(onStateChange, { phase: 'starting' });
48
180
  // Get selected rows if any are selected
49
181
  // const selectedRowIds = Object.keys(table.getState().rowSelection).filter(
50
182
  // key => table.getState().rowSelection[key]
@@ -56,36 +188,64 @@ async function exportClientData(table, options) {
56
188
  const hasSelectedRows = selectedRows.length > 0;
57
189
  const rowsToExport = hasSelectedRows ? selectedRows : table.getFilteredRowModel().rows;
58
190
  // Prepare data for export - get all visible columns and their values, excluding hideInExport columns
59
- const exportData = rowsToExport.map((row, index) => {
191
+ const exportData = [];
192
+ const visibleColumns = table.getVisibleLeafColumns().filter((col) => col.columnDef.hideInExport !== true);
193
+ for (let index = 0; index < rowsToExport.length; index++) {
194
+ throwIfExportCancelled(signal);
195
+ const row = rowsToExport[index];
60
196
  onProgress === null || onProgress === void 0 ? void 0 : onProgress({
61
197
  processedRows: index + 1,
62
198
  totalRows: rowsToExport.length,
63
199
  percentage: Math.round(((index + 1) / rowsToExport.length) * 100),
64
200
  });
65
- const rowData = {};
66
- // Get all visible cells for this row, excluding columns marked as hideInExport
67
- row.getVisibleCells().forEach(cell => {
68
- const columnDef = cell.column.columnDef;
69
- // Skip columns marked as hideInExport
70
- if (columnDef.hideInExport === true) {
71
- return;
72
- }
73
- const header = typeof columnDef.header === 'string' ? columnDef.header : cell.column.id;
74
- // Use getValue() - it already handles all formatting
75
- rowData[header] = cell.getValue() || '';
201
+ notifyState(onStateChange, {
202
+ phase: 'processing',
203
+ processedRows: index + 1,
204
+ totalRows: rowsToExport.length,
205
+ percentage: Math.round(((index + 1) / rowsToExport.length) * 100),
76
206
  });
77
- return rowData;
78
- });
207
+ const rowData = {};
208
+ for (const column of visibleColumns) {
209
+ const columnDef = column.columnDef;
210
+ const header = resolveExportHeader(columnDef, column.id);
211
+ const cell = row.getVisibleCells().find((visibleCell) => visibleCell.column.id === column.id);
212
+ const baseValue = cell ? cell.getValue() : (_a = row === null || row === void 0 ? void 0 : row.original) === null || _a === void 0 ? void 0 : _a[column.id];
213
+ const transformedValue = applyExportFormatTransform(columnDef, applyExportValueTransform(columnDef, baseValue, row.original, index, column.id), row.original, index, column.id);
214
+ rowData[header] = normalizeExportValue(transformedValue);
215
+ }
216
+ exportData.push(rowData);
217
+ }
79
218
  // Export the data
80
- await exportToFile(exportData, format, filename);
219
+ notifyState(onStateChange, {
220
+ phase: 'downloading',
221
+ processedRows: exportData.length,
222
+ totalRows: exportData.length,
223
+ percentage: 100,
224
+ });
225
+ await exportToFile(exportData, format, filename, signal, sanitizeCSV);
81
226
  onComplete === null || onComplete === void 0 ? void 0 : onComplete({
82
227
  success: true,
83
228
  filename: `${filename}.${format === 'excel' ? 'xlsx' : 'csv'}`,
84
229
  totalRows: exportData.length,
85
230
  });
231
+ notifyState(onStateChange, {
232
+ phase: 'completed',
233
+ processedRows: exportData.length,
234
+ totalRows: exportData.length,
235
+ percentage: 100,
236
+ });
86
237
  }
87
238
  catch (error) {
239
+ if (isCancelledError(error)) {
240
+ notifyState(onStateChange, { phase: 'cancelled', code: EXPORT_CANCELLED_CODE });
241
+ return;
242
+ }
88
243
  console.error('Client export failed:', error);
244
+ notifyState(onStateChange, {
245
+ phase: 'error',
246
+ message: error instanceof Error ? error.message : 'Export failed',
247
+ code: 'CLIENT_EXPORT_ERROR',
248
+ });
89
249
  onError === null || onError === void 0 ? void 0 : onError({
90
250
  message: error instanceof Error ? error.message : 'Export failed',
91
251
  code: 'CLIENT_EXPORT_ERROR',
@@ -99,49 +259,126 @@ async function exportClientData(table, options) {
99
259
  * - Export all returned data (server handles selection/filtering)
100
260
  */
101
261
  async function exportServerData(table, options) {
102
- const { format, filename, fetchData, currentFilters, selection, onProgress, onComplete, onError } = options;
262
+ var _a, _b, _c, _d;
263
+ const { format, filename, fetchData, currentFilters, selection, onProgress, onComplete, onError, onStateChange, signal, chunkSize = DEFAULT_CHUNK_SIZE, strictTotalCheck = false, sanitizeCSV = true, } = options;
103
264
  try {
265
+ throwIfExportCancelled(signal);
266
+ notifyState(onStateChange, { phase: 'starting' });
104
267
  // Initial progress
105
268
  onProgress === null || onProgress === void 0 ? void 0 : onProgress({});
269
+ notifyState(onStateChange, { phase: 'fetching' });
106
270
  // First, get total count to determine if we need chunking
107
271
  const initialResponse = await fetchData({
108
272
  ...currentFilters,
109
273
  pagination: { pageIndex: 0, pageSize: 1 }
110
- }, selection);
111
- if (!initialResponse || !initialResponse.data || !Array.isArray(initialResponse.data)) {
274
+ }, selection, signal);
275
+ if (isServerExportBlobResult(initialResponse)) {
276
+ throwIfExportCancelled(signal);
277
+ const resolvedName = initialResponse.filename || `${filename}.${format === 'excel' ? 'xlsx' : 'csv'}`;
278
+ notifyState(onStateChange, { phase: 'downloading' });
279
+ downloadFile(initialResponse.blob, resolvedName, initialResponse.mimeType || initialResponse.blob.type || 'application/octet-stream');
280
+ onComplete === null || onComplete === void 0 ? void 0 : onComplete({
281
+ success: true,
282
+ filename: resolvedName,
283
+ totalRows: (_a = initialResponse.total) !== null && _a !== void 0 ? _a : 0,
284
+ });
285
+ notifyState(onStateChange, { phase: 'completed' });
286
+ return;
287
+ }
288
+ if (isServerExportFileUrlResult(initialResponse)) {
289
+ throwIfExportCancelled(signal);
290
+ const resolvedName = initialResponse.filename || `${filename}.${format === 'excel' ? 'xlsx' : 'csv'}`;
291
+ notifyState(onStateChange, { phase: 'downloading' });
292
+ await downloadFromUrl(initialResponse.fileUrl, resolvedName, initialResponse.mimeType || 'application/octet-stream', signal);
293
+ onComplete === null || onComplete === void 0 ? void 0 : onComplete({
294
+ success: true,
295
+ filename: resolvedName,
296
+ totalRows: (_b = initialResponse.total) !== null && _b !== void 0 ? _b : 0,
297
+ });
298
+ notifyState(onStateChange, { phase: 'completed' });
299
+ return;
300
+ }
301
+ if (!isServerExportDataResult(initialResponse)) {
112
302
  throw new Error('Invalid data received from server');
113
303
  }
114
- const totalRows = initialResponse.total || initialResponse.data.length;
115
- const CHUNK_SIZE = 1000; // Fetch 1000 rows per request
116
- const needsChunking = totalRows > CHUNK_SIZE;
304
+ const totalRows = typeof initialResponse.total === 'number' ? initialResponse.total : initialResponse.data.length;
117
305
  let allData = [];
118
- if (needsChunking) {
119
- // Fetch data in chunks (no progress events during fetching)
120
- const totalPages = Math.ceil(totalRows / CHUNK_SIZE);
121
- for (let page = 1; page <= totalPages; page++) {
122
- // Fetch current chunk
123
- const chunkFilters = {
124
- ...currentFilters,
125
- pagination: {
126
- pageIndex: page - 1,
127
- pageSize: CHUNK_SIZE,
128
- },
129
- };
130
- const chunkResponse = await fetchData(chunkFilters, selection);
131
- if (!chunkResponse || !chunkResponse.data || !Array.isArray(chunkResponse.data)) {
132
- throw new Error(`Failed to fetch chunk ${page}`);
133
- }
134
- allData = [...allData, ...chunkResponse.data];
135
- // Small delay to prevent overwhelming the server
136
- if (page < totalPages) {
137
- await new Promise(resolve => setTimeout(resolve, 100));
306
+ const hasTotal = typeof totalRows === 'number' && totalRows >= 0;
307
+ for (let page = 0; page < MAX_SERVER_EXPORT_PAGES; page++) {
308
+ throwIfExportCancelled(signal);
309
+ const chunkFilters = {
310
+ ...currentFilters,
311
+ pagination: {
312
+ pageIndex: page,
313
+ pageSize: chunkSize,
314
+ },
315
+ };
316
+ const chunkResponse = await fetchData(chunkFilters, selection, signal);
317
+ if (isServerExportBlobResult(chunkResponse)) {
318
+ throwIfExportCancelled(signal);
319
+ const resolvedName = chunkResponse.filename || `${filename}.${format === 'excel' ? 'xlsx' : 'csv'}`;
320
+ notifyState(onStateChange, { phase: 'downloading' });
321
+ downloadFile(chunkResponse.blob, resolvedName, chunkResponse.mimeType || chunkResponse.blob.type || 'application/octet-stream');
322
+ onComplete === null || onComplete === void 0 ? void 0 : onComplete({
323
+ success: true,
324
+ filename: resolvedName,
325
+ totalRows: (_c = chunkResponse.total) !== null && _c !== void 0 ? _c : allData.length,
326
+ });
327
+ notifyState(onStateChange, { phase: 'completed' });
328
+ return;
329
+ }
330
+ if (isServerExportFileUrlResult(chunkResponse)) {
331
+ throwIfExportCancelled(signal);
332
+ const resolvedName = chunkResponse.filename || `${filename}.${format === 'excel' ? 'xlsx' : 'csv'}`;
333
+ notifyState(onStateChange, { phase: 'downloading' });
334
+ await downloadFromUrl(chunkResponse.fileUrl, resolvedName, chunkResponse.mimeType || 'application/octet-stream', signal);
335
+ onComplete === null || onComplete === void 0 ? void 0 : onComplete({
336
+ success: true,
337
+ filename: resolvedName,
338
+ totalRows: (_d = chunkResponse.total) !== null && _d !== void 0 ? _d : allData.length,
339
+ });
340
+ notifyState(onStateChange, { phase: 'completed' });
341
+ return;
342
+ }
343
+ if (!isServerExportDataResult(chunkResponse)) {
344
+ throw new Error(`Failed to fetch chunk ${page + 1}`);
345
+ }
346
+ const chunkData = chunkResponse.data;
347
+ if (chunkData.length === 0) {
348
+ break;
349
+ }
350
+ allData.push(...chunkData);
351
+ const percentage = hasTotal && totalRows > 0
352
+ ? Math.min(100, Math.round((allData.length / totalRows) * 100))
353
+ : undefined;
354
+ onProgress === null || onProgress === void 0 ? void 0 : onProgress({
355
+ processedRows: allData.length,
356
+ totalRows: hasTotal ? totalRows : undefined,
357
+ percentage,
358
+ });
359
+ notifyState(onStateChange, {
360
+ phase: 'fetching',
361
+ processedRows: allData.length,
362
+ totalRows: hasTotal ? totalRows : undefined,
363
+ percentage,
364
+ });
365
+ if (hasTotal) {
366
+ if (allData.length >= totalRows) {
367
+ break;
138
368
  }
139
369
  }
370
+ else if (chunkData.length < chunkSize) {
371
+ break;
372
+ }
373
+ await waitWithAbort(100, signal);
374
+ }
375
+ if (hasTotal && allData.length > totalRows) {
376
+ allData = allData.slice(0, totalRows);
140
377
  }
141
- else {
142
- // Small dataset, use single request
143
- allData = initialResponse.data;
378
+ if (hasTotal && strictTotalCheck && allData.length < totalRows) {
379
+ throw new Error(`Expected ${totalRows} rows for export but received ${allData.length}`);
144
380
  }
381
+ throwIfExportCancelled(signal);
145
382
  // Get visible columns for proper headers and data processing, excluding hideInExport columns
146
383
  const visibleColumns = table.getVisibleLeafColumns().filter(col => {
147
384
  const columnDef = col.columnDef;
@@ -150,44 +387,59 @@ async function exportServerData(table, options) {
150
387
  // Prepare data for export with proper column processing
151
388
  const exportData = [];
152
389
  for (let index = 0; index < allData.length; index++) {
390
+ throwIfExportCancelled(signal);
153
391
  const rowData = allData[index];
154
392
  const exportRow = {};
155
393
  visibleColumns.forEach(column => {
156
- var _a;
157
394
  const columnId = column.id;
158
395
  const columnDef = column.columnDef;
159
- const header = typeof columnDef.header === 'string'
160
- ? columnDef.header
161
- : columnId;
396
+ const header = resolveExportHeader(columnDef, columnId);
162
397
  // Get value from raw data
163
398
  let value = rowData[columnId];
164
399
  // Apply accessorFn if defined
165
400
  if (column.accessorFn && typeof column.accessorFn === 'function') {
166
- value = ((_a = (column.accessorFn(rowData, index) || '')) === null || _a === void 0 ? void 0 : _a.toString()) || '';
167
- }
168
- // Convert to string for export
169
- if (value === null || value === undefined) {
170
- value = '';
171
- }
172
- else if (typeof value === 'object') {
173
- value = JSON.stringify(value);
174
- }
175
- else {
176
- value = String(value);
401
+ value = column.accessorFn(rowData, index);
177
402
  }
403
+ value = applyExportValueTransform(columnDef, value, rowData, index, columnId);
404
+ value = applyExportFormatTransform(columnDef, value, rowData, index, columnId);
405
+ value = normalizeExportValue(value);
178
406
  exportRow[header] = value;
179
407
  });
180
408
  exportData.push(exportRow);
409
+ if (allData.length > 0) {
410
+ notifyState(onStateChange, {
411
+ phase: 'processing',
412
+ processedRows: index + 1,
413
+ totalRows: allData.length,
414
+ percentage: Math.round(((index + 1) / allData.length) * 100),
415
+ });
416
+ }
181
417
  }
182
- await exportToFile(exportData, format, filename);
418
+ notifyState(onStateChange, { phase: 'downloading' });
419
+ await exportToFile(exportData, format, filename, signal, sanitizeCSV);
183
420
  onComplete === null || onComplete === void 0 ? void 0 : onComplete({
184
421
  success: true,
185
422
  filename: `${filename}.${format === 'excel' ? 'xlsx' : 'csv'}`,
186
423
  totalRows: exportData.length,
187
424
  });
425
+ notifyState(onStateChange, {
426
+ phase: 'completed',
427
+ processedRows: exportData.length,
428
+ totalRows: exportData.length,
429
+ percentage: 100,
430
+ });
188
431
  }
189
432
  catch (error) {
433
+ if (isCancelledError(error)) {
434
+ notifyState(onStateChange, { phase: 'cancelled', code: EXPORT_CANCELLED_CODE });
435
+ return;
436
+ }
190
437
  console.error('Server export failed:', error);
438
+ notifyState(onStateChange, {
439
+ phase: 'error',
440
+ message: error instanceof Error ? error.message : 'Export failed',
441
+ code: 'SERVER_EXPORT_ERROR',
442
+ });
191
443
  onError === null || onError === void 0 ? void 0 : onError({
192
444
  message: error instanceof Error ? error.message : 'Export failed',
193
445
  code: 'SERVER_EXPORT_ERROR',
@@ -197,12 +449,14 @@ async function exportServerData(table, options) {
197
449
  /**
198
450
  * Export data to file (CSV or Excel)
199
451
  */
200
- async function exportToFile(data, format, filename) {
452
+ async function exportToFile(data, format, filename, signal, sanitizeCSV = true) {
453
+ throwIfExportCancelled(signal);
201
454
  if (data.length === 0) {
202
455
  throw new Error('No data to export');
203
456
  }
204
457
  if (format === 'csv') {
205
- const csv = convertToCSV(data);
458
+ const csv = convertToCSV(data, sanitizeCSV);
459
+ throwIfExportCancelled(signal);
206
460
  downloadFile(csv, `${filename}.csv`, 'text/csv');
207
461
  }
208
462
  else {
@@ -211,22 +465,28 @@ async function exportToFile(data, format, filename) {
211
465
  XLSX.utils.book_append_sheet(workbook, worksheet, 'Data');
212
466
  const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
213
467
  const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
468
+ throwIfExportCancelled(signal);
214
469
  downloadFile(blob, `${filename}.xlsx`, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
215
470
  }
216
471
  }
217
472
  /**
218
473
  * Convert data to CSV format
219
474
  */
220
- function convertToCSV(data) {
475
+ function convertToCSV(data, sanitizeCSV) {
221
476
  if (data.length === 0)
222
477
  return '';
223
478
  const headers = Object.keys(data[0]);
224
479
  const csvRows = [headers.join(',')];
225
480
  for (const row of data) {
226
481
  const values = headers.map(header => {
227
- const value = row[header] || '';
482
+ var _a;
483
+ const rawValue = (_a = row[header]) !== null && _a !== void 0 ? _a : '';
484
+ const normalizedValue = sanitizeCSV ? sanitizeCSVCellValue(rawValue) : rawValue;
485
+ const value = normalizedValue === null || normalizedValue === undefined
486
+ ? ''
487
+ : String(normalizedValue);
228
488
  // Escape quotes and wrap in quotes if contains comma or quote
229
- if (typeof value === 'string' && (value.includes(',') || value.includes('"') || value.includes('\n'))) {
489
+ if (value.includes(',') || value.includes('"') || value.includes('\n')) {
230
490
  return `"${value.replace(/"/g, '""')}"`;
231
491
  }
232
492
  return value;
@@ -235,6 +495,42 @@ function convertToCSV(data) {
235
495
  }
236
496
  return csvRows.join('\n');
237
497
  }
498
+ function sanitizeCSVCellValue(value) {
499
+ if (typeof value !== 'string' || value.length === 0) {
500
+ return value;
501
+ }
502
+ const firstCharacter = value[0];
503
+ if (firstCharacter === '=' || firstCharacter === '+' || firstCharacter === '-' || firstCharacter === '@') {
504
+ return `'${value}`;
505
+ }
506
+ return value;
507
+ }
508
+ async function downloadFromUrl(url, filename, mimeType, signal) {
509
+ throwIfExportCancelled(signal);
510
+ try {
511
+ const response = await fetch(url, { signal });
512
+ if (!response.ok) {
513
+ throw new Error(`Failed to download export file from URL (${response.status})`);
514
+ }
515
+ const blob = await response.blob();
516
+ throwIfExportCancelled(signal);
517
+ downloadFile(blob, filename, mimeType || blob.type || 'application/octet-stream');
518
+ return;
519
+ }
520
+ catch (error) {
521
+ if (isCancelledError(error)) {
522
+ throw error;
523
+ }
524
+ // Fallback for URLs that block fetch due to CORS - browser handles URL directly.
525
+ const link = document.createElement('a');
526
+ link.href = url;
527
+ link.download = filename;
528
+ link.style.display = 'none';
529
+ document.body.appendChild(link);
530
+ link.click();
531
+ document.body.removeChild(link);
532
+ }
533
+ }
238
534
  /**
239
535
  * Download file to user's device
240
536
  */
@@ -45,7 +45,7 @@ export declare function validateSlotProps<T, K extends keyof DataTableSlots<T>>(
45
45
  /**
46
46
  * Helper to create slot props with proper typing
47
47
  */
48
- export declare function createSlotProps<T>(table: any, additionalProps?: Record<string, any>): Record<string, any>;
48
+ export declare function createSlotProps(table: any, additionalProps?: Record<string, any>): Record<string, any>;
49
49
  /**
50
50
  * Enhanced slot component wrapper that handles all prop merging automatically
51
51
  */
@@ -1 +1 @@
1
- {"version":3,"file":"slot-helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/slot-helpers.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"slot-helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/slot-helpers.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ackplus/react-tanstack-data-table",
3
3
  "type": "commonjs",
4
- "version": "1.1.11",
4
+ "version": "1.1.13",
5
5
  "description": "A powerful React data table component built with MUI and TanStack Table",
6
6
  "keywords": [
7
7
  "react",
@@ -42,7 +42,9 @@
42
42
  "src"
43
43
  ],
44
44
  "scripts": {
45
- "build": "tsc -p tsconfig.build.json"
45
+ "build": "tsc -p tsconfig.build.json",
46
+ "lint": "tsc -p tsconfig.build.json --noEmit",
47
+ "test": "pnpm run build && node --test tests/column-filter.feature.test.cjs"
46
48
  },
47
49
  "dependencies": {
48
50
  "lodash": "^4.17.21",
@@ -36,10 +36,10 @@ export function MenuDropdown({
36
36
  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
37
37
  const isOpen = useMemo(() => Boolean(anchorEl), [anchorEl]);
38
38
  const handleClick = useCallback(
39
- (event: React.MouseEvent<HTMLButtonElement>) => {
39
+ (event: React.MouseEvent<HTMLElement>) => {
40
40
  event.preventDefault();
41
41
  event.stopPropagation();
42
- setAnchorEl(event.currentTarget);
42
+ setAnchorEl(event.currentTarget as HTMLElement);
43
43
  },
44
44
  [],
45
45
  );
@@ -54,8 +54,14 @@ export function MenuDropdown({
54
54
  if (typeof anchor === 'function') {
55
55
  node = anchor({ isOpen: isOpen });
56
56
  }
57
+ const existingOnClick = (node as ReactElement<any>).props?.onClick;
57
58
  return cloneElement(node as ReactElement<any>, {
58
- onClick: handleClick,
59
+ onClick: (event: React.MouseEvent<HTMLElement>) => {
60
+ existingOnClick?.(event);
61
+ if (!event.defaultPrevented) {
62
+ handleClick(event);
63
+ }
64
+ },
59
65
  });
60
66
  }
61
67
  return <Button onClick={handleClick}>{label as any}</Button>;
@@ -100,8 +100,8 @@ export function FilterValueInput<T>(props: FilterValueInputProps<T>): ReactEleme
100
100
 
101
101
  // Select type with options
102
102
  if (options && options.length > 0) {
103
- // Multi-select for 'in' operator
104
- if (operator === 'in') {
103
+ // Multi-select for set operators
104
+ if (operator === 'in' || operator === 'notIn') {
105
105
  const currentValue = Array.isArray(filter.value) ? filter.value : [];
106
106
 
107
107
  return (