Skip to content

Commit d747786

Browse files
authored
feat: add pointer representation by a chosen column instead of objectId (#1852)
1 parent 5efcea8 commit d747786

12 files changed

+321
-140
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ npm-debug.log
1414

1515
logs/
1616
test_logs
17+
18+
# visual studio code
19+
.vscode

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
4040
- [Run with Docker](#run-with-docker)
4141
- [Features](#features)
4242
- [Browse as User](#browse-as-user)
43+
- [Change Pointer Key](#change-pointer-key)
44+
- [Limitations](#limitations)
4345
- [CSV Export](#csv-export)
4446
- [Contributing](#contributing)
4547

@@ -605,6 +607,19 @@ This feature allows you to use the data browser as another user, respecting that
605607

606608
> ⚠️ Logging in as another user will trigger the same Cloud Triggers as if the user logged in themselves using any other login method. Logging in as another user requires to enter that user's password.
607609
610+
## Change Pointer Key
611+
612+
▶️ *Core > Browser > Edit > Change pointer key*
613+
614+
This feature allows you to change how a pointer is represented in the browser. By default, a pointer is represented by the `objectId` of the linked object. You can change this to any other column of the object class. For example, if class `Installation` has a field that contains a pointer to class `User`, the pointer will show the `objectId` of the user by default. You can change this to display the field `email` of the user, so that a pointer displays the user's email address instead.
615+
616+
### Limitations
617+
618+
- This does not work for an array of pointers; the pointer will always display the `objectId`.
619+
- System columns like `createdAt`, `updatedAt`, `ACL` cannot be set as pointer key.
620+
- This feature uses browser storage; switching to a different browser resets the pointer key to `objectId`.
621+
622+
> ⚠️ For each custom pointer key in each row, a server request is triggered to resolve the custom pointer key. For example, if the browser shows a class with 50 rows and each row contains 3 custom pointer keys, a total of 150 separate server requests are triggered.
608623
## CSV Export
609624

610625
▶️ *Core > Browser > Export*

src/components/BrowserCell/BrowserCell.react.js

+167-132
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import React, { Component } from 'react';
1515
import styles from 'components/BrowserCell/BrowserCell.scss';
1616
import { unselectable } from 'stylesheets/base.scss';
1717
import Tooltip from '../Tooltip/PopperTooltip.react';
18+
import * as ColumnPreferences from 'lib/ColumnPreferences';
1819

1920
export default class BrowserCell extends Component {
2021
constructor() {
@@ -23,13 +24,161 @@ export default class BrowserCell extends Component {
2324
this.cellRef = React.createRef();
2425
this.copyableValue = undefined;
2526
this.state = {
26-
showTooltip: false
27+
showTooltip: false,
28+
content: null,
29+
classes: []
30+
};
31+
}
32+
33+
async renderCellContent() {
34+
let content = this.props.value;
35+
let isNewRow = this.props.row < 0;
36+
this.copyableValue = content;
37+
let classes = [styles.cell, unselectable];
38+
if (this.props.hidden) {
39+
content = this.props.value !== undefined || !isNewRow ? '(hidden)' : this.props.isRequired ? '(required)' : '(undefined)';
40+
classes.push(styles.empty);
41+
} else if (this.props.value === undefined) {
42+
if (this.props.type === 'ACL') {
43+
this.copyableValue = content = 'Public Read + Write';
44+
} else {
45+
this.copyableValue = content = '(undefined)';
46+
classes.push(styles.empty);
47+
}
48+
content = isNewRow && this.props.isRequired && this.props.value === undefined ? '(required)' : content;
49+
} else if (this.props.value === null) {
50+
this.copyableValue = content = '(null)';
51+
classes.push(styles.empty);
52+
} else if (this.props.value === '') {
53+
content = <span>&nbsp;</span>;
54+
classes.push(styles.empty);
55+
} else if (this.props.type === 'Pointer') {
56+
const defaultPointerKey = await ColumnPreferences.getPointerDefaultKey(this.props.appId, this.props.value.className);
57+
let dataValue = this.props.value.id;
58+
if( defaultPointerKey !== 'objectId' ) {
59+
dataValue = this.props.value.get(defaultPointerKey);
60+
if ( dataValue && typeof dataValue === 'object' ){
61+
if ( dataValue instanceof Date ) {
62+
dataValue = dataValue.toLocaleString();
63+
}
64+
else {
65+
if ( !this.props.value.id ) {
66+
dataValue = this.props.value.id;
67+
} else {
68+
dataValue = '(undefined)';
69+
}
70+
}
71+
}
72+
if ( !dataValue ) {
73+
if ( this.props.value.id ) {
74+
dataValue = this.props.value.id;
75+
} else {
76+
dataValue = '(undefined)';
77+
}
78+
}
79+
}
80+
81+
if (this.props.value && this.props.value.__type) {
82+
const object = new Parse.Object(this.props.value.className);
83+
object.id = this.props.value.objectId;
84+
this.props.value = object;
85+
}
86+
87+
content = this.props.onPointerClick ? (
88+
<Pill value={ dataValue } onClick={this.props.onPointerClick.bind(undefined, this.props.value)} followClick={true} />
89+
) : (
90+
dataValue
91+
);
92+
93+
this.copyableValue = this.props.value.id;
94+
}
95+
else if (this.props.type === 'Array') {
96+
if ( this.props.value[0] && typeof this.props.value[0] === 'object' && this.props.value[0].__type === 'Pointer' ) {
97+
const array = [];
98+
this.props.value.map( (v, i) => {
99+
if ( typeof v !== 'object' || v.__type !== 'Pointer' ) {
100+
throw new Error('Invalid type found in pointer array');
101+
}
102+
const object = new Parse.Object(v.className);
103+
object.id = v.objectId;
104+
array.push(
105+
<Pill key={i} value={v.objectId} onClick={this.props.onPointerClick.bind(undefined, object)} followClick={true} />
106+
);
107+
});
108+
this.copyableValue = content = <ul>
109+
{ array.map( a => <li>{a}</li>) }
110+
</ul>
111+
if ( array.length > 1 ) {
112+
classes.push(styles.hasMore);
113+
}
114+
}
115+
else {
116+
this.copyableValue = content = JSON.stringify(this.props.value);
117+
}
118+
}
119+
else if (this.props.type === 'Date') {
120+
if (typeof value === 'object' && this.props.value.__type) {
121+
this.props.value = new Date(this.props.value.iso);
122+
} else if (typeof value === 'string') {
123+
this.props.value = new Date(this.props.value);
124+
}
125+
this.copyableValue = content = dateStringUTC(this.props.value);
126+
} else if (this.props.type === 'Boolean') {
127+
this.copyableValue = content = this.props.value ? 'True' : 'False';
128+
} else if (this.props.type === 'Object' || this.props.type === 'Bytes') {
129+
this.copyableValue = content = JSON.stringify(this.props.value);
130+
} else if (this.props.type === 'File') {
131+
const fileName = this.props.value.url() ? getFileName(this.props.value) : 'Uploading\u2026';
132+
content = <Pill value={fileName} fileDownloadLink={this.props.value.url()} />;
133+
this.copyableValue = fileName;
134+
} else if (this.props.type === 'ACL') {
135+
let pieces = [];
136+
let json = this.props.value.toJSON();
137+
if (Object.prototype.hasOwnProperty.call(json, '*')) {
138+
if (json['*'].read && json['*'].write) {
139+
pieces.push('Public Read + Write');
140+
} else if (json['*'].read) {
141+
pieces.push('Public Read');
142+
} else if (json['*'].write) {
143+
pieces.push('Public Write');
144+
}
145+
}
146+
for (let role in json) {
147+
if (role !== '*') {
148+
pieces.push(role);
149+
}
150+
}
151+
if (pieces.length === 0) {
152+
pieces.push('Master Key Only');
153+
}
154+
this.copyableValue = content = pieces.join(', ');
155+
} else if (this.props.type === 'GeoPoint') {
156+
this.copyableValue = content = `(${this.props.value.latitude}, ${this.props.value.longitude})`;
157+
} else if (this.props.type === 'Polygon') {
158+
this.copyableValue = content = this.props.value.coordinates.map(coord => `(${coord})`)
159+
} else if (this.props.type === 'Relation') {
160+
content = this.props.setRelation ? (
161+
<div style={{ textAlign: 'center' }}>
162+
<Pill onClick={() => this.props.setRelation(this.props.value)} value='View relation' followClick={true} />
163+
</div>
164+
) : (
165+
'Relation'
166+
);
167+
this.copyableValue = undefined;
27168
}
28169
this.onContextMenu = this.onContextMenu.bind(this);
29170

171+
if (this.props.markRequiredField && this.props.isRequired && !this.props.value) {
172+
classes.push(styles.required);
173+
}
174+
175+
this.setState({ ...this.state, content, classes })
30176
}
31177

32-
componentDidUpdate(prevProps) {
178+
async componentDidUpdate(prevProps) {
179+
if ( this.props.value !== prevProps.value ) {
180+
await this.renderCellContent();
181+
}
33182
if (this.props.current) {
34183
const node = this.cellRef.current;
35184
const { setRelation } = this.props;
@@ -58,7 +207,7 @@ export default class BrowserCell extends Component {
58207
}
59208

60209
shouldComponentUpdate(nextProps, nextState) {
61-
if (nextState.showTooltip !== this.state.showTooltip) {
210+
if (nextState.showTooltip !== this.state.showTooltip || nextState.content !== this.state.content ) {
62211
return true;
63212
}
64213
const shallowVerifyProps = [...new Set(Object.keys(this.props).concat(Object.keys(nextProps)))]
@@ -225,139 +374,27 @@ export default class BrowserCell extends Component {
225374
})));
226375
}
227376

377+
componentDidMount(){
378+
this.renderCellContent();
379+
}
380+
228381
//#endregion
229382

230383
render() {
231-
let { type, value, hidden, width, current, onSelect, onEditChange, setCopyableValue, setRelation, onPointerClick, onPointerCmdClick, row, col, field, onEditSelectedRow, readonly, isRequired, markRequiredFieldRow } = this.props;
232-
let content = value;
384+
let { type, value, hidden, width, current, onSelect, onEditChange, setCopyableValue, onPointerCmdClick, row, col, field, onEditSelectedRow, readonly, isRequired, markRequiredFieldRow } = this.props;
233385
let isNewRow = row < 0;
234-
this.copyableValue = content;
235-
let classes = [styles.cell, unselectable];
236-
if (hidden) {
237-
content = value !== undefined || !isNewRow ? '(hidden)' : isRequired ? '(required)' : '(undefined)';
238-
classes.push(styles.empty);
239-
} else if (value === undefined) {
240-
if (type === 'ACL') {
241-
this.copyableValue = content = 'Public Read + Write';
242-
} else {
243-
this.copyableValue = content = '(undefined)';
244-
classes.push(styles.empty);
245-
}
246-
content = isNewRow && isRequired && value === undefined ? '(required)' : content;
247-
} else if (value === null) {
248-
this.copyableValue = content = '(null)';
249-
classes.push(styles.empty);
250-
} else if (value === '') {
251-
content = <span>&nbsp;</span>;
252-
classes.push(styles.empty);
253-
} else if (type === 'Pointer') {
254-
if (value && value.__type) {
255-
const object = new Parse.Object(value.className);
256-
object.id = value.objectId;
257-
value = object;
258-
}
259-
content = onPointerClick ? (
260-
<Pill
261-
value={value.id}
262-
onClick={onPointerClick.bind(undefined, value)}
263-
followClick={true}
264-
/>
265-
) : (
266-
value.id
267-
);
268-
this.copyableValue = value.id;
269-
}
270-
else if (type === 'Array') {
271-
if (value[0] && typeof value[0] === 'object' && value[0].__type === 'Pointer') {
272-
const array = [];
273-
value.map((v, i) => {
274-
if (typeof v !== 'object' || v.__type !== 'Pointer') {
275-
throw new Error('Invalid type found in pointer array');
276-
}
277-
const object = new Parse.Object(v.className);
278-
object.id = v.objectId;
279-
array.push(
280-
<Pill
281-
key={v.objectId}
282-
value={v.objectId}
283-
onClick={onPointerClick.bind(undefined, object)}
284-
followClick={true}
285-
/>
286-
);
287-
});
288-
content = <ul className={styles.hasMore}>
289-
{array.map(a => <li>{a}</li>)}
290-
</ul>
291-
this.copyableValue = JSON.stringify(value);
292-
if (array.length > 1) {
293-
classes.push(styles.removePadding);
294-
}
295-
}
296-
else {
297-
this.copyableValue = content = JSON.stringify(value);
298-
}
299-
}
300-
else if (type === 'Date') {
301-
if (typeof value === 'object' && value.__type) {
302-
value = new Date(value.iso);
303-
} else if (typeof value === 'string') {
304-
value = new Date(value);
305-
}
306-
this.copyableValue = content = dateStringUTC(value);
307-
} else if (type === 'Boolean') {
308-
this.copyableValue = content = value ? 'True' : 'False';
309-
} else if (type === 'Object' || type === 'Bytes') {
310-
this.copyableValue = content = JSON.stringify(value);
311-
} else if (type === 'File') {
312-
const fileName = value.url() ? getFileName(value) : 'Uploading\u2026';
313-
content = <Pill value={fileName} fileDownloadLink={value.url()} />;
314-
this.copyableValue = fileName;
315-
} else if (type === 'ACL') {
316-
let pieces = [];
317-
let json = value.toJSON();
318-
if (Object.prototype.hasOwnProperty.call(json, '*')) {
319-
if (json['*'].read && json['*'].write) {
320-
pieces.push('Public Read + Write');
321-
} else if (json['*'].read) {
322-
pieces.push('Public Read');
323-
} else if (json['*'].write) {
324-
pieces.push('Public Write');
325-
}
326-
}
327-
for (let role in json) {
328-
if (role !== '*') {
329-
pieces.push(role);
330-
}
331-
}
332-
if (pieces.length === 0) {
333-
pieces.push('Master Key Only');
334-
}
335-
this.copyableValue = content = pieces.join(', ');
336-
} else if (type === 'GeoPoint') {
337-
this.copyableValue = content = `(${value.latitude}, ${value.longitude})`;
338-
} else if (type === 'Polygon') {
339-
this.copyableValue = content = value.coordinates.map(coord => `(${coord})`)
340-
} else if (type === 'Relation') {
341-
content = setRelation ? (
342-
<div style={{ textAlign: 'center' }}>
343-
<Pill onClick={() => setRelation(value)} value='View relation' followClick={true} />
344-
</div>
345-
) : (
346-
'Relation'
347-
);
348-
this.copyableValue = undefined;
349-
}
350386

351-
if (current) {
387+
let classes = [...this.state.classes];
388+
389+
if ( current ) {
352390
classes.push(styles.current);
353391
}
354-
355392
if (markRequiredFieldRow === row && isRequired && !value) {
356393
classes.push(styles.required);
357394
}
358395

359396
return readonly ? (
360-
<Tooltip placement='bottom' tooltip='Read only (CTRL+C to copy)' visible={this.state.showTooltip} >
397+
<Tooltip placement='bottom' tooltip='Read only (CTRL+C to copy)' visible={this.state.showTooltip}>
361398
<span
362399
ref={this.cellRef}
363400
className={classes.join(' ')}
@@ -382,7 +419,7 @@ export default class BrowserCell extends Component {
382419
}}
383420
onContextMenu={this.onContextMenu}
384421
>
385-
{isNewRow ? '(auto)' : content}
422+
{row < 0 || isNewRow ? '(auto)' : this.state.content}
386423
</span>
387424
</Tooltip>
388425
) : (
@@ -413,13 +450,11 @@ export default class BrowserCell extends Component {
413450
if (['ACL', 'Boolean', 'File'].includes(type)) {
414451
e.preventDefault();
415452
}
416-
onEditChange(true);
417-
}
418-
}}
419-
onContextMenu={this.onContextMenu}
420-
>
421-
{content}
422-
</span>
453+
}}}
454+
onContextMenu={this.onContextMenu.bind(this)}
455+
>
456+
{this.state.content}
457+
</span>
423458
);
424459
}
425460
}

0 commit comments

Comments
 (0)