diff --git a/packages/react-bootstrap-table2-example/examples/csv/index.js b/packages/react-bootstrap-table2-example/examples/csv/index.js new file mode 100644 index 0000000..e8069c1 --- /dev/null +++ b/packages/react-bootstrap-table2-example/examples/csv/index.js @@ -0,0 +1,86 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; + +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitContext, { CSVExport } from 'react-bootstrap-table2-toolkit'; +import Code from 'components/common/code-block'; +import { productsGenerator } from 'utils/common'; + +const products = productsGenerator(); + +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + +const sourceCode = `\ +import BootstrapTable from 'react-bootstrap-table-next'; +import ToolkitContext, { Search } from 'react-bootstrap-table2-toolkit'; + +const { SearchBar, searchFactory } = Search; +const columns = [{ + dataField: 'id', + text: 'Product ID' +}, { + dataField: 'name', + text: 'Product Name' +}, { + dataField: 'price', + text: 'Product Price' +}]; + + + + { + props => ( +
+

Input something at below input field:

+ +
+ +
+ ) + } +
+
+`; + +export default () => ( +
+ + + { + props => ( +
+

Input something at below input field:

+ +
+ +
+ ) + } +
+
+ { sourceCode } +
+); diff --git a/packages/react-bootstrap-table2-example/stories/index.js b/packages/react-bootstrap-table2-example/stories/index.js index d556b1f..04d589e 100644 --- a/packages/react-bootstrap-table2-example/stories/index.js +++ b/packages/react-bootstrap-table2-example/stories/index.js @@ -133,6 +133,9 @@ import FullyCustomSearch from 'examples/search/fully-custom-search'; import SearchFormattedData from 'examples/search/search-formatted'; import CustomSearchValue from 'examples/search/custom-search-value'; +// CSV +import ExportCSV from 'examples/csv'; + // loading overlay import EmptyTableOverlay from 'examples/loading-overlay/empty-table-overlay'; import TableOverlay from 'examples/loading-overlay/table-overlay'; @@ -289,6 +292,9 @@ storiesOf('Table Search', module) .add('Search Fromatted Value', () => ) .add('Custom Search Value', () => ); +storiesOf('Export CSV', module) + .add('Basic Export CSV', () => ); + storiesOf('EmptyTableOverlay', module) .add('Empty Table Overlay', () => ) .add('Table Overlay', () => ); diff --git a/packages/react-bootstrap-table2-toolkit/context.js b/packages/react-bootstrap-table2-toolkit/context.js index a561203..e55be86 100644 --- a/packages/react-bootstrap-table2-toolkit/context.js +++ b/packages/react-bootstrap-table2-toolkit/context.js @@ -1,12 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; +import statelessDrcorator from './statelessOp'; import createContext from './src/search/context'; const ToolkitContext = React.createContext(); -class ToolkitProvider extends React.Component { +class ToolkitProvider extends statelessDrcorator(React.Component) { static propTypes = { keyField: PropTypes.string.isRequired, data: PropTypes.array.isRequired, @@ -17,11 +18,21 @@ class ToolkitProvider extends React.Component { PropTypes.shape({ searchFormatted: PropTypes.bool }) + ]), + exportCSV: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.shape({ + fileName: PropTypes.string, + separator: PropTypes.string, + ignoreHeader: PropTypes.bool, + noAutoBOM: PropTypes.bool + }) ]) } static defaultProps = { - search: null + search: false, + exportCSV: false } constructor(props) { @@ -53,6 +64,9 @@ class ToolkitProvider extends React.Component { searchProps: { onSearch: this.onSearch }, + csvProps: { + onExport: this.handleExportCSV + }, baseProps } } > diff --git a/packages/react-bootstrap-table2-toolkit/index.js b/packages/react-bootstrap-table2-toolkit/index.js index 836f07d..f9553ac 100644 --- a/packages/react-bootstrap-table2-toolkit/index.js +++ b/packages/react-bootstrap-table2-toolkit/index.js @@ -4,3 +4,4 @@ import ToolkitProvider from './provider'; export default ToolkitProvider; export const ToolkitContext = Context; export { default as Search } from './src/search'; +export { default as CSVExport } from './src/csv'; diff --git a/packages/react-bootstrap-table2-toolkit/package.json b/packages/react-bootstrap-table2-toolkit/package.json index efdbc64..6338d8a 100644 --- a/packages/react-bootstrap-table2-toolkit/package.json +++ b/packages/react-bootstrap-table2-toolkit/package.json @@ -43,5 +43,8 @@ "prop-types": "^15.0.0", "react": "^16.3.0", "react-dom": "^16.3.0" + }, + "dependencies": { + "file-saver": "1.3.8" } } diff --git a/packages/react-bootstrap-table2-toolkit/src/csv/button.js b/packages/react-bootstrap-table2-toolkit/src/csv/button.js new file mode 100644 index 0000000..d0ee010 --- /dev/null +++ b/packages/react-bootstrap-table2-toolkit/src/csv/button.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const ExportCSVButton = (props) => { + const { + onExport, + children, + ...rest + } = props; + + return ( + + ); +}; + +ExportCSVButton.propTypes = { + children: PropTypes.node.isRequired, + onExport: PropTypes.func.isRequired, + className: PropTypes.string, + style: PropTypes.object +}; +ExportCSVButton.defaultProps = { + className: 'react-bs-table-csv-btn btn btn-default', + style: {} +}; + +export default ExportCSVButton; diff --git a/packages/react-bootstrap-table2-toolkit/src/csv/exporter.js b/packages/react-bootstrap-table2-toolkit/src/csv/exporter.js new file mode 100644 index 0000000..0cb9252 --- /dev/null +++ b/packages/react-bootstrap-table2-toolkit/src/csv/exporter.js @@ -0,0 +1,64 @@ +/* eslint no-unneeded-ternary: 0 */ +import FileSaver from 'file-saver'; + +export const getMetaInfo = columns => + columns + .map(column => ({ + field: column.dataField, + type: column.csvType || String, + formatter: column.csvFormatter, + formatExtraData: column.formatExtraData, + header: column.csvText || column.text, + export: column.csvExport === false ? false : true, + row: Number(column.row) || 0, + rowSpan: Number(column.rowSpan) || 1, + colSpan: Number(column.colSpan) || 1 + })) + .filter(_ => _.export); + +export const transform = ( + data, + meta, + { + separator, + ignoreHeader + } +) => { + const visibleColumns = meta.filter(m => m.export); + let content = ''; + // extract csv header + if (!ignoreHeader) { + content += visibleColumns.map(m => `"${m.header}"`).join(separator); + content += '\n'; + } + // extract csv body + if (data.length === 0) return content; + content += data + .map((row, rowIndex) => + visibleColumns.map((m) => { + let cellContent = row[m.field]; + if (m.formatter) { + cellContent = m.formatter(cellContent, row, rowIndex, m.formatExtraData); + } + if (m.type === String) { + return `"${cellContent}"`; + } + return cellContent; + }).join(separator)).join('\n'); + + return content; +}; + +export const save = ( + content, + { + noAutoBOM, + fileName + } +) => { + FileSaver.saveAs( + new Blob(['\ufeff', content], { type: 'text/plain;charset=utf-8' }), + fileName, + noAutoBOM + ); +}; diff --git a/packages/react-bootstrap-table2-toolkit/src/csv/index.js b/packages/react-bootstrap-table2-toolkit/src/csv/index.js new file mode 100644 index 0000000..4c60453 --- /dev/null +++ b/packages/react-bootstrap-table2-toolkit/src/csv/index.js @@ -0,0 +1,3 @@ +import ExportCSVButton from './button'; + +export default { ExportCSVButton }; diff --git a/packages/react-bootstrap-table2-toolkit/src/op/csv.js b/packages/react-bootstrap-table2-toolkit/src/op/csv.js new file mode 100644 index 0000000..71cf3b2 --- /dev/null +++ b/packages/react-bootstrap-table2-toolkit/src/op/csv.js @@ -0,0 +1,24 @@ +import { getMetaInfo, transform, save } from '../csv/exporter'; + +const csvDefaultOptions = { + fileName: 'spreadsheet.csv', + separator: ',', + ignoreHeader: false, + noAutoBOM: true +}; + +export default Base => + class CSVOperation extends Base { + handleExportCSV = () => { + const { columns, data, exportCSV } = this.props; + const meta = getMetaInfo(columns); + const options = exportCSV === true ? + csvDefaultOptions : + { + ...csvDefaultOptions, + ...exportCSV + }; + const content = transform(data, meta, options); + save(content, options); + } + }; diff --git a/packages/react-bootstrap-table2-toolkit/src/op/index.js b/packages/react-bootstrap-table2-toolkit/src/op/index.js new file mode 100644 index 0000000..847e057 --- /dev/null +++ b/packages/react-bootstrap-table2-toolkit/src/op/index.js @@ -0,0 +1,5 @@ +import csvOperation from './csv'; + +export default { + csvOperation +}; diff --git a/packages/react-bootstrap-table2-toolkit/statelessOp.js b/packages/react-bootstrap-table2-toolkit/statelessOp.js new file mode 100644 index 0000000..b39287d --- /dev/null +++ b/packages/react-bootstrap-table2-toolkit/statelessOp.js @@ -0,0 +1,4 @@ +import Operation from './src/op'; + +export default Base => + class StatelessOperation extends Operation.csvOperation(Base) {}; diff --git a/packages/react-bootstrap-table2-toolkit/yarn.lock b/packages/react-bootstrap-table2-toolkit/yarn.lock new file mode 100644 index 0000000..bcdf61c --- /dev/null +++ b/packages/react-bootstrap-table2-toolkit/yarn.lock @@ -0,0 +1,7 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +file-saver@1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8"