Skip to content

Commit 69cab35

Browse files
authored
Context menu with quick filters and links to related records in other Parse Objects 📃 (#1431)
* Context menu with quick filter initials * Support for filtering Pointer type of entities * Adding more filters to context menu * Introducing general, reusable ContextMenu component * Move ContextMenu item in pig list to keep alphabetical order * Reverting changes in parse-dashboard-config.json file * Showing "Get related records from..." context menu option when clicking on Pointer or objectId * Changing categories to fill context menu horizontally * Fully functional 'Get related records from...' context menu item * Fixing CI build by removing empty test file * Closing context menu after chosing an option * Properly handling 'Get related records from...' logic when executing it on cell containing Pointer * Fixing click on empty cell by checking value * Closing context menu after clicking outside it * Adding key attributes to ContextMenu JSX * Positioning menu sections to not go off the screen + animations * Functional options to add a filter to existing one(s) * Showing "Add fiter..." context menu option only if there's any filter applied * Generating context menu filters dynamically basing on available ones * Code cleaning + missing import * Passing BLACKLISTED_FILTERS when getting filters available for context menu * Restoring accidentally removed line * Sorting objects listed in context menu item "Get related records from..." alphabetically; * Handling context menu filters for "Date" type of data * Getting "compareTo" value only for constrains that are comparable * Removing duplicated prop * Fixing lint errors * Adding "Edit row" option to BrowserCell context menu * Keeping BLACKLISTED_FILTERS in single place * Removing unnecessary onSelect call (as cell is already selected at this stage) * Removing unnecessary setCopyableValue call (as it should be called at this stage)
2 parents bf764a0 + 4595ca6 commit 69cab35

File tree

12 files changed

+483
-31
lines changed

12 files changed

+483
-31
lines changed

Parse-Dashboard/parse-dashboard-config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@
1111
}
1212
],
1313
"iconsFolder": "icons"
14-
}
14+
}

src/components/BrowserCell/BrowserCell.react.js

Lines changed: 148 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* This source code is licensed under the license found in the LICENSE file in
66
* the root directory of this source tree.
77
*/
8+
import * as Filters from 'lib/Filters';
9+
import { List, Map } from 'immutable';
810
import { dateStringUTC } from 'lib/DateUtils';
911
import getFileName from 'lib/getFileName';
1012
import Parse from 'parse';
@@ -64,8 +66,144 @@ export default class BrowserCell extends Component {
6466
return isRefDifferent;
6567
}
6668

