@opencorestack/opengridx 0.1.6 → 0.1.8
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 +30 -1
- package/README.md +9 -0
- package/dist/index.d.ts +12 -1
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +2505 -2450
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +12 -12
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +177 -0
- package/docs/README.md +60 -0
- package/docs/components/cell.md +44 -0
- package/docs/components/column-group-header.md +57 -0
- package/docs/components/column-resize.md +63 -0
- package/docs/components/column-visibility.md +61 -0
- package/docs/components/datagrid.md +70 -0
- package/docs/components/filter-panel.md +26 -0
- package/docs/components/header.md +39 -0
- package/docs/components/pagination.md +34 -0
- package/docs/components/row.md +31 -0
- package/docs/components/toolbar.md +40 -0
- package/docs/components/tooltip.md +68 -0
- package/docs/customization/slots-api.md +227 -0
- package/docs/customization/theming.md +293 -0
- package/docs/features/aggregation-pivot.md +79 -0
- package/docs/features/cell-spanning.md +520 -0
- package/docs/features/clipboard.md +76 -0
- package/docs/features/custom-pagination.md +143 -0
- package/docs/features/data-source.md +72 -0
- package/docs/features/editing-reordering.md +99 -0
- package/docs/features/export-guide.md +434 -0
- package/docs/features/filtering.md +83 -0
- package/docs/features/infinite-scroll.md +87 -0
- package/docs/features/list-view.md +65 -0
- package/docs/features/loading-states.md +68 -0
- package/docs/features/master-detail.md +46 -0
- package/docs/features/pinning.md +67 -0
- package/docs/features/selection.md +70 -0
- package/docs/features/sorting-pagination.md +66 -0
- package/docs/features/state-persistence.md +67 -0
- package/docs/features/tree-data-grouping.md +67 -0
- package/docs/features/virtualization.md +188 -0
- package/docs/roadmap.md +71 -0
- package/lib/components/Cell/Cell.css +119 -0
- package/lib/components/Cell/Cell.tsx +264 -0
- package/lib/components/Cell/GridEditInputCell.css +62 -0
- package/lib/components/Cell/GridEditInputCell.tsx +152 -0
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +67 -0
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +220 -0
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +31 -0
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +85 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +137 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +190 -0
- package/lib/components/DataGrid/DataGrid.css +224 -0
- package/lib/components/DataGrid/DataGrid.tsx +2269 -0
- package/lib/components/DetailPanel/DetailPanel.css +36 -0
- package/lib/components/DetailPanel/DetailPanel.tsx +34 -0
- package/lib/components/FilterPanel/FilterPanel.css +196 -0
- package/lib/components/FilterPanel/FilterPanel.tsx +214 -0
- package/lib/components/FilterPanel/index.ts +1 -0
- package/lib/components/Header/ColumnMenu.css +68 -0
- package/lib/components/Header/ColumnMenu.tsx +249 -0
- package/lib/components/Header/Header.css +224 -0
- package/lib/components/Header/Header.tsx +516 -0
- package/lib/components/ListView/ListView.css +112 -0
- package/lib/components/ListView/ListViewRow.tsx +63 -0
- package/lib/components/Pagination/Pagination.css +91 -0
- package/lib/components/Pagination/Pagination.tsx +151 -0
- package/lib/components/QuickFilter/QuickFilter.css +23 -0
- package/lib/components/QuickFilter/QuickFilter.tsx +79 -0
- package/lib/components/Row/Row.css +126 -0
- package/lib/components/Row/Row.tsx +377 -0
- package/lib/components/Skeleton/Skeleton.css +55 -0
- package/lib/components/Skeleton/Skeleton.tsx +22 -0
- package/lib/components/SkeletonRow/SkeletonRow.css +32 -0
- package/lib/components/SkeletonRow/SkeletonRow.tsx +89 -0
- package/lib/components/SkeletonRow/index.ts +1 -0
- package/lib/components/Toolbar/GlobalSearch.css +110 -0
- package/lib/components/Toolbar/GlobalSearch.tsx +167 -0
- package/lib/components/Toolbar/GridToolbar.css +221 -0
- package/lib/components/Toolbar/GridToolbar.tsx +658 -0
- package/lib/components/Toolbar/PivotPanel.css +212 -0
- package/lib/components/Toolbar/PivotPanel.tsx +239 -0
- package/lib/components/Tooltip/Tooltip.css +59 -0
- package/lib/components/Tooltip/Tooltip.tsx +106 -0
- package/lib/components/ui/Button.css +89 -0
- package/lib/components/ui/Button.tsx +39 -0
- package/lib/components/ui/Checkbox.css +71 -0
- package/lib/components/ui/Checkbox.tsx +66 -0
- package/lib/components/ui/DragHandleIcon.css +19 -0
- package/lib/components/ui/DragHandleIcon.tsx +18 -0
- package/lib/components/ui/ExpandIcon.css +38 -0
- package/lib/components/ui/ExpandIcon.tsx +75 -0
- package/lib/components/ui/Input.css +59 -0
- package/lib/components/ui/Input.tsx +45 -0
- package/lib/components/ui/index.ts +15 -0
- package/lib/hooks/core/useDataGrid.ts +392 -0
- package/lib/hooks/core/useGridApiRef.ts +10 -0
- package/lib/hooks/features/useAggregation.ts +143 -0
- package/lib/hooks/features/useGridClipboard.ts +133 -0
- package/lib/hooks/features/useGridDataSource.ts +190 -0
- package/lib/hooks/features/useGridEditing.ts +100 -0
- package/lib/hooks/features/useGridSpanning.ts +349 -0
- package/lib/hooks/features/usePivot.ts +174 -0
- package/lib/hooks/useColumnReorder.ts +114 -0
- package/lib/hooks/useRowGrouping.ts +302 -0
- package/lib/hooks/useRowReorder.ts +83 -0
- package/lib/hooks/useTreeData.ts +323 -0
- package/lib/index.ts +106 -0
- package/lib/state/index.ts +17 -0
- package/lib/state/types.ts +48 -0
- package/lib/state/useGridStateStorage.ts +113 -0
- package/lib/styles/base/reset.css +21 -0
- package/lib/styles/base/variables.css +161 -0
- package/lib/styles/opengridx.css +27 -0
- package/lib/styles/opengridx.css.d.ts +4 -0
- package/lib/theme/DataGridThemeProvider.tsx +276 -0
- package/lib/theme/defaultStyles.ts +0 -0
- package/lib/theme/index.ts +21 -0
- package/lib/theme/presets.ts +148 -0
- package/lib/theme/types.ts +155 -0
- package/lib/types/index.ts +764 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +516 -0
- package/lib/utils/export/index.ts +550 -0
- package/lib/utils/filtering/index.ts +240 -0
- package/lib/utils/pinning/index.ts +282 -0
- package/lib/utils/sorting/index.ts +55 -0
- package/lib/utils/virtualization/index.ts +106 -0
- package/lib/vite-env.d.ts +1 -0
- package/llms.txt +2 -1
- package/package.json +12 -4
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
|
|
2
|
+
import type { GridColDef, GridRowModel, GridAggregationModel } from '../../types';
|
|
3
|
+
import { formatAggregationValue } from '../../hooks/features/useAggregation';
|
|
4
|
+
|
|
5
|
+
export interface CsvExportOptions {
|
|
6
|
+
fileName?: string;
|
|
7
|
+
includeHeaders?: boolean;
|
|
8
|
+
delimiter?: string;
|
|
9
|
+
selectedRows?: (string | number)[];
|
|
10
|
+
aggregationResult?: Record<string, any> | null;
|
|
11
|
+
aggregationModel?: GridAggregationModel | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function exportToCsv<R extends GridRowModel>(
|
|
15
|
+
rows: R[],
|
|
16
|
+
columns: GridColDef<R>[],
|
|
17
|
+
options: CsvExportOptions = {}
|
|
18
|
+
): void {
|
|
19
|
+
const {
|
|
20
|
+
fileName = 'export.csv',
|
|
21
|
+
includeHeaders = true,
|
|
22
|
+
delimiter = ',',
|
|
23
|
+
selectedRows
|
|
24
|
+
} = options;
|
|
25
|
+
|
|
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
|
+
const exportColumns = columns.filter(col => {
|
|
36
|
+
if (col.exportable === false) return false;
|
|
37
|
+
if (col.field === '__check__' || col.field === '__actions__') return false;
|
|
38
|
+
return true;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let csvContent = '';
|
|
42
|
+
|
|
43
|
+
// Add headers
|
|
44
|
+
if (includeHeaders) {
|
|
45
|
+
const headers = exportColumns.map(col => escapeCSV(col.headerName || col.field));
|
|
46
|
+
csvContent += headers.join(delimiter) + '\n';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
rowsToExport.forEach(row => {
|
|
50
|
+
const values = exportColumns.map(col => {
|
|
51
|
+
let value: any = row[col.field as keyof R];
|
|
52
|
+
|
|
53
|
+
if (col.valueGetter) {
|
|
54
|
+
value = col.valueGetter({ row, field: col.field, value });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (col.valueFormatter && value !== undefined && value !== null) {
|
|
58
|
+
value = col.valueFormatter({ value, row, field: col.field });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return escapeCSV(String(value ?? ''));
|
|
62
|
+
});
|
|
63
|
+
csvContent += values.join(delimiter) + '\n';
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Add Aggregation
|
|
67
|
+
if (options.aggregationResult) {
|
|
68
|
+
const aggResult = options.aggregationResult;
|
|
69
|
+
const aggModel = options.aggregationModel || {};
|
|
70
|
+
|
|
71
|
+
// Print Aggregation Function Labels
|
|
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]);
|
|
89
|
+
}
|
|
90
|
+
return escapeCSV(String(formatted));
|
|
91
|
+
}
|
|
92
|
+
return escapeCSV('');
|
|
93
|
+
});
|
|
94
|
+
csvContent += aggValues.join(delimiter) + '\n';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
downloadFile(csvContent, fileName, 'text/csv;charset=utf-8;');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function escapeCSV(value: string): string {
|
|
101
|
+
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
|
102
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface ExcelExportOptions {
|
|
108
|
+
fileName?: string;
|
|
109
|
+
sheetName?: string;
|
|
110
|
+
includeHeaders?: boolean;
|
|
111
|
+
selectedRows?: (string | number)[];
|
|
112
|
+
aggregationResult?: Record<string, any> | null;
|
|
113
|
+
aggregationModel?: GridAggregationModel | null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function exportToExcel<R extends GridRowModel>(
|
|
117
|
+
rows: R[],
|
|
118
|
+
columns: GridColDef<R>[],
|
|
119
|
+
options: ExcelExportOptions = {}
|
|
120
|
+
): void {
|
|
121
|
+
const {
|
|
122
|
+
fileName = 'export.xlsx',
|
|
123
|
+
sheetName = 'Sheet1',
|
|
124
|
+
includeHeaders = true,
|
|
125
|
+
selectedRows
|
|
126
|
+
} = options;
|
|
127
|
+
|
|
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
|
+
const exportColumns = columns.filter(col => {
|
|
138
|
+
if (col.exportable === false) return false;
|
|
139
|
+
if (col.field === '__check__' || col.field === '__actions__') return false;
|
|
140
|
+
return true;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
let html = '<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">';
|
|
144
|
+
html += '<head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet>';
|
|
145
|
+
html += `<x:Name>${sheetName}</x:Name>`;
|
|
146
|
+
html += '<x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]-->';
|
|
147
|
+
html += '<meta charset="UTF-8"></head><body>';
|
|
148
|
+
html += '<table border="1">';
|
|
149
|
+
|
|
150
|
+
if (includeHeaders) {
|
|
151
|
+
html += '<thead><tr>';
|
|
152
|
+
exportColumns.forEach(col => {
|
|
153
|
+
html += `<th>${escapeHTML(col.headerName || col.field)}</th>`;
|
|
154
|
+
});
|
|
155
|
+
html += '</tr></thead>';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
html += '<tbody>';
|
|
159
|
+
rowsToExport.forEach(row => {
|
|
160
|
+
html += '<tr>';
|
|
161
|
+
exportColumns.forEach(col => {
|
|
162
|
+
let value: any = row[col.field as keyof R];
|
|
163
|
+
|
|
164
|
+
if (col.valueGetter) {
|
|
165
|
+
value = col.valueGetter({ row, field: col.field, value });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (col.valueFormatter && value !== undefined && value !== null) {
|
|
169
|
+
value = col.valueFormatter({ value, row, field: col.field });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
html += `<td>${escapeHTML(String(value ?? ''))}</td>`;
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (options.aggregationResult) {
|
|
177
|
+
const aggResult = options.aggregationResult;
|
|
178
|
+
const aggModel = options.aggregationModel || {};
|
|
179
|
+
|
|
180
|
+
html += '<tr style="font-size: 11px; color: #666; background-color: #f5f5f5;">';
|
|
181
|
+
exportColumns.forEach((col) => {
|
|
182
|
+
if (aggModel[col.field]) {
|
|
183
|
+
html += `<td style="text-align: right; font-weight: bold;">${escapeHTML(aggModel[col.field].toUpperCase())}</td>`;
|
|
184
|
+
} else {
|
|
185
|
+
html += `<td></td>`;
|
|
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]);
|
|
199
|
+
}
|
|
200
|
+
html += `<td style="text-align: right;">${escapeHTML(String(formatted))}</td>`;
|
|
201
|
+
} else {
|
|
202
|
+
html += `<td></td>`;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
html += '</tr>';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
html += '</tbody></table></body></html>';
|
|
209
|
+
|
|
210
|
+
downloadFile(html, fileName, 'application/vnd.ms-excel');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function escapeHTML(value: string): string {
|
|
214
|
+
return value
|
|
215
|
+
.replace(/&/g, '&')
|
|
216
|
+
.replace(/</g, '<')
|
|
217
|
+
.replace(/>/g, '>')
|
|
218
|
+
.replace(/"/g, '"')
|
|
219
|
+
.replace(/'/g, ''');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// JSON Export
|
|
224
|
+
// ============================================================================
|
|
225
|
+
|
|
226
|
+
export interface JsonExportOptions {
|
|
227
|
+
fileName?: string;
|
|
228
|
+
pretty?: boolean;
|
|
229
|
+
selectedRows?: (string | number)[];
|
|
230
|
+
aggregationResult?: Record<string, any> | null;
|
|
231
|
+
aggregationModel?: GridAggregationModel | null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Export data to JSON format
|
|
236
|
+
*/
|
|
237
|
+
export function exportToJson<R extends GridRowModel>(
|
|
238
|
+
rows: R[],
|
|
239
|
+
columns: GridColDef<R>[],
|
|
240
|
+
options: JsonExportOptions = {}
|
|
241
|
+
): void {
|
|
242
|
+
const {
|
|
243
|
+
fileName = 'export.json',
|
|
244
|
+
pretty = true,
|
|
245
|
+
selectedRows
|
|
246
|
+
} = options;
|
|
247
|
+
|
|
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
|
+
const exportColumns = columns.filter(col => {
|
|
260
|
+
if (col.exportable === false) return false;
|
|
261
|
+
if (col.field === '__check__' || col.field === '__actions__') return false;
|
|
262
|
+
return true;
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Build JSON data
|
|
266
|
+
const data = rowsToExport.map(row => {
|
|
267
|
+
const obj: any = {};
|
|
268
|
+
exportColumns.forEach(col => {
|
|
269
|
+
let value: any = row[col.field as keyof R];
|
|
270
|
+
|
|
271
|
+
// Use valueGetter if available
|
|
272
|
+
if (col.valueGetter) {
|
|
273
|
+
value = col.valueGetter({ row, field: col.field, value });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
obj[col.field] = value;
|
|
277
|
+
});
|
|
278
|
+
return obj;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
let outData: any = data;
|
|
282
|
+
|
|
283
|
+
if (options.aggregationResult) {
|
|
284
|
+
const aggResult = options.aggregationResult;
|
|
285
|
+
const aggModel = options.aggregationModel || {};
|
|
286
|
+
|
|
287
|
+
const summaryLabelObj: any = {};
|
|
288
|
+
exportColumns.forEach((col) => {
|
|
289
|
+
if (aggModel[col.field]) {
|
|
290
|
+
summaryLabelObj[col.field] = aggModel[col.field].toUpperCase();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const summaryDataObj: any = {};
|
|
295
|
+
exportColumns.forEach((col) => {
|
|
296
|
+
if (aggResult[col.field] !== undefined && aggResult[col.field] !== null) {
|
|
297
|
+
let aggVal = aggResult[col.field];
|
|
298
|
+
if (col.valueFormatter) {
|
|
299
|
+
aggVal = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
300
|
+
} else if (aggModel[col.field]) {
|
|
301
|
+
aggVal = formatAggregationValue(aggVal, aggModel[col.field]);
|
|
302
|
+
}
|
|
303
|
+
summaryDataObj[col.field] = aggVal;
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
outData = {
|
|
308
|
+
data: data,
|
|
309
|
+
aggregation: {
|
|
310
|
+
labels: summaryLabelObj,
|
|
311
|
+
values: summaryDataObj
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const jsonContent = pretty ? JSON.stringify(outData, null, 2) : JSON.stringify(outData);
|
|
317
|
+
|
|
318
|
+
// Download file
|
|
319
|
+
downloadFile(jsonContent, fileName, 'application/json;charset=utf-8;');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ============================================================================
|
|
323
|
+
// Print
|
|
324
|
+
// ============================================================================
|
|
325
|
+
|
|
326
|
+
export interface PrintOptions {
|
|
327
|
+
title?: string;
|
|
328
|
+
selectedRows?: (string | number)[];
|
|
329
|
+
aggregationResult?: Record<string, any> | null;
|
|
330
|
+
aggregationModel?: GridAggregationModel | null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Open print dialog with formatted table
|
|
335
|
+
*/
|
|
336
|
+
export async function printGrid<R extends GridRowModel>(
|
|
337
|
+
rows: R[],
|
|
338
|
+
columns: GridColDef<R>[],
|
|
339
|
+
titleOrOptions?: string | PrintOptions
|
|
340
|
+
): Promise<void> {
|
|
341
|
+
// 1. Open window immediately to resolve user activation constraints and provide feedback
|
|
342
|
+
const printWindow = window.open('', '_blank');
|
|
343
|
+
|
|
344
|
+
if (!printWindow) {
|
|
345
|
+
console.error('Failed to open print window. Please check your popup blocker settings.');
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 2. Show loading state
|
|
350
|
+
printWindow.document.write(`
|
|
351
|
+
<!DOCTYPE html>
|
|
352
|
+
<html>
|
|
353
|
+
<head>
|
|
354
|
+
<title>Preparing Print...</title>
|
|
355
|
+
<style>
|
|
356
|
+
body { font-family: sans-serif; height: 100vh; display: flex; align-items: center; justify-content: center; margin: 0; background: #f5f5f5; }
|
|
357
|
+
.loader { text-align: center; }
|
|
358
|
+
.spinner { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 0 auto 20px; }
|
|
359
|
+
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
360
|
+
</style>
|
|
361
|
+
</head>
|
|
362
|
+
<body>
|
|
363
|
+
<div class="loader">
|
|
364
|
+
<div class="spinner"></div>
|
|
365
|
+
<h2>Generating Print Preview...</h2>
|
|
366
|
+
<p>Processing data, please wait.</p>
|
|
367
|
+
</div>
|
|
368
|
+
</body>
|
|
369
|
+
</html>
|
|
370
|
+
`);
|
|
371
|
+
printWindow.document.close();
|
|
372
|
+
|
|
373
|
+
// 3. Yield to main thread to allow the new window to render its loading state
|
|
374
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
let title = 'Print';
|
|
378
|
+
let selectedRows: (string | number)[] | undefined;
|
|
379
|
+
let aggregationResult: Record<string, any> | null = null;
|
|
380
|
+
let aggregationModel: GridAggregationModel | null = null;
|
|
381
|
+
|
|
382
|
+
if (typeof titleOrOptions === 'string') {
|
|
383
|
+
title = titleOrOptions;
|
|
384
|
+
} else if (typeof titleOrOptions === 'object') {
|
|
385
|
+
title = titleOrOptions.title || 'Print';
|
|
386
|
+
selectedRows = titleOrOptions.selectedRows;
|
|
387
|
+
aggregationResult = titleOrOptions.aggregationResult || null;
|
|
388
|
+
aggregationModel = titleOrOptions.aggregationModel || null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Filter rows if selection is active
|
|
392
|
+
const rowsToExport = selectedRows && selectedRows.length > 0
|
|
393
|
+
? rows.filter(r => selectedRows.includes(r.id))
|
|
394
|
+
: rows;
|
|
395
|
+
|
|
396
|
+
if (rowsToExport.length === 0) {
|
|
397
|
+
printWindow.document.body.innerHTML = '<h3>No rows to export</h3>';
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Filter columns
|
|
402
|
+
const exportColumns = columns.filter(col => {
|
|
403
|
+
if (col.exportable === false) return false;
|
|
404
|
+
if (col.field === '__check__' || col.field === '__actions__') return false;
|
|
405
|
+
return true;
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Build HTML
|
|
409
|
+
let html = `
|
|
410
|
+
<!DOCTYPE html>
|
|
411
|
+
<html>
|
|
412
|
+
<head>
|
|
413
|
+
<title>${title || 'Print'}</title>
|
|
414
|
+
<style>
|
|
415
|
+
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
416
|
+
h1 { font-size: 24px; margin-bottom: 20px; }
|
|
417
|
+
table { border-collapse: collapse; width: 100%; }
|
|
418
|
+
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
|
419
|
+
th { background-color: #f5f5f5; font-weight: bold; }
|
|
420
|
+
tr:nth-child(even) { background-color: #f9f9f9; }
|
|
421
|
+
@media print {
|
|
422
|
+
body { margin: 0; }
|
|
423
|
+
@page { margin: 1cm; }
|
|
424
|
+
}
|
|
425
|
+
</style>
|
|
426
|
+
</head>
|
|
427
|
+
<body>
|
|
428
|
+
`;
|
|
429
|
+
|
|
430
|
+
if (title) {
|
|
431
|
+
html += `<h1>${escapeHTML(title)}</h1>`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
html += '<table>';
|
|
435
|
+
|
|
436
|
+
// Headers
|
|
437
|
+
html += '<thead><tr>';
|
|
438
|
+
exportColumns.forEach(col => {
|
|
439
|
+
html += `<th>${escapeHTML(col.headerName || col.field)}</th>`;
|
|
440
|
+
});
|
|
441
|
+
html += '</tr></thead>';
|
|
442
|
+
|
|
443
|
+
// Rows
|
|
444
|
+
html += '<tbody>';
|
|
445
|
+
|
|
446
|
+
// Process rows (potential bottleneck logic here)
|
|
447
|
+
rowsToExport.forEach(row => {
|
|
448
|
+
html += '<tr>';
|
|
449
|
+
exportColumns.forEach(col => {
|
|
450
|
+
let value: any = row[col.field as keyof R];
|
|
451
|
+
|
|
452
|
+
if (col.valueGetter) {
|
|
453
|
+
value = col.valueGetter({ row, field: col.field, value });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (col.valueFormatter && value !== undefined && value !== null) {
|
|
457
|
+
value = col.valueFormatter({ value, row, field: col.field });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (col.type === 'image' && value) {
|
|
461
|
+
html += `<td><div style="display:flex;justify-content:center;"><img src="${value}" alt="${col.headerName}" style="max-height: 40px; border-radius: 4px;"></div></td>`;
|
|
462
|
+
} else {
|
|
463
|
+
html += `<td>${escapeHTML(String(value ?? ''))}</td>`;
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
html += '</tr>';
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
if (aggregationResult) {
|
|
470
|
+
const aggModel = aggregationModel || {};
|
|
471
|
+
html += '<tfoot><tr style="font-size: 11px; color: #666; background-color: #f5f5f5;">';
|
|
472
|
+
exportColumns.forEach((col) => {
|
|
473
|
+
if (aggModel[col.field]) {
|
|
474
|
+
html += `<td style="text-align: right; font-weight: bold;">${escapeHTML(aggModel[col.field].toUpperCase())}</td>`;
|
|
475
|
+
} else {
|
|
476
|
+
html += `<td></td>`;
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
html += '</tr><tr style="font-weight: bold; background-color: #f5f5f5;">';
|
|
480
|
+
exportColumns.forEach((col) => {
|
|
481
|
+
const aggVal = aggregationResult![col.field];
|
|
482
|
+
if (aggVal !== undefined && aggVal !== null) {
|
|
483
|
+
let formatted = aggVal;
|
|
484
|
+
if (col.valueFormatter) {
|
|
485
|
+
formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
486
|
+
} else if (aggModel[col.field]) {
|
|
487
|
+
formatted = formatAggregationValue(aggVal, aggModel[col.field]);
|
|
488
|
+
}
|
|
489
|
+
html += `<td style="text-align: right;">${escapeHTML(String(formatted))}</td>`;
|
|
490
|
+
} else {
|
|
491
|
+
html += `<td></td>`;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
html += '</tr></tfoot>';
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
html += '</tbody></table></body></html>';
|
|
498
|
+
|
|
499
|
+
// Write the HTML content
|
|
500
|
+
printWindow.document.open();
|
|
501
|
+
printWindow.document.write(html);
|
|
502
|
+
printWindow.document.close();
|
|
503
|
+
|
|
504
|
+
// Set the document title explicitly (fixes about:blank issue)
|
|
505
|
+
printWindow.document.title = title || 'Print Preview';
|
|
506
|
+
|
|
507
|
+
printWindow.focus();
|
|
508
|
+
|
|
509
|
+
let hasPrinted = false;
|
|
510
|
+
|
|
511
|
+
printWindow.onload = () => {
|
|
512
|
+
if (!hasPrinted) {
|
|
513
|
+
hasPrinted = true;
|
|
514
|
+
printWindow.print();
|
|
515
|
+
|
|
516
|
+
setTimeout(() => {
|
|
517
|
+
printWindow.close();
|
|
518
|
+
}, 100);
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
setTimeout(() => {
|
|
523
|
+
if (!hasPrinted && printWindow && !printWindow.closed) {
|
|
524
|
+
hasPrinted = true;
|
|
525
|
+
printWindow.print();
|
|
526
|
+
setTimeout(() => {
|
|
527
|
+
if (!printWindow.closed) printWindow.close();
|
|
528
|
+
}, 100);
|
|
529
|
+
}
|
|
530
|
+
}, 500);
|
|
531
|
+
|
|
532
|
+
} catch (e: any) {
|
|
533
|
+
console.error('Print generation failed:', e);
|
|
534
|
+
if (printWindow && !printWindow.closed) {
|
|
535
|
+
printWindow.document.body.innerHTML = `<div style="color: red; padding: 20px;"><h3>Error Generating Print Preview</h3><p>${e.message}</p></div>`;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function downloadFile(content: string, fileName: string, mimeType: string): void {
|
|
541
|
+
const blob = new Blob([content], { type: mimeType });
|
|
542
|
+
const url = URL.createObjectURL(blob);
|
|
543
|
+
const link = document.createElement('a');
|
|
544
|
+
link.href = url;
|
|
545
|
+
link.download = fileName;
|
|
546
|
+
document.body.appendChild(link);
|
|
547
|
+
link.click();
|
|
548
|
+
document.body.removeChild(link);
|
|
549
|
+
URL.revokeObjectURL(url);
|
|
550
|
+
}
|