Skip to content

Commit 64d3913

Browse files
authored
feat: Add support for confirmation dialog before script execution in data browser (#2481)
1 parent 359ebdc commit 64d3913

File tree

3 files changed

+80
-20
lines changed

3 files changed

+80
-20
lines changed

README.md

+22-7
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
3131
- [Parse Server](#parse-server)
3232
- [Node.js](#nodejs)
3333
- [Configuring Parse Dashboard](#configuring-parse-dashboard)
34+
- [Options](#options)
3435
- [File](#file)
3536
- [Environment variables](#environment-variables)
3637
- [Multiple apps](#multiple-apps)
@@ -42,6 +43,7 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
4243
- [Other Configuration Options](#other-configuration-options)
4344
- [Prevent columns sorting](#prevent-columns-sorting)
4445
- [Custom order in the filter popup](#custom-order-in-the-filter-popup)
46+
- [Persistent Filters](#persistent-filters)
4547
- [Scripts](#scripts)
4648
- [Running as Express Middleware](#running-as-express-middleware)
4749
- [Deploying Parse Dashboard](#deploying-parse-dashboard)
@@ -103,14 +105,26 @@ Parse Dashboard is compatible with the following Parse Server versions.
103105
### Node.js
104106
Parse Dashboard is continuously tested with the most recent releases of Node.js to ensure compatibility. We follow the [Node.js Long Term Support plan](https://github.com/nodejs/Release) and only test against versions that are officially supported and have not reached their end-of-life date.
105107

106-
| Version | Latest Version | End-of-Life | Compatible |
107-
|------------|----------------|-------------|--------------|
108-
| Node.js 14 | 14.20.1 | April 2023 | ✅ Yes |
109-
| Node.js 16 | 16.17.0 | April 2024 | ✅ Yes |
110-
| Node.js 18 | 18.9.0 | May 2025 | ✅ Yes |
108+
| Version | Latest Version | End-of-Life | Compatible |
109+
|------------|----------------|-------------|------------|
110+
| Node.js 14 | 14.20.1 | April 2023 | ✅ Yes |
111+
| Node.js 16 | 16.17.0 | April 2024 | ✅ Yes |
112+
| Node.js 18 | 18.9.0 | May 2025 | ✅ Yes |
111113

112114
## Configuring Parse Dashboard
113115

116+
### Options
117+
118+
| Parameter | Type | Optional | Default | Example | Description |
119+
|----------------------------------------|---------------------|----------|---------|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
120+
| `apps` | Array<Object> | no | - | `[{ ... }, { ... }]` | The apps that are configured for the dashboard. |
121+
| `apps.scripts` | Array<Object> | yes | `[]` | `[{ ... }, { ... }]` | The scripts that can be executed for that app. |
122+
| `apps.scripts.title` | String | no | - | `'Delete User'` | The title that will be displayed in the data browser context menu and the script run confirmation dialog. |
123+
| `apps.scripts.classes` | Array<String> | no | - | `['_User']` | The classes of Parse Objects for which the scripts can be executed. |
124+
| `apps.scripts.cloudCodeFunction` | String | no | - | `'deleteUser'` | The name of the Parse Cloud Function to execute. |
125+
| `apps.scripts.showConfirmationDialog` | Bool | yes | `false` | `true` | Is `true` if a confirmation dialog should be displayed before the script is executed, `false` if the script should be executed immediately. |
126+
| `apps.scripts.confirmationDialogStyle` | String | yes | `info` | `critical` | The style of the confirmation dialog. Valid values: `info` (blue style), `critical` (red style). |
127+
114128
### File
115129

116130
You can also start the dashboard from the command line with a config file. To do this, create a new file called `parse-dashboard-config.json` inside your local Parse Dashboard directory hierarchy. The file should match the following format:
@@ -367,15 +381,16 @@ You can conveniently create a filter definition without having to write it by ha
367381

368382
You can specify scripts to execute Cloud Functions with the `scripts` option:
369383

370-
371384
```json
372385
"apps": [
373386
{
374387
"scripts": [
375388
{
376389
"title": "Delete Account",
377390
"classes": ["_User"],
378-
"cloudCodeFunction": "deleteAccount"
391+
"cloudCodeFunction": "deleteAccount",
392+
"showConfirmationDialog": true,
393+
"confirmationDialogStyle": "critical"
379394
}
380395
]
381396
}

src/components/BrowserCell/BrowserCell.react.js

+52-12
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,21 @@ import React, { Component } from 'react';
1515
import styles from 'components/BrowserCell/BrowserCell.scss';
1616
import baseStyles from 'stylesheets/base.scss';
1717
import * as ColumnPreferences from 'lib/ColumnPreferences';
18+
import labelStyles from 'components/Label/Label.scss';
19+
import Modal from 'components/Modal/Modal.react';
20+
1821
export default class BrowserCell extends Component {
1922
constructor() {
2023
super();
2124

2225
this.cellRef = React.createRef();
2326
this.copyableValue = undefined;
27+
this.selectedScript = null;
2428
this.state = {
2529
showTooltip: false,
2630
content: null,
27-
classes: []
31+
classes: [],
32+
showConfirmationDialog: false,
2833
};
2934
}
3035

@@ -208,7 +213,7 @@ export default class BrowserCell extends Component {
208213
}
209214

210215
shouldComponentUpdate(nextProps, nextState) {
211-
if (nextState.showTooltip !== this.state.showTooltip || nextState.content !== this.state.content ) {
216+
if (nextState.showTooltip !== this.state.showTooltip || nextState.content !== this.state.content || nextState.showConfirmationDialog !== this.state.showConfirmationDialog) {
212217
return true;
213218
}
214219
const shallowVerifyProps = [...new Set(Object.keys(this.props).concat(Object.keys(nextProps)))]
@@ -278,23 +283,20 @@ export default class BrowserCell extends Component {
278283
});
279284
}
280285

286+
const { className, objectId } = this.props;
281287
const validScripts = (this.props.scripts || []).filter(script => script.classes?.includes(this.props.className));
282288
if (validScripts.length) {
283289
onEditSelectedRow && contextMenuOptions.push({
284290
text: 'Scripts',
285291
items: validScripts.map(script => {
286292
return {
287293
text: script.title,
288-
callback: async () => {
289-
try {
290-
const object = Parse.Object.extend(this.props.className).createWithoutData(this.props.objectId);
291-
const response = await Parse.Cloud.run(script.cloudCodeFunction, {object: object.toPointer(), selectedField: this.props.field}, {useMasterKey: true});
292-
this.props.showNote(response || `${script.title} ran with object ${object.id}}`);
293-
this.props.onRefresh();
294-
} catch (e) {
295-
this.props.showNote(e.message, true);
296-
console.log(`Could not run ${script.title}: ${e}`);
297-
}
294+
callback: () => {
295+
this.selectedScript = { ...script, className, objectId };
296+
if(script.showConfirmationDialog)
297+
this.toggleConfirmationDialog();
298+
else
299+
this.executeSript(script);
298300
}
299301
}
300302
})
@@ -304,6 +306,22 @@ export default class BrowserCell extends Component {
304306
return contextMenuOptions;
305307
}
306308

309+
async executeSript(script) {
310+
try {
311+
const object = Parse.Object.extend(this.props.className).createWithoutData(this.props.objectId);
312+
const response = await Parse.Cloud.run(script.cloudCodeFunction, {object: object.toPointer()}, {useMasterKey: true});
313+
this.props.showNote(response || `Ran script "${script.title}" on "${this.props.className}" object "${object.id}".`);
314+
this.props.onRefresh();
315+
} catch (e) {
316+
this.props.showNote(e.message, true);
317+
console.log(`Could not run ${script.title}: ${e}`);
318+
}
319+
}
320+
321+
toggleConfirmationDialog(){
322+
this.setState((prevState) => ({ showConfirmationDialog: !prevState.showConfirmationDialog }));
323+
}
324+
307325
getSetFilterContextMenuOption(constraints) {
308326
if (constraints) {
309327
return {
@@ -423,6 +441,27 @@ export default class BrowserCell extends Component {
423441
classes.push(styles.required);
424442
}
425443

444+
let extras = null;
445+
if (this.state.showConfirmationDialog)
446+
extras = (
447+
<Modal
448+
type={this.selectedScript.confirmationDialogStyle === 'critical' ? Modal.Types.DANGER : Modal.Types.INFO}
449+
icon="warn-outline"
450+
title={this.selectedScript.title}
451+
confirmText="Continue"
452+
cancelText="Cancel"
453+
onCancel={() => this.toggleConfirmationDialog()}
454+
onConfirm={() => {
455+
this.executeSript(this.selectedScript);
456+
this.toggleConfirmationDialog();
457+
}}
458+
>
459+
<div className={[labelStyles.label, labelStyles.text, styles.action].join(' ')}>
460+
{`Do you want to run script "${this.selectedScript.title}" on "${this.selectedScript.className}" object "${this.selectedScript.objectId}"?`}
461+
</div>
462+
</Modal>
463+
);
464+
426465
return <span
427466
ref={this.cellRef}
428467
className={classes.join(' ')}
@@ -454,6 +493,7 @@ export default class BrowserCell extends Component {
454493
onContextMenu={this.onContextMenu.bind(this)}
455494
>
456495
{this.state.content}
496+
{extras}
457497
</span>
458498
}
459499
}

src/components/BrowserCell/BrowserCell.scss

+6-1
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,9 @@
7777

7878
.readonly {
7979
color: #04263bd1;
80-
}
80+
}
81+
82+
.action {
83+
padding: 28px;
84+
border-style: solid;
85+
}

0 commit comments

Comments
 (0)