UNPKG

15.2 kBJavaScriptView Raw
1"use strict";
2
3var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4Object.defineProperty(exports, "__esModule", {
5 value: true
6});
7exports.buildExcel = buildExcel;
8exports.getDataForValueOptionsSheet = getDataForValueOptionsSheet;
9exports.serializeColumn = void 0;
10exports.serializeColumns = serializeColumns;
11exports.serializeRowUnsafe = void 0;
12exports.setupExcelExportWebWorker = setupExcelExportWebWorker;
13var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
14var _interopRequireWildcard2 = _interopRequireDefault(require("@babel/runtime/helpers/interopRequireWildcard"));
15var _xDataGridPro = require("@mui/x-data-grid-pro");
16var _internals = require("@mui/x-data-grid/internals");
17var _warning = require("@mui/x-internals/warning");
18const getExcelJs = async () => {
19 const excelJsModule = await Promise.resolve().then(() => (0, _interopRequireWildcard2.default)(require('exceljs')));
20 return excelJsModule.default ?? excelJsModule;
21};
22const getFormattedValueOptions = (colDef, row, valueOptions, api) => {
23 if (!colDef.valueOptions) {
24 return [];
25 }
26 let valueOptionsFormatted = valueOptions;
27 if (colDef.valueFormatter) {
28 valueOptionsFormatted = valueOptionsFormatted.map(option => {
29 if (typeof option === 'object') {
30 return option;
31 }
32 return String(colDef.valueFormatter(option, row, colDef, {
33 current: api
34 }));
35 });
36 }
37 return valueOptionsFormatted.map(option => typeof option === 'object' ? option.label : option);
38};
39/**
40 * FIXME: This function mutates the colspan info, but colspan info assumes that the columns
41 * passed to it are always consistent. In this case, the exported columns may differ from the
42 * actual rendered columns.
43 * The caller of this function MUST call `resetColSpan()` before and after usage.
44 */
45const serializeRowUnsafe = (id, columns, apiRef, defaultValueOptionsFormulae, options) => {
46 const row = {};
47 const dataValidation = {};
48 const mergedCells = [];
49 const firstCellParams = apiRef.current.getCellParams(id, columns[0].field);
50 const outlineLevel = firstCellParams.rowNode.depth;
51 const hasColSpan = (0, _internals.gridHasColSpanSelector)(apiRef);
52 if (hasColSpan) {
53 // `colSpan` is only calculated for rendered rows, so we need to calculate it during export for every row
54 apiRef.current.calculateColSpan({
55 rowId: id,
56 minFirstColumn: 0,
57 maxLastColumn: columns.length,
58 columns
59 });
60 }
61 columns.forEach((column, colIndex) => {
62 const colSpanInfo = hasColSpan ? apiRef.current.unstable_getCellColSpanInfo(id, colIndex) : undefined;
63 if (colSpanInfo && colSpanInfo.spannedByColSpan) {
64 return;
65 }
66 if (colSpanInfo && colSpanInfo.cellProps.colSpan > 1) {
67 mergedCells.push({
68 leftIndex: colIndex + 1,
69 rightIndex: colIndex + colSpanInfo.cellProps.colSpan
70 });
71 }
72 const cellParams = apiRef.current.getCellParams(id, column.field);
73 let cellValue;
74 switch (cellParams.colDef.type) {
75 case 'singleSelect':
76 {
77 const castColumn = cellParams.colDef;
78 if (typeof castColumn.valueOptions === 'function') {
79 // If value option depends on the row, set specific options to the cell
80 // This dataValidation is buggy with LibreOffice and does not allow to have coma
81 const valueOptions = castColumn.valueOptions({
82 id,
83 row,
84 field: cellParams.field
85 });
86 const formattedValueOptions = getFormattedValueOptions(castColumn, row, valueOptions, apiRef.current);
87 dataValidation[castColumn.field] = {
88 type: 'list',
89 allowBlank: true,
90 formulae: [`"${formattedValueOptions.map(x => x.toString().replaceAll(',', 'CHAR(44)')).join(',')}"`]
91 };
92 } else {
93 const address = defaultValueOptionsFormulae[column.field].address;
94
95 // If value option is defined for the column, refer to another sheet
96 dataValidation[castColumn.field] = {
97 type: 'list',
98 allowBlank: true,
99 formulae: [address]
100 };
101 }
102 const formattedValue = apiRef.current.getCellParams(id, castColumn.field).formattedValue;
103 if (process.env.NODE_ENV !== 'production') {
104 if (String(cellParams.formattedValue) === '[object Object]') {
105 (0, _warning.warnOnce)(['MUI X: When the value of a field is an object or a `renderCell` is provided, the Excel export might not display the value correctly.', 'You can provide a `valueFormatter` with a string representation to be used.']);
106 }
107 }
108 if ((0, _internals.isObject)(formattedValue)) {
109 row[castColumn.field] = formattedValue?.label;
110 } else {
111 row[castColumn.field] = formattedValue;
112 }
113 break;
114 }
115 case 'boolean':
116 case 'number':
117 cellValue = apiRef.current.getCellParams(id, column.field).value;
118 break;
119 case 'date':
120 case 'dateTime':
121 {
122 // Excel does not do any timezone conversion, so we create a date using UTC instead of local timezone
123 // Solution from: https://github.com/exceljs/exceljs/issues/486#issuecomment-432557582
124 // About Date.UTC(): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC#exemples
125 const value = apiRef.current.getCellParams(id, column.field).value;
126 // value may be `undefined` in auto-generated grouping rows
127 if (!value) {
128 break;
129 }
130 const utcDate = new Date(Date.UTC(value.getFullYear(), value.getMonth(), value.getDate(), value.getHours(), value.getMinutes(), value.getSeconds()));
131 row[column.field] = utcDate;
132 break;
133 }
134 case 'actions':
135 break;
136 default:
137 cellValue = apiRef.current.getCellParams(id, column.field).formattedValue;
138 if (process.env.NODE_ENV !== 'production') {
139 if (String(cellParams.formattedValue) === '[object Object]') {
140 (0, _warning.warnOnce)(['MUI X: When the value of a field is an object or a `renderCell` is provided, the Excel export might not display the value correctly.', 'You can provide a `valueFormatter` with a string representation to be used.']);
141 }
142 }
143 break;
144 }
145 if (typeof cellValue === 'string' && options.escapeFormulas) {
146 // See https://owasp.org/www-community/attacks/CSV_Injection
147 if (['=', '+', '-', '@', '\t', '\r'].includes(cellValue[0])) {
148 cellValue = `'${cellValue}`;
149 }
150 }
151 if (typeof cellValue !== 'undefined') {
152 row[column.field] = cellValue;
153 }
154 });
155 return {
156 row,
157 dataValidation,
158 outlineLevel,
159 mergedCells
160 };
161};
162exports.serializeRowUnsafe = serializeRowUnsafe;
163const defaultColumnsStyles = {
164 [_xDataGridPro.GRID_DATE_COL_DEF.type]: {
165 numFmt: 'dd.mm.yyyy'
166 },
167 [_xDataGridPro.GRID_DATETIME_COL_DEF.type]: {
168 numFmt: 'dd.mm.yyyy hh:mm'
169 }
170};
171const serializeColumn = (column, columnsStyles) => {
172 const {
173 field,
174 type
175 } = column;
176 return {
177 key: field,
178 headerText: column.headerName ?? column.field,
179 // Excel width must stay between 0 and 255 (https://support.microsoft.com/en-us/office/change-the-column-width-and-row-height-72f5e3cc-994d-43e8-ae58-9774a0905f46)
180 // From the example of column width behavior (https://docs.microsoft.com/en-US/office/troubleshoot/excel/determine-column-widths#example-of-column-width-behavior)
181 // a value of 10 corresponds to 75px. This is an approximation, because column width depends on the font-size
182 width: Math.min(255, column.width ? column.width / 7.5 : 8.43),
183 style: (0, _extends2.default)({}, type && defaultColumnsStyles?.[type], columnsStyles?.[field])
184 };
185};
186exports.serializeColumn = serializeColumn;
187const addColumnGroupingHeaders = (worksheet, columns, columnGroupPaths, columnGroupDetails) => {
188 const maxDepth = Math.max(...columns.map(({
189 key
190 }) => columnGroupPaths[key]?.length ?? 0));
191 if (maxDepth === 0) {
192 return;
193 }
194 for (let rowIndex = 0; rowIndex < maxDepth; rowIndex += 1) {
195 const row = columns.map(({
196 key
197 }) => {
198 const groupingPath = columnGroupPaths[key];
199 if (groupingPath.length <= rowIndex) {
200 return {
201 groupId: null,
202 parents: groupingPath
203 };
204 }
205 return (0, _extends2.default)({}, columnGroupDetails[groupingPath[rowIndex]], {
206 parents: groupingPath.slice(0, rowIndex)
207 });
208 });
209 const newRow = worksheet.addRow(row.map(group => group.groupId === null ? null : group?.headerName ?? group.groupId));
210
211 // use `rowCount`, since worksheet can have additional rows added in `exceljsPreProcess`
212 const lastRowIndex = newRow.worksheet.rowCount;
213 let leftIndex = 0;
214 let rightIndex = 1;
215 while (rightIndex < columns.length) {
216 const {
217 groupId: leftGroupId,
218 parents: leftParents
219 } = row[leftIndex];
220 const {
221 groupId: rightGroupId,
222 parents: rightParents
223 } = row[rightIndex];
224 const areInSameGroup = leftGroupId === rightGroupId && leftParents.length === rightParents.length && leftParents.every((leftParent, index) => rightParents[index] === leftParent);
225 if (areInSameGroup) {
226 rightIndex += 1;
227 } else {
228 if (rightIndex - leftIndex > 1) {
229 worksheet.mergeCells(lastRowIndex, leftIndex + 1, lastRowIndex, rightIndex);
230 }
231 leftIndex = rightIndex;
232 rightIndex += 1;
233 }
234 }
235 if (rightIndex - leftIndex > 1) {
236 worksheet.mergeCells(lastRowIndex, leftIndex + 1, lastRowIndex, rightIndex);
237 }
238 }
239};
240function serializeColumns(columns, styles) {
241 return columns.map(column => serializeColumn(column, styles));
242}
243async function getDataForValueOptionsSheet(columns, valueOptionsSheetName, api) {
244 const candidateColumns = columns.filter(column => (0, _internals.isSingleSelectColDef)(column) && Array.isArray(column.valueOptions));
245
246 // Creates a temp worksheet to obtain the column letters
247 const excelJS = await getExcelJs();
248 const workbook = new excelJS.Workbook();
249 const worksheet = workbook.addWorksheet('Sheet1');
250 worksheet.columns = candidateColumns.map(column => ({
251 key: column.field
252 }));
253 return candidateColumns.reduce((acc, column) => {
254 const singleSelectColumn = column;
255 const formattedValueOptions = getFormattedValueOptions(singleSelectColumn, {}, singleSelectColumn.valueOptions, api);
256 const header = column.headerName ?? column.field;
257 const values = [header, ...formattedValueOptions];
258 const letter = worksheet.getColumn(column.field).letter;
259 const address = `${valueOptionsSheetName}!$${letter}$2:$${letter}$${values.length}`;
260 acc[column.field] = {
261 values,
262 address
263 };
264 return acc;
265 }, {});
266}
267function addSerializedRowToWorksheet(serializedRow, worksheet) {
268 const {
269 row,
270 dataValidation,
271 outlineLevel,
272 mergedCells
273 } = serializedRow;
274 const newRow = worksheet.addRow(row);
275 Object.keys(dataValidation).forEach(field => {
276 newRow.getCell(field).dataValidation = (0, _extends2.default)({}, dataValidation[field]);
277 });
278 if (outlineLevel) {
279 newRow.outlineLevel = outlineLevel;
280 }
281
282 // use `rowCount`, since worksheet can have additional rows added in `exceljsPreProcess`
283 const lastRowIndex = newRow.worksheet.rowCount;
284 mergedCells.forEach(mergedCell => {
285 worksheet.mergeCells(lastRowIndex, mergedCell.leftIndex, lastRowIndex, mergedCell.rightIndex);
286 });
287}
288async function createValueOptionsSheetIfNeeded(valueOptionsData, sheetName, workbook) {
289 if (Object.keys(valueOptionsData).length === 0) {
290 return;
291 }
292 const valueOptionsWorksheet = workbook.addWorksheet(sheetName);
293 valueOptionsWorksheet.columns = Object.keys(valueOptionsData).map(key => ({
294 key
295 }));
296 Object.entries(valueOptionsData).forEach(([field, {
297 values
298 }]) => {
299 valueOptionsWorksheet.getColumn(field).values = values;
300 });
301}
302async function buildExcel(options, apiRef) {
303 const {
304 columns,
305 rowIds,
306 includeHeaders,
307 includeColumnGroupsHeaders,
308 valueOptionsSheetName = 'Options',
309 exceljsPreProcess,
310 exceljsPostProcess,
311 columnsStyles = {}
312 } = options;
313 const excelJS = await getExcelJs();
314 const workbook = new excelJS.Workbook();
315 const worksheet = workbook.addWorksheet('Sheet1');
316 const serializedColumns = serializeColumns(columns, columnsStyles);
317 worksheet.columns = serializedColumns;
318 if (exceljsPreProcess) {
319 await exceljsPreProcess({
320 workbook,
321 worksheet
322 });
323 }
324 if (includeColumnGroupsHeaders) {
325 const columnGroupPaths = columns.reduce((acc, column) => {
326 acc[column.field] = apiRef.current.getColumnGroupPath(column.field);
327 return acc;
328 }, {});
329 addColumnGroupingHeaders(worksheet, serializedColumns, columnGroupPaths, apiRef.current.getAllGroupDetails());
330 }
331 if (includeHeaders) {
332 worksheet.addRow(columns.map(column => column.headerName ?? column.field));
333 }
334 const valueOptionsData = await getDataForValueOptionsSheet(columns, valueOptionsSheetName, apiRef.current);
335 createValueOptionsSheetIfNeeded(valueOptionsData, valueOptionsSheetName, workbook);
336 apiRef.current.resetColSpan();
337 rowIds.forEach(id => {
338 const serializedRow = serializeRowUnsafe(id, columns, apiRef, valueOptionsData, options);
339 addSerializedRowToWorksheet(serializedRow, worksheet);
340 });
341 apiRef.current.resetColSpan();
342 if (exceljsPostProcess) {
343 await exceljsPostProcess({
344 workbook,
345 worksheet
346 });
347 }
348 return workbook;
349}
350function setupExcelExportWebWorker(workerOptions = {}) {
351 // eslint-disable-next-line no-restricted-globals
352 addEventListener('message', async event => {
353 const {
354 serializedColumns,
355 serializedRows,
356 options,
357 valueOptionsSheetName,
358 valueOptionsData,
359 columnGroupDetails,
360 columnGroupPaths
361 } = event.data;
362 const {
363 exceljsPostProcess,
364 exceljsPreProcess
365 } = workerOptions;
366 const excelJS = await getExcelJs();
367 const workbook = new excelJS.Workbook();
368 const worksheet = workbook.addWorksheet('Sheet1');
369 worksheet.columns = serializedColumns;
370 if (exceljsPreProcess) {
371 await exceljsPreProcess({
372 workbook,
373 worksheet
374 });
375 }
376 if (options.includeColumnGroupsHeaders) {
377 addColumnGroupingHeaders(worksheet, serializedColumns, columnGroupPaths, columnGroupDetails);
378 }
379 const includeHeaders = options.includeHeaders ?? true;
380 if (includeHeaders) {
381 worksheet.addRow(serializedColumns.map(column => column.headerText));
382 }
383 createValueOptionsSheetIfNeeded(valueOptionsData, valueOptionsSheetName, workbook);
384 serializedRows.forEach(serializedRow => {
385 addSerializedRowToWorksheet(serializedRow, worksheet);
386 });
387 if (exceljsPostProcess) {
388 await exceljsPostProcess({
389 workbook,
390 worksheet
391 });
392 }
393 postMessage(await workbook.xlsx.writeBuffer());
394 });
395}
\No newline at end of file