Skip to content

Commit

Permalink
POC: Export to CSV, XLSX, PDF (adazzle#2355)
Browse files Browse the repository at this point in the history
* Initial commit

* Fix dependencies

* Add utility function to export

* serialiseCellValue

* Export to pdf and xlsx

* Update src/hooks/useViewportColumns.ts

Co-authored-by: Nicolas Stepien <[email protected]>

* Update src/hooks/useViewportRows.ts

Co-authored-by: Nicolas Stepien <[email protected]>

* Update stories/demos/exportUtils.tsx

Co-authored-by: Nicolas Stepien <[email protected]>

* Update stories/demos/exportUtils.tsx

Co-authored-by: Nicolas Stepien <[email protected]>

Co-authored-by: Nicolas Stepien <[email protected]>
  • Loading branch information
2 people authored and gernotkogler committed May 13, 2021
1 parent 4f8f521 commit 5bfe24b
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 7 deletions.
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,
// 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) {
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) {
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, {
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);
});
}
}

function serialiseCellValue(value: unknown) {
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);
}

0 comments on commit 5bfe24b

Please sign in to comment.