From 79bc3574facd4e61191aeef75b8b3ada5e997af6 Mon Sep 17 00:00:00 2001
From: pkupczyk <piotr.kupczyk@id.ethz.ch>
Date: Wed, 19 Jan 2022 12:39:20 +0100
Subject: [PATCH] SSDM-12155 : Table Widget : Improve table usability - clean
 up grid configuration widgets

---
 openbis_ng_ui/src/js/common/messages.js       |   4 +-
 .../common/form/RadioGroupField.jsx           | 164 ++++++++++++++++++
 .../src/js/components/common/grid/Grid.jsx    |  20 +--
 .../{GridConfig.jsx => GridColumnsConfig.jsx} |  57 +++---
 ...ConfigRow.jsx => GridColumnsConfigRow.jsx} |  12 +-
 .../js/components/common/grid/GridExports.jsx | 127 +++++++-------
 .../common/grid/GridFiltersConfig.jsx         | 121 +++++++++++++
 7 files changed, 395 insertions(+), 110 deletions(-)
 create mode 100644 openbis_ng_ui/src/js/components/common/form/RadioGroupField.jsx
 rename openbis_ng_ui/src/js/components/common/grid/{GridConfig.jsx => GridColumnsConfig.jsx} (76%)
 rename openbis_ng_ui/src/js/components/common/grid/{GridConfigRow.jsx => GridColumnsConfigRow.jsx} (85%)
 create mode 100644 openbis_ng_ui/src/js/components/common/grid/GridFiltersConfig.jsx

