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

POC: Export to CSV, XLSX, PDF #2355

Merged
merged 9 commits into from
Apr 13, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@
"eslint-plugin-sonarjs": "^0.6.0",
"faker": "^5.4.0",
"jest": "^26.6.3",
"jspdf": "^2.3.1",
"jspdf-autotable": "^3.5.14",
"lodash": "^4.17.20",
"mini-css-extract-plugin": "^1.3.7",
"postcss": "^8.2.6",
Expand All @@ -98,7 +100,8 @@
"react-sortable-hoc": "^1.11.0",
"rollup": "^2.39.0",
"rollup-plugin-postcss": "^4.0.0",
"typescript": "~4.2.2"
"typescript": "~4.2.2",
"xlsx": "^0.16.9"
},
"peerDependencies": {
"react": "^16.14 || ^17.0",
Expand Down
8 changes: 6 additions & 2 deletions src/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export interface DataGridProps<R, SR = unknown> extends SharedDivProps {
/** Toggles whether filters row is displayed or not */
enableFilterRow?: boolean;
cellNavigationMode?: CellNavigationMode;
enableVirtualization?: boolean;

/**
* Miscellaneous
Expand Down Expand Up @@ -208,6 +209,7 @@ function DataGrid<R, SR>({
// Toggles and modes
enableFilterRow = false,
cellNavigationMode = 'NONE',
enableVirtualization = true,
Copy link
Contributor Author

@amanmahajan7 amanmahajan7 Apr 12, 2021

Choose a reason for hiding this comment

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

Not sure about the prop name. Any suggessions? disableVirtualization makes more sense but I kept this to be consistent with enableFilterRow . We can also consider forwarding this prop to the formatters to users can use it during printing.

Copy link
Contributor

Choose a reason for hiding this comment

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

Prop name is good to me. I prefer avoiding "negative" prop/variable names as they make reading logic harder.

We can also consider forwarding this prop to the formatters to users can use it during printing.

What use case do you have in mind?

Copy link
Contributor Author

@amanmahajan7 amanmahajan7 Apr 12, 2021

Choose a reason for hiding this comment

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

If for example there is a cell that shows a complex data like an icon with a tooltip. It may not work out of the box and it would be helpful to provide an alternate formatter for printing. We can revisit if needed

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds like it should be a separate thing, and it could be implemented by the user via a provider.

// Miscellaneous
editorPortalTarget = body,
className,
Expand Down Expand Up @@ -262,7 +264,8 @@ function DataGrid<R, SR>({
scrollLeft,
viewportWidth: gridWidth,
defaultColumnOptions,
rawGroupBy: rowGrouper ? rawGroupBy : undefined
rawGroupBy: rowGrouper ? rawGroupBy : undefined,
enableVirtualization
});

const { rowOverscanStartIdx, rowOverscanEndIdx, rows, rowsCount, isGroupRow } = useViewportRows({
Expand All @@ -272,7 +275,8 @@ function DataGrid<R, SR>({
rowHeight,
clientHeight,
scrollTop,
expandedGroupIds
expandedGroupIds,
enableVirtualization
});

const hasGroups = groupBy.length > 0 && typeof rowGrouper === 'function';
Expand Down
9 changes: 7 additions & 2 deletions src/hooks/useViewportColumns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface ViewportColumnsArgs<R, SR> extends Pick<DataGridProps<R, SR>, 'default
viewportWidth: number;
scrollLeft: number;
columnWidths: ReadonlyMap<string, number>;
enableVirtualization: boolean;
}

export function useViewportColumns<R, SR>({
Expand All @@ -19,7 +20,8 @@ export function useViewportColumns<R, SR>({
viewportWidth,
scrollLeft,
defaultColumnOptions,
rawGroupBy
rawGroupBy,
enableVirtualization
}: ViewportColumnsArgs<R, SR>) {
const minColumnWidth = defaultColumnOptions?.minWidth ?? 80;
const defaultFormatter = defaultColumnOptions?.formatter ?? ValueFormatter;
Expand Down Expand Up @@ -159,6 +161,9 @@ export function useViewportColumns<R, SR>({
}, [columnWidths, columns, viewportWidth, minColumnWidth, lastFrozenColumnIndex]);

const [colOverscanStartIdx, colOverscanEndIdx] = useMemo((): [number, number] => {
if (!enableVirtualization) {
return [0, columns.length - 1];
}
// get the viewport's left side and right side positions for non-frozen columns
const viewportLeft = scrollLeft + totalFrozenColumnWidth;
const viewportRight = scrollLeft + viewportWidth;
Expand Down Expand Up @@ -199,7 +204,7 @@ export function useViewportColumns<R, SR>({
const colOverscanEndIdx = Math.min(lastColIdx, colVisibleEndIdx + 1);

return [colOverscanStartIdx, colOverscanEndIdx];
}, [columns, columnMetrics, lastFrozenColumnIndex, scrollLeft, totalFrozenColumnWidth, viewportWidth]);
}, [columnMetrics, columns, lastFrozenColumnIndex, scrollLeft, totalFrozenColumnWidth, viewportWidth, enableVirtualization]);

const viewportColumns = useMemo((): readonly CalculatedColumn<R, SR>[] => {
const viewportColumns: CalculatedColumn<R, SR>[] = [];
Expand Down
14 changes: 13 additions & 1 deletion src/hooks/useViewportRows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface ViewportRowsArgs<R> {
groupBy: readonly string[];
rowGrouper?: (rows: readonly R[], columnKey: string) => Record<string, readonly R[]>;
expandedGroupIds?: ReadonlySet<unknown>;
enableVirtualization: boolean;
}

// https://github.com/microsoft/TypeScript/issues/41808
Expand All @@ -25,7 +26,8 @@ export function useViewportRows<R>({
scrollTop,
groupBy,
rowGrouper,
expandedGroupIds
expandedGroupIds,
enableVirtualization
}: ViewportRowsArgs<R>) {
const [groupedRows, rowsCount] = useMemo(() => {
if (groupBy.length === 0 || !rowGrouper) return [undefined, rawRows.length];
Expand Down Expand Up @@ -90,6 +92,16 @@ export function useViewportRows<R>({

const isGroupRow = <R>(row: unknown): row is GroupRow<R> => allGroupRows.has(row);

if (!enableVirtualization) {
return {
rowOverscanStartIdx: 0,
rowOverscanEndIdx: rows.length - 1,
rows,
rowsCount,
isGroupRow
};
}

const overscanThreshold = 4;
const rowVisibleStartIdx = Math.floor(scrollTop / rowHeight);
const rowVisibleEndIdx = Math.min(rows.length - 1, Math.floor((scrollTop + clientHeight) / rowHeight));
Expand Down
37 changes: 36 additions & 1 deletion stories/demos/CommonFeatures.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { useState, useCallback, useMemo } from 'react';
import { css } from '@linaria/core';
import faker from 'faker';

import DataGrid, { SelectColumn, TextEditor, SelectCellFormatter } from '../../src';
import type { Column, SortDirection } from '../../src';
import { stopPropagation } from '../../src/utils';
import { SelectEditor } from './components/Editors/SelectEditor';
import { exportToCsv, exportToXlsx, exportToPdf } from './exportUtils';

const toolbarClassname = css`
text-align: right;
margin-bottom: 8px;
`;


const dateFormatter = new Intl.DateTimeFormat(navigator.language);
const currencyFormatter = new Intl.NumberFormat(navigator.language, {
Expand Down Expand Up @@ -266,7 +275,7 @@ export function CommonFeatures() {
setSort([columnKey, direction]);
}, []);

return (
const gridElement = (
<DataGrid
rowKeyGetter={rowKeyGetter}
columns={columns}
Expand All @@ -285,6 +294,32 @@ export function CommonFeatures() {
className="fill-grid"
/>
);

return (
<>
<div className={toolbarClassname}>
<button onClick={() => {
exportToCsv(gridElement, 'CommonFeatures.csv');
}}
>
Export to CSV
</button>
<button onClick={() => {
exportToXlsx(gridElement, 'CommonFeatures.xlsx');
}}
>
Export to XSLX
</button>
<button onClick={() => {
exportToPdf(gridElement, 'CommonFeatures.pdf');
}}
>
Export to PDF
</button>
</div>
{gridElement}
</>
);
}

CommonFeatures.storyName = 'Common Features';
84 changes: 84 additions & 0 deletions stories/demos/exportUtils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { cloneElement } from 'react';
import type { ReactElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import XLSX from 'xlsx';
import { jsPDF } from 'jspdf';
import autoTable from 'jspdf-autotable';

import type { DataGridProps } from '../../src';

export function exportToCsv<R, SR>(gridElement: ReactElement<DataGridProps<R, SR>>, fileName: string) {
const { head, body, foot } = getGridContent(gridElement);
const content = [...head, ...body, ...foot]
.map(cells => cells.map(serialiseCellValue).join(','))
.join('\n');

downloadFile(fileName, new Blob([content], { type: 'text/csv;charset=utf-8;' }));
}

export function exportToXlsx<R, SR>(gridElement: ReactElement<DataGridProps<R, SR>>, fileName: string) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

https://github.com/SheetJS/sheetjs
We can use the commercial version

const { head, body, foot } = getGridContent(gridElement);
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet([...head, ...body, ...foot]);
XLSX.utils.book_append_sheet(wb, ws, 'Sheet 1');
XLSX.writeFile(wb, fileName);
}


export function exportToPdf<R, SR>(gridElement: ReactElement<DataGridProps<R, SR>>, fileName: string) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

https://github.com/simonbengtsson/jsPDF-AutoTable
Handy plugin to convert table to pdf

const doc = new jsPDF({
orientation: 'l',
unit: 'px'
});

const { head, body, foot } = getGridContent(gridElement);
autoTable(doc, {
head,
body,
foot,
horizontalPageBreak: true,
styles: { cellPadding: 1.5, fontSize: 8, cellWidth: 'wrap' },
tableWidth: 'wrap'
});
doc.save(fileName);
}

function getGridContent<R, SR>(gridElement: ReactElement<DataGridProps<R, SR>>) {
const grid = document.createElement('div');
grid.innerHTML = renderToStaticMarkup(cloneElement(gridElement, {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we're rendering images, it might end up downloading a lot of images at once.
We could avoid this by rendering the grid in an anonymous document: https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation/createDocument

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good to know. We can add it if needed or I will create a followup PR

enableVirtualization: false
}));

return {
head: getRows('.rdg-header-row'),
body: getRows('.rdg-row:not(.rdg-summary-row)'),
foot: getRows('.rdg-summary-row')
};

function getRows(selector: string) {
return Array.from(
grid.querySelectorAll<HTMLDivElement>(selector)
).map(gridRow => {
return Array.from(
gridRow.querySelectorAll<HTMLDivElement>('.rdg-cell')
).map(gridCell => gridCell.innerText);
Copy link
Contributor

Choose a reason for hiding this comment

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

So the cell ends up empty when we display an image, a checkbox, or a progress element for example?

Copy link
Contributor Author

@amanmahajan7 amanmahajan7 Apr 12, 2021

Choose a reason for hiding this comment

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

Correct. We can manually update cell value during export or provide a flag to the formatter so it can return a print friendly value

});
}
}

function serialiseCellValue(value: unknown) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we handle \n as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can. It works for now so may be next PR if needed

if (typeof value === 'string') {
const formattedValue = value.replace(/"/g, '""');
return formattedValue.includes(',') ? `"${formattedValue}"` : formattedValue;
}
return value;
}

function downloadFile(fileName: string, data: Blob) {
const downloadLink = document.createElement('a');
downloadLink.download = fileName;
const url = URL.createObjectURL(data);
downloadLink.href = url;
downloadLink.click();
URL.revokeObjectURL(url);
}