Skip to content

Commit c4e238f

Browse files
authored
Add distinct / unique filter (#920)
* Add Distinct / Unique Filter * remove ability to add row * date fix for postgres * disable edit / security toolbar * fix scrolling and ordering * disable editor
1 parent 340d23d commit c4e238f

File tree

8 files changed

+133
-52
lines changed

8 files changed

+133
-52
lines changed

src/components/BrowserCell/BrowserCell.react.js

+10
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,22 @@ let BrowserCell = ({ type, value, hidden, width, current, onSelect, onEditChange
3333
content = <span>&nbsp;</span>;
3434
classes.push(styles.empty);
3535
} else if (type === 'Pointer') {
36+
if (value && value.__type) {
37+
const object = new Parse.Object(value.className);
38+
object.id = value.objectId;
39+
value = object;
40+
}
3641
content = (
3742
<a href='javascript:;' onClick={onPointerClick.bind(undefined, value)}>
3843
<Pill value={value.id} />
3944
</a>
4045
);
4146
} else if (type === 'Date') {
47+
if (typeof value === 'object' && value.__type) {
48+
value = new Date(value.iso);
49+
} else if (typeof value === 'string') {
50+
value = new Date(value);
51+
}
4252
content = dateStringUTC(value);
4353
} else if (type === 'Boolean') {
4454
content = value ? 'True' : 'False';

src/components/BrowserMenu/BrowserMenu.react.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,19 @@ export default class BrowserMenu extends React.Component {
4747
</Popover>
4848
);
4949
}
50+
const classes = [styles.entry];
51+
if (this.props.disabled) {
52+
classes.push(styles.disabled);
53+
}
54+
let onClick = null;
55+
if (!this.props.disabled) {
56+
onClick = () => {
57+
this.setState({ open: true });
58+
};
59+
}
5060
return (
5161
<div className={styles.wrap}>
52-
<div className={styles.entry} onClick={() => this.setState({ open: true })}>
62+
<div className={classes.join(' ')} onClick={onClick}>
5363
<Icon name={this.props.icon} width={14} height={14} />
5464
<span>{this.props.title}</span>
5565
</div>

src/components/BrowserMenu/BrowserMenu.scss

+9
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@
2424
fill: white;
2525
}
2626
}
27+
28+
&.disabled {
29+
cursor: not-allowed;
30+
color: #66637A;
31+
32+
&:hover svg {
33+
fill: #66637A;
34+
}
35+
}
2736
}
2837

2938
.title {

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

+32-3
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ class Browser extends DashboardView {
7171
lastNote: null,
7272

7373
relationCount: 0,
74+
75+
isUnique: false,
76+
uniqueField: null,
7477
};
7578

7679
this.prefetchData = this.prefetchData.bind(this);
@@ -333,7 +336,21 @@ class Browser extends DashboardView {
333336
}
334337

335338
query.limit(200);
336-
const data = await query.find({ useMasterKey: true });
339+
340+
let promise = query.find({ useMasterKey: true });
341+
let isUnique = false;
342+
let uniqueField = null;
343+
filters.forEach(async (filter) => {
344+
if (filter.get('constraint') == 'unique') {
345+
const field = filter.get('field');
346+
promise = query.distinct(field);
347+
isUnique = true;
348+
uniqueField = field;
349+
}
350+
});
351+
await this.setState({ isUnique, uniqueField });
352+
353+
const data = await promise;
337354
return data;
338355
}
339356

