From 1fb009f905da3e00eef1e2b724540145ae65dcd5 Mon Sep 17 00:00:00 2001
From: pkupczyk <piotr.kupczyk@id.ethz.ch>
Date: Thu, 12 Sep 2019 19:55:30 +0200
Subject: [PATCH] SSDM-7594 : Grid - make it possible to configure column order

---
 .../components/common/grid/ColumnConfig.jsx   |  34 ++----
 .../common/grid/ColumnConfigRow.jsx           | 111 ++++++++++++++++++
 .../src/components/common/grid/Grid.jsx       | 110 ++++++++++++-----
 3 files changed, 199 insertions(+), 56 deletions(-)
 create mode 100644 openbis_ng_ui/src/components/common/grid/ColumnConfigRow.jsx

diff --git a/openbis_ng_ui/src/components/common/grid/ColumnConfig.jsx b/openbis_ng_ui/src/components/common/grid/ColumnConfig.jsx
index 7d4da3e8383..6829948807a 100644
--- a/openbis_ng_ui/src/components/common/grid/ColumnConfig.jsx
+++ b/openbis_ng_ui/src/components/common/grid/ColumnConfig.jsx
@@ -4,8 +4,7 @@ import { withStyles } from '@material-ui/core/styles'
 import IconButton from '@material-ui/core/IconButton'
 import SettingsIcon from '@material-ui/icons/Settings'
 import Popover from '@material-ui/core/Popover'
-import FormControlLabel from '@material-ui/core/FormControlLabel'
-import Checkbox from '@material-ui/core/Checkbox'
+import ColumnConfigRow from './ColumnConfigRow.jsx'
 import logger from '../../../common/logger.js'
 
 const styles = () => ({
@@ -28,7 +27,6 @@ class ColumnConfig extends React.Component {
     }
     this.handleOpen = this.handleOpen.bind(this)
     this.handleClose = this.handleClose.bind(this)
-    this.handleChange = this.handleChange.bind(this)
   }
 
   handleOpen(event) {
@@ -43,23 +41,10 @@ class ColumnConfig extends React.Component {
     })
   }
 
