Add infrastructure for managing configurable keyboard shortcuts.

This commit is contained in:
Harshal Dhumal 2018-01-25 12:49:06 +00:00 committed by Dave Page
parent 7c985695b7
commit 66341e6947
7 changed files with 885 additions and 9 deletions

View File

@ -25,10 +25,6 @@ div.pgadmin-preference-body div.ajs-content {
bottom: 0px !important;
}
.preferences_content .control-label, .preferences_content .pgadmin-controls {
min-width: 100px !important;
}
.pgadmin-preference-body {
min-width: 300px !important;
min-height: 400px !important;
@ -40,3 +36,15 @@ div.pgadmin-preference-body div.ajs-content {
min-height: 480px !important;
}
}
.keyboard-shortcut-label {
font-weight: normal !important;
}
.preferences_content input[type="checkbox"]:focus {
outline: 5px auto -webkit-focus-ring-color;
}
.preferences_content .pgadmin-controls input[type="checkbox"] {
margin-left: -20px !important;
}

View File

@ -256,6 +256,8 @@ define('pgadmin.preferences', [
return 'textarea';
case 'switch':
return 'switch';
case 'keyboardshortcut':
return 'keyboardShortcut';
default:
if (console && console.warn) {
// Warning for developer only.

View File

@ -2504,5 +2504,206 @@ define([
},
});
var KeyCodeControlFormatter = Backform.KeyCodeControlFormatter = function() {};
_.extend(KeyCodeControlFormatter.prototype, {
fromRaw: function (rawData) {
return rawData['char'];
},
// we don't need toRaw
toRaw: undefined,
});
Backform.KeyCodeControl = Backform.InputControl.extend({
defaults: _.defaults({
escapeKeyCodes: [16, 17, 18, 27], // Shift, Ctrl, Alt/Option, Escape
}, Backform.InputControl.prototype.defaults),
events: {
'keydown input': 'onkeyDown',
'keyup input': 'preventEvent',
'focus select': 'clearInvalid',
},
formatter: KeyCodeControlFormatter,
preventEvent: function(e) {
var key_code = e.which || e.keyCode,
field = _.defaults(this.field.toJSON(), this.defaults);
if (field.escapeKeyCodes.indexOf(key_code) != -1) {
return;
}
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
},
template: _.template([
'<label class="<%=Backform.controlLabelClassName%> keyboard-shortcut-label"><%=label%></label>',
'<div class="<%=Backform.controlsClassName%>">',
' <input type="<%=type%>" class="<%=Backform.controlClassName%> <%=extraClasses.join(\' \')%>" name="<%=name%>" oncopy="return false; oncut="return false; onpaste="return false;" maxlength="<%=maxlength%>" value="<%-value%>" placeholder="<%-placeholder%>" <%=disabled ? "disabled" : ""%> <%=required ? "required" : ""%> />',
' <% if (helpMessage && helpMessage.length) { %>',
' <span class="<%=Backform.helpMessageClassName%>"><%=helpMessage%></span>',
' <% } %>',
'</div>',
].join('\n')),
onkeyDown: function(e) {
var self = this,
model = this.model,
attrArr = this.field.get('name').split('.'),
name = attrArr.shift(),
changes = {},
key_code = e.which || e.keyCode,
key,
field = _.defaults(this.field.toJSON(), this.defaults);
if (field.escapeKeyCodes.indexOf(key_code) != -1) {
return;
}
if (this.model.errorModel instanceof Backbone.Model) {
this.model.errorModel.unset(name);
}
key = gettext(e.key);
if (key_code == 32) {
key = gettext('Space');
}
changes = {
'key_code': e.which || e.keyCode,
'char': key,
};
this.stopListening(this.model, 'change:' + name, this.render);
model.set(name, changes);
this.listenTo(this.model, 'change:' + name, this.render);
setTimeout(function() {
self.$el.find('input').val(key);
});
e.preventDefault();
},
keyPathAccessor: function(obj, path) {
var res = obj;
path = path.split('.');
for (var i = 0; i < path.length; i++) {
if (_.isNull(res)) return null;
if (_.isEmpty(path[i])) continue;
if (!_.isUndefined(res[path[i]])) res = res[path[i]];
}
return res;
},
});
Backform.KeyboardShortcutControl = Backform.Control.extend({
initialize: function() {
Backform.Control.prototype.initialize.apply(this, arguments);
var fields = this.field.get('fields');
if (fields == null || fields == undefined) {
throw new ReferenceError('"fields" not found in keyboard shortcut');
}
this.innerModel = new Backbone.Model();
this.controls = [];
},
cleanup: function() {
this.stopListening(this.innerModel, 'change', this.onInnerModelChange);
_.each(this.controls, function(c) {
c.remove();
});
this.controls.length = 0;
},
template: _.template([
'<label class="<%=Backform.controlLabelClassName%>"><%=label%></label>',
'<div class="<%=Backform.controlsClassName%>">',
'</div>',
].join('\n')),
onInnerModelChange: function() {
var name = this.field.get('name'),
val = $.extend(true, {}, this.model.get(name));
this.stopListening(this.model, 'change:' + name, this.render);
this.model.set(name,
$.extend(true, val, this.innerModel.toJSON())
);
this.listenTo(this.model, 'change:' + name, this.render);
},
render: function() {
this.cleanup();
this.$el.empty();
var self = this,
initial_value = {},
field = _.defaults(this.field.toJSON(), this.defaults),
value = self.model.get(field['name']),
innerFields = field['fields'];
this.$el.html(self.template(field)).addClass(field.name);
var $container = $(self.$el.find('.pgadmin-controls'));
_.each(innerFields, function(field) {
initial_value[field['name']] = value[field['name']];
});
self.innerModel.set(initial_value);
self.listenTo(self.innerModel, 'change', self.onInnerModelChange);
_.each(innerFields, function(fld) {
var f = new Backform.Field(
_.extend({}, {
id: fld['name'],
name: fld['name'],
control: fld['type'],
label: fld['label'],
})
),
cntr = new (f.get('control')) ({
field: f,
model: self.innerModel,
});
cntr.render();
if(fld['type'] == 'checkbox') {
if (fld['name'] == 'alt_option') {
$container.append($('<div class="pg-el-sm-3 pg-el-xs-12"></div>').append(cntr.$el));
} else {
$container.append($('<div class="pg-el-sm-2 pg-el-xs-12"></div>').append(cntr.$el));
}
} else {
$container.append($('<div class="pg-el-sm-5 pg-el-xs-12"></div>').append(cntr.$el));
}
// We will keep track of all the controls rendered at the
// moment.
self.controls.push(cntr);
});
return self;
},
remove: function() {
/* First do the clean up */
this.cleanup();
Backform.Control.prototype.remove.apply(this, arguments);
},
});
return Backform;
});

