Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add pointer representation by a chosen column instead of objectId #1852

Merged
merged 70 commits into from
Oct 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
8a69790
date fix
faisal154 Jun 3, 2021
5b4a351
removed parse info
faisal154 Jun 3, 2021
bbf59ab
removed momentjs
faisal154 Jun 3, 2021
bf6bed4
package-lock.json reverted
faisal154 Jun 3, 2021
cd8c0fb
removed commented code
faisal154 Jun 3, 2021
ad6489e
query optimization
faisal154 Jun 4, 2021
0d55092
reset 1
faisal154 Jun 4, 2021
5d10e7d
fetch fix
faisal154 Jun 4, 2021
830b063
switch back to community dialog
faisal154 Jun 4, 2021
5a17315
removed sweet alert
faisal154 Jun 4, 2021
efe743e
update pointer value
faisal154 Jun 4, 2021
757c5fc
updated dialog text
faisal154 Jun 4, 2021
69da84b
reset config
faisal154 Jun 4, 2021
efdb7e7
cleanup
faisal154 Jun 4, 2021
dd30f4e
cleanup2
faisal154 Jun 4, 2021
1aac08c
callback for pointer dia fix
faisal154 Jun 4, 2021
30b7464
package lock reverted
faisal154 Jun 7, 2021
2d898a1
package-lock update
faisal154 Jun 7, 2021
8365437
tooltip path revert
faisal154 Jun 7, 2021
7f4be33
constent typo fix
faisal154 Jun 7, 2021
857883a
eslint fix
faisal154 Jun 7, 2021
ff543be
eslint indentation
faisal154 Jun 7, 2021
606a2e3
dialog word fix
faisal154 Jun 7, 2021
0558163
Pointer key dialog
faisal154 Jun 7, 2021
530da77
removed empty line
faisal154 Jun 7, 2021
9f69867
dialog layout fix
faisal154 Jun 7, 2021
a54e87b
dialog layout subtitle update
faisal154 Jun 7, 2021
55eb82f
dialog title
faisal154 Jun 7, 2021
86508ed
Merge branch 'master' of https://github.com/parse-community/parse-das…
faisal154 Aug 3, 2021
c9ac0db
fix
faisal154 Aug 3, 2021
8302288
bug fixes + pointer undefined val
faisal154 Aug 4, 2021
c945b7c
pointer array pill fix
faisal154 Aug 4, 2021
9d67eda
resolved merge conflicts
faisal154 Aug 10, 2021
c2dba8f
removed console.log
faisal154 Aug 13, 2021
68b92e1
Updated README.
faisal154 Aug 16, 2021
67f5113
Updated README -- 2
faisal154 Aug 16, 2021
c88ee65
improved feature wording
mtrezza Aug 16, 2021
2181b51
Fixed wording in dialog
mtrezza Aug 16, 2021
6148800
resolved merge conflicts
faisal154 Aug 17, 2021
80bd65a
refactoring -- 1
faisal154 Aug 31, 2021
8d419c6
refactoring -- 2
faisal154 Aug 31, 2021
098fc00
resolved merge conflicts
faisal154 Aug 31, 2021
4e00fce
resolve merge conflicts
faisal154 Aug 31, 2021
463bc94
revert package lock
faisal154 Aug 31, 2021
a05d7c2
revert package lock -- 2
faisal154 Aug 31, 2021
d42f1a6
change log entry
faisal154 Aug 31, 2021
538e909
Merge branch 'master' into feat/151-pcd
fn-faisal Sep 2, 2021
ef55f60
Merge branch 'master' of https://github.com/parse-community/parse-das…
faisal154 Sep 6, 2021
d9cb681
Merge branch 'master' into feat/151-pcd
mtrezza Sep 7, 2021
d14b7eb
Merge branch 'master' into feat/151-pcd
fn-faisal Sep 13, 2021
4c21af5
changed default null to pointer key + save pkey with appId
faisal154 Sep 13, 2021
98f6c42
reverting index.html file
faisal154 Sep 21, 2021
3d77958
Merge branch 'master' into feat/151-pcd
fn-faisal Sep 23, 2021
86b22c6
removed .vscode + update .gitignore + revert plock
faisal154 Sep 23, 2021
45f01fd
Merge branch 'feat/151-pcd' of https://github.com/fn-faisal/parse-das…
faisal154 Sep 23, 2021
ca548c2
revert package.lock
faisal154 Sep 23, 2021
bfbc9a4
revert package.json
faisal154 Sep 23, 2021
30f6d6b
Merge branch 'master' into feat/151-pcd
fn-faisal Sep 23, 2021
02e912a
Merge branch 'master' into feat/151-pcd
fn-faisal Sep 23, 2021
23780b7
Merge branch 'master' into feat/151-pcd
mtrezza Sep 23, 2021
603d03f
Removed line
fn-faisal Sep 23, 2021
8349aa9
Removed entry from changelog.md
fn-faisal Sep 23, 2021
56028a6
Merge branch 'master' into feat/151-pcd
fn-faisal Sep 23, 2021
fffa016
Merge branch 'master' into feat/151-pcd
mtrezza Sep 27, 2021
862de40
Merge branch 'master' into feat/151-pcd
fn-faisal Sep 29, 2021
dfb1f28
Merge branch 'master' into feat/151-pcd
fn-faisal Sep 29, 2021
34c62cf
Merge branch 'master' into feat/151-pcd
mtrezza Sep 29, 2021
4b00d4b
fixed merge conflicts
faisal154 Oct 11, 2021
d462b37
removed setRequiredColumnFields
faisal154 Oct 11, 2021
62e131c
removed unused code
faisal154 Oct 11, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ npm-debug.log

