diff --git a/configs/rollup.config.js b/configs/rollup.config.js index a489f481..5d04207e 100644 --- a/configs/rollup.config.js +++ b/configs/rollup.config.js @@ -28,7 +28,8 @@ const TSComponentsList = [ 'Button', 'Image', 'Rating', - 'InteractiveWidget' + 'InteractiveWidget', + 'Export' ]; const getInputs = (name, dir) => { diff --git a/package.json b/package.json index 2c745489..0863e2f3 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "@storybook/react-webpack5": "^7.6.15", "@storybook/theming": "^7.6.15", "@types/enzyme-adapter-react-16": "^1.0.9", + "@types/file-saver": "^2.0.7", "@types/jest": "^29.5.11", "autoprefixer": "^10.4.13", "babel-jest": "^29.7.0", @@ -141,6 +142,7 @@ "semantic-release": "^19.0.5", "storybook": "^7.6.15", "storybook-dark-mode": "^3.0.3", + "string-to-html": "^1.3.4", "style-loader": "^3.3.1", "stylelint": "^14.14.0", "stylelint-config-prettier": "^9.0.3", @@ -160,14 +162,18 @@ "dayjs": "^1.11.5", "draft-js": "^0.11.7", "draftjs-to-html": "^0.9.1", + "exceljs": "^4.4.0", "file-saver": "^2.0.5", "highcharts": "^10.2.1", "highcharts-react-official": "^3.1.0", "html-to-draftjs": "^1.5.0", + "html-to-image": "^1.11.11", "i": "^0.3.7", "immer": "^9.0.18", "jodit-react": "^1.3.23", - "npm": "^9.4.1", + "jspdf": "^2.5.1", + "jspdf-autotable": "^3.8.2", + "npm": "^9.9.3", "pure-react-carousel": "^1.30.1", "qrcode.react": "^3.1.0", "rc-slider": "^8.6.9", diff --git a/src/index.ts b/src/index.ts index fa0afdcb..09fda904 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ // Atoms export { default as TextLink } from './lib/atoms/TextLink'; export { default as Button } from './lib/atoms/Button'; +export { default as Export } from './lib/atoms/Export'; export { default as Label } from './lib/atoms/Label'; export { default as Icon } from './lib/atoms/Icon'; export { default as Switcher } from './lib/atoms/Switcher'; diff --git a/src/lib/atoms/Export/Export.stories.tsx b/src/lib/atoms/Export/Export.stories.tsx new file mode 100644 index 00000000..903267ce --- /dev/null +++ b/src/lib/atoms/Export/Export.stories.tsx @@ -0,0 +1,278 @@ +import React, { useRef, useState } from 'react'; +import toHtml from 'string-to-html'; +import { args } from '../../../../stories/assets/storybook.globals'; + +// Helpers +import * as ExportHelper from './exportHelper'; + +// Components +import Button from '../Button/Button'; +import InteractiveWidget from '../../molecules/InteractiveWidget/InteractiveWidget'; +import RichEditor from '../../organisms/RichEditor'; +import ColumnChart from '../../molecules/Charts/ColumnChart'; + +const meta = { + title: 'Atoms/Export', + argTypes: {}, + args: {} +}; + +export default meta; + +export const ExelFormats = () => { + const [currentFunction, setCurrentFunction] = useState<'xls' | 'csv' | 'xlsx'>('xlsx'); + + const exelHeader: ExportHelper.ITableHeader[] = [ + { + header: 'Test2', + key: 'test2', + style: { + fontSize: 10, + color: '#e91e63' + } + }, + { + header: 'Test5', + key: 'test5', + style: { + fontSize: 20, + color: '#e91e63', + bold: true + } + }, + { + header: 'Zzzz', + key: 'test1', + style: { + fontSize: 20, + color: '#e91e63', + bold: true + } + } + ]; + + const createExel: ExportHelper.DataType[] = [ + { + test1: { + value: 'Zzzzz', + style: { + color: '#bd8faa', + fontSize: 10 + } + }, + test5: 'loyality', + + test2: { + value: 'TestWithStyle', + style: { + color: '#e91e63', + fontSize: 10 + } + }, + test3: 'test' + }, + { + test1: { + value: 'Zzzzz2', + style: { + fontSize: 10 + } + }, + test2: 'test', + test4: 'test4', + test3: { + value: 'Styled value', + style: { + fontSize: 10 + } + } + }, + { + test5: 'loyality', + test2: 'loyality', + test3: { + value: 'ffdggdgd', + style: { + fontSize: 10, + bold: true + } + } + } + ]; + + const exportHandler = () => { + ExportHelper[currentFunction](createExel, exelHeader); + }; + + return ( +
+
+ Data:
 {JSON.stringify(createExel, null, 2)} 
+ Header:
 {JSON.stringify(exelHeader, null, 2)} 
+
{' '} + + {' '} +
+ ); +}; + +export const ExportTableAsPdf = () => { + const exelHeader = [ + { + header: 'Test2', + key: 'test2' + }, + { + header: 'Test5', + key: 'test5' + } + ]; + + const createExel: Record[] = [ + { + test3: 'test' + }, + { + test2: 'test', + test4: 'test4' + }, + { + test5: 'loyality', + test2: 'loyality' + } + ]; + + const exportHandler = () => { + ExportHelper.exportToTablePdf(createExel); + }; + + return ( +
+
+ Data:
 {JSON.stringify(createExel, null, 2)} 
+ Header:
 {JSON.stringify(exelHeader, null, 2)} 
+
+ {' '} +
+ ); +}; + +export const ExportAsImage = () => { + const ref = useRef(null); + const [imageType, setImageType] = useState('toJpeg'); + + const exportHandler = () => { + if (ref.current) { + ExportHelper.exportImage(ref.current, imageType); + } + }; + + return ( + <> +
+
+ + +
+
+ +
+
+ + +
+ {/**@ts-ignore */} + +
+ + + + + ); +}; + +export const ExportAsPdf = () => { + const ref = useRef(null); + + const exportHandler = () => { + if (ref.current) { + ExportHelper.pdf(ref.current); + } + }; + const getData = (e) => { + const div = document.createElement('div'); + div.style.width = '400px'; + div.style.margin = '5px'; + div.appendChild(toHtml(e)); + ref.current = div; + }; + + return ( +
+ {/**@ts-ignore */} + + {/**@ts-ignore */} + +
+ ); +}; diff --git a/src/lib/atoms/Export/exportHelper.ts b/src/lib/atoms/Export/exportHelper.ts new file mode 100644 index 00000000..1009a3d7 --- /dev/null +++ b/src/lib/atoms/Export/exportHelper.ts @@ -0,0 +1,238 @@ +import React from 'react'; +import jsPDF from 'jspdf'; +import * as ImageExporter from 'html-to-image'; +import ExcelJS from 'exceljs'; +import { saveAs } from 'file-saver'; +import autoTable, { ColumnInput } from 'jspdf-autotable'; +//TODO: change file location after tests + +const createUniqueArr = (data: object[]) => { + return data.reduce((aggr: string[], val) => { + aggr = [...aggr, ...Object.keys(val)]; + return [...new Set(aggr)]; + }, []); +}; + +export const exportToTablePdf = ( + data: Record[], + columns?: Record[], + fileName: string = 'document' +) => { + try { + const doc = new jsPDF({ + format: 'letter' + }); + + const pdfColumns = columns + ? columns.map((column) => ({ header: column.header, dataKey: column.key })) + : createUniqueArr(data).map((el) => ({ header: el, dataKey: el })); + + autoTable(doc, { + columns: pdfColumns as ColumnInput[], + body: data + }); + + doc.save(`${fileName}.pdf`); + } catch (error) { + return error; + } +}; + +export const pdf = (HTMLelement: HTMLElement, fileName: string = 'document') => { + try { + const doc = new jsPDF({ + format: 'a4', + unit: 'px' + }); + + doc.html(HTMLelement, { + callback(doc) { + doc.save(fileName); + }, + x: 10, + y: 10 + }); + } catch (error) { + return error; + } +}; + +export type ImageFormats = 'toPng' | 'toJpeg'; +export const exportImage = async ( + HTMLelement: HTMLElement, + format: ImageFormats = 'toPng', + fileName: string = 'document' +) => { + try { + const request = await ImageExporter[format](HTMLelement, { cacheBust: true, height: 1000 }); + const link = document.createElement('a'); + link.download = fileName; + link.href = request; + link.click(); + } catch (error) { + return error; + } +}; + +interface IDataWithStyle { + style: { + fontSize?: number; + color?: `#${string}`; + bold?: boolean; + italic?: boolean; + underline?: boolean; + background?: `#${string}`; + }; + value: string | number; +} +export interface ITableHeader extends Partial> { + style?: IDataWithStyle['style']; + header: string; + key: string; +} +export type DataType = Record; + +type AllIndexType = Set>; + +const transformData = { + font: {}, + fontSize(size: number) { + this.font.size = size; + + return { font: this.font }; + }, + color(color: `#${string}`) { + if (!this.font.color) { + this.font.color = {}; + } + this.font.color = { argb: color.replace('#', '') }; + return { font: this.font }; + }, + bold(isBold: boolean) { + this.font.bold = isBold; + return { font: this.font }; + }, + italic(isItalic: boolean) { + this.font.italic = isItalic; + return { font: this.font }; + }, + underline(isUnderline: boolean) { + this.font.underline = isUnderline; + return { font: this.font }; + }, + background: (color: `#${string}`) => ({ + fill: { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: color.replace('#', '') } + } + }) +}; + +const transformedData = (transformedStyles: Record>) => { + Object.keys(transformedStyles).forEach((el) => { + let styles = transformedStyles[el].style; + transformData.font = {}; + Object.keys(styles).forEach((key) => { + if (typeof transformData[key] === 'function') { + transformedStyles[el].style = { + ...transformedStyles[el].style, + ...transformData[key](styles[key]) + }; + } + delete transformedStyles[el].style[key]; + }); + }); +}; + +const tableFormats = async ( + data: DataType[], + header?: ITableHeader[], + fileName: string = 'document', + type: 'xlsx' | 'csv' = 'xlsx' +) => { + try { + let transformedStyles = {}; + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet(); + const allRows: Record = {}; + const dataFromHeader: string[] = []; + const allIndex: AllIndexType = new Set(); + const transformAllIndex = (allIndex: AllIndexType) => { + allIndex.forEach((el) => { + if (typeof el.element === 'object') { + allRows[el.rowIndex as number].getCell(el.colIndex as number).style = + transformedStyles[el.element.value].style; + } + }); + }; + const fillTransformedStyles = (key: string | number, style: IDataWithStyle['style']) => { + transformedStyles = { + ...transformedStyles, + [key]: { + ...transformedStyles[key], + style + } + }; + }; + if (!header) { + const createHeder = createUniqueArr(data).map((val) => { + dataFromHeader.push(val); + return { header: val, key: val }; + }); + worksheet.columns = createHeder; + } else { + const headerWithoutStyles = header.map((el) => { + dataFromHeader.push(el.key!); + if (el.style && el.key) { + fillTransformedStyles(el.key, el.style); + } + return el; + }); + transformedData(transformedStyles); + worksheet.columns = headerWithoutStyles as Partial[]; + worksheet.getRow(1).eachCell((cell, colNumber) => { + const getCol = worksheet.getColumn(colNumber); + if (getCol.key && transformedStyles.hasOwnProperty(getCol.key)) { + cell.style = { ...cell.style, ...transformedStyles[getCol.key].style }; + } + }); + } + + data.forEach((items, rowIndex) => { + let rows = {}; + dataFromHeader.forEach((element, colIndex) => { + const isObject = typeof items[element] === 'object'; + rows[element] = isObject ? (items[element] as IDataWithStyle).value : items[element]; + allIndex.add({ element: items[element], colIndex: colIndex + 1, rowIndex: rowIndex }); + + if (isObject) { + const currentElement = items[element] as IDataWithStyle; + fillTransformedStyles(currentElement.value, currentElement.style); + } + }); + const row = worksheet.addRow(rows); + allRows[rowIndex] = row; + }); + transformedData(transformedStyles); + transformAllIndex(allIndex); + let buffer: ExcelJS.Buffer; + if (type === 'csv') { + buffer = await workbook.csv.writeBuffer(); + } else { + buffer = await workbook.xlsx.writeBuffer(); + } + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }); + saveAs(blob, `${fileName}.${type}`); + } catch (error) { + return error; + } +}; + +export const xlsx = (data: DataType[], header?: ITableHeader[], documentName?: string) => + tableFormats(data, header, documentName); + +export const csv = (data: DataType[], header?: ITableHeader[], documentName?: string) => + tableFormats(data, header, documentName, 'csv'); diff --git a/src/lib/atoms/Export/index.tsx b/src/lib/atoms/Export/index.tsx new file mode 100644 index 00000000..1836aff7 --- /dev/null +++ b/src/lib/atoms/Export/index.tsx @@ -0,0 +1 @@ +export * as default from './exportHelper';