69+
//#region Cell Context Menu related methods
70+
71+
onContextMenu(event) {
72+
if (event.type !== 'contextmenu') { return; }
73+
event.preventDefault();
74+
75+
const { field, hidden, onSelect, setCopyableValue, setContextMenu, row, col } = this.props;
76+
77+
onSelect({ row, col });
78+
setCopyableValue(hidden ? undefined : this.copyableValue);
79+
80+
const available = Filters.availableFilters(this.props.simplifiedSchema, this.props.filters, Filters.BLACKLISTED_FILTERS);
81+
const constraints = available && available[field];
82+
83+
const { pageX, pageY } = event;
84+
const menuItems = this.getContextMenuOptions(constraints);
85+
menuItems.length && setContextMenu(pageX, pageY, menuItems);
86+
}
87+
88+
getContextMenuOptions(constraints) {
89+
let { onEditSelectedRow } = this.props;
90+
const contextMenuOptions = [];
91+
92+
const setFilterContextMenuOption = this.getSetFilterContextMenuOption(constraints);
93+
setFilterContextMenuOption && contextMenuOptions.push(setFilterContextMenuOption);
94+
95+
const addFilterContextMenuOption = this.getAddFilterContextMenuOption(constraints);
96+
addFilterContextMenuOption && contextMenuOptions.push(addFilterContextMenuOption);
97+
98+
const relatedObjectsContextMenuOption = this.getRelatedObjectsContextMenuOption();
99+
relatedObjectsContextMenuOption && contextMenuOptions.push(relatedObjectsContextMenuOption);
100+
101+
onEditSelectedRow && contextMenuOptions.push({
102+
text: 'Edit row',
103+
callback: () => {
104+
let { objectId, onEditSelectedRow } = this.props;
105+
onEditSelectedRow(true, objectId);
106+
}
107+
});
108+
109+
return contextMenuOptions;
110+
}
111+
112+
getSetFilterContextMenuOption(constraints) {
113+
if (constraints) {
114+
return {
115+
text: 'Set filter...', items: constraints.map(constraint => {
116+
const definition = Filters.Constraints[constraint];
117+
const text = `${this.props.field} ${definition.name}${definition.comparable ? (' ' + this.copyableValue) : ''}`;
118+
return {
119+
text,
120+
callback: this.pickFilter.bind(this, constraint)
121+
};
122+
})
123+
};
124+
}
125+
}
126+
127+
getAddFilterContextMenuOption(constraints) {
128+
if (constraints && this.props.filters && this.props.filters.size > 0) {
129+
return {
130+
text: 'Add filter...', items: constraints.map(constraint => {
131+
const definition = Filters.Constraints[constraint];
132+
const text = `${this.props.field} ${definition.name}${definition.comparable ? (' ' + this.copyableValue) : ''}`;
133+
return {
134+
text,
135+
callback: this.pickFilter.bind(this, constraint, true)
136+
};
137+
})
138+
};
139+
}
140+
}
141+
142+
/**
143+
* Returns "Get related records from..." context menu item if cell holds a Pointer
144+
* or objectId and there's a class in relation.
145+
*/
146+
getRelatedObjectsContextMenuOption() {
147+
const { value, schema, onPointerClick } = this.props;
148+
149+
const pointerClassName = (value && value.className)
150+
|| (this.props.field === 'objectId' && this.props.className);
151+
if (pointerClassName) {
152+
const relatedRecordsMenuItem = { text: 'Get related records from...', items: [] };
153+
schema.data.get('classes').sortBy((v, k) => k).forEach((cl, className) => {
154+
cl.forEach((column, field) => {
155+
if (column.targetClass !== pointerClassName) { return; }
156+
relatedRecordsMenuItem.items.push({
157+
text: className, callback: () => {
158+
let relatedObject = value;
159+
if (this.props.field === 'objectId') {
160+
relatedObject = new Parse.Object(pointerClassName);
161+
relatedObject.id = value;
162+
}
163+
onPointerClick({ className, id: relatedObject.toPointer(), field })
164+
}
165+
})
166+
});
167+
});
168+
169+
return relatedRecordsMenuItem.items.length ? relatedRecordsMenuItem : undefined;
170+
}
171+
}
172+
173+
pickFilter(constraint, addToExistingFilter) {
174+
const definition = Filters.Constraints[constraint];
175+
const { filters, type, value, field } = this.props;
176+
const newFilters = addToExistingFilter ? filters : new List();
177+
let compareTo;
178+
if (definition.comparable) {
179+
switch (type) {
180+
case 'Pointer':
181+
compareTo = value.toPointer()
182+
break;
183+
case 'Date':
184+
compareTo = value.__type ? value : {
185+
__type: 'Date',
186+
iso: value
187+
};
188+
break;
189+
190+
default:
191+
compareTo = value;
192+
break;
193+
}
194+
}
195+
196+
this.props.onFilterChange(newFilters.push(new Map({
197+
field,
198+
constraint,
199+
compareTo
200+
})));
201+
}
202+
203+
//#endregion
204+
67205
render() {
68-
let { type, value, hidden, width, current, onSelect, onEditChange, setCopyableValue, setRelation, onPointerClick, row, col, name, onEditSelectedRow } = this.props;
206+
let { type, value, hidden, width, current, onSelect, onEditChange, setCopyableValue, setRelation, onPointerClick, row, col, field, onEditSelectedRow } = this.props;
69207
let content = value;
70208
this.copyableValue = content;
71209
let classes = [styles.cell, unselectable];
@@ -96,8 +234,8 @@ export default class BrowserCell extends Component {
96234
<Pill value={value.id} />
97235
</a>
98236
) : (
99-
value.id
100-
);
237+
value.id
238+
);
101239
this.copyableValue = value.id;
102240
} else if (type === 'Date') {
103241
if (typeof value === 'object' && value.__type) {
@@ -145,11 +283,11 @@ export default class BrowserCell extends Component {
145283
<Pill onClick={() => setRelation(value)} value='View relation' />
146284
</div>
147285
) : (
148-
'Relation'
149-
);
286+
'Relation'
287+
);
150288
this.copyableValue = undefined;
151289
}
152-
290+
153291
if (current) {
154292
classes.push(styles.current);
155293
}
@@ -164,7 +302,7 @@ export default class BrowserCell extends Component {
164302
}}
165303
onDoubleClick={() => {
166304
// Since objectId can't be edited, double click event opens edit row dialog
167-
if (name === 'objectId' && onEditSelectedRow) {
305+
if (field === 'objectId' && onEditSelectedRow) {
168306
onEditSelectedRow(true, value);
169307
} else if (type !== 'Relation') {
170308
onEditChange(true)
@@ -178,7 +316,9 @@ export default class BrowserCell extends Component {
178316
}
179317
onEditChange(true);
180318
}
181-
}}>
319+
}}
320+
onContextMenu={this.onContextMenu.bind(this)}
321+
>
182322
{content}
183323
</span>
184324
);

