From 36e4987f95c780c78f96beecab5be36c618a6eb5 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 27 Apr 2025 20:23:48 +1000 Subject: [PATCH 1/8] feat: confirmation on config param update --- src/dashboard/Data/Browser/Browser.scss | 4 ++ src/dashboard/Data/Config/Config.react.js | 49 ++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/dashboard/Data/Browser/Browser.scss b/src/dashboard/Data/Browser/Browser.scss index 331e444ea4..e47ff5ac5c 100644 --- a/src/dashboard/Data/Browser/Browser.scss +++ b/src/dashboard/Data/Browser/Browser.scss @@ -270,4 +270,8 @@ body:global(.expanded) { .noScroll { overflow-x: hidden; +} + +.confimConfig { + padding: 10px 20px; } \ No newline at end of file diff --git a/src/dashboard/Data/Config/Config.react.js b/src/dashboard/Data/Config/Config.react.js index 005cc032c3..56f9125c23 100644 --- a/src/dashboard/Data/Config/Config.react.js +++ b/src/dashboard/Data/Config/Config.react.js @@ -21,6 +21,7 @@ import TableView from 'dashboard/TableView.react'; import Toolbar from 'components/Toolbar/Toolbar.react'; import browserStyles from 'dashboard/Data/Browser/Browser.scss'; import { CurrentApp } from 'context/currentApp'; +import Modal from 'components/Modal/Modal.react'; @subscribeTo('Config', 'config') class Config extends TableView { @@ -38,6 +39,7 @@ class Config extends TableView { modalValue: '', modalMasterKeyOnly: false, loading: false, + confirmModalOpen: false, }; } @@ -58,6 +60,7 @@ class Config extends TableView { loadData() { this.setState({ loading: true }); this.props.config.dispatch(ActionTypes.FETCH).finally(() => { + this.cacheData = new Map(this.props.config.data); this.setState({ loading: false }); }); } @@ -101,6 +104,30 @@ class Config extends TableView { /> ); } + + if (this.state.confirmModalOpen) { + extras = ( + this.setState({ confirmModalOpen: false })} + onConfirm={() => { + this.setState({ confirmModalOpen: false }); + this.saveParam({ + ...this.confirmData, + override: true, + }); + }} + > +
+ The parameter you are trying to edit has been modified by another user. Do you want to continue? +
+
+ ); + } return extras; } @@ -244,7 +271,27 @@ class Config extends TableView { return data; } - saveParam({ name, value, type, masterKeyOnly }) { + async saveParam({ name, value, type, masterKeyOnly, override }) { + const cachedParams = this.cacheData.get('params'); + const cachedValue = cachedParams.get(name); + + await this.props.config.dispatch(ActionTypes.FETCH); + const fetchedParams = this.props.config.data.get('params'); + + if (cachedValue !== fetchedParams.get(name) && !override) { + this.setState({ + confirmModalOpen: true, + modalOpen: false, + }); + this.confirmData = { + name, + value, + type, + masterKeyOnly, + }; + return; + } + this.props.config .dispatch(ActionTypes.SET, { param: name, From 880c3e38731a8aa4ab570b1863efc5d559811186 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Apr 2025 02:29:03 +0100 Subject: [PATCH 2/8] fix typo Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- src/dashboard/Data/Browser/Browser.scss | 2 +- src/dashboard/Data/Config/Config.react.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dashboard/Data/Browser/Browser.scss b/src/dashboard/Data/Browser/Browser.scss index e47ff5ac5c..705580ce9d 100644 --- a/src/dashboard/Data/Browser/Browser.scss +++ b/src/dashboard/Data/Browser/Browser.scss @@ -272,6 +272,6 @@ body:global(.expanded) { overflow-x: hidden; } -.confimConfig { +.confirmConfig { padding: 10px 20px; } \ No newline at end of file diff --git a/src/dashboard/Data/Config/Config.react.js b/src/dashboard/Data/Config/Config.react.js index 56f9125c23..f02a85c28c 100644 --- a/src/dashboard/Data/Config/Config.react.js +++ b/src/dashboard/Data/Config/Config.react.js @@ -122,7 +122,7 @@ class Config extends TableView { }); }} > -
+
The parameter you are trying to edit has been modified by another user. Do you want to continue?
From aea4d1fbd55411f86be3ccedb1ba55a8546bacee Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Tue, 29 Apr 2025 02:55:49 +0100 Subject: [PATCH 3/8] rephrase dialog Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- src/dashboard/Data/Config/Config.react.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dashboard/Data/Config/Config.react.js b/src/dashboard/Data/Config/Config.react.js index f02a85c28c..88ebcf82ea 100644 --- a/src/dashboard/Data/Config/Config.react.js +++ b/src/dashboard/Data/Config/Config.react.js @@ -123,7 +123,7 @@ class Config extends TableView { }} >
- The parameter you are trying to edit has been modified by another user. Do you want to continue? + The parameter you are trying to save has been modified while you were editing it. This means your edit is not based on the current parameter value. Do you want to continue and overwrite the other changes?
); From 5f652a9d7a1536b34ca809329de19c02fea449e8 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 22 May 2025 20:26:31 +1000 Subject: [PATCH 4/8] add loading state --- src/dashboard/Data/Config/Config.react.js | 147 ++++++++++-------- .../Data/Config/ConfigDialog.react.js | 35 +++-- 2 files changed, 104 insertions(+), 78 deletions(-) diff --git a/src/dashboard/Data/Config/Config.react.js b/src/dashboard/Data/Config/Config.react.js index 88ebcf82ea..ef31b12bba 100644 --- a/src/dashboard/Data/Config/Config.react.js +++ b/src/dashboard/Data/Config/Config.react.js @@ -93,6 +93,7 @@ class Config extends TableView { value={this.state.modalValue} masterKeyOnly={this.state.modalMasterKeyOnly} parseServerVersion={this.context.serverInfo?.parseServerVersion} + loading={this.state.loading} /> ); } else if (this.state.showDeleteParameterDialog) { @@ -272,77 +273,93 @@ class Config extends TableView { } async saveParam({ name, value, type, masterKeyOnly, override }) { - const cachedParams = this.cacheData.get('params'); - const cachedValue = cachedParams.get(name); + try { + this.setState({ loading: true }); + + const fetchedParams = this.props.config.data.get('params'); + const currentValue = fetchedParams.get(name); + await this.props.config.dispatch(ActionTypes.FETCH); + const fetchedParamsAfter = this.props.config.data.get('params'); + const currentValueAfter = fetchedParamsAfter.get(name); - await this.props.config.dispatch(ActionTypes.FETCH); - const fetchedParams = this.props.config.data.get('params'); - - if (cachedValue !== fetchedParams.get(name) && !override) { - this.setState({ - confirmModalOpen: true, - modalOpen: false, - }); - this.confirmData = { - name, - value, - type, - masterKeyOnly, - }; - return; - } + if (currentValue !== currentValueAfter && !override) { + this.setState({ + confirmModalOpen: true, + modalOpen: false, + loading: false, + }); + this.confirmData = { + name, + value, + type, + masterKeyOnly, + }; + return; + } - this.props.config - .dispatch(ActionTypes.SET, { + await this.props.config.dispatch(ActionTypes.SET, { param: name, value: value, masterKeyOnly: masterKeyOnly, - }) - .then( - () => { - this.setState({ modalOpen: false }); - const limit = this.context.cloudConfigHistoryLimit; - const applicationId = this.context.applicationId; - let transformedValue = value; - if (type === 'Date') { - transformedValue = { __type: 'Date', iso: value }; - } - if (type === 'File') { - transformedValue = { name: value._name, url: value._url }; - } - const configHistory = localStorage.getItem(`${applicationId}_configHistory`); - if (!configHistory) { - localStorage.setItem( - `${applicationId}_configHistory`, - JSON.stringify({ - [name]: [ - { - time: new Date(), - value: transformedValue, - }, - ], - }) - ); - } else { - const oldConfigHistory = JSON.parse(configHistory); - localStorage.setItem( - `${applicationId}_configHistory`, - JSON.stringify({ - ...oldConfigHistory, - [name]: !oldConfigHistory[name] - ? [{ time: new Date(), value: transformedValue }] - : [ - { time: new Date(), value: transformedValue }, - ...oldConfigHistory[name], - ].slice(0, limit || 100), - }) - ); - } - }, - () => { - // Catch the error - } + }); + + // Update the cached data after successful save + const params = this.cacheData.get('params'); + params.set(name, value); + if (masterKeyOnly) { + const masterKeyOnlyParams = this.cacheData.get('masterKeyOnly') || new Map(); + masterKeyOnlyParams.set(name, masterKeyOnly); + this.cacheData.set('masterKeyOnly', masterKeyOnlyParams); + } + + this.setState({ modalOpen: false }); + + // Update config history in localStorage + const limit = this.context.cloudConfigHistoryLimit; + const applicationId = this.context.applicationId; + let transformedValue = value; + + if (type === 'Date') { + transformedValue = { __type: 'Date', iso: value }; + } + if (type === 'File') { + transformedValue = { name: value._name, url: value._url }; + } + + const configHistory = localStorage.getItem(`${applicationId}_configHistory`); + const newHistoryEntry = { + time: new Date(), + value: transformedValue, + }; + + if (!configHistory) { + localStorage.setItem( + `${applicationId}_configHistory`, + JSON.stringify({ + [name]: [newHistoryEntry], + }) + ); + } else { + const oldConfigHistory = JSON.parse(configHistory); + const updatedHistory = !oldConfigHistory[name] + ? [newHistoryEntry] + : [newHistoryEntry, ...oldConfigHistory[name]].slice(0, limit || 100); + + localStorage.setItem( + `${applicationId}_configHistory`, + JSON.stringify({ + ...oldConfigHistory, + [name]: updatedHistory, + }) + ); + } + } catch (error) { + this.context.showError?.( + `Failed to save parameter: ${error.message || 'Unknown error occurred'}` ); + } finally { + this.setState({ loading: false }); + } } deleteParam(name) { diff --git a/src/dashboard/Data/Config/ConfigDialog.react.js b/src/dashboard/Data/Config/ConfigDialog.react.js index 7f4c167a95..8124781409 100644 --- a/src/dashboard/Data/Config/ConfigDialog.react.js +++ b/src/dashboard/Data/Config/ConfigDialog.react.js @@ -21,6 +21,7 @@ import validateNumeric from 'lib/validateNumeric'; import styles from 'dashboard/Data/Browser/Browser.scss'; import semver from 'semver/preload.js'; import { dateStringUTC } from 'lib/DateUtils'; +import LoaderContainer from 'components/LoaderContainer/LoaderContainer.react'; import { CurrentApp } from 'context/currentApp'; const PARAM_TYPES = ['Boolean', 'String', 'Number', 'Date', 'Object', 'Array', 'GeoPoint', 'File']; @@ -222,19 +223,8 @@ export default class ConfigDialog extends React.Component { this.setState({ selectedIndex: index, value }); }; - return ( - + const dialogContent = ( +
} input={ @@ -305,6 +295,25 @@ export default class ConfigDialog extends React.Component { className={styles.addColumnToggleWrapper} /> )} +
+ ); + + return ( + + + {dialogContent} + ); } From 56154166e9bef5deb46538839ac33459b247b5aa Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 22 May 2025 20:38:23 +1000 Subject: [PATCH 5/8] Update Config.react.js --- src/dashboard/Data/Config/Config.react.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dashboard/Data/Config/Config.react.js b/src/dashboard/Data/Config/Config.react.js index ef31b12bba..d295962e02 100644 --- a/src/dashboard/Data/Config/Config.react.js +++ b/src/dashboard/Data/Config/Config.react.js @@ -275,7 +275,7 @@ class Config extends TableView { async saveParam({ name, value, type, masterKeyOnly, override }) { try { this.setState({ loading: true }); - + const fetchedParams = this.props.config.data.get('params'); const currentValue = fetchedParams.get(name); await this.props.config.dispatch(ActionTypes.FETCH); @@ -313,12 +313,12 @@ class Config extends TableView { } this.setState({ modalOpen: false }); - + // Update config history in localStorage const limit = this.context.cloudConfigHistoryLimit; const applicationId = this.context.applicationId; let transformedValue = value; - + if (type === 'Date') { transformedValue = { __type: 'Date', iso: value }; } From 08b2fad573bec05e9b45758d53e6900a1e2097b2 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 24 May 2025 21:41:27 +0200 Subject: [PATCH 6/8] add deep comparison --- package-lock.json | 4 ++-- package.json | 1 + src/dashboard/Data/Config/Config.react.js | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1f0f82747f..e747db0462 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "core-js": "3.41.0", "csurf": "1.11.0", "express": "4.21.2", + "fast-deep-equal": "3.1.3", "graphiql": "2.0.8", "graphql": "16.11.0", "immutable": "5.1.2", @@ -11205,8 +11206,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-fifo": { "version": "1.3.2", diff --git a/package.json b/package.json index 0a0a82f00d..e171f084cd 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "core-js": "3.41.0", "csurf": "1.11.0", "express": "4.21.2", + "fast-deep-equal": "3.1.3", "graphiql": "2.0.8", "graphql": "16.11.0", "immutable": "5.1.2", diff --git a/src/dashboard/Data/Config/Config.react.js b/src/dashboard/Data/Config/Config.react.js index d295962e02..044b983227 100644 --- a/src/dashboard/Data/Config/Config.react.js +++ b/src/dashboard/Data/Config/Config.react.js @@ -22,6 +22,7 @@ import Toolbar from 'components/Toolbar/Toolbar.react'; import browserStyles from 'dashboard/Data/Browser/Browser.scss'; import { CurrentApp } from 'context/currentApp'; import Modal from 'components/Modal/Modal.react'; +import equal from 'fast-deep-equal'; @subscribeTo('Config', 'config') class Config extends TableView { @@ -281,8 +282,9 @@ class Config extends TableView { await this.props.config.dispatch(ActionTypes.FETCH); const fetchedParamsAfter = this.props.config.data.get('params'); const currentValueAfter = fetchedParamsAfter.get(name); + const valuesAreEqual = equal(currentValue, currentValueAfter); - if (currentValue !== currentValueAfter && !override) { + if (!valuesAreEqual && !override) { this.setState({ confirmModalOpen: true, modalOpen: false, From 479447f3fa2106d4d12fd33fa5777ef64770a4ec Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 24 May 2025 23:08:41 +0200 Subject: [PATCH 7/8] fetch config before dialog --- src/dashboard/Data/Config/Config.react.js | 59 ++++++++++++++++--- .../Data/Config/ConfigDialog.react.js | 10 ++++ 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/dashboard/Data/Config/Config.react.js b/src/dashboard/Data/Config/Config.react.js index 044b983227..5822c0c91b 100644 --- a/src/dashboard/Data/Config/Config.react.js +++ b/src/dashboard/Data/Config/Config.react.js @@ -58,12 +58,14 @@ class Config extends TableView { this.loadData(); } - loadData() { + async loadData() { this.setState({ loading: true }); - this.props.config.dispatch(ActionTypes.FETCH).finally(() => { + try { + await this.props.config.dispatch(ActionTypes.FETCH); this.cacheData = new Map(this.props.config.data); + } finally { this.setState({ loading: false }); - }); + } } renderToolbar() { @@ -133,8 +135,8 @@ class Config extends TableView { return extras; } - renderRow(data) { - let value = data.value; + parseValueForModal(dataValue) { + let value = dataValue; let modalValue = value; let type = typeof value; @@ -149,11 +151,11 @@ class Config extends TableView { } else if (value instanceof Parse.GeoPoint) { type = 'GeoPoint'; value = `(${value.latitude}, ${value.longitude})`; - modalValue = data.value.toJSON(); - } else if (data.value instanceof Parse.File) { + modalValue = dataValue.toJSON(); + } else if (dataValue instanceof Parse.File) { type = 'File'; value = ( - + Open in new window ); @@ -168,14 +170,53 @@ class Config extends TableView { } type = type.substr(0, 1).toUpperCase() + type.substr(1); } - const openModal = () => + + return { + value: value, + modalValue: modalValue, + type: type, + }; + } + + renderRow(data) { + // Parse modal data + const { value, modalValue, type } = this.parseValueForModal(data.value); + + /** + * Opens the modal dialog to edit the Config parameter. + */ + const openModal = async () => { + + // Show dialog this.setState({ + loading: true, modalOpen: true, modalParam: data.param, modalType: type, modalValue: modalValue, modalMasterKeyOnly: data.masterKeyOnly, }); + + // Fetch config data + await this.loadData(); + + // Get latest param values + const fetchedParams = this.props.config.data.get('params'); + const fetchedValue = fetchedParams.get(this.state.modalParam); + const fetchedMasterKeyOnly = this.props.config.data.get('masterKeyOnly')?.get(this.state.modalParam) || false; + + // Parse fetched data + const { modalValue: fetchedModalValue } = this.parseValueForModal(fetchedValue); + + // Update dialog + this.setState({ + modalValue: fetchedModalValue, + modalMasterKeyOnly: fetchedMasterKeyOnly, + loading: false, + }); + }; + + // Define column styles const columnStyleLarge = { width: '30%', cursor: 'pointer' }; const columnStyleSmall = { width: '15%', cursor: 'pointer' }; diff --git a/src/dashboard/Data/Config/ConfigDialog.react.js b/src/dashboard/Data/Config/ConfigDialog.react.js index 8124781409..02620090e9 100644 --- a/src/dashboard/Data/Config/ConfigDialog.react.js +++ b/src/dashboard/Data/Config/ConfigDialog.react.js @@ -181,6 +181,16 @@ export default class ConfigDialog extends React.Component { }); } + componentDidUpdate(prevProps) { + // Update parameter value or masterKeyOnly if they have changed + if (this.props.value !== prevProps.value || this.props.masterKeyOnly !== prevProps.masterKeyOnly) { + this.setState({ + value: this.props.value, + masterKeyOnly: this.props.masterKeyOnly, + }); + } + } + render() { const newParam = !this.props.param; const typeDropdown = ( From d620bec8eeafaa50dcc7aeeaaee06107dd9298cd Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 24 May 2025 23:16:09 +0200 Subject: [PATCH 8/8] shorten warning message --- src/dashboard/Data/Config/Config.react.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dashboard/Data/Config/Config.react.js b/src/dashboard/Data/Config/Config.react.js index 5822c0c91b..97f5ce3ea3 100644 --- a/src/dashboard/Data/Config/Config.react.js +++ b/src/dashboard/Data/Config/Config.react.js @@ -127,7 +127,7 @@ class Config extends TableView { }} >
- The parameter you are trying to save has been modified while you were editing it. This means your edit is not based on the current parameter value. Do you want to continue and overwrite the other changes? + This parameter changed while you were editing it. If you continue, the latest changes will be lost and replaced with your version. Do you want to proceed?
);