react-restyle-components 0.4.40 → 0.4.41
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/lib/src/core-components/src/components/Table/Table.js +30 -5
- package/lib/src/core-components/src/components/ag-grid/AgGrid.d.ts +11 -0
- package/lib/src/core-components/src/components/ag-grid/AgGrid.js +733 -0
- package/lib/src/core-components/src/components/ag-grid/elements.d.ts +246 -0
- package/lib/src/core-components/src/components/ag-grid/elements.js +1156 -0
- package/lib/src/core-components/src/components/ag-grid/hooks.d.ts +196 -0
- package/lib/src/core-components/src/components/ag-grid/hooks.js +943 -0
- package/lib/src/core-components/src/components/ag-grid/index.d.ts +9 -0
- package/lib/src/core-components/src/components/ag-grid/index.js +13 -0
- package/lib/src/core-components/src/components/ag-grid/types.d.ts +1367 -0
- package/lib/src/core-components/src/components/ag-grid/types.js +6 -0
- package/lib/src/core-components/src/components/index.d.ts +1 -0
- package/lib/src/core-components/src/components/index.js +1 -0
- package/lib/src/core-components/src/tc.global.css +5 -3
- package/lib/src/core-components/src/tc.module.css +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AG Grid Hooks
|
|
3
|
+
* Custom React hooks for AG Grid-like functionality
|
|
4
|
+
*/
|
|
5
|
+
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
|
6
|
+
export function useSorting(initialSort = [], multiSort = false) {
|
|
7
|
+
const [sortModel, setSortModel] = useState(initialSort);
|
|
8
|
+
const handleSort = useCallback((colId, multiSortKey = false) => {
|
|
9
|
+
setSortModel((prev) => {
|
|
10
|
+
const existing = prev.find((s) => s.colId === colId);
|
|
11
|
+
let nextSort;
|
|
12
|
+
if (!existing) {
|
|
13
|
+
nextSort = 'asc';
|
|
14
|
+
}
|
|
15
|
+
else if (existing.sort === 'asc') {
|
|
16
|
+
nextSort = 'desc';
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
nextSort = null;
|
|
20
|
+
}
|
|
21
|
+
if (multiSort && multiSortKey) {
|
|
22
|
+
// Multi-sort: add/update/remove from list
|
|
23
|
+
if (nextSort === null) {
|
|
24
|
+
return prev.filter((s) => s.colId !== colId);
|
|
25
|
+
}
|
|
26
|
+
if (existing) {
|
|
27
|
+
return prev.map((s) => s.colId === colId ? { ...s, sort: nextSort } : s);
|
|
28
|
+
}
|
|
29
|
+
return [...prev, { colId, sort: nextSort }];
|
|
30
|
+
}
|
|
31
|
+
// Single sort
|
|
32
|
+
if (nextSort === null) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
return [{ colId, sort: nextSort }];
|
|
36
|
+
});
|
|
37
|
+
}, [multiSort]);
|
|
38
|
+
const getSortForColumn = useCallback((colId) => {
|
|
39
|
+
const index = sortModel.findIndex((s) => s.colId === colId);
|
|
40
|
+
if (index === -1)
|
|
41
|
+
return { sort: null };
|
|
42
|
+
return {
|
|
43
|
+
sort: sortModel[index].sort,
|
|
44
|
+
sortIndex: multiSort ? index : undefined,
|
|
45
|
+
};
|
|
46
|
+
}, [sortModel, multiSort]);
|
|
47
|
+
const clearSort = useCallback(() => {
|
|
48
|
+
setSortModel([]);
|
|
49
|
+
}, []);
|
|
50
|
+
// Apply sorting to data
|
|
51
|
+
const sortData = useCallback((data, columns) => {
|
|
52
|
+
if (sortModel.length === 0)
|
|
53
|
+
return data;
|
|
54
|
+
return [...data].sort((a, b) => {
|
|
55
|
+
for (const { colId, sort } of sortModel) {
|
|
56
|
+
if (!sort)
|
|
57
|
+
continue;
|
|
58
|
+
const col = columns.find((c) => c.field === colId || c.colId === colId);
|
|
59
|
+
if (!col || !col.field)
|
|
60
|
+
continue;
|
|
61
|
+
const field = col.field;
|
|
62
|
+
const aVal = getNestedValue(a, field);
|
|
63
|
+
const bVal = getNestedValue(b, field);
|
|
64
|
+
// Custom comparator
|
|
65
|
+
if (col.comparator) {
|
|
66
|
+
const nodeA = createRowNode(a, 0);
|
|
67
|
+
const nodeB = createRowNode(b, 0);
|
|
68
|
+
const result = col.comparator(aVal, bVal, nodeA, nodeB, sort === 'desc');
|
|
69
|
+
if (result !== 0)
|
|
70
|
+
return sort === 'asc' ? result : -result;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// Default comparison
|
|
74
|
+
let comparison = 0;
|
|
75
|
+
if (aVal === bVal)
|
|
76
|
+
comparison = 0;
|
|
77
|
+
else if (aVal == null)
|
|
78
|
+
comparison = 1;
|
|
79
|
+
else if (bVal == null)
|
|
80
|
+
comparison = -1;
|
|
81
|
+
else if (typeof aVal === 'string' && typeof bVal === 'string') {
|
|
82
|
+
comparison = aVal.localeCompare(bVal);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
comparison = aVal < bVal ? -1 : 1;
|
|
86
|
+
}
|
|
87
|
+
if (comparison !== 0) {
|
|
88
|
+
return sort === 'asc' ? comparison : -comparison;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
92
|
+
});
|
|
93
|
+
}, [sortModel]);
|
|
94
|
+
return {
|
|
95
|
+
sortModel,
|
|
96
|
+
setSortModel,
|
|
97
|
+
handleSort,
|
|
98
|
+
getSortForColumn,
|
|
99
|
+
clearSort,
|
|
100
|
+
sortData,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function useFiltering(initialFilters = {}) {
|
|
104
|
+
const [filterModel, setFilterModel] = useState(initialFilters);
|
|
105
|
+
const [quickFilterText, setQuickFilterText] = useState('');
|
|
106
|
+
const setColumnFilter = useCallback((field, filter) => {
|
|
107
|
+
setFilterModel((prev) => {
|
|
108
|
+
if (filter === null) {
|
|
109
|
+
const { [field]: _, ...rest } = prev;
|
|
110
|
+
return rest;
|
|
111
|
+
}
|
|
112
|
+
return { ...prev, [field]: filter };
|
|
113
|
+
});
|
|
114
|
+
}, []);
|
|
115
|
+
const clearFilters = useCallback(() => {
|
|
116
|
+
setFilterModel({});
|
|
117
|
+
setQuickFilterText('');
|
|
118
|
+
}, []);
|
|
119
|
+
const isFilterActive = useCallback((field) => {
|
|
120
|
+
return field in filterModel;
|
|
121
|
+
}, [filterModel]);
|
|
122
|
+
const hasActiveFilters = useMemo(() => {
|
|
123
|
+
return Object.keys(filterModel).length > 0 || quickFilterText.length > 0;
|
|
124
|
+
}, [filterModel, quickFilterText]);
|
|
125
|
+
// Apply filtering to data
|
|
126
|
+
const filterData = useCallback((data, columns) => {
|
|
127
|
+
let filtered = data;
|
|
128
|
+
// Apply column filters
|
|
129
|
+
for (const [field, filter] of Object.entries(filterModel)) {
|
|
130
|
+
filtered = filtered.filter((row) => {
|
|
131
|
+
const value = getNestedValue(row, field);
|
|
132
|
+
return matchesFilter(value, filter);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// Apply quick filter
|
|
136
|
+
if (quickFilterText) {
|
|
137
|
+
const search = quickFilterText.toLowerCase();
|
|
138
|
+
filtered = filtered.filter((row) => {
|
|
139
|
+
return columns.some((col) => {
|
|
140
|
+
if (!col.field)
|
|
141
|
+
return false;
|
|
142
|
+
const value = getNestedValue(row, col.field);
|
|
143
|
+
if (value == null)
|
|
144
|
+
return false;
|
|
145
|
+
return String(value).toLowerCase().includes(search);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return filtered;
|
|
150
|
+
}, [filterModel, quickFilterText]);
|
|
151
|
+
return {
|
|
152
|
+
filterModel,
|
|
153
|
+
setFilterModel,
|
|
154
|
+
quickFilterText,
|
|
155
|
+
setQuickFilterText,
|
|
156
|
+
setColumnFilter,
|
|
157
|
+
clearFilters,
|
|
158
|
+
isFilterActive,
|
|
159
|
+
hasActiveFilters,
|
|
160
|
+
filterData,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function matchesFilter(value, filter) {
|
|
164
|
+
if (value == null)
|
|
165
|
+
return false;
|
|
166
|
+
const strValue = String(value).toLowerCase();
|
|
167
|
+
switch (filter.filterType) {
|
|
168
|
+
case 'text':
|
|
169
|
+
if (filter.filter == null)
|
|
170
|
+
return true;
|
|
171
|
+
const filterStr = String(filter.filter).toLowerCase();
|
|
172
|
+
switch (filter.type) {
|
|
173
|
+
case 'equals':
|
|
174
|
+
return strValue === filterStr;
|
|
175
|
+
case 'notEqual':
|
|
176
|
+
return strValue !== filterStr;
|
|
177
|
+
case 'startsWith':
|
|
178
|
+
return strValue.startsWith(filterStr);
|
|
179
|
+
case 'endsWith':
|
|
180
|
+
return strValue.endsWith(filterStr);
|
|
181
|
+
case 'contains':
|
|
182
|
+
default:
|
|
183
|
+
return strValue.includes(filterStr);
|
|
184
|
+
case 'notContains':
|
|
185
|
+
return !strValue.includes(filterStr);
|
|
186
|
+
}
|
|
187
|
+
case 'number':
|
|
188
|
+
const numValue = Number(value);
|
|
189
|
+
const filterNum = Number(filter.filter);
|
|
190
|
+
if (isNaN(numValue) || isNaN(filterNum))
|
|
191
|
+
return false;
|
|
192
|
+
switch (filter.type) {
|
|
193
|
+
case 'equals':
|
|
194
|
+
return numValue === filterNum;
|
|
195
|
+
case 'notEqual':
|
|
196
|
+
return numValue !== filterNum;
|
|
197
|
+
case 'greaterThan':
|
|
198
|
+
return numValue > filterNum;
|
|
199
|
+
case 'greaterThanOrEqual':
|
|
200
|
+
return numValue >= filterNum;
|
|
201
|
+
case 'lessThan':
|
|
202
|
+
return numValue < filterNum;
|
|
203
|
+
case 'lessThanOrEqual':
|
|
204
|
+
return numValue <= filterNum;
|
|
205
|
+
case 'inRange':
|
|
206
|
+
return numValue >= filterNum && numValue <= Number(filter.filterTo);
|
|
207
|
+
default:
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
case 'date':
|
|
211
|
+
const dateValue = new Date(value);
|
|
212
|
+
if (isNaN(dateValue.getTime()))
|
|
213
|
+
return false;
|
|
214
|
+
if (filter.dateFrom) {
|
|
215
|
+
const from = new Date(filter.dateFrom);
|
|
216
|
+
if (filter.dateTo) {
|
|
217
|
+
const to = new Date(filter.dateTo);
|
|
218
|
+
return dateValue >= from && dateValue <= to;
|
|
219
|
+
}
|
|
220
|
+
switch (filter.type) {
|
|
221
|
+
case 'equals':
|
|
222
|
+
return dateValue.toDateString() === from.toDateString();
|
|
223
|
+
case 'greaterThan':
|
|
224
|
+
return dateValue > from;
|
|
225
|
+
case 'lessThan':
|
|
226
|
+
return dateValue < from;
|
|
227
|
+
default:
|
|
228
|
+
return dateValue >= from;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
case 'set':
|
|
233
|
+
if (!filter.values || filter.values.length === 0)
|
|
234
|
+
return true;
|
|
235
|
+
return filter.values.includes(String(value));
|
|
236
|
+
default:
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// Pagination Hook
|
|
242
|
+
// ============================================================================
|
|
243
|
+
export function usePagination(totalRows, initialPageSize = 10, initialPage = 0) {
|
|
244
|
+
const [currentPage, setCurrentPage] = useState(initialPage);
|
|
245
|
+
const [pageSize, setPageSize] = useState(initialPageSize);
|
|
246
|
+
const totalPages = useMemo(() => Math.max(1, Math.ceil(totalRows / pageSize)), [totalRows, pageSize]);
|
|
247
|
+
// Clamp current page when total pages changes
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (currentPage >= totalPages) {
|
|
250
|
+
setCurrentPage(Math.max(0, totalPages - 1));
|
|
251
|
+
}
|
|
252
|
+
}, [totalPages, currentPage]);
|
|
253
|
+
const goToPage = useCallback((page) => {
|
|
254
|
+
const validPage = Math.max(0, Math.min(page, totalPages - 1));
|
|
255
|
+
setCurrentPage(validPage);
|
|
256
|
+
}, [totalPages]);
|
|
257
|
+
const goToNextPage = useCallback(() => goToPage(currentPage + 1), [currentPage, goToPage]);
|
|
258
|
+
const goToPreviousPage = useCallback(() => goToPage(currentPage - 1), [currentPage, goToPage]);
|
|
259
|
+
const goToFirstPage = useCallback(() => goToPage(0), [goToPage]);
|
|
260
|
+
const goToLastPage = useCallback(() => goToPage(totalPages - 1), [totalPages, goToPage]);
|
|
261
|
+
const changePageSize = useCallback((size) => {
|
|
262
|
+
setPageSize(size);
|
|
263
|
+
setCurrentPage(0);
|
|
264
|
+
}, []);
|
|
265
|
+
const startRow = currentPage * pageSize;
|
|
266
|
+
const endRow = Math.min(startRow + pageSize, totalRows);
|
|
267
|
+
const paginateData = useCallback((data) => {
|
|
268
|
+
return data.slice(startRow, startRow + pageSize);
|
|
269
|
+
}, [startRow, pageSize]);
|
|
270
|
+
return {
|
|
271
|
+
currentPage,
|
|
272
|
+
pageSize,
|
|
273
|
+
totalPages,
|
|
274
|
+
totalRows,
|
|
275
|
+
startRow,
|
|
276
|
+
endRow,
|
|
277
|
+
goToPage,
|
|
278
|
+
goToNextPage,
|
|
279
|
+
goToPreviousPage,
|
|
280
|
+
goToFirstPage,
|
|
281
|
+
goToLastPage,
|
|
282
|
+
changePageSize,
|
|
283
|
+
paginateData,
|
|
284
|
+
setCurrentPage,
|
|
285
|
+
setPageSize,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// ============================================================================
|
|
289
|
+
// Row Selection Hook
|
|
290
|
+
// ============================================================================
|
|
291
|
+
export function useRowSelection(data, getRowId, mode = 'multiple', isRowSelectable) {
|
|
292
|
+
const [selectedIds, setSelectedIds] = useState(new Set());
|
|
293
|
+
const isSelected = useCallback((id) => selectedIds.has(id), [selectedIds]);
|
|
294
|
+
const toggleRow = useCallback((row, index) => {
|
|
295
|
+
const id = getRowId(row);
|
|
296
|
+
const node = createRowNode(row, index);
|
|
297
|
+
if (isRowSelectable && !isRowSelectable(node))
|
|
298
|
+
return;
|
|
299
|
+
setSelectedIds((prev) => {
|
|
300
|
+
if (mode === 'single') {
|
|
301
|
+
return prev.has(id) ? new Set() : new Set([id]);
|
|
302
|
+
}
|
|
303
|
+
const next = new Set(prev);
|
|
304
|
+
if (next.has(id)) {
|
|
305
|
+
next.delete(id);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
next.add(id);
|
|
309
|
+
}
|
|
310
|
+
return next;
|
|
311
|
+
});
|
|
312
|
+
}, [getRowId, mode, isRowSelectable]);
|
|
313
|
+
const selectAll = useCallback(() => {
|
|
314
|
+
const ids = data
|
|
315
|
+
.filter((row, i) => {
|
|
316
|
+
if (!isRowSelectable)
|
|
317
|
+
return true;
|
|
318
|
+
return isRowSelectable(createRowNode(row, i));
|
|
319
|
+
})
|
|
320
|
+
.map(getRowId);
|
|
321
|
+
setSelectedIds(new Set(ids));
|
|
322
|
+
}, [data, getRowId, isRowSelectable]);
|
|
323
|
+
const deselectAll = useCallback(() => {
|
|
324
|
+
setSelectedIds(new Set());
|
|
325
|
+
}, []);
|
|
326
|
+
const selectRows = useCallback((rows) => {
|
|
327
|
+
const ids = rows.map(getRowId);
|
|
328
|
+
setSelectedIds(new Set(ids));
|
|
329
|
+
}, [getRowId]);
|
|
330
|
+
const isAllSelected = useMemo(() => {
|
|
331
|
+
if (data.length === 0)
|
|
332
|
+
return false;
|
|
333
|
+
const selectableRows = data.filter((row, i) => {
|
|
334
|
+
if (!isRowSelectable)
|
|
335
|
+
return true;
|
|
336
|
+
return isRowSelectable(createRowNode(row, i));
|
|
337
|
+
});
|
|
338
|
+
return selectableRows.every((row) => selectedIds.has(getRowId(row)));
|
|
339
|
+
}, [data, selectedIds, getRowId, isRowSelectable]);
|
|
340
|
+
const isIndeterminate = useMemo(() => {
|
|
341
|
+
if (data.length === 0)
|
|
342
|
+
return false;
|
|
343
|
+
const selectableRows = data.filter((row, i) => {
|
|
344
|
+
if (!isRowSelectable)
|
|
345
|
+
return true;
|
|
346
|
+
return isRowSelectable(createRowNode(row, i));
|
|
347
|
+
});
|
|
348
|
+
const selectedCount = selectableRows.filter((row) => selectedIds.has(getRowId(row))).length;
|
|
349
|
+
return selectedCount > 0 && selectedCount < selectableRows.length;
|
|
350
|
+
}, [data, selectedIds, getRowId, isRowSelectable]);
|
|
351
|
+
const selectedRows = useMemo(() => {
|
|
352
|
+
return data.filter((row) => selectedIds.has(getRowId(row)));
|
|
353
|
+
}, [data, selectedIds, getRowId]);
|
|
354
|
+
const selectedNodes = useMemo(() => {
|
|
355
|
+
return data
|
|
356
|
+
.map((row, i) => createRowNode(row, i))
|
|
357
|
+
.filter((node) => selectedIds.has(node.id));
|
|
358
|
+
}, [data, selectedIds]);
|
|
359
|
+
return {
|
|
360
|
+
selectedIds,
|
|
361
|
+
setSelectedIds,
|
|
362
|
+
isSelected,
|
|
363
|
+
toggleRow,
|
|
364
|
+
selectAll,
|
|
365
|
+
deselectAll,
|
|
366
|
+
selectRows,
|
|
367
|
+
isAllSelected,
|
|
368
|
+
isIndeterminate,
|
|
369
|
+
selectedRows,
|
|
370
|
+
selectedNodes,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
// ============================================================================
|
|
374
|
+
// Row Expansion Hook (for groups and master/detail)
|
|
375
|
+
// ============================================================================
|
|
376
|
+
export function useRowExpansion(getRowId, defaultExpanded = 0 // -1 for all expanded
|
|
377
|
+
) {
|
|
378
|
+
const [expandedIds, setExpandedIds] = useState(new Set());
|
|
379
|
+
const isExpanded = useCallback((id) => expandedIds.has(id), [expandedIds]);
|
|
380
|
+
const toggleExpand = useCallback((row) => {
|
|
381
|
+
const id = getRowId(row);
|
|
382
|
+
setExpandedIds((prev) => {
|
|
383
|
+
const next = new Set(prev);
|
|
384
|
+
if (next.has(id)) {
|
|
385
|
+
next.delete(id);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
next.add(id);
|
|
389
|
+
}
|
|
390
|
+
return next;
|
|
391
|
+
});
|
|
392
|
+
}, [getRowId]);
|
|
393
|
+
const expandAll = useCallback((rows) => {
|
|
394
|
+
const ids = rows.map(getRowId);
|
|
395
|
+
setExpandedIds(new Set(ids));
|
|
396
|
+
}, [getRowId]);
|
|
397
|
+
const collapseAll = useCallback(() => {
|
|
398
|
+
setExpandedIds(new Set());
|
|
399
|
+
}, []);
|
|
400
|
+
const setExpanded = useCallback((id, expanded) => {
|
|
401
|
+
setExpandedIds((prev) => {
|
|
402
|
+
const next = new Set(prev);
|
|
403
|
+
if (expanded) {
|
|
404
|
+
next.add(id);
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
next.delete(id);
|
|
408
|
+
}
|
|
409
|
+
return next;
|
|
410
|
+
});
|
|
411
|
+
}, []);
|
|
412
|
+
return {
|
|
413
|
+
expandedIds,
|
|
414
|
+
setExpandedIds,
|
|
415
|
+
isExpanded,
|
|
416
|
+
toggleExpand,
|
|
417
|
+
expandAll,
|
|
418
|
+
collapseAll,
|
|
419
|
+
setExpanded,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
// ============================================================================
|
|
423
|
+
// Column State Hook
|
|
424
|
+
// ============================================================================
|
|
425
|
+
export function useColumnState(columns, persistKey) {
|
|
426
|
+
const [columnState, setColumnState] = useState(() => {
|
|
427
|
+
// Load from localStorage if available
|
|
428
|
+
if (persistKey && typeof window !== 'undefined') {
|
|
429
|
+
const saved = localStorage.getItem(`ag-grid-columns-${persistKey}`);
|
|
430
|
+
if (saved) {
|
|
431
|
+
try {
|
|
432
|
+
return JSON.parse(saved);
|
|
433
|
+
}
|
|
434
|
+
catch (e) {
|
|
435
|
+
// Ignore
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Initialize from column definitions
|
|
440
|
+
return columns.map((col) => ({
|
|
441
|
+
colId: col.colId || col.field || '',
|
|
442
|
+
hide: col.hide ?? col.initialHide ?? false,
|
|
443
|
+
width: col.width ?? col.initialWidth,
|
|
444
|
+
pinned: col.pinned ?? col.initialPinned ?? null,
|
|
445
|
+
sort: col.sort ?? null,
|
|
446
|
+
}));
|
|
447
|
+
});
|
|
448
|
+
// Persist to localStorage
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
if (persistKey && typeof window !== 'undefined') {
|
|
451
|
+
localStorage.setItem(`ag-grid-columns-${persistKey}`, JSON.stringify(columnState));
|
|
452
|
+
}
|
|
453
|
+
}, [columnState, persistKey]);
|
|
454
|
+
const getColumnState = useCallback((colId) => {
|
|
455
|
+
return columnState.find((s) => s.colId === colId);
|
|
456
|
+
}, [columnState]);
|
|
457
|
+
// Helper to extract colId from string or Column object
|
|
458
|
+
const getColId = (colIdOrColumn) => {
|
|
459
|
+
if (typeof colIdOrColumn === 'string')
|
|
460
|
+
return colIdOrColumn;
|
|
461
|
+
return colIdOrColumn.colId;
|
|
462
|
+
};
|
|
463
|
+
const setColumnVisible = useCallback((colIdOrColumn, visible) => {
|
|
464
|
+
const colId = getColId(colIdOrColumn);
|
|
465
|
+
setColumnState((prev) => prev.map((s) => (s.colId === colId ? { ...s, hide: !visible } : s)));
|
|
466
|
+
}, []);
|
|
467
|
+
const setColumnWidth = useCallback((colIdOrColumn, width) => {
|
|
468
|
+
const colId = getColId(colIdOrColumn);
|
|
469
|
+
setColumnState((prev) => prev.map((s) => (s.colId === colId ? { ...s, width } : s)));
|
|
470
|
+
}, []);
|
|
471
|
+
const setColumnPinned = useCallback((colIdOrColumn, pinned) => {
|
|
472
|
+
const colId = getColId(colIdOrColumn);
|
|
473
|
+
setColumnState((prev) => prev.map((s) => (s.colId === colId ? { ...s, pinned } : s)));
|
|
474
|
+
}, []);
|
|
475
|
+
const moveColumn = useCallback((colIdOrColumn, toIndex) => {
|
|
476
|
+
const colId = getColId(colIdOrColumn);
|
|
477
|
+
setColumnState((prev) => {
|
|
478
|
+
const fromIndex = prev.findIndex((s) => s.colId === colId);
|
|
479
|
+
if (fromIndex === -1 || fromIndex === toIndex)
|
|
480
|
+
return prev;
|
|
481
|
+
const next = [...prev];
|
|
482
|
+
const [removed] = next.splice(fromIndex, 1);
|
|
483
|
+
next.splice(toIndex, 0, removed);
|
|
484
|
+
return next;
|
|
485
|
+
});
|
|
486
|
+
}, []);
|
|
487
|
+
const resetColumnState = useCallback(() => {
|
|
488
|
+
setColumnState(columns.map((col) => ({
|
|
489
|
+
colId: col.colId || col.field || '',
|
|
490
|
+
hide: col.hide ?? col.initialHide ?? false,
|
|
491
|
+
width: col.width ?? col.initialWidth,
|
|
492
|
+
pinned: col.pinned ?? col.initialPinned ?? null,
|
|
493
|
+
sort: col.sort ?? null,
|
|
494
|
+
})));
|
|
495
|
+
}, [columns]);
|
|
496
|
+
// Get processed columns with state applied
|
|
497
|
+
const processedColumns = useMemo(() => {
|
|
498
|
+
return columnState
|
|
499
|
+
.map((state) => {
|
|
500
|
+
const col = columns.find((c) => (c.colId || c.field) === state.colId);
|
|
501
|
+
if (!col)
|
|
502
|
+
return null;
|
|
503
|
+
return {
|
|
504
|
+
...col,
|
|
505
|
+
hide: state.hide,
|
|
506
|
+
width: state.width,
|
|
507
|
+
pinned: state.pinned,
|
|
508
|
+
sort: state.sort,
|
|
509
|
+
};
|
|
510
|
+
})
|
|
511
|
+
.filter(Boolean);
|
|
512
|
+
}, [columns, columnState]);
|
|
513
|
+
const visibleColumns = useMemo(() => {
|
|
514
|
+
return processedColumns.filter((col) => !col.hide);
|
|
515
|
+
}, [processedColumns]);
|
|
516
|
+
return {
|
|
517
|
+
columnState,
|
|
518
|
+
setColumnState,
|
|
519
|
+
getColumnState,
|
|
520
|
+
setColumnVisible,
|
|
521
|
+
setColumnWidth,
|
|
522
|
+
setColumnPinned,
|
|
523
|
+
moveColumn,
|
|
524
|
+
resetColumnState,
|
|
525
|
+
processedColumns,
|
|
526
|
+
visibleColumns,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
export function useCellEditing(onCellValueChanged) {
|
|
530
|
+
const [editingCell, setEditingCell] = useState(null);
|
|
531
|
+
const editHistory = useRef([]);
|
|
532
|
+
const historyIndex = useRef(-1);
|
|
533
|
+
const startEditing = useCallback((rowId, colId, rowIndex, value) => {
|
|
534
|
+
setEditingCell({ rowId, colId, rowIndex, value });
|
|
535
|
+
}, []);
|
|
536
|
+
const stopEditing = useCallback((cancel = false, newValue) => {
|
|
537
|
+
if (!editingCell)
|
|
538
|
+
return;
|
|
539
|
+
if (!cancel && newValue !== undefined && newValue !== editingCell.value) {
|
|
540
|
+
// Record in history for undo/redo
|
|
541
|
+
editHistory.current = editHistory.current.slice(0, historyIndex.current + 1);
|
|
542
|
+
editHistory.current.push({
|
|
543
|
+
data: {},
|
|
544
|
+
field: editingCell.colId,
|
|
545
|
+
oldValue: editingCell.value,
|
|
546
|
+
newValue,
|
|
547
|
+
});
|
|
548
|
+
historyIndex.current = editHistory.current.length - 1;
|
|
549
|
+
}
|
|
550
|
+
setEditingCell(null);
|
|
551
|
+
}, [editingCell]);
|
|
552
|
+
const isEditing = useCallback((rowId, colId) => {
|
|
553
|
+
return editingCell?.rowId === rowId && editingCell?.colId === colId;
|
|
554
|
+
}, [editingCell]);
|
|
555
|
+
const undo = useCallback(() => {
|
|
556
|
+
if (historyIndex.current < 0)
|
|
557
|
+
return null;
|
|
558
|
+
const entry = editHistory.current[historyIndex.current];
|
|
559
|
+
historyIndex.current--;
|
|
560
|
+
return entry;
|
|
561
|
+
}, []);
|
|
562
|
+
const redo = useCallback(() => {
|
|
563
|
+
if (historyIndex.current >= editHistory.current.length - 1)
|
|
564
|
+
return null;
|
|
565
|
+
historyIndex.current++;
|
|
566
|
+
return editHistory.current[historyIndex.current];
|
|
567
|
+
}, []);
|
|
568
|
+
const canUndo = historyIndex.current >= 0;
|
|
569
|
+
const canRedo = historyIndex.current < editHistory.current.length - 1;
|
|
570
|
+
return {
|
|
571
|
+
editingCell,
|
|
572
|
+
startEditing,
|
|
573
|
+
stopEditing,
|
|
574
|
+
isEditing,
|
|
575
|
+
undo,
|
|
576
|
+
redo,
|
|
577
|
+
canUndo,
|
|
578
|
+
canRedo,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
export function useRowGrouping(data, groupByFields, aggregations) {
|
|
582
|
+
const [expandedGroups, setExpandedGroups] = useState(new Set());
|
|
583
|
+
const groupData = useCallback((items, fields, level = 0) => {
|
|
584
|
+
if (fields.length === 0 || items.length === 0)
|
|
585
|
+
return items;
|
|
586
|
+
const [field, ...remainingFields] = fields;
|
|
587
|
+
const groups = new Map();
|
|
588
|
+
items.forEach((item) => {
|
|
589
|
+
const value = getNestedValue(item, field);
|
|
590
|
+
const existing = groups.get(value) || [];
|
|
591
|
+
existing.push(item);
|
|
592
|
+
groups.set(value, existing);
|
|
593
|
+
});
|
|
594
|
+
return Array.from(groups.entries()).map(([value, groupItems]) => {
|
|
595
|
+
const groupKey = `${field}:${value}:${level}`;
|
|
596
|
+
const childrenData = groupData(groupItems, remainingFields, level + 1);
|
|
597
|
+
// Calculate aggregations
|
|
598
|
+
const aggs = {};
|
|
599
|
+
if (aggregations) {
|
|
600
|
+
for (const [aggField, aggFn] of Object.entries(aggregations)) {
|
|
601
|
+
const values = groupItems.map((item) => getNestedValue(item, aggField));
|
|
602
|
+
aggs[aggField] = aggFn(values);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
isGroup: true,
|
|
607
|
+
groupValue: value,
|
|
608
|
+
groupField: field,
|
|
609
|
+
children: childrenData,
|
|
610
|
+
level,
|
|
611
|
+
expanded: expandedGroups.has(groupKey),
|
|
612
|
+
aggregations: aggs,
|
|
613
|
+
};
|
|
614
|
+
});
|
|
615
|
+
}, [expandedGroups, aggregations]);
|
|
616
|
+
const groupedData = useMemo(() => {
|
|
617
|
+
return groupData(data, groupByFields);
|
|
618
|
+
}, [data, groupByFields, groupData]);
|
|
619
|
+
const toggleGroup = useCallback((field, value, level) => {
|
|
620
|
+
const key = `${field}:${value}:${level}`;
|
|
621
|
+
setExpandedGroups((prev) => {
|
|
622
|
+
const next = new Set(prev);
|
|
623
|
+
if (next.has(key)) {
|
|
624
|
+
next.delete(key);
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
next.add(key);
|
|
628
|
+
}
|
|
629
|
+
return next;
|
|
630
|
+
});
|
|
631
|
+
}, []);
|
|
632
|
+
const expandAllGroups = useCallback(() => {
|
|
633
|
+
const collectKeys = (items) => {
|
|
634
|
+
const keys = [];
|
|
635
|
+
items.forEach((item) => {
|
|
636
|
+
if ('isGroup' in item && item.isGroup) {
|
|
637
|
+
keys.push(`${item.groupField}:${item.groupValue}:${item.level}`);
|
|
638
|
+
keys.push(...collectKeys(item.children));
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
return keys;
|
|
642
|
+
};
|
|
643
|
+
setExpandedGroups(new Set(collectKeys(groupedData)));
|
|
644
|
+
}, [groupedData]);
|
|
645
|
+
const collapseAllGroups = useCallback(() => {
|
|
646
|
+
setExpandedGroups(new Set());
|
|
647
|
+
}, []);
|
|
648
|
+
// Flatten grouped data for rendering
|
|
649
|
+
const flattenedData = useMemo(() => {
|
|
650
|
+
const flatten = (items) => {
|
|
651
|
+
const result = [];
|
|
652
|
+
items.forEach((item) => {
|
|
653
|
+
result.push(item);
|
|
654
|
+
if ('isGroup' in item && item.isGroup && item.expanded) {
|
|
655
|
+
result.push(...flatten(item.children));
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
return result;
|
|
659
|
+
};
|
|
660
|
+
return flatten(groupedData);
|
|
661
|
+
}, [groupedData]);
|
|
662
|
+
return {
|
|
663
|
+
groupedData,
|
|
664
|
+
flattenedData,
|
|
665
|
+
expandedGroups,
|
|
666
|
+
toggleGroup,
|
|
667
|
+
expandAllGroups,
|
|
668
|
+
collapseAllGroups,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
// ============================================================================
|
|
672
|
+
// Virtual Scrolling Hook
|
|
673
|
+
// ============================================================================
|
|
674
|
+
export function useVirtualScrolling(data, rowHeight, containerHeight, overscan = 5) {
|
|
675
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
676
|
+
const totalHeight = data.length * rowHeight;
|
|
677
|
+
const visibleRange = useMemo(() => {
|
|
678
|
+
const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
|
|
679
|
+
const endIndex = Math.min(data.length, Math.ceil((scrollTop + containerHeight) / rowHeight) + overscan);
|
|
680
|
+
return { startIndex, endIndex };
|
|
681
|
+
}, [scrollTop, rowHeight, containerHeight, data.length, overscan]);
|
|
682
|
+
const visibleData = useMemo(() => {
|
|
683
|
+
return data.slice(visibleRange.startIndex, visibleRange.endIndex);
|
|
684
|
+
}, [data, visibleRange]);
|
|
685
|
+
const offsetY = visibleRange.startIndex * rowHeight;
|
|
686
|
+
const handleScroll = useCallback((e) => {
|
|
687
|
+
setScrollTop(e.currentTarget.scrollTop);
|
|
688
|
+
}, []);
|
|
689
|
+
return {
|
|
690
|
+
totalHeight,
|
|
691
|
+
visibleData,
|
|
692
|
+
visibleRange,
|
|
693
|
+
offsetY,
|
|
694
|
+
handleScroll,
|
|
695
|
+
scrollTop,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
// ============================================================================
|
|
699
|
+
// Clipboard Hook
|
|
700
|
+
// ============================================================================
|
|
701
|
+
export function useClipboard() {
|
|
702
|
+
const copyToClipboard = useCallback(async (data, columns, includeHeaders = true) => {
|
|
703
|
+
const rows = [];
|
|
704
|
+
if (includeHeaders) {
|
|
705
|
+
const header = columns
|
|
706
|
+
.filter((col) => !col.hide)
|
|
707
|
+
.map((col) => col.headerName || col.field || '')
|
|
708
|
+
.join('\t');
|
|
709
|
+
rows.push(header);
|
|
710
|
+
}
|
|
711
|
+
data.forEach((row) => {
|
|
712
|
+
const values = columns
|
|
713
|
+
.filter((col) => !col.hide)
|
|
714
|
+
.map((col) => {
|
|
715
|
+
if (!col.field)
|
|
716
|
+
return '';
|
|
717
|
+
const value = getNestedValue(row, col.field);
|
|
718
|
+
return value == null ? '' : String(value);
|
|
719
|
+
})
|
|
720
|
+
.join('\t');
|
|
721
|
+
rows.push(values);
|
|
722
|
+
});
|
|
723
|
+
const text = rows.join('\n');
|
|
724
|
+
await navigator.clipboard.writeText(text);
|
|
725
|
+
}, []);
|
|
726
|
+
const pasteFromClipboard = useCallback(async () => {
|
|
727
|
+
const text = await navigator.clipboard.readText();
|
|
728
|
+
return text.split('\n').map((row) => row.split('\t'));
|
|
729
|
+
}, []);
|
|
730
|
+
return {
|
|
731
|
+
copyToClipboard,
|
|
732
|
+
pasteFromClipboard,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
// ============================================================================
|
|
736
|
+
// Responsive Hook
|
|
737
|
+
// ============================================================================
|
|
738
|
+
export function useResponsive(breakpoint = 768) {
|
|
739
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
740
|
+
useEffect(() => {
|
|
741
|
+
const checkMobile = () => {
|
|
742
|
+
setIsMobile(window.innerWidth < breakpoint);
|
|
743
|
+
};
|
|
744
|
+
checkMobile();
|
|
745
|
+
window.addEventListener('resize', checkMobile);
|
|
746
|
+
return () => window.removeEventListener('resize', checkMobile);
|
|
747
|
+
}, [breakpoint]);
|
|
748
|
+
return { isMobile };
|
|
749
|
+
}
|
|
750
|
+
export function useContextMenu() {
|
|
751
|
+
const [contextMenu, setContextMenu] = useState({
|
|
752
|
+
visible: false,
|
|
753
|
+
x: 0,
|
|
754
|
+
y: 0,
|
|
755
|
+
});
|
|
756
|
+
const showContextMenu = useCallback((x, y, data) => {
|
|
757
|
+
setContextMenu({ visible: true, x, y, data });
|
|
758
|
+
}, []);
|
|
759
|
+
const hideContextMenu = useCallback(() => {
|
|
760
|
+
setContextMenu((prev) => ({ ...prev, visible: false }));
|
|
761
|
+
}, []);
|
|
762
|
+
// Close on click outside
|
|
763
|
+
useEffect(() => {
|
|
764
|
+
if (!contextMenu.visible)
|
|
765
|
+
return;
|
|
766
|
+
const handleClick = () => hideContextMenu();
|
|
767
|
+
const handleEscape = (e) => {
|
|
768
|
+
if (e.key === 'Escape')
|
|
769
|
+
hideContextMenu();
|
|
770
|
+
};
|
|
771
|
+
document.addEventListener('click', handleClick);
|
|
772
|
+
document.addEventListener('keydown', handleEscape);
|
|
773
|
+
return () => {
|
|
774
|
+
document.removeEventListener('click', handleClick);
|
|
775
|
+
document.removeEventListener('keydown', handleEscape);
|
|
776
|
+
};
|
|
777
|
+
}, [contextMenu.visible, hideContextMenu]);
|
|
778
|
+
return {
|
|
779
|
+
contextMenu,
|
|
780
|
+
showContextMenu,
|
|
781
|
+
hideContextMenu,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
// ============================================================================
|
|
785
|
+
// Keyboard Navigation Hook
|
|
786
|
+
// ============================================================================
|
|
787
|
+
export function useKeyboardNavigation(data, columns, onCellSelect, onEnter) {
|
|
788
|
+
const [focusedCell, setFocusedCell] = useState(null);
|
|
789
|
+
const handleKeyDown = useCallback((e) => {
|
|
790
|
+
if (!focusedCell)
|
|
791
|
+
return;
|
|
792
|
+
const { rowIndex, colIndex } = focusedCell;
|
|
793
|
+
const visibleCols = columns.filter((col) => !col.hide);
|
|
794
|
+
switch (e.key) {
|
|
795
|
+
case 'ArrowUp':
|
|
796
|
+
e.preventDefault();
|
|
797
|
+
if (rowIndex > 0) {
|
|
798
|
+
setFocusedCell({ rowIndex: rowIndex - 1, colIndex });
|
|
799
|
+
}
|
|
800
|
+
break;
|
|
801
|
+
case 'ArrowDown':
|
|
802
|
+
e.preventDefault();
|
|
803
|
+
if (rowIndex < data.length - 1) {
|
|
804
|
+
setFocusedCell({ rowIndex: rowIndex + 1, colIndex });
|
|
805
|
+
}
|
|
806
|
+
break;
|
|
807
|
+
case 'ArrowLeft':
|
|
808
|
+
e.preventDefault();
|
|
809
|
+
if (colIndex > 0) {
|
|
810
|
+
setFocusedCell({ rowIndex, colIndex: colIndex - 1 });
|
|
811
|
+
}
|
|
812
|
+
break;
|
|
813
|
+
case 'ArrowRight':
|
|
814
|
+
e.preventDefault();
|
|
815
|
+
if (colIndex < visibleCols.length - 1) {
|
|
816
|
+
setFocusedCell({ rowIndex, colIndex: colIndex + 1 });
|
|
817
|
+
}
|
|
818
|
+
break;
|
|
819
|
+
case 'Enter':
|
|
820
|
+
e.preventDefault();
|
|
821
|
+
const col = visibleCols[colIndex];
|
|
822
|
+
if (col?.field) {
|
|
823
|
+
onEnter?.(rowIndex, col.field);
|
|
824
|
+
}
|
|
825
|
+
break;
|
|
826
|
+
case 'Tab':
|
|
827
|
+
if (e.shiftKey) {
|
|
828
|
+
if (colIndex > 0) {
|
|
829
|
+
e.preventDefault();
|
|
830
|
+
setFocusedCell({ rowIndex, colIndex: colIndex - 1 });
|
|
831
|
+
}
|
|
832
|
+
else if (rowIndex > 0) {
|
|
833
|
+
e.preventDefault();
|
|
834
|
+
setFocusedCell({
|
|
835
|
+
rowIndex: rowIndex - 1,
|
|
836
|
+
colIndex: visibleCols.length - 1,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
if (colIndex < visibleCols.length - 1) {
|
|
842
|
+
e.preventDefault();
|
|
843
|
+
setFocusedCell({ rowIndex, colIndex: colIndex + 1 });
|
|
844
|
+
}
|
|
845
|
+
else if (rowIndex < data.length - 1) {
|
|
846
|
+
e.preventDefault();
|
|
847
|
+
setFocusedCell({ rowIndex: rowIndex + 1, colIndex: 0 });
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
}, [focusedCell, data.length, columns, onEnter]);
|
|
853
|
+
useEffect(() => {
|
|
854
|
+
if (focusedCell) {
|
|
855
|
+
const visibleCols = columns.filter((col) => !col.hide);
|
|
856
|
+
const col = visibleCols[focusedCell.colIndex];
|
|
857
|
+
if (col?.field) {
|
|
858
|
+
onCellSelect?.(focusedCell.rowIndex, col.field);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}, [focusedCell, columns, onCellSelect]);
|
|
862
|
+
return {
|
|
863
|
+
focusedCell,
|
|
864
|
+
setFocusedCell,
|
|
865
|
+
handleKeyDown,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
// ============================================================================
|
|
869
|
+
// Utility Functions
|
|
870
|
+
// ============================================================================
|
|
871
|
+
export function getNestedValue(obj, path) {
|
|
872
|
+
return path.split('.').reduce((acc, part) => acc?.[part], obj);
|
|
873
|
+
}
|
|
874
|
+
export function setNestedValue(obj, path, value) {
|
|
875
|
+
const parts = path.split('.');
|
|
876
|
+
const result = { ...obj };
|
|
877
|
+
let current = result;
|
|
878
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
879
|
+
current[parts[i]] = { ...current[parts[i]] };
|
|
880
|
+
current = current[parts[i]];
|
|
881
|
+
}
|
|
882
|
+
current[parts[parts.length - 1]] = value;
|
|
883
|
+
return result;
|
|
884
|
+
}
|
|
885
|
+
export function createRowNode(data, index, id) {
|
|
886
|
+
const rowId = id || String(index);
|
|
887
|
+
return {
|
|
888
|
+
id: rowId,
|
|
889
|
+
data,
|
|
890
|
+
rowIndex: index,
|
|
891
|
+
isSelected: false,
|
|
892
|
+
expanded: false,
|
|
893
|
+
level: 0,
|
|
894
|
+
setData: () => { },
|
|
895
|
+
setSelected: () => { },
|
|
896
|
+
isSelectable: () => true,
|
|
897
|
+
setExpanded: () => { },
|
|
898
|
+
getRowId: () => rowId,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
// Aggregation helper functions
|
|
902
|
+
export const aggregations = {
|
|
903
|
+
sum: (values) => values.reduce((a, b) => (a || 0) + (b || 0), 0),
|
|
904
|
+
avg: (values) => {
|
|
905
|
+
const validValues = values.filter((v) => v != null && !isNaN(v));
|
|
906
|
+
return validValues.length
|
|
907
|
+
? validValues.reduce((a, b) => a + b, 0) / validValues.length
|
|
908
|
+
: 0;
|
|
909
|
+
},
|
|
910
|
+
min: (values) => Math.min(...values.filter((v) => v != null && !isNaN(v))),
|
|
911
|
+
max: (values) => Math.max(...values.filter((v) => v != null && !isNaN(v))),
|
|
912
|
+
count: (values) => values.length,
|
|
913
|
+
first: (values) => values[0],
|
|
914
|
+
last: (values) => values[values.length - 1],
|
|
915
|
+
};
|
|
916
|
+
// Export to CSV
|
|
917
|
+
export function exportToCsv(data, columns, filename = 'export.csv') {
|
|
918
|
+
const exportCols = columns.filter((col) => !col.suppressCsvExport && !col.hide);
|
|
919
|
+
const header = exportCols
|
|
920
|
+
.map((col) => `"${col.headerName || col.field || ''}"`)
|
|
921
|
+
.join(',');
|
|
922
|
+
const rows = data.map((row) => {
|
|
923
|
+
return exportCols
|
|
924
|
+
.map((col) => {
|
|
925
|
+
if (!col.field)
|
|
926
|
+
return '""';
|
|
927
|
+
const value = getNestedValue(row, col.field);
|
|
928
|
+
if (value == null)
|
|
929
|
+
return '""';
|
|
930
|
+
const str = String(value).replace(/"/g, '""');
|
|
931
|
+
return `"${str}"`;
|
|
932
|
+
})
|
|
933
|
+
.join(',');
|
|
934
|
+
});
|
|
935
|
+
const csv = [header, ...rows].join('\n');
|
|
936
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
937
|
+
const url = URL.createObjectURL(blob);
|
|
938
|
+
const link = document.createElement('a');
|
|
939
|
+
link.href = url;
|
|
940
|
+
link.download = filename;
|
|
941
|
+
link.click();
|
|
942
|
+
URL.revokeObjectURL(url);
|
|
943
|
+
}
|