diff --git a/src/components/ColumnsConfiguration/ColumnConfigurationItem.react.js b/src/components/ColumnsConfiguration/ColumnConfigurationItem.react.js new file mode 100644 index 0000000000..a681b5d0f6 --- /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)}> +
+ +
+
{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..b56f1466b8 --- /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..046621f7cf --- /dev/null +++ b/src/components/ColumnsConfiguration/ColumnsConfiguration.scss @@ -0,0 +1,75 @@ +@import 'stylesheets/globals.scss'; + +.entry { + display: inline-block; + height: 14px; + padding: 0 8px; + + svg { + fill: #66637A; + } + + &:hover svg { + fill: white; + } +} + +.title { + margin-top: -4px; + background: #797691; + padding: 4px 8px; + border-radius: 5px 5px 0 0; + + svg { + fill: white; + } +} + +.entry, .title { + @include NotoSansFont; + font-size: 14px; + color: #ffffff; + cursor: pointer; + + svg { + vertical-align: middle; + margin-right: 4px; + } + + span { + vertical-align: middle; + line-height: 14px; + } +} + +.body { + color: white; + position: absolute; + top: 22px; + 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/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/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..a11664dd60 --- /dev/null +++ b/src/icons/manage-columns.svg @@ -0,0 +1,3 @@ + + + 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 { 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 }] ); }); });