Skip to content

Commit 29f4a88

Browse files
authored
feat: Add column freezing in data browser (#2877)
1 parent 02b3153 commit 29f4a88

File tree

6 files changed

+127
-6
lines changed

6 files changed

+127
-6
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
7070
- [Audio Item](#audio-item)
7171
- [Button Item](#button-item)
7272
- [Panel Item](#panel-item)
73+
- [Freeze Columns](#freeze-columns)
7374
- [Browse as User](#browse-as-user)
7475
- [Change Pointer Key](#change-pointer-key)
7576
- [Limitations](#limitations)
@@ -1154,6 +1155,12 @@ Example:
11541155
}
11551156
```
11561157

1158+
### Freeze Columns
1159+
1160+
▶️ *Core > Browser > Freeze column*
1161+
1162+
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.
1163+
11571164
## Browse as User
11581165

11591166
▶️ *Core > Browser > Browse*

src/components/BrowserCell/BrowserCell.react.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -632,11 +632,20 @@ export default class BrowserCell extends Component {
632632
classes.push(styles.selected);
633633
}
634634

635+
const style = { width };
636+
if (this.props.stickyLeft !== undefined) {
637+
style.position = 'sticky';
638+
style.left = this.props.stickyLeft;
639+
style.zIndex = 1;
640+
style.background = this.props.rowBackground;
641+
style.borderBottom = '1px solid #e3e3ea';
642+
}
643+
635644
return (
636645
<span
637646
ref={this.cellRef}
638647
className={classes.join(' ')}
639-
style={{ width }}
648+
style={style}
640649
onClick={e => {
641650
if (e.metaKey === true && type === 'Pointer') {
642651
onPointerCmdClick(value);

src/components/BrowserRow/BrowserRow.react.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export default class BrowserRow extends Component {
5151
onMouseUpRowCheckBox,
5252
onMouseOverRowCheckBox,
5353
onMouseOverRow,
54+
stickyLefts,
55+
freezeIndex,
5456
} = this.props;
5557
const attributes = obj.attributes;
5658
let requiredCols = [];
@@ -69,12 +71,25 @@ export default class BrowserRow extends Component {
6971
} else if (obj.className === '_User' && obj.get('authData') !== undefined) {
7072
requiredCols = ['authData'];
7173
}
74+
const rowBackground = row % 2 ? '#F4F5F7' : '#fdfafb';
75+
const rowStyle = { minWidth: rowWidth };
7276
return (
73-
<div className={styles.tableRow} style={{ minWidth: rowWidth }} onMouseOver={() => onMouseOverRow(obj.id)}>
77+
<div className={styles.tableRow} style={rowStyle} onMouseOver={() => onMouseOverRow(obj.id)}>
7478
<span
7579
className={styles.checkCell}
7680
onMouseUp={onMouseUpRowCheckBox}
7781
onMouseOver={() => onMouseOverRowCheckBox(obj.id)}
82+
style={
83+
freezeIndex >= 0
84+
? {
85+
position: 'sticky',
86+
left: 0,
87+
zIndex: 1,
88+
background: rowBackground,
89+
borderBottom: '1px solid #e3e3ea',
90+
}
91+
: {}
92+
}
7893
>
7994
<input
8095
type="checkbox"
@@ -133,6 +148,8 @@ export default class BrowserRow extends Component {
133148
type={type}
134149
readonly={isUnique || readOnlyFields.indexOf(name) > -1}
135150
width={width}
151+
stickyLeft={freezeIndex >= j ? stickyLefts[j] : undefined}
152+
rowBackground={rowBackground}
136153
current={currentCol === j}
137154
onSelect={setCurrent}
138155
onEditChange={setEditing}

src/components/DataBrowserHeaderBar/DataBrowserHeaderBar.react.js

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ import styles from 'components/DataBrowserHeaderBar/DataBrowserHeaderBar.scss';
1313
import { DndProvider } from 'react-dnd';
1414

1515
export default class DataBrowserHeaderBar extends React.Component {
16+
handleContextMenu = (index, event) => {
17+
event.preventDefault();
18+
const { freezeIndex, freezeColumns, unfreezeColumns, setContextMenu } = this.props;
19+
const items =
20+
freezeIndex >= 0 && index <= freezeIndex
21+
? [{ text: 'Unfreeze column', callback: () => unfreezeColumns() }]
22+
: [{ text: 'Freeze column', callback: () => freezeColumns(index) }];
23+
setContextMenu(event.pageX, event.pageY, items);
24+
};
25+
1626
render() {
1727
const {
1828
headers,
@@ -26,9 +36,16 @@ export default class DataBrowserHeaderBar extends React.Component {
2636
isDataLoaded,
2737
setSelectedObjectId,
2838
setCurrent,
39+
stickyLefts,
40+
handleLefts,
41+
freezeIndex,
2942
} = this.props;
3043
const elements = [
31-
<div key="check" className={[styles.wrap, styles.check].join(' ')}>
44+
<div
45+
key="check"
46+
className={[styles.wrap, styles.check].join(' ')}
47+
style={freezeIndex >= 0 ? { position: 'sticky', left: 0, zIndex: 11 } : {}}
48+
>
3249
{readonly ? null : (
3350
<input type="checkbox" checked={selected} onChange={e => selectAll(e.target.checked)} />
3451
)}
@@ -40,6 +57,11 @@ export default class DataBrowserHeaderBar extends React.Component {
4057
return;
4158
}
4259
const wrapStyle = { width };
60+
if (freezeIndex >= 0 && typeof stickyLefts[i] !== 'undefined' && i <= freezeIndex) {
61+
wrapStyle.position = 'sticky';
62+
wrapStyle.left = stickyLefts[i];
63+
wrapStyle.zIndex = 11;
64+
}
4365
if (i % 2) {
4466
wrapStyle.background = '#726F85';
4567
} else {
@@ -63,7 +85,13 @@ export default class DataBrowserHeaderBar extends React.Component {
6385
}
6486

6587
elements.push(
66-
<div onClick={onClick} key={'header' + i} className={className} style={wrapStyle}>
88+
<div
89+
onClick={onClick}
90+
onContextMenu={e => this.handleContextMenu(i, e)}
91+
key={'header' + i}
92+
className={className}
93+
style={wrapStyle}
94+
>
6795
<DataBrowserHeader
6896
name={name}
6997
type={type}
@@ -74,8 +102,25 @@ export default class DataBrowserHeaderBar extends React.Component {
74102
/>
75103
</div>
76104
);
105+
const handleStyle = {};
106+
if (freezeIndex >= 0 && typeof handleLefts[i] !== 'undefined' && i <= freezeIndex) {
107+
handleStyle.position = 'sticky';
108+
handleStyle.left = handleLefts[i];
109+
handleStyle.zIndex = 11;
110+
if (i === freezeIndex) {
111+
handleStyle.marginRight = 0;
112+
handleStyle.width = 4;
113+
} else {
114+
handleStyle.background = wrapStyle.background;
115+
}
116+
}
77117
elements.push(
78-
<DragHandle key={'handle' + i} className={styles.handle} onDrag={onResize.bind(null, i)} />
118+
<DragHandle
119+
key={'handle' + i}
120+
className={styles.handle}
121+
onDrag={onResize.bind(null, i)}
122+
style={handleStyle}
123+
/>
79124
);
80125
});
81126

src/dashboard/Data/Browser/BrowserTable.react.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,22 @@ export default class BrowserTable extends React.Component {
118118
preventSort,
119119
required,
120120
}));
121+
122+
const stickyLefts = [];
123+
const handleLefts = [];
124+
if (
125+
typeof this.props.freezeIndex === 'number' &&
126+
this.props.freezeIndex >= 0
127+
) {
128+
let left = 30;
129+
headers.forEach((h, i) => {
130+
stickyLefts[i] = left;
131+
handleLefts[i] = left + h.width;
132+
if (h.visible) {
133+
left += h.width;
134+
}
135+
});
136+
}
121137
let editor = null;
122138
let table = <div ref={this.tableRef} />;
123139
if (this.props.data) {
@@ -170,6 +186,8 @@ export default class BrowserTable extends React.Component {
170186
callCloudFunction={this.props.callCloudFunction}
171187
isPanelVisible={this.props.isPanelVisible}
172188
setContextMenu={this.props.setContextMenu}
189+
stickyLefts={stickyLefts}
190+
freezeIndex={this.props.freezeIndex}
173191
onEditSelectedRow={this.props.onEditSelectedRow}
174192
markRequiredFieldRow={this.props.markRequiredFieldRow}
175193
showNote={this.props.showNote}
@@ -251,6 +269,8 @@ export default class BrowserTable extends React.Component {
251269
callCloudFunction={this.props.callCloudFunction}
252270
isPanelVisible={this.props.isPanelVisible}
253271
setContextMenu={this.props.setContextMenu}
272+
stickyLefts={stickyLefts}
273+
freezeIndex={this.props.freezeIndex}
254274
onEditSelectedRow={this.props.onEditSelectedRow}
255275
markRequiredFieldRow={this.props.markRequiredFieldRow}
256276
showNote={this.props.showNote}
@@ -342,6 +362,8 @@ export default class BrowserTable extends React.Component {
342362
setSelectedObjectId={this.props.setSelectedObjectId}
343363
isPanelVisible={this.props.isPanelVisible}
344364
setContextMenu={this.props.setContextMenu}
365+
stickyLefts={stickyLefts}
366+
freezeIndex={this.props.freezeIndex}
345367
onEditSelectedRow={this.props.onEditSelectedRow}
346368
showNote={this.props.showNote}
347369
onRefresh={this.props.onRefresh}
@@ -554,6 +576,11 @@ export default class BrowserTable extends React.Component {
554576
this.props.data.forEach(({ id }) => this.props.selectRow(id, checked))
555577
}
556578
headers={headers}
579+
stickyLefts={stickyLefts}
580+
handleLefts={handleLefts}
581+
freezeIndex={this.props.freezeIndex}
582+
freezeColumns={this.props.freezeColumns}
583+
unfreezeColumns={this.props.unfreezeColumns}
557584
updateOrdering={this.props.updateOrdering}
558585
readonly={!!this.props.relation || !!this.props.isUnique}
559586
handleDragDrop={this.props.handleHeaderDragDrop}
@@ -563,6 +590,7 @@ export default class BrowserTable extends React.Component {
563590
isDataLoaded={!!this.props.data}
564591
setSelectedObjectId={this.props.setSelectedObjectId}
565592
setCurrent={this.props.setCurrent}
593+
setContextMenu={this.props.setContextMenu}
566594
/>
567595
{table}
568596
</div>

src/dashboard/Data/Browser/DataBrowser.react.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export default class DataBrowser extends React.Component {
4949
isResizing: false,
5050
maxWidth: window.innerWidth - 300,
5151
showAggregatedData: true,
52+
frozenColumnIndex: -1,
5253
};
5354

5455
this.handleResizeDiv = this.handleResizeDiv.bind(this);
@@ -66,6 +67,8 @@ export default class DataBrowser extends React.Component {
6667
this.setCopyableValue = this.setCopyableValue.bind(this);
6768
this.setSelectedObjectId = this.setSelectedObjectId.bind(this);
6869
this.setContextMenu = this.setContextMenu.bind(this);
70+
this.freezeColumns = this.freezeColumns.bind(this);
71+
this.unfreezeColumns = this.unfreezeColumns.bind(this);
6972
this.handleCellClick = this.handleCellClick.bind(this);
7073
this.saveOrderTimeout = null;
7174
}
@@ -88,6 +91,7 @@ export default class DataBrowser extends React.Component {
8891
selectedCells: { list: new Set(), rowStart: -1, rowEnd: -1, colStart: -1, colEnd: -1 },
8992
firstSelectedCell: null,
9093
selectedData: [],
94+
frozenColumnIndex: -1,
9195
});
9296
} else if (
9397
Object.keys(props.columns).length !== Object.keys(this.props.columns).length ||
@@ -100,7 +104,7 @@ export default class DataBrowser extends React.Component {
100104
props.className,
101105
columnPreferences[props.className]
102106
);
103-
this.setState({ order });
107+
this.setState({ order, frozenColumnIndex: -1 });
104108
}
105109
if (props && props.className) {
106110
if (
@@ -539,6 +543,14 @@ export default class DataBrowser extends React.Component {
539543
this.setState({ contextMenuX, contextMenuY, contextMenuItems });
540544
}
541545

546+
freezeColumns(index) {
547+
this.setState({ frozenColumnIndex: index });
548+
}
549+
550+
unfreezeColumns() {
551+
this.setState({ frozenColumnIndex: -1 });
552+
}
553+
542554
handleColumnsOrder(order, shouldReload) {
543555
this.setState({ order: [...order] }, () => {
544556
this.updatePreferences(order, shouldReload);
@@ -643,6 +655,9 @@ export default class DataBrowser extends React.Component {
643655
setSelectedObjectId={this.setSelectedObjectId}
644656
callCloudFunction={this.props.callCloudFunction}
645657
setContextMenu={this.setContextMenu}
658+
freezeIndex={this.state.frozenColumnIndex}
659+
freezeColumns={this.freezeColumns}
660+
unfreezeColumns={this.unfreezeColumns}
646661
onFilterChange={this.props.onFilterChange}
647662
onFilterSave={this.props.onFilterSave}
648663
selectedCells={this.state.selectedCells}

0 commit comments

Comments
 (0)