From 4da06c78216061deec993a7c8eec6413fc6e1558 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 2 Aug 2025 10:05:22 +0200
Subject: [PATCH 01/13] refactor: Bump inquirer from 12.6.3 to 12.9.0 (#2959)
---
package-lock.json | 176 +++++++++++++++++++++++-----------------------
package.json | 2 +-
2 files changed, 89 insertions(+), 89 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index c4648074d..6d6ecf383 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,7 +25,7 @@
"graphql": "16.11.0",
"immutable": "5.1.3",
"immutable-devtools": "0.1.5",
- "inquirer": "12.6.3",
+ "inquirer": "12.9.0",
"js-beautify": "1.15.4",
"node-fetch": "3.3.2",
"otpauth": "8.0.3",
@@ -2691,14 +2691,14 @@
}
},
"node_modules/@inquirer/checkbox": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.8.tgz",
- "integrity": "sha512-d/QAsnwuHX2OPolxvYcgSj7A9DO9H6gVOy2DvBTx+P2LH2iRTo/RSGV3iwCzW024nP9hw98KIuDmdyhZQj1UQg==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.0.tgz",
+ "integrity": "sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.13",
- "@inquirer/figures": "^1.0.12",
- "@inquirer/type": "^3.0.7",
+ "@inquirer/core": "^10.1.15",
+ "@inquirer/figures": "^1.0.13",
+ "@inquirer/type": "^3.0.8",
"ansi-escapes": "^4.3.2",
"yoctocolors-cjs": "^2.1.2"
},
@@ -2715,13 +2715,13 @@
}
},
"node_modules/@inquirer/confirm": {
- "version": "5.1.12",
- "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.12.tgz",
- "integrity": "sha512-dpq+ielV9/bqgXRUbNH//KsY6WEw9DrGPmipkpmgC1Y46cwuBTNx7PXFWTjc3MQ+urcc0QxoVHcMI0FW4Ok0hg==",
+ "version": "5.1.14",
+ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.14.tgz",
+ "integrity": "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.13",
- "@inquirer/type": "^3.0.7"
+ "@inquirer/core": "^10.1.15",
+ "@inquirer/type": "^3.0.8"
},
"engines": {
"node": ">=18"
@@ -2736,13 +2736,13 @@
}
},
"node_modules/@inquirer/core": {
- "version": "10.1.13",
- "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.13.tgz",
- "integrity": "sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==",
+ "version": "10.1.15",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz",
+ "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==",
"license": "MIT",
"dependencies": {
- "@inquirer/figures": "^1.0.12",
- "@inquirer/type": "^3.0.7",
+ "@inquirer/figures": "^1.0.13",
+ "@inquirer/type": "^3.0.8",
"ansi-escapes": "^4.3.2",
"cli-width": "^4.1.0",
"mute-stream": "^2.0.0",
@@ -2789,13 +2789,13 @@
}
},
"node_modules/@inquirer/editor": {
- "version": "4.2.13",
- "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.13.tgz",
- "integrity": "sha512-WbicD9SUQt/K8O5Vyk9iC2ojq5RHoCLK6itpp2fHsWe44VxxcA9z3GTWlvjSTGmMQpZr+lbVmrxdHcumJoLbMA==",
+ "version": "4.2.15",
+ "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.15.tgz",
+ "integrity": "sha512-wst31XT8DnGOSS4nNJDIklGKnf+8shuauVrWzgKegWUe28zfCftcWZ2vktGdzJgcylWSS2SrDnYUb6alZcwnCQ==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.13",
- "@inquirer/type": "^3.0.7",
+ "@inquirer/core": "^10.1.15",
+ "@inquirer/type": "^3.0.8",
"external-editor": "^3.1.0"
},
"engines": {
@@ -2811,13 +2811,13 @@
}
},
"node_modules/@inquirer/expand": {
- "version": "4.0.15",
- "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.15.tgz",
- "integrity": "sha512-4Y+pbr/U9Qcvf+N/goHzPEXiHH8680lM3Dr3Y9h9FFw4gHS+zVpbj8LfbKWIb/jayIB4aSO4pWiBTrBYWkvi5A==",
+ "version": "4.0.17",
+ "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.17.tgz",
+ "integrity": "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.13",
- "@inquirer/type": "^3.0.7",
+ "@inquirer/core": "^10.1.15",
+ "@inquirer/type": "^3.0.8",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
@@ -2833,22 +2833,22 @@
}
},
"node_modules/@inquirer/figures": {
- "version": "1.0.12",
- "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz",
- "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==",
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz",
+ "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@inquirer/input": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.12.tgz",
- "integrity": "sha512-xJ6PFZpDjC+tC1P8ImGprgcsrzQRsUh9aH3IZixm1lAZFK49UGHxM3ltFfuInN2kPYNfyoPRh+tU4ftsjPLKqQ==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.1.tgz",
+ "integrity": "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.13",
- "@inquirer/type": "^3.0.7"
+ "@inquirer/core": "^10.1.15",
+ "@inquirer/type": "^3.0.8"
},
"engines": {
"node": ">=18"
@@ -2863,13 +2863,13 @@
}
},
"node_modules/@inquirer/number": {
- "version": "3.0.15",
- "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.15.tgz",
- "integrity": "sha512-xWg+iYfqdhRiM55MvqiTCleHzszpoigUpN5+t1OMcRkJrUrw7va3AzXaxvS+Ak7Gny0j2mFSTv2JJj8sMtbV2g==",
+ "version": "3.0.17",
+ "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.17.tgz",
+ "integrity": "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.13",
- "@inquirer/type": "^3.0.7"
+ "@inquirer/core": "^10.1.15",
+ "@inquirer/type": "^3.0.8"
},
"engines": {
"node": ">=18"
@@ -2884,13 +2884,13 @@
}
},
"node_modules/@inquirer/password": {
- "version": "4.0.15",
- "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.15.tgz",
- "integrity": "sha512-75CT2p43DGEnfGTaqFpbDC2p2EEMrq0S+IRrf9iJvYreMy5mAWj087+mdKyLHapUEPLjN10mNvABpGbk8Wdraw==",
+ "version": "4.0.17",
+ "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.17.tgz",
+ "integrity": "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.13",
- "@inquirer/type": "^3.0.7",
+ "@inquirer/core": "^10.1.15",
+ "@inquirer/type": "^3.0.8",
"ansi-escapes": "^4.3.2"
},
"engines": {
@@ -2906,21 +2906,21 @@
}
},
"node_modules/@inquirer/prompts": {
- "version": "7.5.3",
- "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.5.3.tgz",
- "integrity": "sha512-8YL0WiV7J86hVAxrh3fE5mDCzcTDe1670unmJRz6ArDgN+DBK1a0+rbnNWp4DUB5rPMwqD5ZP6YHl9KK1mbZRg==",
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.0.tgz",
+ "integrity": "sha512-JHwGbQ6wjf1dxxnalDYpZwZxUEosT+6CPGD9Zh4sm9WXdtUp9XODCQD3NjSTmu+0OAyxWXNOqf0spjIymJa2Tw==",
"license": "MIT",
"dependencies": {
- "@inquirer/checkbox": "^4.1.8",
- "@inquirer/confirm": "^5.1.12",
- "@inquirer/editor": "^4.2.13",
- "@inquirer/expand": "^4.0.15",
- "@inquirer/input": "^4.1.12",
- "@inquirer/number": "^3.0.15",
- "@inquirer/password": "^4.0.15",
- "@inquirer/rawlist": "^4.1.3",
- "@inquirer/search": "^3.0.15",
- "@inquirer/select": "^4.2.3"
+ "@inquirer/checkbox": "^4.2.0",
+ "@inquirer/confirm": "^5.1.14",
+ "@inquirer/editor": "^4.2.15",
+ "@inquirer/expand": "^4.0.17",
+ "@inquirer/input": "^4.2.1",
+ "@inquirer/number": "^3.0.17",
+ "@inquirer/password": "^4.0.17",
+ "@inquirer/rawlist": "^4.1.5",
+ "@inquirer/search": "^3.1.0",
+ "@inquirer/select": "^4.3.1"
},
"engines": {
"node": ">=18"
@@ -2935,13 +2935,13 @@
}
},
"node_modules/@inquirer/rawlist": {
- "version": "4.1.3",
- "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.3.tgz",
- "integrity": "sha512-7XrV//6kwYumNDSsvJIPeAqa8+p7GJh7H5kRuxirct2cgOcSWwwNGoXDRgpNFbY/MG2vQ4ccIWCi8+IXXyFMZA==",
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.5.tgz",
+ "integrity": "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.13",
- "@inquirer/type": "^3.0.7",
+ "@inquirer/core": "^10.1.15",
+ "@inquirer/type": "^3.0.8",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
@@ -2957,14 +2957,14 @@
}
},
"node_modules/@inquirer/search": {
- "version": "3.0.15",
- "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.15.tgz",
- "integrity": "sha512-YBMwPxYBrADqyvP4nNItpwkBnGGglAvCLVW8u4pRmmvOsHUtCAUIMbUrLX5B3tFL1/WsLGdQ2HNzkqswMs5Uaw==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.0.tgz",
+ "integrity": "sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.13",
- "@inquirer/figures": "^1.0.12",
- "@inquirer/type": "^3.0.7",
+ "@inquirer/core": "^10.1.15",
+ "@inquirer/figures": "^1.0.13",
+ "@inquirer/type": "^3.0.8",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
@@ -2980,14 +2980,14 @@
}
},
"node_modules/@inquirer/select": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.3.tgz",
- "integrity": "sha512-OAGhXU0Cvh0PhLz9xTF/kx6g6x+sP+PcyTiLvCrewI99P3BBeexD+VbuwkNDvqGkk3y2h5ZiWLeRP7BFlhkUDg==",
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.1.tgz",
+ "integrity": "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.13",
- "@inquirer/figures": "^1.0.12",
- "@inquirer/type": "^3.0.7",
+ "@inquirer/core": "^10.1.15",
+ "@inquirer/figures": "^1.0.13",
+ "@inquirer/type": "^3.0.8",
"ansi-escapes": "^4.3.2",
"yoctocolors-cjs": "^2.1.2"
},
@@ -3004,9 +3004,9 @@
}
},
"node_modules/@inquirer/type": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz",
- "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==",
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz",
+ "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -13566,17 +13566,17 @@
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
"node_modules/inquirer": {
- "version": "12.6.3",
- "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.6.3.tgz",
- "integrity": "sha512-eX9beYAjr1MqYsIjx1vAheXsRk1jbZRvHLcBu5nA9wX0rXR1IfCZLnVLp4Ym4mrhqmh7AuANwcdtgQ291fZDfQ==",
+ "version": "12.9.0",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.9.0.tgz",
+ "integrity": "sha512-LlFVmvWVCun7uEgPB3vups9NzBrjJn48kRNtFGw3xU1H5UXExTEz/oF1JGLaB0fvlkUB+W6JfgLcSEaSdH7RPA==",
"license": "MIT",
"dependencies": {
- "@inquirer/core": "^10.1.13",
- "@inquirer/prompts": "^7.5.3",
- "@inquirer/type": "^3.0.7",
+ "@inquirer/core": "^10.1.15",
+ "@inquirer/prompts": "^7.8.0",
+ "@inquirer/type": "^3.0.8",
"ansi-escapes": "^4.3.2",
"mute-stream": "^2.0.0",
- "run-async": "^3.0.0",
+ "run-async": "^4.0.5",
"rxjs": "^7.8.2"
},
"engines": {
@@ -21913,9 +21913,9 @@
"license": "MIT"
},
"node_modules/run-async": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
- "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.5.tgz",
+ "integrity": "sha512-oN9GTgxUNDBumHTTDmQ8dep6VIJbgj9S3dPP+9XylVLIK4xB9XTXtKWROd5pnhdXR9k0EgO1JRcNh0T+Ny2FsA==",
"license": "MIT",
"engines": {
"node": ">=0.12.0"
diff --git a/package.json b/package.json
index 99b357c99..95f93da9e 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,7 @@
"graphql": "16.11.0",
"immutable": "5.1.3",
"immutable-devtools": "0.1.5",
- "inquirer": "12.6.3",
+ "inquirer": "12.9.0",
"js-beautify": "1.15.4",
"node-fetch": "3.3.2",
"otpauth": "8.0.3",
From 666e07860ba1478aec3cde9db5c98a5772ea07fb Mon Sep 17 00:00:00 2001
From: Manuel <5673677+mtrezza@users.noreply.github.com>
Date: Sun, 3 Aug 2025 14:04:55 +0200
Subject: [PATCH 02/13] feat: Add App Settings option to store dashboard
settings on server (#2958)
---
src/dashboard/Data/Views/Views.react.js | 131 +++++---
.../DashboardSettings.react.js | 134 ++++++++
src/lib/ParseApp.js | 2 +
src/lib/ServerConfigStorage.js | 199 ++++++++++++
src/lib/StoragePreferences.js | 83 +++++
src/lib/ViewPreferencesManager.js | 307 ++++++++++++++++++
6 files changed, 810 insertions(+), 46 deletions(-)
create mode 100644 src/lib/ServerConfigStorage.js
create mode 100644 src/lib/StoragePreferences.js
create mode 100644 src/lib/ViewPreferencesManager.js
diff --git a/src/dashboard/Data/Views/Views.react.js b/src/dashboard/Data/Views/Views.react.js
index 1f7ddcd2d..88f0b98a2 100644
--- a/src/dashboard/Data/Views/Views.react.js
+++ b/src/dashboard/Data/Views/Views.react.js
@@ -14,6 +14,7 @@ import Notification from 'dashboard/Data/Browser/Notification.react';
import TableView from 'dashboard/TableView.react';
import tableStyles from 'dashboard/TableView.scss';
import * as ViewPreferences from 'lib/ViewPreferences';
+import ViewPreferencesManager from 'lib/ViewPreferencesManager';
import generatePath from 'lib/generatePath';
import stringCompare from 'lib/stringCompare';
import { ActionTypes as SchemaActionTypes } from 'lib/stores/SchemaStore';
@@ -37,6 +38,7 @@ class Views extends TableView {
this.section = 'Core';
this.subsection = 'Views';
this._isMounted = false;
+ this.viewPreferencesManager = null; // Will be initialized when context is available
this.state = {
views: [],
counts: {},
@@ -83,49 +85,65 @@ class Views extends TableView {
}
}
- loadViews(app) {
- const views = ViewPreferences.getViews(app.applicationId);
- this.setState({ views, counts: {} }, () => {
- views.forEach(view => {
- if (view.showCounter) {
- if (view.cloudFunction) {
- // For Cloud Function views, call the function to get count
- Parse.Cloud.run(view.cloudFunction, {}, { useMasterKey: true })
- .then(res => {
- if (this._isMounted) {
- this.setState(({ counts }) => ({
- counts: { ...counts, [view.name]: Array.isArray(res) ? res.length : 0 },
- }));
- }
- })
- .catch(error => {
- if (this._isMounted) {
- this.showNote(`Request failed: ${error.message || 'Unknown error occurred'}`, true);
- }
- });
- } else if (view.query && Array.isArray(view.query)) {
- // For aggregation pipeline views, use existing logic
- new Parse.Query(view.className)
- .aggregate(view.query, { useMasterKey: true })
- .then(res => {
- if (this._isMounted) {
- this.setState(({ counts }) => ({
- counts: { ...counts, [view.name]: res.length },
- }));
- }
- })
- .catch(error => {
- if (this._isMounted) {
- this.showNote(`Request failed: ${error.message || 'Unknown error occurred'}`, true);
- }
- });
+ async loadViews(app) {
+ // Initialize ViewPreferencesManager if not already done or if app changed
+ if (!this.viewPreferencesManager || this.viewPreferencesManager.app !== app) {
+ this.viewPreferencesManager = new ViewPreferencesManager(app);
+ }
+
+ try {
+ const views = await this.viewPreferencesManager.getViews(app.applicationId);
+ this.setState({ views, counts: {} }, () => {
+ views.forEach(view => {
+ if (view.showCounter) {
+ if (view.cloudFunction) {
+ // For Cloud Function views, call the function to get count
+ Parse.Cloud.run(view.cloudFunction, {}, { useMasterKey: true })
+ .then(res => {
+ if (this._isMounted) {
+ this.setState(({ counts }) => ({
+ counts: { ...counts, [view.name]: Array.isArray(res) ? res.length : 0 },
+ }));
+ }
+ })
+ .catch(error => {
+ if (this._isMounted) {
+ this.showNote(`Request failed: ${error.message || 'Unknown error occurred'}`, true);
+ }
+ });
+ } else if (view.query && Array.isArray(view.query)) {
+ // For aggregation pipeline views, use existing logic
+ new Parse.Query(view.className)
+ .aggregate(view.query, { useMasterKey: true })
+ .then(res => {
+ if (this._isMounted) {
+ this.setState(({ counts }) => ({
+ counts: { ...counts, [view.name]: res.length },
+ }));
+ }
+ })
+ .catch(error => {
+ if (this._isMounted) {
+ this.showNote(`Request failed: ${error.message || 'Unknown error occurred'}`, true);
+ }
+ });
+ }
}
+ });
+ if (this._isMounted) {
+ this.loadData(this.props.params.name);
}
});
- if (this._isMounted) {
- this.loadData(this.props.params.name);
- }
- });
+ } catch (error) {
+ console.error('Failed to load views from server, falling back to local storage:', error);
+ // Fallback to local storage
+ const views = ViewPreferences.getViews(app.applicationId);
+ this.setState({ views, counts: {} }, () => {
+ if (this._isMounted) {
+ this.loadData(this.props.params.name);
+ }
+ });
+ }
}
loadData(name) {
@@ -671,8 +689,15 @@ class Views extends TableView {
onConfirm={view => {
this.setState(
state => ({ showCreate: false, views: [...state.views, view] }),
- () => {
- ViewPreferences.saveViews(this.context.applicationId, this.state.views);
+ async () => {
+ if (this.viewPreferencesManager) {
+ try {
+ await this.viewPreferencesManager.saveViews(this.context.applicationId, this.state.views);
+ } catch (error) {
+ console.error('Failed to save views:', error);
+ this.showNote('Failed to save view changes', true);
+ }
+ }
this.loadViews(this.context);
}
);
@@ -699,8 +724,15 @@ class Views extends TableView {
newViews[state.editIndex] = view;
return { editView: null, editIndex: null, views: newViews };
},
- () => {
- ViewPreferences.saveViews(this.context.applicationId, this.state.views);
+ async () => {
+ if (this.viewPreferencesManager) {
+ try {
+ await this.viewPreferencesManager.saveViews(this.context.applicationId, this.state.views);
+ } catch (error) {
+ console.error('Failed to save views:', error);
+ this.showNote('Failed to save view changes', true);
+ }
+ }
this.loadViews(this.context);
}
);
@@ -719,8 +751,15 @@ class Views extends TableView {
const newViews = state.views.filter((_, i) => i !== state.deleteIndex);
return { deleteIndex: null, views: newViews };
},
- () => {
- ViewPreferences.saveViews(this.context.applicationId, this.state.views);
+ async () => {
+ if (this.viewPreferencesManager) {
+ try {
+ await this.viewPreferencesManager.saveViews(this.context.applicationId, this.state.views);
+ } catch (error) {
+ console.error('Failed to save views:', error);
+ this.showNote('Failed to save view changes', true);
+ }
+ }
if (this.props.params.name === name) {
const path = generatePath(this.context, 'views');
this.props.navigate(path);
diff --git a/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js b/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js
index 25b657b1e..4e2cd7b0f 100644
--- a/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js
+++ b/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js
@@ -17,6 +17,7 @@ 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 ViewPreferencesManager from 'lib/ViewPreferencesManager';
import bcrypt from 'bcryptjs';
import * as OTPAuth from 'otpauth';
import QRCode from 'qrcode';
@@ -26,6 +27,7 @@ export default class DashboardSettings extends DashboardView {
super();
this.section = 'App Settings';
this.subsection = 'Dashboard Configuration';
+ this.viewPreferencesManager = null;
this.state = {
createUserInput: false,
@@ -39,6 +41,8 @@ export default class DashboardSettings extends DashboardView {
message: null,
passwordInput: '',
passwordHidden: true,
+ migrationLoading: false,
+ storagePreference: 'local', // Will be updated in componentDidMount
copyData: {
data: '',
show: false,
@@ -52,6 +56,81 @@ export default class DashboardSettings extends DashboardView {
};
}
+ componentDidMount() {
+ this.initializeViewPreferencesManager();
+ }
+
+ initializeViewPreferencesManager() {
+ if (this.context) {
+ this.viewPreferencesManager = new ViewPreferencesManager(this.context);
+ this.loadStoragePreference();
+ }
+ }
+
+ loadStoragePreference() {
+ if (this.viewPreferencesManager) {
+ const preference = this.viewPreferencesManager.getStoragePreference(this.context.applicationId);
+ this.setState({ storagePreference: preference });
+ }
+ }
+
+ handleStoragePreferenceChange(preference) {
+ if (this.viewPreferencesManager) {
+ this.viewPreferencesManager.setStoragePreference(this.context.applicationId, preference);
+ this.setState({ storagePreference: preference });
+
+ // Show a notification about the change
+ this.showNote(`Storage preference changed to ${preference === 'server' ? 'server' : 'browser'}`);
+ }
+ }
+
+ async migrateToServer() {
+ if (!this.viewPreferencesManager) {
+ this.showNote('ViewPreferencesManager not initialized');
+ return;
+ }
+
+ if (!this.viewPreferencesManager.isServerConfigEnabled()) {
+ this.showNote('Server configuration is not enabled for this app. Please add a "config" section to your app configuration.');
+ return;
+ }
+
+ this.setState({ migrationLoading: true });
+
+ try {
+ const result = await this.viewPreferencesManager.migrateToServer(this.context.applicationId);
+ if (result.success) {
+ if (result.viewCount > 0) {
+ this.showNote(`Successfully migrated ${result.viewCount} view(s) to server storage.`);
+ } else {
+ this.showNote('No views found to migrate.');
+ }
+ }
+ } catch (error) {
+ this.showNote(`Failed to migrate views: ${error.message}`);
+ } finally {
+ this.setState({ migrationLoading: false });
+ }
+ }
+
+ async deleteFromBrowser() {
+ if (!window.confirm('Are you sure you want to delete all dashboard settings from browser storage? This action cannot be undone.')) {
+ return;
+ }
+
+ if (!this.viewPreferencesManager) {
+ this.showNote('ViewPreferencesManager not initialized');
+ return;
+ }
+
+ const success = this.viewPreferencesManager.deleteFromBrowser(this.context.applicationId);
+ if (success) {
+ this.showNote('Successfully deleted views from browser storage.');
+ } else {
+ this.showNote('Failed to delete views from browser storage.');
+ }
+ }
+
getColumns() {
const data = ColumnPreferences.getAllPreferences(this.context.applicationId);
this.setState({
@@ -382,6 +461,61 @@ export default class DashboardSettings extends DashboardView {
}
/>
+ {this.viewPreferencesManager && this.viewPreferencesManager.isServerConfigEnabled() && (
+
+ )}
{this.state.copyData.show && copyData}
{this.state.createUserInput && createUserInput}
{this.state.newUser.show && userData}
diff --git a/src/lib/ParseApp.js b/src/lib/ParseApp.js
index 18e4ab02b..318c07fe8 100644
--- a/src/lib/ParseApp.js
+++ b/src/lib/ParseApp.js
@@ -50,6 +50,7 @@ export default class ParseApp {
classPreference,
enableSecurityChecks,
cloudConfigHistoryLimit,
+ config,
}) {
this.name = appName;
this.createdAt = created_at ? new Date(created_at) : new Date();
@@ -79,6 +80,7 @@ export default class ParseApp {
this.scripts = scripts;
this.enableSecurityChecks = !!enableSecurityChecks;
this.cloudConfigHistoryLimit = cloudConfigHistoryLimit;
+ this.config = config;
if (!supportedPushLocales) {
console.warn(
diff --git a/src/lib/ServerConfigStorage.js b/src/lib/ServerConfigStorage.js
new file mode 100644
index 000000000..edc5f5f36
--- /dev/null
+++ b/src/lib/ServerConfigStorage.js
@@ -0,0 +1,199 @@
+/*
+ * Copyright (c) 2016-present, Parse, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the license found in the LICENSE file in
+ * the root directory of this source tree.
+ */
+
+import Parse from 'parse';
+
+/**
+ * Utility class for storing dashboard configuration on Parse Server
+ */
+export default class ServerConfigStorage {
+ constructor(app) {
+ this.app = app;
+ this.className = app.config?.className || 'DashboardConfig';
+
+ // Validate className is a non-empty string
+ if (typeof this.className !== 'string' || !this.className.trim()) {
+ throw new Error('Invalid className for ServerConfigStorage');
+ }
+ }
+
+ /**
+ * Stores a configuration value on the server
+ * @param {string} key - The configuration key
+ * @param {*} value - The configuration value
+ * @param {string} appId - The app ID
+ * @param {string | null} userId - The user ID (optional)
+ * @returns {Promise}
+ */
+ async setConfig(key, value, appId, userId = null) {
+ // First, try to find existing config object to update instead of creating duplicates
+ const query = new Parse.Query(this.className);
+ query.equalTo('appId', appId);
+ query.equalTo('key', key);
+
+ if (userId) {
+ query.equalTo('user', new Parse.User({ objectId: userId }));
+ } else {
+ query.doesNotExist('user');
+ }
+
+ let configObject = await query.first({ useMasterKey: true });
+
+ // If no existing object found, create a new one
+ if (!configObject) {
+ configObject = new Parse.Object(this.className);
+ configObject.set('appId', appId);
+ configObject.set('key', key);
+ if (userId) {
+ configObject.set('user', new Parse.User({ objectId: userId }));
+ }
+ }
+
+ // Set the value in the appropriate typed field based on value type
+ const valueType = this._getValueType(value);
+ this._clearAllValueFields(configObject);
+ configObject.set(valueType, value);
+
+ // Use master key for operations
+ return configObject.save(null, { useMasterKey: true });
+ }
+
+ /**
+ * Gets a configuration value from the server
+ * @param {string} key - The configuration key
+ * @param {string} appId - The app ID
+ * @param {string | null} userId - The user ID (optional)
+ * @returns {Promise<*>} The configuration value
+ */
+ async getConfig(key, appId, userId = null) {
+ const query = new Parse.Query(this.className);
+ query.equalTo('appId', appId);
+ query.equalTo('key', key);
+
+ if (userId) {
+ query.equalTo('user', new Parse.User({ objectId: userId }));
+ } else {
+ query.doesNotExist('user');
+ }
+
+ const result = await query.first({ useMasterKey: true });
+ if (!result) {
+ return null;
+ }
+
+ return this._extractValue(result);
+ }
+
+ /**
+ * Gets all configuration values for an app with a key prefix
+ * @param {string} keyPrefix - The key prefix to filter by
+ * @param {string} appId - The app ID
+ * @param {string | null} userId - The user ID (optional)
+ * @returns {Promise