From 1b0bbcd6f86af6608e01d9fc4ee7704c4e0ccf7d Mon Sep 17 00:00:00 2001
From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com>
Date: Thu, 31 Jul 2025 11:06:37 +0200
Subject: [PATCH 01/17] feat
---
src/dashboard/Data/Views/Views.react.js | 128 +++++---
.../DashboardSettings.react.js | 116 +++++++
src/lib/ParseApp.js | 2 +
src/lib/ServerConfigStorage.js | 180 +++++++++++
src/lib/ViewPreferencesManager.js | 282 ++++++++++++++++++
5 files changed, 662 insertions(+), 46 deletions(-)
create mode 100644 src/lib/ServerConfigStorage.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 1f7ddcd2d5..6a4a8e4474 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,14 @@ 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.loadViews(this.context);
}
);
@@ -699,8 +723,14 @@ 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.loadViews(this.context);
}
);
@@ -719,8 +749,14 @@ 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);
+ }
+ }
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 25b657b1e9..1506c75b07 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,
+ hasServerViews: false,
copyData: {
data: '',
show: false,
@@ -52,6 +56,72 @@ export default class DashboardSettings extends DashboardView {
};
}
+ componentDidMount() {
+ this.initializeViewPreferencesManager();
+ }
+
+ initializeViewPreferencesManager() {
+ if (this.context) {
+ this.viewPreferencesManager = new ViewPreferencesManager(this.context);
+ this.checkServerViews();
+ }
+ }
+
+ async checkServerViews() {
+ if (this.viewPreferencesManager) {
+ try {
+ const hasServerViews = await this.viewPreferencesManager.hasServerViews(this.context.applicationId);
+ this.setState({ hasServerViews });
+ } catch (error) {
+ console.error('Failed to check server views:', error);
+ }
+ }
+ }
+
+ async migrateToServer() {
+ if (!this.viewPreferencesManager) {
+ this.showNote('ViewPreferencesManager not initialized');
+ return;
+ }
+
+ if (!this.viewPreferencesManager.serverStorage.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.`);
+ this.setState({ hasServerViews: true });
+ } 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 (!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 +452,52 @@ export default class DashboardSettings extends DashboardView {
}
/>
+ {this.viewPreferencesManager && this.viewPreferencesManager.serverStorage.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 18e4ab02bc..318c07fe87 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 0000000000..170da513ee
--- /dev/null
+++ b/src/lib/ServerConfigStorage.js
@@ -0,0 +1,180 @@
+/*
+ * 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';
+ }
+
+ /**
+ * 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) {
+ const configObject = new Parse.Object(this.className);
+
+ // Set the fields according to the schema
+ 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