diff --git a/README.md b/README.md
index 1e30c18ac1..dc14442d6c 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,7 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
- [Prevent columns sorting](#prevent-columns-sorting)
- [Custom order in the filter popup](#custom-order-in-the-filter-popup)
- [Persistent Filters](#persistent-filters)
+ - [Keyboard Shortcuts](#keyboard-shortcuts)
- [Scripts](#scripts)
- [Resource Cache](#resource-cache)
- [Running as Express Middleware](#running-as-express-middleware)
@@ -530,6 +531,12 @@ For example:
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*.
+### Keyboard Shortcuts
+
+Configure custom keyboard shortcuts for dashboard actions in **App Settings > Keyboard Shortcuts**.
+
+Delete a shortcut key to disable the shortcut.
+
### Scripts
You can specify scripts to execute Cloud Functions with the `scripts` option:
diff --git a/src/components/TextInput/TextInput.react.js b/src/components/TextInput/TextInput.react.js
index 4aa72f3b76..c210f019f5 100644
--- a/src/components/TextInput/TextInput.react.js
+++ b/src/components/TextInput/TextInput.react.js
@@ -69,6 +69,8 @@ class TextInput extends React.Component {
value={this.props.value}
onChange={this.changeValue.bind(this)}
onBlur={this.updateValue.bind(this)}
+ onFocus={this.props.onFocus}
+ maxLength={this.props.maxLength}
/>
);
}
@@ -87,11 +89,13 @@ TextInput.propTypes = {
'A function fired when the input is changed. It receives the new value as its only parameter.'
),
onBlur: PropTypes.func.describe('A function fired when the input is blurred.'),
+ onFocus: PropTypes.func.describe('A function fired when the input is focused.'),
placeholder: PropTypes.string.describe('A placeholder string, for when the input is empty'),
value: PropTypes.string.describe('The current value of the controlled input'),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).describe(
'The height of the field. Can be a string containing any CSS unit, or a number of pixels. Default is 80px.'
),
+ maxLength: PropTypes.number.describe('The maximum length of the input.'),
};
export default withForwardedRef(TextInput);
diff --git a/src/components/TextInput/TextInput.scss b/src/components/TextInput/TextInput.scss
index 670992e39b..08e0ee9b68 100644
--- a/src/components/TextInput/TextInput.scss
+++ b/src/components/TextInput/TextInput.scss
@@ -21,6 +21,11 @@
vertical-align: top;
resize: both;
+ @include placeholder {
+ color: #999;
+ opacity: 1;
+ }
+
&:disabled {
color: $mainTextColor;
}
diff --git a/src/dashboard/Dashboard.js b/src/dashboard/Dashboard.js
index bedf0e1d78..743f715671 100644
--- a/src/dashboard/Dashboard.js
+++ b/src/dashboard/Dashboard.js
@@ -40,7 +40,6 @@ import RestConsole from './Data/ApiConsole/RestConsole.react';
import Retention from './Analytics/Retention/Retention.react';
import SchemaOverview from './Data/Browser/SchemaOverview.react';
import SecuritySettings from './Settings/SecuritySettings.react';
-import SettingsData from './Settings/SettingsData.react';
import SlowQueries from './Analytics/SlowQueries/SlowQueries.react';
import styles from 'dashboard/Apps/AppsIndex.scss';
import UsersSettings from './Settings/UsersSettings.react';
@@ -55,6 +54,7 @@ import { Helmet } from 'react-helmet';
import Playground from './Data/Playground/Playground.react';
import DashboardSettings from './Settings/DashboardSettings/DashboardSettings.react';
import Security from './Settings/Security/Security.react';
+import KeyboardShortcutsSettings from './Settings/KeyboardShortcutsSettings.react';
import semver from 'semver';
import packageInfo from '../../package.json';
@@ -232,16 +232,17 @@ export default class Dashboard extends React.Component {
);
const SettingsRoute = (
- }>
+ <>
} />
} />
+ } />
} />
} />
} />
} />
} />
} />
-
+ >
);
const JobsRoute = (
diff --git a/src/dashboard/DashboardView.react.js b/src/dashboard/DashboardView.react.js
index f438ca3a75..0b37634360 100644
--- a/src/dashboard/DashboardView.react.js
+++ b/src/dashboard/DashboardView.react.js
@@ -33,13 +33,22 @@ export default class DashboardView extends React.Component {
onRouteChanged() {
const path = this.props.location?.pathname ?? window.location.pathname;
- const route = path.split('apps')[1].split('/')[2];
+ const route = path.split('apps')[1]?.split('/')[2] || '';
if (route !== this.state.route) {
this.setState({ route });
}
}
+ getCurrentRoute() {
+ // If state.route is set, use it; otherwise extract from current location
+ if (this.state.route) {
+ return this.state.route;
+ }
+ const path = this.props.location?.pathname ?? window.location.pathname;
+ return path.split('apps')[1]?.split('/')[2] || '';
+ }
+
render() {
let sidebarChildren = null;
if (typeof this.renderSidebar === 'function') {
@@ -212,6 +221,10 @@ export default class DashboardView extends React.Component {
name: 'Dashboard',
link: '/settings/dashboard',
},
+ {
+ name: 'Keyboard Shortcuts',
+ link: '/settings/keyboard-shortcuts',
+ },
];
if (this.context.enableSecurityChecks) {
@@ -313,9 +326,10 @@ export default class DashboardView extends React.Component {
);
let content =
{this.renderContent()}
;
- const canRoute = [...coreSubsections, ...pushSubsections, ...settingsSections]
- .map(({ link }) => link.split('/')[1])
- .includes(this.state.route);
+ const allSections = [...coreSubsections, ...pushSubsections, ...settingsSections];
+ const validRoutes = allSections.map(({ link }) => link.split('/')[1]);
+ const currentRoute = this.getCurrentRoute();
+ const canRoute = validRoutes.includes(currentRoute);
if (!canRoute) {
content = (
diff --git a/src/dashboard/Data/Browser/DataBrowser.react.js b/src/dashboard/Data/Browser/DataBrowser.react.js
index ebaad2729a..54c42ec564 100644
--- a/src/dashboard/Data/Browser/DataBrowser.react.js
+++ b/src/dashboard/Data/Browser/DataBrowser.react.js
@@ -19,6 +19,7 @@ import React from 'react';
import { ResizableBox } from 'react-resizable';
import ScriptConfirmationModal from '../../../components/ScriptConfirmationModal/ScriptConfirmationModal.react';
import styles from './Databrowser.scss';
+import KeyboardShortcutsManager, { matchesShortcut } from 'lib/KeyboardShortcutsPreferences';
import AggregationPanel from '../../../components/AggregationPanel/AggregationPanel';
@@ -146,6 +147,7 @@ export default class DataBrowser extends React.Component {
multiPanelData: {}, // Object mapping objectId to panel data
_objectsToFetch: [], // Temporary field for async fetch handling
loadingObjectIds: new Set(),
+ keyboardShortcuts: null, // Keyboard shortcuts from server
showScriptConfirmationDialog: false,
selectedScript: null,
contextMenuX: null,
@@ -260,9 +262,18 @@ export default class DataBrowser extends React.Component {
this.checkClassNameChange(this.state.prevClassName, props.className);
}
- componentDidMount() {
+ async componentDidMount() {
document.body.addEventListener('keydown', this.handleKey);
window.addEventListener('resize', this.updateMaxWidth);
+
+ // Load keyboard shortcuts from server
+ try {
+ const manager = new KeyboardShortcutsManager(this.props.app);
+ const shortcuts = await manager.getKeyboardShortcuts(this.props.app.applicationId);
+ this.setState({ keyboardShortcuts: shortcuts });
+ } catch (error) {
+ console.warn('Failed to load keyboard shortcuts:', error);
+ }
}
componentWillUnmount() {
@@ -849,6 +860,34 @@ export default class DataBrowser extends React.Component {
}
break;
}
+ default: {
+ // Handle custom keyboard shortcuts from server
+ const shortcuts = this.state.keyboardShortcuts;
+ if (!shortcuts) {
+ break;
+ }
+
+ // Reload data shortcut (only if enabled)
+ if (matchesShortcut(e, shortcuts.dataBrowserReloadData)) {
+ this.handleRefresh();
+ e.preventDefault();
+ break;
+ }
+
+ // Toggle panels shortcut (only if enabled and class has info panels configured)
+ if (matchesShortcut(e, shortcuts.dataBrowserToggleInfoPanels)) {
+ const hasAggregation =
+ this.props.classwiseCloudFunctions?.[
+ `${this.props.app.applicationId}${this.props.appName}`
+ ]?.[this.props.className];
+ if (hasAggregation) {
+ this.togglePanelVisibility();
+ e.preventDefault();
+ }
+ break;
+ }
+ break;
+ }
}
}
diff --git a/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js b/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js
index c7d8f11aaf..52001e00af 100644
--- a/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js
+++ b/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js
@@ -474,7 +474,7 @@ export default class DashboardSettings extends DashboardView {
{this.viewPreferencesManager && this.scriptManager && this.viewPreferencesManager.isServerConfigEnabled() && (