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