View File

@ -13,6 +13,7 @@ module within the system.
"""
import decimal
import simplejson as json
import dateutil.parser as dateutil_parser
from flask import current_app
@ -31,7 +32,7 @@ class _Preference(object):
def __init__(
self, cid, name, label, _type, default, help_str=None, min_val=None,
max_val=None, options=None, select2=None
max_val=None, options=None, select2=None, fields=None
):
"""
__init__
@ -54,6 +55,8 @@ class _Preference(object):
:param max_val: maximum value
:param options: options (Array of list objects)
:param select2: select2 options (object)
:param fields: field schema (if preference has more than one field to
take input from user e.g. keyboardshortcut preference)
:returns: nothing
"""
@ -67,6 +70,7 @@ class _Preference(object):
self.max_val = max_val
self.options = options
self.select2 = select2
self.fields = fields
# Look into the configuration table to find out the id of the specific
# preference.
@ -137,6 +141,12 @@ class _Preference(object):
if self._type == 'text':
if res.value == '':
return self.default
if self._type == 'keyboardshortcut':
try:
return json.loads(res.value)
except Exception as e:
current_app.logger.exeception(e)
return self.default
return res.value
@ -196,6 +206,14 @@ class _Preference(object):
if not has_value and self.select2 and not self.select2['tags']:
return False, gettext("Invalid value for an options option.")
elif self._type == 'keyboardshortcut':
try:
value = json.dumps(value)
except Exception as e:
current_app.logger.exeception(e)
return False, gettext(
"Invalid value for a keyboard shortcut option."
)
pref = UserPrefTable.query.filter_by(
pid=self.pid
@ -231,7 +249,8 @@ class _Preference(object):
'max_val': self.max_val,
'options': self.options,
'select2': self.select2,
'value': self.get()
'value': self.get(),
'fields': self.fields,
}
return res
@ -371,7 +390,7 @@ class Preferences(object):
def register(
self, category, name, label, _type, default, min_val=None,
max_val=None, options=None, help_str=None, category_label=None,
select2=None
select2=None, fields=None
):
"""
register
@ -393,6 +412,8 @@ class Preferences(object):
:param help_str:
:param category_label:
:param select2: select2 control extra options
:param fields: field schema (if preference has more than one field to
take input from user e.g. keyboardshortcut preference)
"""
cat = self.__category(category, category_label)
if name in cat['preferences']:
@ -402,12 +423,13 @@ class Preferences(object):
assert _type is not None, "Type for a preference cannot be none!"
assert _type in (
'boolean', 'integer', 'numeric', 'date', 'datetime',
'options', 'multiline', 'switch', 'node', 'text'
'options', 'multiline', 'switch', 'node', 'text',
'keyboardshortcut'
), "Type cannot be found in the defined list!"
(cat['preferences'])[name] = res = _Preference(
cat['id'], name, label, _type, default, help_str, min_val,
max_val, options, select2
max_val, options, select2, fields
)
return res

View File

@ -0,0 +1,434 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2018, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
define([
'jquery',
'backbone',
'pgadmin.backform',
], function ($, Backbone, Backform) {
describe('KeyboardshortcutControl', function () {
let field, innerFields, control, model, event;
beforeEach(() => {
innerFields = [
{'name': 'key', 'type': 'keyCode', 'label': 'Key'},
{'name': 'alt_option', 'type': 'checkbox',
'label': 'Alt/Option'},
{'name': 'control', 'type': 'checkbox',
'label': 'Ctrl'},
{'name': 'shift', 'type': 'checkbox', 'label': 'Shift'},
];
model = new Backbone.Model({
'shortcut': {
'control': true,
'shift': false,
'alt_option': true,
'key': {
'key_code': 73,
'char': 'I',
},
},
});
field = new Backform.Field({
id: 'shortcut',
name: 'shortcut',
control: 'keyboardShortcut',
label: 'Keyboard shortcut',
fields: innerFields,
});
control = new (field.get('control')) ({
field: field,
model: model,
});
control.render();
});
describe('keyboardShortcut UI setup', function () {
it('keyboard shortcut control should be rendered with inner fields', function () {
expect(control.$el.find('input:text[name="key"]')[0].value).toBe('I');
expect(control.$el.find('input:checkbox[name="alt_option"]')[0].checked).toBeTruthy();
expect(control.$el.find('input:checkbox[name="control"]')[0].checked).toBeTruthy();
expect(control.$el.find('input:checkbox[name="shift"]')[0].checked).toBeFalsy();
});
});
describe('onModelChange', function () {
beforeEach((done) => {
done();
});
it('when model "key" value changes UI and innerModel should update new "key" value', function (done) {
expect(control.$el.find('input:text[name="key"]')[0].value).toBe('I');
expect(control.innerModel.get('key')).toEqual({
'key_code': 73,
'char': 'I',
});
var val = $.extend(true, {}, model.get(field.get('name')));
model.set(field.get('name'),
$.extend(true, val, {
'key': {
'key_code': 65,
'char': 'A',
}
})
);
// wait until UI updates.
setTimeout(function() {
// this should change
expect(control.$el.find('input:text[name="key"]')[0].value).toBe('A');
expect(control.innerModel.get('key')).toEqual({
'key_code': 65,
'char': 'A',
});
// below three should not change.
expect(control.$el.find('input:checkbox[name="alt_option"]')[0].checked).toBeTruthy();
expect(control.innerModel.get('alt_option')).toBeTruthy();
expect(control.$el.find('input:checkbox[name="control"]')[0].checked).toBeTruthy();
expect(control.innerModel.get('control')).toBeTruthy();
expect(control.$el.find('input:checkbox[name="shift"]')[0].checked).toBeFalsy();
expect(control.innerModel.get('shift')).toBeFalsy();
done();
}, 100);
});
it('when model "control" value changes UI and innerModel should update new "control" value', function (done) {
expect(control.$el.find('input:checkbox[name="control"]')[0].checked).toBeTruthy();
expect(control.innerModel.get('control')).toBeTruthy();
var val = $.extend(true, {}, model.get(field.get('name')));
model.set(field.get('name'),
$.extend(true, val, {
'control': false
})
);
// wait until UI updates.
setTimeout(function() {
// this should change
expect(control.$el.find('input:checkbox[name="control"]')[0].checked).toBeFalsy();
expect(control.innerModel.get('control')).toBeFalsy();
// below three should not change.
expect(control.$el.find('input:checkbox[name="alt_option"]')[0].checked).toBeTruthy();
expect(control.innerModel.get('alt_option')).toBeTruthy();
expect(control.$el.find('input:text[name="key"]')[0].value).toBe('I');
expect(control.innerModel.get('key')).toEqual({
'key_code': 73,
'char': 'I',
});
expect(control.$el.find('input:checkbox[name="shift"]')[0].checked).toBeFalsy();
expect(control.innerModel.get('shift')).toBeFalsy();
done();
}, 100);
});
it('when model "shift" value changes UI and innerModel should update new "shift" value', function (done) {
expect(control.$el.find('input:checkbox[name="shift"]')[0].checked).toBeFalsy();
expect(control.innerModel.get('shift')).toBeFalsy();
var val = $.extend(true, {}, model.get(field.get('name')));
model.set(field.get('name'),
$.extend(true, val, {
'shift': true
})
);
// wait until UI updates.
setTimeout(function() {
// this should change
expect(control.$el.find('input:checkbox[name="shift"]')[0].checked).toBeTruthy();
expect(control.innerModel.get('shift')).toBeTruthy();
// below three should not change.
expect(control.$el.find('input:checkbox[name="alt_option"]')[0].checked).toBeTruthy();
expect(control.innerModel.get('alt_option')).toBeTruthy();
expect(control.$el.find('input:text[name="key"]')[0].value).toBe('I');
expect(control.innerModel.get('key')).toEqual({
'key_code': 73,
'char': 'I',
});
expect(control.$el.find('input:checkbox[name="control"]')[0].checked).toBeTruthy();
expect(control.innerModel.get('control')).toBeTruthy();
done();
}, 100);
});
it('when model "alt_option" value changes UI and innerModel should update new "alt_option" value', function (done) {
expect(control.$el.find('input:checkbox[name="alt_option"]')[0].checked).toBeTruthy();
expect(control.innerModel.get('alt_option')).toBeTruthy();
var val = $.extend(true, {}, model.get(field.get('name')));
model.set(field.get('name'),
$.extend(true, val, {
'alt_option': false
})
);
// wait until UI updates.
setTimeout(function() {
// this should change
expect(control.$el.find('input:checkbox[name="alt_option"]')[0].checked).toBeFalsy();
expect(control.innerModel.get('alt_option')).toBeFalsy();
// below three should not change.
expect(control.$el.find('input:checkbox[name="shift"]')[0].checked).toBeFalsy();
expect(control.innerModel.get('shift')).toBeFalsy();
expect(control.$el.find('input:text[name="key"]')[0].value).toBe('I');
expect(control.innerModel.get('key')).toEqual({
'key_code': 73,
'char': 'I',
});
expect(control.$el.find('input:checkbox[name="control"]')[0].checked).toBeTruthy();
expect(control.innerModel.get('control')).toBeTruthy();
done();
}, 100);
});
});
describe('onInnerModelChange', function () {
beforeEach((done) => {
done();
});
it('when innerModel "key" value changes UI and model should update new "key" value', function (done) {
expect(control.$el.find('input:text[name="key"]')[0].value).toBe('I');
expect(model.get(field.get('name'))).toEqual({
'control': true,
'shift': false,
'alt_option': true,
'key': {
'key_code': 73,
'char': 'I',
},
});
control.innerModel.set('key',
{
'key_code': 65,
'char': 'A',
}
);
// wait until UI updates.
setTimeout(function() {
// this should change
expect(control.$el.find('input:text[name="key"]')[0].value).toBe('A');
expect(model.get(field.get('name'))).toEqual({
'control': true,
'shift': false,
'alt_option': true,
'key': {
'key_code': 65,
'char': 'A',
},
});
// below three should not change.
expect(control.$el.find('input:checkbox[name="alt_option"]')[0].checked).toBeTruthy();
expect(control.$el.find('input:checkbox[name="control"]')[0].checked).toBeTruthy();
expect(control.$el.find('input:checkbox[name="shift"]')[0].checked).toBeFalsy();
done();
}, 100);
});
it('when innerModel "control" value changes UI and model should update new "control" value', function (done) {
expect(control.$el.find('input:checkbox[name="control"]')[0].checked).toBeTruthy();
expect(model.get(field.get('name'))).toEqual({
'control': true,
'shift': false,
'alt_option': true,
'key': {
'key_code': 73,
'char': 'I',
},
});
control.innerModel.set('control', false);
// wait until UI updates.
setTimeout(function() {
// this should change
expect(control.$el.find('input:checkbox[name="control"]')[0].checked).toBeFalsy();
expect(model.get(field.get('name'))).toEqual({
'control': false,
'shift': false,
'alt_option': true,
'key': {
'key_code': 73,
'char': 'I',
},
});
// below three should not change.
expect(control.$el.find('input:checkbox[name="alt_option"]')[0].checked).toBeTruthy();
expect(control.$el.find('input:text[name="key"]')[0].value).toBe('I');
expect(control.$el.find('input:checkbox[name="shift"]')[0].checked).toBeFalsy();
done();
}, 100);
});
it('when innerModel "shift" value changes UI and model should update new "shift" value', function (done) {
expect(control.$el.find('input:checkbox[name="shift"]')[0].checked).toBeFalsy();
expect(model.get(field.get('name'))).toEqual({
'control': true,
'shift': false,
'alt_option': true,
'key': {
'key_code': 73,
'char': 'I',
},
});
control.innerModel.set('shift', true);
// wait until UI updates.
setTimeout(function() {
// this should change
expect(control.$el.find('input:checkbox[name="shift"]')[0].checked).toBeTruthy();
expect(model.get(field.get('name'))).toEqual({
'control': true,
'shift': true,
'alt_option': true,
'key': {
'key_code': 73,
'char': 'I',
},
});
// below three should not change.
expect(control.$el.find('input:checkbox[name="alt_option"]')[0].checked).toBeTruthy();
expect(control.$el.find('input:text[name="key"]')[0].value).toBe('I');
expect(control.$el.find('input:checkbox[name="control"]')[0].checked).toBeTruthy();
done();
}, 100);
});
it('when innerModel "alt_option" value changes UI and model should update new "alt_option" value', function (done) {
expect(control.$el.find('input:checkbox[name="alt_option"]')[0].checked).toBeTruthy();
expect(model.get(field.get('name'))).toEqual({
'control': true,
'shift': false,
'alt_option': true,
'key': {
'key_code': 73,
'char': 'I',
},
});
control.innerModel.set('alt_option', false);
// wait until UI updates.
setTimeout(function() {
// this should change
expect(control.$el.find('input:checkbox[name="alt_option"]')[0].checked).toBeFalsy();
expect(model.get(field.get('name'))).toEqual({
'control': true,
'shift': false,
'alt_option': false,
'key': {
'key_code': 73,
'char': 'I',
},
});
// below three should not change.
expect(control.$el.find('input:checkbox[name="shift"]')[0].checked).toBeFalsy();
expect(control.$el.find('input:text[name="key"]')[0].value).toBe('I');
expect(control.$el.find('input:checkbox[name="control"]')[0].checked).toBeTruthy();
done();
}, 100);
});
});
describe('remove keyboardShortcut control', function () {
beforeEach(function() {
spyOn(control, 'cleanup').and.callThrough();
});
it('when removed it should remove all of it\' controls', function () {
control.remove();
expect(control.cleanup).toHaveBeenCalled();
expect(control.controls.length).toBe(0);
});
});
});
});

View File

@ -0,0 +1,199 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2018, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
define([
'backbone',
'pgadmin.backform',
], function (Backbone, Backform) {
describe('keyCodeControl', function () {
let field, control, model, event;
beforeEach(() => {
model = new Backbone.Model({
'key': {
'key_code': 65,
'char': 'A',
}
});
field = new Backform.Field({
id: 'key',
name: 'key',
control: 'keyCode',
label: 'Key',
});
control = new (field.get('control')) ({
field: field,
model: model,
});
control.render();
event = {
which: -1,
keyCode: -1,
key: '',
preventDefault: jasmine.createSpy('preventDefault'),
};
});
describe('onkeyDown', function () {
beforeEach((done) => {
spyOn(model, 'set').and.callThrough();
spyOn(control, 'onkeyDown').and.callThrough();
done();
});
it('when key with escapeKeyCode is pressed model should not update', function (done) {
event.which = 16;
event.keyCode = 16;
event.key = 'Shift';
control.onkeyDown(event);
expect(control.onkeyDown).toHaveBeenCalled();
expect(model.set).not.toHaveBeenCalled();
expect(event.preventDefault).not.toHaveBeenCalled();
expect(model.get('key')).toEqual({
'key_code': 65,
'char': 'A',
});
// wait until UI updates.
setTimeout(function() {
expect(control.$el.find('input')[0].value).toBe('A');
done();
}, 100);
});
it('when key other than escapeKeyCode is pressed model should update', function (done) {
event.which = 66;
event.keyCode = 66;
event.key = 'B';
control.onkeyDown(event);
expect(control.onkeyDown).toHaveBeenCalled();
expect(model.set).toHaveBeenCalled();
expect(event.preventDefault).toHaveBeenCalled();
expect(model.get('key')).toEqual({
'key_code': 66,
'char': 'B',
});
// wait until UI updates.
setTimeout(function() {
expect(control.$el.find('input')[0].value).toBe('B');
done();
}, 100);
});
});
describe('onkeyUp', function () {
beforeEach((done) => {
spyOn(control, 'preventEvent').and.callThrough();
event.stopPropagation = jasmine.createSpy('stopPropagation');
event.stopImmediatePropagation = jasmine.createSpy('stopImmediatePropagation');
done();
});
it('when key with escapeKeyCode is pressed and released event should be propagated', function (done) {
event.which = 17;
event.keyCode = 17;
event.key = 'Ctrl';
control.preventEvent(event);
expect(control.preventEvent).toHaveBeenCalled();
expect(event.preventDefault).not.toHaveBeenCalled();
expect(event.stopPropagation).not.toHaveBeenCalled();
expect(event.stopImmediatePropagation).not.toHaveBeenCalled();
// wait until UI updates.
setTimeout(function() {
expect(control.$el.find('input')[0].value).toBe('A');
done();
}, 100);
});
it('when key other than escapeKeyCode is pressed and released event should not be propagated', function (done) {
event.which = 66;
event.keyCode = 66;
event.key = 'B';
control.preventEvent(event);
expect(control.preventEvent).toHaveBeenCalled();
expect(event.preventDefault).toHaveBeenCalled();
expect(event.stopPropagation).toHaveBeenCalled();
expect(event.stopImmediatePropagation).toHaveBeenCalled();
// wait until UI updates.
setTimeout(function() {
expect(control.$el.find('input')[0].value).toBe('A');
done();
}, 100);
});
});
describe('onModelChange', function () {
beforeEach((done) => {
done();
});
it('when model changes UI should update', function (done) {
expect(control.$el.find('input')[0].value).toBe('A');
model.set('key', {
'key_code': 67,
'char': 'C',
});
// wait until UI updates.
setTimeout(function() {
expect(control.$el.find('input')[0].value).toBe('C');
done();
}, 100);
});
});
});
});

View File

@ -12,6 +12,7 @@ module.exports = {
jQuery: 'jquery',
_: 'underscore',
'underscore.string': 'underscore.string',
'window.jQuery': 'jquery',
}),
],
@ -60,6 +61,13 @@ module.exports = {
'alertify': path.join(__dirname, './node_modules/alertifyjs/build/alertify'),
'jquery.event.drag': path.join(__dirname, './node_modules/slickgrid/lib/jquery.event.drag-2.2'),
'jquery.ui': path.join(__dirname, './node_modules/slickgrid/lib/jquery-ui-1.11.3'),
'spectrum': path.join(__dirname, './node_modules/spectrum-colorpicker/spectrum'),
'bignumber': path.join(__dirname, './node_modules/bignumber.js/bignumber'),
'bootstrap.datetimepicker': path.join(__dirname, './node_modules/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min'),
'bootstrap.switch': path.join(__dirname, './node_modules/bootstrap-switch/dist/js/bootstrap-switch'),
'backbone': path.join(__dirname, './node_modules/backbone/backbone'),
'backform': path.join(__dirname, './node_modules/backform/src/backform'),
'backgrid': path.join(__dirname, './node_modules/backgrid/lib/backgrid'),
'sources': sourcesDir + '/js',
'translations': regressionDir + '/javascript/fake_translations',
'pgadmin.browser.endpoints': regressionDir + '/javascript/fake_endpoints',
@ -70,6 +78,8 @@ module.exports = {
'pgadmin': sourcesDir + '/js/pgadmin',
'pgadmin.sqlfoldcode': sourcesDir + '/js/codemirror/addon/fold/pgadmin-sqlfoldcode',
'pgadmin.alertifyjs': sourcesDir + '/js/alertify.pgadmin.defaults',
'pgadmin.backgrid': sourcesDir + '/js/backgrid.pgadmin',
'pgadmin.backform': sourcesDir + '/js/backform.pgadmin',
},
},
externals: {