From ad6ea75d171d64236a04efe5eba3ddcad6708841 Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Tue, 20 Aug 2019 18:54:59 -0300 Subject: [PATCH 1/4] feat: Column configuration component Allows users to configure the columns visibility and order though a single component. Closes #106 Closes #138 --- .../ColumnConfigurationItem.react.js | 43 +++++++ .../ColumnConfigurationItem.scss | 25 ++++ .../ColumnsConfiguration.react.js | 109 ++++++++++++++++++ .../ColumnsConfiguration.scss | 81 +++++++++++++ .../DataBrowserHeaderBar.react.js | 3 +- .../Data/Browser/BrowserTable.react.js | 16 +-- .../Data/Browser/BrowserToolbar.react.js | 10 ++ .../Data/Browser/DataBrowser.react.js | 9 ++ src/icons/drag-indicator.svg | 3 + src/icons/manage-columns.svg | 7 ++ src/icons/visibility.svg | 3 + src/icons/visibility_off.svg | 3 + src/lib/ColumnPreferences.js | 13 ++- 13 files changed, 314 insertions(+), 11 deletions(-) create mode 100644 src/components/ColumnsConfiguration/ColumnConfigurationItem.react.js create mode 100644 src/components/ColumnsConfiguration/ColumnConfigurationItem.scss create mode 100644 src/components/ColumnsConfiguration/ColumnsConfiguration.react.js create mode 100644 src/components/ColumnsConfiguration/ColumnsConfiguration.scss create mode 100644 src/icons/drag-indicator.svg create mode 100644 src/icons/manage-columns.svg create mode 100644 src/icons/visibility.svg create mode 100644 src/icons/visibility_off.svg diff --git a/src/components/ColumnsConfiguration/ColumnConfigurationItem.react.js b/src/components/ColumnsConfiguration/ColumnConfigurationItem.react.js new file mode 100644 index 0000000000..3c040ea7ea --- /dev/null +++ b/src/components/ColumnsConfiguration/ColumnConfigurationItem.react.js @@ -0,0 +1,43 @@ +import React from 'react'; +import { useDrag, useDrop } from 'react-dnd'; + +import Icon from 'components/Icon/Icon.react'; +import styles from 'components/ColumnsConfiguration/ColumnConfigurationItem.scss'; + +const DND_TYPE = 'ColumnConfigurationItem'; + +export default ({ name, handleColumnDragDrop, index, onChangeVisible, visible }) => { + const [ { isDragging}, drag ] = useDrag({ + item: { type: DND_TYPE, index }, + collect: monitor => ({ isDragging: !!monitor.isDragging() }) + }); + + const [ { canDrop, isOver }, drop ] = useDrop({ + accept: DND_TYPE, + drop: item => handleColumnDragDrop(item.index, index), + canDrop: item => item.index !== index, + collect: monitor => ({ + isOver: !!monitor.isOver(), + canDrop: !!monitor.canDrop() + }) + }); + + return drag(drop( +
onChangeVisible(!visible)}> +
+ +
+
14 ? name : undefined}>{name}
+
+ +
+
+ )); +}; \ No newline at end of file diff --git a/src/components/ColumnsConfiguration/ColumnConfigurationItem.scss b/src/components/ColumnsConfiguration/ColumnConfigurationItem.scss new file mode 100644 index 0000000000..68400d4ae6 --- /dev/null +++ b/src/components/ColumnsConfiguration/ColumnConfigurationItem.scss @@ -0,0 +1,25 @@ +.columnConfigItem { + padding: 8px 10px; + display: flex; + justify-content: space-between; + border-radius: 5px; + cursor: grab; +} + +.icon { + display: flex; + align-items: center; + height: 24px; +} + +.visibilityIcon { + cursor: pointer; + width: 30px; +} + +.columnConfigItemName { + width: 110px; + text-overflow: ellipsis; + overflow: hidden; + line-height: 24px; +} \ No newline at end of file diff --git a/src/components/ColumnsConfiguration/ColumnsConfiguration.react.js b/src/components/ColumnsConfiguration/ColumnsConfiguration.react.js new file mode 100644 index 0000000000..890af9782f --- /dev/null +++ b/src/components/ColumnsConfiguration/ColumnsConfiguration.react.js @@ -0,0 +1,109 @@ +import React from 'react'; +import { DndProvider } from 'react-dnd' +import HTML5Backend from 'react-dnd-html5-backend' +import ReactDOM from 'react-dom'; + +import Button from 'components/Button/Button.react'; +import ColumnConfigurationItem from 'components/ColumnsConfiguration/ColumnConfigurationItem.react'; +import styles from 'components/ColumnsConfiguration/ColumnsConfiguration.scss'; +import Icon from 'components/Icon/Icon.react'; +import Popover from 'components/Popover/Popover.react'; +import Position from 'lib/Position'; + +export default class ColumnsConfiguration extends React.Component { + constructor() { + super(); + + this.state = { + open: false + }; + } + + componentDidMount() { + this.node = ReactDOM.findDOMNode(this); + } + + componentWillReceiveProps(props) { + if (props.schema !== this.props.schema) { + this.setState({ + open: false + }); + } + } + + toggle() { + this.setState({ + open: !this.state.open + }) + } + + showAll() { + this.props.handleColumnsOrder(this.props.order.map(order => ({ ...order, visible: true }))); + } + + hideAll() { + this.props.handleColumnsOrder(this.props.order.map(order => ({ ...order, visible: false }))); + } + + render() { + const { handleColumnDragDrop, handleColumnsOrder, order } = this.props; + const [ title, entry ] = [styles.title, styles.entry ].map(className => ( +
+ + Manage Columns +
+ )); + + let popover = null; + if (this.state.open) { + popover = ( + +
+ {title} +
+
+ + {order.map(({ name, visible, ...rest }, index) => { + return { + const updatedOrder = [...order]; + updatedOrder[index] = { + ...rest, + name, + visible + }; + handleColumnsOrder(updatedOrder); + }} + handleColumnDragDrop={handleColumnDragDrop} /> + })} + +
+
+
+
+
+
+ ); + } + return ( +
+ {entry} + {popover} +
+ ); + } +} diff --git a/src/components/ColumnsConfiguration/ColumnsConfiguration.scss b/src/components/ColumnsConfiguration/ColumnsConfiguration.scss new file mode 100644 index 0000000000..951be76f66 --- /dev/null +++ b/src/components/ColumnsConfiguration/ColumnsConfiguration.scss @@ -0,0 +1,81 @@ +@import 'stylesheets/globals.scss'; + +.wrap { + display: inline-block; +} + +.entry { + height: 30px; + padding: 8px; + + svg { + fill: #66637A; + } + + &:hover svg { + fill: white; + } +} + +.title { + background: #797691; + padding: 8px; + border-radius: 5px 5px 0 0; + + svg { + fill: white; + } +} + +.entry, .title { + @include NotoSansFont; + position: relative; + bottom: -4px; + font-size: 14px; + color: #ffffff; + cursor: pointer; + + svg { + vertical-align: top; + margin-right: 6px; + } + + span { + display: inline-block; + vertical-align: top; + height: 14px; + line-height: 14px; + } +} + +.body { + color: white; + position: absolute; + top: 30px; + right: 0; + border-radius: 5px 0 5px 5px; + background: #797691; + width: 220px; + font-size: 14px; + + .columnConfigContainer { + max-height: calc(100vh - 180px); + overflow: auto; + margin: 10px; + } +} + +.footer { + background: rgba(0,0,0,0.2); + padding: 15px 20px; + display: flex; + justify-content: space-between; + + > a { + margin-right: 10px; + + &:last-child { + margin-right: 0; + } + } +} \ No newline at end of file diff --git a/src/components/DataBrowserHeaderBar/DataBrowserHeaderBar.react.js b/src/components/DataBrowserHeaderBar/DataBrowserHeaderBar.react.js index e63a9a208e..ed0b411696 100644 --- a/src/components/DataBrowserHeaderBar/DataBrowserHeaderBar.react.js +++ b/src/components/DataBrowserHeaderBar/DataBrowserHeaderBar.react.js @@ -27,7 +27,8 @@ export default class DataBrowserHeaderBar extends React.Component { ]; - headers.forEach(({ width, name, type, targetClass, order }, i) => { + headers.forEach(({ width, name, type, targetClass, order, visible }, i) => { + if (!visible) return; let wrapStyle = { width }; if (i % 2) { wrapStyle.background = '#726F85'; diff --git a/src/dashboard/Data/Browser/BrowserTable.react.js b/src/dashboard/Data/Browser/BrowserTable.react.js index 21d70ade19..7c60289361 100644 --- a/src/dashboard/Data/Browser/BrowserTable.react.js +++ b/src/dashboard/Data/Browser/BrowserTable.react.js @@ -89,7 +89,8 @@ export default class BrowserTable extends React.Component { checked={this.props.selection['*'] || this.props.selection[obj.id]} onChange={(e) => this.props.selectRow(obj.id, e.target.checked)} /> - {this.props.order.map(({ name, width }, j) => { + {this.props.order.map(({ name, width, visible }, j) => { + if (!visible) return null; let type = this.props.columns[name].type; let attr = obj; if (!this.props.isUnique) { @@ -147,22 +148,23 @@ export default class BrowserTable extends React.Component { } } - let headers = this.props.order.map(({ name, width }) => ( + let headers = this.props.order.map(({ name, width, visible }) => ( { width: width, name: name, type: this.props.columns[name].type, targetClass: this.props.columns[name].targetClass, - order: ordering.col === name ? ordering.direction : null + order: ordering.col === name ? ordering.direction : null, + visible } )); let editor = null; let table =
; if (this.props.data) { - let rowWidth = 210; - for (let i = 0; i < this.props.order.length; i++) { - rowWidth += this.props.order[i].width; - } + const rowWidth = this.props.order.reduce( + (rowWidth, { visible, width }) => visible ? rowWidth + width : rowWidth, + 210 + ); let newRow = null; if (this.props.newObject && this.state.offset <= 0) { newRow = ( diff --git a/src/dashboard/Data/Browser/BrowserToolbar.react.js b/src/dashboard/Data/Browser/BrowserToolbar.react.js index 67fd5cc4b9..331a9f4d67 100644 --- a/src/dashboard/Data/Browser/BrowserToolbar.react.js +++ b/src/dashboard/Data/Browser/BrowserToolbar.react.js @@ -7,6 +7,8 @@ */ import BrowserFilter from 'components/BrowserFilter/BrowserFilter.react'; import BrowserMenu from 'components/BrowserMenu/BrowserMenu.react'; +import ColumnsConfiguration + from 'components/ColumnsConfiguration/ColumnsConfiguration.react'; import Icon from 'components/Icon/Icon.react'; import MenuItem from 'components/BrowserMenu/MenuItem.react'; import prettyNumber from 'lib/prettyNumber'; @@ -42,6 +44,9 @@ let BrowserToolbar = ({ onRefresh, hidePerms, isUnique, + handleColumnDragDrop, + handleColumnsOrder, + order, enableDeleteAllRows, enableExportClass, @@ -150,6 +155,11 @@ let BrowserToolbar = ({ Add Row
+ +
Refresh diff --git a/src/dashboard/Data/Browser/DataBrowser.react.js b/src/dashboard/Data/Browser/DataBrowser.react.js index bed0253267..c9bacb32a5 100644 --- a/src/dashboard/Data/Browser/DataBrowser.react.js +++ b/src/dashboard/Data/Browser/DataBrowser.react.js @@ -188,6 +188,12 @@ export default class DataBrowser extends React.Component { } } + handleColumnsOrder(order) { + this.setState({ order }, () => { + this.updatePreferences(order); + }); + } + render() { let { className, ...other } = this.props; const { preventSchemaEdits } = this.context.currentApp; @@ -213,6 +219,9 @@ export default class DataBrowser extends React.Component { enableSecurityDialog={this.context.currentApp.serverInfo.features.schemas.editClassLevelPermissions && !preventSchemaEdits} enableColumnManipulation={!preventSchemaEdits} enableClassManipulation={!preventSchemaEdits} + handleColumnDragDrop={this.handleHeaderDragDrop.bind(this)} + handleColumnsOrder={this.handleColumnsOrder.bind(this)} + order={this.state.order} {...other}/>
); diff --git a/src/icons/drag-indicator.svg b/src/icons/drag-indicator.svg new file mode 100644 index 0000000000..cdfd8a21b1 --- /dev/null +++ b/src/icons/drag-indicator.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/manage-columns.svg b/src/icons/manage-columns.svg new file mode 100644 index 0000000000..0e07d7a3b0 --- /dev/null +++ b/src/icons/manage-columns.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/icons/visibility.svg b/src/icons/visibility.svg new file mode 100644 index 0000000000..59f51a67ac --- /dev/null +++ b/src/icons/visibility.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/visibility_off.svg b/src/icons/visibility_off.svg new file mode 100644 index 0000000000..892a3a0dfb --- /dev/null +++ b/src/icons/visibility_off.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/lib/ColumnPreferences.js b/src/lib/ColumnPreferences.js index da5fbd2de6..e43768a620 100644 --- a/src/lib/ColumnPreferences.js +++ b/src/lib/ColumnPreferences.js @@ -72,7 +72,7 @@ export function getColumnSort(sortBy, appId, className) { } export function getOrder(cols, appId, className) { - let prefs = getPreferences(appId, className) || [ { name: 'objectId', width: DEFAULT_WIDTH } ]; + let prefs = getPreferences(appId, className) || [ { name: 'objectId', width: DEFAULT_WIDTH, visible: true } ]; let order = [].concat(prefs); let seen = {}; for (let i = 0; i < order.length; i++) { @@ -83,14 +83,21 @@ export function getOrder(cols, appId, className) { for (let name in cols) { requested[name] = true; if (!seen[name]) { - order.push({ name: name, width: DEFAULT_WIDTH }); + order.push({ name: name, width: DEFAULT_WIDTH, visible: true }); seen[name] = true; updated = true; } } let filtered = []; for (let i = 0; i < order.length; i++) { - let name = order[i].name; + const { name, visible } = order[i]; + + // If "visible" attribute is not defined, sets to true + // and updates the cached preferences. + if (typeof visible === 'undefined') { + order[i].visible = true; + updated = true; + } if (requested[name]) { filtered.push(order[i]); } else { From 4a535bc0fcea882aef8734453588c08cf801d5c4 Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Wed, 21 Aug 2019 10:20:11 -0300 Subject: [PATCH 2/4] fix: Simplify components --- .../ColumnConfigurationItem.react.js | 6 ++--- .../ColumnsConfiguration.react.js | 4 ++-- .../ColumnsConfiguration.scss | 24 +++++++------------ src/components/Popover/Popover.react.js | 2 +- src/icons/manage-columns.svg | 8 ++----- 5 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/components/ColumnsConfiguration/ColumnConfigurationItem.react.js b/src/components/ColumnsConfiguration/ColumnConfigurationItem.react.js index 3c040ea7ea..a681b5d0f6 100644 --- a/src/components/ColumnsConfiguration/ColumnConfigurationItem.react.js +++ b/src/components/ColumnsConfiguration/ColumnConfigurationItem.react.js @@ -32,11 +32,11 @@ export default ({ name, handleColumnDragDrop, index, onChangeVisible, visible }) }} onClick={() => onChangeVisible(!visible)}>
- +
-
14 ? name : undefined}>{name}
+
{name}
- +
)); diff --git a/src/components/ColumnsConfiguration/ColumnsConfiguration.react.js b/src/components/ColumnsConfiguration/ColumnsConfiguration.react.js index 890af9782f..be706eb79c 100644 --- a/src/components/ColumnsConfiguration/ColumnsConfiguration.react.js +++ b/src/components/ColumnsConfiguration/ColumnsConfiguration.react.js @@ -100,10 +100,10 @@ export default class ColumnsConfiguration extends React.Component { ); } return ( -
+ <> {entry} {popover} -
+ ); } } diff --git a/src/components/ColumnsConfiguration/ColumnsConfiguration.scss b/src/components/ColumnsConfiguration/ColumnsConfiguration.scss index 951be76f66..046621f7cf 100644 --- a/src/components/ColumnsConfiguration/ColumnsConfiguration.scss +++ b/src/components/ColumnsConfiguration/ColumnsConfiguration.scss @@ -1,12 +1,9 @@ @import 'stylesheets/globals.scss'; -.wrap { - display: inline-block; -} - .entry { - height: 30px; - padding: 8px; + display: inline-block; + height: 14px; + padding: 0 8px; svg { fill: #66637A; @@ -18,8 +15,9 @@ } .title { + margin-top: -4px; background: #797691; - padding: 8px; + padding: 4px 8px; border-radius: 5px 5px 0 0; svg { @@ -29,21 +27,17 @@ .entry, .title { @include NotoSansFont; - position: relative; - bottom: -4px; font-size: 14px; color: #ffffff; cursor: pointer; svg { - vertical-align: top; - margin-right: 6px; + vertical-align: middle; + margin-right: 4px; } span { - display: inline-block; - vertical-align: top; - height: 14px; + vertical-align: middle; line-height: 14px; } } @@ -51,7 +45,7 @@ .body { color: white; position: absolute; - top: 30px; + top: 22px; right: 0; border-radius: 5px 0 5px 5px; background: #797691; diff --git a/src/components/Popover/Popover.react.js b/src/components/Popover/Popover.react.js index 1e8f01b5ee..b5a8821345 100644 --- a/src/components/Popover/Popover.react.js +++ b/src/components/Popover/Popover.react.js @@ -90,7 +90,7 @@ export default class Popover extends React.Component { } render() { - return
; + return null; } } diff --git a/src/icons/manage-columns.svg b/src/icons/manage-columns.svg index 0e07d7a3b0..a11664dd60 100644 --- a/src/icons/manage-columns.svg +++ b/src/icons/manage-columns.svg @@ -1,7 +1,3 @@ - - - - - - + + From 36b0cb5f5b5d4b2b8e756539efb172f378eecc8c Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Wed, 21 Aug 2019 10:39:14 -0300 Subject: [PATCH 3/4] fix: ColumnPreferences test --- src/lib/tests/ColumnPreferences.test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib/tests/ColumnPreferences.test.js b/src/lib/tests/ColumnPreferences.test.js index 749e077e2c..626433358a 100644 --- a/src/lib/tests/ColumnPreferences.test.js +++ b/src/lib/tests/ColumnPreferences.test.js @@ -43,17 +43,17 @@ describe('ColumnPreferences', () => { it('can retrive column orderings', () => { ColumnPreferences.updatePreferences([{ name: 'objectId', width: 100 }, { name: 'createdAt', width: 150 }], 'testapp', 'Klass'); expect(ColumnPreferences.getOrder({ objectId: {}, createdAt: {} }, 'testapp', 'Klass')).toEqual( - [{ name: 'objectId', width: 100 }, { name: 'createdAt', width: 150 }] + [{ name: 'objectId', width: 100, visible: true }, { name: 'createdAt', width: 150, visible: true }] ); }); it('tacks unknown columns onto the end', () => { ColumnPreferences.updatePreferences([{ name: 'objectId', width: 100 }, { name: 'createdAt', width: 150 }], 'testapp', 'Klass'); expect(ColumnPreferences.getOrder({ objectId: {}, updatedAt: {}, createdAt: {}, someField: {} }, 'testapp', 'Klass')).toEqual( - [{ name: 'objectId', width: 100 }, { name: 'createdAt', width: 150 }, { name: 'updatedAt', width: 150 }, { name: 'someField', width: 150 }] + [{ name: 'objectId', width: 100, visible: true }, { name: 'createdAt', width: 150, visible: true }, { name: 'updatedAt', width: 150, visible: true }, { name: 'someField', width: 150, visible: true }] ); expect(ColumnPreferences.getPreferences('testapp', 'Klass')).toEqual( - [{ name: 'objectId', width: 100 }, { name: 'createdAt', width: 150 }, { name: 'updatedAt', width: 150 }, { name: 'someField', width: 150 }] + [{ name: 'objectId', width: 100, visible: true }, { name: 'createdAt', width: 150, visible: true }, { name: 'updatedAt', width: 150, visible: true }, { name: 'someField', width: 150, visible: true }] ); }); @@ -64,17 +64,17 @@ describe('ColumnPreferences', () => { 'Klass' ); expect(ColumnPreferences.getOrder({ objectId: {}, createdAt: {}, updatedAt: {} }, 'testapp', 'Klass')).toEqual( - [{ name: 'objectId', width: 100 }, { name: 'createdAt', width: 150 }, { name: 'updatedAt', width: 150 }] + [{ name: 'objectId', width: 100, visible: true }, { name: 'createdAt', width: 150, visible: true }, { name: 'updatedAt', width: 150, visible: true }] ); expect(ColumnPreferences.getPreferences('testapp', 'Klass')).toEqual( - [{ name: 'objectId', width: 100 }, { name: 'createdAt', width: 150 }, { name: 'updatedAt', width: 150 }] + [{ name: 'objectId', width: 100, visible: true }, { name: 'createdAt', width: 150, visible: true }, { name: 'updatedAt', width: 150, visible: true }] ); expect(ColumnPreferences.getOrder({ objectId: {}, updatedAt: {}, someField: {} }, 'testapp', 'Klass')).toEqual( - [{ name: 'objectId', width: 100 }, { name: 'updatedAt', width: 150 }, { name: 'someField', width: 150 }] + [{ name: 'objectId', width: 100, visible: true }, { name: 'updatedAt', width: 150, visible: true }, { name: 'someField', width: 150, visible: true }] ); expect(ColumnPreferences.getPreferences('testapp', 'Klass')).toEqual( - [{ name: 'objectId', width: 100 }, { name: 'updatedAt', width: 150 }, { name: 'someField', width: 150 }] + [{ name: 'objectId', width: 100, visible: true }, { name: 'updatedAt', width: 150, visible: true }, { name: 'someField', width: 150, visible: true }] ); }); }); From 517ae298863feea4849dd81397eb7fbae14a400a Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Thu, 22 Aug 2019 10:54:28 -0300 Subject: [PATCH 4/4] fix: External click not closing component --- .../ColumnsConfiguration/ColumnsConfiguration.react.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ColumnsConfiguration/ColumnsConfiguration.react.js b/src/components/ColumnsConfiguration/ColumnsConfiguration.react.js index be706eb79c..b56f1466b8 100644 --- a/src/components/ColumnsConfiguration/ColumnsConfiguration.react.js +++ b/src/components/ColumnsConfiguration/ColumnsConfiguration.react.js @@ -57,7 +57,7 @@ export default class ColumnsConfiguration extends React.Component { let popover = null; if (this.state.open) { popover = ( - +
{title}