From 31de0209ff70a89b44b9cbadf4b9353d62070008 Mon Sep 17 00:00:00 2001 From: pkupczyk <piotr.kupczyk@id.ethz.ch> Date: Wed, 9 Nov 2022 11:54:59 +0100 Subject: [PATCH] SSDM-13152 : Exports for master data and metadata UI - have both XLS and TSV exports --- .../src/js/components/common/grid/Grid.jsx | 4 +- .../components/common/grid/GridController.js | 138 +++++++++++++++++- .../common/grid/GridExportOptions.js | 5 + .../common/grid/GridWithSettings.jsx | 7 +- .../types/common/PropertyTypesGrid.jsx | 1 - .../types/common/VocabularyTypesGrid.jsx | 3 +- .../js/components/types/search/TypeSearch.jsx | 8 +- 7 files changed, 150 insertions(+), 16 deletions(-) diff --git a/openbis_ng_ui/src/js/components/common/grid/Grid.jsx b/openbis_ng_ui/src/js/components/common/grid/Grid.jsx index ecb936950d3..efa5bb5e547 100644 --- a/openbis_ng_ui/src/js/components/common/grid/Grid.jsx +++ b/openbis_ng_ui/src/js/components/common/grid/Grid.jsx @@ -224,9 +224,11 @@ class Grid extends React.PureComponent { } renderExports() { - const { id, exportable, multiselectable } = this.props + const { id, multiselectable } = this.props const { rows, exportOptions } = this.state + const exportable = this.controller.getExportable() + if (!exportable) { return null } 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 020636d595e..67034269458 100644 --- a/openbis_ng_ui/src/js/components/common/grid/GridController.js +++ b/openbis_ng_ui/src/js/components/common/grid/GridController.js @@ -1,5 +1,7 @@ import _ from 'lodash' import autoBind from 'auto-bind' +import FileSaver from 'file-saver' +import CsvStringify from 'csv-stringify' import GridFilterOptions from '@src/js/components/common/grid/GridFilterOptions.js' import GridExportOptions from '@src/js/components/common/grid/GridExportOptions.js' import GridPagingOptions from '@src/js/components/common/grid/GridPagingOptions.js' @@ -1020,15 +1022,138 @@ export default class GridController { } async handleExport() { + const exportable = this.getExportable() + + if (exportable === GridExportOptions.EXPORT_TSV) { + await this.handleExportTSV() + } else if (exportable === GridExportOptions.EXPORT_XLS) { + await this.handleExportXLS() + } + } + + async handleExportTSV() { + const { exportOptions } = this.context.getState() + + 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, rows, columns) { + const arrayOfRowArrays = [] + + const headers = columns.map(column => column.name) + arrayOfRowArrays.push(headers) + + rows.forEach(row => { + var rowAsArray = [] + columns.forEach(column => { + var rowValue = column.getValue({ + row, + column, + operation: 'export', + exportOptions + }) + 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 (exportOptions.values === GridExportOptions.RICH_TEXT) { + // do nothing with the value + } else if (exportOptions.values === GridExportOptions.PLAIN_TEXT) { + rowValue = String(rowValue).replace(/<(?:.|\n)*?>/gm, '') + } else { + throw Error('Unsupported values option: ' + exportOptions.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') + } + ) + } + const state = this.context.getState() const props = this.context.getProps() - const { scheduleExport, loadExported } = props + var data = [] + var columns = [] + var prefix = '' - if (!scheduleExport || !loadExported) { - return + if (exportOptions.columns === GridExportOptions.ALL_COLUMNS) { + columns = this.getAllColumns() + prefix += 'AllColumns' + } else if (exportOptions.columns === GridExportOptions.VISIBLE_COLUMNS) { + columns = this.getVisibleColumns() + prefix += 'VisibleColumns' + } else { + throw Error('Unsupported columns option: ' + exportOptions.columns) } + columns = columns.filter(column => column.exportable) + + if (exportOptions.rows === GridExportOptions.ALL_PAGES) { + if (state.local) { + data = state.sortedRows + } else if (props.loadRows) { + const loadedResult = await props.loadRows({ + filters: state.filters, + globalFilter: state.globalFilter, + page: 0, + pageSize: 1000000, + sortings: state.sortings + }) + data = loadedResult.rows + } + + prefix += 'AllPages' + _exportColumnsFromData(prefix, data, columns) + } else if (exportOptions.rows === GridExportOptions.CURRENT_PAGE) { + data = state.rows + prefix += 'CurrentPage' + _exportColumnsFromData(prefix, data, columns) + } else if (exportOptions.rows === GridExportOptions.SELECTED_ROWS) { + data = Object.values(state.multiselectedRows).map( + selectedRow => selectedRow.data + ) + prefix += 'SelectedRows' + _exportColumnsFromData(prefix, data, columns) + } else { + throw Error('Unsupported rows option: ' + exportOptions.columns) + } + } + + async handleExportXLS() { + const state = this.context.getState() + const props = this.context.getProps() + let exportedRows = [] if (state.exportOptions.rows === GridExportOptions.ALL_PAGES) { @@ -1118,7 +1243,7 @@ export default class GridController { const exportedIds = exportedRows.map(row => row.exportableId) - scheduleExport({ + props.onExportXLS({ exportedIds: exportedIds, exportedProperties: exportedProperties, exportedValues: state.exportOptions.values @@ -1251,6 +1376,11 @@ export default class GridController { return totalCount } + getExportable() { + const { exportable } = this.context.getProps() + return exportable !== undefined ? exportable : GridExportOptions.EXPORT_TSV + } + _getCachedValue(key, newValue) { if (_.isEqual(this.cache[key], newValue)) { return this.cache[key] diff --git a/openbis_ng_ui/src/js/components/common/grid/GridExportOptions.js b/openbis_ng_ui/src/js/components/common/grid/GridExportOptions.js index dc22f039dff..b302e1e2205 100644 --- a/openbis_ng_ui/src/js/components/common/grid/GridExportOptions.js +++ b/openbis_ng_ui/src/js/components/common/grid/GridExportOptions.js @@ -1,3 +1,6 @@ +const EXPORT_TSV = 'EXPORT_TSV' +const EXPORT_XLS = 'EXPORT_XLS' + const ALL_COLUMNS = 'ALL_COLUMNS' const VISIBLE_COLUMNS = 'VISIBLE_COLUMNS' const COLUMNS_OPTIONS = [ALL_COLUMNS, VISIBLE_COLUMNS] @@ -12,6 +15,8 @@ const RICH_TEXT = 'RICH' const VALUES_OPTIONS = [PLAIN_TEXT, RICH_TEXT] export default { + EXPORT_TSV, + EXPORT_XLS, COLUMNS_OPTIONS, ROWS_OPTIONS, VALUES_OPTIONS, diff --git a/openbis_ng_ui/src/js/components/common/grid/GridWithSettings.jsx b/openbis_ng_ui/src/js/components/common/grid/GridWithSettings.jsx index 9812c7217a2..9d58f5e26d9 100644 --- a/openbis_ng_ui/src/js/components/common/grid/GridWithSettings.jsx +++ b/openbis_ng_ui/src/js/components/common/grid/GridWithSettings.jsx @@ -23,8 +23,7 @@ export default class GridWithSettings extends React.PureComponent { {...this.props} loadSettings={this.loadSettings} onSettingsChange={this.onSettingsChange} - scheduleExport={this.props.exportable ? this.scheduleExport : null} - loadExported={this.props.exportable ? this.loadExported : null} + onExportXLS={this.onExportXLS} /> ) } @@ -63,7 +62,7 @@ export default class GridWithSettings extends React.PureComponent { await openbis.updatePersons([update]) } - async scheduleExport({ exportedIds, exportedProperties, exportedValues }) { + async onExportXLS({ exportedIds, exportedProperties, exportedValues }) { try { AppController.getInstance().loadingChange(true) @@ -97,6 +96,4 @@ export default class GridWithSettings extends React.PureComponent { AppController.getInstance().loadingChange(false) } } - - async loadExported() {} } diff --git a/openbis_ng_ui/src/js/components/types/common/PropertyTypesGrid.jsx b/openbis_ng_ui/src/js/components/types/common/PropertyTypesGrid.jsx index 2fa5b20155e..1b879db7380 100644 --- a/openbis_ng_ui/src/js/components/types/common/PropertyTypesGrid.jsx +++ b/openbis_ng_ui/src/js/components/types/common/PropertyTypesGrid.jsx @@ -104,7 +104,6 @@ class PropertyTypesGrid extends React.PureComponent { ]} rows={rows} sort='code' - exportable={false} selectable={true} selectedRowId={selectedRowId} onSelectedRowChange={onSelectedRowChange} diff --git a/openbis_ng_ui/src/js/components/types/common/VocabularyTypesGrid.jsx b/openbis_ng_ui/src/js/components/types/common/VocabularyTypesGrid.jsx index 68260c1ab9c..7fbcd52886c 100644 --- a/openbis_ng_ui/src/js/components/types/common/VocabularyTypesGrid.jsx +++ b/openbis_ng_ui/src/js/components/types/common/VocabularyTypesGrid.jsx @@ -1,5 +1,6 @@ import React from 'react' import GridWithSettings from '@src/js/components/common/grid/GridWithSettings.jsx' +import GridExportOptions from '@src/js/components/common/grid/GridExportOptions.js' import VocabularyTypeLink from '@src/js/components/common/link/VocabularyTypeLink.jsx' import messages from '@src/js/common/messages.js' import logger from '@src/js/common/logger.js' @@ -38,7 +39,7 @@ class VocabularyTypesGrid extends React.PureComponent { ]} rows={rows} sort='code' - exportable={true} + exportable={GridExportOptions.EXPORT_XLS} selectable={true} selectedRowId={selectedRowId} onSelectedRowChange={onSelectedRowChange} diff --git a/openbis_ng_ui/src/js/components/types/search/TypeSearch.jsx b/openbis_ng_ui/src/js/components/types/search/TypeSearch.jsx index 91e2f83ea6f..ec3eadf8e46 100644 --- a/openbis_ng_ui/src/js/components/types/search/TypeSearch.jsx +++ b/openbis_ng_ui/src/js/components/types/search/TypeSearch.jsx @@ -5,6 +5,7 @@ import { withStyles } from '@material-ui/core/styles' import Container from '@src/js/components/common/form/Container.jsx' import AppController from '@src/js/components/AppController.js' import GridContainer from '@src/js/components/common/grid/GridContainer.jsx' +import GridExportOptions from '@src/js/components/common/grid/GridExportOptions.js' import EntityTypesGrid from '@src/js/components/types/common/EntityTypesGrid.jsx' import VocabularyTypesGrid from '@src/js/components/types/common/VocabularyTypesGrid.jsx' import PropertyTypesGrid from '@src/js/components/types/common/PropertyTypesGrid.jsx' @@ -398,7 +399,7 @@ class TypeSearch extends React.Component { } kind={openbis.EntityKind.SAMPLE} rows={this.state.objectTypes} - exportable={true} + exportable={GridExportOptions.EXPORT_XLS} onSelectedRowChange={this.handleSelectedRowChange( objectTypes.OBJECT_TYPE )} @@ -424,7 +425,7 @@ class TypeSearch extends React.Component { } kind={openbis.EntityKind.EXPERIMENT} rows={this.state.collectionTypes} - exportable={true} + exportable={GridExportOptions.EXPORT_XLS} onSelectedRowChange={this.handleSelectedRowChange( objectTypes.COLLECTION_TYPE )} @@ -448,7 +449,7 @@ class TypeSearch extends React.Component { } kind={openbis.EntityKind.DATA_SET} rows={this.state.dataSetTypes} - exportable={true} + exportable={GridExportOptions.EXPORT_XLS} onSelectedRowChange={this.handleSelectedRowChange( objectTypes.DATA_SET_TYPE )} @@ -474,7 +475,6 @@ class TypeSearch extends React.Component { } kind={openbis.EntityKind.MATERIAL} rows={this.state.materialTypes} - exportable={false} onSelectedRowChange={this.handleSelectedRowChange( objectTypes.MATERIAL_TYPE )} -- GitLab