-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[DataGridPremium] Add excel export #3802
Changes from 11 commits
0f57f82
6495245
105f32e
e57ca68
9866b05
f37de8b
93543c9
9609e5c
938954f
8ff3275
8478344
9af8806
1279bb5
caaa7f9
321a40e
f81f54c
861de16
aac9f99
f01e3cd
40e1fd7
f12d3ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import * as React from 'react'; | ||
import { | ||
DataGridPro, | ||
GridToolbarContainer, | ||
GridToolbarExport, | ||
} from '@mui/x-data-grid-pro'; | ||
import { useDemoData } from '@mui/x-data-grid-generator'; | ||
|
||
function CustomToolbar() { | ||
return ( | ||
<GridToolbarContainer> | ||
<GridToolbarExport /> | ||
</GridToolbarContainer> | ||
); | ||
} | ||
|
||
export default function ExcelExport() { | ||
const { data, loading } = useDemoData({ | ||
dataSet: 'Commodity', | ||
rowLength: 4, | ||
maxColumns: 6, | ||
}); | ||
|
||
return ( | ||
<div style={{ height: 300, width: '100%' }}> | ||
<DataGridPro | ||
{...data} | ||
loading={loading} | ||
components={{ | ||
Toolbar: CustomToolbar, | ||
}} | ||
experimentalFeatures={{ excelExport: true }} | ||
/> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import * as React from 'react'; | ||
import { | ||
DataGridPro, | ||
GridToolbarContainer, | ||
GridToolbarExport, | ||
} from '@mui/x-data-grid-pro'; | ||
import { useDemoData } from '@mui/x-data-grid-generator'; | ||
|
||
function CustomToolbar() { | ||
return ( | ||
<GridToolbarContainer> | ||
<GridToolbarExport /> | ||
</GridToolbarContainer> | ||
); | ||
} | ||
|
||
export default function ExcelExport() { | ||
const { data, loading } = useDemoData({ | ||
dataSet: 'Commodity', | ||
rowLength: 4, | ||
maxColumns: 6, | ||
}); | ||
|
||
return ( | ||
<div style={{ height: 300, width: '100%' }}> | ||
<DataGridPro | ||
{...data} | ||
loading={loading} | ||
components={{ | ||
Toolbar: CustomToolbar, | ||
}} | ||
experimentalFeatures={{ excelExport: true }} | ||
/> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<DataGridPro | ||
{...data} | ||
loading={loading} | ||
components={{ | ||
Toolbar: CustomToolbar, | ||
}} | ||
experimentalFeatures={{ excelExport: true }} | ||
/> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import type * as Excel from 'exceljs'; | ||
import { | ||
GridCellParams, | ||
GRID_CHECKBOX_SELECTION_COL_DEF, | ||
GridStateColDef, | ||
GridRowId, | ||
GridColDef, | ||
GridValueOptionsParams, | ||
GridValueFormatterParams, | ||
GridApi, | ||
} from '../../../../models'; | ||
|
||
const getExcelJs = () => import('exceljs'); | ||
|
||
const getFormattedValueOptions = ( | ||
colDef: GridColDef, | ||
{ id, row, field }: GridValueOptionsParams, | ||
api: GridApi, | ||
) => { | ||
// TODO: clean depending on the solution chosen for https://github.com/mui-org/material-ui-x/issues/3806 | ||
if (!colDef.valueOptions) { | ||
return []; | ||
} | ||
let valueOptionsFormatted; | ||
if (typeof colDef.valueOptions === 'function') { | ||
valueOptionsFormatted = colDef.valueOptions({ id, row, field }); | ||
} else { | ||
valueOptionsFormatted = colDef.valueOptions; | ||
} | ||
|
||
if (colDef.valueFormatter) { | ||
valueOptionsFormatted = valueOptionsFormatted.map((option) => { | ||
if (typeof option === 'object') { | ||
return option; | ||
} | ||
|
||
const params: GridValueFormatterParams = { field, api, value: option }; | ||
return String(colDef.valueFormatter(params)); | ||
}); | ||
} | ||
return valueOptionsFormatted.map((option) => | ||
typeof option === 'object' ? option.label : option, | ||
); | ||
}; | ||
|
||
const serialiseRow = ( | ||
id: GridRowId, | ||
columns: GridStateColDef[], | ||
getCellParams: (id: GridRowId, field: string) => GridCellParams, | ||
api: GridApi, | ||
) => { | ||
const row = {}; | ||
const dataValidation = {}; | ||
|
||
const firstCellParams = getCellParams(id, columns[0].field); | ||
const outlineLevel = firstCellParams.rowNode.depth; | ||
|
||
columns.forEach((column) => { | ||
const cellParams = getCellParams(id, column.field); | ||
switch (cellParams.colDef.type) { | ||
case 'singleSelect': { | ||
const valueOptions: GridValueOptionsParams = { | ||
id: cellParams.id, | ||
row: cellParams.row, | ||
field: cellParams.field, | ||
}; | ||
const formattedValueOptions = getFormattedValueOptions( | ||
cellParams.colDef, | ||
valueOptions, | ||
api, | ||
); | ||
dataValidation[column.field] = { | ||
type: 'list', | ||
allowBlank: true, | ||
formulae: [formattedValueOptions.map((x) => `"${x}"`)], | ||
alexfauquette marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
const formattedValue = getCellParams(id, column.field).formattedValue; | ||
row[column.field] = | ||
typeof formattedValue === 'object' ? formattedValue.label : formattedValue; | ||
break; | ||
} | ||
case 'boolean': | ||
case 'number': | ||
case 'date': | ||
case 'dateTime': | ||
row[column.field] = getCellParams(id, column.field).value; | ||
alexfauquette marked this conversation as resolved.
Show resolved
Hide resolved
|
||
break; | ||
case 'actions': | ||
break; | ||
default: | ||
row[column.field] = getCellParams(id, column.field).formattedValue; | ||
break; | ||
} | ||
}); | ||
return { | ||
row, | ||
dataValidation, | ||
outlineLevel, | ||
}; | ||
}; | ||
|
||
const serialiseColumn = (column: GridColDef, includeHeaders: boolean) => { | ||
const { field, headerName } = column; | ||
|
||
return { | ||
...(includeHeaders ? { header: headerName || field } : {}), | ||
key: field, | ||
// TODO (clean that hack) | ||
// the width seems to be the number of small character visible in a cell | ||
// could be nice to move from px width to excel width | ||
width: column.width ? Math.floor(column.width / 5) : 20, | ||
}; | ||
}; | ||
|
||
interface BuildExcelOptions { | ||
columns: GridStateColDef[]; | ||
rowIds: GridRowId[]; | ||
getCellParams: (id: GridRowId, field: string) => GridCellParams; | ||
includeHeaders: boolean; | ||
} | ||
|
||
export async function buildExcel(options: BuildExcelOptions, api): Promise<Excel.Workbook> { | ||
const { columns, rowIds, getCellParams, includeHeaders } = options; | ||
|
||
const columnsWithoutCheckbox = columns.filter( | ||
(column) => column.field !== GRID_CHECKBOX_SELECTION_COL_DEF.field, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can remove the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes it makes sense. I also moved this filter from csvSerializer to useCSVExport to have a |
||
); | ||
|
||
const excelJS = await getExcelJs(); | ||
const workbook: Excel.Workbook = new excelJS.Workbook(); | ||
const worksheet = workbook.addWorksheet('Sheet1'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we allow the name of the sheet to be passed as an input? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about it, but as long as we use only one sheet per workbook, I don't see the interest |
||
|
||
worksheet.columns = columnsWithoutCheckbox.map((column) => | ||
serialiseColumn(column, includeHeaders), | ||
); | ||
|
||
rowIds.forEach((id) => { | ||
const { row, dataValidation, outlineLevel } = serialiseRow( | ||
id, | ||
columnsWithoutCheckbox, | ||
getCellParams, | ||
api, | ||
); | ||
const newRow = worksheet.addRow(row); | ||
|
||
Object.keys(dataValidation).forEach((field) => { | ||
newRow.getCell(field).dataValidation = { | ||
...dataValidation[field], | ||
}; | ||
}); | ||
|
||
if (outlineLevel) { | ||
newRow.outlineLevel = outlineLevel; | ||
} | ||
}); | ||
return workbook; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense for direct dependencies to be locked with
~
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have an opinion on this subject. I had a look at core repo, all the dependencies are with
^