@@ -347,7 +364,11 @@ class Browser extends DashboardView {
347364
const data = await this.fetchParseData(source, filters);
348365
var filteredCounts = { ...this.state.filteredCounts };
349366
if (filters.size > 0) {
350-
filteredCounts[source] = await this.fetchParseDataCount(source,filters);
367+
if (this.state.isUnique) {
368+
filteredCounts[source] = data.length;
369+
} else {
370+
filteredCounts[source] = await this.fetchParseDataCount(source, filters);
371+
}
351372
} else {
352373
delete filteredCounts[source];
353374
}
@@ -372,7 +393,7 @@ class Browser extends DashboardView {
372393
}
373394

374395
fetchNextPage() {
375-
if (!this.state.data) {
396+
if (!this.state.data || this.state.isUnique) {
376397
return null;
377398
}
378399
let className = this.props.params.className;
@@ -889,11 +910,17 @@ class Browser extends DashboardView {
889910
let columns = {
890911
objectId: { type: 'String' }
891912
};
913+
if (this.state.isUnique) {
914+
columns = {};
915+
}
892916
let userPointers = [];
893917
classes.get(className).forEach((field, name) => {
894918
if (name === 'objectId') {
895919
return;
896920
}
921+
if (this.state.isUnique && name !== this.state.uniqueField) {
922+
return;
923+
}
897924
let info = { type: field.type };
898925
if (field.targetClass) {
899926
info.targetClass = field.targetClass;
@@ -916,6 +943,8 @@ class Browser extends DashboardView {
916943
}
917944
browser = (
918945
<DataBrowser
946+
isUnique={this.state.isUnique}
947+
uniqueField={this.state.uniqueField}
919948
count={count}
920949
perms={this.state.clp[className]}
921950
schema={schema}

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

+44-35
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,17 @@ export default class BrowserTable extends React.Component {
9696
</span>
9797
{this.props.order.map(({ name, width }, j) => {
9898
let type = this.props.columns[name].type;
99-
let attr = attributes[name];
100-
if (name === 'objectId') {
101-
attr = obj.id;
102-
} else if (name === 'ACL' && this.props.className === '_User' && !attr) {
103-
attr = new Parse.ACL({ '*': { read: true }, [obj.id]: { read: true, write: true }});
104-
} else if (type === 'Relation' && !attr && obj.id) {
105-
attr = new Parse.Relation(obj, name);
106-
attr.targetClassName = this.props.columns[name].targetClass;
99+
let attr = obj;
100+
if (!this.props.isUnique) {
101+
attr = attributes[name];
102+
if (name === 'objectId') {
103+
attr = obj.id;
104+
} else if (name === 'ACL' && this.props.className === '_User' && !attr) {
105+
attr = new Parse.ACL({ '*': { read: true }, [obj.id]: { read: true, write: true }});
106+
} else if (type === 'Relation' && !attr && obj.id) {
107+
attr = new Parse.Relation(obj, name);
108+
attr.targetClassName = this.props.columns[name].targetClass;
109+
}
107110
}
108111
let current = this.props.current && this.props.current.row === row && this.props.current.col === j;
109112
let hidden = false;
@@ -118,7 +121,7 @@ export default class BrowserTable extends React.Component {
118121
<BrowserCell
119122
key={name}
120123
type={type}
121-
readonly={READ_ONLY.indexOf(name) > -1}
124+
readonly={this.props.isUnique || READ_ONLY.indexOf(name) > -1}
122125
width={width}
123126
current={current}
124127
onSelect={() => this.props.setCurrent({ row: row, col: j })}
@@ -187,16 +190,21 @@ export default class BrowserTable extends React.Component {
187190
if (visible) {
188191
let { name, width } = this.props.order[this.props.current.col];
189192
let { type, targetClass } = this.props.columns[name];
190-
let readonly = READ_ONLY.indexOf(name) > -1;
193+
let readonly = this.props.isUnique || READ_ONLY.indexOf(name) > -1;
191194
if (name === 'sessionToken') {
192195
if (this.props.className === '_User' || this.props.className === '_Session') {
193196
readonly = true;
194197
}
195198
}
196199
let obj = this.props.current.row < 0 ? this.props.newObject : this.props.data[this.props.current.row];
197-
let value = obj.get(name);
200+
let value = obj;
201+
if (!this.props.isUnique) {
202+
value = obj.get(name);
203+
}
198204
if (name === 'objectId') {
199-
value = obj.id;
205+
if (!this.props.isUnique) {
206+
value = obj.id;
207+
}
200208
} else if (name === 'ACL' && this.props.className === '_User' && !value) {
201209
value = new Parse.ACL({ '*': { read: true }, [obj.id]: { read: true, write: true }});
202210
} else if (name === 'password' && this.props.className === '_User') {
@@ -222,27 +230,28 @@ export default class BrowserTable extends React.Component {
222230
for (let i = 0; i < this.props.current.col; i++) {
223231
wrapLeft += this.props.order[i].width;
224232
}
225-
226-
editor = (
227-
<Editor
228-
top={wrapTop}
229-
left={wrapLeft}
230-
type={type}
231-
targetClass={targetClass}
232-
value={value}
233-
readonly={readonly}
234-
width={width}
235-
onCommit={(newValue) => {
236-
if (newValue !== value) {
237-
this.props.updateRow(
238-
this.props.current.row,
239-
name,
240-
newValue
241-
);
242-
}
243-
this.props.setEditing(false);
244-
}} />
245-
);
233+
if (!this.props.isUnique) {
234+
editor = (
235+
<Editor
236+
top={wrapTop}
237+
left={wrapLeft}
238+
type={type}
239+
targetClass={targetClass}
240+
value={value}
241+
readonly={readonly}
242+
width={width}
243+
onCommit={(newValue) => {
244+
if (newValue !== value) {
245+
this.props.updateRow(
246+
this.props.current.row,
247+
name,
248+
newValue
249+
);
250+
}
251+
this.props.setEditing(false);
252+
}} />
253+
);
254+
}
246255
}
247256
}
248257

@@ -264,7 +273,7 @@ export default class BrowserTable extends React.Component {
264273
/>
265274
</div>
266275
);
267-
} else {
276+
} else if (!this.props.isUnique) {
268277
addRow = (
269278
<div className={styles.addRow}>
270279
<a title='Add Row' onClick={this.props.onAddRow}>
@@ -324,7 +333,7 @@ export default class BrowserTable extends React.Component {
324333
selectAll={this.props.selectRow.bind(null, '*')}
325334
headers={headers}
326335
updateOrdering={this.props.updateOrdering}
327-
readonly={!!this.props.relation}
336+
readonly={!!this.props.relation || !!this.props.isUnique}
328337
handleDragDrop={this.props.handleHeaderDragDrop}
329338
onResize={this.props.handleResize}
330339
onAddColumn={this.props.onAddColumn}

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

+14-6
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ let BrowserToolbar = ({
4141
onChangeCLP,
4242
onRefresh,
4343
hidePerms,
44+
isUnique,
4445

4546
enableDeleteAllRows,
4647
enableExportClass,
4748
enableSecurityDialog,
49+
4850
enableColumnManipulation,
49-
enableClassManipulation
51+
enableClassManipulation,
5052
}) => {
5153
let selectionLength = Object.keys(selection).length;
5254
let details = [];
@@ -58,7 +60,7 @@ let BrowserToolbar = ({
5860
}
5961
}
6062

61-
if (!relation) {
63+
if (!relation && !isUnique) {
6264
if (perms && !hidePerms) {
6365
let read = perms.get && perms.find && perms.get['*'] && perms.find['*'];
6466
let write = perms.create && perms.update && perms.delete && perms.create['*'] && perms.update['*'] && perms.delete['*'];
@@ -93,7 +95,7 @@ let BrowserToolbar = ({
9395
);
9496
} else {
9597
menu = (
96-
<BrowserMenu title='Edit' icon='edit-solid'>
98+
<BrowserMenu title='Edit' icon='edit-solid' disabled={isUnique}>
9799
<MenuItem text='Add a row' onClick={onAddRow} />
98100
{enableColumnManipulation ? <MenuItem text='Add a column' onClick={onAddColumn} /> : <noscript />}
99101
{enableClassManipulation ? <MenuItem text='Add a class' onClick={onAddClass} /> : <noscript />}
@@ -129,6 +131,12 @@ let BrowserToolbar = ({
129131
} else if (subsection.length > 30) {
130132
subsection = subsection.substr(0, 30) + '\u2026';
131133
}
134+
const classes = [styles.toolbarButton];
135+
let onClick = onAddRow;
136+
if (isUnique) {
137+
classes.push(styles.toolbarButtonDisabled);
138+
onClick = null;
139+
}
132140
return (
133141
<Toolbar
134142
relation={relation}
@@ -137,7 +145,7 @@ let BrowserToolbar = ({
137145
subsection={subsection}
138146
details={details.join(' \u2022 ')}
139147
>
140-
<a className={styles.toolbarButton} onClick={onAddRow}>
148+
<a className={classes.join(' ')} onClick={onClick}>
141149
<Icon name='plus-solid' width={14} height={14} />
142150
<span>Add Row</span>
143151
</a>
@@ -155,7 +163,7 @@ let BrowserToolbar = ({
155163
<div className={styles.toolbarSeparator} />
156164
{enableSecurityDialog ? <SecurityDialog
157165
setCurrent={setCurrent}
158-
disabled={!!relation}
166+
disabled={!!relation || !!isUnique}
159167
perms={perms}
160168
className={classNameForPermissionsEditor}
161169
onChangeCLP={onChangeCLP}
@@ -166,4 +174,4 @@ let BrowserToolbar = ({
166174
);
167175
};
168176

169-
export default BrowserToolbar;
177+
export default BrowserToolbar;

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ export default class DataBrowser extends React.Component {
5151
current: null,
5252
editing: false,
5353
});
54-
} else if (Object.keys(props.columns).length !== Object.keys(this.props.columns).length) {
54+
} else if (Object.keys(props.columns).length !== Object.keys(this.props.columns).length
55+
|| (props.isUnique && props.uniqueField !== this.props.uniqueField)) {
5556
let order = ColumnPreferences.getOrder(
5657
props.columns,
5758
context.currentApp.applicationId,

src/lib/Filters.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,18 @@ export const Constraints = {
131131
field: 'Object',
132132
composable: true
133133
},
134+
unique: {
135+
name: 'unique',
136+
field: null
137+
},
134138
};
135139

136140
export const FieldConstraints = {
137-
'Pointer': [ 'exists', 'dne', 'eq', 'neq'],
138-
'Boolean': [ 'exists', 'dne', 'eq' ],
139-
'Number': [ 'exists', 'dne', 'eq', 'neq', 'lt', 'lte', 'gt', 'gte' ],
140-
'String': [ 'exists', 'dne', 'eq', 'neq', 'starts', 'ends', 'stringContainsString' ],
141-
'Date': [ 'exists', 'dne', 'before', 'after' ],
141+
'Pointer': [ 'exists', 'dne', 'eq', 'neq', 'unique' ],
142+
'Boolean': [ 'exists', 'dne', 'eq', 'unique' ],
143+
'Number': [ 'exists', 'dne', 'eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'unique' ],
144+
'String': [ 'exists', 'dne', 'eq', 'neq', 'starts', 'ends', 'stringContainsString', 'unique' ],
145+
'Date': [ 'exists', 'dne', 'before', 'after', 'unique' ],
142146
'Object': [
143147
'exists',
144148
'dne',
@@ -149,7 +153,8 @@ export const FieldConstraints = {
149153
'keyGt',
150154
'keyGte',
151155
'keyLt',
152-
'keyLte'
156+
'keyLte',
157+
'unique',
153158
],
154159
'Array': [
155160
'exists',

0 commit comments

Comments
 (0)