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] Implement CSV export #1030

Merged
merged 33 commits into from
Feb 19, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f16ca59
Finish setup
DanailH Feb 11, 2021
548a417
First stable draft version
DanailH Feb 11, 2021
3752bb0
Resolve conflicts
DanailH Feb 11, 2021
1760a62
Remove unneded prop from new story
DanailH Feb 11, 2021
57da33b
Make getLocalization helper accept partial translations
DanailH Feb 12, 2021
d5ccdbc
wip
DanailH Feb 12, 2021
7ae1326
Fix typings
DanailH Feb 12, 2021
0656387
Rework exportAs util
DanailH Feb 12, 2021
258a427
Get latest changes
DanailH Feb 12, 2021
37c11db
Fix formatting
DanailH Feb 12, 2021
835ee07
Remove commented out code
DanailH Feb 12, 2021
55caa1b
Update return type
DanailH Feb 12, 2021
5f5c10a
Add docs
DanailH Feb 12, 2021
b538322
Add test
DanailH Feb 12, 2021
ff07421
Fix accessibility on density selector and export selector
DanailH Feb 12, 2021
edc32b5
Update docs
DanailH Feb 12, 2021
213a173
Update packages/grid/_modules_/grid/models/api/localeTextApi.ts
DanailH Feb 12, 2021
1e10f5d
Update packages/grid/_modules_/grid/constants/localeTextConstants.ts
DanailH Feb 12, 2021
a905d9e
Update packages/grid/_modules_/grid/components/toolbar/ExportSelector…
DanailH Feb 12, 2021
b4783f1
Fix PR comments
DanailH Feb 12, 2021
fa8fef7
Update docs
DanailH Feb 12, 2021
6e096e7
Fix focus issue for GridMenu
DanailH Feb 15, 2021
26dfc1b
remove ids and labledby for now
DanailH Feb 15, 2021
4635975
Fix PR comments
DanailH Feb 15, 2021
e7f0bfa
Fix formatting
DanailH Feb 15, 2021
40e7991
Remove new grid prop
DanailH Feb 16, 2021
5054556
Trigger Build
DanailH Feb 16, 2021
f9df9b8
give a try without the div, it works, it will also help catch wrong u…
oliviertassinari Feb 16, 2021
89b0b72
fix my typo, use prev to give more semantic to what the ref is about
oliviertassinari Feb 16, 2021
4f8c602
abstract logic and rename api
DanailH Feb 18, 2021
b5b26ba
Merge branch 'feature/DataGrid-197-csv-export' of github.com:DanailH/…
DanailH Feb 18, 2021
f27af51
Resolve conflicts
DanailH Feb 19, 2021
30488c1
Fix tests
DanailH Feb 19, 2021
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
2 changes: 2 additions & 0 deletions packages/grid/_modules_/grid/GridComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { RootContainerRef } from './models/rootContainerRef';
import { ApiContext } from './components/api-context';
import { useFilter } from './hooks/features/filter/useFilter';
import { useLocaleText } from './hooks/features/localeText/useLocaleText';
import { useCsvExport } from './hooks/features/export';