logs/
test_logs

# visual studio code
.vscode
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
- [Run with Docker](#run-with-docker)
- [Features](#features)
- [Browse as User](#browse-as-user)
- [Change Pointer Key](#change-pointer-key)
- [Limitations](#limitations)
- [CSV Export](#csv-export)
- [Contributing](#contributing)

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

> ⚠️ 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.

## Change Pointer Key

▶️ *Core > Browser > Edit > Change pointer key*

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.

### Limitations

- This does not work for an array of pointers; the pointer will always display the `objectId`.
- System columns like `createdAt`, `updatedAt`, `ACL` cannot be set as pointer key.
- This feature uses browser storage; switching to a different browser resets the pointer key to `objectId`.

> ⚠️ 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.
## CSV Export

▶️ *Core > Browser > Export*
Expand Down
299 changes: 167 additions & 132 deletions src/components/BrowserCell/BrowserCell.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import React, { Component } from 'react';
import styles from 'components/BrowserCell/BrowserCell.scss';
import { unselectable } from 'stylesheets/base.scss';
import Tooltip from '../Tooltip/PopperTooltip.react';
import * as ColumnPreferences from 'lib/ColumnPreferences';

export default class BrowserCell extends Component {
constructor() {
Expand All @@ -23,13 +24,161 @@ export default class BrowserCell extends Component {
this.cellRef = React.createRef();
this.copyableValue = undefined;
this.state = {
showTooltip: false
showTooltip: false,
content: null,
classes: []
};
}

async renderCellContent() {
let content = this.props.value;
let isNewRow = this.props.row < 0;
this.copyableValue = content;
let classes = [styles.cell, unselectable];
if (this.props.hidden) {
content = this.props.value !== undefined || !isNewRow ? '(hidden)' : this.props.isRequired ? '(required)' : '(undefined)';
classes.push(styles.empty);
} else if (this.props.value === undefined) {
if (this.props.type === 'ACL') {
this.copyableValue = content = 'Public Read + Write';
} else {
this.copyableValue = content = '(undefined)';
classes.push(styles.empty);
}
content = isNewRow && this.props.isRequired && this.props.value === undefined ? '(required)' : content;
} else if (this.props.value === null) {
this.copyableValue = content = '(null)';
classes.push(styles.empty);
} else if (this.props.value === '') {
content = <span>&nbsp;</span>;
classes.push(styles.empty);
} else if (this.props.type === 'Pointer') {
const defaultPointerKey = await ColumnPreferences.getPointerDefaultKey(this.props.appId, this.props.value.className);
let dataValue = this.props.value.id;
if( defaultPointerKey !== 'objectId' ) {
dataValue = this.props.value.get(defaultPointerKey);
if ( dataValue && typeof dataValue === 'object' ){
if ( dataValue instanceof Date ) {
dataValue = dataValue.toLocaleString();
}
else {
if ( !this.props.value.id ) {
dataValue = this.props.value.id;
} else {
dataValue = '(undefined)';
}
}
}
if ( !dataValue ) {
if ( this.props.value.id ) {
dataValue = this.props.value.id;
} else {
dataValue = '(undefined)';
}
}
}

if (this.props.value && this.props.value.__type) {
const object = new Parse.Object(this.props.value.className);
object.id = this.props.value.objectId;
this.props.value = object;
}

content = this.props.onPointerClick ? (
<Pill value={ dataValue } onClick={this.props.onPointerClick.bind(undefined, this.props.value)} followClick={true} />
) : (
dataValue
);

this.copyableValue = this.props.value.id;
}
else if (this.props.type === 'Array') {
if ( this.props.value[0] && typeof this.props.value[0] === 'object' && this.props.value[0].__type === 'Pointer' ) {
const array = [];
this.props.value.map( (v, i) => {
if ( typeof v !== 'object' || v.__type !== 'Pointer' ) {
throw new Error('Invalid type found in pointer array');
}
const object = new Parse.Object(v.className);
object.id = v.objectId;
array.push(
<Pill key={i} value={v.objectId} onClick={this.props.onPointerClick.bind(undefined, object)} followClick={true} />
);
});
this.copyableValue = content = <ul>
{ array.map( a => <li>{a}</li>) }
</ul>
if ( array.length > 1 ) {
classes.push(styles.hasMore);
}
}
else {
this.copyableValue = content = JSON.stringify(this.props.value);
}
}
else if (this.props.type === 'Date') {
if (typeof value === 'object' && this.props.value.__type) {
this.props.value = new Date(this.props.value.iso);
} else if (typeof value === 'string') {
this.props.value = new Date(this.props.value);
}
this.copyableValue = content = dateStringUTC(this.props.value);
} else if (this.props.type === 'Boolean') {
this.copyableValue = content = this.props.value ? 'True' : 'False';
} else if (this.props.type === 'Object' || this.props.type === 'Bytes') {
this.copyableValue = content = JSON.stringify(this.props.value);
} else if (this.props.type === 'File') {
const fileName = this.props.value.url() ? getFileName(this.props.value) : 'Uploading\u2026';
content = <Pill value={fileName} fileDownloadLink={this.props.value.url()} />;
this.copyableValue = fileName;
} else if (this.props.type === 'ACL') {
let pieces = [];
let json = this.props.value.toJSON();
if (Object.prototype.hasOwnProperty.call(json, '*')) {
if (json['*'].read && json['*'].write) {
pieces.push('Public Read + Write');
} else if (json['*'].read) {
pieces.push('Public Read');
} else if (json['*'].write) {
pieces.push('Public Write');
}
}
for (let role in json) {
if (role !== '*') {
pieces.push(role);
}
}
if (pieces.length === 0) {
pieces.push('Master Key Only');
}
this.copyableValue = content = pieces.join(', ');
} else if (this.props.type === 'GeoPoint') {
this.copyableValue = content = `(${this.props.value.latitude}, ${this.props.value.longitude})`;
} else if (this.props.type === 'Polygon') {
this.copyableValue = content = this.props.value.coordinates.map(coord => `(${coord})`)
} else if (this.props.type === 'Relation') {
content = this.props.setRelation ? (
<div style={{ textAlign: 'center' }}>
<Pill onClick={() => this.props.setRelation(this.props.value)} value='View relation' followClick={true} />
</div>
) : (
'Relation'
);
this.copyableValue = undefined;
}
this.onContextMenu = this.onContextMenu.bind(this);

if (this.props.markRequiredField && this.props.isRequired && !this.props.value) {
classes.push(styles.required);
}

this.setState({ ...this.state, content, classes })
}

componentDidUpdate(prevProps) {
async componentDidUpdate(prevProps) {
if ( this.props.value !== prevProps.value ) {
await this.renderCellContent();
}
if (this.props.current) {
const node = this.cellRef.current;
const { setRelation } = this.props;
Expand Down Expand Up @@ -58,7 +207,7 @@ export default class BrowserCell extends Component {
}

shouldComponentUpdate(nextProps, nextState) {
if (nextState.showTooltip !== this.state.showTooltip) {
if (nextState.showTooltip !== this.state.showTooltip || nextState.content !== this.state.content ) {
return true;
}
const shallowVerifyProps = [...new Set(Object.keys(this.props).concat(Object.keys(nextProps)))]
Expand Down Expand Up @@ -225,139 +374,27 @@ export default class BrowserCell extends Component {
})));
}

componentDidMount(){
this.renderCellContent();
}

//#endregion

render() {
let { type, value, hidden, width, current, onSelect, onEditChange, setCopyableValue, setRelation, onPointerClick, onPointerCmdClick, row, col, field, onEditSelectedRow, readonly, isRequired, markRequiredFieldRow } = this.props;
let content = value;
let { type, value, hidden, width, current, onSelect, onEditChange, setCopyableValue, onPointerCmdClick, row, col, field, onEditSelectedRow, readonly, isRequired, markRequiredFieldRow } = this.props;
let isNewRow = row < 0;
this.copyableValue = content;
let classes = [styles.cell, unselectable];
if (hidden) {
content = value !== undefined || !isNewRow ? '(hidden)' : isRequired ? '(required)' : '(undefined)';
classes.push(styles.empty);
} else if (value === undefined) {
if (type === 'ACL') {
this.copyableValue = content = 'Public Read + Write';
} else {
this.copyableValue = content = '(undefined)';
classes.push(styles.empty);
}
content = isNewRow && isRequired && value === undefined ? '(required)' : content;
} else if (value === null) {
this.copyableValue = content = '(null)';
classes.push(styles.empty);
} else if (value === '') {
content = <span>&nbsp;</span>;
classes.push(styles.empty);
} else if (type === 'Pointer') {
if (value && value.__type) {
const object = new Parse.Object(value.className);
object.id = value.objectId;
value = object;
}
content = onPointerClick ? (
<Pill
value={value.id}
onClick={onPointerClick.bind(undefined, value)}
followClick={true}
/>
) : (
value.id
);
this.copyableValue = value.id;
}
else if (type === 'Array') {
if (value[0] && typeof value[0] === 'object' && value[0].__type === 'Pointer') {
const array = [];
value.map((v, i) => {
if (typeof v !== 'object' || v.__type !== 'Pointer') {
throw new Error('Invalid type found in pointer array');
}
const object = new Parse.Object(v.className);
object.id = v.objectId;
array.push(
<Pill
key={v.objectId}
value={v.objectId}
onClick={onPointerClick.bind(undefined, object)}
followClick={true}
/>
);
});
content = <ul className={styles.hasMore}>
{array.map(a => <li>{a}</li>)}
</ul>
this.copyableValue = JSON.stringify(value);
if (array.length > 1) {
classes.push(styles.removePadding);
}
}
else {
this.copyableValue = content = JSON.stringify(value);
}
}
else if (type === 'Date') {
if (typeof value === 'object' && value.__type) {
value = new Date(value.iso);
} else if (typeof value === 'string') {
value = new Date(value);
}
this.copyableValue = content = dateStringUTC(value);
} else if (type === 'Boolean') {
this.copyableValue = content = value ? 'True' : 'False';
} else if (type === 'Object' || type === 'Bytes') {
this.copyableValue = content = JSON.stringify(value);
} else if (type === 'File') {
const fileName = value.url() ? getFileName(value) : 'Uploading\u2026';
content = <Pill value={fileName} fileDownloadLink={value.url()} />;
this.copyableValue = fileName;
} else if (type === 'ACL') {
let pieces = [];
let json = value.toJSON();
if (Object.prototype.hasOwnProperty.call(json, '*')) {
if (json['*'].read && json['*'].write) {
pieces.push('Public Read + Write');
} else if (json['*'].read) {
pieces.push('Public Read');
} else if (json['*'].write) {
pieces.push('Public Write');
}
}
for (let role in json) {
if (role !== '*') {
pieces.push(role);
}
}
if (pieces.length === 0) {
pieces.push('Master Key Only');
}
this.copyableValue = content = pieces.join(', ');
} else if (type === 'GeoPoint') {
this.copyableValue = content = `(${value.latitude}, ${value.longitude})`;
} else if (type === 'Polygon') {
this.copyableValue = content = value.coordinates.map(coord => `(${coord})`)
} else if (type === 'Relation') {
content = setRelation ? (
<div style={{ textAlign: 'center' }}>
<Pill onClick={() => setRelation(value)} value='View relation' followClick={true} />
</div>
) : (
'Relation'
);
this.copyableValue = undefined;
}

if (current) {
let classes = [...this.state.classes];

if ( current ) {
classes.push(styles.current);
}

if (markRequiredFieldRow === row && isRequired && !value) {
classes.push(styles.required);
}

return readonly ? (
<Tooltip placement='bottom' tooltip='Read only (CTRL+C to copy)' visible={this.state.showTooltip} >
<Tooltip placement='bottom' tooltip='Read only (CTRL+C to copy)' visible={this.state.showTooltip}>
<span
ref={this.cellRef}
className={classes.join(' ')}
Expand All @@ -382,7 +419,7 @@ export default class BrowserCell extends Component {
}}
onContextMenu={this.onContextMenu}
>
{isNewRow ? '(auto)' : content}
{row < 0 || isNewRow ? '(auto)' : this.state.content}
</span>
</Tooltip>
) : (
Expand Down Expand Up @@ -413,13 +450,11 @@ export default class BrowserCell extends Component {
if (['ACL', 'Boolean', 'File'].includes(type)) {
e.preventDefault();
}
onEditChange(true);
}
}}
onContextMenu={this.onContextMenu}
>
{content}
</span>
}}}
onContextMenu={this.onContextMenu.bind(this)}
>
{this.state.content}
</span>
);
}
}
Loading