Skip to content
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

[DataGrid] Allow to customize GridToolbarExport's CSV export #1695

Merged
merged 5 commits into from
Jun 7, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,44 @@ import MenuItem from '@material-ui/core/MenuItem';
import { isHideMenuKey, isTabKey } from '../../utils/keyboardUtils';
import { GridApiContext } from '../GridApiContext';
import { GridMenu } from '../menu/GridMenu';
import { GridExportOption } from '../../models';
import { GridExportCsvOptions } from '../../models/gridExport';

export const GridToolbarExport = React.forwardRef<HTMLButtonElement, ButtonProps>(
interface GridExportFormatCsv {
format: 'csv';
formatOptions?: GridExportCsvOptions;
}

type GridExportFormatOption = GridExportFormatCsv;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need these 2 types, why not only 1?
Not sure everybody would agree but I would export the type used in the props, so users can easily type the component props.

Copy link
Member

@oliviertassinari oliviertassinari Jun 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not only 1?

I assume that because as soon as we have excel, it needs:

Suggested change
type GridExportFormatOption = GridExportFormatCsv;
type GridExportFormatOption = GridExportFormatCsv | GridExportFormatExcel;

Not sure everybody would agree but I would export the type used in the props, so users can easily type the component props.

Why would a developer ever care about them? Isn't this type completely internal to the component, with no public API implications? I mean, it seems completely dependent on how GridToolbarExport is written.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you export the button to recompose your toolbar and you allow to pass props in the button, you might want to type those props and what's inside them

Copy link
Member

@oliviertassinari oliviertassinari Jun 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you export the button to recompose your toolbar and you allow to pass props in the button, you might want to type those props and what's inside them

Then I don't understand your previous comment. The GridToolbarExportProps type is already public for this use case.

You can use https://codesandbox.io/s/material-demo-forked-o7cee?file=/demo.tsx to provide an example if you were thinking of something else, it uses the last commit of this PR.


type GridExportOption = GridExportFormatOption & {
label: React.ReactNode;
};

export interface GridToolbarExportProps extends ButtonProps {
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved
csvOptions?: GridExportCsvOptions;
}

export const GridToolbarExport = React.forwardRef<HTMLButtonElement, GridToolbarExportProps>(
function GridToolbarExport(props, ref) {
const { csvOptions, ...other } = props;
const apiRef = React.useContext(GridApiContext);
const exportButtonId = useId();
const exportMenuId = useId();
const buttonId = useId();
const menuId = useId();
const [anchorEl, setAnchorEl] = React.useState(null);
const ExportIcon = apiRef!.current.components!.ExportIcon!;

const ExportOptions: Array<GridExportOption> = [
{
label: apiRef!.current.getLocaleText('toolbarExportCSV'),
format: 'csv',
},
];
const exportOptions: Array<GridExportOption> = [];
exportOptions.push({
label: apiRef!.current.getLocaleText('toolbarExportCSV'),
format: 'csv',
formatOptions: csvOptions,
});

const handleExportSelectorOpen = (event) => setAnchorEl(event.currentTarget);
const handleExportSelectorClose = () => setAnchorEl(null);
const handleExport = (format) => {
if (format === 'csv') {
apiRef!.current.exportDataAsCsv();
const handleMenuOpen = (event) => setAnchorEl(event.currentTarget);
const handleMenuClose = () => setAnchorEl(null);
const handleExport = (option: GridExportOption) => () => {
if (option.format === 'csv') {
apiRef!.current.exportDataAsCsv(option.formatOptions);
}

setAnchorEl(null);
Expand All @@ -39,46 +55,44 @@ export const GridToolbarExport = React.forwardRef<HTMLButtonElement, ButtonProps
event.preventDefault();
}
if (isHideMenuKey(event.key)) {
handleExportSelectorClose();
handleMenuClose();
}
};

const renderExportOptions: Array<React.ReactElement> = ExportOptions.map((option, index) => (
<MenuItem key={index} onClick={() => handleExport(option.format)}>
{option.label}
</MenuItem>
));

return (
<React.Fragment>
<Button
ref={ref}
color="primary"
size="small"
startIcon={<ExportIcon />}
onClick={handleExportSelectorOpen}
onClick={handleMenuOpen}
aria-expanded={anchorEl ? 'true' : undefined}
aria-haspopup="menu"
aria-labelledby={exportMenuId}
id={exportButtonId}
{...props}
aria-labelledby={menuId}
id={buttonId}
{...other}
>
{apiRef!.current.getLocaleText('toolbarExport')}
</Button>
<GridMenu
open={Boolean(anchorEl)}
target={anchorEl}
onClickAway={handleExportSelectorClose}
onClickAway={handleMenuClose}
position="bottom-start"
>
<MenuList
id={exportMenuId}
id={menuId}
className="MuiDataGrid-gridMenuList"
aria-labelledby={exportButtonId}
aria-labelledby={buttonId}
onKeyDown={handleListKeyDown}
autoFocusItem={Boolean(anchorEl)}
>
{renderExportOptions}
{exportOptions.map((option, index) => (
<MenuItem key={index} onClick={handleExport(option)}>
{option.label}
</MenuItem>
))}
</MenuList>
</GridMenu>
</React.Fragment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { useGridSelector } from '../core/useGridSelector';
import { visibleGridColumnsSelector } from '../columns';
import { visibleSortedGridRowsSelector } from '../filter';
import { gridSelectionStateSelector } from '../selection';
import { GridCsvExportApi } from '../../../models';
import { GridCsvExportApi } from '../../../models/api/gridCsvExportApi';
import { GridExportCsvOptions } from '../../../models/gridExport';
import { useLogger } from '../../utils/useLogger';
import { exportAs } from '../../../utils';
import { buildCSV } from './seralizers/csvSeraliser';
Expand All @@ -16,19 +17,30 @@ export const useGridCsvExport = (apiRef: GridApiRef): void => {
const visibleSortedRows = useGridSelector(apiRef, visibleSortedGridRowsSelector);
const selection = useGridSelector(apiRef, gridSelectionStateSelector);

const getDataAsCsv = React.useCallback((): string => {
logger.debug(`Get data as CSV`);
const getDataAsCsv = React.useCallback(
// TODO remove once we use the options
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(options?: GridExportCsvOptions): string => {
logger.debug(`Get data as CSV`);

return buildCSV(visibleColumns, visibleSortedRows, selection, apiRef.current.getCellValue);
}, [logger, visibleColumns, visibleSortedRows, selection, apiRef]);
return buildCSV(visibleColumns, visibleSortedRows, selection, apiRef.current.getCellValue);
},
[logger, visibleColumns, visibleSortedRows, selection, apiRef],
);

const exportDataAsCsv = React.useCallback((): void => {
logger.debug(`Export data as CSV`);
const csv = getDataAsCsv();
const blob = new Blob([csv], { type: 'text/csv' });
const exportDataAsCsv = React.useCallback(
(options?: GridExportCsvOptions): void => {
logger.debug(`Export data as CSV`);
const csv = getDataAsCsv(options);

exportAs(blob, 'csv', 'data');
}, [logger, getDataAsCsv]);
const blob = new Blob([options?.utf8WithBom ? new Uint8Array([0xef, 0xbb, 0xbf]) : '', csv], {
dtassone marked this conversation as resolved.
Show resolved Hide resolved
type: 'text/csv',
});

exportAs(blob, 'csv', options?.fileName);
},
[logger, getDataAsCsv],
);

const csvExportApi: GridCsvExportApi = {
getDataAsCsv,
Expand Down
17 changes: 11 additions & 6 deletions packages/grid/_modules_/grid/models/api/gridCsvExportApi.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { GridExportCsvOptions } from '../gridExport';

/**
* The csv export API interface that is available in the grid [[apiRef]].
* The CSV export API interface that is available in the grid [[apiRef]].
*/
export interface GridCsvExportApi {
/**
* Returns the grid data formatted as CSV.
* @returns {string} The data as CSV.
* Returns the grid data as a CSV string.
* This method is used internally by `exportDataAsCsv`.
* @param {GridExportCsvOptions} options The options to apply on the export.
* @returns string
*/
getDataAsCsv: () => string;
getDataAsCsv: (options?: GridExportCsvOptions) => string;
/**
* Exports the grid data as CSV and sends it to the user.
* Downloads and exports a CSV of the grid's data.
* @param {GridExportCsvOptions} options The options to apply on the export.
*/
exportDataAsCsv: () => void;
exportDataAsCsv: (options?: GridExportCsvOptions) => void;
}
14 changes: 7 additions & 7 deletions packages/grid/_modules_/grid/models/gridExport.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/**
* Available export formats. To be extended in future.
* The options to apply on the CSV export.
*/
export type GridExportFormat = 'csv';
export interface GridExportCsvOptions {
fileName?: string;
utf8WithBom?: boolean;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need the withBom suffix? I don't think it brings any value

Copy link
Member

@oliviertassinari oliviertassinari Jun 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dtassone How will a developer differentiate if he's going to include a BOM or not?

Copy link
Member

@dtassone dtassone Jun 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what they want is UTF8, the BOM part is just for the reader...

Copy link
Member

@oliviertassinari oliviertassinari Jun 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the default format? UTF8? Are developers using any other format?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure actually, I thought it was ASCII and we were missing the special characters.

Copy link
Member

@oliviertassinari oliviertassinari Jun 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michallukowski How about we start without, only an option to customize the filename?

Suggested change
utf8WithBom?: boolean;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason for this pull request was to add an option for developers to set a flag if they wanted to add a BOM for a utf-8 encoded file as mentioned in #1440. The filename setting came up by the way. What's the problem with the utf8WithBom option? If you really don't want this option, we can remove it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think we can default to utf8 with Bom, and either remove or reverse the prop so only users that don't want BOM for a special reason would actually set it.

Copy link
Contributor Author

@michallukowski michallukowski Jun 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's a good idea to add a BOM by default. According to the Unicode Standard:

Use of a BOM is neither required nor recommended for UTF-8, but may be encountered in contexts where UTF-8 data is converted from other encoding forms that use a BOM or where the BOM is used as a UTF-8 signature

https://en.wikipedia.org/wiki/Byte_order_mark

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More context: ag-grid/ag-grid#3916

}

/**
* Export option interface
* Available export formats.
*/
export interface GridExportOption {
label: React.ReactNode;
format: GridExportFormat;
}
export type GridExportFormat = 'csv';