Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
78 changes: 15 additions & 63 deletions src/components/BrowserCell/BrowserCell.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import * as Filters from 'lib/Filters';
import { List, Map } from 'immutable';
import { dateStringUTC } from 'lib/DateUtils';
import getFileName from 'lib/getFileName';
import { getValidScripts, executeScript } from 'lib/ScriptUtils';
import Parse from 'parse';
import Pill from 'components/Pill/Pill.react';
import React, { Component } from 'react';
import ScriptConfirmationModal from 'components/ScriptConfirmationModal/ScriptConfirmationModal.react';
import styles from 'components/BrowserCell/BrowserCell.scss';
import baseStyles from 'stylesheets/base.scss';
import * as ColumnPreferences from 'lib/ColumnPreferences';
import labelStyles from 'components/Label/Label.scss';
import Modal from 'components/Modal/Modal.react';

export default class BrowserCell extends Component {
constructor() {
Expand Down Expand Up @@ -348,33 +348,8 @@ export default class BrowserCell extends Component {
}

const { className, objectId, field, scripts = [], rowValue } = this.props;
let validator = null;
const validScripts = (scripts || []).filter(script => {
if (script.classes?.includes(className)) {
return true;
}
for (const script of script?.classes || []) {
if (script?.name !== className) {
continue;
}
const fields = script?.fields || [];
if (script?.fields.includes(field) || script?.fields.includes('*')) {
return true;
}
for (const currentField of fields) {
if (Object.prototype.toString.call(currentField) === '[object Object]') {
if (currentField.name === field) {
if (typeof currentField.validator === 'string') {
validator = eval(currentField.validator);
} else {
validator = currentField.validator;
}
return true;
}
}
}
}
});
const { validScripts, validator } = getValidScripts(scripts, className, field);

if (validScripts.length) {
onEditSelectedRow &&
contextMenuOptions.push({
Expand All @@ -400,24 +375,13 @@ export default class BrowserCell extends Component {
}

async executeScript(script) {
try {
const object = Parse.Object.extend(this.props.className).createWithoutData(
this.props.objectId
);
const response = await Parse.Cloud.run(
script.cloudCodeFunction,
{ object: object.toPointer() },
{ useMasterKey: true }
);
this.props.showNote(
response ||
`Ran script "${script.title}" on "${this.props.className}" object "${object.id}".`
);
this.props.onRefresh();
} catch (e) {
this.props.showNote(e.message, true);
console.log(`Could not run ${script.title}: ${e}`);
}
await executeScript(
script,
this.props.className,
this.props.objectId,
this.props.showNote,
this.props.onRefresh
);
}

toggleConfirmationDialog() {
Expand Down Expand Up @@ -590,26 +554,14 @@ export default class BrowserCell extends Component {
let extras = null;
if (this.state.showConfirmationDialog) {
extras = (
<Modal
type={
this.selectedScript.confirmationDialogStyle === 'critical'
? Modal.Types.DANGER
: Modal.Types.INFO
}
icon="warn-outline"
title={this.selectedScript.title}
confirmText="Continue"
cancelText="Cancel"
<ScriptConfirmationModal
script={this.selectedScript}
onCancel={() => this.toggleConfirmationDialog()}
onConfirm={() => {
this.executeSript(this.selectedScript);
this.executeScript(this.selectedScript);
this.toggleConfirmationDialog();
}}
>
<div className={[labelStyles.label, labelStyles.text, styles.action].join(' ')}>
{`Do you want to run script "${this.selectedScript.title}" on "${this.selectedScript.className}" object "${this.selectedScript.objectId}"?`}
</div>
</Modal>
/>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2016-present, Parse, LLC
* All rights reserved.
*
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/
import React from 'react';
import Modal from 'components/Modal/Modal.react';
import labelStyles from 'components/Label/Label.scss';
import browserCellStyles from 'components/BrowserCell/BrowserCell.scss';

/**
* Confirmation dialog for executing scripts
*/
export default function ScriptConfirmationModal({ script, onConfirm, onCancel }) {
if (!script) {
return null;
}

return (
<Modal
type={script.confirmationDialogStyle === 'critical' ? Modal.Types.DANGER : Modal.Types.INFO}
icon="warn-outline"
title={script.title}
confirmText="Continue"
cancelText="Cancel"
onCancel={onCancel}
onConfirm={onConfirm}
>
<div className={[labelStyles.label, labelStyles.text, browserCellStyles.action].join(' ')}>
{`Do you want to run script "${script.title}" on "${script.className}" object "${script.objectId}"?`}
</div>
</Modal>
);
}
76 changes: 76 additions & 0 deletions src/dashboard/Data/Browser/DataBrowser.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import copy from 'copy-to-clipboard';
import BrowserTable from 'dashboard/Data/Browser/BrowserTable.react';
import BrowserToolbar from 'dashboard/Data/Browser/BrowserToolbar.react';
import * as ColumnPreferences from 'lib/ColumnPreferences';
import { CurrentApp } from 'context/currentApp';
import { dateStringUTC } from 'lib/DateUtils';
import getFileName from 'lib/getFileName';
import { getValidScripts, executeScript } from '../../../lib/ScriptUtils';
import Parse from 'parse';
import React from 'react';
import { ResizableBox } from 'react-resizable';
import ScriptConfirmationModal from '../../../components/ScriptConfirmationModal/ScriptConfirmationModal.react';
import styles from './Databrowser.scss';

import AggregationPanel from '../../../components/AggregationPanel/AggregationPanel';
Expand Down Expand Up @@ -76,6 +79,8 @@ function formatValueForCopy(value, type) {
* and the keyboard interactions for the data table.
*/
export default class DataBrowser extends React.Component {
static contextType = CurrentApp;

constructor(props) {
super(props);

Expand Down Expand Up @@ -141,6 +146,11 @@ export default class DataBrowser extends React.Component {
multiPanelData: {}, // Object mapping objectId to panel data
_objectsToFetch: [], // Temporary field for async fetch handling
loadingObjectIds: new Set(),
showScriptConfirmationDialog: false,
selectedScript: null,
contextMenuX: null,
contextMenuY: null,
contextMenuItems: null,
};

this.handleResizeDiv = this.handleResizeDiv.bind(this);
Expand Down Expand Up @@ -172,6 +182,7 @@ export default class DataBrowser extends React.Component {
this.addPanel = this.addPanel.bind(this);
this.removePanel = this.removePanel.bind(this);
this.handlePanelScroll = this.handlePanelScroll.bind(this);
this.handlePanelHeaderContextMenu = this.handlePanelHeaderContextMenu.bind(this);
this.handleWrapperWheel = this.handleWrapperWheel.bind(this);
this.saveOrderTimeout = null;
this.aggregationPanelRef = React.createRef();
Expand Down Expand Up @@ -962,6 +973,51 @@ export default class DataBrowser extends React.Component {
this.setState({ contextMenuX, contextMenuY, contextMenuItems });
}

handlePanelHeaderContextMenu(event, objectId) {
const { scripts = [] } = this.context || {};
const className = this.props.className;
const field = 'objectId';

const { validScripts, validator } = getValidScripts(scripts, className, field);

const menuItems = [];

// Add Scripts menu if there are valid scripts
if (validScripts.length && this.props.onEditSelectedRow) {
menuItems.push({
text: 'Scripts',
items: validScripts.map(script => {
return {
text: script.title,
disabled: validator?.(objectId, field) === false,
callback: () => {
const selectedScript = { ...script, className, objectId };
if (script.showConfirmationDialog) {
this.setState({
showScriptConfirmationDialog: true,
selectedScript
});
} else {
executeScript(
script,
className,
objectId,
this.props.showNote,
this.props.onRefresh
);
}
},
};
}),
});
}

const { pageX, pageY } = event;
if (menuItems.length) {
this.setContextMenu(pageX, pageY, menuItems);
}
}
Comment on lines +976 to +1019
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add defensive check for context access.

Line 977 destructures scripts from this.context without verifying that this.context is not null. If the component is used outside a CurrentApp provider, this will throw an error.

Apply this diff:

 handlePanelHeaderContextMenu(event, objectId) {
-  const { scripts = [] } = this.context || {};
+  const { scripts = [] } = this.context ?? {};
   const className = this.props.className;
   const field = 'objectId';

Note: The ?? operator provides the same protection but is more idiomatic for null/undefined checks.

🤖 Prompt for AI Agents
In src/dashboard/Data/Browser/DataBrowser.react.js around lines 976 to 1019, the
code destructures scripts from this.context without guarding against
this.context being null/undefined; update the destructuring to safely handle a
missing context (e.g. use the nullish coalescing operator to default to an empty
object before destructuring, so scripts defaults to an empty array) and ensure
downstream logic uses that safe value.


freezeColumns(index) {
this.setState({ frozenColumnIndex: index });
}
Expand Down Expand Up @@ -1645,6 +1701,10 @@ export default class DataBrowser extends React.Component {
onMouseDown={(e) => {
e.preventDefault();
}}
onContextMenu={(e) => {
e.preventDefault();
this.handlePanelHeaderContextMenu(e, objectId);
}}
>
<input
type="checkbox"
Expand Down Expand Up @@ -1746,6 +1806,22 @@ export default class DataBrowser extends React.Component {
items={this.state.contextMenuItems}
/>
)}
{this.state.showScriptConfirmationDialog && (
<ScriptConfirmationModal
script={this.state.selectedScript}
onCancel={() => this.setState({ showScriptConfirmationDialog: false, selectedScript: null })}
onConfirm={() => {
executeScript(
this.state.selectedScript,
this.state.selectedScript.className,
this.state.selectedScript.objectId,
this.props.showNote,
this.props.onRefresh
);
this.setState({ showScriptConfirmationDialog: false, selectedScript: null });
}}
/>
)}
</div>
);
}
Expand Down
79 changes: 79 additions & 0 deletions src/lib/ScriptUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2016-present, Parse, LLC
* All rights reserved.
*
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/
import Parse from 'parse';

/**
* Filters scripts to only those valid for the given className and field
* @param {Array} scripts - Array of script configurations
* @param {string} className - The Parse class name
* @param {string} field - The field name
* @returns {Object} - { validScripts: Array, validator: Function|null }
*/
export function getValidScripts(scripts, className, field) {
let validator = null;
const validScripts = (scripts || []).filter(script => {
if (script.classes?.includes(className)) {
return true;
}
for (const scriptClass of script?.classes || []) {
if (scriptClass?.name !== className) {
continue;
}
const fields = scriptClass?.fields || [];
if (scriptClass?.fields.includes(field) || scriptClass?.fields.includes('*')) {
return true;
}
for (const currentField of fields) {
if (Object.prototype.toString.call(currentField) === '[object Object]') {
if (currentField.name === field) {
if (typeof currentField.validator === 'string') {
// SAFETY: eval() is used here on validator strings from trusted admin-controlled
// dashboard configuration only (not user input). These validators are used solely
// for UI validation logic to enable/disable script menu items. This is an accepted
// tradeoff in this trusted admin context. If requirements change, consider replacing
// with Function constructor or a safer expression parser.
validator = eval(currentField.validator);
} else {
validator = currentField.validator;
}
return true;
}
}
}
}
return false;
});

return { validScripts, validator };
}

/**
* Executes a Parse Cloud Code script
* @param {Object} script - The script configuration
* @param {string} className - The Parse class name
* @param {string} objectId - The object ID
* @param {Function} showNote - Callback to show notification
* @param {Function} onRefresh - Callback to refresh data
*/
export async function executeScript(script, className, objectId, showNote, onRefresh) {
try {
const object = Parse.Object.extend(className).createWithoutData(objectId);
const response = await Parse.Cloud.run(
script.cloudCodeFunction,
{ object: object.toPointer() },
{ useMasterKey: true }
);
showNote?.(
response || `Ran script "${script.title}" on "${className}" object "${object.id}".`
);
onRefresh?.();
} catch (e) {
showNote?.(e.message, true);
console.error(`Could not run ${script.title}:`, e);
}
}
Loading