diff --git a/openbis_ng_ui/package.json b/openbis_ng_ui/package.json index 7396f78297977a52564b57a57d43b1e1a318c7df..c98328cf991b7925534c8e41435a9441d768395d 100644 --- a/openbis_ng_ui/package.json +++ b/openbis_ng_ui/package.json @@ -10,7 +10,9 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.56", "@material-ui/pickers": "^3.3.10", + "csv": "^5.5.3", "date-fns": "^2.22.1", + "file-saver": "^2.0.5", "history": "^4.10.1", "install": "^0.13.0", "npm": "^6.14.8", diff --git a/openbis_ng_ui/src/js/components/common/grid/GridController.js b/openbis_ng_ui/src/js/components/common/grid/GridController.js index 00d47d0697a26e487edbcc8dfe0784c90c78836c..209e7c240f4bcb03cd8217aa262dc2f5700d26a5 100644 --- a/openbis_ng_ui/src/js/components/common/grid/GridController.js +++ b/openbis_ng_ui/src/js/components/common/grid/GridController.js @@ -1,6 +1,9 @@ import _ from 'lodash' import autoBind from 'auto-bind' +import FileSaver from 'file-saver' +import CsvStringify from 'csv-stringify' import compare from '@src/js/common/compare.js' +import GridExportOptions from '@src/js/components/common/grid/GridExportOptions.js' export default class GridController { constructor() { @@ -293,7 +296,8 @@ export default class GridController { filterable: column.filterable === undefined ? true : column.filterable, visible: column.visible === undefined ? true : column.visible, configurable: - column.configurable === undefined ? true : column.configurable + column.configurable === undefined ? true : column.configurable, + exportable: column.exportable === undefined ? true : column.exportable } } @@ -658,6 +662,122 @@ export default class GridController { } } + async handleExport(options) { + function _stringToUtf16ByteArray(str) { + var bytes = [] + bytes.push(255, 254) + for (var i = 0; i < str.length; ++i) { + var charCode = str.charCodeAt(i) + bytes.push(charCode & 0xff) //low byte + bytes.push((charCode & 0xff00) >>> 8) //high byte (might be 0) + } + return bytes + } + + function _exportColumnsFromData(namePrefix, data, headings) { + var arrayOfRowArrays = [] + arrayOfRowArrays.push(headings) + for (var dIdx = 0; dIdx < data.length; dIdx++) { + var rowAsArray = [] + for (var hIdx = 0; hIdx < headings.length; hIdx++) { + var headerKey = headings[hIdx] + var rowValue = data[dIdx][headerKey] + if (!rowValue) { + rowValue = '' + } else { + var specialCharsRemover = document.createElement('textarea') + specialCharsRemover.innerHTML = rowValue + rowValue = specialCharsRemover.value //Removes special HTML Chars + rowValue = String(rowValue).replace(/\r?\n|\r|\t/g, ' ') //Remove carriage returns and tabs + + if (options.values === GridExportOptions.RICH_TEXT) { + // do nothing with the value + } else if (options.values === GridExportOptions.PLAIN_TEXT) { + rowValue = String(rowValue).replace(/<(?:.|\n)*?>/gm, '') + } else { + throw Error('Unsupported values option: ' + options.values) + } + } + rowAsArray.push(rowValue) + } + arrayOfRowArrays.push(rowAsArray) + } + + CsvStringify( + { + header: false, + delimiter: '\t', + quoted: false + }, + arrayOfRowArrays, + function (err, tsv) { + var utf16bytes = _stringToUtf16ByteArray(tsv) + var utf16bytesArray = new Uint8Array(utf16bytes.length) + utf16bytesArray.set(utf16bytes, 0) + var blob = new Blob([utf16bytesArray], { + type: 'text/tsv;charset=UTF-16LE;' + }) + FileSaver.saveAs(blob, 'exportedTable' + namePrefix + '.tsv') + } + ) + } + + var data = [] + var headings = [] + var prefix = '' + + if (options.columns === GridExportOptions.ALL) { + const allColumns = this.getAllColumns() + allColumns.forEach(function (column) { + if (column.exportable) { + headings.push(column.name) + } + }) + prefix += 'AllColumns' + } else if (options.columns === GridExportOptions.VISIBLE) { + const visibleColumns = this.getVisibleColumns() + visibleColumns.forEach(function (column) { + if (column.exportable) { + headings.push(column.name) + } + }) + prefix += 'VisibleColumns' + } else { + throw Error('Unsupported columns option: ' + options.columns) + } + + const state = this.context.getState() + const props = this.context.getProps() + + if (options.rows === GridExportOptions.ALL) { + if (props.rows) { + data = props.rows + } else if (props.loadRows) { + const loadedResult = await props.loadRows({ + filters: state.filters, + page: 0, + pageSize: 1000000, + sort: state.sort, + sortDirection: state.sortDirection + }) + if (_.isArray(loadedResult)) { + data = loadedResult + } else { + data = loadedResult.rows + } + } + + prefix += 'AllRows' + _exportColumnsFromData(prefix, data, headings) + } else if (options.rows === GridExportOptions.VISIBLE) { + data = state.rows + prefix += 'VisibleRows' + _exportColumnsFromData(prefix, data, headings) + } else { + throw Error('Unsupported rows option: ' + options.columns) + } + } + getAllColumns() { const { allColumns, columnsSorting } = this.context.getState() diff --git a/openbis_ng_ui/src/js/components/common/grid/GridExportOptions.js b/openbis_ng_ui/src/js/components/common/grid/GridExportOptions.js new file mode 100644 index 0000000000000000000000000000000000000000..09e5bea82fe4708e6282a98e41e19e901abaa5a3 --- /dev/null +++ b/openbis_ng_ui/src/js/components/common/grid/GridExportOptions.js @@ -0,0 +1,11 @@ +const ALL = 'ALL' +const VISIBLE = 'VISIBLE' +const PLAIN_TEXT = 'PLAIN_TEXT' +const RICH_TEXT = 'RICH_TEXT' + +export default { + ALL, + VISIBLE, + PLAIN_TEXT, + RICH_TEXT +} diff --git a/openbis_ng_ui/src/js/components/common/grid/GridExports.jsx b/openbis_ng_ui/src/js/components/common/grid/GridExports.jsx index bf77b71d6cea5df921068e0488c8ce9962c008bc..af9f09aad6dabbaaa5be9379247122d8b2f21f54 100644 --- a/openbis_ng_ui/src/js/components/common/grid/GridExports.jsx +++ b/openbis_ng_ui/src/js/components/common/grid/GridExports.jsx @@ -5,6 +5,7 @@ import Popover from '@material-ui/core/Popover' import SelectField from '@src/js/components/common/form/SelectField.jsx' import Button from '@src/js/components/common/form/Button.jsx' import Container from '@src/js/components/common/form/Container.jsx' +import GridExportOptions from '@src/js/components/common/grid/GridExportOptions.js' import messages from '@src/js/common/messages.js' import logger from '@src/js/common/logger.js' @@ -18,19 +19,24 @@ const styles = theme => ({ } }) -const ALL = 'ALL' -const VISIBLE = 'VISIBLE' -const PLAIN_TEXT = 'PLAIN_TEXT' -const RICH_TEXT = 'RICH_TEXT' - class GridExports extends React.PureComponent { constructor(props) { super(props) this.state = { - el: null + el: null, + columns: { + value: GridExportOptions.VISIBLE + }, + rows: { + value: GridExportOptions.VISIBLE + }, + values: { + value: GridExportOptions.RICH_TEXT + } } this.handleOpen = this.handleOpen.bind(this) this.handleClose = this.handleClose.bind(this) + this.handleChange = this.handleChange.bind(this) this.handleExport = this.handleExport.bind(this) } @@ -46,11 +52,24 @@ class GridExports extends React.PureComponent { }) } - handleExport(action) { + handleChange(event) { + this.setState({ + [event.target.name]: { + value: event.target.value + } + }) + } + + handleExport() { const { onExport } = this.props this.handleClose() if (onExport) { - onExport(action) + const { columns, rows, values } = this.state + onExport({ + columns: columns.value, + rows: rows.value, + values: values.value + }) } } @@ -58,7 +77,7 @@ class GridExports extends React.PureComponent { logger.log(logger.DEBUG, 'GridExports.render') const { disabled, classes } = this.props - const { el } = this.state + const { el, columns, rows, values } = this.state return ( <div className={classes.container}> @@ -89,14 +108,14 @@ class GridExports extends React.PureComponent { options={[ { label: messages.get(messages.ALL), - value: ALL + value: GridExportOptions.ALL }, { label: messages.get(messages.VISIBLE), - value: VISIBLE + value: GridExportOptions.VISIBLE } ]} - value={VISIBLE} + value={columns.value} variant='standard' onChange={this.handleChange} /> @@ -108,14 +127,14 @@ class GridExports extends React.PureComponent { options={[ { label: messages.get(messages.ALL), - value: ALL + value: GridExportOptions.ALL }, { label: messages.get(messages.VISIBLE), - value: VISIBLE + value: GridExportOptions.VISIBLE } ]} - value={VISIBLE} + value={rows.value} variant='standard' onChange={this.handleChange} /> @@ -127,20 +146,24 @@ class GridExports extends React.PureComponent { options={[ { label: messages.get(messages.PLAIN_TEXT), - value: PLAIN_TEXT + value: GridExportOptions.PLAIN_TEXT }, { label: messages.get(messages.RICH_TEXT), - value: RICH_TEXT + value: GridExportOptions.RICH_TEXT } ]} - value={RICH_TEXT} + value={values.value} variant='standard' onChange={this.handleChange} /> </div> <div className={classes.field}> - <Button label={messages.get(messages.EXPORT)} type='neutral' /> + <Button + label={messages.get(messages.EXPORT)} + type='neutral' + onClick={this.handleExport} + /> </div> </Container> </Popover>