diff --git a/docs/en_US/query_tool.rst b/docs/en_US/query_tool.rst index 9df372c34..2f7000e3e 100644 --- a/docs/en_US/query_tool.rst +++ b/docs/en_US/query_tool.rst @@ -96,6 +96,11 @@ and commenting: * Implement or remove SQL style or toggle C style comment notation within your code. +You can also **drag and drop** certain objects from the treeview which +can save time in typing long object names. Text containing the object name will be +fully qualified with schema. Double quotes will be added if required. +For functions and procedures, the function name along with parameter names will +be pasted in the Query Tool. The Data Output Panel ********************* diff --git a/docs/en_US/release_notes.rst b/docs/en_US/release_notes.rst index 25bab2912..e52164a16 100644 --- a/docs/en_US/release_notes.rst +++ b/docs/en_US/release_notes.rst @@ -11,6 +11,7 @@ notes for it. .. toctree:: :maxdepth: 1 + release_notes_4_10 release_notes_4_9 release_notes_4_8 release_notes_4_7 diff --git a/docs/en_US/release_notes_4_10.rst b/docs/en_US/release_notes_4_10.rst new file mode 100644 index 000000000..bbb25b6f3 --- /dev/null +++ b/docs/en_US/release_notes_4_10.rst @@ -0,0 +1,15 @@ +************ +Version 4.10 +************ + +Release date: 2019-07-25 + +This release contains a number of bug fixes and new features since the release of pgAdmin4 4.9. + +New features +************ + +| `Feature #4139 `_ - Allow some objects to be dragged/dropped into the Query Tool to insert their signature into the query text. + +Bug fixes +********* diff --git a/docs/en_US/tree_control.rst b/docs/en_US/tree_control.rst index 73eb114f0..f5008d5db 100644 --- a/docs/en_US/tree_control.rst +++ b/docs/en_US/tree_control.rst @@ -19,6 +19,12 @@ view: control. * Click the minus sign (-) to the left of a node to close that node. +You can also **drag and drop** certain objects to the Query Tool which +can save time in typing long object names. Text containing the object name will be +fully qualified with schema. Double quotes will be added if required. +For functions and procedures, the function name along with parameter names will +be pasted in the Query Tool. + Access context-sensitive menus by right-clicking on a node of the tree control to perform common tasks. Menus display options that include one or more of the following selections (options appear in alphabetical order): diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js index 29c11c2e8..382528877 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -12,7 +12,7 @@ define('pgadmin.browser', [ 'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore', 'underscore.string', 'bootstrap', 'sources/pgadmin', 'pgadmin.alertifyjs', 'bundled_codemirror', 'sources/check_node_visibility', './toolbar', 'pgadmin.help', - 'sources/csrf', 'pgadmin.browser.utils', + 'sources/csrf', 'sources/utils', 'pgadmin.browser.utils', 'wcdocker', 'jquery.contextmenu', 'jquery.aciplugin', 'jquery.acitree', 'pgadmin.browser.preferences', 'pgadmin.browser.messages', 'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin.browser.layout', @@ -24,7 +24,7 @@ define('pgadmin.browser', [ tree, gettext, url_for, require, $, _, S, Bootstrap, pgAdmin, Alertify, codemirror, - checkNodeVisibility, toolBar, help, csrfToken + checkNodeVisibility, toolBar, help, csrfToken, pgadminUtils, ) { window.jQuery = window.$ = $; // Some scripts do export their object in the window only. @@ -102,6 +102,46 @@ define('pgadmin.browser', [ b.tree = $('#tree').aciTree('api'); b.treeMenu.register($('#tree')); + + b.treeMenu.registerDraggableType({ + 'table partition type sequence package view mview foreign_table edbvar' : (data, item)=>{ + return pgadminUtils.fully_qualify(b, data, item); + }, + 'schema column' : (data)=>{ + return pgadminUtils.quote_ident(data._label); + }, + 'edbfunc function edbproc procedure' : (data, item)=>{ + let newData = {...data}, + parsedFunc = null, + dropVal = '', + curPos = {from: 0, to: 0}; + + parsedFunc = pgadminUtils.parseFuncParams(newData._label); + newData._label = parsedFunc.func_name; + dropVal = pgadminUtils.fully_qualify(b, newData, item); + + if(parsedFunc.params.length > 0) { + dropVal = dropVal + '('; + curPos.from = dropVal.length; + dropVal = dropVal + parsedFunc.params[0][0]; + curPos.to = dropVal.length; + + for(let i=1; i{ + this.registerDraggableType(type, typeOrTypeDict[type]); + }); + } else { + if(dropDetailsFunc != null) { + typeOrTypeDict.replace(/ +/, ' ').split(' ').forEach((type)=>{ + this.draggableTypes[type] = dropDetailsFunc; + }); + } + } + } + + getDraggable(type) { + if(this.draggableTypes[type]) { + return this.draggableTypes[type]; + } else { + return null; + } + } + + prepareDraggable(data, item) { + let dropDetailsFunc = this.getDraggable(data._type); + + if(dropDetailsFunc != null) { + item.find('.aciTreeItem') + .attr('draggable', true) + .on('dragstart', (e)=> { + let dropDetails = dropDetailsFunc(data, item); + let origEvent = e.originalEvent; + + if(typeof dropDetails == 'string') { + dropDetails = { + text:dropDetails, + cur:{ + from:dropDetails.length, + to: dropDetails.length, + }, + }; + } else { + if(!dropDetails.cur) { + dropDetails = { + ...dropDetails, + cur:{ + from:dropDetails.text.length, + to: dropDetails.text.length, + }, + }; + } + } + + origEvent.dataTransfer.setData('text', JSON.stringify(dropDetails)); + + /* setDragImage is not supported in IE. We leave it to + * its default look and feel + */ + if(origEvent.dataTransfer.setDragImage) { + let dragItem = $(` +
+ ${dropDetails.text} +
` + ); + + $('body .drag-tree-node').remove(); + $('body').append(dragItem); + + origEvent.dataTransfer.setDragImage(dragItem[0], 0, 0); + } + }); + } } addNewNode(id, data, domNode, parentPath) { @@ -163,6 +245,9 @@ export class Tree { if (eventName === 'added') { const id = api.getId(item); const data = api.itemData(item); + + this.prepareDraggable(data, item); + const parentId = this.translateTreeNodeIdFromACITree(api.parent(item)); this.addNewNode(id, data, item, parentId); } diff --git a/web/pgadmin/static/js/utils.js b/web/pgadmin/static/js/utils.js index 1c58a9eba..fe9fd8ed6 100644 --- a/web/pgadmin/static/js/utils.js +++ b/web/pgadmin/static/js/utils.js @@ -8,6 +8,7 @@ ////////////////////////////////////////////////////////////////////////// import _ from 'underscore'; +import { getTreeNodeHierarchyFromIdentifier } from 'sources/tree/pgadmin_tree_node'; export function parseShortcutValue(obj) { var shortcut = ''; @@ -83,3 +84,118 @@ export function getGCD(inp_arr) { export function getMod(no, divisor) { return ((no % divisor) + divisor) % divisor; } + +export function parseFuncParams(label) { + let paramArr = [], + funcName = '', + paramStr = ''; + + if(label.endsWith('()')) { + funcName = label.substring(0, label.length-2); + } else if(!label.endsWith(')')) { + funcName = label; + } else if(!label.endsWith('()') && label.endsWith(')')) { + let i = 0, + startBracketPos = label.length; + + /* Parse through the characters in reverse to find the param start bracket */ + i = label.length-2; + while(i >= 0) { + if(label[i] == '(') { + startBracketPos = i; + break; + } else if(label[i] == '"') { + /* If quotes, skip all the chars till next quote */ + i--; + while(label[i] != '"') i--; + } + i--; + } + + funcName = label.substring(0, startBracketPos); + paramStr = label.substring(startBracketPos+1, label.length-1); + + let paramStart = 0, + paramName = '', + paramModes = ['IN', 'OUT', 'INOUT', 'VARIADIC']; + + paramStart = i = 0; + while(i < paramStr.length) { + if(paramStr[i] == '"') { + /* If quotes, skip all the chars till next quote */ + i++; + while(paramStr[i] != '"') i++; + } else if (paramStr[i] == ' ') { + /* if paramName is already set, ignore till comma + * Or if paramName is parsed as one of the modes, reset. + */ + if(paramName == '' || paramModes.indexOf(paramName) > -1 ) { + paramName = paramStr.substring(paramStart, i); + paramStart = i+1; + } + } + else if (paramStr[i] == ',') { + paramArr.push([paramName, paramStr.substring(paramStart, i)]); + paramName = ''; + paramStart = i+1; + } + i++; + } + paramArr.push([paramName, paramStr.substring(paramStart)]); + } + + return { + 'func_name': funcName, + 'param_string': paramStr, + 'params': paramArr, + }; +} + +export function quote_ident(value) { + /* check if the string is number or not */ + let quoteIt = false; + if (!isNaN(parseInt(value))){ + quoteIt = true; + } + + if(value.search(/[^a-z0-9_]/g) > -1) { + /* escape double quotes */ + value = value.replace(/"/g, '""'); + quoteIt = true; + } + + if(quoteIt) { + return `"${value}"`; + } else { + return value; + } +} + +export function fully_qualify(pgBrowser, data, item) { + const parentData = getTreeNodeHierarchyFromIdentifier.call(pgBrowser, item); + let namespace = ''; + + if (parentData.schema !== undefined) { + namespace = quote_ident(parentData.schema._label); + } + else if (parentData.view !== undefined) { + namespace = quote_ident(parentData.view._label); + } + else if (parentData.catalog !== undefined) { + namespace = quote_ident(parentData.catalog._label); + } + + if (parentData.package !== undefined && data._type != 'package') { + if(namespace == '') { + namespace = quote_ident(parentData.package._label); + } else { + namespace += '.' + quote_ident(parentData.package._label); + } + } + + if(namespace != '') { + return namespace + '.' + quote_ident(data._label); + } else { + return quote_ident(data._label); + } +} diff --git a/web/pgadmin/static/scss/_pgadmin.style.scss b/web/pgadmin/static/scss/_pgadmin.style.scss index ed4265453..f5a8877c8 100644 --- a/web/pgadmin/static/scss/_pgadmin.style.scss +++ b/web/pgadmin/static/scss/_pgadmin.style.scss @@ -983,3 +983,15 @@ table.table-empty-rows{ padding: 0px !important; position: absolute; } + +.drag-tree-node { + position: absolute; + top:-100px; + left:0; + z-index: 99999; + color: $input-focus-color; + background: $input-bg; + border: $input-border-width solid $input-focus-border-color; + border-radius: $input-border-radius; + padding: $input-btn-padding-y $input-btn-padding-x; +} diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js index a85605745..ef468f63a 100644 --- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js +++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js @@ -341,8 +341,31 @@ define('tools.querytool', [ gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], extraKeys: pgBrowser.editor_shortcut_keys, scrollbarStyle: 'simple', + dragDrop: false, }); + if(self.handler.is_query_tool) { + self.query_tool_obj.setOption('dragDrop', true); + self.query_tool_obj.on('drop', (editor, e) => { + var cursor = editor.coordsChar({ + left: e.x, + top: e.y, + }); + var dropDetails = JSON.parse(e.dataTransfer.getData('text')); + e.codemirrorIgnore = true; + e.dataTransfer.clearData('text'); + editor.replaceRange(dropDetails.text, cursor); + editor.focus(); + editor.setSelection({ + ...cursor, + ch: cursor.ch + dropDetails.cur.from, + },{ + ...cursor, + ch: cursor.ch +dropDetails.cur.to, + }); + }); + } + pgBrowser.Events.on('pgadmin:query_tool:sql_panel:focus', ()=>{ self.query_tool_obj.focus(); }); diff --git a/web/regression/javascript/pgadmin_utils_spec.js b/web/regression/javascript/pgadmin_utils_spec.js index 02bd54780..bc1959a41 100644 --- a/web/regression/javascript/pgadmin_utils_spec.js +++ b/web/regression/javascript/pgadmin_utils_spec.js @@ -7,7 +7,7 @@ // ////////////////////////////////////////////////////////////// -import { getEpoch, getGCD, getMod } from 'sources/utils'; +import { getEpoch, getGCD, getMod, quote_ident, parseFuncParams } from 'sources/utils'; describe('getEpoch', function () { it('should return non zero', function () { @@ -51,3 +51,87 @@ describe('getMod', function () { expect(getMod(-7,5)).toEqual(3); }); }); + +describe('quote_ident', function () { + it('normal string', function () { + expect(quote_ident('abcd')).toEqual('abcd'); + }); + + it('contains certain characters string', function () { + expect(quote_ident('Abcd')).toEqual('"Abcd"'); + expect(quote_ident('abc$d')).toEqual('"abc$d"'); + expect(quote_ident('ab cd')).toEqual('"ab cd"'); + }); + + it('starts with number', function () { + expect(quote_ident('1a')).toEqual('"1a"'); + expect(quote_ident('a1')).toEqual('a1'); + }); +}); + +describe('parseFuncParams', function () { + let funcLabel = '', + expectedObj = {}; + + it('function with params', function () { + funcLabel = 'func1(a integer, b text)'; + expectedObj = { + 'func_name': 'func1', + 'param_string': 'a integer, b text', + 'params': [ + ['a', 'integer'], + ['b', 'text'], + ], + }; + expect(parseFuncParams(funcLabel)).toEqual(expectedObj); + }); + + it('function without params', function () { + funcLabel = 'func1()'; + expectedObj = { + 'func_name': 'func1', + 'param_string': '', + 'params': [], + }; + expect(parseFuncParams(funcLabel)).toEqual(expectedObj); + }); + + it('function name special chars', function () { + funcLabel = 'fun(c1(a integer, b text)'; + expectedObj = { + 'func_name': 'fun(c1', + 'param_string': 'a integer, b text', + 'params': [ + ['a', 'integer'], + ['b', 'text'], + ], + }; + expect(parseFuncParams(funcLabel)).toEqual(expectedObj); + }); + + it('function params special chars', function () { + funcLabel = 'func1("a(b" integer, "a b" text)'; + expectedObj = { + 'func_name': 'func1', + 'param_string': '"a(b" integer, "a b" text', + 'params': [ + ['"a(b"', 'integer'], + ['"a b"', 'text'], + ], + }; + expect(parseFuncParams(funcLabel)).toEqual(expectedObj); + }); + + it('function params with modes', function () { + funcLabel = 'func1(IN a integer, OUT b text)'; + expectedObj = { + 'func_name': 'func1', + 'param_string': 'IN a integer, OUT b text', + 'params': [ + ['a', 'integer'], + ['b', 'text'], + ], + }; + expect(parseFuncParams(funcLabel)).toEqual(expectedObj); + }); +});