Added Macro support. Fixes #1402

This commit is contained in:
Khushboo Vashi authored and Akshay Joshi committed 2020-09-28 15:26:45 +05:30
1 parent 952197f130
commit 4616a74029
17 files changed
+1429 -14

No files matched your search

+172 -4
View File
@@ -9,10 +9,10 @@
define([
'sources/gettext', 'underscore', 'jquery', 'backbone', 'backform', 'backgrid', 'alertify',
'moment', 'bignumber', 'sources/utils', 'sources/keyboard_shortcuts', 'sources/select2/configure_show_on_scroll',
'moment', 'bignumber', 'codemirror', 'sources/utils', 'sources/keyboard_shortcuts', 'sources/select2/configure_show_on_scroll',
'bootstrap.datetimepicker', 'backgrid.filter', 'bootstrap.toggle',
], function(
gettext, _, $, Backbone, Backform, Backgrid, Alertify, moment, BigNumber,
gettext, _, $, Backbone, Backform, Backgrid, Alertify, moment, BigNumber, CodeMirror,
commonUtils, keyboardShortcuts, configure_show_on_scroll
) {
/*
@@ -44,7 +44,7 @@ define([
_.extend(Backgrid.InputCellEditor.prototype.events, {
'keydown': function(e) {
let preferences = pgBrowser.get_preferences_for_module('browser');
if(keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,e)) {
if(preferences && keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,e)) {
pgBrowser.keyboardNavigation.bindAddGridRow();
} else {
Backgrid.InputCellEditor.prototype.saveOrCancel.apply(this, arguments);
@@ -324,7 +324,7 @@ define([
events: {
'keydown': function (event) {
let preferences = pgBrowser.get_preferences_for_module('browser');
if(keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,event)) {
if(preferences && keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,event)) {
pgBrowser.keyboardNavigation.bindAddGridRow();
}
},
@@ -606,6 +606,96 @@ define([
},
});
Backgrid.Extension.ClearCell = Backgrid.Cell.extend({
defaults: _.defaults({
defaultClearMsg: gettext('Are you sure you wish to clear this row?'),
defaultClearTitle: gettext('Clear Row'),
}, Backgrid.Cell.prototype.defaults),
/** @property */
className: 'clear-cell',
events: {
'click': 'clearRow',
},
clearRow: function(e) {
e.preventDefault();
if (_.isEmpty(e.currentTarget.innerHTML)) return false;
var that = this;
// We will check if row is deletable or not
var clear_msg = !_.isUndefined(this.column.get('customClearMsg')) ?
this.column.get('customClearMsg') : that.defaults.defaultClearMsg;
var clear_title = !_.isUndefined(this.column.get('customClearTitle')) ?
this.column.get('customClearTitle') : that.defaults.defaultClearTitle;
Alertify.confirm(
clear_title,
clear_msg,
function() {
that.model.set('name', null);
that.model.set('sql', null);
},
function() {
return true;
}
);
},
exitEditMode: function() {
this.$el.removeClass('editor');
},
initialize: function() {
Backgrid.Cell.prototype.initialize.apply(this, arguments);
},
render: function() {
var self = this;
this.$el.empty();
$(this.$el).attr('tabindex', 0);
if (this.model.get('name') !== null && this.model.get('sql') !== null)
this.$el.html('<i aria-label="' + gettext('Clear row') + '" class=\'fa fa-eraser\' title=\'' + gettext('Clear row') + '\'></i>');
// Listen for Tab/Shift-Tab key
this.$el.on('keydown', function(e) {
// with keyboard navigation on space key, mark row for deletion
if (e.keyCode == 32) {
self.$el.click();
}
var gotoCell;
if (e.keyCode == 9 || e.keyCode == 16) {
// go to Next Cell & if Shift is also pressed go to Previous Cell
gotoCell = e.shiftKey ? self.$el.prev() : self.$el.next();
}
if (gotoCell) {
let command = new Backgrid.Command({
key: 'Tab',
keyCode: 9,
which: 9,
shiftKey: e.shiftKey,
});
setTimeout(function() {
// When we have Editable Cell
if (gotoCell.hasClass('editable')) {
e.preventDefault();
e.stopPropagation();
self.model.trigger('backgrid:edited', self.model,
self.column, command);
}
else {
// When we have Non-Editable Cell
self.model.trigger('backgrid:edited', self.model,
self.column, command);
}
}, 20);
}
});
this.delegateEvents();
return this;
},
});
Backgrid.Extension.CustomHeaderCell = Backgrid.HeaderCell.extend({
initialize: function() {
// Here, we will add custom classes to header cell
@@ -2081,6 +2171,84 @@ define([
},
});
Backgrid.Extension.SqlCell = Backgrid.Extension.TextareaCell.extend({
className: 'sql-cell',
defaults: {
lineWrapping: true,
},
template: _.template([
'<div data-toggle="tooltip" data-placement="top" data-html="true" title="<%- val %>"><textarea aria-label="' + gettext('SQL') +'" + style="display: none;"><%- val %></textarea><div>',
].join('\n')),
render: function() {
let self = this,
col = _.defaults(this.column.toJSON(), this.defaults),
model = this.model,
column = this.column,
columnName = this.column.get('name'),
editable = Backgrid.callByNeed(col.editable, column, model);
if (this.sqlCell) {
this.sqlCell.toTextArea();
delete this.sqlCell;
this.sqlCell = null;
}
this.$el.empty();
this.$el.append(this.template({
val:this.formatter.fromRaw(model.get(columnName), model),
})
);
this.$el.addClass(columnName);
this.updateStateClassesMaybe();
this.delegateEvents();
setTimeout(function() {
self.sqlCell = CodeMirror.fromTextArea(
(self.$el.find('textarea')[0]), {
mode: 'text/x-pgsql',
readOnly: !editable,
singleCursorHeightPerLine: true,
screenReaderLabel: columnName,
});
});
return this;
},
enterEditMode: function () {
if (!this.$el.hasClass('editor')) this.$el.addClass('editor');
this.sqlCell.focus();
this.sqlCell.on('blur', this.exitEditMode.bind(this));
},
exitEditMode: function () {
this.$el.removeClass('editor');
this.saveOrCancel.apply(this, arguments);
},
saveOrCancel: function() {
var model = this.model;
var column = this.column;
if (this.sqlCell) {
var val = this.sqlCell.getTextArea().value;
var newValue = this.sqlCell.getValue();
if (_.isUndefined(newValue)) {
model.trigger('backgrid:error', model, column, val);
}
else {
model.set(column.get('name'), newValue);
}
}
},
remove: function() {
if (this.sqlCell) {
$(this.$el.find('[data-toggle="tooltip"]')).tooltip('dispose');
this.sqlCell.toTextArea();
delete this.sqlCell;
this.sqlCell = null;
}
return Backgrid.Extension.TextareaCell.prototype.remove.apply(this, arguments);
},
});
return Backgrid;
});
+24 -1
View File
@@ -324,15 +324,38 @@ function keyboardShortcutsQueryTool(
}
}
}
} else {
// Macros
let macroId = this.validateMacros(sqlEditorController, event);
if (macroId !== false) {
this._stopEventPropagation(event);
queryToolActions.executeMacro(sqlEditorController, macroId);
}
}
return panel_type;
}
function validateMacros(sqlEditorController, event) {
let keyCode = event.which || event.keyCode;
let macro = sqlEditorController.macros.filter(mc =>
mc.alt == event.altKey &&
mc.control == event.ctrlKey &&
mc.key_code == keyCode);
if (macro.length == 1) {
return macro[0].id;
}
return false;
}
export {
keyboardShortcutsDebugger as processEventDebugger,
keyboardShortcutsQueryTool as processEventQueryTool,
focusDockerPanel, validateShortcutKeys,
focusDockerPanel, validateShortcutKeys, validateMacros,
_stopEventPropagation, isMac, isKeyCtrlAlt, isKeyAltShift, isKeyCtrlShift,
isKeyCtrlAltShift, isAltShiftBoth, isCtrlShiftBoth, isCtrlAltBoth,
shortcut_key, shortcut_title, shortcut_accesskey_title,
+325
View File
@@ -0,0 +1,325 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import $ from 'jquery';
import Alertify from 'pgadmin.alertifyjs';
import pgAdmin from 'sources/pgadmin';
import Backform from 'pgadmin.backform';
import macroModel from 'sources/sqleditor/macro_model';
import axios from 'axios';
let MacroDialog = {
'dialog': function(handler) {
let title = gettext('Manage Macros');
// Check the alertify dialog already loaded then delete it to clear
// the cache
if (Alertify.macroDialog) {
delete Alertify.macroDialog;
}
// Create Dialog
Alertify.dialog('macroDialog', function factory() {
let $container = $('<div class=\'macro_dialog\'></div>');
return {
main: function() {
this.set('title', '<i class="fa fa-scroll sql-icon-lg" aria-hidden="true" role="img"></i> ' + gettext('Manage Macros'));
},
build: function() {
this.elements.content.appendChild($container.get(0));
Alertify.pgDialogBuild.apply(this);
},
setup: function() {
return {
buttons: [{
text: '',
key: 112,
className: 'btn btn-primary-icon pull-left fa fa-question pg-alertify-icon-button',
attrs: {
name: 'dialog_help',
type: 'button',
label: gettext('Help'),
'aria-label': gettext('Help'),
url: url_for('help.static', {
'filename': 'querytool.html',
}),
},
}, {
text: gettext('Cancel'),
key: 27,
className: 'btn btn-secondary fa fa-times pg-alertify-button',
'data-btn-name': 'cancel',
}, {
text: gettext('Save'),
className: 'btn btn-primary fa fa-save pg-alertify-button',
'data-btn-name': 'ok',
}],
// Set options for dialog
options: {
title: title,
//disable both padding and overflow control.
padding: !1,
overflow: !1,
model: 0,
resizable: true,
maximizable: true,
pinnable: false,
closableByDimmer: false,
modal: false,
autoReset: false,
},
};
},
hooks: {
// triggered when the dialog is closed
onclose: function() {
if (this.view) {
this.macroCollectionModel.stopSession();
this.view.model.stopSession();
this.view.remove({
data: true,
internal: true,
silent: true,
});
}
},
},
prepare: function() {
let self = this;
$container.html('');
// Status bar
this.statusBar = $(
'<div class=\'pg-prop-status-bar pg-el-xs-12 d-none\'>' +
' <div class="error-in-footer"> ' +
' <div class="d-flex px-2 py-1"> ' +
' <div class="pr-2"> ' +
' <i class="fa fa-exclamation-triangle text-danger" aria-hidden="true"></i> ' +
' </div> ' +
' <div class="alert-text" role="alert"></div> ' +
' </div> ' +
' </div> ' +
'</div>').appendTo($container);
// To show progress on filter Saving/Updating on AJAX
this.showFilterProgress = $(
`<div id="show_filter_progress" class="pg-sp-container sql-editor-busy-fetching d-none">
<div class="pg-sp-content">
<div class="row"><div class="col-12 pg-sp-icon sql-editor-busy-icon"></div></div>
<div class="row"><div class="col-12 pg-sp-text sql-editor-busy-text">` + gettext('Loading data...') + `</div></div>
</div>
</div>`
).appendTo($container);
self.macroCollectionModel = macroModel(handler.transId);
let fields = Backform.generateViewSchema(
null, self.macroCollectionModel, 'edit', null, null, true
);
let ManageMacroDialog = Backform.Dialog.extend({
template: {
'panel': _.template(
'<div role="tabpanel" tabindex="-1" class="tab-pane <%=label%> <%=tabPanelCodeClass%> pg-el-sm-12 pg-el-md-12 pg-el-lg-12 pg-el-12 fade" id="<%=cId%>"></div>'
),
},
render: function() {
this.cleanup();
var m = this.model,
controls = this.controls,
tmpls = this.template,
dialog_obj = this,
idx = (this.tabIndex * 100),
evalF = function(f, d, model) {
return (_.isFunction(f) ? !!f.apply(d, [model]) : !!f);
};
this.$el
.empty()
.attr('role', 'tabpanel')
.attr('class', _.result(this, 'tabPanelClassName'));
m.panelEl = this.$el;
var tabContent = $('<div class="tab-content pg-el-sm-12 pg-el-md-12 pg-el-lg-12 pg-el-12 macro-tab"></div>')
.appendTo(this.$el);
_.each(this.schema, function(o) {
idx++;
if (!o.version_compatible || !evalF(o.visible, o, m)) {
return;
}
var el = $((tmpls['panel'])(_.extend(o, {
'tabIndex': idx,
'tabPanelCodeClass': o.tabPanelCodeClass ? o.tabPanelCodeClass : '',
})))
.appendTo(tabContent)
.removeClass('collapse').addClass('collapse');
o.fields.each(function(f) {
var cntr = new(f.get('control'))({
field: f,
model: m,
dialog: dialog_obj,
tabIndex: idx,
});
el.append(cntr.render().$el);
controls.push(cntr);
});
});
tabContent.find('.tab-pane').first().addClass('active show');
return this;
},
});
let view = self.view = new ManageMacroDialog({
el: '<div></div>',
model: self.macroCollectionModel,
schema: fields,
});
self.macroCollectionModel.fetch({
success: function() {
// We got the latest attributes of the object. Render the view
// now.
$container.append(self.view.render().$el);
// Enable/disable save button and show/hide statusbar based on session
self.view.listenTo(self.view.model, 'pgadmin-session:start', function() {
self.view.listenTo(self.view.model, 'pgadmin-session:invalid', function(msg) {
self.statusBar.removeClass('d-none');
$(self.statusBar.find('.alert-text')).html(msg);
// Disable Okay button
self.__internal.buttons[2].element.disabled = true;
});
view.listenTo(self.view.model, 'pgadmin-session:valid', function() {
self.statusBar.addClass('d-none');
$(self.statusBar.find('.alert-text')).html('');
// Enable Okay button
self.__internal.buttons[2].element.disabled = false;
});
});
view.listenTo(self.view.model, 'pgadmin-session:stop', function() {
view.stopListening(self.view.model, 'pgadmin-session:invalid');
view.stopListening(self.view.model, 'pgadmin-session:valid');
});
// Starts monitoring changes to model
self.view.model.startNewSession();
}});
$(this.elements.body.childNodes[0]).addClass(
'alertify_tools_dialog_properties obj_properties'
);
},
// Callback functions when click on the buttons of the Alertify dialogs
callback: function(e) {
let self = this;
if (e.button.element.name == 'dialog_help') {
e.cancel = true;
pgAdmin.Browser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
null, null);
return;
} else if (e.button['data-btn-name'] === 'ok') {
e.cancel = true; // Do not close dialog
let data = self.view.model.get('macro').toJSON(true);
if (data == undefined || data == null) {
self.close();
return;
}
axios.put(
url_for('sqleditor.set_macros', {
'trans_id': handler.transId,
}),
data
).then(function (result) {
// Hide Progress ...
$(
self.showFilterProgress[0]
).addClass('d-none');
let macroResponse = result;
if (macroResponse.status) {
setTimeout(
function() {
// Update Macro Menu
let macros = self.view.model.get('macro').toJSON().filter(m => m.name !== undefined || m.name !== null);
handler.macros = macros;
var str = `
<li>
<a class="dropdown-item" id="btn-manage-macros" href="#" tabindex="0">
<span> Manage Macros... </span>
</a>
</li>
<li class="dropdown-divider"></li>`;
_.each(macros, function(m) {
if (m.name) {
str += `<li>
<a class="dropdown-item btn-macro" data-macro-id="`+ m.id +`" href="#" tabindex="0">
<span>` + m.name + `</span>
<span> (` + m.key_label + `) </span>
</a>
</li>`;
}
});
$($.find('div.btn-group.mr-1.user_macros ul.dropdown-menu')).html($(str));
self.close(); // Close the dialog now
Alertify.success(gettext('Macro updated successfully'));
}, 10
);
} else {
Alertify.alert(
gettext('Validation Error'),
macroResponse.result
);
}
}).catch(function (error) {
// Hide Progress ...
$(
self.showFilterProgress[0]
).addClass('d-none');
setTimeout(
function() {
Alertify.error(error.response.data.errormsg);
}, 10
);
});
} else {
self.close();
}
},
};
});
Alertify.macroDialog(title).resizeTo(pgAdmin.Browser.stdW.calc(pgAdmin.Browser.stdW.lg),
pgAdmin.Browser.stdH.calc(pgAdmin.Browser.stdH.lg));
},
};
module.exports = MacroDialog;
@@ -0,0 +1,224 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import pgAdmin from 'sources/pgadmin';
import Backform from 'pgadmin.backform';
import Backgrid from 'pgadmin.backgrid';
import url_for from 'sources/url_for';
import $ from 'jquery';
import _ from 'underscore';
import Alertify from 'pgadmin.alertifyjs';
export default function macroModel(transId) {
let MacroModel = pgAdmin.Browser.DataModel.extend({
idAttribute: 'id',
defaults: {
id: undefined,
key: undefined,
name: undefined,
sql: undefined,
key_label: undefined,
},
schema: [{
id: 'key_label',
name: 'key_label',
label: gettext('Key'),
type: 'text',
cell: 'string',
editable: false,
cellHeaderClasses: 'width_percent_10',
headerCell: Backgrid.Extension.CustomHeaderCell,
disabled: false,
}, {
id: 'name',
name: 'name',
label: gettext('Name'),
cell: 'string',
type: 'text',
editable: true,
cellHeaderClasses: 'width_percent_20',
headerCell: Backgrid.Extension.CustomHeaderCell,
disabled: false,
}, {
id: 'sql',
name: 'sql',
label: gettext('SQL'),
cell: Backgrid.Extension.SqlCell,
type: 'multiline',
control: Backform.SqlCodeControl,
editable: true,
cellHeaderClasses: 'width_percent_70',
headerCell: Backgrid.Extension.CustomHeaderCell,
disabled: false,
},
],
validate: function() {
let msg = null;
this.errorModel.clear();
if (_.isEmpty(this.get('name')) && !(_.isEmpty(this.get('sql')))) {
msg = gettext('Please enter macro name.');
this.errorModel.set('name', msg);
return msg;
} else if (_.isEmpty(this.get('sql')) && !(_.isEmpty(this.get('name')))) {
msg = gettext('Please enter macro sql.');
this.errorModel.set('sql', msg);
return msg;
}
return null;
},
});
let MacroCollectionModel = pgAdmin.Browser.DataModel.extend({
defaults: {
macro: undefined,
},
urlRoot: url_for('sqleditor.get_macros', {'trans_id': transId}),
schema: [{
id: 'macro',
name: 'macro',
label: gettext('Macros'),
model: MacroModel,
editable: true,
type: 'collection',
control: Backform.SubNodeCollectionControl.extend({
showGridControl: function(data) {
var self = this,
gridBody = $('<div class=\'pgadmin-control-group backgrid form-group pg-el-12 object subnode\'></div>');
var subnode = data.subnode.schema ? data.subnode : data.subnode.prototype,
gridSchema = Backform.generateGridColumnsFromModel(
data.node_info, subnode, this.field.get('mode'), data.columns, data.schema_node
);
// Clean up existing grid if any (in case of re-render)
if (self.grid) {
self.grid.remove();
}
// Set visibility of Add button
if (data.disabled || data.canAdd == false) {
$(gridBody).find('button.add').remove();
}
// Insert Clear Cell into Grid
gridSchema.columns.unshift({
name: 'pg-backform-clear',
label: '<i aria-label="' + gettext('Clear row') + '" class="fa fa-eraser" title="' + gettext('Clear row') + '"></i>',
cell: Backgrid.Extension.ClearCell,
editable: false,
cell_priority: -1,
sortable: false,
headerCell: Backgrid.Extension.CustomHeaderCell.extend({
className: 'header-icon-cell',
events: {
'click': 'clearrAll',
},
clearrAll: function(e) {
e.preventDefault();
var that = this;
// We will check if row is deletable or not
Alertify.confirm(
gettext('Clear All Rows'),
gettext('Are you sure you wish to clear all rows?'),
function() {
_.each(that.collection.toJSON(), function(m) {
that.collection.get(m.id).set({'name': null, 'sql': null});
});
},
function() {
return true;
}
);
},
render: function() {
this.$el.empty();
var column = this.column;
var label = $('<button type="button" title="' + gettext('Clear row') + '" aria-label="Clear row" aria-expanded="false" tabindex="0">').html(column.get('label')).append('<span class=\'sort-caret\' aria-hidden=\'true\'></span>');
this.$el.append(label);
this.$el.addClass(column.get('name'));
this.$el.addClass(column.get('direction'));
this.$el.attr('role', 'columnheader');
this.$el.attr('aria-label', 'columnheader');
this.$el.attr('alt', 'columnheader');
this.delegateEvents();
return this;
},
}),
});
var collection = self.model.get(data.name);
if (!collection) {
collection = new(pgAdmin.Browser.Node.Collection)(null, {
handler: self.model.handler || self.model,
model: data.model,
top: self.model.top || self.model,
silent: true,
});
self.model.set(data.name, collection, {
silent: true,
});
}
var cellEditing = function(args) {
var ctx = this,
cell = args[0];
// Search for any other rows which are open.
this.each(function(m) {
// Check if row which we are about to close is not current row.
if (cell.model != m) {
var idx = ctx.indexOf(m);
if (idx > -1) {
var row = grid.body.rows[idx],
rowEditCell = row.$el.find('.subnode-edit-in-process').parent();
// Only close row if it's open.
if (rowEditCell.length > 0) {
var event = new Event('click');
rowEditCell[0].dispatchEvent(event);
}
}
}
});
};
// Listen for any row which is about to enter in edit mode.
collection.on('enteringEditMode', cellEditing, collection);
// Initialize a new Grid instance
var grid = self.grid = new Backgrid.Grid({
columns: gridSchema.columns,
collection: collection,
row: this.row,
className: 'backgrid table presentation table-bordered table-noouter-border table-hover',
});
// Render subNode grid
var subNodeGrid = grid.render().$el;
var $dialog = gridBody.append(subNodeGrid);
return $dialog;
},
}),
columns: ['key_label', 'name', 'sql'],
visible: true,
}],
validate: function() {
return null;
},
});
let model = new MacroCollectionModel();
return model;
}
@@ -39,6 +39,11 @@ let queryToolActions = {
$('.sql-editor-message').html('');
},
executeMacro: function (sqlEditorController, MacroId) {
this._clearMessageTab();
sqlEditorController.check_data_changes_to_execute_query(null, false, MacroId);
},
executeQuery: function (sqlEditorController) {
this._clearMessageTab();
sqlEditorController.check_data_changes_to_execute_query();
@@ -359,3 +359,31 @@ table tr th button {
}
}
}
.backgrid .sql-cell .CodeMirror-scroll {
overflow: hidden !important;
}
.backgrid .sql-cell .cm-s-default {
height: 50px !important;
}
.backgrid .sql-cell .CodeMirror-hscrollbar > div {
width: 100% !important;
}
.backgrid .sql-cell .CodeMirror-hscrollbar {
overflow-x: hidden !important;
}
.backgrid .sql-cell .CodeMirror-vscrollbar {
overflow-y: hidden !important;
}
.backgrid .sql-cell .CodeMirror-sizer {
padding-bottom: 0 !important;
}
.backgrid .sql-cell .CodeMirror-scrollbar-filler {
display: none !important;
}