diff --git a/README.md b/README.md index fb73ee0ee7..0a12d09e8c 100644 --- a/README.md +++ b/README.md @@ -329,13 +329,39 @@ If you have classes with a lot of columns and you filter them often with the sam { "name": "email", "filterSortToTop": true - } + } ] } } ] ``` +### Persistent Filters + +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. + +For example: + +```json +"apps": [{ + "classPreference": { + "_Role": { + "filters": [{ + "name": "Filter Name", + "filter": [ + { + "field": "objectId", + "constraint": "exists" + } + ] + }] + } + } +}] +``` + +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*. + # Running as Express Middleware 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 The user requires an authenticator app to generate the one-time password. These apps are provided by many 3rd parties and mostly for free. -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: +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: ```json { diff --git a/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js b/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js index 3dfc4d4bc7..c528c19fd0 100644 --- a/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js +++ b/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js @@ -16,6 +16,7 @@ import Toolbar from 'components/Toolbar/Toolbar.react'; import CodeSnippet from 'components/CodeSnippet/CodeSnippet.react'; import Notification from 'dashboard/Data/Browser/Notification.react'; import * as ColumnPreferences from 'lib/ColumnPreferences'; +import * as ClassPreferences from 'lib/ClassPreferences'; import bcrypt from 'bcryptjs'; import * as OTPAuth from 'otpauth'; import QRCode from 'qrcode'; @@ -38,9 +39,10 @@ export default class DashboardSettings extends DashboardView { message: null, passwordInput: '', passwordHidden: true, - columnData: { + copyData: { data: '', show: false, + type: '' }, newUser: { data: '', @@ -53,7 +55,14 @@ export default class DashboardSettings extends DashboardView { getColumns() { const data = ColumnPreferences.getAllPreferences(this.context.applicationId); this.setState({ - columnData: { data: JSON.stringify(data, null, 2), show: true }, + copyData: { data: JSON.stringify(data, null, 2), show: true, type: 'Column Preferences' }, + }); + } + + getClasses() { + const data = ClassPreferences.getAllPreferences(this.context.applicationId); + this.setState({ + copyData: { data: JSON.stringify(data, null, 2), show: true, type: 'Class Preferences' }, }); } @@ -190,14 +199,14 @@ export default class DashboardSettings extends DashboardView { this.createUser()} />} /> ); - const columnPreferences = ( + const copyData = (
-
- +
+
-
); @@ -225,9 +234,10 @@ export default class DashboardSettings extends DashboardView {
} input={ this.getColumns()} />} /> + } input={ this.getClasses()} />} /> } input={ this.setState({ createUserInput: true })} />} />
- {this.state.columnData.show && columnPreferences} + {this.state.copyData.show && copyData} {this.state.createUserInput && createUserInput} {this.state.newUser.show && userData} diff --git a/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss b/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss index 579c813055..66ddd63796 100644 --- a/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss +++ b/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss @@ -1,4 +1,4 @@ -.columnData { +.copyData { max-height: 50vh; overflow-y: scroll; } diff --git a/src/lib/ClassPreferences.js b/src/lib/ClassPreferences.js index 75cc0d7b1e..b36b1a5a7b 100644 --- a/src/lib/ClassPreferences.js +++ b/src/lib/ClassPreferences.js @@ -38,3 +38,26 @@ export function getPreferences(appId, className) { function path(appId, className) { return `ParseDashboard:${VERSION}:${appId}:ClassPreference:${className}`; } + +export function getAllPreferences(appId) { + const storageKeys = Object.keys(localStorage); + const result = {}; + for (const key of storageKeys) { + const split = key.split(':') + if (split.length <= 1 || split[2] !== appId) { + continue; + } + const className = split.at(-1); + const preferences = getPreferences(appId, className); + if (preferences) { + preferences.filters = preferences.filters.map(filter => { + if (typeof filter.filter === 'string') { + filter.filter = JSON.parse(filter.filter); + } + return filter; + }); + result[className] = preferences; + } + } + return result; +} diff --git a/src/lib/ParseApp.js b/src/lib/ParseApp.js index 7a4bb1b888..b71d14dc48 100644 --- a/src/lib/ParseApp.js +++ b/src/lib/ParseApp.js @@ -8,6 +8,7 @@ import * as AJAX from 'lib/AJAX'; import encodeFormData from 'lib/encodeFormData'; import Parse from 'parse'; +import { updatePreferences, getPreferences } from 'lib/ClassPreferences'; function setEnablePushSource(setting, enable) { let path = `/apps/${this.slug}/update_push_notifications`; @@ -44,7 +45,8 @@ export default class ParseApp { supportedPushLocales, preventSchemaEdits, graphQLServerURL, - columnPreference + columnPreference, + classPreference }) { this.name = appName; this.createdAt = created_at ? new Date(created_at) : new Date(); @@ -97,6 +99,23 @@ export default class ParseApp { } this.hasCheckedForMigraton = false; + + if (classPreference) { + for (const className in classPreference) { + const preferences = getPreferences(appId, className) || { filters: [] }; + const { filters } = classPreference[className]; + for (const filter of filters) { + if (Array.isArray(filter.filter)) { + filter.filter = JSON.stringify(filter.filter); + } + if (preferences.filters.some(row => JSON.stringify(row) === JSON.stringify(filter))) { + continue; + } + preferences.filters.push(filter); + } + updatePreferences(preferences, appId, className); + } + } } setParseKeys() {