export const GridComponent = React.forwardRef<HTMLDivElement, GridComponentProps>(
function GridComponent(props, ref) {
Expand Down Expand Up @@ -85,6 +86,7 @@ export const GridComponent = React.forwardRef<HTMLDivElement, GridComponentProps
useVirtualRows(columnsHeaderRef, windowRef, renderingZoneRef, apiRef);
useColumnReorder(apiRef);
useColumnResize(columnsHeaderRef, apiRef);
useCsvExport(apiRef);
usePagination(apiRef);

const components = useComponents(props.components, props.componentsProps, apiRef);
Expand Down
5 changes: 5 additions & 0 deletions packages/grid/_modules_/grid/components/icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,8 @@ export const DragIcon = createSvgIcon(
<path d="M11 18c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />,
'Drag',
);

export const DownloadIcon = createSvgIcon(
<path d="M5,20h14v-2H5V20z M19,9h-4V3H9v6H5l7,7L19,9z" />,
'Download',
);
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function DensitySelector() {
};

// Disable the button if the corresponding is disabled
if (options.disableColumnFilter) {
if (options.disableDensitySelector) {
return null;
}

Expand Down
77 changes: 77 additions & 0 deletions packages/grid/_modules_/grid/components/toolbar/ExportSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as React from 'react';
import MenuList from '@material-ui/core/MenuList';
import Button from '@material-ui/core/Button';
import MenuItem from '@material-ui/core/MenuItem';
import { ApiContext } from '../api-context';
import { useGridSelector } from '../../hooks/features/core/useGridSelector';
import { optionsSelector } from '../../hooks/utils/optionsSelector';
import { GridMenu } from '../menu/GridMenu';
import { ExportOption } from '../../models';

export function ExportSelector() {
const apiRef = React.useContext(ApiContext);
const options = useGridSelector(apiRef, optionsSelector);
const [anchorEl, setAnchorEl] = React.useState(null);

const ExportIcon = apiRef!.current.components!.ExportIcon!;

const ExportOptions: Array<ExportOption> = [
{
label: 'CSV',
format: 'csv',
},
];

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

setAnchorEl(null);
};

const handleListKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Tab' || event.key === 'Escape') {
event.preventDefault();
handleExportSelectorClose();
}
};

// Disable the button if the corresponding is disabled
if (options.disableCsvExport) {
return null;
}

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

return (
<React.Fragment>
<Button
color="primary"
size="small"
startIcon={<ExportIcon />}
onClick={handleExportSelectorOpen}
aria-label=""
aria-haspopup="true"
>
{'EXPORT'}
</Button>
<GridMenu
open={Boolean(anchorEl)}
target={anchorEl}
onClickAway={handleExportSelectorClose}
position="bottom-start"
>
<MenuList id="menu-list-grow" onKeyDown={handleListKeyDown}>
{renderExportOptions}
</MenuList>
</GridMenu>
</React.Fragment>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { GridToolbarContainer } from '../containers/GridToolbarContainer';
import { ColumnsToolbarButton } from './ColumnsToolbarButton';
import { DensitySelector } from './DensitySelector';
import { FilterToolbarButton } from './FilterToolbarButton';
import { ExportSelector } from './ExportSelector';

export function GridToolbar() {
const apiRef = useContext(ApiContext);
Expand All @@ -15,7 +16,8 @@ export function GridToolbar() {
if (
options.disableColumnFilter &&
options.disableColumnSelector &&
options.disableDensitySelector
options.disableDensitySelector &&
options.disableCsvExport
) {
return null;
}
Expand All @@ -25,6 +27,7 @@ export function GridToolbar() {
<ColumnsToolbarButton />
<FilterToolbarButton />
<DensitySelector />
<ExportSelector />
</GridToolbarContainer>
);
}
1 change: 1 addition & 0 deletions packages/grid/_modules_/grid/components/toolbar/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './ColumnsToolbarButton';
export * from './DensitySelector';
export * from './ExportSelector';
export * from './FilterToolbarButton';
export * from './GridToolbar';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useCsvExport';
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as React from 'react';
import { ApiRef } from '../../../models/api/apiRef';
import { useApiMethod } from '../../root/useApiMethod';
import { useGridSelector } from '../core/useGridSelector';
import { visibleColumnsSelector } from '../columns';
import { visibleSortedRowsSelector } from '../filter';
import { selectionStateSelector } from '../selection';
import { Columns, CsvExportApi, RowId, RowModel } from '../../../models';
import { useLogger } from '../../utils/useLogger';
import { exportAs } from '../../../utils';

export const useCsvExport = (apiRef: ApiRef): void => {
const logger = useLogger('useCsvExport');
const visibleColumns = useGridSelector(apiRef, visibleColumnsSelector);
const visibleSortedRows = useGridSelector(apiRef, visibleSortedRowsSelector);
const selection = useGridSelector(apiRef, selectionStateSelector);

const buildRow = (row: RowModel, columns: Columns) => {
const mappedRow: RowModel[] = [];
columns.forEach((column) => column.field !== '__check__' && mappedRow.push(row[column.field]));
return mappedRow;
};

const buildCSV = React.useCallback(
(columns: Columns, rows: RowModel[], selectedRows: Record<RowId, boolean>) => {
const selectedRowsIds = Object.keys(selectedRows);

if (selectedRowsIds.length) {
rows = rows.filter((row) => selectedRowsIds.includes(`${row.id}`));
}

const CSVHead = `${columns
.filter((column) => column.field !== '__check__')
.map((column) => column.headerName)
.toString()}\r\n`;
const CSVBody = rows
.reduce((soFar, row) => `${soFar}${buildRow(row, columns)}\r\n`, '')
.trim();
const csv = `${CSVHead}${CSVBody}`.trim();

return csv;
},
[],
);

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

exportAs(blob, 'csv', 'data');
}, [logger, visibleColumns, visibleSortedRows, selection, buildCSV]);

const getDataAsCsv = React.useCallback(() => {
logger.debug(`Get data as CSV`);
const csv = buildCSV(visibleColumns, visibleSortedRows, selection);

return csv;
}, [logger, visibleColumns, visibleSortedRows, selection, buildCSV]);

const csvExportApi: CsvExportApi = {
exportDataAsCsv,
getDataAsCsv,
};

useApiMethod(apiRef, csvExportApi, 'CsvExportApi');
};
3 changes: 3 additions & 0 deletions packages/grid/_modules_/grid/hooks/features/useComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const useComponents = (
DensityStandardIcon:
(componentSlotsProp && componentSlotsProp.DensityStandardIcon) ||
DEFAULT_SLOTS_COMPONENTS.DensityStandardIcon,
ExportIcon:
(componentSlotsProp && componentSlotsProp.ExportIcon) ||
DEFAULT_SLOTS_COMPONENTS.ExportIcon,
OpenFilterButtonIcon:
(componentSlotsProp && componentSlotsProp.OpenFilterButtonIcon) ||
DEFAULT_SLOTS_COMPONENTS.OpenFilterButtonIcon,
Expand Down
15 changes: 15 additions & 0 deletions packages/grid/_modules_/grid/models/api/csvExportApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* The csv export API interface that is available in the grid [[apiRef]].
*/
export interface CsvExportApi {
/**
* Export the grid data as CSV.
* @returns void
*/
exportDataAsCsv: () => void;
/**
* Get the grid data as CSV.
* @returns
*/
getDataAsCsv: () => any;
}
2 changes: 2 additions & 0 deletions packages/grid/_modules_/grid/models/api/gridApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import { CoreApi } from './coreApi';
import { EventsApi } from './eventsApi';
import { DensityApi } from './densityApi';
import { LocaleTextApi } from './localeTextApi';
import { CsvExportApi } from './csvExportApi';

/**
* The full grid API.
*/
export type GridApi = CoreApi &
ComponentsApi &
CsvExportApi &
StateApi &
DensityApi &
EventsApi &
Expand Down
1 change: 1 addition & 0 deletions packages/grid/_modules_/grid/models/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './apiRef';
export * from './coreApi';
export * from './columnApi';
export * from './componentsApi';
export * from './csvExportApi';
export * from './densityApi';
export * from './eventsApi';
export * from './gridApi';
Expand Down
12 changes: 12 additions & 0 deletions packages/grid/_modules_/grid/models/export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Available export formats. To be extended in future.
*/
export type ExportFormat = 'csv';

/**
* Export option interface
*/
export interface ExportOption {
label: string;
format: ExportFormat;
}
4 changes: 4 additions & 0 deletions packages/grid/_modules_/grid/models/gridIconSlotsComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,8 @@ export interface GridIconSlotsComponent {
* Icon displayed on the comfortable density option in the toolbar.
*/
DensityComfortableIcon?: React.ElementType;
/**
* Icon displayed on the open export button present in the toolbar by default
*/
ExportIcon?: React.ElementType;
}
5 changes: 5 additions & 0 deletions packages/grid/_modules_/grid/models/gridOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ export interface GridOptions {
* @default false
*/
disableDensitySelector?: boolean;
/**
* If `true`, csv export option is disabled.
* @default false
*/
disableCsvExport?: boolean;
/**
* If `true`, reordering columns is disabled.
* @default false
Expand Down
2 changes: 2 additions & 0 deletions packages/grid/_modules_/grid/models/gridSlotsComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
TripleDotsVerticalIcon,
ViewHeadlineIcon,
ViewStreamIcon,
DownloadIcon,
} from '../components/icons/index';
import { LoadingOverlay } from '../components/LoadingOverlay';
import { GridColumnMenu, GridColumnMenuProps } from '../components/menu/columnMenu/GridColumnMenu';
Expand Down Expand Up @@ -95,6 +96,7 @@ export const DEFAULT_SLOTS_ICONS: GridIconSlotsComponent = {
DensityCompactIcon: ViewHeadlineIcon,
DensityStandardIcon: TableRowsIcon,
DensityComfortableIcon: ViewStreamIcon,
ExportIcon: DownloadIcon,
};

export const DEFAULT_SLOTS_COMPONENTS: ApiRefComponentsProperty = {
Expand Down
1 change: 1 addition & 0 deletions packages/grid/_modules_/grid/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from './gridIconSlotsComponent';
export * from './gridSlotsComponent';
export * from './gridSlotsComponentsProps';
export * from './density';
export * from './export';
23 changes: 23 additions & 0 deletions packages/grid/_modules_/grid/utils/exportAs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const exportAs = (blob: Blob, extension: string, filename: string): void => {
/* taken from react-csv */
if (navigator && navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(blob, filename);
} else {
let dataURI = '';
// TODO: Handle more export cases in future
if (extension === 'csv') {
dataURI = `data:text/csv;charset=utf-8,${'csv'}`;
}

const URL = window.URL || window.webkitURL;
const downloadURI =
typeof URL.createObjectURL === 'undefined' ? dataURI : URL.createObjectURL(blob);

const link = document.createElement('a');
link.setAttribute('href', downloadURI);
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
1 change: 1 addition & 0 deletions packages/grid/_modules_/grid/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './mergeUtils';
export * from './paramsUtils';
export * from './getLocalization';
export * from './material-ui-utils';
export * from './exportAs';
16 changes: 16 additions & 0 deletions packages/storybook/src/stories/grid-toolbar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,19 @@ export const DensitySelectorComfortable = () => {
</div>
);
};
export const CsvExport = () => {
const data = useData(100, 50);

return (
<div style={{ height: 600 }}>
<XGrid
columns={data.columns}
rows={data.rows}
checkboxSelection
components={{
Toolbar: GridToolbar,
}}
/>
</div>
);
};