Skip to content

Commit

Permalink
[DataGridPremium] Improve Excel export serialization performance (@ch…
Browse files Browse the repository at this point in the history
…erniavskii) (#16545)

Co-authored-by: Andrew Cherniavskii <[email protected]>
  • Loading branch information
github-actions[bot] and cherniavskii authored Feb 11, 2025
1 parent 3a6b72d commit 9d5bfbe
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,26 +36,32 @@ const getFormattedValueOptions = (
row: GridValidRowModel,
valueOptions: ValueOptions[],
api: GridApi,
callback: (value: any, index: number) => void,
) => {
if (!colDef.valueOptions) {
return [];
return;
}
let valueOptionsFormatted = valueOptions;
const valueFormatter = colDef.valueFormatter;

if (colDef.valueFormatter) {
valueOptionsFormatted = valueOptionsFormatted.map((option) => {
for (let i = 0; i < valueOptions.length; i += 1) {
const option = valueOptions[i];
let value: any;
if (valueFormatter) {
if (typeof option === 'object') {
return option;
value = option.label;
} else {
value = String(colDef.valueFormatter!(option as never, row, colDef, { current: api }));
}

return String(colDef.valueFormatter!(option as never, row, colDef, { current: api }));
});
} else {
value = typeof option === 'object' ? option.label : option;
}
callback(value, i);
}
return valueOptionsFormatted.map((option) =>
typeof option === 'object' ? option.label : option,
);
};

const commaRegex = /,/g;
const commaReplacement = 'CHAR(44)';

/**
* FIXME: This function mutates the colspan info, but colspan info assumes that the columns
* passed to it are always consistent. In this case, the exported columns may differ from the
Expand All @@ -69,12 +75,16 @@ export const serializeRowUnsafe = (
defaultValueOptionsFormulae: { [field: string]: { address: string } },
options: Pick<BuildExcelOptions, 'escapeFormulas'>,
): SerializedRow => {
const row: SerializedRow['row'] = {};
const serializedRow: SerializedRow['row'] = {};
const dataValidation: SerializedRow['dataValidation'] = {};
const mergedCells: SerializedRow['mergedCells'] = [];

const firstCellParams = apiRef.current.getCellParams(id, columns[0].field);
const outlineLevel = firstCellParams.rowNode.depth;
const row = apiRef.current.getRow(id);
const rowNode = apiRef.current.getRowNode(id);
if (!row || !rowNode) {
throw new Error(`No row with id #${id} found`);
}
const outlineLevel = rowNode.depth;
const hasColSpan = gridHasColSpanSelector(apiRef);

if (hasColSpan) {
Expand All @@ -101,35 +111,40 @@ export const serializeRowUnsafe = (
});
}

const cellParams = apiRef.current.getCellParams(id, column.field);

let cellValue: string | undefined;

switch (cellParams.colDef.type) {
switch (column.type) {
case 'singleSelect': {
const castColumn = cellParams.colDef as GridSingleSelectColDef;
const castColumn = column as GridSingleSelectColDef;
if (typeof castColumn.valueOptions === 'function') {
// If value option depends on the row, set specific options to the cell
// This dataValidation is buggy with LibreOffice and does not allow to have coma
const valueOptions = castColumn.valueOptions({
id,
row,
field: cellParams.field,
field: column.field,
});
const formattedValueOptions = getFormattedValueOptions(

let formulae: string = '"';
getFormattedValueOptions(
castColumn,
row,
valueOptions,
apiRef.current,
(value, index) => {
const formatted = value.toString().replace(commaRegex, commaReplacement);
formulae += formatted;
if (index < valueOptions.length - 1) {
formulae += ',';
}
},
);
formulae += '"';

dataValidation[castColumn.field] = {
type: 'list',
allowBlank: true,
formulae: [
`"${formattedValueOptions
.map((x) => x.toString().replaceAll(',', 'CHAR(44)'))
.join(',')}"`,
],
formulae: [formulae],
};
} else {
const address = defaultValueOptionsFormulae[column.field].address;
Expand All @@ -142,32 +157,32 @@ export const serializeRowUnsafe = (
};
}

const formattedValue = apiRef.current.getCellParams(id, castColumn.field).formattedValue;
const formattedValue = apiRef.current.getRowFormattedValue(row, castColumn);
if (process.env.NODE_ENV !== 'production') {
if (String(cellParams.formattedValue) === '[object Object]') {
if (String(formattedValue) === '[object Object]') {
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.',
]);
}
}
if (isObject<{ label: any }>(formattedValue)) {
row[castColumn.field] = formattedValue?.label;
serializedRow[castColumn.field] = formattedValue?.label;
} else {
row[castColumn.field] = formattedValue as any;
serializedRow[castColumn.field] = formattedValue as any;
}
break;
}
case 'boolean':
case 'number':
cellValue = apiRef.current.getCellParams(id, column.field).value as any;
cellValue = apiRef.current.getRowValue(row, column);
break;
case 'date':
case 'dateTime': {
// Excel does not do any timezone conversion, so we create a date using UTC instead of local timezone
// Solution from: https://github.com/exceljs/exceljs/issues/486#issuecomment-432557582
// About Date.UTC(): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC#exemples
const value = apiRef.current.getCellParams<any, Date>(id, column.field).value;
const value = apiRef.current.getRowValue(row, column) as Date;
// value may be `undefined` in auto-generated grouping rows
if (!value) {
break;
Expand All @@ -182,15 +197,15 @@ export const serializeRowUnsafe = (
value.getSeconds(),
),
);
row[column.field] = utcDate;
serializedRow[column.field] = utcDate;
break;
}
case 'actions':
break;
default:
cellValue = apiRef.current.getCellParams(id, column.field).formattedValue as any;
cellValue = apiRef.current.getRowFormattedValue(row, column);
if (process.env.NODE_ENV !== 'production') {
if (String(cellParams.formattedValue) === '[object Object]') {
if (String(cellValue) === '[object Object]') {
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.',
Expand All @@ -208,12 +223,12 @@ export const serializeRowUnsafe = (
}

if (typeof cellValue !== 'undefined') {
row[column.field] = cellValue;
serializedRow[column.field] = cellValue;
}
});

return {
row,
row: serializedRow,
dataValidation,
outlineLevel,
mergedCells,
Expand Down Expand Up @@ -251,38 +266,43 @@ export async function getDataForValueOptionsSheet(
valueOptionsSheetName: string,
api: GridPrivateApiPremium,
): Promise<ValueOptionsData> {
const candidateColumns = columns.filter(
(column) => isSingleSelectColDef(column) && Array.isArray(column.valueOptions),
);

// Creates a temp worksheet to obtain the column letters
const excelJS = await getExcelJs();
const workbook: Excel.Workbook = new excelJS.Workbook();
const worksheet = workbook.addWorksheet('Sheet1');

worksheet.columns = candidateColumns.map((column) => ({ key: column.field }));
const record: Record<string, { values: (string | number)[]; address: string }> = {};
const worksheetColumns: typeof worksheet.columns = [];

for (let i = 0; i < columns.length; i += 1) {
const column = columns[i];
const isCandidateColumn = isSingleSelectColDef(column) && Array.isArray(column.valueOptions);
if (!isCandidateColumn) {
continue;
}

return candidateColumns.reduce<Record<string, { values: (string | number)[]; address: string }>>(
(acc, column) => {
const singleSelectColumn = column as GridSingleSelectColDef;
const formattedValueOptions = getFormattedValueOptions(
singleSelectColumn,
{},
singleSelectColumn.valueOptions as Array<ValueOptions>,
api,
);
const header = column.headerName ?? column.field;
const values = [header, ...formattedValueOptions];
worksheetColumns.push({ key: column.field });
worksheet.columns = worksheetColumns;

const header = column.headerName ?? column.field;
const values: any[] = [header];
getFormattedValueOptions(
column,
{},
column.valueOptions as Array<ValueOptions>,
api,
(value) => {
values.push(value);
},
);

const letter = worksheet.getColumn(column.field).letter;
const address = `${valueOptionsSheetName}!$${letter}$2:$${letter}$${values.length}`;
const letter = worksheet.getColumn(column.field).letter;
const address = `${valueOptionsSheetName}!$${letter}$2:$${letter}$${values.length}`;

acc[column.field] = { values, address };
record[column.field] = { values, address };
}

return acc;
},
{},
);
return record;
}
interface BuildExcelOptions
extends Pick<GridExcelExportOptions, 'exceljsPreProcess' | 'exceljsPostProcess'>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
serializeRowUnsafe,
} from './serializer/excelSerializer';
import { GridExcelExportMenuItem } from '../../../components';
import type { SerializedRow } from './serializer/utils';

/**
* @requires useGridColumns (state)
Expand Down Expand Up @@ -143,11 +144,14 @@ export const useGridExcelExport = (
const serializedColumns = serializeColumns(exportedColumns, options.columnsStyles || {});

apiRef.current.resetColSpan();
const serializedRows = exportedRowIds.map((id) =>
serializeRowUnsafe(id, exportedColumns, apiRef, valueOptionsData, {
const serializedRows: SerializedRow[] = [];
for (let i = 0; i < exportedRowIds.length; i += 1) {
const id = exportedRowIds[i];
const serializedRow = serializeRowUnsafe(id, exportedColumns, apiRef, valueOptionsData, {
escapeFormulas: options.escapeFormulas ?? true,
}),
);
});
serializedRows.push(serializedRow);
}
apiRef.current.resetColSpan();

const columnGroupPaths = exportedColumns.reduce<Record<string, string[]>>((acc, column) => {
Expand Down

0 comments on commit 9d5bfbe

Please sign in to comment.