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
6 changes: 4 additions & 2 deletions docs/en_US/keyboard_shortcuts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ When using main browser window, the following keyboard shortcuts are available:
+----------------------------+--------------------+------------------------------------+
| Shift + Alt + s | Shift + Option + s | Search objects |
+----------------------------+--------------------+------------------------------------+
| Shift + Alt + [ | Shift + Option + [ | Tabbed panel backward |
| Ctrl + Alt + [ | Ctrl + Option + [ | Tabbed panel backward |
+----------------------------+--------------------+------------------------------------+
| Shift + Alt + ] | Shift + Option + ] | Tabbed panel forward |
| Ctrl + Alt + ] | Ctrl + Option + ] | Tabbed panel forward |
+----------------------------+--------------------+------------------------------------+
| Shift + Alt + w | Shift + Ctrl + w | Close tab panel |
+----------------------------+--------------------+------------------------------------+
Expand Down Expand Up @@ -98,6 +98,8 @@ When using the syntax-highlighting SQL editors, the following shortcuts are avai
+--------------------------+----------------------+-------------------------------------+
| Ctrl + / | Cmd + / | Comment/Uncomment code (Block) |
+--------------------------+----------------------+-------------------------------------+
| Ctrl + Shift + d | Cmd + Shift + d | Duplicate current line/selection |
+--------------------------+----------------------+-------------------------------------+
| Ctrl + a | Cmd + a | Select all |
+--------------------------+----------------------+-------------------------------------+
| Ctrl + c | Cmd + c | Copy selected text to the clipboard |
Expand Down
12 changes: 6 additions & 6 deletions web/pgadmin/browser/register_browser_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,9 @@ def register_browser_preferences(self):
'keyboardshortcut',
{
'alt': True,
'shift': True,
'control': False,
'key': {'key_code': 91, 'char': '['}
'shift': False,
'control': True,
'key': {'key_code': 219, 'char': '['}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=fields
Expand Down Expand Up @@ -200,9 +200,9 @@ def register_browser_preferences(self):
'keyboardshortcut',
{
'alt': True,
'shift': True,
'control': False,
'key': {'key_code': 93, 'char': ']'}
'shift': False,
'control': True,
'key': {'key_code': 221, 'char': ']'}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=fields
Expand Down
71 changes: 39 additions & 32 deletions web/pgadmin/browser/static/js/keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,42 +150,49 @@ _.extend(pgBrowser.keyboardNavigation, {
bindRightPanel: function(event, combo) {
const self = this;
const shortcutObj = this.keyboardShortcut;
const activeElement = document.activeElement;
const rootDock = document.getElementById('root');
if (!rootDock) return;

if (activeElement.closest('.dock-tab-btn')) {
const currDockTab = activeElement.closest('.dock-tab-btn');
const dockLayout = currDockTab.closest('.dock-layout');
const dockLayoutTabs = dockLayout ? Array.from(dockLayout.querySelectorAll('.dock-tab-btn')) : null;
// Find the active workspace tab independently of where the keyboard focus
// currently sits. Previously this relied on document.activeElement, which
// breaks when focus is inside a tool's own (nested) dock layout - the SQL
// editor, an ERD/Schema Diff canvas - or inside the PSQL iframe, because
// the resolved tab id then belonged to the tool's inner tab rather than a
// main workspace tab (issue #7232).
//
// rc-dock renders the object explorer and the workspace as separate
// tab-sets, and each marks its own active tab with `dock-tab-active`, so
// prefer the active tab that is not the object explorer.
//
// Only the outermost dock layout is considered. The SQL editor, ERD and
// Schema Diff each render a DockLayout of their own inside a workspace
// tab, and their active tabs (Data Output, Messages, Notifications and
// so on) carry the same `dock-tab-active` class, so a search across the
// whole tree can land on one of those and cycle a tool's inner tabs
// instead of the workspace tabs it was asked to move between.
const topDockLayout = rootDock.querySelector('.dock-layout');
if (!topDockLayout) return;

if (dockLayoutTabs && dockLayoutTabs.length > 1) {
const activeTabIndex = dockLayoutTabs.indexOf(currDockTab);
self._focusTab(dockLayoutTabs, activeTabIndex, shortcutObj, combo);
}
}
else if (activeElement.nodeName === 'IFRAME' || activeElement.closest('.dock-tabpane.dock-tabpane-active')) {
let activeTabId = '';
activeTabId = (activeElement.nodeName === 'IFRAME') ? activeElement.id : activeElement.closest('.dock-tabpane.dock-tabpane-active').id;
const dockLayout = document.getElementById('root');
const dockLayoutTabs = dockLayout ? Array.from(dockLayout.querySelectorAll('.dock-tab-btn')) : null;
const activeTabBtns = Array.from(
topDockLayout.querySelectorAll('.dock-tab.dock-tab-active .dock-tab-btn')
).filter(tab => tab.closest('.dock-layout') === topDockLayout);
const activeTabBtn =
activeTabBtns.find(tab => !tab.id.includes('id-object-explorer')) ||
activeTabBtns[0];
if (!activeTabBtn) return;

if (dockLayoutTabs && dockLayoutTabs.length > 1 && activeTabId) {
const activeTabIndex = dockLayoutTabs.findIndex(tab => tab.id.slice(14) === activeTabId);
self._focusTab(dockLayoutTabs, activeTabIndex, shortcutObj, combo);
}
}
else if (activeElement === document.body || document.querySelector('div[data-test="app-menu-bar"]')) {
const activeTabs = document.getElementsByClassName('dock-tabpane dock-tabpane-active');
// Restrict navigation to the tabs of the same tab-set (dock panel) as the
// active tab, so cycling stays within the workspace tabs and does not
// include the object explorer or a tool's nested tabs.
const panel = activeTabBtn.closest('.dock-panel');
const dockLayoutTabs = panel ? Array.from(
panel.querySelectorAll('.dock-tab-btn'))
.filter(tab => tab.closest('.dock-panel') === panel &&
tab.closest('.dock-layout') === topDockLayout) : [];

if (activeTabs.length > 1) {
const activeTabId = activeTabs[1].id;
const dockLayout = document.getElementById('root');
const dockLayoutTabs = dockLayout ? Array.from(dockLayout.querySelectorAll('.dock-tab-btn')) : null;

if (dockLayoutTabs && dockLayoutTabs.length > 1 && activeTabId) {
const activeTabIndex = dockLayoutTabs.findIndex(tab => tab.id.slice(14) === activeTabId);
self._focusTab(dockLayoutTabs, activeTabIndex, shortcutObj, combo);
}
}
if (dockLayoutTabs.length > 1) {
const activeTabIndex = dockLayoutTabs.indexOf(activeTabBtn);
self._focusTab(dockLayoutTabs, activeTabIndex, shortcutObj, combo);
}
},
_focusTab: function(dockLayoutTabs, activeTabIdx, shortcut_obj, combo){
Expand Down
49 changes: 42 additions & 7 deletions web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export default function SchemaDialogView({
);
};

const save = (changeData) => {
const save = (changeData, onSaved) => {
props.onSave(schemaState.isNew, changeData)
.then(()=>{
if(schema.informText) {
Expand All @@ -106,6 +106,7 @@ export default function SchemaDialogView({
schema.informText,
);
}
onSaved?.();
}).catch((err)=>{
schemaState.setError({
name: 'apierror',
Expand All @@ -119,7 +120,11 @@ export default function SchemaDialogView({
});
};

const onSaveClick = () => {
// closeOnSave is only ever passed explicitly as true, by the Ctrl/Cmd+Enter
// handler below. The Save button's onClick passes its click event instead,
// which is never === true, so a plain Save click keeps its existing
// behaviour (some dialogs, e.g. object properties, stay open after Save).
const onSaveClick = (closeOnSave) => {
// Do nothing when there is no change or there is an error
if (
!schemaState._changes || Object.keys(schemaState._changes).length === 0 ||
Expand All @@ -129,15 +134,17 @@ export default function SchemaDialogView({
setSaving(true);
setLoaderText(schemaState.customLoadingText || gettext('Saving...'));

const onSaved = closeOnSave === true ? () => props.onClose?.() : undefined;

if (!schema.warningText) {
save(schemaState.changes(true));
save(schemaState.changes(true), onSaved);
return;
}

Notifier.confirm(
gettext('Warning'),
schema.warningText,
() => { save(schemaState.changes(true)); },
() => { save(schemaState.changes(true), onSaved); },
() => {
setSaving(false);
setLoaderText('');
Expand Down Expand Up @@ -177,9 +184,35 @@ export default function SchemaDialogView({
return <SaveIcon />;
};

const onKeyDown = (e) => {
// Ctrl/Cmd+Enter saves and closes the dialog from anywhere within it
// (issue #7167). onSaveClick is a no-op when there is nothing to save or
// there is a validation error, so this is safe to call unconditionally.
if ((e.ctrlKey || e.metaKey) && !e.altKey && e.key === 'Enter') {
e.preventDefault();
onSaveClick(true);
return;
}

// Escape closes the dialog, mirroring the Close button (issue #5691).
// This is needed for dialogs rendered as dockable panels (Properties,
// Backup, and other utility dialogs); dialogs rendered inside a MUI modal
// already close on Escape, so skip those to avoid a double close. The
// !e.defaultPrevented guard lets an inner control that handles Escape
// (e.g. an open dropdown) consume it first.
if (e.key === 'Escape' && !e.defaultPrevented && props.onClose &&
!e.currentTarget.closest('.MuiDialog-root')) {
e.preventDefault();
props.onClose();
}
};

/* I am Groot */
return useMemo(() =>
<StyledBox>
// Only the children are memoized: the wrapper carries onKeyDown, which
// closes over props.onClose and onSaveClick, and memoizing it would pin
// whichever versions of those existed when the deps last changed.
const dialogContent = useMemo(() =>
<>
<SchemaStateContext.Provider value={schemaState}>
<Box className='Dialog-form'>
<FormLoader/>
Expand Down Expand Up @@ -234,8 +267,10 @@ export default function SchemaDialogView({
</Box>
}
</SchemaStateContext.Provider>
</StyledBox>, [schema._id, viewHelperProps.mode, resetKey]
</>, [schema._id, viewHelperProps.mode, resetKey]
);

return <StyledBox onKeyDown={onKeyDown}>{dialogContent}</StyledBox>;
}

SchemaDialogView.propTypes = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
keymap,
} from '@codemirror/view';
import { EditorState, Compartment } from '@codemirror/state';
import { history, defaultKeymap, historyKeymap, indentLess, indentMore, deleteCharBackwardStrict } from '@codemirror/commands';
import { history, defaultKeymap, historyKeymap, indentLess, indentMore, deleteCharBackwardStrict, copyLineDown } from '@codemirror/commands';
import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap, acceptCompletion } from '@codemirror/autocomplete';
import {
foldGutter,
Expand Down Expand Up @@ -140,6 +140,12 @@ const defaultExtensions = [
key: 'Backspace',
preventDefault: true,
run: deleteCharBackwardStrict,
},{
// Duplicate the current line, or the selected lines if there is a
// selection (issue #3834).
key: 'Mod-Shift-d',
preventDefault: true,
run: copyLineDown,
}]),
PgSQL.language.data.of({
autocomplete: false,
Expand Down
Loading
Loading