From 8ebd0dd0cdd10dc0db2d9bcf9ebfa0fc5b0ccb09 Mon Sep 17 00:00:00 2001
From: pkupczyk <piotr.kupczyk@id.ethz.ch>
Date: Mon, 7 Nov 2022 14:04:48 +0100
Subject: [PATCH] SSDM-13152 : Exports for master data and metadata UI - master
 data grids

---
 openbis_ng_ui/src/js/common/consts/ids.js     |   6 +
 .../src/js/components/common/grid/Grid.jsx    |   8 +-
 .../components/common/grid/GridController.js  | 176 ++++++++----------
 .../common/grid/GridWithSettings.jsx          |  26 +++
 .../ActiveUserReportFacade.js                 |   3 +-
 .../types/common/EntityTypesGrid.jsx          |  11 +-
 .../types/common/PropertyTypesGrid.jsx        |   1 +
 .../types/common/VocabularyTypesGrid.jsx      |   1 +
 .../js/components/types/search/TypeSearch.jsx |  20 ++
 9 files changed, 150 insertions(+), 102 deletions(-)

diff --git a/openbis_ng_ui/src/js/common/consts/ids.js b/openbis_ng_ui/src/js/common/consts/ids.js
index 54947bbc761..0861f9e0607 100644
--- a/openbis_ng_ui/src/js/common/consts/ids.js
+++ b/openbis_ng_ui/src/js/common/consts/ids.js
@@ -1,5 +1,7 @@
 // app
 const WEB_APP_ID = 'openbis_ng_ui'
+const WEB_APP_SERVICE = 'openbis-ng-ui-service'
+const EXPORT_SERVICE = 'xls-export'
 
 // grids
 const OBJECT_TYPES_GRID_ID = 'object_types_grid'
