Skip to content
Merged
131 changes: 85 additions & 46 deletions src/dashboard/Data/Views/Views.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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: {},
Expand Down Expand Up @@ -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);
}
});
}
}
Comment on lines +88 to 147
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider showing user feedback on storage errors

When views fail to load from or save to server storage, errors are only logged to console. Users won't know their views weren't saved to their preferred storage location.

Consider using the existing showNote method to inform users:

     } catch (error) {
       console.error('Failed to load views from server, falling back to local storage:', error);
+      this.showNote('Failed to load views from server, using local storage', true);
       // Fallback to local storage
       const views = ViewPreferences.getViews(app.applicationId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
}
});
}
}
} catch (error) {
console.error('Failed to load views from server, falling back to local storage:', error);
+ this.showNote('Failed to load views from server, using local storage', true);
// Fallback to local storage
const views = ViewPreferences.getViews(app.applicationId);
this.setState({ views, counts: {} }, () => {
if (this._isMounted) {
this.loadData(this.props.params.name);
}
});
}
🤖 Prompt for AI Agents
In src/dashboard/Data/Views/Views.react.js around lines 88 to 147, the loadViews
method logs errors to the console when failing to load views from the server but
does not inform the user. To fix this, add a call to this.showNote with a
user-friendly error message inside the catch block that handles the server load
failure. This will notify users that their views could not be loaded from the
server and the app is falling back to local storage.


loadData(name) {
Expand Down Expand Up @@ -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);
}
);
Expand All @@ -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);
}
);
Expand All @@ -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);
Expand Down
134 changes: 134 additions & 0 deletions src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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({
Expand Down Expand Up @@ -382,6 +461,61 @@ export default class DashboardSettings extends DashboardView {
}
/>
</Fieldset>
{this.viewPreferencesManager && this.viewPreferencesManager.isServerConfigEnabled() && (
<Fieldset legend="Settings Storage">
<Field
label={
<Label
text="Storage Location"
description="Choose where your dashboard settings are stored and loaded from. Server storage allows sharing settings across devices and users, while Browser storage is local to this device."
/>
}
input={
<Toggle
value={this.state.storagePreference}
type={Toggle.Types.CUSTOM}
optionLeft="local"
optionRight="server"
labelLeft="Browser"
labelRight="Server"
colored={true}
onChange={(preference) => this.handleStoragePreferenceChange(preference)}
/>
}
/>
<Field
label={
<Label
text="Migrate Settings to Server"
description="Migrates your current browser-stored dashboard settings to the server. This does not change your storage preference - use the switch above to select the server as storage location after migration. ⚠️ This overwrites existing server settings."
/>
}
input={
<FormButton
color="blue"
value={this.state.migrationLoading ? 'Migrating...' : 'Migrate to Server'}
disabled={this.state.migrationLoading}
onClick={() => this.migrateToServer()}
/>
}
/>
<Field
label={
<Label
text="Delete Settings from Browser"
description="Removes your dashboard settings from the browser's local storage. This action is irreversible. Make sure to migrate your settings to server and test them first."
/>
}
input={
<FormButton
color="red"
value="Delete from Browser"
onClick={() => this.deleteFromBrowser()}
/>
}
/>
</Fieldset>
)}
{this.state.copyData.show && copyData}
{this.state.createUserInput && createUserInput}
{this.state.newUser.show && userData}
Expand Down
2 changes: 2 additions & 0 deletions src/lib/ParseApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -79,6 +80,7 @@ export default class ParseApp {
this.scripts = scripts;
this.enableSecurityChecks = !!enableSecurityChecks;
this.cloudConfigHistoryLimit = cloudConfigHistoryLimit;
this.config = config;

if (!supportedPushLocales) {
console.warn(
Expand Down
Loading
Loading