mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
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:
parent
bdb8f20aed
commit
173b812b93
@ -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
|
||||
*********************
|
||||
|
@ -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
|
||||
|
15
docs/en_US/release_notes_4_10.rst
Normal file
15
docs/en_US/release_notes_4_10.rst
Normal 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
|
||||
*********
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user