src/components/BrowserFilter/BrowserFilter.react.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import ReactDOM from 'react-dom';
1717
import styles from 'components/BrowserFilter/BrowserFilter.scss';
1818
import { List, Map } from 'immutable';
1919

20-
const BLACKLISTED_FILTERS = [ 'containsAny', 'doesNotContainAny' ];
2120
const POPOVER_CONTENT_ID = 'browserFilterPopover';
2221

2322
export default class BrowserFilter extends React.Component {
@@ -27,7 +26,7 @@ export default class BrowserFilter extends React.Component {
2726
this.state = {
2827
open: false,
2928
filters: new List(),
30-
blacklistedFilters: BLACKLISTED_FILTERS.concat(props.blacklistedFilters)
29+
blacklistedFilters: Filters.BLACKLISTED_FILTERS.concat(props.blacklistedFilters)
3130
};
3231
this.toggle = this.toggle.bind(this);
3332
}

src/components/BrowserRow/BrowserRow.react.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default class BrowserRow extends Component {
1919
}
2020

2121
render() {
22-
const { className, columns, currentCol, isUnique, obj, onPointerClick, order, readOnlyFields, row, rowWidth, selection, selectRow, setCopyableValue, setCurrent, setEditing, setRelation, onEditSelectedRow } = this.props;
22+
const { className, columns, currentCol, isUnique, obj, onPointerClick, order, readOnlyFields, row, rowWidth, selection, selectRow, setCopyableValue, setCurrent, setEditing, setRelation, onEditSelectedRow, setContextMenu, onFilterChange } = this.props;
2323
let attributes = obj.attributes;
2424
return (
2525
<div className={styles.tableRow} style={{ minWidth: rowWidth }}>
@@ -61,7 +61,11 @@ export default class BrowserRow extends Component {
6161
return (
6262
<BrowserCell
6363
key={name}
64-
name={name}
64+
schema={this.props.schema}
65+
simplifiedSchema={this.props.simplifiedSchema}
66+
filters={this.props.filters}
67+
className={className}
68+
field={name}
6569
row={row}
6670
col={j}
6771
type={type}
@@ -71,10 +75,13 @@ export default class BrowserRow extends Component {
7175
onSelect={setCurrent}
7276
onEditChange={setEditing}
7377
onPointerClick={onPointerClick}
78+
onFilterChange={onFilterChange}
7479
setRelation={setRelation}
80+
objectId={obj.id}
7581
value={attr}
7682
hidden={hidden}
7783
setCopyableValue={setCopyableValue}
84+
setContextMenu={setContextMenu}
7885
onEditSelectedRow={onEditSelectedRow} />
7986
);
8087
})}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
import React from 'react';
9+
import ContextMenu from 'components/ContextMenu/ContextMenu.react';
10+
11+
export const component = ContextMenu;
12+
13+
export const demos = [
14+
{
15+
name: 'Context menu',
16+
render: () => (
17+
<div style={{
18+
position: 'relative',
19+
height: '100px'
20+
}}>
21+
<ContextMenu
22+
x={0}
23+
y={0}
24+
items={[
25+
{
26+
text: 'Category 1', items: [
27+
{ text: 'C1 Item 1', callback: () => { alert('C1 Item 1 clicked!') } },
28+
{ text: 'C1 Item 2', callback: () => { alert('C1 Item 2 clicked!') } },
29+
{
30+
text: 'Sub Category 1', items: [
31+
{ text: 'SC1 Item 1', callback: () => { alert('SC1 Item 1 clicked!') } },
32+
{ text: 'SC1 Item 2', callback: () => { alert('SC1 Item 2 clicked!') } },
33+
]
34+
}
35+
]
36+
},
37+
{
38+
text: 'Category 2', items: [
39+
{ text: 'C2 Item 1', callback: () => { alert('C2 Item 1 clicked!') } },
40+
{ text: 'C2 Item 2', callback: () => { alert('C2 Item 2 clicked!') } }
41+
]
42+
}
43+
]}
44+
/>
45+
</div>
46+
)
47+
}
48+
];

0 commit comments

Comments
 (0)