diff --git a/openbis_ng_ui/src/js/common/messages.js b/openbis_ng_ui/src/js/common/messages.js
index 43c9dded2ce..2b87c8b0177 100644
--- a/openbis_ng_ui/src/js/common/messages.js
+++ b/openbis_ng_ui/src/js/common/messages.js
@@ -237,7 +237,7 @@ const messages_en = {
   [keys.CODE]: 'Code',
   [keys.COLLECTION_TYPES]: 'Collection Types',
   [keys.COLLECTION_TYPE]: 'Collection Type',
-  [keys.COLUMN_FILTERS]: 'Per Column',
+  [keys.COLUMN_FILTERS]: 'Filter Per Column',
   [keys.COLUMNS]: 'Columns',
   [keys.CONFIRMATION]: 'Confirmation',
   [keys.CONFIRMATION_ACTIVATE_USER]: 'Are you sure you want to activate the user?',
@@ -293,7 +293,7 @@ const messages_en = {
   [keys.FREEZES]: 'Freezes',
   [keys.GENERATED_CODE_PREFIX]: 'Generated code prefix',
   [keys.GENERATE_CODES]: 'Generate Codes',
-  [keys.GLOBAL_FILTER]: 'Global',
+  [keys.GLOBAL_FILTER]: 'Global Filter',
   [keys.GROUPS]: 'Groups',
   [keys.GROUP]: 'Group',
   [keys.HIDE]: 'hide',
diff --git a/openbis_ng_ui/src/js/components/common/form/RadioGroupField.jsx b/openbis_ng_ui/src/js/components/common/form/RadioGroupField.jsx
new file mode 100644
index 00000000000..c269d191254
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/common/form/RadioGroupField.jsx
@@ -0,0 +1,164 @@
+import React from 'react'
+import { withStyles } from '@material-ui/core/styles'
+import Radio from '@material-ui/core/Radio'
+import Typography from '@material-ui/core/Typography'
+import FormFieldContainer from '@src/js/components/common/form/FormFieldContainer.jsx'
+import FormFieldLabel from '@src/js/components/common/form/FormFieldLabel.jsx'
+import logger from '@src/js/common/logger.js'
+
+const styles = theme => ({
+  container: {},
+  radioContainer: {
+    display: 'flex',
+    padding: `${theme.spacing(1) / 2}px 0px`,
+    '&:first-child': {
+      paddingTop: 0
+    },
+    '&:last-child': {
+      paddingBottom: 0
+    }
+  },
+  radio: {
+    padding: '2px',
+    marginRight: '4px'
+  },
+  label: {
+    cursor: 'pointer'
+  },
+  labelDisabled: {
+    cursor: 'inherit'
+  }
+})
+
+class RadioGroupField extends React.PureComponent {
+  static defaultProps = {
+    mode: 'edit'
+  }
+
+  constructor(props) {
+    super(props)
+
+    const references = {}
+    if (props.options) {
+      props.options.forEach(option => {
+        if (option.value) {
+          references[option.value] = React.createRef()
+        }
+      })
+    }
+
+    this.references = references
+    this.action = null
+    this.handleLabelClick = this.handleLabelClick.bind(this)
+    this.handleChange = this.handleChange.bind(this)
+    this.handleFocus = this.handleFocus.bind(this)
+  }
+
+  handleLabelClick(value) {
+    const reference = this.getReference(value)
+    if (reference && reference.current) {
+      reference.current.click()
+    }
+  }
+
+  handleChange(event) {
+    this.handleEvent(event, this.props.onChange)
+  }
+
+  handleFocus(event) {
+    this.handleEvent(event, this.props.onFocus)
+    if (this.action) {
+      this.action.focusVisible()
+    }
+  }
+
+  handleEvent(event, handler) {
+    const { name } = this.props
+
+    if (handler) {
+      const newEvent = {
+        ...event,
+        target: {
+          ...event.target,
+          name: name,
+          value: event.target.value
+        }
+      }
+      delete newEvent.target.checked
+      handler(newEvent)
+    }
+  }
+
+  render() {
+    logger.log(logger.DEBUG, 'RadioGroupField.render')
+
+    const {
+      description,
+      options,
+      error,
+      metadata,
+      mode,
+      styles,
+      classes,
+      onClick
+    } = this.props
+
+    if (mode !== 'view' && mode !== 'edit') {
+      throw 'Unsupported mode: ' + mode
+    }
+
+    return (
+      <FormFieldContainer
+        description={description}
+        error={error}
+        metadata={metadata}
+        styles={styles}
+        onClick={onClick}
+      >
+        <div className={classes.container}>
+          {options.map(option => this.renderOption(option))}
+        </div>
+      </FormFieldContainer>
+    )
+  }
+
+  renderOption(option) {
+    const { value, disabled, mode, styles, classes, onClick } = this.props
+
+    const isDisabled = disabled || mode !== 'edit'
+
+    return (
+      <div key={option.value} className={classes.radioContainer}>
+        <Radio
+          inputRef={this.getReference(option.value)}
+          action={action => (this.action = action)}
+          value={option.value}
+          checked={option.value === value}
+          disabled={isDisabled}
+          onChange={this.handleChange}
+          onFocus={this.handleFocus}
+          onClick={onClick}
+          classes={{ root: classes.radio }}
+          size='small'
+        />
+        <Typography
+          component='label'
+          className={isDisabled ? classes.labelDisabled : classes.label}
+          onClick={() => this.handleLabelClick(option.value)}
+        >
+          <FormFieldLabel
+            label={option.label}
+            styles={styles}
+            onClick={onClick}
+          />
+        </Typography>
+      </div>
+    )
+  }
+
+  getReference(value) {
+    return this.references[value]
+  }
+}
+
+export default withStyles(styles)(RadioGroupField)
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 a473211dc4d..1a778a577d8 100644
--- a/openbis_ng_ui/src/js/components/common/grid/Grid.jsx
+++ b/openbis_ng_ui/src/js/components/common/grid/Grid.jsx
@@ -14,7 +14,7 @@ import GridRow from '@src/js/components/common/grid/GridRow.jsx'
 import GridActions from '@src/js/components/common/grid/GridActions.jsx'
 import GridExports from '@src/js/components/common/grid/GridExports.jsx'
 import GridPaging from '@src/js/components/common/grid/GridPaging.jsx'
-import GridConfig from '@src/js/components/common/grid/GridConfig.jsx'
+import GridColumnsConfig from '@src/js/components/common/grid/GridColumnsConfig.jsx'
 import GridFiltersConfig from '@src/js/components/common/grid/GridFiltersConfig.jsx'
 import ComponentContext from '@src/js/components/common/ComponentContext.js'
 import logger from '@src/js/common/logger.js'
@@ -52,9 +52,6 @@ const styles = theme => ({
     zIndex: '200',
     backgroundColor: theme.palette.background.paper
   },
-  tableBody: {
-    '& tr:last-child td': {}
-  },
   tableFooter: {
     position: 'sticky',
     bottom: 0,
@@ -180,9 +177,7 @@ class Grid extends React.PureComponent {
                                 this.controller.handlePageSizeChange
                               }
                             />
-                            <GridConfig
-                              filterModes={filterModes}
-                              filterMode={filterMode}
+                            <GridColumnsConfig
                               columns={allColumns}
                               columnsVisibility={columnsVisibility}
                               loading={loading}
@@ -192,22 +187,11 @@ class Grid extends React.PureComponent {
                               onOrderChange={
                                 this.controller.handleColumnOrderChange
                               }
-                              onFilterModeChange={
-                                this.controller.handleFilterModeChange
-                              }
                             />
                             <GridFiltersConfig
                               filterModes={filterModes}
                               filterMode={filterMode}
-                              columns={allColumns}
-                              columnsVisibility={columnsVisibility}
                               loading={loading}
-                              onVisibleChange={
-                                this.controller.handleColumnVisibleChange
-                              }
-                              onOrderChange={
-                                this.controller.handleColumnOrderChange
-                              }
                               onFilterModeChange={
                                 this.controller.handleFilterModeChange
                               }
diff --git a/openbis_ng_ui/src/js/components/common/grid/GridConfig.jsx b/openbis_ng_ui/src/js/components/common/grid/GridColumnsConfig.jsx
similarity index 76%
rename from openbis_ng_ui/src/js/components/common/grid/GridConfig.jsx
rename to openbis_ng_ui/src/js/components/common/grid/GridColumnsConfig.jsx
index fa2d6a89133..3c6659d5d8c 100644
--- a/openbis_ng_ui/src/js/components/common/grid/GridConfig.jsx
+++ b/openbis_ng_ui/src/js/components/common/grid/GridColumnsConfig.jsx
@@ -3,13 +3,11 @@ import React from 'react'
 import autoBind from 'auto-bind'
 import { withStyles } from '@material-ui/core/styles'
 import { DragDropContext, Droppable } from 'react-beautiful-dnd'
-import Button from '@src/js/components/common/form/Button.jsx'
+import Popover from '@material-ui/core/Popover'
 import Mask from '@src/js/components/common/loading/Mask.jsx'
 import Container from '@src/js/components/common/form/Container.jsx'
-import Link from '@src/js/components/common/form/Link.jsx'
-import SettingsIcon from '@material-ui/icons/Settings'
-import GridConfigRow from '@src/js/components/common/grid/GridConfigRow.jsx'
-import Popover from '@material-ui/core/Popover'
+import Button from '@src/js/components/common/form/Button.jsx'
+import GridColumnsConfigRow from '@src/js/components/common/grid/GridColumnsConfigRow.jsx'
 import messages from '@src/js/common/messages.js'
 import logger from '@src/js/common/logger.js'
 
@@ -19,17 +17,20 @@ const styles = theme => ({
     alignItems: 'center',
     paddingRight: theme.spacing(1)
   },
-  filters: {
-    paddingBottom: theme.spacing(1)
-  },
-  columnsList: {
+  columns: {
     listStyle: 'none',
     margin: 0,
     padding: 0
+  },
+  buttons: {
+    marginBottom: theme.spacing(1),
+    '& button': {
+      marginRight: theme.spacing(1)
+    }
   }
 })
 
-class GridConfig extends React.PureComponent {
+class GridColumnsConfig extends React.PureComponent {
   constructor(props) {
     super(props)
     autoBind(this)
@@ -77,7 +78,7 @@ class GridConfig extends React.PureComponent {
   }
 
   render() {
-    logger.log(logger.DEBUG, 'GridConfig.render')
+    logger.log(logger.DEBUG, 'GridColumnsConfig.render')
 
     const { classes, loading } = this.props
     const { el } = this.state
@@ -85,13 +86,11 @@ class GridConfig extends React.PureComponent {
     return (
       <div className={classes.container}>
         <Button
-          label='Columns'
+          label={messages.get(messages.COLUMNS)}
           color='default'
           variant='outlined'
           onClick={this.handleOpen}
-        >
-          <SettingsIcon fontSize='small' />
-        </Button>
+        />
         <Popover
           open={Boolean(el)}
           anchorEl={el}
@@ -106,7 +105,7 @@ class GridConfig extends React.PureComponent {
           }}
         >
           <Mask visible={loading}>
-            <Container>{this.renderColumns()}</Container>
+            <Container square={true}>{this.renderColumns()}</Container>
           </Mask>
         </Popover>
       </div>
@@ -117,16 +116,26 @@ class GridConfig extends React.PureComponent {
     const { classes, columns, columnsVisibility, onVisibleChange } = this.props
     return (
       <div>
+        <div className={classes.buttons}>
+          <Button
+            label={messages.get(messages.SHOW_ALL)}
+            onClick={this.handleShowAll}
+          />
+          <Button
+            label={messages.get(messages.HIDE_ALL)}
+            onClick={this.handleHideAll}
+          />
+        </div>
         <DragDropContext onDragEnd={this.handleDragEnd}>
           <Droppable droppableId='root'>
             {provided => (
               <ol
                 ref={provided.innerRef}
                 {...provided.droppableProps}
-                className={classes.columnsList}
+                className={classes.columns}
               >
                 {columns.map((column, index) => (
-                  <GridConfigRow
+                  <GridColumnsConfigRow
                     key={column.name}
                     column={column}
                     visible={columnsVisibility[column.name]}
@@ -139,19 +148,9 @@ class GridConfig extends React.PureComponent {
             )}
           </Droppable>
         </DragDropContext>
-        <br />
-        <Button
-          label={messages.get(messages.SHOW_ALL)}
-          onClick={this.handleShowAll}
-        />
-        <span>&nbsp;</span>
-        <Button
-          label={messages.get(messages.HIDE_ALL)}
-          onClick={this.handleHideAll}
-        />
       </div>
     )
   }
 }
 
-export default _.flow(withStyles(styles))(GridConfig)
+export default _.flow(withStyles(styles))(GridColumnsConfig)
diff --git a/openbis_ng_ui/src/js/components/common/grid/GridConfigRow.jsx b/openbis_ng_ui/src/js/components/common/grid/GridColumnsConfigRow.jsx
similarity index 85%
rename from openbis_ng_ui/src/js/components/common/grid/GridConfigRow.jsx
rename to openbis_ng_ui/src/js/components/common/grid/GridColumnsConfigRow.jsx
index bc8e62ceed2..d70a2b119ca 100644
--- a/openbis_ng_ui/src/js/components/common/grid/GridConfigRow.jsx
+++ b/openbis_ng_ui/src/js/components/common/grid/GridColumnsConfigRow.jsx
@@ -9,7 +9,13 @@ const styles = theme => ({
   row: {
     display: 'flex',
     alignItems: 'center',
-    padding: `${theme.spacing(1) / 2}px 0px`
+    padding: `${theme.spacing(1) / 2}px 0px`,
+    '&:first-child': {
+      paddingTop: 0
+    },
+    '&:last-child': {
+      paddingBottom: 0
+    }
   },
   label: {
     marginLeft: 0
@@ -20,7 +26,7 @@ const styles = theme => ({
   }
 })
 
-class GridConfigRow extends React.PureComponent {
+class GridColumnsConfigRow extends React.PureComponent {
   constructor(props) {
     super(props)
     this.handleVisibleChange = this.handleVisibleChange.bind(this)
@@ -61,4 +67,4 @@ class GridConfigRow extends React.PureComponent {
   }
 }
 
-export default withStyles(styles)(GridConfigRow)
+export default withStyles(styles)(GridColumnsConfigRow)
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 e919b40a69c..6826c69ad64 100644
--- a/openbis_ng_ui/src/js/components/common/grid/GridExports.jsx
+++ b/openbis_ng_ui/src/js/components/common/grid/GridExports.jsx
@@ -12,14 +12,23 @@ import logger from '@src/js/common/logger.js'
 
 const styles = theme => ({
   container: {
-    padding: theme.spacing(1),
-    paddingLeft: theme.spacing(1)
+    display: 'flex',
+    alignItems: 'center'
   },
   popup: {
     maxWidth: '300px'
   },
   field: {
-    paddingBottom: theme.spacing(1)
+    paddingBottom: theme.spacing(1),
+    '&:first-child': {
+      paddingTop: 0
+    },
+    '&:last-child': {
+      paddingBottom: 0
+    }
+  },
+  button: {
+    paddingTop: theme.spacing(1)
   }
 })
 
@@ -113,63 +122,65 @@ class GridExports extends React.PureComponent {
             horizontal: 'left'
           }}
         >
-          <Container className={classes.popup}>
-            <div className={classes.field}>
-              <SelectField
-                label={messages.get(messages.COLUMNS)}
-                name='columns'
-                options={[
-                  {
-                    label: messages.get(messages.ALL_COLUMNS),
-                    value: GridExportOptions.ALL_COLUMNS
-                  },
-                  {
-                    label: messages.get(messages.VISIBLE_COLUMNS),
-                    value: GridExportOptions.VISIBLE_COLUMNS
-                  }
-                ]}
-                value={exportOptions.columns}
-                variant='standard'
-                onChange={this.handleChange}
-              />
-            </div>
-            <div className={classes.field}>
-              <SelectField
-                label={messages.get(messages.ROWS)}
-                name='rows'
-                options={rowsOptions}
-                value={exportOptions.rows}
-                variant='standard'
-                onChange={this.handleChange}
-              />
-            </div>
-            <div className={classes.field}>
-              <SelectField
-                label={messages.get(messages.VALUES)}
-                name='values'
-                options={[
-                  {
-                    label: messages.get(messages.PLAIN_TEXT),
-                    value: GridExportOptions.PLAIN_TEXT
-                  },
-                  {
-                    label: messages.get(messages.RICH_TEXT),
-                    value: GridExportOptions.RICH_TEXT
-                  }
-                ]}
-                value={exportOptions.values}
-                variant='standard'
-                onChange={this.handleChange}
-              />
-            </div>
-            {exportOptions.values === GridExportOptions.PLAIN_TEXT && (
+          <Container square={true} className={classes.popup}>
+            <div>
+              <div className={classes.field}>
+                <SelectField
+                  label={messages.get(messages.COLUMNS)}
+                  name='columns'
+                  options={[
+                    {
+                      label: messages.get(messages.ALL_COLUMNS),
+                      value: GridExportOptions.ALL_COLUMNS
+                    },
+                    {
+                      label: messages.get(messages.VISIBLE_COLUMNS),
+                      value: GridExportOptions.VISIBLE_COLUMNS
+                    }
+                  ]}
+                  value={exportOptions.columns}
+                  variant='standard'
+                  onChange={this.handleChange}
+                />
+              </div>
               <div className={classes.field}>
-                <Message type='warning'>
-                  {messages.get(messages.EXPORT_PLAIN_TEXT_WARNING)}
-                </Message>
+                <SelectField
+                  label={messages.get(messages.ROWS)}
+                  name='rows'
+                  options={rowsOptions}
+                  value={exportOptions.rows}
+                  variant='standard'
+                  onChange={this.handleChange}
+                />
               </div>
-            )}
-            <div className={classes.field}>
+              <div className={classes.field}>
+                <SelectField
+                  label={messages.get(messages.VALUES)}
+                  name='values'
+                  options={[
+                    {
+                      label: messages.get(messages.PLAIN_TEXT),
+                      value: GridExportOptions.PLAIN_TEXT
+                    },
+                    {
+                      label: messages.get(messages.RICH_TEXT),
+                      value: GridExportOptions.RICH_TEXT
+                    }
+                  ]}
+                  value={exportOptions.values}
+                  variant='standard'
+                  onChange={this.handleChange}
+                />
+              </div>
+              {exportOptions.values === GridExportOptions.PLAIN_TEXT && (
+                <div className={classes.field}>
+                  <Message type='warning'>
+                    {messages.get(messages.EXPORT_PLAIN_TEXT_WARNING)}
+                  </Message>
+                </div>
+              )}
+            </div>
+            <div className={classes.button}>
               <Button
                 label={messages.get(messages.EXPORT)}
                 type='neutral'
diff --git a/openbis_ng_ui/src/js/components/common/grid/GridFiltersConfig.jsx b/openbis_ng_ui/src/js/components/common/grid/GridFiltersConfig.jsx
new file mode 100644
index 00000000000..bb9a5fbbf70
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/common/grid/GridFiltersConfig.jsx
@@ -0,0 +1,121 @@
+import _ from 'lodash'
+import React from 'react'
+import autoBind from 'auto-bind'
+import { withStyles } from '@material-ui/core/styles'
+import Popover from '@material-ui/core/Popover'
+import Mask from '@src/js/components/common/loading/Mask.jsx'
+import Container from '@src/js/components/common/form/Container.jsx'
+import RadioGroupField from '@src/js/components/common/form/RadioGroupField.jsx'
+import Button from '@src/js/components/common/form/Button.jsx'
+import GridFilterOptions from '@src/js/components/common/grid/GridFilterOptions.js'
+import messages from '@src/js/common/messages.js'
+import logger from '@src/js/common/logger.js'
+
+const styles = theme => ({
+  container: {
+    display: 'flex',
+    alignItems: 'center',
+    paddingRight: theme.spacing(1)
+  }
+})
+
+class GridFiltersConfig extends React.PureComponent {
+  constructor(props) {
+    super(props)
+    autoBind(this)
+    this.state = {
+      el: null
+    }
+  }
+
+  handleOpen(event) {
+    this.setState({
+      el: event.currentTarget
+    })
+  }
+
+  handleClose() {
+    this.setState({
+      el: null
+    })
+  }
+
+  handleFilterModeChange(event) {
+    const { onFilterModeChange } = this.props
+    if (onFilterModeChange) {
+      onFilterModeChange(event.target.value)
+    }
+  }
+
+  render() {
+    logger.log(logger.DEBUG, 'GridFiltersConfig.render')
+
+    const { filterModes, loading, classes } = this.props
+    const { el } = this.state
+
+    if (filterModes && filterModes.length <= 1) {
+      return null
+    }
+
+    return (
+      <div className={classes.container}>
+        <Button
+          label={messages.get(messages.FILTERS)}
+          color='default'
+          variant='outlined'
+          onClick={this.handleOpen}
+        />
+        <Popover
+          open={Boolean(el)}
+          anchorEl={el}
+          onClose={this.handleClose}
+          anchorOrigin={{
+            vertical: 'bottom',
+            horizontal: 'left'
+          }}
+          transformOrigin={{
+            vertical: 'top',
+            horizontal: 'left'
+          }}
+        >
+          <Mask visible={loading}>
+            <Container square={true}>{this.renderFilters()}</Container>
+          </Mask>
+        </Popover>
+      </div>
+    )
+  }
+
+  renderFilters() {
+    const { filterModes, filterMode } = this.props
+
+    const allOptions = [
+      {
+        value: GridFilterOptions.COLUMN_FILTERS,
+        label: messages.get(messages.COLUMN_FILTERS)
+      },
+      {
+        value: GridFilterOptions.GLOBAL_FILTER,
+        label: messages.get(messages.GLOBAL_FILTER)
+      }
+    ]
+
+    const chosenOptions = allOptions.filter(
+      option =>
+        filterModes === null ||
+        filterModes === undefined ||
+        filterModes.includes(option.value)
+    )
+
+    return (
+      <RadioGroupField
+        name='filterMode'
+        value={filterMode}
+        options={chosenOptions}
+        onChange={this.handleFilterModeChange}
+      />
+    )
+  }
+}
+
+export default _.flow(withStyles(styles))(GridFiltersConfig)
-- 
GitLab