Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/en_US/keyboard_shortcuts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ When using main browser window, the following keyboard shortcuts are available:
+----------------------------+--------------------+------------------------------------+
| Shift + Alt + b | Shift + Option + b | Object explorer |
+----------------------------+--------------------+------------------------------------+
| Ctrl + Alt + b | Ctrl + Option + b | Toggle Object Explorer |
+----------------------------+--------------------+------------------------------------+
| Shift + Alt + n | Shift + Option + n | Create object |
+----------------------------+--------------------+------------------------------------+
| Shift + Alt + d | Shift + Option + d | Delete object |
Expand Down
15 changes: 15 additions & 0 deletions web/pgadmin/browser/register_browser_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,21 @@ def register_browser_preferences(self):
fields=fields
)

self.preference.register(
'keyboard_shortcuts',
'toggle_object_explorer',
gettext('Toggle Object Explorer'),
'keyboardshortcut',
{
'alt': True,
'shift': False,
'control': True,
'key': {'key_code': 66, 'char': 'b'}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=fields
)

self.preference.register(
'keyboard_shortcuts',
'tabbed_panel_backward',
Expand Down
4 changes: 4 additions & 0 deletions web/pgadmin/browser/static/js/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,7 @@ export const AllPermissionTypes = {
// Fired on pgAdmin.Browser.Events when code outside React, e.g. the keyboard
// shortcut that focuses the tree, needs the Object Explorer on screen.
export const SHOW_OBJECT_EXPLORER_EVENT = 'pgadmin:object-explorer:show';

// Fired on pgAdmin.Browser.Events to collapse or restore the Object Explorer,
// the same thing clicking the current workspace icon does.
export const TOGGLE_OBJECT_EXPLORER_EVENT = 'pgadmin:object-explorer:toggle';
9 changes: 8 additions & 1 deletion web/pgadmin/browser/static/js/keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import * as commonUtils from '../../../static/js/utils';
import gettext from 'sources/gettext';
import pgWindow from 'sources/window';
import usePreferences from '../../../preferences/static/js/store';
import { SHOW_OBJECT_EXPLORER_EVENT } from './constants';
import { SHOW_OBJECT_EXPLORER_EVENT, TOGGLE_OBJECT_EXPLORER_EVENT }
from './constants';


const pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {};
Expand All @@ -41,6 +42,7 @@ _.extend(pgBrowser.keyboardNavigation, {
...(prefStore.getPreferences('browser', 'main_menu_tools')?.value) && {'tools_shortcut': commonUtils.parseShortcutValue(prefStore.getPreferences('browser', 'main_menu_tools')?.value)},
...(prefStore.getPreferences('browser', 'main_menu_help')?.value) && {'help_shortcut': commonUtils.parseShortcutValue(prefStore.getPreferences('browser', 'main_menu_help')?.value)},
'left_tree_shortcut': commonUtils.parseShortcutValue(prefStore.getPreferences('browser', 'browser_tree')?.value),
'toggle_object_explorer': commonUtils.parseShortcutValue(prefStore.getPreferences('browser', 'toggle_object_explorer')?.value),
'tabbed_panel_backward': commonUtils.parseShortcutValue(prefStore.getPreferences('browser', 'tabbed_panel_backward')?.value),
'tabbed_panel_forward': commonUtils.parseShortcutValue(prefStore.getPreferences('browser', 'tabbed_panel_forward')?.value),
'sub_menu_query_tool': commonUtils.parseShortcutValue(prefStore.getPreferences('browser', 'sub_menu_query_tool')?.value),
Expand All @@ -64,6 +66,7 @@ _.extend(pgBrowser.keyboardNavigation, {
}}, // Main menu
'bindRightPanel': {'shortcuts': [this.keyboardShortcut.tabbed_panel_backward, this.keyboardShortcut.tabbed_panel_forward, this.keyboardShortcut.close_tab_panel]}, // Main window panels
'bindLeftTree': {'shortcuts': this.keyboardShortcut.left_tree_shortcut}, // Main menu,
'bindToggleObjectExplorer': {'shortcuts': this.keyboardShortcut.toggle_object_explorer}, // Collapse/restore the Object Explorer
'bindSubMenuQueryTool': {'shortcuts': this.keyboardShortcut.sub_menu_query_tool}, // Sub menu - Open Query Tool,
'bindSubMenuViewData': {'shortcuts': this.keyboardShortcut.sub_menu_view_data}, // Sub menu - Open View Data,
'bindSubMenuSearchObjects': {'shortcuts': this.keyboardShortcut.sub_menu_search_objects}, // Sub menu - Open search objects,
Expand Down Expand Up @@ -213,6 +216,10 @@ _.extend(pgBrowser.keyboardNavigation, {
tree.t.select(tree.i);
}, 0);
},
bindToggleObjectExplorer: function() {
// The provider owns the visibility state; it listens for this.
pgAdmin.Browser.Events.trigger(TOGGLE_OBJECT_EXPLORER_EVENT);
},
bindSubMenuQueryTool: function() {
const tree = this.getTreeDetails();

Expand Down
12 changes: 11 additions & 1 deletion web/pgadmin/misc/workspaces/static/js/WorkspaceProvider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@

import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { BROWSER_PANELS, SHOW_OBJECT_EXPLORER_EVENT, WORKSPACES }
import { BROWSER_PANELS, SHOW_OBJECT_EXPLORER_EVENT,
TOGGLE_OBJECT_EXPLORER_EVENT, WORKSPACES }
from '../../../../browser/static/js/constants';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
import usePreferences from '../../../../preferences/static/js/store';
Expand Down Expand Up @@ -171,6 +172,15 @@ export function WorkspaceProvider({children}) {
SHOW_OBJECT_EXPLORER_EVENT, ()=>setObjectExplorerVisible(true));
}, [pgAdmin, setObjectExplorerVisible]);

useEffect(()=>{
// Classic layout has no workspace toolbar and always shows the Object
// Explorer, so the shortcut has nothing to collapse there.
if(isClassic) return;

return pgAdmin.Browser.Events.registerListener(
TOGGLE_OBJECT_EXPLORER_EVENT, toggleObjectExplorer);
}, [pgAdmin, isClassic, toggleObjectExplorer]);

const value = useMemo(()=>({
config: config,
currentWorkspace: currentWorkspace,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////

// keyboard.js reaches pgadmin.js by relative path, which skips the
// sources/pgadmin alias that maps to the fake, so point it there explicitly.
jest.mock('../../../pgadmin/static/js/pgadmin', () =>
jest.requireActual('../fake_pgadmin'));

import pgAdmin from 'sources/pgadmin';
import { TOGGLE_OBJECT_EXPLORER_EVENT } from
'../../../pgadmin/browser/static/js/constants';
import '../../../pgadmin/browser/static/js/keyboard';

/* The shortcut does not own the visibility state, the workspace provider
* does, so all it has to do is say what the user asked for. */
describe('keyboardNavigation.bindToggleObjectExplorer', () => {
it('asks the workspace provider to toggle the Object Explorer',
async () => {
const listener = jest.fn();
const dereg = pgAdmin.Browser.Events.registerListener(
TOGGLE_OBJECT_EXPLORER_EVENT, listener);

try {
pgAdmin.Browser.keyboardNavigation.bindToggleObjectExplorer();
// EventBus dispatches through a couple of microtasks, so wait for a
// macrotask to be sure they have all run.
await new Promise((resolve) => setTimeout(resolve, 0));

expect(listener).toHaveBeenCalled();
} finally {
dereg();
}
});
});
29 changes: 28 additions & 1 deletion web/regression/javascript/workspaces/WorkspaceProvider.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import { act, render } from '@testing-library/react';

import { SHOW_OBJECT_EXPLORER_EVENT } from
import { SHOW_OBJECT_EXPLORER_EVENT, TOGGLE_OBJECT_EXPLORER_EVENT } from
'../../../pgadmin/browser/static/js/constants';
import { WorkspaceProvider, useWorkspace } from
'../../../pgadmin/misc/workspaces/static/js/WorkspaceProvider';
Expand All @@ -19,6 +19,18 @@ import getApiInstance from '../../../pgadmin/static/js/api_instance';

jest.mock('../../../pgadmin/static/js/api_instance');

// The workspace layout, since that is the only layout with a workspace
// toolbar and therefore the only one where the Object Explorer can be
// collapsed at all.
jest.mock('../../../pgadmin/preferences/static/js/store', () => {
const store = () => ({
getPreferencesForModule: () => ({layout: 'workspace'}),
});
store.subscribe = () => () => {/* nothing to unsubscribe */};
store.getState = () => ({});
return {__esModule: true, default: store};
});

// The Object Explorer can be collapsed, and that choice is persisted, so the
// visibility state has to survive a reload and the writes must not be able to
// land out of order (#9631).
Expand Down Expand Up @@ -97,6 +109,21 @@ describe('WorkspaceProvider Object Explorer visibility', () => {
expect(values).toEqual(['false', 'true']);
});

it('toggles when asked to by code outside React', async () => {
await renderProvider();
expect(workspace.isObjectExplorerVisible).toBe(true);

await act(async () => {
pgAdmin.Browser.Events.trigger(TOGGLE_OBJECT_EXPLORER_EVENT);
});
expect(workspace.isObjectExplorerVisible).toBe(false);

await act(async () => {
pgAdmin.Browser.Events.trigger(TOGGLE_OBJECT_EXPLORER_EVENT);
});
expect(workspace.isObjectExplorerVisible).toBe(true);
});

it('shows the panel when asked to by code outside React', async () => {
pgAdmin.Browser.utils.layout['Browser/ObjectExplorerVisible'] = 'false';
await renderProvider();
Expand Down
Loading