Allow some objects to be dragged/dropped into the Query Tool to insert their signature into the query text. Fixes #4139

This commit is contained in:
Aditya Toshniwal 2019-06-27 10:30:05 -04:00 committed by Dave Page
parent bdb8f20aed
commit 173b812b93
10 changed files with 390 additions and 3 deletions

View File

@ -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
*********************

View File

@ -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

View File

@ -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 <https://redmine.postgresql.org/issues/4139>`_ - Allow some objects to be dragged/dropped into the Query Tool to insert their signature into the query text.
Bug fixes
*********

View File

@ -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):

View File

@ -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<parsedFunc.params.length; i++) {
dropVal = dropVal + ', ' + parsedFunc.params[i][0];
}
dropVal = dropVal + ')';
} else {
dropVal = dropVal + '()';
curPos.from = curPos.to = dropVal.length + 1;
}
return {
text: dropVal,
cur: curPos,
};
},
});
};
// Extend the browser class attributes

View File

@ -8,6 +8,7 @@
//////////////////////////////////////////////////////////////////////////
import {isValidData} from 'sources/utils';
import $ from 'jquery';
export class TreeNode {
constructor(id, data, domNode, parent) {
@ -97,6 +98,87 @@ export class Tree {
constructor() {
this.rootNode = new TreeNode(undefined, {});
this.aciTreeApi = undefined;
this.draggableTypes = {};
}
/*
*
* The dropDetailsFunc should return an object of sample
* {text: 'xyz', cur: {from:0, to:0} where text is the drop text and
* cur is selection range of text after dropping. If returned as
* string, by default cursor will be set to the end of text
*/
registerDraggableType(typeOrTypeDict, dropDetailsFunc=null) {
if(typeof typeOrTypeDict == 'object') {
Object.keys(typeOrTypeDict).forEach((type)=>{
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 = $(`
<div class="drag-tree-node">
<span>${dropDetails.text}</span>
</div>`
);
$('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);
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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();
});

View File

@ -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);
});
});