-  handleChange(event) {
-    let columns = [...this.props.visibleColumns]
-    let column = event.target.value
-
-    if (columns.includes(column)) {
-      _.pull(columns, column)
-    } else {
-      columns.push(column)
-    }
-
-    this.props.onColumnsChange(columns)
-  }
-
   render() {
     logger.log(logger.DEBUG, 'ColumnConfig.render')
 
-    const { classes, allColumns, visibleColumns } = this.props
+    const { classes, columns, onVisibleChange, onOrderChange } = this.props
     const { el } = this.state
 
     return (
@@ -81,17 +66,12 @@ class ColumnConfig extends React.Component {
           }}
         >
           <ol className={classes.columns}>
-            {allColumns.map(column => (
+            {columns.map(column => (
               <li key={column.field}>
-                <FormControlLabel
-                  control={
-                    <Checkbox
-                      value={column.field}
-                      checked={visibleColumns.includes(column.field)}
-                      onChange={this.handleChange}
-                    />
-                  }
-                  label={column.label || column.field}
+                <ColumnConfigRow
+                  column={column}
+                  onVisibleChange={onVisibleChange}
+                  onOrderChange={onOrderChange}
                 />
               </li>
             ))}
diff --git a/openbis_ng_ui/src/components/common/grid/ColumnConfigRow.jsx b/openbis_ng_ui/src/components/common/grid/ColumnConfigRow.jsx
new file mode 100644
index 00000000000..0ff58ae3755
--- /dev/null
+++ b/openbis_ng_ui/src/components/common/grid/ColumnConfigRow.jsx
@@ -0,0 +1,111 @@
+import _ from 'lodash'
+import React from 'react'
+import { DragSource } from 'react-dnd'
+import { DropTarget } from 'react-dnd'
+import FormControlLabel from '@material-ui/core/FormControlLabel'
+import Checkbox from '@material-ui/core/Checkbox'
+import DragHandleIcon from '@material-ui/icons/DragHandle'
+import RootRef from '@material-ui/core/RootRef'
+import { withStyles } from '@material-ui/core/styles'
+import logger from '../../../common/logger.js'
+
+const styles = () => ({
+  row: {
+    display: 'flex',
+    alignItems: 'center'
+  },
+  label: {
+    marginLeft: 0
+  },
+  drag: {
+    display: 'flex',
+    cursor: 'grab'
+  }
+})
+
+const source = {
+  beginDrag(props) {
+    return { column: props.column.field }
+  },
+  endDrag(props, monitor) {
+    if (monitor.getItem() && monitor.getDropResult()) {
+      const { column: sourceColumn } = monitor.getItem()
+      const { column: targetColumn } = monitor.getDropResult()
+      props.onOrderChange(sourceColumn, targetColumn)
+    }
+  }
+}
+
+function sourceCollect(connect, monitor) {
+  return {
+    connectDragSource: connect.dragSource(),
+    connectDragPreview: connect.dragPreview(),
+    isDragging: monitor.isDragging()
+  }
+}
+
+const target = {
+  drop(props) {
+    return { column: props.column.field }
+  }
+}
+
+function targetCollect(connect, monitor) {
+  return {
+    connectDropTarget: connect.dropTarget(),
+    isDragging: monitor.getItem() !== null
+  }
+}
+
+class ColumnConfigRow extends React.Component {
+  constructor(props) {
+    super(props)
+    this.handleRef = React.createRef()
+    this.rowRef = React.createRef()
+    this.handleVisibleChange = this.handleVisibleChange.bind(this)
+  }
+
+  componentDidMount() {
+    this.props.connectDragSource(this.handleRef.current)
+    this.props.connectDragPreview(this.rowRef.current)
+    this.props.connectDropTarget(this.rowRef.current)
+  }
+
+  handleVisibleChange() {
+    this.props.onVisibleChange(this.props.column.field)
+  }
+
+  render() {
+    logger.log(logger.DEBUG, 'ColumnConfigRow.render')
+
+    const { classes, column } = this.props
+
+    return (
+      <RootRef rootRef={this.rowRef}>
+        <div className={classes.row}>
+          <RootRef rootRef={this.handleRef}>
+            <div className={classes.drag}>
+              <DragHandleIcon />
+            </div>
+          </RootRef>
+          <FormControlLabel
+            classes={{ root: classes.label }}
+            control={
+              <Checkbox
+                checked={column.visible}
+                onChange={this.handleVisibleChange}
+              />
+            }
+            label={column.label || column.field}
+          />
+        </div>
+      </RootRef>
+    )
+  }
+}
+
+export default _.flow(
+  DragSource('column', source, sourceCollect),
+  DropTarget('column', target, targetCollect),
+  withStyles(styles)
+)(ColumnConfigRow)
diff --git a/openbis_ng_ui/src/components/common/grid/Grid.jsx b/openbis_ng_ui/src/components/common/grid/Grid.jsx
index aaef1a30bed..2ad7ca827ed 100644
--- a/openbis_ng_ui/src/components/common/grid/Grid.jsx
+++ b/openbis_ng_ui/src/components/common/grid/Grid.jsx
@@ -67,25 +67,26 @@ class Grid extends React.Component {
 
     const sortDefault = _.isFunction(props.data) ? false : true
 
-    this.columnsArray = props.columns.map(column => ({
+    let columns = props.columns.map(column => ({
       ...column,
       label: column.label || _.upperFirst(column.field),
       render: column.render || (row => _.get(row, column.field)),
-      sort: column.sort === undefined ? sortDefault : Boolean(column.sort)
+      sort: column.sort === undefined ? sortDefault : Boolean(column.sort),
+      visible: true
     }))
-    this.columnsMap = _.keyBy(props.columns, 'field')
 
     this.state = {
       loaded: false,
       filters: this.props.filters || {},
       page: 0,
       pageSize: 10,
-      visibleColumns: Object.keys(this.columnsMap),
+      columns: columns,
       columnConfigEl: null
     }
 
     this.handleFilterChange = this.handleFilterChange.bind(this)
-    this.handleColumnsChange = this.handleColumnsChange.bind(this)
+    this.handleColumnVisibleChange = this.handleColumnVisibleChange.bind(this)
+    this.handleColumnOrderChange = this.handleColumnOrderChange.bind(this)
     this.handlePageChange = this.handlePageChange.bind(this)
     this.handlePageSizeChange = this.handlePageSizeChange.bind(this)
   }
@@ -140,8 +141,26 @@ class Grid extends React.Component {
         if (gridSettings) {
           let settings = JSON.parse(gridSettings.value)
           if (settings) {
+            let newColumns = [...this.state.columns]
+            newColumns.sort((c1, c2) => {
+              let index1 = _.findIndex(settings.columns, ['field', c1.field])
+              let index2 = _.findIndex(settings.columns, ['field', c2.field])
+              return index1 - index2
+            })
+            newColumns = newColumns.map(column => {
+              let setting = _.find(settings.columns, ['field', column.field])
+              if (setting) {
+                return {
+                  ...column,
+                  visible: setting.visible
+                }
+              } else {
+                return column
+              }
+            })
             this.setState(() => ({
-              ...settings
+              ...settings,
+              columns: newColumns
             }))
           }
         }
@@ -150,11 +169,16 @@ class Grid extends React.Component {
   }
 
   saveSettings() {
+    let columns = this.state.columns.map(column => ({
+      field: column.field,
+      visible: column.visible
+    }))
+
     let settings = {
       pageSize: this.state.pageSize,
       sort: this.state.sort,
       sortDirection: this.state.sortDirection,
-      visibleColumns: this.state.visibleColumns
+      columns
     }
 
     let gridSettings = new dto.WebAppSettingCreation()
@@ -185,10 +209,42 @@ class Grid extends React.Component {
     )
   }
 
-  handleColumnsChange(visibleColumns) {
+  handleColumnVisibleChange(field) {
+    let columns = this.state.columns.map(column => {
+      if (column.field === field) {
+        return {
+          ...column,
+          visible: !column.visible
+        }
+      } else {
+        return column
+      }
+    })
+
     this.setState(
       () => ({
-        visibleColumns
+        columns
+      }),
+      () => {
+        this.saveSettings()
+      }
+    )
+  }
+
+  handleColumnOrderChange(sourceField, targetField) {
+    let columns = [...this.state.columns]
+    let sourceIndex = _.findIndex(columns, ['field', sourceField])
+    let targetIndex = _.findIndex(columns, ['field', targetField])
+
+    if (sourceIndex !== -1 && targetIndex !== -1) {
+      let temp = columns[sourceIndex]
+      columns[sourceIndex] = columns[targetIndex]
+      columns[targetIndex] = temp
+    }
+
+    this.setState(
+      () => ({
+        columns
       }),
       () => {
         this.saveSettings()
@@ -254,9 +310,9 @@ class Grid extends React.Component {
 
     return _.filter(objects, row => {
       let matchesAll = true
-      this.state.visibleColumns.forEach(visibleColumn => {
-        let value = _.get(row, visibleColumn)
-        let filter = this.state.filters[visibleColumn]
+      this.state.columns.forEach(column => {
+        let value = _.get(row, column.field)
+        let filter = this.state.filters[column.field]
         matchesAll = matchesAll && matches(value, filter)
       })
       return matchesAll
@@ -267,9 +323,9 @@ class Grid extends React.Component {
     const { sort, sortDirection } = this.state
 
     if (sort) {
+      let column = _.find(this.state.columns, ['field', sort])
       return objects.sort((t1, t2) => {
         let sign = sortDirection === 'asc' ? 1 : -1
-        let column = this.columnsMap[sort]
         let v1 = _.get(t1, column.field) || ''
         let v2 = _.get(t2, column.field) || ''
         return sign * v1.localeCompare(v2)
@@ -295,7 +351,7 @@ class Grid extends React.Component {
     }
 
     const { classes } = this.props
-    const { page, pageSize, visibleColumns } = this.state
+    const { page, pageSize, columns } = this.state
 
     let pagedObjects = null
     let totalCount = null
@@ -316,19 +372,17 @@ class Grid extends React.Component {
           <Table classes={{ root: classes.table }}>
             <TableHead classes={{ root: classes.tableHeader }}>
               <TableRow>
-                {this.columnsArray.map(column => this.renderFilterCell(column))}
+                {columns.map(column => this.renderFilterCell(column))}
               </TableRow>
               <TableRow>
-                {this.columnsArray.map(column => this.renderHeaderCell(column))}
+                {columns.map(column => this.renderHeaderCell(column))}
               </TableRow>
             </TableHead>
             <TableBody>
               {pagedObjects.map(row => {
                 return (
                   <TableRow key={row.id} hover>
-                    {this.columnsArray.map(column =>
-                      this.renderRowCell(column, row)
-                    )}
+                    {columns.map(column => this.renderRowCell(column, row))}
                   </TableRow>
                 )
               })}
@@ -347,9 +401,9 @@ class Grid extends React.Component {
             onPageSizeChange={this.handlePageSizeChange}
           />
           <ColumnConfig
-            allColumns={this.columnsArray}
-            visibleColumns={visibleColumns}
-            onColumnsChange={this.handleColumnsChange}
+            columns={columns}
+            onVisibleChange={this.handleColumnVisibleChange}
+            onOrderChange={this.handleColumnOrderChange}
           />
         </div>
       </div>
@@ -357,9 +411,9 @@ class Grid extends React.Component {
   }
 
   renderHeaderCell(column) {
-    const { visibleColumns, sort, sortDirection } = this.state
+    const { sort, sortDirection } = this.state
 
-    if (visibleColumns.includes(column.field)) {
+    if (column.visible) {
       if (column.sort) {
         return (
           <TableCell key={column.field}>
@@ -381,9 +435,9 @@ class Grid extends React.Component {
   }
 
   renderFilterCell(column) {
-    const { visibleColumns, filters } = this.state
+    const { filters } = this.state
 
-    if (visibleColumns.includes(column.field)) {
+    if (column.visible) {
       let filter = filters[column.field] || ''
       let filterChange = filter => {
         this.handleFilterChange(column.field, filter)
@@ -399,9 +453,7 @@ class Grid extends React.Component {
   }
 
   renderRowCell(column, row) {
-    const { visibleColumns } = this.state
-
-    if (visibleColumns.includes(column.field)) {
+    if (column.visible) {
       let rendered = column.render(row)
       return (
         <TableCell key={column.field}>
-- 
GitLab