@opencorestack/opengridx 1.2.3 → 1.3.0
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/CHANGELOG.md +16 -0
- package/dist/index.d.ts +41 -0
- package/dist/opengridx.es.js +3641 -3473
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +26 -29
- package/dist/opengridx.umd.js.map +1 -1
- package/lib/components/DataGrid/DataGrid.tsx +48 -2
- package/lib/hooks/core/useDataGrid.ts +1 -0
- package/lib/types/index.ts +34 -0
- package/lib/utils/export/exportToPdf.ts +67 -30
- package/lib/utils/export/groupedExport.test.ts +144 -0
- package/lib/utils/export/index.ts +400 -271
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import type { GridColDef, GridRowModel, GridAggregationModel } from '../../types';
|
|
2
|
+
import type { GridColDef, GridRowModel, GridAggregationModel, GridGroupedExportRow } from '../../types';
|
|
3
3
|
import { formatAggregationValue } from '../../hooks/features/useAggregation';
|
|
4
4
|
|
|
5
5
|
export interface CsvExportOptions {
|
|
@@ -9,6 +9,8 @@ export interface CsvExportOptions {
|
|
|
9
9
|
selectedRows?: (string | number)[];
|
|
10
10
|
aggregationResult?: Record<string, unknown> | null;
|
|
11
11
|
aggregationModel?: GridAggregationModel | null;
|
|
12
|
+
/** When provided, emits group headers, leaf rows, subtotals, and a grand total instead of a flat row list. */
|
|
13
|
+
groupedRows?: GridGroupedExportRow[];
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export function exportToCsv<R extends GridRowModel>(
|
|
@@ -20,18 +22,10 @@ export function exportToCsv<R extends GridRowModel>(
|
|
|
20
22
|
fileName = 'export.csv',
|
|
21
23
|
includeHeaders = true,
|
|
22
24
|
delimiter = ',',
|
|
23
|
-
selectedRows
|
|
25
|
+
selectedRows,
|
|
26
|
+
groupedRows,
|
|
24
27
|
} = options;
|
|
25
28
|
|
|
26
|
-
const rowsToExport = selectedRows && selectedRows.length > 0
|
|
27
|
-
? rows.filter(r => selectedRows.includes(r.id))
|
|
28
|
-
: rows;
|
|
29
|
-
|
|
30
|
-
if (rowsToExport.length === 0) {
|
|
31
|
-
console.warn('No rows to export');
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
29
|
const exportColumns = columns.filter(col => {
|
|
36
30
|
if (col.exportable === false) return false;
|
|
37
31
|
if (col.field === '__check__' || col.field === '__actions__') return false;
|
|
@@ -40,58 +34,110 @@ export function exportToCsv<R extends GridRowModel>(
|
|
|
40
34
|
|
|
41
35
|
let csvContent = '';
|
|
42
36
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
37
|
+
if (groupedRows && groupedRows.length > 0) {
|
|
38
|
+
// Grouped export path
|
|
39
|
+
if (includeHeaders) {
|
|
40
|
+
csvContent += exportColumns.map(col => escapeCSV(col.headerName || col.field)).join(delimiter) + '\n';
|
|
41
|
+
}
|
|
42
|
+
groupedRows.forEach(entry => {
|
|
43
|
+
if (entry.type === 'group-header') {
|
|
44
|
+
const indent = ' '.repeat(entry.depth);
|
|
45
|
+
const label = `${indent}${entry.groupField ?? ''}: ${String(entry.groupValue ?? '')}`;
|
|
46
|
+
const cells = exportColumns.map((_, i) => i === 0 ? escapeCSV(label) : '');
|
|
47
|
+
csvContent += cells.join(delimiter) + '\n';
|
|
48
|
+
} else if (entry.type === 'leaf' && entry.row) {
|
|
49
|
+
const row = entry.row as R;
|
|
50
|
+
const indent = ' '.repeat(entry.depth);
|
|
51
|
+
const cells = exportColumns.map((col, i) => {
|
|
52
|
+
let value: unknown = (row as GridRowModel)[col.field];
|
|
53
|
+
if (col.valueGetter) value = col.valueGetter({ row, field: col.field, value });
|
|
54
|
+
if (col.valueFormatter && value !== undefined && value !== null) {
|
|
55
|
+
value = col.valueFormatter({ value, row, field: col.field });
|
|
56
|
+
}
|
|
57
|
+
return escapeCSV((i === 0 ? indent : '') + String(value ?? ''));
|
|
58
|
+
});
|
|
59
|
+
csvContent += cells.join(delimiter) + '\n';
|
|
60
|
+
} else if (entry.type === 'group-subtotal' && entry.aggregatedValues) {
|
|
61
|
+
const indent = ' '.repeat(entry.depth);
|
|
62
|
+
const aggModel = options.aggregationModel || {};
|
|
63
|
+
const cells = exportColumns.map((col, i) => {
|
|
64
|
+
if (i === 0) return escapeCSV(`${indent}Subtotal`);
|
|
65
|
+
const aggVal = entry.aggregatedValues![col.field];
|
|
66
|
+
if (aggVal !== undefined && aggVal !== null) {
|
|
67
|
+
let formatted: unknown = aggVal;
|
|
68
|
+
if (col.valueFormatter) {
|
|
69
|
+
formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
70
|
+
} else if (aggModel[col.field]) {
|
|
71
|
+
formatted = formatAggregationValue(aggVal, aggModel[col.field]);
|
|
72
|
+
}
|
|
73
|
+
return escapeCSV(String(formatted));
|
|
74
|
+
}
|
|
75
|
+
return '';
|
|
76
|
+
});
|
|
77
|
+
csvContent += cells.join(delimiter) + '\n';
|
|
78
|
+
} else if (entry.type === 'grand-total' && entry.aggregatedValues) {
|
|
79
|
+
const aggModel = options.aggregationModel || {};
|
|
80
|
+
const cells = exportColumns.map((col, i) => {
|
|
81
|
+
if (i === 0) return escapeCSV('Grand Total');
|
|
82
|
+
const aggVal = entry.aggregatedValues![col.field];
|
|
83
|
+
if (aggVal !== undefined && aggVal !== null) {
|
|
84
|
+
let formatted: unknown = aggVal;
|
|
85
|
+
if (col.valueFormatter) {
|
|
86
|
+
formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
87
|
+
} else if (aggModel[col.field]) {
|
|
88
|
+
formatted = formatAggregationValue(aggVal, aggModel[col.field]);
|
|
89
|
+
}
|
|
90
|
+
return escapeCSV(String(formatted));
|
|
91
|
+
}
|
|
92
|
+
return '';
|
|
93
|
+
});
|
|
94
|
+
csvContent += cells.join(delimiter) + '\n';
|
|
55
95
|
}
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
// Flat export path (existing behavior)
|
|
99
|
+
const rowsToExport = selectedRows && selectedRows.length > 0
|
|
100
|
+
? rows.filter(r => selectedRows.includes(r.id))
|
|
101
|
+
: rows;
|
|
56
102
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
103
|
+
if (rowsToExport.length === 0) {
|
|
104
|
+
console.warn('No rows to export');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
60
107
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
108
|
+
if (includeHeaders) {
|
|
109
|
+
csvContent += exportColumns.map(col => escapeCSV(col.headerName || col.field)).join(delimiter) + '\n';
|
|
110
|
+
}
|
|
65
111
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const aggLabels = exportColumns.map((col) => {
|
|
73
|
-
if (aggModel[col.field]) {
|
|
74
|
-
return escapeCSV(aggModel[col.field].toUpperCase());
|
|
75
|
-
}
|
|
76
|
-
return escapeCSV('');
|
|
77
|
-
});
|
|
78
|
-
csvContent += aggLabels.join(delimiter) + '\n';
|
|
79
|
-
|
|
80
|
-
// Print Aggregation Values
|
|
81
|
-
const aggValues = exportColumns.map((col) => {
|
|
82
|
-
const aggVal = aggResult[col.field];
|
|
83
|
-
if (aggVal !== undefined && aggVal !== null) {
|
|
84
|
-
let formatted = aggVal;
|
|
85
|
-
if (col.valueFormatter) {
|
|
86
|
-
formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
87
|
-
} else if (aggModel[col.field]) {
|
|
88
|
-
formatted = formatAggregationValue(aggVal, aggModel[col.field]);
|
|
112
|
+
rowsToExport.forEach(row => {
|
|
113
|
+
const values = exportColumns.map(col => {
|
|
114
|
+
let value: unknown = (row as GridRowModel)[col.field];
|
|
115
|
+
if (col.valueGetter) value = col.valueGetter({ row, field: col.field, value });
|
|
116
|
+
if (col.valueFormatter && value !== undefined && value !== null) {
|
|
117
|
+
value = col.valueFormatter({ value, row, field: col.field });
|
|
89
118
|
}
|
|
90
|
-
return escapeCSV(String(
|
|
91
|
-
}
|
|
92
|
-
|
|
119
|
+
return escapeCSV(String(value ?? ''));
|
|
120
|
+
});
|
|
121
|
+
csvContent += values.join(delimiter) + '\n';
|
|
93
122
|
});
|
|
94
|
-
|
|
123
|
+
|
|
124
|
+
if (options.aggregationResult) {
|
|
125
|
+
const aggResult = options.aggregationResult;
|
|
126
|
+
const aggModel = options.aggregationModel || {};
|
|
127
|
+
const aggLabels = exportColumns.map(col => escapeCSV(aggModel[col.field] ? aggModel[col.field].toUpperCase() : ''));
|
|
128
|
+
csvContent += aggLabels.join(delimiter) + '\n';
|
|
129
|
+
const aggValues = exportColumns.map(col => {
|
|
130
|
+
const aggVal = aggResult[col.field];
|
|
131
|
+
if (aggVal !== undefined && aggVal !== null) {
|
|
132
|
+
let formatted: unknown = aggVal;
|
|
133
|
+
if (col.valueFormatter) formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
134
|
+
else if (aggModel[col.field]) formatted = formatAggregationValue(aggVal, aggModel[col.field]);
|
|
135
|
+
return escapeCSV(String(formatted));
|
|
136
|
+
}
|
|
137
|
+
return '';
|
|
138
|
+
});
|
|
139
|
+
csvContent += aggValues.join(delimiter) + '\n';
|
|
140
|
+
}
|
|
95
141
|
}
|
|
96
142
|
|
|
97
143
|
downloadFile(csvContent, fileName, 'text/csv;charset=utf-8;');
|
|
@@ -111,6 +157,8 @@ export interface ExcelExportOptions {
|
|
|
111
157
|
selectedRows?: (string | number)[];
|
|
112
158
|
aggregationResult?: Record<string, unknown> | null;
|
|
113
159
|
aggregationModel?: GridAggregationModel | null;
|
|
160
|
+
/** When provided, emits group headers, leaf rows, subtotals, and a grand total instead of a flat row list. */
|
|
161
|
+
groupedRows?: GridGroupedExportRow[];
|
|
114
162
|
}
|
|
115
163
|
|
|
116
164
|
export function exportToExcel<R extends GridRowModel>(
|
|
@@ -122,91 +170,123 @@ export function exportToExcel<R extends GridRowModel>(
|
|
|
122
170
|
fileName = 'export.xls',
|
|
123
171
|
sheetName = 'Sheet1',
|
|
124
172
|
includeHeaders = true,
|
|
125
|
-
selectedRows
|
|
173
|
+
selectedRows,
|
|
174
|
+
groupedRows,
|
|
126
175
|
} = options;
|
|
127
176
|
|
|
128
|
-
const rowsToExport = selectedRows && selectedRows.length > 0
|
|
129
|
-
? rows.filter(r => selectedRows.includes(r.id))
|
|
130
|
-
: rows;
|
|
131
|
-
|
|
132
|
-
if (rowsToExport.length === 0) {
|
|
133
|
-
console.warn('No rows to export');
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
177
|
const exportColumns = columns.filter(col => {
|
|
138
178
|
if (col.exportable === false) return false;
|
|
139
179
|
if (col.field === '__check__' || col.field === '__actions__') return false;
|
|
140
180
|
return true;
|
|
141
181
|
});
|
|
142
182
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
183
|
+
const header = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">'
|
|
184
|
+
+ '<head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet>'
|
|
185
|
+
+ `<x:Name>${sheetName}</x:Name>`
|
|
186
|
+
+ '<x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]-->'
|
|
187
|
+
+ '<meta charset="UTF-8"></head><body><table border="1">';
|
|
188
|
+
|
|
189
|
+
let html = header;
|
|
149
190
|
|
|
150
191
|
if (includeHeaders) {
|
|
151
192
|
html += '<thead><tr>';
|
|
152
|
-
exportColumns.forEach(col => {
|
|
153
|
-
html += `<th>${escapeHTML(col.headerName || col.field)}</th>`;
|
|
154
|
-
});
|
|
193
|
+
exportColumns.forEach(col => { html += `<th>${escapeHTML(col.headerName || col.field)}</th>`; });
|
|
155
194
|
html += '</tr></thead>';
|
|
156
195
|
}
|
|
157
196
|
|
|
158
197
|
html += '<tbody>';
|
|
159
|
-
rowsToExport.forEach(row => {
|
|
160
|
-
html += '<tr>';
|
|
161
|
-
exportColumns.forEach(col => {
|
|
162
|
-
let value: unknown = (row as GridRowModel)[col.field];
|
|
163
|
-
|
|
164
|
-
if (col.valueGetter) {
|
|
165
|
-
value = col.valueGetter({ row, field: col.field, value });
|
|
166
|
-
}
|
|
167
198
|
|
|
168
|
-
|
|
169
|
-
|
|
199
|
+
if (groupedRows && groupedRows.length > 0) {
|
|
200
|
+
const aggModel = options.aggregationModel || {};
|
|
201
|
+
groupedRows.forEach(entry => {
|
|
202
|
+
if (entry.type === 'group-header') {
|
|
203
|
+
const indent = ' '.repeat(entry.depth * 2);
|
|
204
|
+
const label = `${indent}${entry.groupField ?? ''}: ${String(entry.groupValue ?? '')}`;
|
|
205
|
+
html += `<tr style="font-weight:bold;background:#e8eaf6;">`;
|
|
206
|
+
html += `<td colspan="${exportColumns.length}">${escapeHTML(label)}</td>`;
|
|
207
|
+
html += '</tr>';
|
|
208
|
+
} else if (entry.type === 'leaf' && entry.row) {
|
|
209
|
+
const row = entry.row as R;
|
|
210
|
+
const indent = ' '.repeat(entry.depth * 2);
|
|
211
|
+
html += '<tr>';
|
|
212
|
+
exportColumns.forEach((col, i) => {
|
|
213
|
+
let value: unknown = (row as GridRowModel)[col.field];
|
|
214
|
+
if (col.valueGetter) value = col.valueGetter({ row, field: col.field, value });
|
|
215
|
+
if (col.valueFormatter && value !== undefined && value !== null) {
|
|
216
|
+
value = col.valueFormatter({ value, row, field: col.field });
|
|
217
|
+
}
|
|
218
|
+
html += `<td>${escapeHTML((i === 0 ? indent : '') + String(value ?? ''))}</td>`;
|
|
219
|
+
});
|
|
220
|
+
html += '</tr>';
|
|
221
|
+
} else if ((entry.type === 'group-subtotal' || entry.type === 'grand-total') && entry.aggregatedValues) {
|
|
222
|
+
const isGrand = entry.type === 'grand-total';
|
|
223
|
+
const indent = isGrand ? '' : ' '.repeat(entry.depth * 2);
|
|
224
|
+
const label = isGrand ? 'Grand Total' : `${indent}Subtotal`;
|
|
225
|
+
html += `<tr style="font-weight:bold;background:#f5f5f5;">`;
|
|
226
|
+
exportColumns.forEach((col, i) => {
|
|
227
|
+
if (i === 0) { html += `<td>${escapeHTML(label)}</td>`; return; }
|
|
228
|
+
const aggVal = entry.aggregatedValues![col.field];
|
|
229
|
+
if (aggVal !== undefined && aggVal !== null) {
|
|
230
|
+
let formatted: unknown = aggVal;
|
|
231
|
+
if (col.valueFormatter) formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
232
|
+
else if (aggModel[col.field]) formatted = formatAggregationValue(aggVal, aggModel[col.field]);
|
|
233
|
+
html += `<td style="text-align:right;">${escapeHTML(String(formatted))}</td>`;
|
|
234
|
+
} else {
|
|
235
|
+
html += '<td></td>';
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
html += '</tr>';
|
|
170
239
|
}
|
|
171
|
-
|
|
172
|
-
html += `<td>${escapeHTML(String(value ?? ''))}</td>`;
|
|
173
240
|
});
|
|
174
|
-
}
|
|
241
|
+
} else {
|
|
242
|
+
const rowsToExport = selectedRows && selectedRows.length > 0
|
|
243
|
+
? rows.filter(r => selectedRows.includes(r.id))
|
|
244
|
+
: rows;
|
|
175
245
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
246
|
+
if (rowsToExport.length === 0) {
|
|
247
|
+
console.warn('No rows to export');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
179
250
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
});
|
|
188
|
-
html += '</tr>';
|
|
189
|
-
|
|
190
|
-
html += '<tr style="font-weight: bold; background-color: #f5f5f5;">';
|
|
191
|
-
exportColumns.forEach((col) => {
|
|
192
|
-
const aggVal = aggResult[col.field];
|
|
193
|
-
if (aggVal !== undefined && aggVal !== null) {
|
|
194
|
-
let formatted = aggVal;
|
|
195
|
-
if (col.valueFormatter) {
|
|
196
|
-
formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
197
|
-
} else if (aggModel[col.field]) {
|
|
198
|
-
formatted = formatAggregationValue(aggVal, aggModel[col.field]);
|
|
251
|
+
rowsToExport.forEach(row => {
|
|
252
|
+
html += '<tr>';
|
|
253
|
+
exportColumns.forEach(col => {
|
|
254
|
+
let value: unknown = (row as GridRowModel)[col.field];
|
|
255
|
+
if (col.valueGetter) value = col.valueGetter({ row, field: col.field, value });
|
|
256
|
+
if (col.valueFormatter && value !== undefined && value !== null) {
|
|
257
|
+
value = col.valueFormatter({ value, row, field: col.field });
|
|
199
258
|
}
|
|
200
|
-
html += `<td
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
}
|
|
259
|
+
html += `<td>${escapeHTML(String(value ?? ''))}</td>`;
|
|
260
|
+
});
|
|
261
|
+
html += '</tr>';
|
|
204
262
|
});
|
|
205
|
-
|
|
263
|
+
|
|
264
|
+
if (options.aggregationResult) {
|
|
265
|
+
const aggResult = options.aggregationResult;
|
|
266
|
+
const aggModel = options.aggregationModel || {};
|
|
267
|
+
html += '<tr style="font-size: 11px; color: #666; background-color: #f5f5f5;">';
|
|
268
|
+
exportColumns.forEach(col => {
|
|
269
|
+
html += aggModel[col.field]
|
|
270
|
+
? `<td style="text-align:right;font-weight:bold;">${escapeHTML(aggModel[col.field].toUpperCase())}</td>`
|
|
271
|
+
: '<td></td>';
|
|
272
|
+
});
|
|
273
|
+
html += '</tr><tr style="font-weight:bold;background-color:#f5f5f5;">';
|
|
274
|
+
exportColumns.forEach(col => {
|
|
275
|
+
const aggVal = aggResult[col.field];
|
|
276
|
+
if (aggVal !== undefined && aggVal !== null) {
|
|
277
|
+
let formatted: unknown = aggVal;
|
|
278
|
+
if (col.valueFormatter) formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
279
|
+
else if (aggModel[col.field]) formatted = formatAggregationValue(aggVal, aggModel[col.field]);
|
|
280
|
+
html += `<td style="text-align:right;">${escapeHTML(String(formatted))}</td>`;
|
|
281
|
+
} else {
|
|
282
|
+
html += '<td></td>';
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
html += '</tr>';
|
|
286
|
+
}
|
|
206
287
|
}
|
|
207
288
|
|
|
208
289
|
html += '</tbody></table></body></html>';
|
|
209
|
-
|
|
210
290
|
downloadFile(html, fileName, 'application/vnd.ms-excel');
|
|
211
291
|
}
|
|
212
292
|
|
|
@@ -229,6 +309,8 @@ export interface JsonExportOptions {
|
|
|
229
309
|
selectedRows?: (string | number)[];
|
|
230
310
|
aggregationResult?: Record<string, unknown> | null;
|
|
231
311
|
aggregationModel?: GridAggregationModel | null;
|
|
312
|
+
/** When provided, emits a nested grouping tree instead of a flat array. */
|
|
313
|
+
groupedRows?: GridGroupedExportRow[];
|
|
232
314
|
}
|
|
233
315
|
|
|
234
316
|
/**
|
|
@@ -242,85 +324,113 @@ export function exportToJson<R extends GridRowModel>(
|
|
|
242
324
|
const {
|
|
243
325
|
fileName = 'export.json',
|
|
244
326
|
pretty = true,
|
|
245
|
-
selectedRows
|
|
327
|
+
selectedRows,
|
|
328
|
+
groupedRows,
|
|
246
329
|
} = options;
|
|
247
330
|
|
|
248
|
-
// Filter rows if selection is active
|
|
249
|
-
const rowsToExport = selectedRows && selectedRows.length > 0
|
|
250
|
-
? rows.filter(r => selectedRows.includes(r.id))
|
|
251
|
-
: rows;
|
|
252
|
-
|
|
253
|
-
if (rowsToExport.length === 0) {
|
|
254
|
-
console.warn('No rows to export');
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Filter columns
|
|
259
331
|
const exportColumns = columns.filter(col => {
|
|
260
332
|
if (col.exportable === false) return false;
|
|
261
333
|
if (col.field === '__check__' || col.field === '__actions__') return false;
|
|
262
334
|
return true;
|
|
263
335
|
});
|
|
264
336
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
337
|
+
type GroupNode = {
|
|
338
|
+
group: string;
|
|
339
|
+
field: string;
|
|
340
|
+
value: unknown;
|
|
341
|
+
rows: Record<string, unknown>[];
|
|
342
|
+
subtotals?: Record<string, unknown>;
|
|
343
|
+
children?: GroupNode[];
|
|
344
|
+
};
|
|
270
345
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
346
|
+
type JsonPayload =
|
|
347
|
+
| Record<string, unknown>[]
|
|
348
|
+
| { data: Record<string, unknown>[]; aggregation: { labels: Record<string, unknown>; values: Record<string, unknown> } }
|
|
349
|
+
| { groups: GroupNode[]; grandTotal?: Record<string, unknown> };
|
|
350
|
+
|
|
351
|
+
let outData: JsonPayload;
|
|
352
|
+
|
|
353
|
+
if (groupedRows && groupedRows.length > 0) {
|
|
354
|
+
// Build nested tree from the flat ordered list
|
|
355
|
+
const stack: GroupNode[] = [];
|
|
356
|
+
const roots: GroupNode[] = [];
|
|
357
|
+
let grandTotal: Record<string, unknown> | undefined;
|
|
358
|
+
|
|
359
|
+
groupedRows.forEach(entry => {
|
|
360
|
+
if (entry.type === 'group-header') {
|
|
361
|
+
const node: GroupNode = {
|
|
362
|
+
group: `${entry.groupField ?? ''}: ${String(entry.groupValue ?? '')}`,
|
|
363
|
+
field: entry.groupField ?? '',
|
|
364
|
+
value: entry.groupValue,
|
|
365
|
+
rows: [],
|
|
366
|
+
children: [],
|
|
367
|
+
};
|
|
368
|
+
while (stack.length > entry.depth) stack.pop();
|
|
369
|
+
if (stack.length === 0) {
|
|
370
|
+
roots.push(node);
|
|
371
|
+
} else {
|
|
372
|
+
(stack[stack.length - 1].children ??= []).push(node);
|
|
373
|
+
}
|
|
374
|
+
stack.push(node);
|
|
375
|
+
} else if (entry.type === 'leaf' && entry.row) {
|
|
376
|
+
const row = entry.row as R;
|
|
377
|
+
const obj: Record<string, unknown> = {};
|
|
378
|
+
exportColumns.forEach(col => {
|
|
379
|
+
let value: unknown = (row as GridRowModel)[col.field];
|
|
380
|
+
if (col.valueGetter) value = col.valueGetter({ row, field: col.field, value });
|
|
381
|
+
obj[col.field] = value;
|
|
382
|
+
});
|
|
383
|
+
if (stack.length > 0) stack[stack.length - 1].rows.push(obj);
|
|
384
|
+
} else if (entry.type === 'group-subtotal' && entry.aggregatedValues && stack.length > 0) {
|
|
385
|
+
stack[stack.length - 1].subtotals = { ...entry.aggregatedValues };
|
|
386
|
+
stack.pop();
|
|
387
|
+
} else if (entry.type === 'grand-total' && entry.aggregatedValues) {
|
|
388
|
+
grandTotal = { ...entry.aggregatedValues };
|
|
274
389
|
}
|
|
275
|
-
|
|
276
|
-
obj[col.field] = value;
|
|
277
390
|
});
|
|
278
|
-
return obj;
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
type ExportJsonPayload =
|
|
282
|
-
| Record<string, unknown>[]
|
|
283
|
-
| { data: Record<string, unknown>[]; aggregation: { labels: Record<string, unknown>; values: Record<string, unknown> } };
|
|
284
|
-
|
|
285
|
-
let outData: ExportJsonPayload = data;
|
|
286
391
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
392
|
+
outData = grandTotal ? { groups: roots, grandTotal } : { groups: roots };
|
|
393
|
+
} else {
|
|
394
|
+
const rowsToExport = selectedRows && selectedRows.length > 0
|
|
395
|
+
? rows.filter(r => selectedRows.includes(r.id))
|
|
396
|
+
: rows;
|
|
290
397
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
});
|
|
398
|
+
if (rowsToExport.length === 0) {
|
|
399
|
+
console.warn('No rows to export');
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
297
402
|
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
summaryDataObj[col.field] = aggVal;
|
|
308
|
-
}
|
|
403
|
+
const data = rowsToExport.map(row => {
|
|
404
|
+
const obj: Record<string, unknown> = {};
|
|
405
|
+
exportColumns.forEach(col => {
|
|
406
|
+
let value: unknown = (row as GridRowModel)[col.field];
|
|
407
|
+
if (col.valueGetter) value = col.valueGetter({ row, field: col.field, value });
|
|
408
|
+
obj[col.field] = value;
|
|
409
|
+
});
|
|
410
|
+
return obj;
|
|
309
411
|
});
|
|
310
412
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
413
|
+
if (options.aggregationResult) {
|
|
414
|
+
const aggResult = options.aggregationResult;
|
|
415
|
+
const aggModel = options.aggregationModel || {};
|
|
416
|
+
const summaryLabelObj: Record<string, unknown> = {};
|
|
417
|
+
exportColumns.forEach(col => { if (aggModel[col.field]) summaryLabelObj[col.field] = aggModel[col.field].toUpperCase(); });
|
|
418
|
+
const summaryDataObj: Record<string, unknown> = {};
|
|
419
|
+
exportColumns.forEach(col => {
|
|
420
|
+
if (aggResult[col.field] !== undefined && aggResult[col.field] !== null) {
|
|
421
|
+
let aggVal: unknown = aggResult[col.field];
|
|
422
|
+
if (col.valueFormatter) aggVal = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
423
|
+
else if (aggModel[col.field]) aggVal = formatAggregationValue(aggVal, aggModel[col.field]);
|
|
424
|
+
summaryDataObj[col.field] = aggVal;
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
outData = { data, aggregation: { labels: summaryLabelObj, values: summaryDataObj } };
|
|
428
|
+
} else {
|
|
429
|
+
outData = data;
|
|
430
|
+
}
|
|
318
431
|
}
|
|
319
432
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
// Download file
|
|
323
|
-
downloadFile(jsonContent, fileName, 'application/json;charset=utf-8;');
|
|
433
|
+
downloadFile(pretty ? JSON.stringify(outData, null, 2) : JSON.stringify(outData), fileName, 'application/json;charset=utf-8;');
|
|
324
434
|
}
|
|
325
435
|
|
|
326
436
|
// ============================================================================
|
|
@@ -332,6 +442,8 @@ export interface PrintOptions {
|
|
|
332
442
|
selectedRows?: (string | number)[];
|
|
333
443
|
aggregationResult?: Record<string, unknown> | null;
|
|
334
444
|
aggregationModel?: GridAggregationModel | null;
|
|
445
|
+
/** When provided, emits group headers, leaf rows, subtotals, and a grand total instead of a flat row list. */
|
|
446
|
+
groupedRows?: GridGroupedExportRow[];
|
|
335
447
|
}
|
|
336
448
|
|
|
337
449
|
/**
|
|
@@ -383,6 +495,8 @@ export async function printGrid<R extends GridRowModel>(
|
|
|
383
495
|
let aggregationResult: Record<string, unknown> | null = null;
|
|
384
496
|
let aggregationModel: GridAggregationModel | null = null;
|
|
385
497
|
|
|
498
|
+
let groupedRows: GridGroupedExportRow[] | undefined;
|
|
499
|
+
|
|
386
500
|
if (typeof titleOrOptions === 'string') {
|
|
387
501
|
title = titleOrOptions;
|
|
388
502
|
} else if (typeof titleOrOptions === 'object') {
|
|
@@ -390,112 +504,127 @@ export async function printGrid<R extends GridRowModel>(
|
|
|
390
504
|
selectedRows = titleOrOptions.selectedRows;
|
|
391
505
|
aggregationResult = titleOrOptions.aggregationResult || null;
|
|
392
506
|
aggregationModel = titleOrOptions.aggregationModel || null;
|
|
507
|
+
groupedRows = titleOrOptions.groupedRows;
|
|
393
508
|
}
|
|
394
509
|
|
|
395
|
-
// Filter rows if selection is active
|
|
396
|
-
const rowsToExport = selectedRows && selectedRows.length > 0
|
|
397
|
-
? rows.filter(r => selectedRows.includes(r.id))
|
|
398
|
-
: rows;
|
|
399
|
-
|
|
400
|
-
if (rowsToExport.length === 0) {
|
|
401
|
-
printWindow.document.body.innerHTML = '<h3>No rows to export</h3>';
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Filter columns
|
|
406
510
|
const exportColumns = columns.filter(col => {
|
|
407
511
|
if (col.exportable === false) return false;
|
|
408
512
|
if (col.field === '__check__' || col.field === '__actions__') return false;
|
|
409
513
|
return true;
|
|
410
514
|
});
|
|
411
515
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
<body>
|
|
432
|
-
`;
|
|
433
|
-
|
|
434
|
-
if (title) {
|
|
435
|
-
html += `<h1>${escapeHTML(title)}</h1>`;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
html += '<table>';
|
|
439
|
-
|
|
440
|
-
// Headers
|
|
441
|
-
html += '<thead><tr>';
|
|
442
|
-
exportColumns.forEach(col => {
|
|
443
|
-
html += `<th>${escapeHTML(col.headerName || col.field)}</th>`;
|
|
444
|
-
});
|
|
445
|
-
html += '</tr></thead>';
|
|
446
|
-
|
|
447
|
-
// Rows
|
|
448
|
-
html += '<tbody>';
|
|
449
|
-
|
|
450
|
-
// Process rows (potential bottleneck logic here)
|
|
451
|
-
rowsToExport.forEach(row => {
|
|
452
|
-
html += '<tr>';
|
|
453
|
-
exportColumns.forEach(col => {
|
|
454
|
-
let value: unknown = (row as GridRowModel)[col.field];
|
|
455
|
-
|
|
456
|
-
if (col.valueGetter) {
|
|
457
|
-
value = col.valueGetter({ row, field: col.field, value });
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (col.valueFormatter && value !== undefined && value !== null) {
|
|
461
|
-
value = col.valueFormatter({ value, row, field: col.field });
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (col.type === 'image' && value) {
|
|
465
|
-
html += `<td><div style="display:flex;justify-content:center;"><img src="${value}" alt="${col.headerName}" style="max-height: 40px; border-radius: 4px;"></div></td>`;
|
|
466
|
-
} else {
|
|
467
|
-
html += `<td>${escapeHTML(String(value ?? ''))}</td>`;
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
html += '</tr>';
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
if (aggregationResult) {
|
|
516
|
+
let html = `<!DOCTYPE html><html><head><title>${title || 'Print'}</title><style>
|
|
517
|
+
body{font-family:Arial,sans-serif;margin:20px;}
|
|
518
|
+
h1{font-size:24px;margin-bottom:20px;}
|
|
519
|
+
table{border-collapse:collapse;width:100%;}
|
|
520
|
+
th,td{border:1px solid #ddd;padding:8px;text-align:left;}
|
|
521
|
+
th{background-color:#f5f5f5;font-weight:bold;}
|
|
522
|
+
tr:nth-child(even){background-color:#f9f9f9;}
|
|
523
|
+
.group-header{background:#e8eaf6;font-weight:bold;}
|
|
524
|
+
.group-subtotal{background:#f0f4ff;font-style:italic;}
|
|
525
|
+
.grand-total{background:#f5f5f5;font-weight:bold;}
|
|
526
|
+
@media print{body{margin:0;}@page{margin:1cm;}}
|
|
527
|
+
</style></head><body>`;
|
|
528
|
+
|
|
529
|
+
if (title) html += `<h1>${escapeHTML(title)}</h1>`;
|
|
530
|
+
html += '<table><thead><tr>';
|
|
531
|
+
exportColumns.forEach(col => { html += `<th>${escapeHTML(col.headerName || col.field)}</th>`; });
|
|
532
|
+
html += '</tr></thead><tbody>';
|
|
533
|
+
|
|
534
|
+
if (groupedRows && groupedRows.length > 0) {
|
|
474
535
|
const aggModel = aggregationModel || {};
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
536
|
+
groupedRows.forEach(entry => {
|
|
537
|
+
if (entry.type === 'group-header') {
|
|
538
|
+
const indent = ' '.repeat(entry.depth * 4);
|
|
539
|
+
const label = `${indent}${escapeHTML(entry.groupField ?? '')}: ${escapeHTML(String(entry.groupValue ?? ''))}`;
|
|
540
|
+
html += `<tr class="group-header"><td colspan="${exportColumns.length}">${label}</td></tr>`;
|
|
541
|
+
} else if (entry.type === 'leaf' && entry.row) {
|
|
542
|
+
const row = entry.row as R;
|
|
543
|
+
const indent = ' '.repeat(entry.depth * 4);
|
|
544
|
+
html += '<tr>';
|
|
545
|
+
exportColumns.forEach((col, i) => {
|
|
546
|
+
let value: unknown = (row as GridRowModel)[col.field];
|
|
547
|
+
if (col.valueGetter) value = col.valueGetter({ row, field: col.field, value });
|
|
548
|
+
if (col.valueFormatter && value !== undefined && value !== null) {
|
|
549
|
+
value = col.valueFormatter({ value, row, field: col.field });
|
|
550
|
+
}
|
|
551
|
+
if (col.type === 'image' && value) {
|
|
552
|
+
html += `<td><img src="${value}" style="max-height:40px;border-radius:4px;"></td>`;
|
|
553
|
+
} else {
|
|
554
|
+
html += `<td>${(i === 0 ? indent : '') + escapeHTML(String(value ?? ''))}</td>`;
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
html += '</tr>';
|
|
558
|
+
} else if ((entry.type === 'group-subtotal' || entry.type === 'grand-total') && entry.aggregatedValues) {
|
|
559
|
+
const isGrand = entry.type === 'grand-total';
|
|
560
|
+
const indent = isGrand ? '' : ' '.repeat(entry.depth * 4);
|
|
561
|
+
const label = isGrand ? 'Grand Total' : `${indent}Subtotal`;
|
|
562
|
+
const cls = isGrand ? 'grand-total' : 'group-subtotal';
|
|
563
|
+
html += `<tr class="${cls}">`;
|
|
564
|
+
exportColumns.forEach((col, i) => {
|
|
565
|
+
if (i === 0) { html += `<td>${label}</td>`; return; }
|
|
566
|
+
const aggVal = entry.aggregatedValues![col.field];
|
|
567
|
+
if (aggVal !== undefined && aggVal !== null) {
|
|
568
|
+
let formatted: unknown = aggVal;
|
|
569
|
+
if (col.valueFormatter) formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
570
|
+
else if (aggModel[col.field]) formatted = formatAggregationValue(aggVal, aggModel[col.field]);
|
|
571
|
+
html += `<td style="text-align:right;">${escapeHTML(String(formatted))}</td>`;
|
|
572
|
+
} else {
|
|
573
|
+
html += '<td></td>';
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
html += '</tr>';
|
|
481
577
|
}
|
|
482
578
|
});
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
579
|
+
} else {
|
|
580
|
+
const rowsToExport = selectedRows && selectedRows.length > 0
|
|
581
|
+
? rows.filter(r => selectedRows.includes(r.id))
|
|
582
|
+
: rows;
|
|
583
|
+
|
|
584
|
+
if (rowsToExport.length === 0) {
|
|
585
|
+
printWindow.document.body.innerHTML = '<h3>No rows to export</h3>';
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
rowsToExport.forEach(row => {
|
|
590
|
+
html += '<tr>';
|
|
591
|
+
exportColumns.forEach(col => {
|
|
592
|
+
let value: unknown = (row as GridRowModel)[col.field];
|
|
593
|
+
if (col.valueGetter) value = col.valueGetter({ row, field: col.field, value });
|
|
594
|
+
if (col.valueFormatter && value !== undefined && value !== null) {
|
|
595
|
+
value = col.valueFormatter({ value, row, field: col.field });
|
|
492
596
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
597
|
+
if (col.type === 'image' && value) {
|
|
598
|
+
html += `<td><div style="display:flex;justify-content:center;"><img src="${value}" alt="${col.headerName}" style="max-height:40px;border-radius:4px;"></div></td>`;
|
|
599
|
+
} else {
|
|
600
|
+
html += `<td>${escapeHTML(String(value ?? ''))}</td>`;
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
html += '</tr>';
|
|
497
604
|
});
|
|
498
|
-
|
|
605
|
+
|
|
606
|
+
if (aggregationResult) {
|
|
607
|
+
const aggModel = aggregationModel || {};
|
|
608
|
+
html += '<tfoot><tr style="font-size:11px;color:#666;background-color:#f5f5f5;">';
|
|
609
|
+
exportColumns.forEach(col => {
|
|
610
|
+
html += aggModel[col.field]
|
|
611
|
+
? `<td style="text-align:right;font-weight:bold;">${escapeHTML(aggModel[col.field].toUpperCase())}</td>`
|
|
612
|
+
: '<td></td>';
|
|
613
|
+
});
|
|
614
|
+
html += '</tr><tr style="font-weight:bold;background-color:#f5f5f5;">';
|
|
615
|
+
exportColumns.forEach(col => {
|
|
616
|
+
const aggVal = aggregationResult![col.field];
|
|
617
|
+
if (aggVal !== undefined && aggVal !== null) {
|
|
618
|
+
let formatted: unknown = aggVal;
|
|
619
|
+
if (col.valueFormatter) formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
620
|
+
else if (aggModel[col.field]) formatted = formatAggregationValue(aggVal, aggModel[col.field]);
|
|
621
|
+
html += `<td style="text-align:right;">${escapeHTML(String(formatted))}</td>`;
|
|
622
|
+
} else {
|
|
623
|
+
html += '<td></td>';
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
html += '</tr></tfoot>';
|
|
627
|
+
}
|
|
499
628
|
}
|
|
500
629
|
|
|
501
630
|
html += '</tbody></table></body></html>';
|