diff --git a/docs/en_US/release_notes_8_5.rst b/docs/en_US/release_notes_8_5.rst index 4166bee59..f3856c303 100644 --- a/docs/en_US/release_notes_8_5.rst +++ b/docs/en_US/release_notes_8_5.rst @@ -29,7 +29,10 @@ Housekeeping Bug fixes ********* + | `Issue #7116 `_ - Bug fixes and improvements in pgAdmin CLI. + | `Issue #7165 `_ - Fixed schema diff wrong query generation for table, foreign table and sequence. | `Issue #7229 `_ - Fix an issue in table dialog where changing column name was not syncing table constraints appropriately. | `Issue #7262 `_ - Fix an issue in editor where replace option in query tool edit menu is not working on non-Mac OS. | `Issue #7268 `_ - Fix an issue in editor where Format SQL shortcut and multiline selection are not working. | `Issue #7269 `_ - Fix an issue in editor where "Use Spaces?" Preference of Editor is not working. + | `Issue #7277 `_ - Fix an issue in query tool where toggle case of selected text loses selection. diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/CustomEditorView.js b/web/pgadmin/static/js/components/ReactCodeMirror/CustomEditorView.js index c11eb4bb5..d622342d0 100644 --- a/web/pgadmin/static/js/components/ReactCodeMirror/CustomEditorView.js +++ b/web/pgadmin/static/js/components/ReactCodeMirror/CustomEditorView.js @@ -1,7 +1,7 @@ import { EditorView } from '@codemirror/view'; -import { StateEffect, EditorState } from '@codemirror/state'; +import { StateEffect, EditorState, EditorSelection } from '@codemirror/state'; import { autocompletion } from '@codemirror/autocomplete'; import {undo, indentMore, indentLess, toggleComment} from '@codemirror/commands'; import { errorMarkerEffect } from './extensions/errorMarker'; @@ -49,7 +49,10 @@ export default class CustomEditorView extends EditorView { } replaceSelection(newValue) { - this.dispatch(this.state.replaceSelection(newValue)); + this.dispatch(this.state.changeByRange(range => ({ + changes: { from: range.from, to: range.to, insert: newValue }, + range: EditorSelection.range(range.from, range.to) + }))); } getCursor() { diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx b/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx index 765e63056..bc8eae488 100644 --- a/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx +++ b/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx @@ -110,7 +110,6 @@ const defaultExtensions = [ EditorState.allowMultipleSelections.of(true), indentOnInput(), syntaxHighlighting, - keymap.of([defaultKeymap, closeBracketsKeymap, historyKeymap, foldKeymap, completionKeymap].flat()), keymap.of([{ key: 'Tab', preventDefault: true, @@ -147,6 +146,8 @@ export default function Editor({ const preferencesStore = usePreferences(); const editable = !disabled; + + const shortcuts = useRef(new Compartment()); const configurables = useRef(new Compartment()); const editableConfig = useRef(new Compartment()); @@ -168,13 +169,14 @@ export default function Editor({ icon.innerHTML = arrowRightHtml; } return icon; - } + }, })); } if (editorContainerRef.current) { const state = EditorState.create({ extensions: [ ...finalExtns, + shortcuts.current.of([]), configurables.current.of([]), editableConfig.current.of([ EditorView.editable.of(!disabled), @@ -209,7 +211,6 @@ export default function Editor({ }), breakpoint ? breakpointGutter : [], showActiveLine ? highlightActiveLine() : activeLineExtn(), - keymap.of(customKeyMap??[]), ], }); @@ -243,6 +244,13 @@ export default function Editor({ } }, [value]); + useEffect(()=>{ + const keys = keymap.of([customKeyMap??[], defaultKeymap, closeBracketsKeymap, historyKeymap, foldKeymap, completionKeymap].flat()); + editor.current?.dispatch({ + effects: shortcuts.current.reconfigure(keys) + }); + }, [customKeyMap]); + useEffect(() => { let pref = preferencesStore.getPreferencesForModule('sqleditor'); let newConfigExtn = []; diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx b/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx index 63280721f..dc67f78c7 100644 --- a/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx +++ b/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx @@ -90,7 +90,7 @@ export default function CodeMirror({className, currEditor, showCopyBtn=false, cu setShowGoto(true); }, }, - ...customKeyMap], []); + ...customKeyMap], [customKeyMap]); const closeFind = () => { setShowFind([false, false]); diff --git a/web/pgadmin/static/js/utils.js b/web/pgadmin/static/js/utils.js index 1846a5c5b..230b2a403 100644 --- a/web/pgadmin/static/js/utils.js +++ b/web/pgadmin/static/js/utils.js @@ -14,6 +14,7 @@ import convert from 'convert-units'; import getApiInstance from './api_instance'; import usePreferences from '../../preferences/static/js/store'; import pgAdmin from 'sources/pgadmin'; +import { isMac } from './keyboard_shortcuts'; export function parseShortcutValue(obj) { let shortcut = ''; @@ -27,6 +28,37 @@ export function parseShortcutValue(obj) { return shortcut; } +export function isShortcutValue(obj) { + if(!obj) return false; + if([obj.alt, obj.control, obj?.key, obj?.key?.char].every((k)=>!_.isUndefined(k))){ + return true; + } + return false; +} + +// Convert shortcut obj to codemirror key format +export function toCodeMirrorKey(obj) { + let shortcut = ''; + if (!obj){ + return shortcut; + } + if (obj.alt) { shortcut += 'Alt-'; } + if (obj.shift) { shortcut += 'Shift-'; } + if (obj.control) { + if(isMac() && obj.ctrl_is_meta) { + shortcut += 'Mod-'; + } else { + shortcut += 'Ctrl-'; + } + } + if(obj?.key.char?.length == 1) { + shortcut += obj?.key.char?.toLowerCase(); + } else { + shortcut += obj?.key.char; + } + return shortcut; +} + export function getEpoch(inp_date) { let date_obj = inp_date ? inp_date : new Date(); return parseInt(date_obj.getTime()/1000); diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx index 51ffe1441..e3f5aaefd 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx @@ -79,13 +79,85 @@ function onBeforeUnload(e) { e.preventDefault(); e.returnValue = 'prevent'; } + +const FIXED_PREF = { + find: { + 'control': true, + ctrl_is_meta: true, + 'shift': false, + 'alt': false, + 'key': { + 'key_code': 70, + 'char': 'F', + }, + }, + replace: { + 'control': true, + ctrl_is_meta: true, + 'shift': false, + 'alt': true, + 'key': { + 'key_code': 70, + 'char': 'F', + }, + }, + gotolinecol: { + 'control': true, + ctrl_is_meta: true, + 'shift': false, + 'alt': false, + 'key': { + 'key_code': 76, + 'char': 'L', + }, + }, + indent: { + 'control': false, + 'shift': false, + 'alt': false, + 'key': { + 'key_code': 9, + 'char': 'Tab', + }, + }, + unindent: { + 'control': false, + 'shift': true, + 'alt': false, + 'key': { + 'key_code': 9, + 'char': 'Tab', + }, + }, + comment: { + 'control': true, + ctrl_is_meta: true, + 'shift': false, + 'alt': false, + 'key': { + 'key_code': 191, + 'char': '/', + }, + }, + format_sql: { + 'control': true, + ctrl_is_meta: true, + 'shift': false, + 'alt': false, + 'key': { + 'key_code': 75, + 'char': 'k', + }, + }, +}; + export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedNodeInfo, qtPanelDocker, qtPanelId, eventBusObj}) { const containerRef = React.useRef(null); const preferencesStore = usePreferences(); const [qtState, _setQtState] = useState({ preferences: { browser: preferencesStore.getPreferencesForModule('browser'), - sqleditor: preferencesStore.getPreferencesForModule('sqleditor'), + sqleditor: {...preferencesStore.getPreferencesForModule('sqleditor'), ...FIXED_PREF}, graphs: preferencesStore.getPreferencesForModule('graphs'), misc: preferencesStore.getPreferencesForModule('misc'), }, @@ -362,7 +434,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN state => { setQtState({preferences: { browser: state.getPreferencesForModule('browser'), - sqleditor: state.getPreferencesForModule('sqleditor'), + sqleditor: {...state.getPreferencesForModule('sqleditor'), ...FIXED_PREF}, graphs: state.getPreferencesForModule('graphs'), misc: state.getPreferencesForModule('misc'), }}); diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx index 4257ebc97..f77c45671 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx @@ -48,87 +48,6 @@ const useStyles = makeStyles((theme)=>({ }, })); -const FIXED_PREF = { - find: { - 'control': true, - ctrl_is_meta: true, - 'shift': false, - 'alt': false, - 'key': { - 'key_code': 70, - 'char': 'F', - }, - }, - replace: { - 'control': true, - ctrl_is_meta: true, - 'shift': false, - 'alt': true, - 'key': { - 'key_code': 70, - 'char': 'F', - }, - }, - gotolinecol: { - 'control': true, - ctrl_is_meta: true, - 'shift': false, - 'alt': false, - 'key': { - 'key_code': 76, - 'char': 'L', - }, - }, - indent: { - 'control': false, - 'shift': false, - 'alt': false, - 'key': { - 'key_code': 9, - 'char': 'Tab', - }, - }, - unindent: { - 'control': false, - 'shift': true, - 'alt': false, - 'key': { - 'key_code': 9, - 'char': 'Tab', - }, - }, - comment: { - 'control': true, - ctrl_is_meta: true, - 'shift': false, - 'alt': false, - 'key': { - 'key_code': 191, - 'char': '/', - }, - }, - uncomment: { - 'control': true, - ctrl_is_meta: true, - 'shift': false, - 'alt': false, - 'key': { - 'key_code': 190, - 'char': '.', - }, - }, - format_sql: { - 'control': true, - ctrl_is_meta: true, - 'shift': false, - 'alt': false, - 'key': { - 'key_code': 75, - 'char': 'k', - }, - }, -}; - function autoCommitRollback(type, api, transId, value) { let url = url_for(`sqleditor.${type}`, { 'trans_id': transId, @@ -451,7 +370,7 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) { } }, { - shortcut: FIXED_PREF.format_sql, + shortcut: queryToolPref.format_sql, options: { callback: ()=>{formatSQL();} } @@ -596,25 +515,25 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) { onClose={onMenuClose} label={gettext('Edit Menu')} > - {eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, false);}}>{gettext('Find')} - {eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, true);}}>{gettext('Replace')} - {eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'gotoLineCol');}}>{gettext('Go to Line/Column')} - {eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'indentMore');}}>{gettext('Indent Selection')} - {eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'indentLess');}}>{gettext('Unindent Selection')} - {eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'toggleComment');}}>{gettext('Toggle Comment')} {gettext('Toggle Case Of Selected Text')} {gettext('Clear Query')} - {gettext('Format SQL')} + {gettext('Format SQL')} { let errorLineNo = 0, startMarker = 0, @@ -255,38 +257,6 @@ export default function Query() { eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_QUERY_CHANGE, ()=>{ change(); }); - eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL, ()=>{ - let selection = true, sql = editor.current?.getSelection(); - let sqlEditorPref = preferencesStore.getPreferencesForModule('sqleditor'); - /* New library does not support capitalize casing - so if a user has set capitalize casing we will - use preserve casing which is default for the library. - */ - let formatPrefs = { - language: 'postgresql', - keywordCase: sqlEditorPref.keyword_case === 'capitalize' ? 'preserve' : sqlEditorPref.keyword_case, - identifierCase: sqlEditorPref.identifier_case === 'capitalize' ? 'preserve' : sqlEditorPref.identifier_case, - dataTypeCase: sqlEditorPref.data_type_case, - functionCase: sqlEditorPref.function_case, - logicalOperatorNewline: sqlEditorPref.logical_operator_new_line, - expressionWidth: sqlEditorPref.expression_width, - linesBetweenQueries: sqlEditorPref.lines_between_queries, - tabWidth: sqlEditorPref.tab_size, - useTabs: !sqlEditorPref.use_spaces, - denseOperators: !sqlEditorPref.spaces_around_operators, - newlineBeforeSemicolon: sqlEditorPref.new_line_before_semicolon - }; - if(sql == '') { - sql = editor.current.getValue(); - selection = false; - } - let formattedSql = format(sql,formatPrefs); - if(selection) { - editor.current.replaceSelection(formattedSql, 'around'); - } else { - editor.current.setValue(formattedSql); - } - }); eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_TOGGLE_CASE, ()=>{ let selectedText = editor.current?.getSelection(); if (!selectedText) return; @@ -334,7 +304,46 @@ export default function Query() { /> )); }; - return eventBus.registerListener(QUERY_TOOL_EVENTS.WARN_SAVE_TEXT_CLOSE, warnSaveTextClose); + + const formatSQL = ()=>{ + let selection = true, sql = editor.current?.getSelection(); + /* New library does not support capitalize casing + so if a user has set capitalize casing we will + use preserve casing which is default for the library. + */ + let formatPrefs = { + language: 'postgresql', + keywordCase: queryToolPref.keyword_case === 'capitalize' ? 'preserve' : queryToolPref.keyword_case, + identifierCase: queryToolPref.identifier_case === 'capitalize' ? 'preserve' : queryToolPref.identifier_case, + dataTypeCase: queryToolPref.data_type_case, + functionCase: queryToolPref.function_case, + logicalOperatorNewline: queryToolPref.logical_operator_new_line, + expressionWidth: queryToolPref.expression_width, + linesBetweenQueries: queryToolPref.lines_between_queries, + tabWidth: queryToolPref.tab_size, + useTabs: !queryToolPref.use_spaces, + denseOperators: !queryToolPref.spaces_around_operators, + newlineBeforeSemicolon: queryToolPref.new_line_before_semicolon + }; + if(sql == '') { + sql = editor.current.getValue(); + selection = false; + } + let formattedSql = format(sql,formatPrefs); + if(selection) { + editor.current.replaceSelection(formattedSql, 'around'); + } else { + editor.current.setValue(formattedSql); + } + }; + + const unregisterFormatSQL = eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL, formatSQL); + const unregisterWarn = eventBus.registerListener(QUERY_TOOL_EVENTS.WARN_SAVE_TEXT_CLOSE, warnSaveTextClose); + + return ()=>{ + unregisterFormatSQL(); + unregisterWarn(); + }; }, [queryToolCtx.preferences]); useEffect(()=>{ @@ -400,6 +409,33 @@ export default function Query() { } }; + const shortcutOverrideKeys = useMemo( + ()=>{ + const queryToolPref = queryToolCtx.preferences.sqleditor; + return Object.values(queryToolPref) + .filter((p)=>isShortcutValue(p)) + .map((p)=>({ + key: toCodeMirrorKey(p), run: (_v, e)=>{ + queryToolCtx.mainContainerRef?.current?.dispatchEvent(new KeyboardEvent('keydown', { + which: e.which, + keyCode: e.keyCode, + altKey: e.altKey, + shiftKey: e.shiftKey, + ctrlKey: e.ctrlKey, + metaKey: e.metaKey, + })); + if(toCodeMirrorKey(p) == 'Mod-k') { + eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL); + } + return true; + }, + preventDefault: true, + stopPropagation: true, + })); + }, + [queryToolCtx.preferences] + ); + return { editor.current=obj; @@ -409,14 +445,6 @@ export default function Query() { onCursorActivity={cursorActivity} onChange={change} autocomplete={true} - customKeyMap={[ - { - key: 'Mod-k', run: (_view, e) => { - e.preventDefault(); - e.stopPropagation(); - eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL); - }, - } - ]} + customKeyMap={shortcutOverrideKeys} />; }