From acb4398013cc7e36972e5e4920eba9e44092ef85 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 23 Jan 2026 02:52:59 +0100 Subject: [PATCH] fix --- .../BrowserCell/BrowserCell.react.js | 54 +------------- .../StringEditor/StringEditor.react.js | 41 ++++++++--- .../Data/Browser/BrowserTable.react.js | 1 + .../Data/Browser/DataBrowser.react.js | 49 +++++++++---- src/dashboard/Data/Browser/Editor.react.js | 8 +- src/lib/RelatedRecordsUtils.js | 73 +++++++++++++++++++ 6 files changed, 148 insertions(+), 78 deletions(-) create mode 100644 src/lib/RelatedRecordsUtils.js diff --git a/src/components/BrowserCell/BrowserCell.react.js b/src/components/BrowserCell/BrowserCell.react.js index e4b03e1911..9a05c4c48c 100644 --- a/src/components/BrowserCell/BrowserCell.react.js +++ b/src/components/BrowserCell/BrowserCell.react.js @@ -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'; @@ -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); } /** diff --git a/src/components/StringEditor/StringEditor.react.js b/src/components/StringEditor/StringEditor.react.js index f8c2745442..c4067a3558 100644 --- a/src/components/StringEditor/StringEditor.react.js +++ b/src/components/StringEditor/StringEditor.react.js @@ -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; } @@ -84,13 +84,12 @@ 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, @@ -98,8 +97,28 @@ export default class StringEditor extends React.Component { 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); } diff --git a/src/dashboard/Data/Browser/BrowserTable.react.js b/src/dashboard/Data/Browser/BrowserTable.react.js index d8c716657f..79a509a2c8 100644 --- a/src/dashboard/Data/Browser/BrowserTable.react.js +++ b/src/dashboard/Data/Browser/BrowserTable.react.js @@ -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} /> ); } diff --git a/src/dashboard/Data/Browser/DataBrowser.react.js b/src/dashboard/Data/Browser/DataBrowser.react.js index 0a47bae695..7b2a55f0eb 100644 --- a/src/dashboard/Data/Browser/DataBrowser.react.js +++ b/src/dashboard/Data/Browser/DataBrowser.react.js @@ -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'; @@ -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, @@ -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); } @@ -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} diff --git a/src/dashboard/Data/Browser/Editor.react.js b/src/dashboard/Data/Browser/Editor.react.js index 60aee64476..0a5ea3716d 100644 --- a/src/dashboard/Data/Browser/Editor.react.js +++ b/src/dashboard/Data/Browser/Editor.react.js @@ -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 = ( @@ -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') { @@ -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') { @@ -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') { @@ -109,6 +112,7 @@ const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit setContextMenu={setContextMenu} arrayConfigParams={arrayConfigParams} onAddToArrayConfig={onAddToArrayConfig} + getRelatedRecordsMenuItem={getRelatedRecordsMenuItem} /> ); } else { @@ -137,7 +141,7 @@ const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit ); } }; - content = ; + content = ; } return
{content}
; diff --git a/src/lib/RelatedRecordsUtils.js b/src/lib/RelatedRecordsUtils.js new file mode 100644 index 0000000000..3558b15c65 --- /dev/null +++ b/src/lib/RelatedRecordsUtils.js @@ -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; +}