From 228d83903fd8698da7b96a5b74699f3ff2d5dad4 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 21 May 2023 11:06:33 +1000 Subject: [PATCH 01/30] feat: Add visual configurator for Parse Dashboard settings (#2406) --- src/dashboard/Dashboard.js | 4 +- src/dashboard/DashboardView.react.js | 7 +- .../DashboardSettings.react.js | 242 ++++++++++++++++++ .../DashboardSettings/DashboardSettings.scss | 28 ++ src/lib/ColumnPreferences.js | 21 +- 5 files changed, 297 insertions(+), 5 deletions(-) create mode 100644 src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js create mode 100644 src/dashboard/Settings/DashboardSettings/DashboardSettings.scss diff --git a/src/dashboard/Dashboard.js b/src/dashboard/Dashboard.js index 9993dc0722..516133dead 100644 --- a/src/dashboard/Dashboard.js +++ b/src/dashboard/Dashboard.js @@ -51,6 +51,7 @@ import { setBasePath } from 'lib/AJAX'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import Playground from './Data/Playground/Playground.react'; +import DashboardSettings from './Settings/DashboardSettings/DashboardSettings.react'; const ShowSchemaOverview = false; //In progress features. Change false to true to work on this feature. @@ -199,12 +200,13 @@ export default class Dashboard extends React.Component { const SettingsRoute = ( }> + } /> } /> } /> } /> } /> } /> - } /> + } /> ) diff --git a/src/dashboard/DashboardView.react.js b/src/dashboard/DashboardView.react.js index 8402dcc04f..b224ac6778 100644 --- a/src/dashboard/DashboardView.react.js +++ b/src/dashboard/DashboardView.react.js @@ -198,7 +198,10 @@ export default class DashboardView extends React.Component { } */ - let settingsSections = []; + const settingsSections = [{ + name: 'Dashboard', + link: '/settings/dashboard' + }]; // Settings - nothing remotely like this in parse-server yet. Maybe it will arrive soon. /* @@ -292,7 +295,7 @@ export default class DashboardView extends React.Component { ); let content =
{this.renderContent()}
; - const canRoute = [...coreSubsections, ...pushSubsections] + const canRoute = [...coreSubsections, ...pushSubsections, ...settingsSections] .map(({ link }) => link.split('/')[1]) .includes(this.state.route); diff --git a/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js b/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js new file mode 100644 index 0000000000..3dfc4d4bc7 --- /dev/null +++ b/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js @@ -0,0 +1,242 @@ +import DashboardView from 'dashboard/DashboardView.react'; +import Field from 'components/Field/Field.react'; +import Fieldset from 'components/Fieldset/Fieldset.react'; +import FlowView from 'components/FlowView/FlowView.react'; +import FormButton from 'components/FormButton/FormButton.react'; +import Label from 'components/Label/Label.react'; +import Button from 'components/Button/Button.react'; +import React from 'react'; +import styles from 'dashboard/Settings/DashboardSettings/DashboardSettings.scss'; +import TextInput from 'components/TextInput/TextInput.react'; +import Toggle from 'components/Toggle/Toggle.react'; +import Icon from 'components/Icon/Icon.react'; +import Dropdown from 'components/Dropdown/Dropdown.react'; +import Option from 'components/Dropdown/Option.react'; +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 bcrypt from 'bcryptjs'; +import * as OTPAuth from 'otpauth'; +import QRCode from 'qrcode'; + +export default class DashboardSettings extends DashboardView { + constructor() { + super(); + this.section = 'App Settings'; + this.subsection = 'Dashboard Configuration'; + + this.state = { + createUserInput: false, + username: '', + password: '', + encrypt: true, + mfa: false, + mfaDigits: 6, + mfaPeriod: 30, + mfaAlgorithm: 'SHA1', + message: null, + passwordInput: '', + passwordHidden: true, + columnData: { + data: '', + show: false, + }, + newUser: { + data: '', + show: false, + mfa: '', + }, + }; + } + + getColumns() { + const data = ColumnPreferences.getAllPreferences(this.context.applicationId); + this.setState({ + columnData: { data: JSON.stringify(data, null, 2), show: true }, + }); + } + + copy(data, label) { + navigator.clipboard.writeText(data); + this.showNote(`${label} copied to clipboard`); + } + + createUser() { + if (!this.state.username) { + this.showNote('Please enter a username'); + return; + } + if (!this.state.password) { + this.showNote('Please enter a password'); + return; + } + + let pass = this.state.password; + if (this.state.encrypt) { + const salt = bcrypt.genSaltSync(10); + pass = bcrypt.hashSync(pass, salt); + } + + const user = { + username: this.state.username, + pass, + }; + + let mfa; + if (this.state.mfa) { + const secret = new OTPAuth.Secret(); + const totp = new OTPAuth.TOTP({ + issuer: this.context.name, + label: user.username, + algorithm: this.state.mfaAlgorithm || 'SHA1', + digits: this.state.mfaDigits || 6, + period: this.state.mfaPeriod || 30, + secret, + }); + mfa = totp.toString(); + user.mfa = secret.base32; + if (totp.algorithm !== 'SHA1') { + user.mfaAlgorithm = totp.algorithm; + } + if (totp.digits != 6) { + user.mfaDigits = totp.digits; + } + if (totp.period != 30) { + user.mfaPeriod = totp.period; + } + + setTimeout(() => { + const canvas = document.getElementById('canvas'); + QRCode.toCanvas(canvas, mfa); + }, 10); + } + + this.setState({ + newUser: { + show: true, + data: JSON.stringify(user, null, 2), + mfa, + }, + }); + } + + generatePassword() { + let chars = '0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*()ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + let pwordLength = 20; + let password = ''; + + const array = new Uint32Array(chars.length); + window.crypto.getRandomValues(array); + + for (let i = 0; i < pwordLength; i++) { + password += chars[array[i] % chars.length]; + } + this.setState({ password }); + } + + showNote(message) { + if (!message) { + return; + } + + clearTimeout(this.noteTimeout); + + this.setState({ message }); + + this.noteTimeout = setTimeout(() => { + this.setState({ message: null }); + }, 3500); + } + + renderForm() { + const createUserInput = ( +
+ } input={ this.setState({ username })} />} /> + + Password + this.setState({ passwordHidden: !this.state.passwordHidden })}> + + + + } + description={ this.generatePassword()}>Generate strong password} + /> + } + input={
+ ); + const columnPreferences = ( +
+
+ +
+
+
+
+ ); + const userData = ( +
+ Add the following data to your Parse Dashboard configuration "users": + {this.state.encrypt &&
Make sure the dashboard option useEncryptedPasswords is set to true.
} +
+ +
+ {this.state.mfa && ( + + )} +
+
+
+ ); + return ( +
+
+ } input={ this.getColumns()} />} /> + } input={ this.setState({ createUserInput: true })} />} /> +
+ {this.state.columnData.show && columnPreferences} + {this.state.createUserInput && createUserInput} + {this.state.newUser.show && userData} + + +
+ ); + } + + renderContent() { + return {}} onSubmit={() => {}} renderForm={() => this.renderForm()} />; + } +} diff --git a/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss b/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss new file mode 100644 index 0000000000..579c813055 --- /dev/null +++ b/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss @@ -0,0 +1,28 @@ +.columnData { + max-height: 50vh; + overflow-y: scroll; +} +.newUser { + max-height: 100px; + overflow-y: scroll; +} +.settings_page { + padding: 120px 0 80px 0; +} +.footer { + display: flex; + padding: 10px; + justify-content: end; + gap: 10px; +} +.password { + display: flex; + gap: 4px; +} +.userData { + padding: 10px; +} +.mfa { + display: block; + margin-top: 10px; +} diff --git a/src/lib/ColumnPreferences.js b/src/lib/ColumnPreferences.js index a9c691d0ef..78619dbe18 100644 --- a/src/lib/ColumnPreferences.js +++ b/src/lib/ColumnPreferences.js @@ -45,6 +45,23 @@ export function getPreferences(appId, 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) { + continue; + } + const className = split.at(-1); + const preferences = getPreferences(appId, className); + if (preferences) { + result[className] = preferences; + } + } + return result; +} + export function getColumnSort(sortBy, appId, className) { let cachedSort = getPreferences(appId, COLUMN_SORT) || [ { name: className, value: DEFAULT_COLUMN_SORT } ]; let ordering = [].concat(cachedSort); @@ -74,7 +91,7 @@ export function getColumnSort(sortBy, appId, className) { export function getOrder(cols, appId, className, defaultPrefs) { let prefs = getPreferences(appId, className) || [ { name: 'objectId', width: DEFAULT_WIDTH, visible: true, cached: true } ]; - + if (defaultPrefs) { // Check that every default pref is in the prefs array. @@ -85,7 +102,7 @@ export function getOrder(cols, appId, className, defaultPrefs) { } }); - // Iterate over the current prefs + // Iterate over the current prefs prefs = prefs.map((prefsItem) => { // Get the default prefs item. const defaultPrefsItem = defaultPrefs.find(defaultPrefsItem => defaultPrefsItem.name === prefsItem.name) || {}; From 52ca7fc064cca60c7c7474da7417b39d0e596dbd Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 21 May 2023 01:09:02 +0000 Subject: [PATCH 02/30] chore(release): 5.2.0-alpha.1 [skip ci] # [5.2.0-alpha.1](https://github.com/ParsePlatform/parse-dashboard/compare/5.1.0...5.2.0-alpha.1) (2023-05-21) ### Bug Fixes * Uncaught error when editing Number field in Edit Row dialog ([#2401](https://github.com/ParsePlatform/parse-dashboard/issues/2401)) ([26bd6fa](https://github.com/ParsePlatform/parse-dashboard/commit/26bd6fa39be1076621856a9c86dcd1307f8f7fdd)) ### Features * Add visual configurator for Parse Dashboard settings ([#2406](https://github.com/ParsePlatform/parse-dashboard/issues/2406)) ([228d839](https://github.com/ParsePlatform/parse-dashboard/commit/228d83903fd8698da7b96a5b74699f3ff2d5dad4)) * Data types and pointer classes are sorted alphabetically in dialog to add new column ([#2400](https://github.com/ParsePlatform/parse-dashboard/issues/2400)) ([d9d285b](https://github.com/ParsePlatform/parse-dashboard/commit/d9d285b7f90434d3bb138c2c765272498e3f09c3)) * Sort Cloud Code Jobs alphabetically ([#2402](https://github.com/ParsePlatform/parse-dashboard/issues/2402)) ([77fc372](https://github.com/ParsePlatform/parse-dashboard/commit/77fc372bedb9fb4eca728c1bc076e823c5bc3a2c)) --- changelogs/CHANGELOG_alpha.md | 13 +++++++++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 6233812411..aea883a3e4 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,16 @@ +# [5.2.0-alpha.1](https://github.com/ParsePlatform/parse-dashboard/compare/5.1.0...5.2.0-alpha.1) (2023-05-21) + + +### Bug Fixes + +* Uncaught error when editing Number field in Edit Row dialog ([#2401](https://github.com/ParsePlatform/parse-dashboard/issues/2401)) ([26bd6fa](https://github.com/ParsePlatform/parse-dashboard/commit/26bd6fa39be1076621856a9c86dcd1307f8f7fdd)) + +### Features + +* Add visual configurator for Parse Dashboard settings ([#2406](https://github.com/ParsePlatform/parse-dashboard/issues/2406)) ([228d839](https://github.com/ParsePlatform/parse-dashboard/commit/228d83903fd8698da7b96a5b74699f3ff2d5dad4)) +* Data types and pointer classes are sorted alphabetically in dialog to add new column ([#2400](https://github.com/ParsePlatform/parse-dashboard/issues/2400)) ([d9d285b](https://github.com/ParsePlatform/parse-dashboard/commit/d9d285b7f90434d3bb138c2c765272498e3f09c3)) +* Sort Cloud Code Jobs alphabetically ([#2402](https://github.com/ParsePlatform/parse-dashboard/issues/2402)) ([77fc372](https://github.com/ParsePlatform/parse-dashboard/commit/77fc372bedb9fb4eca728c1bc076e823c5bc3a2c)) + # [5.1.0-alpha.13](https://github.com/ParsePlatform/parse-dashboard/compare/5.1.0-alpha.12...5.1.0-alpha.13) (2023-03-07) diff --git a/package-lock.json b/package-lock.json index 6b9e508d00..33e793eccb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-beta.1", + "version": "5.2.0-alpha.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c13ea52156..9f18de539c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-beta.1", + "version": "5.2.0-alpha.1", "repository": { "type": "git", "url": "https://github.com/ParsePlatform/parse-dashboard" From a20cb8e534d3fecd8d337463864b15048772a9a5 Mon Sep 17 00:00:00 2001 From: Frans Bouwmeester Date: Sat, 27 May 2023 02:32:24 +0200 Subject: [PATCH 03/30] feat: Add links to users and roles in ACL dialog and handle invalid entries (#2436) --- src/components/ACLEditor/ACLEditor.react.js | 11 ++++- .../PermissionsDialog.react.js | 45 +++++++++++++++---- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/components/ACLEditor/ACLEditor.react.js b/src/components/ACLEditor/ACLEditor.react.js index a1f46dc328..ff58f22b0e 100644 --- a/src/components/ACLEditor/ACLEditor.react.js +++ b/src/components/ACLEditor/ACLEditor.react.js @@ -9,8 +9,10 @@ import Parse from 'parse'; import PermissionsDialog from 'components/PermissionsDialog/PermissionsDialog.react'; import React from 'react'; -function validateEntry(text) { +function validateEntry(text, returnInvalid = true) { + let type = 'unknown'; + let entry = text; let userQuery; let roleQuery; @@ -19,22 +21,26 @@ function validateEntry(text) { } if (text.startsWith('user:')) { + type = 'user'; // no need to query roles roleQuery = { find: () => Promise.resolve([]) }; let user = text.substring(5); + entry = user; userQuery = new Parse.Query.or( new Parse.Query(Parse.User).equalTo('username', user), new Parse.Query(Parse.User).equalTo('objectId', user) ); } else if (text.startsWith('role:')) { + type = 'role'; // no need to query users userQuery = { find: () => Promise.resolve([]) }; let role = text.substring(5); + entry = role; roleQuery = new Parse.Query.or( new Parse.Query(Parse.Role).equalTo('name', role), new Parse.Query(Parse.Role).equalTo('objectId', role) @@ -61,6 +67,9 @@ function validateEntry(text) { } else if (role.length > 0) { return { entry: role[0], type: 'role' }; } else { + if(returnInvalid) { + return Promise.resolve({entry, type}) + } return Promise.reject(); } }); diff --git a/src/components/PermissionsDialog/PermissionsDialog.react.js b/src/components/PermissionsDialog/PermissionsDialog.react.js index b4d749494f..8ae4241d19 100644 --- a/src/components/PermissionsDialog/PermissionsDialog.react.js +++ b/src/components/PermissionsDialog/PermissionsDialog.react.js @@ -20,6 +20,8 @@ import Toggle from 'components/Toggle/Toggle.react'; import Autocomplete from 'components/Autocomplete/Autocomplete.react'; import { Map, fromJS } from 'immutable'; import TrackVisibility from 'components/TrackVisibility/TrackVisibility.react'; +import {CurrentApp} from '../../context/currentApp'; +import generatePath from '../../lib/generatePath'; let origin = new Position(0, 0); @@ -517,6 +519,8 @@ function renderPointerCheckboxes( const intersectionMargin = '10px 0px 0px 20px'; export default class PermissionsDialog extends React.Component { + static contextType = CurrentApp; + constructor(props) { super(props); @@ -647,7 +651,15 @@ export default class PermissionsDialog extends React.Component { let key; let value = {}; - if (type === 'user') { + if(typeof entry === 'string') { + key = type + ':' + entry; + value[type] = { + name: entry, + id: undefined + }; + } + + else if (type === 'user') { key = entry.id; value[type] = { name: entry.get('username'), @@ -655,7 +667,7 @@ export default class PermissionsDialog extends React.Component { }; } - if (type === 'role') { + else if (type === 'role') { key = 'role:' + entry.getName(); value[type] = { name: entry.getName(), @@ -663,7 +675,7 @@ export default class PermissionsDialog extends React.Component { }; } - if (type === 'pointer') { + else if (type === 'pointer') { key = entry; value[type] = true; } @@ -971,7 +983,21 @@ export default class PermissionsDialog extends React.Component { return output; } + urlForKey(key) { + let isRole = key.startsWith('role:') + let className = isRole ? '_Role' : '_User'; + let field = isRole ? 'name' : 'objectId'; + let value = isRole ? key.replace('role:', '') : key + let filters = JSON.stringify([{ + field, + constraint: 'eq', + compareTo: value + }]); + return generatePath(this.context, `browser/${className}?filters=${encodeURIComponent(filters)}`); + } + renderRow(key, columns, types) { + const pill = text => ( @@ -982,20 +1008,21 @@ export default class PermissionsDialog extends React.Component { const type = (types && types.get(key)) || {}; let pointer = this.state.pointerPerms.has(key); - let label = {key}; + let label = {key}; if (type.user) { label = (

- {type.user.id} + + {type.user.id} + {pill('User')}

- {'username: '} - {type.user.name} + username: {type.user.name ?? 'user not found'}

); @@ -1005,11 +1032,11 @@ export default class PermissionsDialog extends React.Component {

{'role:'} - {type.role.name} + {type.role.name}

- id: {type.role.id} + id: {type.role.id ?? 'role not found'}

); From 163bd8d1cceae70cb6296f8e30b2240735ba48af Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 27 May 2023 00:35:03 +0000 Subject: [PATCH 04/30] chore(release): 5.2.0-alpha.2 [skip ci] # [5.2.0-alpha.2](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.1...5.2.0-alpha.2) (2023-05-27) ### Features * Add links to users and roles in ACL dialog and handle invalid entries ([#2436](https://github.com/ParsePlatform/parse-dashboard/issues/2436)) ([a20cb8e](https://github.com/ParsePlatform/parse-dashboard/commit/a20cb8e534d3fecd8d337463864b15048772a9a5)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index aea883a3e4..18cb12d31d 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [5.2.0-alpha.2](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.1...5.2.0-alpha.2) (2023-05-27) + + +### Features + +* Add links to users and roles in ACL dialog and handle invalid entries ([#2436](https://github.com/ParsePlatform/parse-dashboard/issues/2436)) ([a20cb8e](https://github.com/ParsePlatform/parse-dashboard/commit/a20cb8e534d3fecd8d337463864b15048772a9a5)) + # [5.2.0-alpha.1](https://github.com/ParsePlatform/parse-dashboard/compare/5.1.0...5.2.0-alpha.1) (2023-05-21) diff --git a/package-lock.json b/package-lock.json index 33e793eccb..7e4aee5694 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.1", + "version": "5.2.0-alpha.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9f18de539c..fac808a02f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.1", + "version": "5.2.0-alpha.2", "repository": { "type": "git", "url": "https://github.com/ParsePlatform/parse-dashboard" From 6f3dab60c257c76345235acfd3e43aafadeb84c8 Mon Sep 17 00:00:00 2001 From: patelmilanun <20059797+patelmilanun@users.noreply.github.com> Date: Sat, 27 May 2023 04:25:38 -0700 Subject: [PATCH 05/30] =?UTF-8?q?fix:=20Empty=20table=20in=20data=20browse?= =?UTF-8?q?r=20when=20navigating=20back=20using=20the=20"back=E2=80=9D=20b?= =?UTF-8?q?utton=20(#2423)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/dashboard/Data/Browser/Browser.react.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js index 46e18e8947..b02f19f77f 100644 --- a/src/dashboard/Data/Browser/Browser.react.js +++ b/src/dashboard/Data/Browser/Browser.react.js @@ -179,7 +179,7 @@ class Browser extends DashboardView { } componentWillReceiveProps(nextProps, nextContext) { - if (this.props.params.appId !== nextProps.params.appId || this.props.params.className !== nextProps.params.className || this.props.location.search !== nextProps.location.search) { + if (this.props.params.appId !== nextProps.params.appId || this.props.params.className !== nextProps.params.className || this.props.location.search !== nextProps.location.search || this.props.params?.relationName !== nextProps.params?.relationName) { if (this.props.params.appId !== nextProps.params.appId || !this.props.params.className) { this.setState({ counts: {} }); Parse.Object._clearAllState(); From 9dc8dabaacee336e9b6fac3c2f714a7e8440d5ac Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 27 May 2023 11:27:28 +0000 Subject: [PATCH 06/30] chore(release): 5.2.0-alpha.3 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [5.2.0-alpha.3](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.2...5.2.0-alpha.3) (2023-05-27) ### Bug Fixes * Empty table in data browser when navigating back using the "back” button ([#2423](https://github.com/ParsePlatform/parse-dashboard/issues/2423)) ([6f3dab6](https://github.com/ParsePlatform/parse-dashboard/commit/6f3dab60c257c76345235acfd3e43aafadeb84c8)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 18cb12d31d..7c2dc77fc4 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [5.2.0-alpha.3](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.2...5.2.0-alpha.3) (2023-05-27) + + +### Bug Fixes + +* Empty table in data browser when navigating back using the "back” button ([#2423](https://github.com/ParsePlatform/parse-dashboard/issues/2423)) ([6f3dab6](https://github.com/ParsePlatform/parse-dashboard/commit/6f3dab60c257c76345235acfd3e43aafadeb84c8)) + # [5.2.0-alpha.2](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.1...5.2.0-alpha.2) (2023-05-27) diff --git a/package-lock.json b/package-lock.json index 7e4aee5694..741b2e6104 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.2", + "version": "5.2.0-alpha.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fac808a02f..5952a22272 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.2", + "version": "5.2.0-alpha.3", "repository": { "type": "git", "url": "https://github.com/ParsePlatform/parse-dashboard" From b96b48fdd5ce452783e0887c36231971a1822173 Mon Sep 17 00:00:00 2001 From: Ashish Baravaliya <49753983+AshishBarvaliya@users.noreply.github.com> Date: Sat, 27 May 2023 07:39:29 -0400 Subject: [PATCH 07/30] fix: Incorrect date picker position in data browser filter dialog (#2425) --- .../BrowserFilter/BrowserFilter.react.js | 9 ++- .../BrowserFilter/FilterRow.react.js | 56 ++++++++++--------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/components/BrowserFilter/BrowserFilter.react.js b/src/components/BrowserFilter/BrowserFilter.react.js index 17de624df6..20446a3654 100644 --- a/src/components/BrowserFilter/BrowserFilter.react.js +++ b/src/components/BrowserFilter/BrowserFilter.react.js @@ -24,6 +24,7 @@ export default class BrowserFilter extends React.Component { this.state = { open: false, + editMode: true, filters: new List(), blacklistedFilters: Filters.BLACKLISTED_FILTERS.concat(props.blacklistedFilters) }; @@ -48,7 +49,8 @@ export default class BrowserFilter extends React.Component { } this.setState(prevState => ({ open: !prevState.open, - filters: filters + filters: filters, + editMode: this.props.filters.size === 0 })); this.props.setCurrent(null); } @@ -59,7 +61,8 @@ export default class BrowserFilter extends React.Component { this.setState(({ filters }) => ({ filters: filters.push( new Map({ field: field, constraint: available[field][0] }) - ) + ), + editMode: true })); } @@ -116,7 +119,7 @@ export default class BrowserFilter extends React.Component { onChange={filters => this.setState({ filters: filters })} onSearch={this.apply.bind(this)} renderRow={props => ( - 0} parentContentId={POPOVER_CONTENT_ID} /> + 0} editMode={this.state.editMode} parentContentId={POPOVER_CONTENT_ID} /> )} />
diff --git a/src/components/BrowserFilter/FilterRow.react.js b/src/components/BrowserFilter/FilterRow.react.js index 80cf0e0ba3..cf8cc5a90d 100644 --- a/src/components/BrowserFilter/FilterRow.react.js +++ b/src/components/BrowserFilter/FilterRow.react.js @@ -11,7 +11,7 @@ import DateTimeEntry from 'components/DateTimeEntry/DateTimeEntry.react'; import Icon from 'components/Icon/Icon.react'; import Parse from 'parse'; import PropTypes from 'lib/PropTypes'; -import React from 'react'; +import React, { useCallback } from 'react'; import styles from 'components/BrowserFilter/BrowserFilter.scss'; import validateNumeric from 'lib/validateNumeric'; @@ -20,13 +20,7 @@ for (let c in Constraints) { constraintLookup[Constraints[c].name] = c; } -let setFocus = (input) => { - if (input !== null) { - input.focus(); - } -} - -function compareValue(info, value, onChangeCompareTo, onKeyDown, active, parentContentId) { +function compareValue(info, value, onChangeCompareTo, onKeyDown, active, parentContentId, setFocus) { switch (info.type) { case null: return null; @@ -91,25 +85,35 @@ let FilterRow = ({ onDeleteRow, active, parentContentId, - }) => ( -
- - Constraints[c].name)} - onChange={(c) => onChangeConstraint(constraintLookup[c], compareTo)} /> - {compareValue(compareInfo, compareTo, onChangeCompareTo, onKeyDown, active, parentContentId)} - -
-); + editMode + }) => { + + let setFocus = useCallback((input) => { + if (input !== null && editMode) { + input.focus(); + } + }, []) + + return ( +
+ + Constraints[c].name)} + onChange={(c) => onChangeConstraint(constraintLookup[c], compareTo)} /> + {compareValue(compareInfo, compareTo, onChangeCompareTo, onKeyDown, active, parentContentId, setFocus)} + +
+ ); +} -export default FilterRow; +export default React.memo(FilterRow); FilterRow.propTypes = { fields: PropTypes.arrayOf(PropTypes.string).isRequired, From cd1a46ea7cf857da8049e1d6c34ae0a1c6ba490a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 27 May 2023 11:41:45 +0000 Subject: [PATCH 08/30] chore(release): 5.2.0-alpha.4 [skip ci] # [5.2.0-alpha.4](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.3...5.2.0-alpha.4) (2023-05-27) ### Bug Fixes * Incorrect date picker position in data browser filter dialog ([#2425](https://github.com/ParsePlatform/parse-dashboard/issues/2425)) ([b96b48f](https://github.com/ParsePlatform/parse-dashboard/commit/b96b48fdd5ce452783e0887c36231971a1822173)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 7c2dc77fc4..566ecf9a02 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [5.2.0-alpha.4](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.3...5.2.0-alpha.4) (2023-05-27) + + +### Bug Fixes + +* Incorrect date picker position in data browser filter dialog ([#2425](https://github.com/ParsePlatform/parse-dashboard/issues/2425)) ([b96b48f](https://github.com/ParsePlatform/parse-dashboard/commit/b96b48fdd5ce452783e0887c36231971a1822173)) + # [5.2.0-alpha.3](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.2...5.2.0-alpha.3) (2023-05-27) diff --git a/package-lock.json b/package-lock.json index 741b2e6104..678db47123 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.3", + "version": "5.2.0-alpha.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5952a22272..0dd9bdfe5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.3", + "version": "5.2.0-alpha.4", "repository": { "type": "git", "url": "https://github.com/ParsePlatform/parse-dashboard" From 6c5f19f6ed2cda1f1f0dc59bdaed3ae49f264380 Mon Sep 17 00:00:00 2001 From: Ghanshyam Tanchak <81280194+ghanshyamtanchak@users.noreply.github.com> Date: Sat, 27 May 2023 17:56:21 +0530 Subject: [PATCH 09/30] fix: Back button in data browser disappears after page refresh (#2421) --- src/components/Toolbar/Toolbar.react.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Toolbar/Toolbar.react.js b/src/components/Toolbar/Toolbar.react.js index 505c37021e..3d7ac51e3b 100644 --- a/src/components/Toolbar/Toolbar.react.js +++ b/src/components/Toolbar/Toolbar.react.js @@ -15,7 +15,7 @@ let Toolbar = (props) => { const action = useNavigationType(); const navigate = useNavigate(); let backButton; - if ((props.relation || (props.filters && props.filters.size)) && action !== NavigationType.Pop) { + if (props.relation || ((props.filters && props.filters.size) && action !== NavigationType.Pop)) { backButton = ( Date: Sat, 27 May 2023 12:29:02 +0000 Subject: [PATCH 10/30] chore(release): 5.2.0-alpha.5 [skip ci] # [5.2.0-alpha.5](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.4...5.2.0-alpha.5) (2023-05-27) ### Bug Fixes * Back button in data browser disappears after page refresh ([#2421](https://github.com/ParsePlatform/parse-dashboard/issues/2421)) ([6c5f19f](https://github.com/ParsePlatform/parse-dashboard/commit/6c5f19f6ed2cda1f1f0dc59bdaed3ae49f264380)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 566ecf9a02..c8bc2583ad 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [5.2.0-alpha.5](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.4...5.2.0-alpha.5) (2023-05-27) + + +### Bug Fixes + +* Back button in data browser disappears after page refresh ([#2421](https://github.com/ParsePlatform/parse-dashboard/issues/2421)) ([6c5f19f](https://github.com/ParsePlatform/parse-dashboard/commit/6c5f19f6ed2cda1f1f0dc59bdaed3ae49f264380)) + # [5.2.0-alpha.4](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.3...5.2.0-alpha.4) (2023-05-27) diff --git a/package-lock.json b/package-lock.json index 678db47123..bdfefa5b33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.4", + "version": "5.2.0-alpha.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0dd9bdfe5f..8d904d9aa8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.4", + "version": "5.2.0-alpha.5", "repository": { "type": "git", "url": "https://github.com/ParsePlatform/parse-dashboard" From 422ffb2897bb2664eb47b5aaad5094a8b39431bf Mon Sep 17 00:00:00 2001 From: Ashish Baravaliya <49753983+AshishBarvaliya@users.noreply.github.com> Date: Sat, 27 May 2023 08:33:18 -0400 Subject: [PATCH 11/30] fix: Text input cursor jumps to first position when writing long text (#2413) --- src/components/TextInput/TextInput.react.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/TextInput/TextInput.react.js b/src/components/TextInput/TextInput.react.js index 38149edf12..6ab9df19a5 100644 --- a/src/components/TextInput/TextInput.react.js +++ b/src/components/TextInput/TextInput.react.js @@ -15,6 +15,7 @@ class TextInput extends React.Component { if (props.multiline !== this.props.multiline) { const node = props.forwardedRef.current; node.focus(); + if (this.props.value) node.setSelectionRange(this.props.value.length, this.props.value.length); } } From 60407303a450f8db57023f6d300f9e761cde293b Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 27 May 2023 12:35:11 +0000 Subject: [PATCH 12/30] chore(release): 5.2.0-alpha.6 [skip ci] # [5.2.0-alpha.6](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.5...5.2.0-alpha.6) (2023-05-27) ### Bug Fixes * Text input cursor jumps to first position when writing long text ([#2413](https://github.com/ParsePlatform/parse-dashboard/issues/2413)) ([422ffb2](https://github.com/ParsePlatform/parse-dashboard/commit/422ffb2897bb2664eb47b5aaad5094a8b39431bf)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index c8bc2583ad..cba8221eb6 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [5.2.0-alpha.6](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.5...5.2.0-alpha.6) (2023-05-27) + + +### Bug Fixes + +* Text input cursor jumps to first position when writing long text ([#2413](https://github.com/ParsePlatform/parse-dashboard/issues/2413)) ([422ffb2](https://github.com/ParsePlatform/parse-dashboard/commit/422ffb2897bb2664eb47b5aaad5094a8b39431bf)) + # [5.2.0-alpha.5](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.4...5.2.0-alpha.5) (2023-05-27) diff --git a/package-lock.json b/package-lock.json index bdfefa5b33..a145b1526c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.5", + "version": "5.2.0-alpha.6", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 8d904d9aa8..0c021df051 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.5", + "version": "5.2.0-alpha.6", "repository": { "type": "git", "url": "https://github.com/ParsePlatform/parse-dashboard" From 8190bebd07c0afcc2a3ec6e2c9c4f0f4de90811c Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 27 May 2023 14:38:34 +0200 Subject: [PATCH 13/30] ci: Prevent workflow concurrency (#2438) --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 498403c49c..c3cf088219 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,3 +151,6 @@ jobs: run: ./scripts/before_script.sh env: CI: true +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true From e761f9715a8366466c6620994ce698adbbfc48cd Mon Sep 17 00:00:00 2001 From: patelmilanun <20059797+patelmilanun@users.noreply.github.com> Date: Sat, 27 May 2023 05:39:49 -0700 Subject: [PATCH 14/30] fix: File uploading status not updating in data browser (#2422) --- src/components/BrowserCell/BrowserCell.react.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/BrowserCell/BrowserCell.react.js b/src/components/BrowserCell/BrowserCell.react.js index 96d40b8fcc..b32e5e7a40 100644 --- a/src/components/BrowserCell/BrowserCell.react.js +++ b/src/components/BrowserCell/BrowserCell.react.js @@ -177,6 +177,9 @@ export default class BrowserCell extends Component { componentDidUpdate(prevProps) { if ( this.props.value !== prevProps.value ) { this.renderCellContent(); + this.props.value._previousSave + ?.then(() => this.renderCellContent()) + ?.catch(err => console.log(err)) } if (this.props.current) { const node = this.cellRef.current; From 1b23966c809bdda819c96b313a4b33bb42e05c49 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 27 May 2023 12:41:57 +0000 Subject: [PATCH 15/30] chore(release): 5.2.0-alpha.7 [skip ci] # [5.2.0-alpha.7](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.6...5.2.0-alpha.7) (2023-05-27) ### Bug Fixes * File uploading status not updating in data browser ([#2422](https://github.com/ParsePlatform/parse-dashboard/issues/2422)) ([e761f97](https://github.com/ParsePlatform/parse-dashboard/commit/e761f9715a8366466c6620994ce698adbbfc48cd)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index cba8221eb6..16c209bedb 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [5.2.0-alpha.7](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.6...5.2.0-alpha.7) (2023-05-27) + + +### Bug Fixes + +* File uploading status not updating in data browser ([#2422](https://github.com/ParsePlatform/parse-dashboard/issues/2422)) ([e761f97](https://github.com/ParsePlatform/parse-dashboard/commit/e761f9715a8366466c6620994ce698adbbfc48cd)) + # [5.2.0-alpha.6](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.5...5.2.0-alpha.6) (2023-05-27) diff --git a/package-lock.json b/package-lock.json index a145b1526c..95baee17c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.6", + "version": "5.2.0-alpha.7", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0c021df051..25295f2348 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.6", + "version": "5.2.0-alpha.7", "repository": { "type": "git", "url": "https://github.com/ParsePlatform/parse-dashboard" From e7ace9efa068b92c5cf0e5ccdad169ac7d71e81a Mon Sep 17 00:00:00 2001 From: Ashish Baravaliya <49753983+AshishBarvaliya@users.noreply.github.com> Date: Sat, 27 May 2023 08:45:16 -0400 Subject: [PATCH 16/30] fix: Cannot navigate to nested relation field in data browser (#2420) --- src/dashboard/Data/Browser/Browser.react.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js index b02f19f77f..7a0f65472a 100644 --- a/src/dashboard/Data/Browser/Browser.react.js +++ b/src/dashboard/Data/Browser/Browser.react.js @@ -857,7 +857,7 @@ class Browser extends DashboardView { getRelationURL() { const relation = this.state.relation; - const className = this.props.params.className; + const className = relation.parent.className; const entityId = relation.parent.id; const relationName = relation.key; return generatePath(this.context, `browser/${className}/${entityId}/${relationName}`); From 1fbcee7ac8aedc09d86598d1d6668dcb5c1cd368 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 27 May 2023 12:47:15 +0000 Subject: [PATCH 17/30] chore(release): 5.2.0-alpha.8 [skip ci] # [5.2.0-alpha.8](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.7...5.2.0-alpha.8) (2023-05-27) ### Bug Fixes * Cannot navigate to nested relation field in data browser ([#2420](https://github.com/ParsePlatform/parse-dashboard/issues/2420)) ([e7ace9e](https://github.com/ParsePlatform/parse-dashboard/commit/e7ace9efa068b92c5cf0e5ccdad169ac7d71e81a)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 16c209bedb..535e981d0b 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [5.2.0-alpha.8](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.7...5.2.0-alpha.8) (2023-05-27) + + +### Bug Fixes + +* Cannot navigate to nested relation field in data browser ([#2420](https://github.com/ParsePlatform/parse-dashboard/issues/2420)) ([e7ace9e](https://github.com/ParsePlatform/parse-dashboard/commit/e7ace9efa068b92c5cf0e5ccdad169ac7d71e81a)) + # [5.2.0-alpha.7](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.6...5.2.0-alpha.7) (2023-05-27) diff --git a/package-lock.json b/package-lock.json index 95baee17c3..d2ced1c441 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.7", + "version": "5.2.0-alpha.8", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 25295f2348..5f3c504665 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.7", + "version": "5.2.0-alpha.8", "repository": { "type": "git", "url": "https://github.com/ParsePlatform/parse-dashboard" From 286269f3e2c4e1c83b14003ce72caaf1f39b16c1 Mon Sep 17 00:00:00 2001 From: TravisHeaver Date: Sat, 27 May 2023 07:57:49 -0500 Subject: [PATCH 18/30] fix: Dashboard crashes when adding a row with modal in a class that contains a pointer to another class that contains an array of pointers (#2416) --- src/components/BrowserCell/BrowserCell.react.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/BrowserCell/BrowserCell.react.js b/src/components/BrowserCell/BrowserCell.react.js index b32e5e7a40..aa134b58c3 100644 --- a/src/components/BrowserCell/BrowserCell.react.js +++ b/src/components/BrowserCell/BrowserCell.react.js @@ -92,7 +92,7 @@ export default class BrowserCell extends Component { this.copyableValue = this.props.value.id; } else if (this.props.type === 'Array') { - if ( this.props.value[0] && typeof this.props.value[0] === 'object' && this.props.value[0].__type === 'Pointer' ) { + if ( this.props.value[0] && typeof this.props.value[0] === 'object' && this.props.value[0].__type === 'Pointer' && typeof this.props.onPointerClick === 'function' ) { const array = []; this.props.value.map( (v, i) => { if ( typeof v !== 'object' || v.__type !== 'Pointer' ) { From 34b7f8320c21e1eacbce0280eabd238b359333dc Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 27 May 2023 13:00:37 +0000 Subject: [PATCH 19/30] chore(release): 5.2.0-alpha.9 [skip ci] # [5.2.0-alpha.9](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.8...5.2.0-alpha.9) (2023-05-27) ### Bug Fixes * Dashboard crashes when adding a row with modal in a class that contains a pointer to another class that contains an array of pointers ([#2416](https://github.com/ParsePlatform/parse-dashboard/issues/2416)) ([286269f](https://github.com/ParsePlatform/parse-dashboard/commit/286269f3e2c4e1c83b14003ce72caaf1f39b16c1)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 535e981d0b..3f828b0f18 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [5.2.0-alpha.9](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.8...5.2.0-alpha.9) (2023-05-27) + + +### Bug Fixes + +* Dashboard crashes when adding a row with modal in a class that contains a pointer to another class that contains an array of pointers ([#2416](https://github.com/ParsePlatform/parse-dashboard/issues/2416)) ([286269f](https://github.com/ParsePlatform/parse-dashboard/commit/286269f3e2c4e1c83b14003ce72caaf1f39b16c1)) + # [5.2.0-alpha.8](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.7...5.2.0-alpha.8) (2023-05-27) diff --git a/package-lock.json b/package-lock.json index d2ced1c441..84f45c3d87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.8", + "version": "5.2.0-alpha.9", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5f3c504665..49d0529ee3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.8", + "version": "5.2.0-alpha.9", "repository": { "type": "git", "url": "https://github.com/ParsePlatform/parse-dashboard" From 66233698b333422f306dc7024949aef2ea028f34 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 1 Jun 2023 23:17:24 +1000 Subject: [PATCH 20/30] fix: Option missing for Parse Config parameter to require master key (#2440) --- src/dashboard/Data/Config/Config.react.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/dashboard/Data/Config/Config.react.js b/src/dashboard/Data/Config/Config.react.js index 4f5a9e2dda..80f83d08d1 100644 --- a/src/dashboard/Data/Config/Config.react.js +++ b/src/dashboard/Data/Config/Config.react.js @@ -58,7 +58,6 @@ class Config extends TableView { } renderExtras() { - const { currentApp = {} } = this.context; let extras = null; if (this.state.modalOpen) { extras = ( @@ -69,7 +68,7 @@ class Config extends TableView { type={this.state.modalType} value={this.state.modalValue} masterKeyOnly={this.state.modalMasterKeyOnly} - parseServerVersion={currentApp.serverInfo && currentApp.serverInfo.parseServerVersion} /> + parseServerVersion={this.context.serverInfo?.parseServerVersion} /> ); } else if (this.state.showDeleteParameterDialog) { extras = ( @@ -129,7 +128,7 @@ class Config extends TableView { } openModal() } - + let openDeleteParameterDialog = () => this.setState({ showDeleteParameterDialog: true, modalParam: data.param From b1542ab183bd1c6275d649da003d9659689e95e5 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 1 Jun 2023 13:19:31 +0000 Subject: [PATCH 21/30] chore(release): 5.2.0-alpha.10 [skip ci] # [5.2.0-alpha.10](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.9...5.2.0-alpha.10) (2023-06-01) ### Bug Fixes * Option missing for Parse Config parameter to require master key ([#2440](https://github.com/ParsePlatform/parse-dashboard/issues/2440)) ([6623369](https://github.com/ParsePlatform/parse-dashboard/commit/66233698b333422f306dc7024949aef2ea028f34)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 3f828b0f18..e963538eab 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [5.2.0-alpha.10](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.9...5.2.0-alpha.10) (2023-06-01) + + +### Bug Fixes + +* Option missing for Parse Config parameter to require master key ([#2440](https://github.com/ParsePlatform/parse-dashboard/issues/2440)) ([6623369](https://github.com/ParsePlatform/parse-dashboard/commit/66233698b333422f306dc7024949aef2ea028f34)) + # [5.2.0-alpha.9](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.8...5.2.0-alpha.9) (2023-05-27) diff --git a/package-lock.json b/package-lock.json index 84f45c3d87..8aefad5f81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.9", + "version": "5.2.0-alpha.10", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 49d0529ee3..d70da4a630 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.9", + "version": "5.2.0-alpha.10", "repository": { "type": "git", "url": "https://github.com/ParsePlatform/parse-dashboard" From 667675c031b0483be210a88da3b159f5f815d6fb Mon Sep 17 00:00:00 2001 From: Ashish Baravaliya <49753983+AshishBarvaliya@users.noreply.github.com> Date: Thu, 8 Jun 2023 08:00:30 -0400 Subject: [PATCH 22/30] fix: Scroll position is preserved when re-opening the same class in data browser via navigation bar (#2445) --- src/dashboard/Data/Browser/BrowserTable.react.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/dashboard/Data/Browser/BrowserTable.react.js b/src/dashboard/Data/Browser/BrowserTable.react.js index dc19d72a54..235241ccd8 100644 --- a/src/dashboard/Data/Browser/BrowserTable.react.js +++ b/src/dashboard/Data/Browser/BrowserTable.react.js @@ -47,6 +47,10 @@ export default class BrowserTable extends React.Component { } else if (this.props.ordering !== props.ordering) { this.setState({ offset: 0 }); this.tableRef.current.scrollTop = 0; + } else if (this.props.filters.size !== props.filters.size) { + this.setState({ offset: 0 }, () => { + this.tableRef.current.scrollTop = 0; + }); } } From 27cdaf194819a2dc5ba0e15b59dd68bc5c9924c5 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 8 Jun 2023 12:03:04 +0000 Subject: [PATCH 23/30] chore(release): 5.2.0-alpha.11 [skip ci] # [5.2.0-alpha.11](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.10...5.2.0-alpha.11) (2023-06-08) ### Bug Fixes * Scroll position is preserved when re-opening the same class in data browser via navigation bar ([#2445](https://github.com/ParsePlatform/parse-dashboard/issues/2445)) ([667675c](https://github.com/ParsePlatform/parse-dashboard/commit/667675c031b0483be210a88da3b159f5f815d6fb)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index e963538eab..21e7c62163 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [5.2.0-alpha.11](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.10...5.2.0-alpha.11) (2023-06-08) + + +### Bug Fixes + +* Scroll position is preserved when re-opening the same class in data browser via navigation bar ([#2445](https://github.com/ParsePlatform/parse-dashboard/issues/2445)) ([667675c](https://github.com/ParsePlatform/parse-dashboard/commit/667675c031b0483be210a88da3b159f5f815d6fb)) + # [5.2.0-alpha.10](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.9...5.2.0-alpha.10) (2023-06-01) diff --git a/package-lock.json b/package-lock.json index 8aefad5f81..255488384c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.10", + "version": "5.2.0-alpha.11", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d70da4a630..f24d342ff2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.10", + "version": "5.2.0-alpha.11", "repository": { "type": "git", "url": "https://github.com/ParsePlatform/parse-dashboard" From a9ec3a915ff354304f382c17e8d5311b2c96d7ff Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 9 Jun 2023 22:07:25 +1000 Subject: [PATCH 24/30] feat: Add filter views to save frequently used filters in data browser (#2404) --- .../Autocomplete/Autocomplete.react.js | 8 +- .../BrowserFilter/BrowserFilter.react.js | 103 +- .../CategoryList/CategoryList.react.js | 115 ++- src/components/CategoryList/CategoryList.scss | 44 +- src/dashboard/Data/Browser/Browser.react.js | 877 ++++++++---------- .../Data/Browser/BrowserToolbar.react.js | 2 + .../Data/Browser/DataBrowser.react.js | 1 + src/lib/ClassPreferences.js | 40 + 8 files changed, 661 insertions(+), 529 deletions(-) create mode 100644 src/lib/ClassPreferences.js diff --git a/src/components/Autocomplete/Autocomplete.react.js b/src/components/Autocomplete/Autocomplete.react.js index be6d4e953f..785f61656b 100644 --- a/src/components/Autocomplete/Autocomplete.react.js +++ b/src/components/Autocomplete/Autocomplete.react.js @@ -235,7 +235,7 @@ export default class Autocomplete extends Component { // Tab // do not type it e.preventDefault(); - + e.stopPropagation(); // move focus to input this.inputRef.current.focus(); @@ -318,7 +318,7 @@ export default class Autocomplete extends Component { onClick={onClick} /> ); - } + } return ( @@ -372,5 +372,5 @@ Autocomplete.propTypes = { ), error: PropTypes.string.describe( 'Error to be rendered in place of label if defined' - ) -} + ) +} diff --git a/src/components/BrowserFilter/BrowserFilter.react.js b/src/components/BrowserFilter/BrowserFilter.react.js index 20446a3654..30e8f7f2b5 100644 --- a/src/components/BrowserFilter/BrowserFilter.react.js +++ b/src/components/BrowserFilter/BrowserFilter.react.js @@ -5,15 +5,18 @@ * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ -import * as Filters from 'lib/Filters'; -import Button from 'components/Button/Button.react'; -import Filter from 'components/Filter/Filter.react'; -import FilterRow from 'components/BrowserFilter/FilterRow.react'; -import Icon from 'components/Icon/Icon.react'; -import Popover from 'components/Popover/Popover.react'; -import Position from 'lib/Position'; -import React from 'react'; -import styles from 'components/BrowserFilter/BrowserFilter.scss'; +import * as Filters from 'lib/Filters'; +import Button from 'components/Button/Button.react'; +import Filter from 'components/Filter/Filter.react'; +import FilterRow from 'components/BrowserFilter/FilterRow.react'; +import Icon from 'components/Icon/Icon.react'; +import Popover from 'components/Popover/Popover.react'; +import Field from 'components/Field/Field.react'; +import TextInput from 'components/TextInput/TextInput.react'; +import Label from 'components/Label/Label.react'; +import Position from 'lib/Position'; +import React from 'react'; +import styles from 'components/BrowserFilter/BrowserFilter.scss'; import { List, Map } from 'immutable'; const POPOVER_CONTENT_ID = 'browserFilterPopover'; @@ -26,7 +29,9 @@ export default class BrowserFilter extends React.Component { open: false, editMode: true, filters: new List(), - blacklistedFilters: Filters.BLACKLISTED_FILTERS.concat(props.blacklistedFilters) + confirmName: false, + name: '', + blacklistedFilters: Filters.BLACKLISTED_FILTERS.concat(props.blacklistedFilters), }; this.toggle = this.toggle.bind(this); this.wrapRef = React.createRef(); @@ -43,13 +48,13 @@ export default class BrowserFilter extends React.Component { if (this.props.filters.size === 0) { let available = Filters.availableFilters(this.props.schema, null, this.state.blacklistedFilters); let field = Object.keys(available)[0]; - filters = new List([ - new Map({ field: field, constraint: available[field][0] }) - ]); + filters = new List([new Map({ field: field, constraint: available[field][0] })]); } - this.setState(prevState => ({ + this.setState((prevState) => ({ open: !prevState.open, filters: filters, + name: '', + confirmName: false, editMode: this.props.filters.size === 0 })); this.props.setCurrent(null); @@ -71,7 +76,7 @@ export default class BrowserFilter extends React.Component { } apply() { - let formatted = this.state.filters.map(filter => { + let formatted = this.state.filters.map((filter) => { // TODO: type is unused? /*let type = this.props.schema[filter.get('field')].type; if (Filters.Constraints[filter.get('constraint')].hasOwnProperty('field')) { @@ -82,13 +87,25 @@ export default class BrowserFilter extends React.Component { // remove compareTo for constraints which are not comparable let isComparable = Filters.Constraints[filter.get('constraint')].comparable; if (!isComparable) { - return filter.delete('compareTo') + return filter.delete('compareTo'); } return filter; }); this.props.onChange(formatted); } + save() { + let formatted = this.state.filters.map((filter) => { + let isComparable = Filters.Constraints[filter.get('constraint')].comparable; + if (!isComparable) { + return filter.delete('compareTo'); + } + return filter; + }); + this.props.onSaveFilter(formatted, this.state.name); + this.toggle(); + } + render() { let popover = null; let buttonStyle = [styles.entry]; @@ -102,49 +119,45 @@ export default class BrowserFilter extends React.Component { if (this.props.filters.size) { popoverStyle.push(styles.active); } - let available = Filters.availableFilters( - this.props.schema, - this.state.filters - ); + let available = Filters.availableFilters(this.props.schema, this.state.filters); popover = (
this.props.setCurrent(null)} id={POPOVER_CONTENT_ID}> -
+
this.setState({ filters: filters })} + onChange={(filters) => this.setState({ filters: filters })} onSearch={this.apply.bind(this)} renderRow={props => ( 0} editMode={this.state.editMode} parentContentId={POPOVER_CONTENT_ID} /> )} /> -
-
+ {this.state.confirmName && } input={ this.setState({ name })} />} />} + {this.state.confirmName && ( +
+
+ )} + {!this.state.confirmName && ( +
+
+ )}
diff --git a/src/components/CategoryList/CategoryList.react.js b/src/components/CategoryList/CategoryList.react.js index 286a040cbc..82afff31c5 100644 --- a/src/components/CategoryList/CategoryList.react.js +++ b/src/components/CategoryList/CategoryList.react.js @@ -5,11 +5,11 @@ * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ -import PropTypes from 'lib/PropTypes'; -import React from 'react'; -import styles from 'components/CategoryList/CategoryList.scss'; -import { Link } from 'react-router-dom'; -import generatePath from 'lib/generatePath'; +import PropTypes from 'lib/PropTypes'; +import React from 'react'; +import styles from 'components/CategoryList/CategoryList.scss'; +import { Link } from 'react-router-dom'; +import generatePath from 'lib/generatePath'; import { CurrentApp } from 'context/currentApp'; export default class CategoryList extends React.Component { @@ -17,6 +17,9 @@ export default class CategoryList extends React.Component { constructor() { super(); this.listWrapperRef = React.createRef(); + this.state = { + openClasses: [], + }; } componentDidMount() { @@ -41,19 +44,50 @@ export default class CategoryList extends React.Component { _updateHighlight() { if (this.highlight) { + let height = 0; for (let i = 0; i < this.props.categories.length; i++) { let c = this.props.categories[i]; let id = c.id || c.name; if (id === this.props.current) { + if (this.state.openClasses.includes(id)) { + const query = new URLSearchParams(this.props.params); + if (query.has('filters')) { + const queryFilter = query.get('filters') + for (let i = 0; i < c.filters?.length; i++) { + const filter = c.filters[i]; + if (queryFilter === filter.filter) { + height += (i + 1) * 20 + break; + } + } + } + } this.highlight.style.display = 'block'; - this.highlight.style.top = (i * 20) + 'px'; + this.highlight.style.top = height + 'px'; return; } + if (this.state.openClasses.includes(id)) { + height = height + (20 * (c.filters.length + 1)) + } else { + height += 20; + } } this.highlight.style.display = 'none'; } } + toggleDropdown(e, id) { + e.preventDefault(); + const openClasses = [...this.state.openClasses]; + const index = openClasses.indexOf(id); + if (openClasses.includes(id)) { + openClasses.splice(index, 1); + } else { + openClasses.push(id); + } + this.setState({ openClasses }); + } + render() { if (this.props.categories.length === 0) { return null; @@ -67,15 +101,68 @@ export default class CategoryList extends React.Component { } let count = c.count; let className = id === this.props.current ? styles.active : ''; - let link = generatePath( - this.context, - (this.props.linkPrefix || '') + (c.link || id) - ); + let selectedFilter = null; + if (this.state.openClasses.includes(id)) { + const query = new URLSearchParams(this.props.params); + if (query.has('filters')) { + const queryFilter = query.get('filters') + for (let i = 0; i < c.filters?.length; i++) { + const filter = c.filters[i]; + if (queryFilter === filter.filter) { + selectedFilter = i; + className = ''; + break; + } + } + } + } + let link = generatePath(this.context, (this.props.linkPrefix || '') + (c.link || id)); return ( - - {count} - {c.name} - +
+ + {this.state.openClasses.includes(id) && + c.filters.map((filterData, index) => { + const { name, filter } = filterData; + const url = `${this.props.linkPrefix}${c.name}?filters=${encodeURIComponent(filter)}`; + return ( +
+ { + e.preventDefault(); + this.props.filterClicked(url); + }} + key={name + index} + > + {name} + + { + e.preventDefault(); + this.props.removeFilter(filterData); + }} + > + × + +
+ ); + })} +
); })}
diff --git a/src/components/CategoryList/CategoryList.scss b/src/components/CategoryList/CategoryList.scss index f24bd69580..867f4fa04f 100644 --- a/src/components/CategoryList/CategoryList.scss +++ b/src/components/CategoryList/CategoryList.scss @@ -37,7 +37,6 @@ width: 50px; text-align: right; } - &:last-of-type { margin-right: 50px; overflow: hidden; @@ -62,3 +61,46 @@ border: 0; height: 1px; } + +.close { + font-size: 20px !important; + font-weight: bold !important; +} +.expand { + display: flex !important; + align-items: center; + cursor: pointer; + width: 0px; + margin-right: 20px; + padding-left: 0px !important; + &:after { + @include arrow('down', 10px, 7px, #8fb9cf); + content: ''; + margin-left: 10px; + } +} +.link { + display: flex; + a { + &:first-of-type { + flex-grow: 1 + } + } +} + +.childLink { + display: flex; + a { + &:first-of-type { + flex-grow: 1; + display: flex; + } + span { + text-align: left !important; + margin-left: 14px; + display: flex; + flex-grow: 1; + margin-right: 0px !important; + } + } +} diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js index 7a0f65472a..09bacbca65 100644 --- a/src/dashboard/Data/Browser/Browser.react.js +++ b/src/dashboard/Data/Browser/Browser.react.js @@ -5,37 +5,38 @@ * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ -import { ActionTypes } from 'lib/stores/SchemaStore'; -import AddColumnDialog from 'dashboard/Data/Browser/AddColumnDialog.react'; -import CategoryList from 'components/CategoryList/CategoryList.react'; -import CreateClassDialog from 'dashboard/Data/Browser/CreateClassDialog.react'; -import DashboardView from 'dashboard/DashboardView.react'; -import DataBrowser from 'dashboard/Data/Browser/DataBrowser.react'; +import { ActionTypes } from 'lib/stores/SchemaStore'; +import AddColumnDialog from 'dashboard/Data/Browser/AddColumnDialog.react'; +import CategoryList from 'components/CategoryList/CategoryList.react'; +import CreateClassDialog from 'dashboard/Data/Browser/CreateClassDialog.react'; +import DashboardView from 'dashboard/DashboardView.react'; +import DataBrowser from 'dashboard/Data/Browser/DataBrowser.react'; import { DefaultColumns, SpecialClasses } from 'lib/Constants'; -import DeleteRowsDialog from 'dashboard/Data/Browser/DeleteRowsDialog.react'; -import DropClassDialog from 'dashboard/Data/Browser/DropClassDialog.react'; -import EmptyState from 'components/EmptyState/EmptyState.react'; -import ExportDialog from 'dashboard/Data/Browser/ExportDialog.react'; -import AttachRowsDialog from 'dashboard/Data/Browser/AttachRowsDialog.react'; -import AttachSelectedRowsDialog from 'dashboard/Data/Browser/AttachSelectedRowsDialog.react'; -import CloneSelectedRowsDialog from 'dashboard/Data/Browser/CloneSelectedRowsDialog.react'; -import EditRowDialog from 'dashboard/Data/Browser/EditRowDialog.react'; -import ExportSelectedRowsDialog from 'dashboard/Data/Browser/ExportSelectedRowsDialog.react'; -import ExportSchemaDialog from 'dashboard/Data/Browser/ExportSchemaDialog.react'; -import { List, Map } from 'immutable'; -import Notification from 'dashboard/Data/Browser/Notification.react'; -import Parse from 'parse'; -import prettyNumber from 'lib/prettyNumber'; -import queryFromFilters from 'lib/queryFromFilters'; -import React from 'react'; -import RemoveColumnDialog from 'dashboard/Data/Browser/RemoveColumnDialog.react'; -import PointerKeyDialog from 'dashboard/Data/Browser/PointerKeyDialog.react'; -import SidebarAction from 'components/Sidebar/SidebarAction'; -import stringCompare from 'lib/stringCompare'; -import styles from 'dashboard/Data/Browser/Browser.scss'; -import subscribeTo from 'lib/subscribeTo'; -import * as ColumnPreferences from 'lib/ColumnPreferences'; -import { Helmet } from 'react-helmet'; +import DeleteRowsDialog from 'dashboard/Data/Browser/DeleteRowsDialog.react'; +import DropClassDialog from 'dashboard/Data/Browser/DropClassDialog.react'; +import EmptyState from 'components/EmptyState/EmptyState.react'; +import ExportDialog from 'dashboard/Data/Browser/ExportDialog.react'; +import AttachRowsDialog from 'dashboard/Data/Browser/AttachRowsDialog.react'; +import AttachSelectedRowsDialog from 'dashboard/Data/Browser/AttachSelectedRowsDialog.react'; +import CloneSelectedRowsDialog from 'dashboard/Data/Browser/CloneSelectedRowsDialog.react'; +import EditRowDialog from 'dashboard/Data/Browser/EditRowDialog.react'; +import ExportSelectedRowsDialog from 'dashboard/Data/Browser/ExportSelectedRowsDialog.react'; +import ExportSchemaDialog from 'dashboard/Data/Browser/ExportSchemaDialog.react'; +import { List, Map } from 'immutable'; +import Notification from 'dashboard/Data/Browser/Notification.react'; +import Parse from 'parse'; +import prettyNumber from 'lib/prettyNumber'; +import queryFromFilters from 'lib/queryFromFilters'; +import React from 'react'; +import RemoveColumnDialog from 'dashboard/Data/Browser/RemoveColumnDialog.react'; +import PointerKeyDialog from 'dashboard/Data/Browser/PointerKeyDialog.react'; +import SidebarAction from 'components/Sidebar/SidebarAction'; +import stringCompare from 'lib/stringCompare'; +import styles from 'dashboard/Data/Browser/Browser.scss'; +import subscribeTo from 'lib/subscribeTo'; +import * as ColumnPreferences from 'lib/ColumnPreferences'; +import * as ClassPreferences from 'lib/ClassPreferences'; +import { Helmet } from 'react-helmet'; import generatePath from 'lib/generatePath'; import { withRouter } from 'lib/withRouter'; @@ -158,8 +159,8 @@ class Browser extends DashboardView { window.addEventListener('popstate', () => { this.setState({ - relation: null - }) + relation: null, + }); }); } @@ -169,8 +170,7 @@ class Browser extends DashboardView { this.action = new SidebarAction('Create a class', this.showCreateClass.bind(this)); } - this.props.schema.dispatch(ActionTypes.FETCH) - .then(() => this.handleFetchedSchema()); + this.props.schema.dispatch(ActionTypes.FETCH).then(() => this.handleFetchedSchema()); if (!this.props.params.className && this.props.schema.data.get('classes')) { this.redirectToFirstClass(this.props.schema.data.get('classes')); } else if (this.props.params.className) { @@ -184,8 +184,7 @@ class Browser extends DashboardView { this.setState({ counts: {} }); Parse.Object._clearAllState(); - nextProps.schema.dispatch(ActionTypes.FETCH) - .then(() => this.handleFetchedSchema()); + nextProps.schema.dispatch(ActionTypes.FETCH).then(() => this.handleFetchedSchema()); } this.prefetchData(nextProps, nextContext); } @@ -205,24 +204,23 @@ class Browser extends DashboardView { const parent = await parentObjectQuery.get(entityId, { useMasterKey }); relation = parent.relation(relationName); } - this.setState({ - data: null, - newObject: null, - lastMax: -1, - ordering: ColumnPreferences.getColumnSort( - false, - context.applicationId, - className, - ), - selection: {}, - relation: isRelationRoute ? relation : null, - }, () => { - if (isRelationRoute) { - this.fetchRelation(relation, filters); - } else if (className) { - this.fetchData(className, filters); + this.setState( + { + data: null, + newObject: null, + lastMax: -1, + ordering: ColumnPreferences.getColumnSort(false, context.applicationId, className), + selection: {}, + relation: isRelationRoute ? relation : null, + }, + () => { + if (isRelationRoute) { + this.fetchRelation(relation, filters); + } else if (className) { + this.fetchData(className, filters); + } } - }); + ); } extractFiltersFromQuery(props) { @@ -234,7 +232,7 @@ class Browser extends DashboardView { const query = new URLSearchParams(props.location.search); if (query.has('filters')) { const queryFilters = JSON.parse(query.get('filters')); - queryFilters.forEach((filter) => filters = filters.push(new Map(filter))); + queryFilters.forEach((filter) => (filters = filters.push(new Map(filter)))); } return filters; } @@ -302,27 +300,33 @@ class Browser extends DashboardView { } createClass(className) { - this.props.schema.dispatch(ActionTypes.CREATE_CLASS, { className }).then(() => { - this.state.counts[className] = 0; - this.props.navigate(generatePath(this.context, 'browser/' + className)); - }).finally(() => { - this.setState({ showCreateClassDialog: false }); - }); + this.props.schema + .dispatch(ActionTypes.CREATE_CLASS, { className }) + .then(() => { + this.state.counts[className] = 0; + this.props.navigate(generatePath(this.context, 'browser/' + className)); + }) + .finally(() => { + this.setState({ showCreateClassDialog: false }); + }); } dropClass(className) { - this.props.schema.dispatch(ActionTypes.DROP_CLASS, { className }).then(() => { - this.setState({showDropClassDialog: false }); - delete this.state.counts[className]; - this.props.navigate(generatePath(this.context, 'browser')); - }, (error) => { - let msg = typeof error === 'string' ? error : error.message; - if (msg) { - msg = msg[0].toUpperCase() + msg.substr(1); - } + this.props.schema.dispatch(ActionTypes.DROP_CLASS, { className }).then( + () => { + this.setState({ showDropClassDialog: false }); + delete this.state.counts[className]; + this.props.navigate(generatePath(this.context, 'browser')); + }, + (error) => { + let msg = typeof error === 'string' ? error : error.message; + if (msg) { + msg = msg[0].toUpperCase() + msg.substr(1); + } - this.showNote(msg, true); - }); + this.showNote(msg, true); + } + ); } exportClass(className) { @@ -342,16 +346,9 @@ class Browser extends DashboardView { schema = await new Parse.Schema(className).get(); } const element = document.createElement('a'); - const file = new Blob( - [ - JSON.stringify( - schema, - null, - 2, - ), - ], - { type: 'application/json' } - ); + const file = new Blob([JSON.stringify(schema, null, 2)], { + type: 'application/json', + }); element.href = URL.createObjectURL(file); element.download = `${all ? 'schema' : className}.json`; document.body.appendChild(element); // Required for this to work in FireFox @@ -363,12 +360,13 @@ class Browser extends DashboardView { } newColumn(payload, required) { - return this.props.schema.dispatch(ActionTypes.ADD_COLUMN, payload) + return this.props.schema + .dispatch(ActionTypes.ADD_COLUMN, payload) .then(() => { if (required) { let requiredCols = [...this.state.requiredColumnFields, name]; this.setState({ - requiredColumnFields: requiredCols + requiredColumnFields: requiredCols, }); } }) @@ -384,7 +382,7 @@ class Browser extends DashboardView { name: name, targetClass: target, required, - defaultValue + defaultValue, }; this.newColumn(payload, required).finally(() => { this.setState({ showAddColumnDialog: false, keepAddingCols: false }); @@ -398,7 +396,7 @@ class Browser extends DashboardView { name: name, targetClass: target, required, - defaultValue + defaultValue, }; this.newColumn(payload, required).finally(() => { this.setState({ showAddColumnDialog: false, keepAddingCols: false }); @@ -410,27 +408,25 @@ class Browser extends DashboardView { if (!this.state.newObject) { const relation = this.state.relation; this.setState({ - newObject: (relation ? - new Parse.Object(relation.targetClassName) - : new Parse.Object(this.props.params.className) ), + newObject: relation ? new Parse.Object(relation.targetClassName) : new Parse.Object(this.props.params.className), }); } } - abortAddRow(){ - if(this.state.newObject){ + abortAddRow() { + if (this.state.newObject) { this.setState({ - newObject: null + newObject: null, }); } if (this.state.markRequiredFieldRow !== 0) { this.setState({ - markRequiredFieldRow: 0 + markRequiredFieldRow: 0, }); } } - saveNewRow(){ + saveNewRow() { const { useMasterKey } = this.state; const obj = this.state.newObject; if (!obj) { @@ -443,21 +439,21 @@ class Browser extends DashboardView { if (className) { let classColumns = this.props.schema.data.get('classes').get(className); classColumns.forEach(({ required }, name) => { - if (name === 'objectId' || this.state.isUnique && name !== this.state.uniqueField) { - return; - } - if (required) { - requiredCols.push(name); - } - if (className === '_User' && (name === 'username' || name === 'password')) { - if (!obj.get('authData')) { - requiredCols.push(name); - } - } - if (className === '_Role' && (name === 'name' || name === 'ACL')) { + if (name === 'objectId' || (this.state.isUnique && name !== this.state.uniqueField)) { + return; + } + if (required) { + requiredCols.push(name); + } + if (className === '_User' && (name === 'username' || name === 'password')) { + if (!obj.get('authData')) { requiredCols.push(name); } - }); + } + if (className === '_Role' && (name === 'name' || name === 'ACL')) { + requiredCols.push(name); + } + }); } if (requiredCols.length) { for (let idx = 0; idx < requiredCols.length; idx++) { @@ -465,7 +461,7 @@ class Browser extends DashboardView { if (obj.get(name) == null) { this.showNote('Please enter all required fields', true); this.setState({ - markRequiredFieldRow: -1 + markRequiredFieldRow: -1, }); return; } @@ -473,11 +469,11 @@ class Browser extends DashboardView { } if (this.state.markRequiredFieldRow) { this.setState({ - markRequiredFieldRow: 0 + markRequiredFieldRow: 0, }); } obj.save(null, { useMasterKey }).then( - objectSaved => { + (objectSaved) => { let msg = objectSaved.className + ' with id \'' + objectSaved.id + '\' created'; this.showNote(msg, false); @@ -496,11 +492,11 @@ class Browser extends DashboardView { relationCount: this.state.relationCount + 1, counts: { ...this.state.counts, - [targetClassName]: this.state.counts[targetClassName] + 1 - } + [targetClassName]: this.state.counts[targetClassName] + 1, + }, }); }, - error => { + (error) => { let msg = typeof error === 'string' ? error : error.message; if (msg) { msg = msg[0].toUpperCase() + msg.substr(1); @@ -520,7 +516,7 @@ class Browser extends DashboardView { this.setState(state); }, - error => { + (error) => { let msg = typeof error === 'string' ? error : error.message; if (msg) { msg = msg[0].toUpperCase() + msg.substr(1); @@ -533,9 +529,7 @@ class Browser extends DashboardView { saveEditCloneRow(rowIndex) { let obj; if (rowIndex < -1) { - obj = this.state.editCloneRows[ - rowIndex + (this.state.editCloneRows.length + 1) - ]; + obj = this.state.editCloneRows[rowIndex + (this.state.editCloneRows.length + 1)]; } if (!obj) { return; @@ -547,21 +541,21 @@ class Browser extends DashboardView { if (className) { let classColumns = this.props.schema.data.get('classes').get(className); classColumns.forEach(({ required }, name) => { - if (name === 'objectId' || this.state.isUnique && name !== this.state.uniqueField) { - return; - } - if (required) { - requiredCols.push(name); - } - if (className === '_User' && (name === 'username' || name === 'password')) { - if (!obj.get('authData')) { - requiredCols.push(name); - } - } - if (className === '_Role' && (name === 'name' || name === 'ACL')) { + if (name === 'objectId' || (this.state.isUnique && name !== this.state.uniqueField)) { + return; + } + if (required) { + requiredCols.push(name); + } + if (className === '_User' && (name === 'username' || name === 'password')) { + if (!obj.get('authData')) { requiredCols.push(name); } - }); + } + if (className === '_Role' && (name === 'name' || name === 'ACL')) { + requiredCols.push(name); + } + }); } if (requiredCols.length) { for (let idx = 0; idx < requiredCols.length; idx++) { @@ -569,7 +563,7 @@ class Browser extends DashboardView { if (obj.get(name) == null) { this.showNote('Please enter all required fields', true); this.setState({ - markRequiredFieldRow: rowIndex + markRequiredFieldRow: rowIndex, }); return; } @@ -577,48 +571,48 @@ class Browser extends DashboardView { } if (this.state.markRequiredFieldRow) { this.setState({ - markRequiredFieldRow: 0 + markRequiredFieldRow: 0, }); } - obj.save(null, { useMasterKey: true }).then((objectSaved) => { - let msg = objectSaved.className + ' with id \'' + objectSaved.id + '\' ' + 'created'; - this.showNote(msg, false); + obj.save(null, { useMasterKey: true }).then( + (objectSaved) => { + let msg = objectSaved.className + ' with id \'' + objectSaved.id + '\' ' + 'created'; + this.showNote(msg, false); - const state = { data: this.state.data, editCloneRows: this.state.editCloneRows }; - state.editCloneRows = state.editCloneRows.filter( - cloneObj => cloneObj._localId !== obj._localId - ); - if (state.editCloneRows.length === 0) state.editCloneRows = null; - if (this.props.params.className === obj.className) { - this.state.data.unshift(obj); - } - this.state.counts[obj.className] += 1; - this.setState(state); - }, (error) => { - let msg = typeof error === 'string' ? error : error.message; - if (msg) { - msg = msg[0].toUpperCase() + msg.substr(1); - } + const state = { + data: this.state.data, + editCloneRows: this.state.editCloneRows, + }; + state.editCloneRows = state.editCloneRows.filter((cloneObj) => cloneObj._localId !== obj._localId); + if (state.editCloneRows.length === 0) state.editCloneRows = null; + if (this.props.params.className === obj.className) { + this.state.data.unshift(obj); + } + this.state.counts[obj.className] += 1; + this.setState(state); + }, + (error) => { + let msg = typeof error === 'string' ? error : error.message; + if (msg) { + msg = msg[0].toUpperCase() + msg.substr(1); + } - this.showNote(msg, true); - }); + this.showNote(msg, true); + } + ); } abortEditCloneRow(rowIndex) { let obj; if (rowIndex < -1) { - obj = this.state.editCloneRows[ - rowIndex + (this.state.editCloneRows.length + 1) - ]; + obj = this.state.editCloneRows[rowIndex + (this.state.editCloneRows.length + 1)]; } if (!obj) { return; } const state = { editCloneRows: this.state.editCloneRows }; - state.editCloneRows = state.editCloneRows.filter( - cloneObj => cloneObj._localId !== obj._localId - ); + state.editCloneRows = state.editCloneRows.filter((cloneObj) => cloneObj._localId !== obj._localId); if (state.editCloneRows.length === 0) state.editCloneRows = null; this.setState(state); } @@ -631,20 +625,20 @@ class Browser extends DashboardView { cancelPendingEditRows() { this.setState({ - editCloneRows: null + editCloneRows: null, }); } addEditCloneRows(cloneRows) { this.setState({ - editCloneRows: cloneRows + editCloneRows: cloneRows, }); } removeColumn(name) { let payload = { className: this.props.params.className, - name: name + name: name, }; this.props.schema.dispatch(ActionTypes.DROP_COLUMN, payload).finally(() => { let state = { showRemoveColumnDialog: false }; @@ -667,7 +661,7 @@ class Browser extends DashboardView { this.setState({ clp: this.props.schema.data.get('CLPs').toJS(), counts, - computingClassCounts: false + computingClassCounts: false, }); } } @@ -698,12 +692,12 @@ class Browser extends DashboardView { const { useMasterKey } = this.state; const query = queryFromFilters(source, filters); const sortDir = this.state.ordering[0] === '-' ? '-' : '+'; - const field = this.state.ordering.substr(sortDir === '-' ? 1 : 0) + const field = this.state.ordering.substr(sortDir === '-' ? 1 : 0); if (sortDir === '-') { - query.descending(field) + query.descending(field); } else { - query.ascending(field) + query.ascending(field); } query.limit(MAX_ROWS_FETCHED); @@ -729,7 +723,7 @@ class Browser extends DashboardView { excludeFields(query, className) { let columns = ColumnPreferences.getPreferences(this.context.applicationId, className); if (columns) { - columns = columns.filter(clmn => !clmn.visible).map(clmn => clmn.name); + columns = columns.filter((clmn) => !clmn.visible).map((clmn) => clmn.name); for (let columnsKey in columns) { query.exclude(columns[columnsKey]); } @@ -756,7 +750,12 @@ class Browser extends DashboardView { } else { delete filteredCounts[source]; } - this.setState({ data: data, filters, lastMax: MAX_ROWS_FETCHED , filteredCounts: filteredCounts}); + this.setState({ + data: data, + filters, + lastMax: MAX_ROWS_FETCHED, + filteredCounts: filteredCounts, + }); } async fetchRelation(relation, filters = new List()) { @@ -822,7 +821,7 @@ class Browser extends DashboardView { query.find({ useMasterKey }).then((nextPage) => { if (className === this.props.params.className) { this.setState((state) => ({ - data: state.data.concat(nextPage) + data: state.data.concat(nextPage), })); } }); @@ -836,23 +835,48 @@ class Browser extends DashboardView { } else { const source = this.props.params.className; const _filters = JSON.stringify(filters.toJSON()); - const url = `browser/${source}${(filters.size === 0 ? '' : `?filters=${(encodeURIComponent(_filters))}`)}`; + const url = `browser/${source}${filters.size === 0 ? '' : `?filters=${encodeURIComponent(_filters)}`}`; // filters param change is making the fetch call this.props.navigate(generatePath(this.context, url)); } } + saveFilters(filters, name) { + const _filters = JSON.stringify(filters.toJSON()); + const preferences = ClassPreferences.getPreferences(this.context.applicationId, this.props.params.className); + if (!preferences.filters.includes(_filters)) { + preferences.filters.push({ + name, + filter: _filters, + }); + } + ClassPreferences.updatePreferences(preferences, this.context.applicationId, this.props.params.className); + super.forceUpdate(); + } + + removeFilter(filter) { + const preferences = ClassPreferences.getPreferences(this.context.applicationId, this.props.params.className); + let i = preferences.filters.length; + while (i--) { + const item = preferences.filters[i]; + if (JSON.stringify(item) === JSON.stringify(filter)) { + preferences.filters.splice(i, 1); + } + } + ClassPreferences.updatePreferences(preferences, this.context.applicationId, this.props.params.className); + super.forceUpdate(); + } + updateOrdering(ordering) { let source = this.state.relation || this.props.params.className; - this.setState({ - ordering: ordering, - selection: {} - }, () => this.fetchData(source, this.state.filters)); - ColumnPreferences.getColumnSort( - ordering, - this.context.applicationId, - this.props.params.className + this.setState( + { + ordering: ordering, + selection: {}, + }, + () => this.fetchData(source, this.state.filters) ); + ColumnPreferences.getColumnSort(ordering, this.context.applicationId, this.props.params.className); } getRelationURL() { @@ -864,36 +888,43 @@ class Browser extends DashboardView { } setRelation(relation, filters) { - this.setState({ - relation: relation, - data: null, - }, () => { - let filterQueryString; - if (filters && filters.size) { - filterQueryString = encodeURIComponent(JSON.stringify(filters.toJSON())); + this.setState( + { + relation: relation, + data: null, + }, + () => { + let filterQueryString; + if (filters && filters.size) { + filterQueryString = encodeURIComponent(JSON.stringify(filters.toJSON())); + } + const url = `${this.getRelationURL()}${filterQueryString ? `?filters=${filterQueryString}` : ''}`; + this.props.navigate(url); } - const url = `${this.getRelationURL()}${filterQueryString ? `?filters=${filterQueryString}` : ''}`; - this.props.navigate(url); - }); + ); this.fetchRelation(relation, filters); } handlePointerClick({ className, id, field = 'objectId' }) { - let filters = JSON.stringify([{ + let filters = JSON.stringify([ + { field, constraint: 'eq', - compareTo: id - }]); + compareTo: id, + }, + ]); this.props.navigate(generatePath(this.context, `browser/${className}?filters=${encodeURIComponent(filters)}`)); } handlePointerCmdClick({ className, id, field = 'objectId' }) { - let filters = JSON.stringify([{ - field, - constraint: 'eq', - compareTo: id - }]); - window.open(generatePath(this.context, `browser/${className}?filters=${encodeURIComponent(filters)}`),'_blank'); + let filters = JSON.stringify([ + { + field, + constraint: 'eq', + compareTo: id, + }, + ]); + window.open(generatePath(this.context, `browser/${className}?filters=${encodeURIComponent(filters)}`), '_blank'); } handleCLPChange(clp) { @@ -909,7 +940,7 @@ class Browser extends DashboardView { let isNewObject = row === -1; let isEditCloneObj = row < -1; let obj = isNewObject ? this.state.newObject : this.state.data[row]; - if(isEditCloneObj){ + if (isEditCloneObj) { obj = this.state.editCloneRows[row + (this.state.editCloneRows.length + 1)]; } if (!obj) { @@ -927,7 +958,7 @@ class Browser extends DashboardView { if (isNewObject) { this.setState({ - isNewObject: obj + isNewObject: obj, }); return; } @@ -936,78 +967,82 @@ class Browser extends DashboardView { let cloneRows = [...this.state.editCloneRows]; cloneRows.splice(editObjIndex, 1, obj); this.setState({ - editCloneRows: cloneRows + editCloneRows: cloneRows, }); return; } const { useMasterKey } = this.state; - obj.save(null, { useMasterKey }).then((objectSaved) => { - let msg = objectSaved.className + ' with id \'' + objectSaved.id + '\' updated'; - this.showNote(msg, false); + obj.save(null, { useMasterKey }).then( + (objectSaved) => { + let msg = objectSaved.className + ' with id \'' + objectSaved.id + '\' updated'; + this.showNote(msg, false); - const state = { data: this.state.data, editCloneRows: this.state.editCloneRows }; + const state = { + data: this.state.data, + editCloneRows: this.state.editCloneRows, + }; - if (isNewObject) { - const relation = this.state.relation; - if (relation) { - const parent = relation.parent; - const parentRelation = parent.relation(relation.key); - parentRelation.add(obj); - const targetClassName = relation.targetClassName; - parent.save(null, { useMasterKey: true }).then(() => { - this.setState({ - newObject: null, - data: [ - obj, - ...this.state.data, - ], - relationCount: this.state.relationCount + 1, - counts: { - ...this.state.counts, - [targetClassName]: this.state.counts[targetClassName] + 1, + if (isNewObject) { + const relation = this.state.relation; + if (relation) { + const parent = relation.parent; + const parentRelation = parent.relation(relation.key); + parentRelation.add(obj); + const targetClassName = relation.targetClassName; + parent.save(null, { useMasterKey: true }).then( + () => { + this.setState({ + newObject: null, + data: [obj, ...this.state.data], + relationCount: this.state.relationCount + 1, + counts: { + ...this.state.counts, + [targetClassName]: this.state.counts[targetClassName] + 1, + }, + }); }, - }); - }, (error) => { - let msg = typeof error === 'string' ? error : error.message; - if (msg) { - msg = msg[0].toUpperCase() + msg.substr(1); + (error) => { + let msg = typeof error === 'string' ? error : error.message; + if (msg) { + msg = msg[0].toUpperCase() + msg.substr(1); + } + obj.set(attr, prev); + this.setState({ data: this.state.data }); + this.showNote(msg, true); + } + ); + } else { + state.newObject = null; + if (this.props.params.className === obj.className) { + this.state.data.unshift(obj); } - obj.set(attr, prev); - this.setState({ data: this.state.data }); - this.showNote(msg, true); - }); - } else { - state.newObject = null; + this.state.counts[obj.className] += 1; + } + } + if (isEditCloneObj) { + state.editCloneRows = state.editCloneRows.filter((cloneObj) => cloneObj._localId !== obj._localId); + if (state.editCloneRows.length === 0) state.editCloneRows = null; if (this.props.params.className === obj.className) { this.state.data.unshift(obj); } this.state.counts[obj.className] += 1; } - } - if (isEditCloneObj) { - state.editCloneRows = state.editCloneRows.filter( - cloneObj => cloneObj._localId !== obj._localId - ); - if (state.editCloneRows.length === 0) state.editCloneRows = null; - if (this.props.params.className === obj.className) { - this.state.data.unshift(obj); + this.setState(state); + }, + (error) => { + let msg = typeof error === 'string' ? error : error.message; + if (msg) { + msg = msg[0].toUpperCase() + msg.substr(1); + } + if (!isNewObject && !isEditCloneObj) { + obj.set(attr, prev); + this.setState({ data: this.state.data }); } - this.state.counts[obj.className] += 1; - } - this.setState(state); - }, (error) => { - let msg = typeof error === 'string' ? error : error.message; - if (msg) { - msg = msg[0].toUpperCase() + msg.substr(1); - } - if (!isNewObject && !isEditCloneObj) { - obj.set(attr, prev); - this.setState({ data: this.state.data }); - } - this.showNote(msg, true); - }); + this.showNote(msg, true); + } + ); } deleteRows(rows) { @@ -1040,7 +1075,9 @@ class Browser extends DashboardView { } const toDeleteObjectIds = []; - toDelete.forEach((obj) => { toDeleteObjectIds.push(obj.id); }); + toDelete.forEach((obj) => { + toDeleteObjectIds.push(obj.id); + }); const { useMasterKey } = this.state; let relation = this.state.relation; @@ -1057,52 +1094,55 @@ class Browser extends DashboardView { } }); } else if (toDelete.length) { - Parse.Object.destroyAll(toDelete, { useMasterKey }).then(() => { - let deletedNote; + Parse.Object.destroyAll(toDelete, { useMasterKey }).then( + () => { + let deletedNote; - if (toDeleteObjectIds.length == 1) { - deletedNote = className + ' with id \'' + toDeleteObjectIds[0] + '\' deleted'; - } else { - deletedNote = toDeleteObjectIds.length + ' ' + className + ' objects deleted'; - } + if (toDeleteObjectIds.length == 1) { + deletedNote = className + ' with id \'' + toDeleteObjectIds[0] + '\' deleted'; + } else { + deletedNote = toDeleteObjectIds.length + ' ' + className + ' objects deleted'; + } - this.showNote(deletedNote, false); + this.showNote(deletedNote, false); - if (this.props.params.className === className) { - for (let i = 0; i < indexes.length; i++) { - this.state.data.splice(indexes[i] - i, 1); - } - this.state.counts[className] -= indexes.length; + if (this.props.params.className === className) { + for (let i = 0; i < indexes.length; i++) { + this.state.data.splice(indexes[i] - i, 1); + } + this.state.counts[className] -= indexes.length; - // If after deletion, the remaining elements on the table is lesser than the maximum allowed elements - // we fetch more data to fill the table - if (this.state.data.length < MAX_ROWS_FETCHED) { - this.prefetchData(this.props, this.context); - } else { - this.forceUpdate(); - } - } - }, (error) => { - let errorDeletingNote = null; - - if (error.code === Parse.Error.AGGREGATE_ERROR) { - if (error.errors.length == 1) { - errorDeletingNote = 'Error deleting ' + className + ' with id \'' + error.errors[0].object.id + '\''; - } else if (error.errors.length < toDeleteObjectIds.length) { - errorDeletingNote = 'Error deleting ' + error.errors.length + ' out of ' + toDeleteObjectIds.length + ' ' + className + ' objects'; - } else { - errorDeletingNote = 'Error deleting all ' + error.errors.length + ' ' + className + ' objects'; + // If after deletion, the remaining elements on the table is lesser than the maximum allowed elements + // we fetch more data to fill the table + if (this.state.data.length < MAX_ROWS_FETCHED) { + this.prefetchData(this.props, this.context); + } else { + this.forceUpdate(); + } } - } else { - if (toDeleteObjectIds.length == 1) { - errorDeletingNote = 'Error deleting ' + className + ' with id \'' + toDeleteObjectIds[0] + '\''; + }, + (error) => { + let errorDeletingNote = null; + + if (error.code === Parse.Error.AGGREGATE_ERROR) { + if (error.errors.length == 1) { + errorDeletingNote = 'Error deleting ' + className + ' with id \'' + error.errors[0].object.id + '\''; + } else if (error.errors.length < toDeleteObjectIds.length) { + errorDeletingNote = 'Error deleting ' + error.errors.length + ' out of ' + toDeleteObjectIds.length + ' ' + className + ' objects'; + } else { + errorDeletingNote = 'Error deleting all ' + error.errors.length + ' ' + className + ' objects'; + } } else { - errorDeletingNote = 'Error deleting ' + toDeleteObjectIds.length + ' ' + className + ' objects'; + if (toDeleteObjectIds.length == 1) { + errorDeletingNote = 'Error deleting ' + className + ' with id \'' + toDeleteObjectIds[0] + '\''; + } else { + errorDeletingNote = 'Error deleting ' + toDeleteObjectIds.length + ' ' + className + ' objects'; + } } - } - this.showNote(errorDeletingNote, true); - }); + this.showNote(errorDeletingNote, true); + } + ); } } } @@ -1162,7 +1202,7 @@ class Browser extends DashboardView { if (missedObjectsCount) { const missedObjects = []; objectIds.forEach((objectId) => { - const object = objects.find(x => x.id === objectId); + const object = objects.find((x) => x.id === objectId); if (!object) { missedObjects.push(objectId); } @@ -1173,12 +1213,9 @@ class Browser extends DashboardView { parent.relation(relation.key).add(objects); await parent.save(null, { useMasterKey }); // remove duplication - this.state.data.forEach(origin => objects = objects.filter(object => object.id !== origin.id)); + this.state.data.forEach((origin) => (objects = objects.filter((object) => object.id !== origin.id))); this.setState({ - data: [ - ...objects, - ...this.state.data, - ], + data: [...objects, ...this.state.data], relationCount: this.state.relationCount + objects.length, showAttachRowsDialog: false, }); @@ -1249,32 +1286,30 @@ class Browser extends DashboardView { showCloneSelectedRowsDialog: false, counts: { ...this.state.counts, - [className]: this.state.counts[className] + toClone.length - } + [className]: this.state.counts[className] + toClone.length, + }, }); } catch (error) { //for duplicate, username missing or required field missing errors if (error.code === 137 || error.code === 200 || error.code === 142) { let failedSaveObj = []; let savedObjects = []; - toClone.forEach(cloneObj => { - cloneObj.dirty() - ? failedSaveObj.push(cloneObj) - : savedObjects.push(cloneObj); + toClone.forEach((cloneObj) => { + cloneObj.dirty() ? failedSaveObj.push(cloneObj) : savedObjects.push(cloneObj); }); if (savedObjects.length) { this.setState({ data: [...savedObjects, ...this.state.data], counts: { ...this.state.counts, - [className]: this.state.counts[className] + savedObjects.length - } + [className]: this.state.counts[className] + savedObjects.length, + }, }); } this.addEditCloneRows(failedSaveObj); } this.setState({ - showCloneSelectedRowsDialog: false + showCloneSelectedRowsDialog: false, }); this.showNote(error.message, true); } @@ -1282,19 +1317,19 @@ class Browser extends DashboardView { showExportSelectedRowsDialog(rows) { this.setState({ - rowsToExport: rows + rowsToExport: rows, }); } showExportSchemaDialog() { this.setState({ - showExportSchemaDialog: true - }) + showExportSchemaDialog: true, + }); } cancelExportSelectedRows() { this.setState({ - rowsToExport: null + rowsToExport: null, }); } @@ -1321,11 +1356,7 @@ class Browser extends DashboardView { columnsObject[column.name] = column; }); // get ordered list of class columns - const columns = ColumnPreferences.getOrder( - columnsObject, - this.context.applicationId, - className - ).filter((column) => column.visible); + const columns = ColumnPreferences.getOrder(columnsObject, this.context.applicationId, className).filter((column) => column.visible); if (type === '.json') { const element = document.createElement('a'); @@ -1338,7 +1369,7 @@ class Browser extends DashboardView { return json; }), null, - indentation ? 2 : null, + indentation ? 2 : null ), ], { type: 'application/json' } @@ -1372,11 +1403,7 @@ class Browser extends DashboardView { colValue = object.get(column.name); } // Stringify objects and arrays - if ( - Object.prototype.toString.call(colValue) === - '[object Object]' || - Object.prototype.toString.call(colValue) === '[object Array]' - ) { + if (Object.prototype.toString.call(colValue) === '[object Object]' || Object.prototype.toString.call(colValue) === '[object Array]') { colValue = JSON.stringify(colValue); } if (typeof colValue === 'string') { @@ -1427,9 +1454,7 @@ class Browser extends DashboardView { this.setState({ exportingCount: batch.length }); } const one_gigabyte = Math.pow(2, 30); - const size = - new TextEncoder().encode(JSON.stringify(batch)).length / - one_gigabyte; + const size = new TextEncoder().encode(JSON.stringify(batch)).length / one_gigabyte; if (size.length > 1) { processObjects(batch); batch = []; @@ -1448,22 +1473,22 @@ class Browser extends DashboardView { getClassRelationColumns(className) { const currentClassName = this.props.params.className; return this.getClassColumns(className, false) - .map(column => { + .map((column) => { if (column.type === 'Relation' && column.targetClass === currentClassName) { return column.name; } }) - .filter(column => column); + .filter((column) => column); } getClassColumns(className, onlyTouchable = true) { let columns = []; const classes = this.props.schema.data.get('classes'); classes.get(className).forEach((field, name) => { - columns.push({ - ...field, - name, - }); + columns.push({ + ...field, + name, + }); }); if (onlyTouchable) { let untouchable = DefaultColumns.All; @@ -1499,14 +1524,16 @@ class Browser extends DashboardView { special.sort((a, b) => stringCompare(a.name, b.name)); categories.sort((a, b) => stringCompare(a.name, b.name)); if (special.length > 0 && categories.length > 0) { - special.push({ type: 'separator', id: 'classSeparator' }) + special.push({ type: 'separator', id: 'classSeparator' }); } - return ( - - ); + const allCategories = []; + for (const row of [...special, ...categories]) { + const { filters = [] } = ClassPreferences.getPreferences(this.context.applicationId, row.name); + row.filters = filters; + allCategories.push(row); + } + + return this.props.navigate(generatePath(this.context, url))} removeFilter={(filter) => this.removeFilter(filter)} categories={allCategories} />; } showNote(message, isError) { @@ -1549,26 +1576,21 @@ class Browser extends DashboardView { }); } - handleShowAcl(row, col){ + handleShowAcl(row, col) { this.dataBrowserRef.current.setEditing(true); this.dataBrowserRef.current.setCurrent({ row, col }); } // skips key controls handling when dialog is opened - onDialogToggle(opened){ - this.setState({showPermissionsDialog: opened}); + onDialogToggle(opened) { + this.setState({ showPermissionsDialog: opened }); } - async onChangeDefaultKey (name) { - ColumnPreferences.setPointerDefaultKey( - this.context.applicationId, - this.props.params.className, - name - ); + async onChangeDefaultKey(name) { + ColumnPreferences.setPointerDefaultKey(this.context.applicationId, this.props.params.className, name); this.setState({ showPointerKeyDialog: false }); } - renderContent() { let browser = null; let className = this.props.params.className; @@ -1580,24 +1602,18 @@ class Browser extends DashboardView { if (classes.size === 0) { browser = (
- +
); } else if (className && classes.get(className)) { - let columns = { - objectId: { type: 'String' } + objectId: { type: 'String' }, }; if (this.state.isUnique) { columns = {}; } classes.get(className).forEach(({ type, targetClass, required }, name) => { - if (name === 'objectId' || this.state.isUnique && name !== this.state.uniqueField) { + if (name === 'objectId' || (this.state.isUnique && name !== this.state.uniqueField)) { return; } const info = { type, required: !!required }; @@ -1634,6 +1650,7 @@ class Browser extends DashboardView { schema={this.props.schema} filters={this.state.filters} onFilterChange={this.updateFilters} + onFilterSave={(...args) => this.saveFilters(...args)} onRemoveColumn={this.showRemoveColumn} onDeleteRows={this.showDeleteRows} onDropClass={this.showDropClass} @@ -1647,14 +1664,12 @@ class Browser extends DashboardView { onEditPermissions={this.onDialogToggle} onExportSelectedRows={this.showExportSelectedRowsDialog} onExportSchema={this.showExportSchemaDialog} - onSaveNewRow={this.saveNewRow} onShowPointerKey={this.showPointerKeyDialog} onAbortAddRow={this.abortAddRow} onSaveEditCloneRow={this.saveEditCloneRow} onAbortEditCloneRow={this.abortEditCloneRow} onCancelPendingEditRows={this.cancelPendingEditRows} - currentUser={this.state.currentUser} useMasterKey={this.state.useMasterKey} login={this.login} @@ -1684,31 +1699,17 @@ class Browser extends DashboardView { onAbortAddRow={this.abortAddRow} onAddRowWithModal={this.addRowWithModal} onAddClass={this.showCreateClass} - showNote={this.showNote} /> + showNote={this.showNote} + /> ); } } let extras = null; - if(this.state.showPointerKeyDialog){ - let currentColumns = this.getClassColumns(className).map(column => column.name); - extras = ( - this.setState({ showPointerKeyDialog: false })} - onConfirm={this.onChangeDefaultKey} /> - ); - } - else if (this.state.showCreateClassDialog) { - extras = ( - this.setState({ showCreateClassDialog: false })} - onConfirm={this.createClass} /> - ); + if (this.state.showPointerKeyDialog) { + let currentColumns = this.getClassColumns(className).map((column) => column.name); + extras = this.setState({ showPointerKeyDialog: false })} onConfirm={this.onChangeDefaultKey} />; + } else if (this.state.showCreateClassDialog) { + extras = this.setState({ showCreateClassDialog: false })} onConfirm={this.createClass} />; } else if (this.state.showAddColumnDialog) { const currentApp = this.context || {}; let currentColumns = []; @@ -1724,95 +1725,50 @@ class Browser extends DashboardView { onConfirm={this.addColumn} onContinue={this.addColumnAndContinue} showNote={this.showNote} - parseServerVersion={currentApp.serverInfo && currentApp.serverInfo.parseServerVersion} /> + parseServerVersion={currentApp.serverInfo && currentApp.serverInfo.parseServerVersion} + /> ); } else if (this.state.showRemoveColumnDialog) { - let currentColumns = this.getClassColumns(className).map(column => column.name); - extras = ( - this.setState({ showRemoveColumnDialog: false })} - onConfirm={this.removeColumn} /> - ); + let currentColumns = this.getClassColumns(className).map((column) => column.name); + extras = this.setState({ showRemoveColumnDialog: false })} onConfirm={this.removeColumn} />; } else if (this.state.rowsToDelete) { - extras = ( - this.setState({ rowsToDelete: null })} - onConfirm={() => this.deleteRows(this.state.rowsToDelete)} /> - ); + extras = this.setState({ rowsToDelete: null })} onConfirm={() => this.deleteRows(this.state.rowsToDelete)} />; } else if (this.state.showDropClassDialog) { extras = ( this.setState({ - showDropClassDialog: false, - lastError: null, - lastNote: null, - })} - onConfirm={() => this.dropClass(className)} /> + onCancel={() => + this.setState({ + showDropClassDialog: false, + lastError: null, + lastNote: null, + }) + } + onConfirm={() => this.dropClass(className)} + /> ); } else if (this.state.showExportDialog) { - extras = ( - this.setState({ showExportDialog: false })} - onConfirm={() => this.exportClass(className)} /> - ); + extras = this.setState({ showExportDialog: false })} onConfirm={() => this.exportClass(className)} />; } else if (this.state.showExportSchemaDialog) { - extras = ( - this.setState({ showExportSchemaDialog: false })} - onConfirm={(...args) => this.exportSchema(...args)} /> - ); + extras = this.setState({ showExportSchemaDialog: false })} onConfirm={(...args) => this.exportSchema(...args)} />; } else if (this.state.showAttachRowsDialog) { - extras = ( - - ) + extras = ; } else if (this.state.showAttachSelectedRowsDialog) { - extras = ( - - ); + extras = ; } else if (this.state.showCloneSelectedRowsDialog) { - extras = ( - - ); + extras = ; } else if (this.state.showEditRowDialog) { const classColumns = this.getClassColumns(className, false); // create object with classColumns as property keys needed for ColumnPreferences.getOrder function const columnsObject = {}; classColumns.forEach((column) => { - columnsObject[column.name] = column + columnsObject[column.name] = column; }); // get ordered list of class columns - const columnPreferences = this.context.columnPreference || {} - const columns = ColumnPreferences.getOrder( - columnsObject, - this.context.applicationId, - className, - columnPreferences[className] - ); + const columnPreferences = this.context.columnPreference || {}; + const columns = ColumnPreferences.getOrder(columnsObject, this.context.applicationId, className, columnPreferences[className]); // extend columns with their type and targetClass properties - columns.forEach(column => { + columns.forEach((column) => { const { type, targetClass } = columnsObject[column.name]; column.type = type; column.targetClass = targetClass; @@ -1832,16 +1788,14 @@ class Browser extends DashboardView { this.selectRow(selectedId, true); } - const row = data.findIndex(d => d.id === selectedId); + const row = data.findIndex((d) => d.id === selectedId); - const attributes = selectedId - ? data[row].attributes - : newObject.attributes; + const attributes = selectedId ? data[row].attributes : newObject.attributes; const selectedObject = { row: row, id: selectedId, - ...attributes + ...attributes, }; extras = ( @@ -1858,7 +1812,7 @@ class Browser extends DashboardView { schema={this.props.schema} useMasterKey={this.state.useMasterKey} /> - ) + ); } else if (this.state.rowsToExport) { extras = ( - ); + notification = ; } else if (this.state.lastNote) { - notification = ( - - ); - } - else if (this.state.exporting) { - notification = ( - - ); + notification = ; + } else if (this.state.exporting) { + notification = ; } return (
diff --git a/src/dashboard/Data/Browser/BrowserToolbar.react.js b/src/dashboard/Data/Browser/BrowserToolbar.react.js index 5076c9a141..73824bc86c 100644 --- a/src/dashboard/Data/Browser/BrowserToolbar.react.js +++ b/src/dashboard/Data/Browser/BrowserToolbar.react.js @@ -31,6 +31,7 @@ let BrowserToolbar = ({ relation, setCurrent, onFilterChange, + onFilterSave, onAddColumn, onAddRow, onAddRowWithModal, @@ -275,6 +276,7 @@ let BrowserToolbar = ({ schema={schemaSimplifiedData} filters={filters} onChange={onFilterChange} + onSaveFilter={onFilterSave} className={classNameForEditors} blacklistedFilters={onAddRow ? [] : ['unique']} disabled={isPendingEditCloneRows} diff --git a/src/dashboard/Data/Browser/DataBrowser.react.js b/src/dashboard/Data/Browser/DataBrowser.react.js index 962ba7e46c..a1bca22cb7 100644 --- a/src/dashboard/Data/Browser/DataBrowser.react.js +++ b/src/dashboard/Data/Browser/DataBrowser.react.js @@ -316,6 +316,7 @@ export default class DataBrowser extends React.Component { setCopyableValue={this.setCopyableValue} setContextMenu={this.setContextMenu} onFilterChange={this.props.onFilterChange} + onFilterSave={this.props.onFilterSave} {...other} /> Date: Fri, 9 Jun 2023 12:09:23 +0000 Subject: [PATCH 25/30] chore(release): 5.2.0-alpha.12 [skip ci] # [5.2.0-alpha.12](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.11...5.2.0-alpha.12) (2023-06-09) ### Features * Add filter views to save frequently used filters in data browser ([#2404](https://github.com/ParsePlatform/parse-dashboard/issues/2404)) ([a9ec3a9](https://github.com/ParsePlatform/parse-dashboard/commit/a9ec3a915ff354304f382c17e8d5311b2c96d7ff)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 21e7c62163..ecbf5a90e6 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [5.2.0-alpha.12](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.11...5.2.0-alpha.12) (2023-06-09) + + +### Features + +* Add filter views to save frequently used filters in data browser ([#2404](https://github.com/ParsePlatform/parse-dashboard/issues/2404)) ([a9ec3a9](https://github.com/ParsePlatform/parse-dashboard/commit/a9ec3a915ff354304f382c17e8d5311b2c96d7ff)) + # [5.2.0-alpha.11](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.10...5.2.0-alpha.11) (2023-06-08) diff --git a/package-lock.json b/package-lock.json index 255488384c..fc47a83d50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.11", + "version": "5.2.0-alpha.12", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f24d342ff2..f56fc888bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.11", + "version": "5.2.0-alpha.12", "repository": { "type": "git", "url": "https://github.com/ParsePlatform/parse-dashboard" From f56f946023c628c96030e9d2d66284c53decd33e Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 10 Jun 2023 23:29:38 +1000 Subject: [PATCH 26/30] feat: Add export of saved data browser filters via `classPreference` settings (#2455) --- README.md | 31 +++++++++++++++++-- .../DashboardSettings.react.js | 26 +++++++++++----- .../DashboardSettings/DashboardSettings.scss | 2 +- src/lib/ClassPreferences.js | 23 ++++++++++++++ src/lib/ParseApp.js | 21 ++++++++++++- 5 files changed, 90 insertions(+), 13 deletions(-) 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() { From ca33d089726950b65bcc9b193cdf56923c83d850 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 10 Jun 2023 13:31:31 +0000 Subject: [PATCH 27/30] chore(release): 5.2.0-alpha.13 [skip ci] # [5.2.0-alpha.13](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.12...5.2.0-alpha.13) (2023-06-10) ### Features * Add export of saved data browser filters via `classPreference` settings ([#2455](https://github.com/ParsePlatform/parse-dashboard/issues/2455)) ([f56f946](https://github.com/ParsePlatform/parse-dashboard/commit/f56f946023c628c96030e9d2d66284c53decd33e)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index ecbf5a90e6..4f8e664c9c 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [5.2.0-alpha.13](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.12...5.2.0-alpha.13) (2023-06-10) + + +### Features + +* Add export of saved data browser filters via `classPreference` settings ([#2455](https://github.com/ParsePlatform/parse-dashboard/issues/2455)) ([f56f946](https://github.com/ParsePlatform/parse-dashboard/commit/f56f946023c628c96030e9d2d66284c53decd33e)) + # [5.2.0-alpha.12](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.11...5.2.0-alpha.12) (2023-06-09) diff --git a/package-lock.json b/package-lock.json index fc47a83d50..0b3fd27123 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.12", + "version": "5.2.0-alpha.13", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f56fc888bb..f8ad86490c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.12", + "version": "5.2.0-alpha.13", "repository": { "type": "git", "url": "https://github.com/ParsePlatform/parse-dashboard" From 32aeea244d81404579f55adf25244c11e3a797d1 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 10 Jun 2023 15:51:16 +0200 Subject: [PATCH 28/30] fix: Hitting backspace key in data browser crashes dashboard (#2456) --- src/components/BrowserCell/BrowserCell.react.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/BrowserCell/BrowserCell.react.js b/src/components/BrowserCell/BrowserCell.react.js index aa134b58c3..94d5cfc2b8 100644 --- a/src/components/BrowserCell/BrowserCell.react.js +++ b/src/components/BrowserCell/BrowserCell.react.js @@ -177,7 +177,7 @@ export default class BrowserCell extends Component { componentDidUpdate(prevProps) { if ( this.props.value !== prevProps.value ) { this.renderCellContent(); - this.props.value._previousSave + this.props.value?._previousSave ?.then(() => this.renderCellContent()) ?.catch(err => console.log(err)) } From e717fbb8fd25401cba41e2b675c2e0af6c1813a8 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 10 Jun 2023 13:53:17 +0000 Subject: [PATCH 29/30] chore(release): 5.2.0-alpha.14 [skip ci] # [5.2.0-alpha.14](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.13...5.2.0-alpha.14) (2023-06-10) ### Bug Fixes * Hitting backspace key in data browser crashes dashboard ([#2456](https://github.com/ParsePlatform/parse-dashboard/issues/2456)) ([32aeea2](https://github.com/ParsePlatform/parse-dashboard/commit/32aeea244d81404579f55adf25244c11e3a797d1)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 4f8e664c9c..31fa633b72 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [5.2.0-alpha.14](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.13...5.2.0-alpha.14) (2023-06-10) + + +### Bug Fixes + +* Hitting backspace key in data browser crashes dashboard ([#2456](https://github.com/ParsePlatform/parse-dashboard/issues/2456)) ([32aeea2](https://github.com/ParsePlatform/parse-dashboard/commit/32aeea244d81404579f55adf25244c11e3a797d1)) + # [5.2.0-alpha.13](https://github.com/ParsePlatform/parse-dashboard/compare/5.2.0-alpha.12...5.2.0-alpha.13) (2023-06-10) diff --git a/package-lock.json b/package-lock.json index 0b3fd27123..ac0ce9f4bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.13", + "version": "5.2.0-alpha.14", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f8ad86490c..1ae8ea0a1e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "5.2.0-alpha.13", + "version": "5.2.0-alpha.14", "repository": { "type": "git", "url": "https://github.com/ParsePlatform/parse-dashboard" From c781f6152e7acbdbe3db2ec649c8818b03b8c612 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 8 Jun 2023 19:14:45 +0200 Subject: [PATCH 30/30] empty commit