Skip to content

Commit 33d3595

Browse files
authored
feat: Add context menu item to get related records for selected text in data browser cell (#3142)
1 parent f25709a commit 33d3595

File tree

6 files changed

+148
-78
lines changed

6 files changed

+148
-78
lines changed

src/components/BrowserCell/BrowserCell.react.js

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { List, Map } from 'immutable';
1010
import { dateStringUTC } from 'lib/DateUtils';
1111
import getFileName from 'lib/getFileName';
1212
import { getValidScripts, executeScript } from 'lib/ScriptUtils';
13+
import { buildRelatedTextFieldsMenuItem } from 'lib/RelatedRecordsUtils';
1314
import Parse from 'parse';
1415
import Pill from 'components/Pill/Pill.react';
1516
import React, { Component } from 'react';
@@ -521,60 +522,11 @@ export default class BrowserCell extends Component {
521522

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

528-
const relatedRecordsMenuItem = {
529-
text: 'Related records',
530-
items: [],
531-
};
532-
533-
// Group fields by class name for hierarchical navigation
534-
schema.data
535-
.get('classes')
536-
.sortBy((_v, k) => k)
537-
.forEach((cl, className) => {
538-
const classFields = [];
539-
540-
cl.forEach((column, field) => {
541-
if (column.type !== 'String') {
542-
return;
543-
}
544-
// Exclude objectId - it's a special field referenced by pointers, not strings
545-
if (field === 'objectId') {
546-
return;
547-
}
548-
// Exclude hidden/sensitive fields
549-
if (field === 'password' && className === '_User') {
550-
return;
551-
}
552-
if (field === 'sessionToken' && (className === '_User' || className === '_Session')) {
553-
return;
554-
}
555-
classFields.push({
556-
text: field,
557-
callback: () => {
558-
onPointerClick({
559-
className,
560-
id: value,
561-
field,
562-
});
563-
},
564-
});
565-
});
566-
567-
if (classFields.length > 0) {
568-
// Sort fields alphabetically
569-
classFields.sort((a, b) => a.text.localeCompare(b.text));
570-
relatedRecordsMenuItem.items.push({
571-
text: className,
572-
items: classFields,
573-
});
574-
}
575-
});
576-
577-
return relatedRecordsMenuItem.items.length ? relatedRecordsMenuItem : undefined;
529+
return buildRelatedTextFieldsMenuItem(schema, value, onPointerClick);
578530
}
579531

580532
/**

src/components/StringEditor/StringEditor.react.js

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,15 @@ export default class StringEditor extends React.Component {
6363
}
6464

6565
handleContextMenu(e) {
66-
const { setContextMenu, arrayConfigParams, onAddToArrayConfig } = this.props;
66+
const { setContextMenu, arrayConfigParams, onAddToArrayConfig, getRelatedRecordsMenuItem } = this.props;
6767

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

73-
// Check if there are array config params available
74-
if (!arrayConfigParams || arrayConfigParams.length === 0 || !onAddToArrayConfig || !setContextMenu) {
73+
// Check if setContextMenu is available
74+
if (!setContextMenu) {
7575
return;
7676
}
7777

@@ -84,22 +84,41 @@ export default class StringEditor extends React.Component {
8484
return;
8585
}
8686

87-
// Prevent default context menu
88-
e.preventDefault();
89-
e.stopPropagation();
90-
9187
// Build context menu items
92-
const menuItems = [
93-
{
88+
const menuItems = [];
89+
90+
// Add "Add to config parameter" option if available
91+
if (arrayConfigParams && arrayConfigParams.length > 0 && onAddToArrayConfig) {
92+
menuItems.push({
9493
text: 'Add to config parameter...',
9594
items: arrayConfigParams.map(param => ({
9695
text: param.name,
9796
callback: () => {
9897
onAddToArrayConfig(param.name, selectedText);
9998
},
10099
})),
101-
},
102-
];
100+
});
101+
}
102+
103+
// Add "Related records" option if available (using selected text)
104+
if (getRelatedRecordsMenuItem) {
105+
const relatedRecordsItem = getRelatedRecordsMenuItem(selectedText);
106+
if (relatedRecordsItem) {
107+
if (menuItems.length > 0) {
108+
menuItems.push({ type: 'separator' });
109+
}
110+
menuItems.push(relatedRecordsItem);
111+
}
112+
}
113+
114+
// Only show context menu if there are items
115+
if (menuItems.length === 0) {
116+
return;
117+
}
118+
119+
// Prevent default context menu
120+
e.preventDefault();
121+
e.stopPropagation();
103122

104123
setContextMenu(e.pageX, e.pageY, menuItems);
105124
}

src/dashboard/Data/Browser/BrowserTable.react.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@ export default class BrowserTable extends React.Component {
493493
setContextMenu={this.props.setContextMenu}
494494
arrayConfigParams={this.props.arrayConfigParams}
495495
onAddToArrayConfig={this.props.onAddToArrayConfig}
496+
getRelatedRecordsMenuItem={this.props.getRelatedRecordsMenuItem}
496497
/>
497498
);
498499
}

src/dashboard/Data/Browser/DataBrowser.react.js

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import styles from './Databrowser.scss';
2525
import KeyboardShortcutsManager, { matchesShortcut } from 'lib/KeyboardShortcutsPreferences';
2626

2727
import AggregationPanel from '../../../components/AggregationPanel/AggregationPanel';
28+
import { buildRelatedTextFieldsMenuItem } from '../../../lib/RelatedRecordsUtils';
2829

2930
const BROWSER_SHOW_ROW_NUMBER = 'browserShowRowNumber';
3031
const AGGREGATION_PANEL_VISIBLE = 'aggregationPanelVisible';
@@ -1165,19 +1166,13 @@ export default class DataBrowser extends React.Component {
11651166
return;
11661167
}
11671168

1168-
// Get array config params from props
1169-
const arrayParams = this.props.arrayConfigParams || [];
1170-
1171-
if (arrayParams.length === 0) {
1172-
return;
1173-
}
1174-
1175-
// Prevent default context menu
1176-
event.preventDefault();
1177-
11781169
// Build context menu items
1179-
const menuItems = [
1180-
{
1170+
const menuItems = [];
1171+
1172+
// Add "Add to config parameter" option if available
1173+
const arrayParams = this.props.arrayConfigParams || [];
1174+
if (arrayParams.length > 0) {
1175+
menuItems.push({
11811176
text: 'Add to config parameter',
11821177
items: arrayParams.map(param => ({
11831178
text: param.name,
@@ -1187,8 +1182,29 @@ export default class DataBrowser extends React.Component {
11871182
}
11881183
},
11891184
})),
1190-
},
1191-
];
1185+
});
1186+
}
1187+
1188+
// Add "Related records" option if available (search text in String fields)
1189+
const relatedRecordsItem = buildRelatedTextFieldsMenuItem(
1190+
this.props.schema,
1191+
selectedText,
1192+
this.props.onPointerCmdClick
1193+
);
1194+
if (relatedRecordsItem) {
1195+
if (menuItems.length > 0) {
1196+
menuItems.push({ type: 'separator' });
1197+
}
1198+
menuItems.push(relatedRecordsItem);
1199+
}
1200+
1201+
// Only show context menu if there are items
1202+
if (menuItems.length === 0) {
1203+
return;
1204+
}
1205+
1206+
// Prevent default context menu
1207+
event.preventDefault();
11921208

11931209
this.setContextMenu(event.pageX, event.pageY, menuItems);
11941210
}
@@ -2032,6 +2048,11 @@ export default class DataBrowser extends React.Component {
20322048
setSelectedObjectId={this.setSelectedObjectId}
20332049
callCloudFunction={this.handleCallCloudFunction}
20342050
setContextMenu={this.setContextMenu}
2051+
getRelatedRecordsMenuItem={(textValue) => buildRelatedTextFieldsMenuItem(
2052+
this.props.schema,
2053+
textValue,
2054+
this.props.onPointerCmdClick
2055+
)}
20352056
freezeIndex={this.state.frozenColumnIndex}
20362057
freezeColumns={this.freezeColumns}
20372058
unfreezeColumns={this.unfreezeColumns}

src/dashboard/Data/Browser/Editor.react.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import decode from 'parse/lib/browser/decode';
1616
import React from 'react';
1717
import StringEditor from 'components/StringEditor/StringEditor.react';
1818

19-
const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit, onCancel, setContextMenu, arrayConfigParams, onAddToArrayConfig }) => {
19+
const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit, onCancel, setContextMenu, arrayConfigParams, onAddToArrayConfig, getRelatedRecordsMenuItem }) => {
2020
let content = null;
2121
if (type === 'String') {
2222
content = (
@@ -31,6 +31,7 @@ const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit
3131
setContextMenu={setContextMenu}
3232
arrayConfigParams={arrayConfigParams}
3333
onAddToArrayConfig={onAddToArrayConfig}
34+
getRelatedRecordsMenuItem={getRelatedRecordsMenuItem}
3435
/>
3536
);
3637
} else if (type === 'Array' || type === 'Object') {
@@ -53,6 +54,7 @@ const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit
5354
setContextMenu={setContextMenu}
5455
arrayConfigParams={arrayConfigParams}
5556
onAddToArrayConfig={onAddToArrayConfig}
57+
getRelatedRecordsMenuItem={getRelatedRecordsMenuItem}
5658
/>
5759
);
5860
} else if (type === 'Polygon') {
@@ -95,6 +97,7 @@ const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit
9597
setContextMenu={setContextMenu}
9698
arrayConfigParams={arrayConfigParams}
9799
onAddToArrayConfig={onAddToArrayConfig}
100+
getRelatedRecordsMenuItem={getRelatedRecordsMenuItem}
98101
/>
99102
);
100103
} else if (type === 'Date') {
@@ -109,6 +112,7 @@ const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit
109112
setContextMenu={setContextMenu}
110113
arrayConfigParams={arrayConfigParams}
111114
onAddToArrayConfig={onAddToArrayConfig}
115+
getRelatedRecordsMenuItem={getRelatedRecordsMenuItem}
112116
/>
113117
);
114118
} else {
@@ -137,7 +141,7 @@ const Editor = ({ top, left, type, targetClass, value, readonly, width, onCommit
137141
);
138142
}
139143
};
140-
content = <StringEditor value={value ? value.id : ''} width={width} onCommit={encodeCommit} onCancel={onCancel} setContextMenu={setContextMenu} arrayConfigParams={arrayConfigParams} onAddToArrayConfig={onAddToArrayConfig} />;
144+
content = <StringEditor value={value ? value.id : ''} width={width} onCommit={encodeCommit} onCancel={onCancel} setContextMenu={setContextMenu} arrayConfigParams={arrayConfigParams} onAddToArrayConfig={onAddToArrayConfig} getRelatedRecordsMenuItem={getRelatedRecordsMenuItem} />;
141145
}
142146

143147
return <div style={{ position: 'absolute', top: top, left: left }}>{content}</div>;

src/lib/RelatedRecordsUtils.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright (c) 2016-present, Parse, LLC
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the LICENSE file in
6+
* the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* Builds a "Related records" context menu item for a text value.
11+
* Finds all classes with String type fields that can be filtered by this value.
12+
* Groups fields by class name in a hierarchical submenu structure.
13+
*
14+
* @param {Object} schema - The schema object containing class definitions
15+
* @param {string} textValue - The text value to search for
16+
* @param {Function} onNavigate - Callback function to navigate to the related records
17+
* @returns {Object|undefined} The menu item object or undefined if no fields found
18+
*/
19+
export function buildRelatedTextFieldsMenuItem(schema, textValue, onNavigate) {
20+
if (!textValue || typeof textValue !== 'string' || textValue.trim() === '' || !schema || !onNavigate) {
21+
return undefined;
22+
}
23+
24+
const relatedRecordsMenuItem = {
25+
text: 'Related records',
26+
items: [],
27+
};
28+
29+
schema.data
30+
.get('classes')
31+
.sortBy((_v, k) => k)
32+
.forEach((cl, className) => {
33+
const classFields = [];
34+
35+
cl.forEach((column, field) => {
36+
if (column.type !== 'String') {
37+
return;
38+
}
39+
// Exclude objectId - it's a special field referenced by pointers, not strings
40+
if (field === 'objectId') {
41+
return;
42+
}
43+
// Exclude hidden/sensitive fields
44+
if (field === 'password' && className === '_User') {
45+
return;
46+
}
47+
if (field === 'sessionToken' && (className === '_User' || className === '_Session')) {
48+
return;
49+
}
50+
classFields.push({
51+
text: field,
52+
callback: () => {
53+
onNavigate({
54+
className,
55+
id: textValue,
56+
field,
57+
});
58+
},
59+
});
60+
});
61+
62+
if (classFields.length > 0) {
63+
// Sort fields alphabetically
64+
classFields.sort((a, b) => a.text.localeCompare(b.text));
65+
relatedRecordsMenuItem.items.push({
66+
text: className,
67+
items: classFields,
68+
});
69+
}
70+
});
71+
72+
return relatedRecordsMenuItem.items.length ? relatedRecordsMenuItem : undefined;
73+
}

0 commit comments

Comments
 (0)