diff --git a/docs/data/data-grid/export/ExcelExportWithWebWorker.js b/docs/data/data-grid/export/ExcelExportWithWebWorker.js index a286931a6d791..933aa8da927d5 100644 --- a/docs/data/data-grid/export/ExcelExportWithWebWorker.js +++ b/docs/data/data-grid/export/ExcelExportWithWebWorker.js @@ -33,7 +33,7 @@ export default function ExcelExportWithWebWorker() { const { data, loading } = useDemoData({ dataSet: 'Commodity', - rowLength: 10000, + rowLength: 50_000, editable: true, }); diff --git a/docs/data/data-grid/export/ExcelExportWithWebWorker.tsx b/docs/data/data-grid/export/ExcelExportWithWebWorker.tsx index 6da8b45acdd9f..1b67f14889a62 100644 --- a/docs/data/data-grid/export/ExcelExportWithWebWorker.tsx +++ b/docs/data/data-grid/export/ExcelExportWithWebWorker.tsx @@ -33,7 +33,7 @@ export default function ExcelExportWithWebWorker() { const { data, loading } = useDemoData({ dataSet: 'Commodity', - rowLength: 10000, + rowLength: 50_000, editable: true, }); diff --git a/docs/data/data-grid/export/excelExportWorker.js b/docs/data/data-grid/export/excelExportWorker.js index fa46ebc24e1e0..90e419828b989 100644 --- a/docs/data/data-grid/export/excelExportWorker.js +++ b/docs/data/data-grid/export/excelExportWorker.js @@ -1,4 +1,4 @@ // Used in ExcelExportWithWebWorker demo -import { setupExcelExportWebWorker } from '@mui/x-data-grid-premium'; +import { setupExcelExportWebWorker } from '@mui/x-data-grid-premium/setupExcelExportWebWorker'; setupExcelExportWebWorker(); diff --git a/docs/data/data-grid/export/excelExportWorker.ts b/docs/data/data-grid/export/excelExportWorker.ts index fa46ebc24e1e0..90e419828b989 100644 --- a/docs/data/data-grid/export/excelExportWorker.ts +++ b/docs/data/data-grid/export/excelExportWorker.ts @@ -1,4 +1,4 @@ // Used in ExcelExportWithWebWorker demo -import { setupExcelExportWebWorker } from '@mui/x-data-grid-premium'; +import { setupExcelExportWebWorker } from '@mui/x-data-grid-premium/setupExcelExportWebWorker'; setupExcelExportWebWorker(); diff --git a/docs/data/data-grid/export/export.md b/docs/data/data-grid/export/export.md index b2b6b34453916..05744fb19448c 100644 --- a/docs/data/data-grid/export/export.md +++ b/docs/data/data-grid/export/export.md @@ -336,7 +336,7 @@ This file will be later used as the worker script, so it must be accessible by a ```tsx // in file ./worker.ts -import { setupExcelExportWebWorker } from '@mui/x-data-grid-premium'; +import { setupExcelExportWebWorker } from '@mui/x-data-grid-premium/setupExcelExportWebWorker'; setupExcelExportWebWorker(); ``` diff --git a/docs/worker.ts b/docs/worker.ts deleted file mode 100644 index 632fc4f39b3cf..0000000000000 --- a/docs/worker.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { setupExcelExportWebWorker } from '@mui/x-data-grid-premium'; - -setupExcelExportWebWorker(); diff --git a/packages/x-data-grid-premium/src/hooks/features/export/index.ts b/packages/x-data-grid-premium/src/hooks/features/export/index.ts index ae6724bdbdb0b..87c3b5c70a1f4 100644 --- a/packages/x-data-grid-premium/src/hooks/features/export/index.ts +++ b/packages/x-data-grid-premium/src/hooks/features/export/index.ts @@ -1,3 +1,3 @@ export * from './gridExcelExportInterface'; -export { setupExcelExportWebWorker } from './serializer/excelSerializer'; +export { setupExcelExportWebWorker } from './serializer/setupExcelExportWebWorker'; diff --git a/packages/x-data-grid-premium/src/hooks/features/export/serializer/excelSerializer.ts b/packages/x-data-grid-premium/src/hooks/features/export/serializer/excelSerializer.ts index 174e595231b79..6f1c661c9d87c 100644 --- a/packages/x-data-grid-premium/src/hooks/features/export/serializer/excelSerializer.ts +++ b/packages/x-data-grid-premium/src/hooks/features/export/serializer/excelSerializer.ts @@ -13,18 +13,23 @@ import { GridStateColDef, GridSingleSelectColDef, isObject, - GridColumnGroupLookup, isSingleSelectColDef, gridHasColSpanSelector, } from '@mui/x-data-grid/internals'; import { warnOnce } from '@mui/x-internals/warning'; import { ColumnsStylesInterface, GridExcelExportOptions } from '../gridExcelExportInterface'; import { GridPrivateApiPremium } from '../../../../models/gridApiPremium'; +import { + addColumnGroupingHeaders, + addSerializedRowToWorksheet, + createValueOptionsSheetIfNeeded, + getExcelJs, + SerializedColumns, + SerializedRow, + ValueOptionsData, +} from './utils'; -const getExcelJs = async () => { - const excelJsModule = await import('exceljs'); - return excelJsModule.default ?? excelJsModule; -}; +export type { ExcelExportInitEvent } from './utils'; const getFormattedValueOptions = ( colDef: GridSingleSelectColDef, @@ -51,13 +56,6 @@ const getFormattedValueOptions = ( ); }; -interface SerializedRow { - row: Record; - dataValidation: Record; - outlineLevel: number; - mergedCells: { leftIndex: number; rightIndex: number }[]; -} - /** * 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 @@ -241,68 +239,6 @@ export const serializeColumn = (column: GridColDef, columnsStyles: ColumnsStyles }; }; -const addColumnGroupingHeaders = ( - worksheet: Excel.Worksheet, - columns: SerializedColumns, - columnGroupPaths: Record, - columnGroupDetails: GridColumnGroupLookup, -) => { - const maxDepth = Math.max(...columns.map(({ key }) => columnGroupPaths[key]?.length ?? 0)); - if (maxDepth === 0) { - return; - } - - for (let rowIndex = 0; rowIndex < maxDepth; rowIndex += 1) { - const row = columns.map(({ key }) => { - const groupingPath = columnGroupPaths[key]; - if (groupingPath.length <= rowIndex) { - return { groupId: null, parents: groupingPath }; - } - return { - ...columnGroupDetails[groupingPath[rowIndex]], - parents: groupingPath.slice(0, rowIndex), - }; - }); - - const newRow = worksheet.addRow( - row.map((group) => (group.groupId === null ? null : (group?.headerName ?? group.groupId))), - ); - - // use `rowCount`, since worksheet can have additional rows added in `exceljsPreProcess` - const lastRowIndex = newRow.worksheet.rowCount; - let leftIndex = 0; - let rightIndex = 1; - while (rightIndex < columns.length) { - const { groupId: leftGroupId, parents: leftParents } = row[leftIndex]; - const { groupId: rightGroupId, parents: rightParents } = row[rightIndex]; - - const areInSameGroup = - leftGroupId === rightGroupId && - leftParents.length === rightParents.length && - leftParents.every((leftParent, index) => rightParents[index] === leftParent); - if (areInSameGroup) { - rightIndex += 1; - } else { - if (rightIndex - leftIndex > 1) { - worksheet.mergeCells(lastRowIndex, leftIndex + 1, lastRowIndex, rightIndex); - } - leftIndex = rightIndex; - rightIndex += 1; - } - } - if (rightIndex - leftIndex > 1) { - worksheet.mergeCells(lastRowIndex, leftIndex + 1, lastRowIndex, rightIndex); - } - } -}; - -type SerializedColumns = Array<{ - key: string; - width: number; - style: Partial; - headerText: string; -}>; - export function serializeColumns( columns: GridStateColDef[], styles: ColumnsStylesInterface, @@ -310,8 +246,6 @@ export function serializeColumns( return columns.map((column) => serializeColumn(column, styles)); } -type ValueOptionsData = Record; - export async function getDataForValueOptionsSheet( columns: GridStateColDef[], valueOptionsSheetName: string, @@ -350,47 +284,6 @@ export async function getDataForValueOptionsSheet( {}, ); } - -function addSerializedRowToWorksheet(serializedRow: SerializedRow, worksheet: Excel.Worksheet) { - const { row, dataValidation, outlineLevel, mergedCells } = serializedRow; - - const newRow = worksheet.addRow(row); - - Object.keys(dataValidation).forEach((field) => { - newRow.getCell(field).dataValidation = { - ...dataValidation[field], - }; - }); - - if (outlineLevel) { - newRow.outlineLevel = outlineLevel; - } - - // use `rowCount`, since worksheet can have additional rows added in `exceljsPreProcess` - const lastRowIndex = newRow.worksheet.rowCount; - mergedCells.forEach((mergedCell) => { - worksheet.mergeCells(lastRowIndex, mergedCell.leftIndex, lastRowIndex, mergedCell.rightIndex); - }); -} - -async function createValueOptionsSheetIfNeeded( - valueOptionsData: ValueOptionsData, - sheetName: string, - workbook: Excel.Workbook, -) { - if (Object.keys(valueOptionsData).length === 0) { - return; - } - - const valueOptionsWorksheet = workbook.addWorksheet(sheetName); - - valueOptionsWorksheet.columns = Object.keys(valueOptionsData).map((key) => ({ key })); - - Object.entries(valueOptionsData).forEach(([field, { values }]) => { - valueOptionsWorksheet.getColumn(field).values = values; - }); -} - interface BuildExcelOptions extends Pick, Pick< @@ -472,66 +365,3 @@ export async function buildExcel( return workbook; } - -export interface ExcelExportInitEvent { - serializedColumns: SerializedColumns; - serializedRows: SerializedRow[]; - valueOptionsSheetName: string; - columnGroupPaths: Record; - columnGroupDetails: GridColumnGroupLookup; - valueOptionsData: ValueOptionsData; - options: Omit< - GridExcelExportOptions, - 'exceljsPreProcess' | 'exceljsPostProcess' | 'columnsStyles' | 'valueOptionsSheetName' - >; -} - -export function setupExcelExportWebWorker( - workerOptions: Pick = {}, -) { - // eslint-disable-next-line no-restricted-globals - addEventListener('message', async (event: MessageEvent) => { - const { - serializedColumns, - serializedRows, - options, - valueOptionsSheetName, - valueOptionsData, - columnGroupDetails, - columnGroupPaths, - } = event.data; - - const { exceljsPostProcess, exceljsPreProcess } = workerOptions; - - const excelJS = await getExcelJs(); - const workbook: Excel.Workbook = new excelJS.Workbook(); - const worksheet = workbook.addWorksheet('Sheet1'); - - worksheet.columns = serializedColumns; - - if (exceljsPreProcess) { - await exceljsPreProcess({ workbook, worksheet }); - } - - if (options.includeColumnGroupsHeaders) { - addColumnGroupingHeaders(worksheet, serializedColumns, columnGroupPaths, columnGroupDetails); - } - - const includeHeaders = options.includeHeaders ?? true; - if (includeHeaders) { - worksheet.addRow(serializedColumns.map((column) => column.headerText)); - } - - createValueOptionsSheetIfNeeded(valueOptionsData, valueOptionsSheetName, workbook); - - serializedRows.forEach((serializedRow) => { - addSerializedRowToWorksheet(serializedRow, worksheet); - }); - - if (exceljsPostProcess) { - await exceljsPostProcess({ workbook, worksheet }); - } - - postMessage(await workbook.xlsx.writeBuffer()); - }); -} diff --git a/packages/x-data-grid-premium/src/hooks/features/export/serializer/setupExcelExportWebWorker.ts b/packages/x-data-grid-premium/src/hooks/features/export/serializer/setupExcelExportWebWorker.ts new file mode 100644 index 0000000000000..1a1440225c41e --- /dev/null +++ b/packages/x-data-grid-premium/src/hooks/features/export/serializer/setupExcelExportWebWorker.ts @@ -0,0 +1,59 @@ +import type * as Excel from 'exceljs'; +import type { GridExcelExportOptions } from '../gridExcelExportInterface'; +import { + addColumnGroupingHeaders, + addSerializedRowToWorksheet, + createValueOptionsSheetIfNeeded, + ExcelExportInitEvent, + getExcelJs, +} from './utils'; + +export function setupExcelExportWebWorker( + workerOptions: Pick = {}, +) { + // eslint-disable-next-line no-restricted-globals + addEventListener('message', async (event: MessageEvent) => { + const { + serializedColumns, + serializedRows, + options, + valueOptionsSheetName, + valueOptionsData, + columnGroupDetails, + columnGroupPaths, + } = event.data; + + const { exceljsPostProcess, exceljsPreProcess } = workerOptions; + + const excelJS = await getExcelJs(); + const workbook: Excel.Workbook = new excelJS.Workbook(); + const worksheet = workbook.addWorksheet('Sheet1'); + + worksheet.columns = serializedColumns; + + if (exceljsPreProcess) { + await exceljsPreProcess({ workbook, worksheet }); + } + + if (options.includeColumnGroupsHeaders) { + addColumnGroupingHeaders(worksheet, serializedColumns, columnGroupPaths, columnGroupDetails); + } + + const includeHeaders = options.includeHeaders ?? true; + if (includeHeaders) { + worksheet.addRow(serializedColumns.map((column) => column.headerText)); + } + + createValueOptionsSheetIfNeeded(valueOptionsData, valueOptionsSheetName, workbook); + + serializedRows.forEach((serializedRow) => { + addSerializedRowToWorksheet(serializedRow, worksheet); + }); + + if (exceljsPostProcess) { + await exceljsPostProcess({ workbook, worksheet }); + } + + postMessage(await workbook.xlsx.writeBuffer()); + }); +} diff --git a/packages/x-data-grid-premium/src/hooks/features/export/serializer/utils.ts b/packages/x-data-grid-premium/src/hooks/features/export/serializer/utils.ts new file mode 100644 index 0000000000000..c40fb9ab44041 --- /dev/null +++ b/packages/x-data-grid-premium/src/hooks/features/export/serializer/utils.ts @@ -0,0 +1,135 @@ +import type * as Excel from 'exceljs'; +import type { GridColumnGroupLookup } from '@mui/x-data-grid/internals'; +import type { GridExcelExportOptions } from '../gridExcelExportInterface'; + +export const getExcelJs = async () => { + const excelJsModule = await import('exceljs'); + return excelJsModule.default ?? excelJsModule; +}; + +export interface SerializedRow { + row: Record; + dataValidation: Record; + outlineLevel: number; + mergedCells: { leftIndex: number; rightIndex: number }[]; +} + +export const addColumnGroupingHeaders = ( + worksheet: Excel.Worksheet, + columns: SerializedColumns, + columnGroupPaths: Record, + columnGroupDetails: GridColumnGroupLookup, +) => { + const maxDepth = Math.max(...columns.map(({ key }) => columnGroupPaths[key]?.length ?? 0)); + if (maxDepth === 0) { + return; + } + + for (let rowIndex = 0; rowIndex < maxDepth; rowIndex += 1) { + const row = columns.map(({ key }) => { + const groupingPath = columnGroupPaths[key]; + if (groupingPath.length <= rowIndex) { + return { groupId: null, parents: groupingPath }; + } + return { + ...columnGroupDetails[groupingPath[rowIndex]], + parents: groupingPath.slice(0, rowIndex), + }; + }); + + const newRow = worksheet.addRow( + row.map((group) => (group.groupId === null ? null : (group?.headerName ?? group.groupId))), + ); + + // use `rowCount`, since worksheet can have additional rows added in `exceljsPreProcess` + const lastRowIndex = newRow.worksheet.rowCount; + let leftIndex = 0; + let rightIndex = 1; + while (rightIndex < columns.length) { + const { groupId: leftGroupId, parents: leftParents } = row[leftIndex]; + const { groupId: rightGroupId, parents: rightParents } = row[rightIndex]; + + const areInSameGroup = + leftGroupId === rightGroupId && + leftParents.length === rightParents.length && + leftParents.every((leftParent, index) => rightParents[index] === leftParent); + if (areInSameGroup) { + rightIndex += 1; + } else { + if (rightIndex - leftIndex > 1) { + worksheet.mergeCells(lastRowIndex, leftIndex + 1, lastRowIndex, rightIndex); + } + leftIndex = rightIndex; + rightIndex += 1; + } + } + if (rightIndex - leftIndex > 1) { + worksheet.mergeCells(lastRowIndex, leftIndex + 1, lastRowIndex, rightIndex); + } + } +}; + +export type SerializedColumns = Array<{ + key: string; + width: number; + style: Partial; + headerText: string; +}>; + +export type ValueOptionsData = Record; + +export function addSerializedRowToWorksheet( + serializedRow: SerializedRow, + worksheet: Excel.Worksheet, +) { + const { row, dataValidation, outlineLevel, mergedCells } = serializedRow; + + const newRow = worksheet.addRow(row); + + Object.keys(dataValidation).forEach((field) => { + newRow.getCell(field).dataValidation = { + ...dataValidation[field], + }; + }); + + if (outlineLevel) { + newRow.outlineLevel = outlineLevel; + } + + // use `rowCount`, since worksheet can have additional rows added in `exceljsPreProcess` + const lastRowIndex = newRow.worksheet.rowCount; + mergedCells.forEach((mergedCell) => { + worksheet.mergeCells(lastRowIndex, mergedCell.leftIndex, lastRowIndex, mergedCell.rightIndex); + }); +} + +export async function createValueOptionsSheetIfNeeded( + valueOptionsData: ValueOptionsData, + sheetName: string, + workbook: Excel.Workbook, +) { + if (Object.keys(valueOptionsData).length === 0) { + return; + } + + const valueOptionsWorksheet = workbook.addWorksheet(sheetName); + + valueOptionsWorksheet.columns = Object.keys(valueOptionsData).map((key) => ({ key })); + + Object.entries(valueOptionsData).forEach(([field, { values }]) => { + valueOptionsWorksheet.getColumn(field).values = values; + }); +} + +export interface ExcelExportInitEvent { + serializedColumns: SerializedColumns; + serializedRows: SerializedRow[]; + valueOptionsSheetName: string; + columnGroupPaths: Record; + columnGroupDetails: GridColumnGroupLookup; + valueOptionsData: ValueOptionsData; + options: Omit< + GridExcelExportOptions, + 'exceljsPreProcess' | 'exceljsPostProcess' | 'columnsStyles' | 'valueOptionsSheetName' + >; +} diff --git a/packages/x-data-grid-premium/src/setupExcelExportWebWorker.ts b/packages/x-data-grid-premium/src/setupExcelExportWebWorker.ts new file mode 100644 index 0000000000000..c0d3d9c12bfdd --- /dev/null +++ b/packages/x-data-grid-premium/src/setupExcelExportWebWorker.ts @@ -0,0 +1 @@ +export { setupExcelExportWebWorker } from './hooks/features/export/serializer/setupExcelExportWebWorker';