diff --git a/README.md b/README.md index faf84cd2be..37667813be 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https - [Audio Item](#audio-item) - [Button Item](#button-item) - [Panel Item](#panel-item) + - [Freeze Columns](#freeze-columns) - [Browse as User](#browse-as-user) - [Change Pointer Key](#change-pointer-key) - [Limitations](#limitations) @@ -1154,6 +1155,12 @@ Example: } ``` +### Freeze Columns + +▶️ *Core > Browser > Freeze column* + +Right-click on a table column header to freeze columns from the left up to the clicked column in the data browser. When scrolling horizontally, the frozen columns remain visible while the other columns scroll underneath. + ## Browse as User ▶️ *Core > Browser > Browse* diff --git a/src/components/BrowserCell/BrowserCell.react.js b/src/components/BrowserCell/BrowserCell.react.js index 212a58a8a4..8621e703f2 100644 --- a/src/components/BrowserCell/BrowserCell.react.js +++ b/src/components/BrowserCell/BrowserCell.react.js @@ -632,11 +632,20 @@ export default class BrowserCell extends Component { classes.push(styles.selected); } + const style = { width }; + if (this.props.stickyLeft !== undefined) { + style.position = 'sticky'; + style.left = this.props.stickyLeft; + style.zIndex = 1; + style.background = this.props.rowBackground; + style.borderBottom = '1px solid #e3e3ea'; + } + return ( { if (e.metaKey === true && type === 'Pointer') { onPointerCmdClick(value); diff --git a/src/components/BrowserRow/BrowserRow.react.js b/src/components/BrowserRow/BrowserRow.react.js index 35e2752647..12f577de63 100644 --- a/src/components/BrowserRow/BrowserRow.react.js +++ b/src/components/BrowserRow/BrowserRow.react.js @@ -51,6 +51,8 @@ export default class BrowserRow extends Component { onMouseUpRowCheckBox, onMouseOverRowCheckBox, onMouseOverRow, + stickyLefts, + freezeIndex, } = this.props; const attributes = obj.attributes; let requiredCols = []; @@ -69,12 +71,25 @@ export default class BrowserRow extends Component { } else if (obj.className === '_User' && obj.get('authData') !== undefined) { requiredCols = ['authData']; } + const rowBackground = row % 2 ? '#F4F5F7' : '#fdfafb'; + const rowStyle = { minWidth: rowWidth }; return ( -
onMouseOverRow(obj.id)}> +
onMouseOverRow(obj.id)}> onMouseOverRowCheckBox(obj.id)} + style={ + freezeIndex >= 0 + ? { + position: 'sticky', + left: 0, + zIndex: 1, + background: rowBackground, + borderBottom: '1px solid #e3e3ea', + } + : {} + } > -1} width={width} + stickyLeft={freezeIndex >= j ? stickyLefts[j] : undefined} + rowBackground={rowBackground} current={currentCol === j} onSelect={setCurrent} onEditChange={setEditing} diff --git a/src/components/DataBrowserHeaderBar/DataBrowserHeaderBar.react.js b/src/components/DataBrowserHeaderBar/DataBrowserHeaderBar.react.js index 1c6f8587e1..b5955df6dd 100644 --- a/src/components/DataBrowserHeaderBar/DataBrowserHeaderBar.react.js +++ b/src/components/DataBrowserHeaderBar/DataBrowserHeaderBar.react.js @@ -13,6 +13,16 @@ import styles from 'components/DataBrowserHeaderBar/DataBrowserHeaderBar.scss'; import { DndProvider } from 'react-dnd'; export default class DataBrowserHeaderBar extends React.Component { + handleContextMenu = (index, event) => { + event.preventDefault(); + const { freezeIndex, freezeColumns, unfreezeColumns, setContextMenu } = this.props; + const items = + freezeIndex >= 0 && index <= freezeIndex + ? [{ text: 'Unfreeze column', callback: () => unfreezeColumns() }] + : [{ text: 'Freeze column', callback: () => freezeColumns(index) }]; + setContextMenu(event.pageX, event.pageY, items); + }; + render() { const { headers, @@ -26,9 +36,16 @@ export default class DataBrowserHeaderBar extends React.Component { isDataLoaded, setSelectedObjectId, setCurrent, + stickyLefts, + handleLefts, + freezeIndex, } = this.props; const elements = [ -
+
= 0 ? { position: 'sticky', left: 0, zIndex: 11 } : {}} + > {readonly ? null : ( selectAll(e.target.checked)} /> )} @@ -40,6 +57,11 @@ export default class DataBrowserHeaderBar extends React.Component { return; } const wrapStyle = { width }; + if (freezeIndex >= 0 && typeof stickyLefts[i] !== 'undefined' && i <= freezeIndex) { + wrapStyle.position = 'sticky'; + wrapStyle.left = stickyLefts[i]; + wrapStyle.zIndex = 11; + } if (i % 2) { wrapStyle.background = '#726F85'; } else { @@ -63,7 +85,13 @@ export default class DataBrowserHeaderBar extends React.Component { } elements.push( -
+
this.handleContextMenu(i, e)} + key={'header' + i} + className={className} + style={wrapStyle} + >
); + const handleStyle = {}; + if (freezeIndex >= 0 && typeof handleLefts[i] !== 'undefined' && i <= freezeIndex) { + handleStyle.position = 'sticky'; + handleStyle.left = handleLefts[i]; + handleStyle.zIndex = 11; + if (i === freezeIndex) { + handleStyle.marginRight = 0; + handleStyle.width = 4; + } else { + handleStyle.background = wrapStyle.background; + } + } elements.push( - + ); }); diff --git a/src/dashboard/Data/Browser/BrowserTable.react.js b/src/dashboard/Data/Browser/BrowserTable.react.js index 125b299cc6..a5c1220d50 100644 --- a/src/dashboard/Data/Browser/BrowserTable.react.js +++ b/src/dashboard/Data/Browser/BrowserTable.react.js @@ -118,6 +118,22 @@ export default class BrowserTable extends React.Component { preventSort, required, })); + + const stickyLefts = []; + const handleLefts = []; + if ( + typeof this.props.freezeIndex === 'number' && + this.props.freezeIndex >= 0 + ) { + let left = 30; + headers.forEach((h, i) => { + stickyLefts[i] = left; + handleLefts[i] = left + h.width; + if (h.visible) { + left += h.width; + } + }); + } let editor = null; let table =
; if (this.props.data) { @@ -170,6 +186,8 @@ export default class BrowserTable extends React.Component { callCloudFunction={this.props.callCloudFunction} isPanelVisible={this.props.isPanelVisible} setContextMenu={this.props.setContextMenu} + stickyLefts={stickyLefts} + freezeIndex={this.props.freezeIndex} onEditSelectedRow={this.props.onEditSelectedRow} markRequiredFieldRow={this.props.markRequiredFieldRow} showNote={this.props.showNote} @@ -251,6 +269,8 @@ export default class BrowserTable extends React.Component { callCloudFunction={this.props.callCloudFunction} isPanelVisible={this.props.isPanelVisible} setContextMenu={this.props.setContextMenu} + stickyLefts={stickyLefts} + freezeIndex={this.props.freezeIndex} onEditSelectedRow={this.props.onEditSelectedRow} markRequiredFieldRow={this.props.markRequiredFieldRow} showNote={this.props.showNote} @@ -342,6 +362,8 @@ export default class BrowserTable extends React.Component { setSelectedObjectId={this.props.setSelectedObjectId} isPanelVisible={this.props.isPanelVisible} setContextMenu={this.props.setContextMenu} + stickyLefts={stickyLefts} + freezeIndex={this.props.freezeIndex} onEditSelectedRow={this.props.onEditSelectedRow} showNote={this.props.showNote} onRefresh={this.props.onRefresh} @@ -554,6 +576,11 @@ export default class BrowserTable extends React.Component { this.props.data.forEach(({ id }) => this.props.selectRow(id, checked)) } headers={headers} + stickyLefts={stickyLefts} + handleLefts={handleLefts} + freezeIndex={this.props.freezeIndex} + freezeColumns={this.props.freezeColumns} + unfreezeColumns={this.props.unfreezeColumns} updateOrdering={this.props.updateOrdering} readonly={!!this.props.relation || !!this.props.isUnique} handleDragDrop={this.props.handleHeaderDragDrop} @@ -563,6 +590,7 @@ export default class BrowserTable extends React.Component { isDataLoaded={!!this.props.data} setSelectedObjectId={this.props.setSelectedObjectId} setCurrent={this.props.setCurrent} + setContextMenu={this.props.setContextMenu} /> {table}
diff --git a/src/dashboard/Data/Browser/DataBrowser.react.js b/src/dashboard/Data/Browser/DataBrowser.react.js index 50aeef95af..1ab16c48e9 100644 --- a/src/dashboard/Data/Browser/DataBrowser.react.js +++ b/src/dashboard/Data/Browser/DataBrowser.react.js @@ -49,6 +49,7 @@ export default class DataBrowser extends React.Component { isResizing: false, maxWidth: window.innerWidth - 300, showAggregatedData: true, + frozenColumnIndex: -1, }; this.handleResizeDiv = this.handleResizeDiv.bind(this); @@ -66,6 +67,8 @@ export default class DataBrowser extends React.Component { this.setCopyableValue = this.setCopyableValue.bind(this); this.setSelectedObjectId = this.setSelectedObjectId.bind(this); this.setContextMenu = this.setContextMenu.bind(this); + this.freezeColumns = this.freezeColumns.bind(this); + this.unfreezeColumns = this.unfreezeColumns.bind(this); this.handleCellClick = this.handleCellClick.bind(this); this.saveOrderTimeout = null; } @@ -88,6 +91,7 @@ export default class DataBrowser extends React.Component { selectedCells: { list: new Set(), rowStart: -1, rowEnd: -1, colStart: -1, colEnd: -1 }, firstSelectedCell: null, selectedData: [], + frozenColumnIndex: -1, }); } else if ( Object.keys(props.columns).length !== Object.keys(this.props.columns).length || @@ -100,7 +104,7 @@ export default class DataBrowser extends React.Component { props.className, columnPreferences[props.className] ); - this.setState({ order }); + this.setState({ order, frozenColumnIndex: -1 }); } if (props && props.className) { if ( @@ -539,6 +543,14 @@ export default class DataBrowser extends React.Component { this.setState({ contextMenuX, contextMenuY, contextMenuItems }); } + freezeColumns(index) { + this.setState({ frozenColumnIndex: index }); + } + + unfreezeColumns() { + this.setState({ frozenColumnIndex: -1 }); + } + handleColumnsOrder(order, shouldReload) { this.setState({ order: [...order] }, () => { this.updatePreferences(order, shouldReload); @@ -643,6 +655,9 @@ export default class DataBrowser extends React.Component { setSelectedObjectId={this.setSelectedObjectId} callCloudFunction={this.props.callCloudFunction} setContextMenu={this.setContextMenu} + freezeIndex={this.state.frozenColumnIndex} + freezeColumns={this.freezeColumns} + unfreezeColumns={this.unfreezeColumns} onFilterChange={this.props.onFilterChange} onFilterSave={this.props.onFilterSave} selectedCells={this.state.selectedCells}