@opencorestack/opengridx 3.9.0 → 3.10.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 +37 -0
- package/README.md +11 -7
- package/dist/index.d.ts +55 -0
- package/dist/opengridx.es.js +6346 -6279
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +16 -16
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +4 -0
- package/docs/architecture/datagrid-orchestration.md +4 -2
- package/docs/architecture/use-grid-virtualization.md +2 -0
- package/docs/components/pagination.md +1 -1
- package/docs/features/ai-toolkit.md +1 -0
- package/docs/features/rtl.md +1 -1
- package/docs/performance.md +19 -1
- package/docs/roadmap.md +26 -5
- package/lib/components/Cell/Cell.tsx +2 -1
- package/lib/components/DataGrid/DataGrid.tsx +19 -18
- package/lib/components/DataGrid/GridListView.tsx +1 -1
- package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
- package/lib/components/DataGrid/GridViewport.tsx +2 -1
- package/lib/components/DataGrid/GridVirtualRows.tsx +1 -1
- package/lib/components/Header/Header.tsx +4 -3
- package/lib/components/Row/Row.tsx +6 -2
- package/lib/components/Toolbar/AggregationPanel.tsx +90 -0
- package/lib/components/Toolbar/ColumnsPanelWrapper.tsx +56 -0
- package/lib/components/Toolbar/FilterPanelWrapper.tsx +82 -0
- package/lib/components/Toolbar/GridToolbar.tsx +7 -380
- package/lib/components/Toolbar/ToolbarIcons.tsx +35 -0
- package/lib/components/Toolbar/useToolbarPanel.ts +71 -0
- package/lib/hooks/core/gridKeyDown.ts +272 -0
- package/lib/hooks/core/useDataGrid.ts +13 -5
- package/lib/hooks/core/useGridApiMethods.ts +2 -2
- package/lib/hooks/core/useGridCellRangeDrag.ts +345 -0
- package/lib/hooks/core/useGridCellSelection.ts +17 -297
- package/lib/hooks/core/useGridColumnLookup.ts +2 -1
- package/lib/hooks/core/useGridColumns.tsx +3 -2
- package/lib/hooks/core/useGridControlledState.ts +3 -2
- package/lib/hooks/core/useGridHierarchy.ts +1 -1
- package/lib/hooks/core/useGridKeyboardNavigation.ts +51 -235
- package/lib/hooks/core/useGridLiveRowSelection.ts +2 -2
- package/lib/hooks/core/useGridRowInteractions.ts +1 -1
- package/lib/hooks/core/useGridRowSelection.ts +11 -2
- package/lib/hooks/core/useGridToolbarProps.ts +3 -2
- package/lib/hooks/features/useGridAiAssistant.ts +3 -2
- package/lib/hooks/useColumnReorder.ts +2 -1
- package/lib/hooks/useTreeData.ts +4 -2
- package/lib/types/locale.ts +22 -0
- package/lib/utils/aggregation/index.ts +16 -0
- package/lib/utils/aiAssistant.ts +46 -15
- package/lib/utils/export/excelCells.ts +244 -0
- package/lib/utils/export/exportCsv.ts +179 -0
- package/lib/utils/export/exportExcelHtml.ts +220 -0
- package/lib/utils/export/exportJson.ts +179 -0
- package/lib/utils/export/exportRows.ts +85 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +22 -242
- package/lib/utils/export/index.ts +5 -695
- package/lib/utils/export/printGrid.ts +282 -0
- package/lib/utils/gridLocale.ts +30 -0
- package/lib/utils/selectionSets.ts +36 -0
- package/lib/utils/untypedColumns.ts +16 -0
- package/llms.txt +3 -1
- package/package.json +6 -2
- package/lib/theme/__baseline__/default-theme.darwin.chromium.json +0 -1
- package/lib/theme/__baseline__/default-theme.darwin.firefox.json +0 -1
- package/lib/theme/__baseline__/default-theme.darwin.webkit.json +0 -1
|
@@ -1,697 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
escapeHTML,
|
|
7
|
-
formatExportAggregate,
|
|
8
|
-
formatExportValue,
|
|
9
|
-
getExportColumns,
|
|
10
|
-
getRawExportValue,
|
|
11
|
-
hasSelection,
|
|
12
|
-
isNonTextValue,
|
|
13
|
-
isTrustedNonText,
|
|
14
|
-
neutralizeFormula,
|
|
15
|
-
pickExportedAggregates,
|
|
16
|
-
rowsForExport,
|
|
17
|
-
sanitizeSheetName,
|
|
18
|
-
shouldExportGrouped,
|
|
19
|
-
summaryLabelText,
|
|
20
|
-
} from './exportShared';
|
|
21
|
-
|
|
22
|
-
export interface CsvExportOptions<R extends GridValidRowModel = GridRowModel> {
|
|
23
|
-
fileName?: string;
|
|
24
|
-
includeHeaders?: boolean;
|
|
25
|
-
delimiter?: string;
|
|
26
|
-
/**
|
|
27
|
-
* Export only these row IDs. A non-empty selection takes precedence over `groupedRows` (the
|
|
28
|
-
* selected rows are exported flat), and the totals row is recomputed over the selected rows.
|
|
29
|
-
*/
|
|
30
|
-
selectedRows?: (string | number)[];
|
|
31
|
-
/**
|
|
32
|
-
* The grid's `getRowId`, when it has one: `selectedRows` holds those ids, and the grid does not
|
|
33
|
-
* copy them onto `row.id` (v3.0+). Defaults to `row.id`.
|
|
34
|
-
*/
|
|
35
|
-
getRowId?: (row: R) => GridRowId;
|
|
36
|
-
aggregationResult?: Record<string, unknown> | null;
|
|
37
|
-
aggregationModel?: GridAggregationModel | null;
|
|
38
|
-
/** When provided, emits group headers, leaf rows, subtotals, and a grand total instead of a flat row list. */
|
|
39
|
-
groupedRows?: GridGroupedExportRow[];
|
|
40
|
-
/**
|
|
41
|
-
* Prefix text cells that start with `=`, `+`, `-`, `@`, tab or carriage return with `'`, so
|
|
42
|
-
* spreadsheet apps show them as text instead of running them as formulas (CSV injection).
|
|
43
|
-
* Plain signed numbers are left alone. Default: `true`
|
|
44
|
-
* @since v3.0
|
|
45
|
-
*/
|
|
46
|
-
escapeFormulas?: boolean;
|
|
47
|
-
/**
|
|
48
|
-
* Start the file with a UTF-8 byte-order mark, so Excel reads non-ASCII text (é, ü, ₹, 中文)
|
|
49
|
-
* correctly. Default: `true`
|
|
50
|
-
* @since v3.0
|
|
51
|
-
*/
|
|
52
|
-
bom?: boolean;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** One exported cell of a data row: its text, and whether that text was formatted from a non-text value. */
|
|
56
|
-
interface ExportCell {
|
|
57
|
-
text: string;
|
|
58
|
-
raw: unknown;
|
|
59
|
-
/** The text came from the grid's own formatting of a number, date or boolean (see `isTrustedNonText`). */
|
|
60
|
-
fromNonText: boolean;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function exportCell<R extends GridRowModel>(row: R, col: GridColDef<R>): ExportCell {
|
|
64
|
-
const raw = getRawExportValue(row, col);
|
|
65
|
-
const text = formatExportValue(row, col, raw);
|
|
66
|
-
return { text, raw, fromNonText: isTrustedNonText(raw, text, !!col.valueFormatter) };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Aggregate text per export column for one subtotal / total record. */
|
|
70
|
-
function aggregateTexts<R extends GridRowModel>(
|
|
71
|
-
exportColumns: GridColDef<R>[],
|
|
72
|
-
aggregates: Record<string, unknown>,
|
|
73
|
-
aggModel: GridAggregationModel,
|
|
74
|
-
): string[] {
|
|
75
|
-
return exportColumns.map(col => formatExportAggregate(col, aggModel[col.field], aggregates[col.field], aggregates));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const UTF8_BOM = String.fromCharCode(0xfeff);
|
|
79
|
-
|
|
80
|
-
export function exportToCsv<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef<R>[], options?: CsvExportOptions<R>): void;
|
|
81
|
-
export function exportToCsv<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef[], options?: CsvExportOptions<R>): void;
|
|
82
|
-
export function exportToCsv<R extends GridRowModel>(
|
|
83
|
-
rows: R[],
|
|
84
|
-
columns: GridColDef<R>[],
|
|
85
|
-
options: CsvExportOptions<R> = {}
|
|
86
|
-
): void {
|
|
87
|
-
const {
|
|
88
|
-
fileName = 'export.csv',
|
|
89
|
-
includeHeaders = true,
|
|
90
|
-
delimiter = ',',
|
|
91
|
-
selectedRows,
|
|
92
|
-
groupedRows,
|
|
93
|
-
escapeFormulas = true,
|
|
94
|
-
bom = true,
|
|
95
|
-
} = options;
|
|
96
|
-
|
|
97
|
-
const exportColumns = getExportColumns(columns);
|
|
98
|
-
const aggModel = options.aggregationModel || {};
|
|
99
|
-
const field = (text: string, fromNonText = false): string =>
|
|
100
|
-
escapeCSV(escapeFormulas ? neutralizeFormula(text, fromNonText) : text, delimiter);
|
|
101
|
-
const headerLine = () => exportColumns.map(col => field(col.headerName || col.field)).join(delimiter) + '\n';
|
|
102
|
-
|
|
103
|
-
let csvContent = '';
|
|
104
|
-
|
|
105
|
-
if (shouldExportGrouped(groupedRows, selectedRows)) {
|
|
106
|
-
// Grouped export path
|
|
107
|
-
if (includeHeaders) csvContent += headerLine();
|
|
108
|
-
groupedRows.forEach(entry => {
|
|
109
|
-
const indent = ' '.repeat(entry.depth);
|
|
110
|
-
if (entry.type === 'group-header') {
|
|
111
|
-
const labelText = groupHeaderLabel(entry, columns);
|
|
112
|
-
const label = escapeCSV(indent + (escapeFormulas ? neutralizeFormula(labelText) : labelText), delimiter);
|
|
113
|
-
csvContent += exportColumns.map((_, i) => (i === 0 ? label : '')).join(delimiter) + '\n';
|
|
114
|
-
} else if (entry.type === 'leaf' && entry.row) {
|
|
115
|
-
const row = entry.row as R;
|
|
116
|
-
const cells = exportColumns.map((col, i) => {
|
|
117
|
-
const cell = exportCell(row, col);
|
|
118
|
-
const text = escapeFormulas ? neutralizeFormula(cell.text, cell.fromNonText) : cell.text;
|
|
119
|
-
return escapeCSV((i === 0 ? indent : '') + text, delimiter);
|
|
120
|
-
});
|
|
121
|
-
csvContent += cells.join(delimiter) + '\n';
|
|
122
|
-
} else if ((entry.type === 'group-subtotal' || entry.type === 'grand-total') && entry.aggregatedValues) {
|
|
123
|
-
const values = entry.aggregatedValues;
|
|
124
|
-
const label = entry.type === 'grand-total' ? 'Grand Total' : `${indent}Subtotal`;
|
|
125
|
-
const texts = aggregateTexts(exportColumns, values, aggModel);
|
|
126
|
-
const cells = exportColumns.map((col, i) => (i === 0
|
|
127
|
-
? escapeCSV(summaryLabelText(label, texts[0]), delimiter)
|
|
128
|
-
: field(texts[i], isNonTextValue(values[col.field]))));
|
|
129
|
-
csvContent += cells.join(delimiter) + '\n';
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
} else {
|
|
133
|
-
const rowsToExport = rowsForExport(rows, selectedRows, options.getRowId);
|
|
134
|
-
|
|
135
|
-
if (rowsToExport.length === 0) {
|
|
136
|
-
console.warn('No rows to export');
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (includeHeaders) csvContent += headerLine();
|
|
141
|
-
|
|
142
|
-
rowsToExport.forEach(row => {
|
|
143
|
-
const values = exportColumns.map(col => {
|
|
144
|
-
const cell = exportCell(row, col);
|
|
145
|
-
return field(cell.text, cell.fromNonText);
|
|
146
|
-
});
|
|
147
|
-
csvContent += values.join(delimiter) + '\n';
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
const aggResult = aggregationForExport(rowsToExport, hasSelection(selectedRows), columns, options.aggregationResult, options.aggregationModel);
|
|
151
|
-
if (aggResult) {
|
|
152
|
-
const aggLabels = exportColumns.map(col => field(aggModel[col.field] ? aggModel[col.field].toUpperCase() : ''));
|
|
153
|
-
csvContent += aggLabels.join(delimiter) + '\n';
|
|
154
|
-
const texts = aggregateTexts(exportColumns, aggResult, aggModel);
|
|
155
|
-
csvContent += exportColumns.map((col, i) => field(texts[i], isNonTextValue(aggResult[col.field]))).join(delimiter) + '\n';
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
downloadFile((bom ? UTF8_BOM : '') + csvContent, fileName, 'text/csv;charset=utf-8;');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** Quote a CSV field when it contains the delimiter, a quote, or a line break (LF or a bare CR). */
|
|
163
|
-
function escapeCSV(value: string, delimiter: string): string {
|
|
164
|
-
if ((delimiter !== '' && value.includes(delimiter)) || value.includes('"') || value.includes('\n') || value.includes('\r')) {
|
|
165
|
-
return `"${value.replace(/"/g, '""')}"`;
|
|
166
|
-
}
|
|
167
|
-
return value;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export interface ExcelExportOptions<R extends GridValidRowModel = GridRowModel> {
|
|
171
|
-
/**
|
|
172
|
-
* Output filename. The file is an HTML table that Excel opens as a legacy `.xls` workbook, so a
|
|
173
|
-
* `.xlsx` extension is replaced with `.xls` (Excel refuses HTML content named `.xlsx`). Use
|
|
174
|
-
* `exportToExcelAdvanced` for a real `.xlsx`. Default: `'export.xls'`
|
|
175
|
-
*/
|
|
176
|
-
fileName?: string;
|
|
177
|
-
/** Sheet tab name. Characters Excel rejects are replaced and the name is cut to 31 characters. Default: `'Sheet1'` */
|
|
178
|
-
sheetName?: string;
|
|
179
|
-
includeHeaders?: boolean;
|
|
180
|
-
/**
|
|
181
|
-
* Export only these row IDs. A non-empty selection takes precedence over `groupedRows` (the
|
|
182
|
-
* selected rows are exported flat), and the totals rows are recomputed over the selected rows.
|
|
183
|
-
*/
|
|
184
|
-
selectedRows?: (string | number)[];
|
|
185
|
-
/**
|
|
186
|
-
* The grid's `getRowId`, when it has one: `selectedRows` holds those ids, and the grid does not
|
|
187
|
-
* copy them onto `row.id` (v3.0+). Defaults to `row.id`.
|
|
188
|
-
*/
|
|
189
|
-
getRowId?: (row: R) => GridRowId;
|
|
190
|
-
aggregationResult?: Record<string, unknown> | null;
|
|
191
|
-
aggregationModel?: GridAggregationModel | null;
|
|
192
|
-
/** When provided, emits group headers, leaf rows, subtotals, and a grand total instead of a flat row list. */
|
|
193
|
-
groupedRows?: GridGroupedExportRow[];
|
|
194
|
-
/**
|
|
195
|
-
* Prefix text cells that start with `=`, `+`, `-`, `@`, tab or carriage return with `'`, so
|
|
196
|
-
* Excel does not run them as formulas. Default: `true`
|
|
197
|
-
* @since v3.0
|
|
198
|
-
*/
|
|
199
|
-
escapeFormulas?: boolean;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** Excel's text number format: the cell keeps its text as typed (leading zeros, long ids, "1-2"). */
|
|
203
|
-
const XLS_TEXT_CLASS = 'ogx-xls-text';
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Whether an HTML-Excel data cell should be marked as text. Number, date and boolean columns, and
|
|
207
|
-
* values that are numbers, dates or booleans, are left for Excel to type.
|
|
208
|
-
*/
|
|
209
|
-
function isXlsTextCell<R extends GridRowModel>(col: GridColDef<R>, raw: unknown): boolean {
|
|
210
|
-
return col.type !== 'number' && col.type !== 'date' && col.type !== 'boolean' && !isNonTextValue(raw);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function xlsFileName(fileName: string): string {
|
|
214
|
-
if (!/\.xlsx$/i.test(fileName)) return fileName;
|
|
215
|
-
const corrected = fileName.replace(/\.xlsx$/i, '.xls');
|
|
216
|
-
console.warn(
|
|
217
|
-
`[exportToExcel] "${fileName}" renamed to "${corrected}": exportToExcel writes an HTML table that Excel ` +
|
|
218
|
-
'refuses to open as .xlsx. Use exportToExcelAdvanced for a real .xlsx file.'
|
|
219
|
-
);
|
|
220
|
-
return corrected;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export function exportToExcel<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef<R>[], options?: ExcelExportOptions<R>): void;
|
|
224
|
-
export function exportToExcel<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef[], options?: ExcelExportOptions<R>): void;
|
|
225
|
-
export function exportToExcel<R extends GridRowModel>(
|
|
226
|
-
rows: R[],
|
|
227
|
-
columns: GridColDef<R>[],
|
|
228
|
-
options: ExcelExportOptions<R> = {}
|
|
229
|
-
): void {
|
|
230
|
-
const {
|
|
231
|
-
fileName = 'export.xls',
|
|
232
|
-
sheetName = 'Sheet1',
|
|
233
|
-
includeHeaders = true,
|
|
234
|
-
selectedRows,
|
|
235
|
-
groupedRows,
|
|
236
|
-
escapeFormulas = true,
|
|
237
|
-
} = options;
|
|
238
|
-
|
|
239
|
-
const exportColumns = getExportColumns(columns);
|
|
240
|
-
const aggModel = options.aggregationModel || {};
|
|
241
|
-
const cellText = (text: string, fromNonText = false): string =>
|
|
242
|
-
escapeHTML(escapeFormulas ? neutralizeFormula(text, fromNonText) : text);
|
|
243
|
-
const textTd = (html: string): string => `<td class="${XLS_TEXT_CLASS}">${html}</td>`;
|
|
244
|
-
|
|
245
|
-
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">'
|
|
246
|
-
+ '<head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet>'
|
|
247
|
-
+ `<x:Name>${escapeHTML(sanitizeSheetName(sheetName))}</x:Name>`
|
|
248
|
-
+ '<x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]-->'
|
|
249
|
-
+ `<meta charset="UTF-8"><style>.${XLS_TEXT_CLASS}{mso-number-format:"\\@";}</style></head><body><table border="1">`;
|
|
250
|
-
|
|
251
|
-
let html = header;
|
|
252
|
-
|
|
253
|
-
if (includeHeaders) {
|
|
254
|
-
html += '<thead><tr>';
|
|
255
|
-
exportColumns.forEach(col => { html += `<th class="${XLS_TEXT_CLASS}">${cellText(col.headerName || col.field)}</th>`; });
|
|
256
|
-
html += '</tr></thead>';
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
html += '<tbody>';
|
|
260
|
-
|
|
261
|
-
const dataCell = (cell: ExportCell, col: GridColDef<R>, prefix = ''): string => {
|
|
262
|
-
const content = prefix + cellText(cell.text, cell.fromNonText);
|
|
263
|
-
return isXlsTextCell(col, cell.raw) ? textTd(content) : `<td>${content}</td>`;
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
if (shouldExportGrouped(groupedRows, selectedRows)) {
|
|
267
|
-
groupedRows.forEach(entry => {
|
|
268
|
-
const indent = ' '.repeat(entry.depth * 2);
|
|
269
|
-
if (entry.type === 'group-header') {
|
|
270
|
-
html += `<tr style="font-weight:bold;background:#e8eaf6;">`;
|
|
271
|
-
html += `<td colspan="${exportColumns.length}" class="${XLS_TEXT_CLASS}">${escapeHTML(indent)}${cellText(groupHeaderLabel(entry, columns))}</td>`;
|
|
272
|
-
html += '</tr>';
|
|
273
|
-
} else if (entry.type === 'leaf' && entry.row) {
|
|
274
|
-
const row = entry.row as R;
|
|
275
|
-
html += '<tr>';
|
|
276
|
-
exportColumns.forEach((col, i) => { html += dataCell(exportCell(row, col), col, i === 0 ? escapeHTML(indent) : ''); });
|
|
277
|
-
html += '</tr>';
|
|
278
|
-
} else if ((entry.type === 'group-subtotal' || entry.type === 'grand-total') && entry.aggregatedValues) {
|
|
279
|
-
const values = entry.aggregatedValues;
|
|
280
|
-
const label = entry.type === 'grand-total' ? 'Grand Total' : `${indent}Subtotal`;
|
|
281
|
-
const texts = aggregateTexts(exportColumns, values, aggModel);
|
|
282
|
-
html += `<tr style="font-weight:bold;background:#f5f5f5;">`;
|
|
283
|
-
exportColumns.forEach((col, i) => {
|
|
284
|
-
if (i === 0) { html += textTd(escapeHTML(summaryLabelText(label, texts[0]))); return; }
|
|
285
|
-
html += texts[i] ? `<td style="text-align:right;">${cellText(texts[i], isNonTextValue(values[col.field]))}</td>` : '<td></td>';
|
|
286
|
-
});
|
|
287
|
-
html += '</tr>';
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
} else {
|
|
291
|
-
const rowsToExport = rowsForExport(rows, selectedRows, options.getRowId);
|
|
292
|
-
|
|
293
|
-
if (rowsToExport.length === 0) {
|
|
294
|
-
console.warn('No rows to export');
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
rowsToExport.forEach(row => {
|
|
299
|
-
html += '<tr>';
|
|
300
|
-
exportColumns.forEach(col => { html += dataCell(exportCell(row, col), col); });
|
|
301
|
-
html += '</tr>';
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
const aggResult = aggregationForExport(rowsToExport, hasSelection(selectedRows), columns, options.aggregationResult, options.aggregationModel);
|
|
305
|
-
if (aggResult) {
|
|
306
|
-
html += '<tr style="font-size: 11px; color: #666; background-color: #f5f5f5;">';
|
|
307
|
-
exportColumns.forEach(col => {
|
|
308
|
-
html += aggModel[col.field]
|
|
309
|
-
? `<td style="text-align:right;font-weight:bold;">${escapeHTML(aggModel[col.field].toUpperCase())}</td>`
|
|
310
|
-
: '<td></td>';
|
|
311
|
-
});
|
|
312
|
-
html += '</tr><tr style="font-weight:bold;background-color:#f5f5f5;">';
|
|
313
|
-
const texts = aggregateTexts(exportColumns, aggResult, aggModel);
|
|
314
|
-
exportColumns.forEach((col, i) => {
|
|
315
|
-
html += texts[i] ? `<td style="text-align:right;">${cellText(texts[i], isNonTextValue(aggResult[col.field]))}</td>` : '<td></td>';
|
|
316
|
-
});
|
|
317
|
-
html += '</tr>';
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
html += '</tbody></table></body></html>';
|
|
322
|
-
downloadFile(html, xlsFileName(fileName), 'application/vnd.ms-excel');
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// ============================================================================
|
|
326
|
-
// JSON Export
|
|
327
|
-
// ============================================================================
|
|
328
|
-
|
|
329
|
-
export interface JsonExportOptions<R extends GridValidRowModel = GridRowModel> {
|
|
330
|
-
fileName?: string;
|
|
331
|
-
pretty?: boolean;
|
|
332
|
-
/**
|
|
333
|
-
* Export only these row IDs. A non-empty selection takes precedence over `groupedRows` (the
|
|
334
|
-
* selected rows are exported flat), and the aggregation values are recomputed over them.
|
|
335
|
-
*/
|
|
336
|
-
selectedRows?: (string | number)[];
|
|
337
|
-
/**
|
|
338
|
-
* The grid's `getRowId`, when it has one: `selectedRows` holds those ids, and the grid does not
|
|
339
|
-
* copy them onto `row.id` (v3.0+). Defaults to `row.id`.
|
|
340
|
-
*/
|
|
341
|
-
getRowId?: (row: R) => GridRowId;
|
|
342
|
-
aggregationResult?: Record<string, unknown> | null;
|
|
343
|
-
aggregationModel?: GridAggregationModel | null;
|
|
344
|
-
/** When provided, emits a nested grouping tree instead of a flat array. */
|
|
345
|
-
groupedRows?: GridGroupedExportRow[];
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* Export data to JSON format. Values are written raw (after `valueGetter`, without
|
|
350
|
-
* `valueFormatter`), including aggregation values, so the output can be parsed as data.
|
|
351
|
-
*/
|
|
352
|
-
export function exportToJson<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef<R>[], options?: JsonExportOptions<R>): void;
|
|
353
|
-
export function exportToJson<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef[], options?: JsonExportOptions<R>): void;
|
|
354
|
-
export function exportToJson<R extends GridRowModel>(
|
|
355
|
-
rows: R[],
|
|
356
|
-
columns: GridColDef<R>[],
|
|
357
|
-
options: JsonExportOptions<R> = {}
|
|
358
|
-
): void {
|
|
359
|
-
const {
|
|
360
|
-
fileName = 'export.json',
|
|
361
|
-
pretty = true,
|
|
362
|
-
selectedRows,
|
|
363
|
-
groupedRows,
|
|
364
|
-
} = options;
|
|
365
|
-
|
|
366
|
-
const exportColumns = getExportColumns(columns);
|
|
367
|
-
|
|
368
|
-
type GroupNode = {
|
|
369
|
-
group: string;
|
|
370
|
-
field: string;
|
|
371
|
-
value: unknown;
|
|
372
|
-
rows: Record<string, unknown>[];
|
|
373
|
-
subtotals?: Record<string, unknown>;
|
|
374
|
-
children?: GroupNode[];
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
type JsonPayload =
|
|
378
|
-
| Record<string, unknown>[]
|
|
379
|
-
| { data: Record<string, unknown>[]; aggregation: { labels: Record<string, unknown>; values: Record<string, unknown> } }
|
|
380
|
-
| { groups: GroupNode[]; grandTotal?: Record<string, unknown> };
|
|
381
|
-
|
|
382
|
-
const rowObject = (row: R): Record<string, unknown> => {
|
|
383
|
-
const obj: Record<string, unknown> = {};
|
|
384
|
-
exportColumns.forEach(col => { obj[col.field] = getRawExportValue(row, col); });
|
|
385
|
-
return obj;
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
let outData: JsonPayload;
|
|
389
|
-
|
|
390
|
-
if (shouldExportGrouped(groupedRows, selectedRows)) {
|
|
391
|
-
// Build nested tree from the flat ordered list
|
|
392
|
-
const stack: GroupNode[] = [];
|
|
393
|
-
const roots: GroupNode[] = [];
|
|
394
|
-
let grandTotal: Record<string, unknown> | undefined;
|
|
395
|
-
|
|
396
|
-
groupedRows.forEach(entry => {
|
|
397
|
-
if (entry.type === 'group-header') {
|
|
398
|
-
const node: GroupNode = {
|
|
399
|
-
group: groupHeaderLabel(entry, columns),
|
|
400
|
-
field: entry.groupField ?? '',
|
|
401
|
-
value: entry.groupValue,
|
|
402
|
-
rows: [],
|
|
403
|
-
children: [],
|
|
404
|
-
};
|
|
405
|
-
while (stack.length > entry.depth) stack.pop();
|
|
406
|
-
if (stack.length === 0) {
|
|
407
|
-
roots.push(node);
|
|
408
|
-
} else {
|
|
409
|
-
(stack[stack.length - 1].children ??= []).push(node);
|
|
410
|
-
}
|
|
411
|
-
stack.push(node);
|
|
412
|
-
} else if (entry.type === 'leaf' && entry.row) {
|
|
413
|
-
if (stack.length > 0) stack[stack.length - 1].rows.push(rowObject(entry.row as R));
|
|
414
|
-
} else if (entry.type === 'group-subtotal' && entry.aggregatedValues && stack.length > 0) {
|
|
415
|
-
stack[stack.length - 1].subtotals = pickExportedAggregates(entry.aggregatedValues, exportColumns);
|
|
416
|
-
stack.pop();
|
|
417
|
-
} else if (entry.type === 'grand-total' && entry.aggregatedValues) {
|
|
418
|
-
grandTotal = pickExportedAggregates(entry.aggregatedValues, exportColumns);
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
outData = grandTotal ? { groups: roots, grandTotal } : { groups: roots };
|
|
423
|
-
} else {
|
|
424
|
-
const rowsToExport = rowsForExport(rows, selectedRows, options.getRowId);
|
|
425
|
-
|
|
426
|
-
if (rowsToExport.length === 0) {
|
|
427
|
-
console.warn('No rows to export');
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const data = rowsToExport.map(rowObject);
|
|
432
|
-
|
|
433
|
-
const aggResult = aggregationForExport(rowsToExport, hasSelection(selectedRows), columns, options.aggregationResult, options.aggregationModel);
|
|
434
|
-
if (aggResult) {
|
|
435
|
-
const aggModel = options.aggregationModel || {};
|
|
436
|
-
const summaryLabelObj: Record<string, unknown> = {};
|
|
437
|
-
exportColumns.forEach(col => { if (aggModel[col.field]) summaryLabelObj[col.field] = aggModel[col.field].toUpperCase(); });
|
|
438
|
-
const summaryDataObj: Record<string, unknown> = {};
|
|
439
|
-
exportColumns.forEach(col => {
|
|
440
|
-
if (aggResult[col.field] !== undefined && aggResult[col.field] !== null) summaryDataObj[col.field] = aggResult[col.field];
|
|
441
|
-
});
|
|
442
|
-
outData = { data, aggregation: { labels: summaryLabelObj, values: summaryDataObj } };
|
|
443
|
-
} else {
|
|
444
|
-
outData = data;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
downloadFile(pretty ? JSON.stringify(outData, null, 2) : JSON.stringify(outData), fileName, 'application/json;charset=utf-8;');
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// ============================================================================
|
|
452
|
-
// Print
|
|
453
|
-
// ============================================================================
|
|
454
|
-
|
|
455
|
-
export interface PrintOptions<R extends GridValidRowModel = GridRowModel> {
|
|
456
|
-
title?: string;
|
|
457
|
-
/**
|
|
458
|
-
* Print only these row IDs. A non-empty selection takes precedence over `groupedRows` (the
|
|
459
|
-
* selected rows are printed flat), and the totals are recomputed over the selected rows.
|
|
460
|
-
*/
|
|
461
|
-
selectedRows?: (string | number)[];
|
|
462
|
-
/**
|
|
463
|
-
* The grid's `getRowId`, when it has one: `selectedRows` holds those ids, and the grid does not
|
|
464
|
-
* copy them onto `row.id` (v3.0+). Defaults to `row.id`.
|
|
465
|
-
*/
|
|
466
|
-
getRowId?: (row: R) => GridRowId;
|
|
467
|
-
aggregationResult?: Record<string, unknown> | null;
|
|
468
|
-
aggregationModel?: GridAggregationModel | null;
|
|
469
|
-
/** When provided, emits group headers, leaf rows, subtotals, and a grand total instead of a flat row list. */
|
|
470
|
-
groupedRows?: GridGroupedExportRow[];
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const SAFE_IMAGE_URL = /^(?:https?:|data:image\/|blob:)/i;
|
|
474
|
-
const URL_SCHEME = /^[a-z][a-z\d+.-]*:/i;
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* The URL to use as an `<img src>` in the print window, or null when it is not an image URL:
|
|
478
|
-
* http(s), `data:image/…`, `blob:` and relative URLs are allowed; `javascript:`, `data:text/html`,
|
|
479
|
-
* `file:` and other schemes are not.
|
|
480
|
-
*/
|
|
481
|
-
function printableImageUrl(value: string): string | null {
|
|
482
|
-
const url = value.trim();
|
|
483
|
-
if (!url) return null;
|
|
484
|
-
if (SAFE_IMAGE_URL.test(url)) return url;
|
|
485
|
-
return URL_SCHEME.test(url) ? null : url;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Open print dialog with formatted table
|
|
490
|
-
*/
|
|
491
|
-
export function printGrid<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef<R>[], titleOrOptions?: string | PrintOptions<R>): Promise<void>;
|
|
492
|
-
export function printGrid<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef[], titleOrOptions?: string | PrintOptions<R>): Promise<void>;
|
|
493
|
-
export async function printGrid<R extends GridRowModel>(
|
|
494
|
-
rows: R[],
|
|
495
|
-
columns: GridColDef<R>[],
|
|
496
|
-
titleOrOptions?: string | PrintOptions<R>
|
|
497
|
-
): Promise<void> {
|
|
498
|
-
// 1. Open window immediately to resolve user activation constraints and provide feedback
|
|
499
|
-
const printWindow = window.open('', '_blank');
|
|
500
|
-
|
|
501
|
-
if (!printWindow) {
|
|
502
|
-
console.error('Failed to open print window. Please check your popup blocker settings.');
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// 2. Show loading state
|
|
507
|
-
printWindow.document.write(`
|
|
508
|
-
<!DOCTYPE html>
|
|
509
|
-
<html>
|
|
510
|
-
<head>
|
|
511
|
-
<title>Preparing Print...</title>
|
|
512
|
-
<style>
|
|
513
|
-
body { font-family: sans-serif; height: 100vh; display: flex; align-items: center; justify-content: center; margin: 0; background: #f5f5f5; }
|
|
514
|
-
.loader { text-align: center; }
|
|
515
|
-
.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; }
|
|
516
|
-
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
517
|
-
</style>
|
|
518
|
-
</head>
|
|
519
|
-
<body>
|
|
520
|
-
<div class="loader">
|
|
521
|
-
<div class="spinner"></div>
|
|
522
|
-
<h2>Generating Print Preview...</h2>
|
|
523
|
-
<p>Processing data, please wait.</p>
|
|
524
|
-
</div>
|
|
525
|
-
</body>
|
|
526
|
-
</html>
|
|
527
|
-
`);
|
|
528
|
-
printWindow.document.close();
|
|
529
|
-
|
|
530
|
-
// 3. Yield to main thread to allow the new window to render its loading state
|
|
531
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
532
|
-
|
|
533
|
-
try {
|
|
534
|
-
let title = 'Print';
|
|
535
|
-
let selectedRows: (string | number)[] | undefined;
|
|
536
|
-
let getRowId: ((row: R) => GridRowId) | undefined;
|
|
537
|
-
let aggregationResult: Record<string, unknown> | null = null;
|
|
538
|
-
let aggregationModel: GridAggregationModel | null = null;
|
|
539
|
-
|
|
540
|
-
let groupedRows: GridGroupedExportRow[] | undefined;
|
|
541
|
-
|
|
542
|
-
if (typeof titleOrOptions === 'string') {
|
|
543
|
-
title = titleOrOptions;
|
|
544
|
-
} else if (typeof titleOrOptions === 'object') {
|
|
545
|
-
title = titleOrOptions.title || 'Print';
|
|
546
|
-
selectedRows = titleOrOptions.selectedRows;
|
|
547
|
-
getRowId = titleOrOptions.getRowId;
|
|
548
|
-
aggregationResult = titleOrOptions.aggregationResult || null;
|
|
549
|
-
aggregationModel = titleOrOptions.aggregationModel || null;
|
|
550
|
-
groupedRows = titleOrOptions.groupedRows;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const exportColumns = getExportColumns(columns);
|
|
554
|
-
const aggModel = aggregationModel || {};
|
|
555
|
-
|
|
556
|
-
// Group-row rules are qualified with `tr` so they outrank the stripe rule, which has the same
|
|
557
|
-
// specificity, by coming later.
|
|
558
|
-
let html = `<!DOCTYPE html><html><head><title>${escapeHTML(title || 'Print')}</title><style>
|
|
559
|
-
body{font-family:Arial,sans-serif;margin:20px;}
|
|
560
|
-
h1{font-size:24px;margin-bottom:20px;}
|
|
561
|
-
table{border-collapse:collapse;width:100%;}
|
|
562
|
-
th,td{border:1px solid #ddd;padding:8px;text-align:left;}
|
|
563
|
-
th{background-color:#f5f5f5;font-weight:bold;}
|
|
564
|
-
tr:nth-child(even){background-color:#f9f9f9;}
|
|
565
|
-
tr.group-header{background:#e8eaf6;font-weight:bold;}
|
|
566
|
-
tr.group-subtotal{background:#f0f4ff;font-style:italic;}
|
|
567
|
-
tr.grand-total{background:#f5f5f5;font-weight:bold;}
|
|
568
|
-
@media print{body{margin:0;}@page{margin:1cm;}}
|
|
569
|
-
</style></head><body>`;
|
|
570
|
-
|
|
571
|
-
if (title) html += `<h1>${escapeHTML(title)}</h1>`;
|
|
572
|
-
html += '<table><thead><tr>';
|
|
573
|
-
exportColumns.forEach(col => { html += `<th>${escapeHTML(col.headerName || col.field)}</th>`; });
|
|
574
|
-
html += '</tr></thead><tbody>';
|
|
575
|
-
|
|
576
|
-
const dataCell = (row: R, col: GridColDef<R>, prefixHtml = '', centerImage = false): string => {
|
|
577
|
-
const { text } = exportCell(row, col);
|
|
578
|
-
const src = col.type === 'image' ? printableImageUrl(text) : null;
|
|
579
|
-
if (src !== null) {
|
|
580
|
-
const img = `<img src="${escapeHTML(src)}" alt="${escapeHTML(col.headerName || col.field)}" style="max-height:40px;border-radius:4px;">`;
|
|
581
|
-
return centerImage ? `<td><div style="display:flex;justify-content:center;">${img}</div></td>` : `<td>${img}</td>`;
|
|
582
|
-
}
|
|
583
|
-
return `<td>${prefixHtml}${escapeHTML(text)}</td>`;
|
|
584
|
-
};
|
|
585
|
-
|
|
586
|
-
if (shouldExportGrouped(groupedRows, selectedRows)) {
|
|
587
|
-
groupedRows.forEach(entry => {
|
|
588
|
-
const indent = ' '.repeat(entry.depth * 4);
|
|
589
|
-
if (entry.type === 'group-header') {
|
|
590
|
-
html += `<tr class="group-header"><td colspan="${exportColumns.length}">${indent}${escapeHTML(groupHeaderLabel(entry, columns))}</td></tr>`;
|
|
591
|
-
} else if (entry.type === 'leaf' && entry.row) {
|
|
592
|
-
const row = entry.row as R;
|
|
593
|
-
html += '<tr>';
|
|
594
|
-
exportColumns.forEach((col, i) => { html += dataCell(row, col, i === 0 ? indent : ''); });
|
|
595
|
-
html += '</tr>';
|
|
596
|
-
} else if ((entry.type === 'group-subtotal' || entry.type === 'grand-total') && entry.aggregatedValues) {
|
|
597
|
-
const isGrand = entry.type === 'grand-total';
|
|
598
|
-
const texts = aggregateTexts(exportColumns, entry.aggregatedValues, aggModel);
|
|
599
|
-
html += `<tr class="${isGrand ? 'grand-total' : 'group-subtotal'}">`;
|
|
600
|
-
exportColumns.forEach((_, i) => {
|
|
601
|
-
if (i === 0) {
|
|
602
|
-
html += `<td>${isGrand ? '' : indent}${escapeHTML(summaryLabelText(isGrand ? 'Grand Total' : 'Subtotal', texts[0]))}</td>`;
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
html += texts[i] ? `<td style="text-align:right;">${escapeHTML(texts[i])}</td>` : '<td></td>';
|
|
606
|
-
});
|
|
607
|
-
html += '</tr>';
|
|
608
|
-
}
|
|
609
|
-
});
|
|
610
|
-
} else {
|
|
611
|
-
const rowsToExport = rowsForExport(rows, selectedRows, getRowId);
|
|
612
|
-
|
|
613
|
-
if (rowsToExport.length === 0) {
|
|
614
|
-
printWindow.document.body.innerHTML = '<h3>No rows to export</h3>';
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
rowsToExport.forEach(row => {
|
|
619
|
-
html += '<tr>';
|
|
620
|
-
exportColumns.forEach(col => { html += dataCell(row, col, '', true); });
|
|
621
|
-
html += '</tr>';
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
const aggResult = aggregationForExport(rowsToExport, hasSelection(selectedRows), columns, aggregationResult, aggregationModel);
|
|
625
|
-
if (aggResult) {
|
|
626
|
-
html += '<tfoot><tr style="font-size:11px;color:#666;background-color:#f5f5f5;">';
|
|
627
|
-
exportColumns.forEach(col => {
|
|
628
|
-
html += aggModel[col.field]
|
|
629
|
-
? `<td style="text-align:right;font-weight:bold;">${escapeHTML(aggModel[col.field].toUpperCase())}</td>`
|
|
630
|
-
: '<td></td>';
|
|
631
|
-
});
|
|
632
|
-
html += '</tr><tr style="font-weight:bold;background-color:#f5f5f5;">';
|
|
633
|
-
aggregateTexts(exportColumns, aggResult, aggModel).forEach(text => {
|
|
634
|
-
html += text ? `<td style="text-align:right;">${escapeHTML(text)}</td>` : '<td></td>';
|
|
635
|
-
});
|
|
636
|
-
html += '</tr></tfoot>';
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
html += '</tbody></table></body></html>';
|
|
641
|
-
|
|
642
|
-
// Write the HTML content
|
|
643
|
-
printWindow.document.open();
|
|
644
|
-
printWindow.document.write(html);
|
|
645
|
-
printWindow.document.close();
|
|
646
|
-
|
|
647
|
-
// Set the document title explicitly (fixes about:blank issue)
|
|
648
|
-
printWindow.document.title = title || 'Print Preview';
|
|
649
|
-
|
|
650
|
-
printWindow.focus();
|
|
651
|
-
|
|
652
|
-
let hasPrinted = false;
|
|
653
|
-
|
|
654
|
-
printWindow.onload = () => {
|
|
655
|
-
if (!hasPrinted) {
|
|
656
|
-
hasPrinted = true;
|
|
657
|
-
printWindow.print();
|
|
658
|
-
|
|
659
|
-
setTimeout(() => {
|
|
660
|
-
printWindow.close();
|
|
661
|
-
}, 100);
|
|
662
|
-
}
|
|
663
|
-
};
|
|
664
|
-
|
|
665
|
-
setTimeout(() => {
|
|
666
|
-
if (!hasPrinted && printWindow && !printWindow.closed) {
|
|
667
|
-
hasPrinted = true;
|
|
668
|
-
printWindow.print();
|
|
669
|
-
setTimeout(() => {
|
|
670
|
-
if (!printWindow.closed) printWindow.close();
|
|
671
|
-
}, 100);
|
|
672
|
-
}
|
|
673
|
-
}, 500);
|
|
674
|
-
|
|
675
|
-
} catch (e) {
|
|
676
|
-
console.error('Print generation failed:', e);
|
|
677
|
-
if (printWindow && !printWindow.closed) {
|
|
678
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
679
|
-
printWindow.document.body.innerHTML = `<div style="color: red; padding: 20px;"><h3>Error Generating Print Preview</h3><p>${escapeHTML(message)}</p></div>`;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function downloadFile(content: string, fileName: string, mimeType: string): void {
|
|
685
|
-
const blob = new Blob([content], { type: mimeType });
|
|
686
|
-
const url = URL.createObjectURL(blob);
|
|
687
|
-
const link = document.createElement('a');
|
|
688
|
-
link.href = url;
|
|
689
|
-
link.download = fileName;
|
|
690
|
-
document.body.appendChild(link);
|
|
691
|
-
link.click();
|
|
692
|
-
document.body.removeChild(link);
|
|
693
|
-
URL.revokeObjectURL(url);
|
|
694
|
-
}
|
|
695
|
-
|
|
1
|
+
// The exporters, one file per format; this module only re-exports them (public import path).
|
|
2
|
+
export { exportToCsv, type CsvExportOptions } from './exportCsv';
|
|
3
|
+
export { exportToExcel, type ExcelExportOptions } from './exportExcelHtml';
|
|
4
|
+
export { exportToJson, type JsonExportOptions } from './exportJson';
|
|
5
|
+
export { printGrid, type PrintOptions } from './printGrid';
|
|
696
6
|
export { exportToPdf } from './exportToPdf';
|
|
697
7
|
export type { PdfExportOptions, PdfExportProgress } from '../../types';
|