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
54 changes: 3 additions & 51 deletions src/components/BrowserCell/BrowserCell.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { List, Map } from 'immutable';
import { dateStringUTC } from 'lib/DateUtils';
import getFileName from 'lib/getFileName';
import { getValidScripts, executeScript } from 'lib/ScriptUtils';
import { buildRelatedTextFieldsMenuItem } from 'lib/RelatedRecordsUtils';
import Parse from 'parse';
import Pill from 'components/Pill/Pill.react';
import React, { Component } from 'react';
Expand Down Expand Up @@ -521,60 +522,11 @@ export default class BrowserCell extends Component {

// Only show for String type cells with a non-empty value
// Exclude objectId field - it uses getRelatedObjectsContextMenuOption() for pointer-based lookups
if (type !== 'String' || field === 'objectId' || !value || typeof value !== 'string' || value.trim() === '') {
if (type !== 'String' || field === 'objectId') {
return;
}

const relatedRecordsMenuItem = {
text: 'Related records',
items: [],
};

// Group fields by class name for hierarchical navigation
schema.data
.get('classes')
.sortBy((_v, k) => k)
.forEach((cl, className) => {
const classFields = [];

cl.forEach((column, field) => {
if (column.type !== 'String') {
return;
}
// Exclude objectId - it's a special field referenced by pointers, not strings
if (field === 'objectId') {
return;
}
// Exclude hidden/sensitive fields
if (field === 'password' && className === '_User') {
return;
}
if (field === 'sessionToken' && (className === '_User' || className === '_Session')) {
return;
}
classFields.push({
text: field,
callback: () => {
onPointerClick({
className,
id: value,
field,
});
},
});
});

if (classFields.length > 0) {
// Sort fields alphabetically
classFields.sort((a, b) => a.text.localeCompare(b.text));
relatedRecordsMenuItem.items.push({
text: className,
items: classFields,
});
}
});

return relatedRecordsMenuItem.items.length ? relatedRecordsMenuItem : undefined;
return buildRelatedTextFieldsMenuItem(schema, value, onPointerClick);
}

/**
Expand Down
41 changes: 30 additions & 11 deletions src/components/StringEditor/StringEditor.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,15 @@ export default class StringEditor extends React.Component {
}

handleContextMenu(e) {
const { setContextMenu, arrayConfigParams, onAddToArrayConfig } = this.props;
const { setContextMenu, arrayConfigParams, onAddToArrayConfig, getRelatedRecordsMenuItem } = this.props;

// Only show custom context menu when Alt key is held
if (!e.altKey) {
return;
}

// Check if there are array config params available
if (!arrayConfigParams || arrayConfigParams.length === 0 || !onAddToArrayConfig || !setContextMenu) {
// Check if setContextMenu is available
if (!setContextMenu) {
return;
}

Expand All @@ -84,22 +84,41 @@ export default class StringEditor extends React.Component {
return;
}

// Prevent default context menu
e.preventDefault();
e.stopPropagation();

// Build context menu items
const menuItems = [
{
const menuItems = [];

// Add "Add to config parameter" option if available
if (arrayConfigParams && arrayConfigParams.length > 0 && onAddToArrayConfig) {
menuItems.push({
text: 'Add to config parameter...',
items: arrayConfigParams.map(param => ({
text: param.name,
callback: () => {
onAddToArrayConfig(param.name, selectedText);
},
})),
},
];
});
}

// Add "Related records" option if available (using selected text)
if (getRelatedRecordsMenuItem) {
const relatedRecordsItem = getRelatedRecordsMenuItem(selectedText);
if (relatedRecordsItem) {
if (menuItems.length > 0) {
menuItems.push({ type: 'separator' });
}
menuItems.push(relatedRecordsItem);
}
}

// Only show context menu if there are items
if (menuItems.length === 0) {
return;
}

// Prevent default context menu
e.preventDefault();
e.stopPropagation();

setContextMenu(e.pageX, e.pageY, menuItems);
}
Expand Down
1 change: 1 addition & 0 deletions src/dashboard/Data/Browser/BrowserTable.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ export default class BrowserTable extends React.Component {
setContextMenu={this.props.setContextMenu}
arrayConfigParams={this.props.arrayConfigParams}
onAddToArrayConfig={this.props.onAddToArrayConfig}
getRelatedRecordsMenuItem={this.props.getRelatedRecordsMenuItem}
/>
);
}
Expand Down
49 changes: 35 additions & 14 deletions src/dashboard/Data/Browser/DataBrowser.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import styles from './Databrowser.scss';
import KeyboardShortcutsManager, { matchesShortcut } from 'lib/KeyboardShortcutsPreferences';

import AggregationPanel from '../../../components/AggregationPanel/AggregationPanel';
import { buildRelatedTextFieldsMenuItem } from '../../../lib/RelatedRecordsUtils';

const BROWSER_SHOW_ROW_NUMBER = 'browserShowRowNumber';
const AGGREGATION_PANEL_VISIBLE = 'aggregationPanelVisible';
Expand Down Expand Up @@ -1165,19 +1166,13 @@ export default class DataBrowser extends React.Component {
return;
}

// Get array config params from props
const arrayParams = this.props.arrayConfigParams || [];

if (arrayParams.length === 0) {
return;
}

// Prevent default context menu
event.preventDefault();

// Build context menu items
const menuItems = [
{
const menuItems = [];

// Add "Add to config parameter" option if available
const arrayParams = this.props.arrayConfigParams || [];
if (arrayParams.length > 0) {
menuItems.push({
text: 'Add to config parameter',
items: arrayParams.map(param => ({
text: param.name,
Expand All @@ -1187,8 +1182,29 @@ export default class DataBrowser extends React.Component {
}
},
})),
},
];
});
}

// Add "Related records" option if available (search text in String fields)
const relatedRecordsItem = buildRelatedTextFieldsMenuItem(
this.props.schema,
selectedText,
this.props.onPointerCmdClick
);
if (relatedRecordsItem) {
if (menuItems.length > 0) {
menuItems.push({ type: 'separator' });
}
menuItems.push(relatedRecordsItem);
}

// Only show context menu if there are items
if (menuItems.length === 0) {
return;
}

// Prevent default context menu
event.preventDefault();

this.setContextMenu(event.pageX, event.pageY, menuItems);
}
Expand Down Expand Up @@ -2032,6 +2048,11 @@ export default class DataBrowser extends React.Component {
setSelectedObjectId={this.setSelectedObjectId}
callCloudFunction={this.handleCallCloudFunction}
setContextMenu={this.setContextMenu}
getRelatedRecordsMenuItem={(textValue) => buildRelatedTextFieldsMenuItem(
this.props.schema,
textValue,
this.props.onPointerCmdClick
)}
freezeIndex={this.state.frozenColumnIndex}
freezeColumns={this.freezeColumns}
unfreezeColumns={this.unfreezeColumns}
Expand Down
8 changes: 6 additions & 2 deletions src/dashboard/Data/Browser/Editor.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import decode from 'parse/lib/browser/decode';
import React from 'react';
import StringEditor from 'components/StringEditor/StringEditor.react';

const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit, onCancel, setContextMenu, arrayConfigParams, onAddToArrayConfig }) => {
const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit, onCancel, setContextMenu, arrayConfigParams, onAddToArrayConfig, getRelatedRecordsMenuItem }) => {
let content = null;
if (type === 'String') {
content = (
Expand All @@ -31,6 +31,7 @@ const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit
setContextMenu={setContextMenu}
arrayConfigParams={arrayConfigParams}
onAddToArrayConfig={onAddToArrayConfig}
getRelatedRecordsMenuItem={getRelatedRecordsMenuItem}
/>
);
} else if (type === 'Array' || type === 'Object') {
Expand All @@ -53,6 +54,7 @@ const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit
setContextMenu={setContextMenu}
arrayConfigParams={arrayConfigParams}
onAddToArrayConfig={onAddToArrayConfig}
getRelatedRecordsMenuItem={getRelatedRecordsMenuItem}
/>
);
} else if (type === 'Polygon') {
Expand Down Expand Up @@ -95,6 +97,7 @@ const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit
setContextMenu={setContextMenu}
arrayConfigParams={arrayConfigParams}
onAddToArrayConfig={onAddToArrayConfig}
getRelatedRecordsMenuItem={getRelatedRecordsMenuItem}
/>
);
} else if (type === 'Date') {
Expand All @@ -109,6 +112,7 @@ const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit
setContextMenu={setContextMenu}
arrayConfigParams={arrayConfigParams}
onAddToArrayConfig={onAddToArrayConfig}
getRelatedRecordsMenuItem={getRelatedRecordsMenuItem}
/>
);
} else {
Expand Down Expand Up @@ -137,7 +141,7 @@ const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit
);
}
};
content = <StringEditor value={value ? value.id : ''} width={width} onCommit={encodeCommit} onCancel={onCancel} setContextMenu={setContextMenu} arrayConfigParams={arrayConfigParams} onAddToArrayConfig={onAddToArrayConfig} />;
content = <StringEditor value={value ? value.id : ''} width={width} onCommit={encodeCommit} onCancel={onCancel} setContextMenu={setContextMenu} arrayConfigParams={arrayConfigParams} onAddToArrayConfig={onAddToArrayConfig} getRelatedRecordsMenuItem={getRelatedRecordsMenuItem} />;
}

return <div style={{ position: 'absolute', top: top, left: left }}>{content}</div>;
Expand Down
73 changes: 73 additions & 0 deletions src/lib/RelatedRecordsUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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.
*/

/**
* Builds a "Related records" context menu item for a text value.
* Finds all classes with String type fields that can be filtered by this value.
* Groups fields by class name in a hierarchical submenu structure.
*
* @param {Object} schema - The schema object containing class definitions
* @param {string} textValue - The text value to search for
* @param {Function} onNavigate - Callback function to navigate to the related records
* @returns {Object|undefined} The menu item object or undefined if no fields found
*/
export function buildRelatedTextFieldsMenuItem(schema, textValue, onNavigate) {
if (!textValue || typeof textValue !== 'string' || textValue.trim() === '' || !schema || !onNavigate) {
return undefined;
}

const relatedRecordsMenuItem = {
text: 'Related records',
items: [],
};

schema.data
.get('classes')
.sortBy((_v, k) => k)
.forEach((cl, className) => {
const classFields = [];

cl.forEach((column, field) => {
if (column.type !== 'String') {
return;
}
// Exclude objectId - it's a special field referenced by pointers, not strings
if (field === 'objectId') {
return;
}
// Exclude hidden/sensitive fields
if (field === 'password' && className === '_User') {
return;
}
if (field === 'sessionToken' && (className === '_User' || className === '_Session')) {
return;
}
classFields.push({
text: field,
callback: () => {
onNavigate({
className,
id: textValue,
field,
});
},
});
});

if (classFields.length > 0) {
// Sort fields alphabetically
classFields.sort((a, b) => a.text.localeCompare(b.text));
relatedRecordsMenuItem.items.push({
text: className,
items: classFields,
});
}
});

return relatedRecordsMenuItem.items.length ? relatedRecordsMenuItem : undefined;
}
Loading