Skip to content

Commit f56f946

Browse files
authored
feat: Add export of saved data browser filters via classPreference settings (#2455)
1 parent 31a1cfa commit f56f946

File tree

5 files changed

+90
-13
lines changed

5 files changed

+90
-13
lines changed

README.md

+28-3
Original file line numberDiff line numberDiff line change
@@ -329,13 +329,39 @@ If you have classes with a lot of columns and you filter them often with the sam
329329
{
330330
"name": "email",
331331
"filterSortToTop": true
332-
}
332+
}
333333
]
334334
}
335335
}
336336
]
337337
```
338338

339+
### Persistent Filters
340+
341+
The filters you save in the data browser of Parse Dashboard are only available for the current dashboard user in the current browser session. To make filters permanently available for all dashboard users of an app, you can define filters in the `classPreference` setting.
342+
343+
For example:
344+
345+
```json
346+
"apps": [{
347+
"classPreference": {
348+
"_Role": {
349+
"filters": [{
350+
"name": "Filter Name",
351+
"filter": [
352+
{
353+
"field": "objectId",
354+
"constraint": "exists"
355+
}
356+
]
357+
}]
358+
}
359+
}
360+
}]
361+
```
362+
363+
You can conveniently create a filter definition without having to write it by hand by first saving a filter in the data browser, then exporting the filter definition under *App Settings > Export Class Preferences*.
364+
339365
# Running as Express Middleware
340366

341367
Instead of starting Parse Dashboard with the CLI, you can also run it as an [express](https://github.com/expressjs/express) middleware.
@@ -452,8 +478,7 @@ With MFA enabled, a user must provide a one-time password that is typically boun
452478

453479
The user requires an authenticator app to generate the one-time password. These apps are provided by many 3rd parties and mostly for free.
454480

455-
If you create a new user by running `parse-dashboard --createUser`, you will be asked whether you want to enable MFA for the new user. To enable MFA for an existing user,
456-
run `parse-dashboard --createMFA` to generate a `mfa` secret that you then add to the existing user configuration, for example:
481+
If you create a new user by running `parse-dashboard --createUser`, you will be asked whether you want to enable MFA for the new user. To enable MFA for an existing user, run `parse-dashboard --createMFA` to generate a `mfa` secret that you then add to the existing user configuration, for example:
457482

458483
```json
459484
{

src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js

+18-8
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Toolbar from 'components/Toolbar/Toolbar.react';
1616
import CodeSnippet from 'components/CodeSnippet/CodeSnippet.react';
1717
import Notification from 'dashboard/Data/Browser/Notification.react';
1818
import * as ColumnPreferences from 'lib/ColumnPreferences';
19+
import * as ClassPreferences from 'lib/ClassPreferences';
1920
import bcrypt from 'bcryptjs';
2021
import * as OTPAuth from 'otpauth';
2122
import QRCode from 'qrcode';
@@ -38,9 +39,10 @@ export default class DashboardSettings extends DashboardView {
3839
message: null,
3940
passwordInput: '',
4041
passwordHidden: true,
41-
columnData: {
42+
copyData: {
4243
data: '',
4344
show: false,
45+
type: ''
4446
},
4547
newUser: {
4648
data: '',
@@ -53,7 +55,14 @@ export default class DashboardSettings extends DashboardView {
5355
getColumns() {
5456
const data = ColumnPreferences.getAllPreferences(this.context.applicationId);
5557
this.setState({
56-
columnData: { data: JSON.stringify(data, null, 2), show: true },
58+
copyData: { data: JSON.stringify(data, null, 2), show: true, type: 'Column Preferences' },
59+
});
60+
}
61+
62+
getClasses() {
63+
const data = ClassPreferences.getAllPreferences(this.context.applicationId);
64+
this.setState({
65+
copyData: { data: JSON.stringify(data, null, 2), show: true, type: 'Class Preferences' },
5766
});
5867
}
5968

@@ -190,14 +199,14 @@ export default class DashboardSettings extends DashboardView {
190199
<Field input={<Button color="blue" value="Create" width="120px" onClick={() => this.createUser()} />} />
191200
</Fieldset>
192201
);
193-
const columnPreferences = (
202+
const copyData = (
194203
<div>
195-
<div className={styles.columnData}>
196-
<CodeSnippet source={this.state.columnData.data} language="json" />
204+
<div className={styles.copyData}>
205+
<CodeSnippet source={this.state.copyData.data} language="json" />
197206
</div>
198207
<div className={styles.footer}>
199-
<Button color="blue" value="Copy" width="120px" onClick={() => this.copy(this.state.columnData.data, 'Column Preferences')} />
200-
<Button primary={true} value="Done" width="120px" onClick={() => this.setState({ columnData: { data: '', show: false } })} />
208+
<Button color="blue" value="Copy" width="120px" onClick={() => this.copy(this.state.copyData.data, this.state.copyData.type)} />
209+
<Button primary={true} value="Done" width="120px" onClick={() => this.setState({ copyData: { data: '', show: false } })} />
201210
</div>
202211
</div>
203212
);
@@ -225,9 +234,10 @@ export default class DashboardSettings extends DashboardView {
225234
<div className={styles.settings_page}>
226235
<Fieldset legend="Dashboard Configuration">
227236
<Field label={<Label text="Export Column Preferences" />} input={<FormButton color="blue" value="Export" onClick={() => this.getColumns()} />} />
237+
<Field label={<Label text="Export Class Preferences" />} input={<FormButton color="blue" value="Export" onClick={() => this.getClasses()} />} />
228238
<Field label={<Label text="Create New User" />} input={<FormButton color="blue" value="Create" onClick={() => this.setState({ createUserInput: true })} />} />
229239
</Fieldset>
230-
{this.state.columnData.show && columnPreferences}
240+
{this.state.copyData.show && copyData}
231241
{this.state.createUserInput && createUserInput}
232242
{this.state.newUser.show && userData}
233243
<Toolbar section="Settings" subsection="Dashboard Configuration" />

src/dashboard/Settings/DashboardSettings/DashboardSettings.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.columnData {
1+
.copyData {
22
max-height: 50vh;
33
overflow-y: scroll;
44
}

src/lib/ClassPreferences.js

+23
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,26 @@ export function getPreferences(appId, className) {
3838
function path(appId, className) {
3939
return `ParseDashboard:${VERSION}:${appId}:ClassPreference:${className}`;
4040
}
41+
42+
export function getAllPreferences(appId) {
43+
const storageKeys = Object.keys(localStorage);
44+
const result = {};
45+
for (const key of storageKeys) {
46+
const split = key.split(':')
47+
if (split.length <= 1 || split[2] !== appId) {
48+
continue;
49+
}
50+
const className = split.at(-1);
51+
const preferences = getPreferences(appId, className);
52+
if (preferences) {
53+
preferences.filters = preferences.filters.map(filter => {
54+
if (typeof filter.filter === 'string') {
55+
filter.filter = JSON.parse(filter.filter);
56+
}
57+
return filter;
58+
});
59+
result[className] = preferences;
60+
}
61+
}
62+
return result;
63+
}

src/lib/ParseApp.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import * as AJAX from 'lib/AJAX';
99
import encodeFormData from 'lib/encodeFormData';
1010
import Parse from 'parse';
11+
import { updatePreferences, getPreferences } from 'lib/ClassPreferences';
1112

1213
function setEnablePushSource(setting, enable) {
1314
let path = `/apps/${this.slug}/update_push_notifications`;
@@ -44,7 +45,8 @@ export default class ParseApp {
4445
supportedPushLocales,
4546
preventSchemaEdits,
4647
graphQLServerURL,
47-
columnPreference
48+
columnPreference,
49+
classPreference
4850
}) {
4951
this.name = appName;
5052
this.createdAt = created_at ? new Date(created_at) : new Date();
@@ -97,6 +99,23 @@ export default class ParseApp {
9799
}
98100

99101
this.hasCheckedForMigraton = false;
102+
103+
if (classPreference) {
104+
for (const className in classPreference) {
105+
const preferences = getPreferences(appId, className) || { filters: [] };
106+
const { filters } = classPreference[className];
107+
for (const filter of filters) {
108+
if (Array.isArray(filter.filter)) {
109+
filter.filter = JSON.stringify(filter.filter);
110+
}
111+
if (preferences.filters.some(row => JSON.stringify(row) === JSON.stringify(filter))) {
112+
continue;
113+
}
114+
preferences.filters.push(filter);
115+
}
116+
updatePreferences(preferences, appId, className);
117+
}
118+
}
100119
}
101120

102121
setParseKeys() {

0 commit comments

Comments
 (0)