@@ -31,6 +33,10 @@ export default {
   // app
   WEB_APP_ID,
 
+  // service,
+  WEB_APP_SERVICE,
+  EXPORT_SERVICE,
+
   // grids
   OBJECT_TYPES_GRID_ID,
   COLLECTION_TYPES_GRID_ID,
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 c54e15a602d..ecb936950d3 100644
--- a/openbis_ng_ui/src/js/components/common/grid/Grid.jsx
+++ b/openbis_ng_ui/src/js/components/common/grid/Grid.jsx
@@ -224,13 +224,17 @@ class Grid extends React.PureComponent {
   }
 
   renderExports() {
-    const { id, multiselectable } = this.props
+    const { id, exportable, multiselectable } = this.props
     const { rows, exportOptions } = this.state
 
+    if (!exportable) {
+      return null
+    }
+
     return (
       <GridExports
         id={id}
-        disabled={rows.length === 0}
+        disabled={!exportable || rows.length === 0}
         exportOptions={exportOptions}
         multiselectable={multiselectable}
         onExport={this.controller.handleExport}
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 19f1578c3c2..e43161b6b71 100644
--- a/openbis_ng_ui/src/js/components/common/grid/GridController.js
+++ b/openbis_ng_ui/src/js/components/common/grid/GridController.js
@@ -1,7 +1,5 @@
 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'
@@ -1022,122 +1020,106 @@ export default class GridController {
   }
 
   async handleExport() {
-    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()
 
-    var data = []
-    var columns = []
-    var prefix = ''
+    const { scheduleExport, loadExported } = props
 
-    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)
+    if (!scheduleExport || !loadExported) {
+      return
     }
 
-    columns = columns.filter(column => column.exportable)
+    let exportedRows = []
 
-    if (exportOptions.rows === GridExportOptions.ALL_PAGES) {
+    if (state.exportOptions.rows === GridExportOptions.ALL_PAGES) {
       if (state.local) {
-        data = state.sortedRows
+        exportedRows = state.sortedRows
       } else if (props.loadRows) {
+        const columns = {}
+
+        state.allColumns.forEach(column => {
+          columns[column.name] = column
+        })
+
         const loadedResult = await props.loadRows({
+          columns: columns,
+          filterMode: state.filterMode,
           filters: state.filters,
           globalFilter: state.globalFilter,
           page: 0,
           pageSize: 1000000,
           sortings: state.sortings
         })
-        data = loadedResult.rows
+        exportedRows = 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(
+    } else if (state.exportOptions.rows === GridExportOptions.CURRENT_PAGE) {
+      exportedRows = state.rows
+    } else if (state.exportOptions.rows === GridExportOptions.SELECTED_ROWS) {
+      exportedRows = Object.values(state.multiselectedRows).map(
         selectedRow => selectedRow.data
       )
-      prefix += 'SelectedRows'
-      _exportColumnsFromData(prefix, data, columns)
     } else {
-      throw Error('Unsupported rows option: ' + exportOptions.columns)
+      throw Error('Unsupported rows option: ' + state.exportOptions.columns)
+    }
+
+    if (exportedRows.some(row => _.isEmpty(row.exportableId))) {
+      throw Error(
+        "Some of the rows to be exported do not have 'exportableId' set."
+      )
     }
+
+    let exportedProperties = {}
+
+    if (state.exportOptions.columns === GridExportOptions.ALL_COLUMNS) {
+      exportedProperties = {}
+    } else if (
+      state.exportOptions.columns === GridExportOptions.VISIBLE_COLUMNS
+    ) {
+      const { newAllColumns } = await this._loadColumns(
+        exportedRows,
+        state.columnsVisibility,
+        state.columnsSorting
+      )
+
+      newAllColumns.forEach(column => {
+        if (column.exportableProperty) {
+          const propertyCode = column.exportableProperty.code
+          const propertyTypesMap = column.exportableProperty.types
+
+          Object.keys(propertyTypesMap).forEach(kind => {
+            const propertyTypesForKind = propertyTypesMap[kind]
+
+            propertyTypesForKind.forEach(propertyTypePermId => {
+              let exportedPropertiesForKind = exportedProperties[kind]
+
+              if (!exportedPropertiesForKind) {
+                exportedProperties[kind] = exportedPropertiesForKind = {}
+              }
+
+              let exportedPropertiesForKindAndType =
+                exportedPropertiesForKind[propertyTypePermId]
+
+              if (!exportedPropertiesForKindAndType) {
+                exportedPropertiesForKind[propertyTypePermId] =
+                  exportedPropertiesForKindAndType = []
+              }
+
+              exportedPropertiesForKindAndType.push(propertyCode)
+            })
+          })
+        }
+      })
+    } else {
+      throw Error('Unsupported columns option: ' + state.exportOptions.columns)
+    }
+
+    const exportedIds = exportedRows.map(row => row.exportableId)
+
+    scheduleExport({
+      exportedIds: exportedIds,
+      exportedProperties: exportedProperties,
+      exportedValues: state.exportOptions.values
+    })
   }
 
   async handleExportOptionsChange(exportOptions) {
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 5e1ef524efb..c35a1f01a38 100644
--- a/openbis_ng_ui/src/js/components/common/grid/GridWithSettings.jsx
+++ b/openbis_ng_ui/src/js/components/common/grid/GridWithSettings.jsx
@@ -1,6 +1,7 @@
 import React from 'react'
 import autoBind from 'auto-bind'
 import Grid from '@src/js/components/common/grid/Grid.jsx'
+import GridExportOptions from '@src/js/components/common/grid/GridExportOptions.js'
 import openbis from '@src/js/services/openbis.js'
 import ids from '@src/js/common/consts/ids.js'
 import logger from '@src/js/common/logger.js'
@@ -22,6 +23,8 @@ 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}
       />
     )
   }
@@ -59,4 +62,27 @@ export default class GridWithSettings extends React.PureComponent {
 
     await openbis.updatePersons([update])
   }
+
+  async scheduleExport({ exportedIds, exportedProperties, exportedValues }) {
+    const serviceId = new openbis.CustomASServiceCode(ids.EXPORT_SERVICE)
+
+    const serviceOptions = new openbis.CustomASServiceExecutionOptions()
+    serviceOptions.withParameter('method', 'export')
+    serviceOptions.withParameter('file_name', this.props.id)
+    serviceOptions.withParameter('ids', exportedIds)
+    serviceOptions.withParameter('export_referred', true)
+    serviceOptions.withParameter('export_properties', exportedProperties)
+
+    if (exportedValues === GridExportOptions.PLAIN_TEXT) {
+      serviceOptions.withParameter('text_formatting', 'PLAIN')
+    } else if (exportedValues === GridExportOptions.RICH_TEXT) {
+      serviceOptions.withParameter('text_formatting', 'RICH')
+    } else {
+      throw Error('Unsupported text formatting ' + exportedValues)
+    }
+
+    return await openbis.executeService(serviceId, serviceOptions)
+  }
+
+  async loadExported() {}
 }
diff --git a/openbis_ng_ui/src/js/components/tools/form/activeUserReport/ActiveUserReportFacade.js b/openbis_ng_ui/src/js/components/tools/form/activeUserReport/ActiveUserReportFacade.js
index 8916c740266..29244e8a6a4 100644
--- a/openbis_ng_ui/src/js/components/tools/form/activeUserReport/ActiveUserReportFacade.js
+++ b/openbis_ng_ui/src/js/components/tools/form/activeUserReport/ActiveUserReportFacade.js
@@ -1,8 +1,9 @@
 import openbis from '@src/js/services/openbis.js'
+import ids from '@src/js/common/consts/ids.js'
 
 export default class ActiveUserReportFacade {
   async sendReport() {
-    const serviceId = new openbis.CustomASServiceCode('openbis-ng-ui-service')
+    const serviceId = new openbis.CustomASServiceCode(ids.WEB_APP_SERVICE)
     const serviceOptions = new openbis.CustomASServiceExecutionOptions()
     serviceOptions.withParameter('method', 'sendCountActiveUsersEmail')
     return openbis.executeService(serviceId, serviceOptions)
diff --git a/openbis_ng_ui/src/js/components/types/common/EntityTypesGrid.jsx b/openbis_ng_ui/src/js/components/types/common/EntityTypesGrid.jsx
index ca7e09d3b78..1a0003e327c 100644
--- a/openbis_ng_ui/src/js/components/types/common/EntityTypesGrid.jsx
+++ b/openbis_ng_ui/src/js/components/types/common/EntityTypesGrid.jsx
@@ -10,8 +10,14 @@ class EntityTypesGrid extends React.PureComponent {
   render() {
     logger.log(logger.DEBUG, 'EntityTypesGrid.render')
 
-    const { id, rows, selectedRowId, onSelectedRowChange, controllerRef } =
-      this.props
+    const {
+      id,
+      rows,
+      exportable,
+      selectedRowId,
+      onSelectedRowChange,
+      controllerRef
+    } = this.props
 
     return (
       <GridWithSettings
@@ -21,6 +27,7 @@ class EntityTypesGrid extends React.PureComponent {
         columns={this.getColumns()}
         rows={rows}
         sort='code'
+        exportable={exportable}
         selectable={true}
         selectedRowId={selectedRowId}
         onSelectedRowChange={onSelectedRowChange}
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 1b879db7380..2fa5b20155e 100644
--- a/openbis_ng_ui/src/js/components/types/common/PropertyTypesGrid.jsx
+++ b/openbis_ng_ui/src/js/components/types/common/PropertyTypesGrid.jsx
@@ -104,6 +104,7 @@ 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 34ab388f9c3..68260c1ab9c 100644
--- a/openbis_ng_ui/src/js/components/types/common/VocabularyTypesGrid.jsx
+++ b/openbis_ng_ui/src/js/components/types/common/VocabularyTypesGrid.jsx
@@ -38,6 +38,7 @@ class VocabularyTypesGrid extends React.PureComponent {
         ]}
         rows={rows}
         sort='code'
+        exportable={true}
         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 35ead6de702..91e2f83ea6f 100644
--- a/openbis_ng_ui/src/js/components/types/search/TypeSearch.jsx
+++ b/openbis_ng_ui/src/js/components/types/search/TypeSearch.jsx
@@ -73,6 +73,10 @@ class TypeSearch extends React.Component {
       .filter(result.objects, this.props.searchText, ['code', 'description'])
       .map(object => ({
         id: _.get(object, 'code'),
+        exportableId: {
+          exportable_kind: 'SAMPLE_TYPE',
+          perm_id: object.getPermId().getPermId()
+        },
         code: _.get(object, 'code'),
         description: _.get(object, 'description'),
         subcodeUnique: _.get(object, 'subcodeUnique', false),
@@ -103,6 +107,10 @@ class TypeSearch extends React.Component {
       .filter(result.objects, this.props.searchText, ['code', 'description'])
       .map(object => ({
         id: _.get(object, 'code'),
+        exportableId: {
+          exportable_kind: 'EXPERIMENT_TYPE',
+          perm_id: object.getPermId().getPermId()
+        },
         code: _.get(object, 'code'),
         description: _.get(object, 'description'),
         validationPlugin: _.get(object, 'validationPlugin.name')
@@ -130,6 +138,10 @@ class TypeSearch extends React.Component {
       .filter(result.objects, this.props.searchText, ['code', 'description'])
       .map(object => ({
         id: _.get(object, 'code'),
+        exportableId: {
+          exportable_kind: 'DATASET_TYPE',
+          perm_id: object.getPermId().getPermId()
+        },
         code: _.get(object, 'code'),
         description: _.get(object, 'description'),
         validationPlugin: _.get(object, 'validationPlugin.name'),
@@ -184,6 +196,10 @@ class TypeSearch extends React.Component {
       .filter(result.objects, this.props.searchText, ['code', 'description'])
       .map(object => ({
         id: object.code,
+        exportableId: {
+          exportable_kind: 'VOCABULARY',
+          perm_id: object.getPermId().getPermId()
+        },
         code: object.code,
         description: object.description,
         urlTemplate: object.urlTemplate
@@ -382,6 +398,7 @@ class TypeSearch extends React.Component {
             }
             kind={openbis.EntityKind.SAMPLE}
             rows={this.state.objectTypes}
+            exportable={true}
             onSelectedRowChange={this.handleSelectedRowChange(
               objectTypes.OBJECT_TYPE
             )}
@@ -407,6 +424,7 @@ class TypeSearch extends React.Component {
             }
             kind={openbis.EntityKind.EXPERIMENT}
             rows={this.state.collectionTypes}
+            exportable={true}
             onSelectedRowChange={this.handleSelectedRowChange(
               objectTypes.COLLECTION_TYPE
             )}
@@ -430,6 +448,7 @@ class TypeSearch extends React.Component {
             }
             kind={openbis.EntityKind.DATA_SET}
             rows={this.state.dataSetTypes}
+            exportable={true}
             onSelectedRowChange={this.handleSelectedRowChange(
               objectTypes.DATA_SET_TYPE
             )}
@@ -455,6 +474,7 @@ class TypeSearch extends React.Component {
             }
             kind={openbis.EntityKind.MATERIAL}
             rows={this.state.materialTypes}
+            exportable={false}
             onSelectedRowChange={this.handleSelectedRowChange(
               objectTypes.MATERIAL_TYPE
             )}
-- 
GitLab