Hey Akshay and Neethu We refactored the patch to add tests for the resize feature. We were able to write test cases for the drag event by using spies and setting the rect dimensions. In cases like this, we can just test some components in order to have enough confidence in the code. So we isolated the function that implements the behavior of this feature and tested that it was performing as expected.
We ran the patch through the pipelines and all of the tests passed. Sincerely, Joao and Victoria On Wed, Mar 28, 2018 at 8:03 AM Akshay Joshi <akshay.jo...@enterprisedb.com> wrote: > Hi > > On Fri, Mar 2, 2018 at 3:40 AM, Joao De Almeida Pereira < > jdealmeidapere...@pivotal.io> wrote: > >> Hello Neethu, >> We passed the patch through our CI pipeline and all tests pass. >> The code looks good, but we are trying to decouple files as much as we >> can so that we do not end up with files with over 1000 lines, that are hard >> to read and to maintain. Also we are trying to create Unit Tests to have >> more coverage in our Javascript code. >> >> Can you split the new implementation code into it's own file and create >> some tests to ensure the behavior will not be broken in the future?iYou >> have some examples >> on: pgadmin/browser/server_groups/servers/databases/external_tables/* >> > > I have spilt the new implementation into different file. Its' been > hard to write jasmine/feature test case as it requires drag event and exact > co-ordinate to resize the slickgrid cell. > Attached is the modified patch. > > >> >> Thanks >> Joao >> >> On Thu, Mar 1, 2018 at 10:37 AM Neethu Mariya Joy < >> neethumariya...@gmail.com> wrote: >> >>> Hi, >>> I am Neethu Mariya Joy, an undergraduate pursuing BE in Computer Science >>> at BITS Pilani. >>> >>> I've attempted to fix https://redmine.postgresql.org/issues/3083. Since >>> the textarea resize feature is the default HTML feature, I have not changed >>> it. Instead, I've added draggable borders to the wrapper which expands the >>> textarea inside it. >>> >>> I'm attaching my patch as bug3083.diff below as per the contribution >>> guidelines. >>> >>> Hope this helps. Thank you for your consideration! >>> >>> Sincerely, >>> Neethu Mariya Joy >>> GitHub <https://github.com/Roboneet> | Linkedin >>> <https://www.linkedin.com/in/neethu-mariya-joy-653655128/> >>> >>> >>> > > > -- > *Akshay Joshi* > > *Sr. Software Architect * > > > > *Phone: +91 20-3058-9517 <+91%2020%203058%209517>Mobile: +91 976-788-8246 > <+91%2097678%2088246>* >
diff --git a/web/pgadmin/static/js/slickgrid/editors.js b/web/pgadmin/static/js/slickgrid/editors.js index 7652bf3b..a7df4baf 100644 --- a/web/pgadmin/static/js/slickgrid/editors.js +++ b/web/pgadmin/static/js/slickgrid/editors.js @@ -3,8 +3,9 @@ * @module Editors * @namespace Slick */ +import {resizeContentOnDrag} from 'resize_editor'; -(function($) { +(function ($) { // register namespace $.extend(true, window, { 'Slick': { @@ -52,6 +53,7 @@ val = $.trim(val); return !(val != '' && (val.charAt(0) != '{' || val.charAt(val.length - 1) != '}')); } + /* * This function handles the [default] and [null] values for cells * if row is copied, otherwise returns the editor value. @@ -129,7 +131,7 @@ var defaultValue; var scope = this; - this.init = function() { + this.init = function () { var $container = $('body'); $wrapper = getWrapper().appendTo($container); @@ -140,11 +142,12 @@ $buttons.find('button:last').on('click', this.cancel); $input.bind('keydown', this.handleKeyDown); + resizeContentOnDrag($wrapper, $input); scope.position(args.position); $input.focus().select(); }; - this.handleKeyDown = function(e) { + this.handleKeyDown = function (e) { if (e.which == $.ui.keyCode.ENTER && e.ctrlKey) { scope.save(); } else if (e.which == $.ui.keyCode.ESCAPE) { @@ -159,40 +162,40 @@ } }; - this.save = function() { + this.save = function () { args.commitChanges(); }; - this.cancel = function() { + this.cancel = function () { $input.val(defaultValue); args.cancelChanges(); }; - this.hide = function() { + this.hide = function () { $wrapper.hide(); }; - this.show = function() { + this.show = function () { $wrapper.show(); }; - this.position = function(position) { + this.position = function (position) { calculateEditorPosition(position, $wrapper); $wrapper .css('top', position.top) .css('left', position.left); }; - this.destroy = function() { + this.destroy = function () { $wrapper.remove(); }; - this.focus = function() { + this.focus = function () { $input.focus(); }; // When text editor opens - this.loadValue = function(item) { + this.loadValue = function (item) { if ( _.isUndefined(item[args.column.field]) || _.isNull(item[args.column.field]) @@ -218,7 +221,7 @@ } }; - this.serializeValue = function() { + this.serializeValue = function () { var value = $input.val(); // If empty return null @@ -241,11 +244,11 @@ } }; - this.applyValue = function(item, state) { + this.applyValue = function (item, state) { setValue(args, item, state, 'text'); }; - this.isValueChanged = function() { + this.isValueChanged = function () { // Use _.isNull(value) for comparison for null instead of // defaultValue == null, because it returns true for undefined value. if ($input.val() == '' && _.isUndefined(defaultValue)) { @@ -256,7 +259,7 @@ } }; - this.validate = function() { + this.validate = function () { if (args.column.validator) { var validationResults = args.column.validator($input.val()); if (!validationResults.valid) { @@ -286,7 +289,7 @@ var defaultValue; var scope = this; - this.init = function() { + this.init = function () { var $container = $('body'); $wrapper = getWrapper().appendTo($container); @@ -297,11 +300,12 @@ $buttons.find('button:last').on('click', this.cancel); $input.bind('keydown', this.handleKeyDown); + resizeContentOnDrag($wrapper, $input); scope.position(args.position); $input.focus().select(); }; - this.handleKeyDown = function(e) { + this.handleKeyDown = function (e) { if (e.which == $.ui.keyCode.ENTER && e.ctrlKey) { scope.save(); } else if (e.which == $.ui.keyCode.ESCAPE) { @@ -316,45 +320,45 @@ } }; - this.save = function() { + this.save = function () { args.commitChanges(); }; - this.cancel = function() { + this.cancel = function () { $input.val(defaultValue); args.cancelChanges(); }; - this.hide = function() { + this.hide = function () { $wrapper.hide(); }; - this.show = function() { + this.show = function () { $wrapper.show(); }; - this.position = function(position) { + this.position = function (position) { calculateEditorPosition(position, $wrapper); $wrapper .css('top', position.top) .css('left', position.left); }; - this.destroy = function() { + this.destroy = function () { $wrapper.remove(); }; - this.focus = function() { + this.focus = function () { $input.focus(); }; - this.loadValue = function(item) { + this.loadValue = function (item) { var data = defaultValue = item[args.column.field]; if (data && typeof data === 'object' && !Array.isArray(data)) { data = JSON.stringify(data, null, 4); } else if (Array.isArray(data)) { var temp = []; - $.each(data, function(i, val) { + $.each(data, function (i, val) { if (typeof val === 'object') { temp.push(JSON.stringify(val, null, 4)); } else { @@ -367,18 +371,18 @@ $input.select(); }; - this.serializeValue = function() { + this.serializeValue = function () { if ($input.val() === '') { return null; } return $input.val(); }; - this.applyValue = function(item, state) { + this.applyValue = function (item, state) { setValue(args, item, state, 'text'); }; - this.isValueChanged = function() { + this.isValueChanged = function () { if ($input.val() == '' && _.isUndefined(defaultValue)) { return false; } else { @@ -386,7 +390,7 @@ } }; - this.validate = function() { + this.validate = function () { if (args.column.validator) { var validationResults = args.column.validator($input.val()); if (!validationResults.valid) { @@ -409,7 +413,7 @@ var defaultValue; var scope = this; - this.init = function() { + this.init = function () { var $container = $('body'); $wrapper = getWrapper().appendTo($container); @@ -419,11 +423,12 @@ $buttons.find('button:first').on('click', this.cancel); $input.bind('keydown', this.handleKeyDown); + resizeContentOnDrag($wrapper, $input); scope.position(args.position); $input.focus().select(); }; - this.handleKeyDown = function(e) { + this.handleKeyDown = function (e) { if (e.which == $.ui.keyCode.ENTER && e.ctrlKey) { scope.cancel(); } else if (e.which == $.ui.keyCode.ESCAPE) { @@ -440,52 +445,52 @@ } }; - this.cancel = function() { + this.cancel = function () { $input.val(defaultValue); args.cancelChanges(); }; - this.hide = function() { + this.hide = function () { $wrapper.hide(); }; - this.show = function() { + this.show = function () { $wrapper.show(); }; - this.position = function(position) { + this.position = function (position) { calculateEditorPosition(position, $wrapper); $wrapper .css('top', position.top) .css('left', position.left); }; - this.destroy = function() { + this.destroy = function () { $wrapper.remove(); }; - this.focus = function() { + this.focus = function () { $input.focus(); }; - this.loadValue = function(item) { + this.loadValue = function (item) { $input.val(defaultValue = item[args.column.field]); $input.select(); }; - this.serializeValue = function() { + this.serializeValue = function () { return $input.val(); }; - this.applyValue = function(item, state) { + this.applyValue = function (item, state) { item[args.column.field] = state; }; - this.isValueChanged = function() { + this.isValueChanged = function () { return (!($input.val() == '' && defaultValue == null)) && ($input.val() != defaultValue); }; - this.validate = function() { + this.validate = function () { if (args.column.validator) { var validationResults = args.column.validator($input.val()); if (!validationResults.valid) { @@ -508,7 +513,7 @@ var defaultValue; var scope = this; - this.init = function() { + this.init = function () { var $container = $('body'); $wrapper = getWrapper().appendTo($container); @@ -518,11 +523,12 @@ $buttons.find('button:first').on('click', this.cancel); $input.bind('keydown', this.handleKeyDown); + resizeContentOnDrag($wrapper, $input); scope.position(args.position); $input.focus().select(); }; - this.handleKeyDown = function(e) { + this.handleKeyDown = function (e) { if (e.which == $.ui.keyCode.ENTER && e.ctrlKey) { scope.cancel(); } else if (e.which == $.ui.keyCode.ESCAPE) { @@ -539,41 +545,41 @@ } }; - this.cancel = function() { + this.cancel = function () { $input.val(defaultValue); args.cancelChanges(); }; - this.hide = function() { + this.hide = function () { $wrapper.hide(); }; - this.show = function() { + this.show = function () { $wrapper.show(); }; - this.position = function(position) { + this.position = function (position) { calculateEditorPosition(position, $wrapper); $wrapper .css('top', position.top) .css('left', position.left); }; - this.destroy = function() { + this.destroy = function () { $wrapper.remove(); }; - this.focus = function() { + this.focus = function () { $input.focus(); }; - this.loadValue = function(item) { + this.loadValue = function (item) { var data = defaultValue = item[args.column.field]; if (typeof data === 'object' && !Array.isArray(data)) { data = JSON.stringify(data, null, 4); } else if (Array.isArray(data)) { var temp = []; - $.each(data, function(i, val) { + $.each(data, function (i, val) { if (typeof val === 'object') { temp.push(JSON.stringify(val, null, 4)); } else { @@ -586,19 +592,19 @@ $input.select(); }; - this.serializeValue = function() { + this.serializeValue = function () { return $input.val(); }; - this.applyValue = function(item, state) { + this.applyValue = function (item, state) { item[args.column.field] = state; }; - this.isValueChanged = function() { + this.isValueChanged = function () { return (!($input.val() == '' && defaultValue == null)) && ($input.val() != defaultValue); }; - this.validate = function() { + this.validate = function () { if (args.column.validator) { var validationResults = args.column.validator($input.val()); if (!validationResults.valid) { @@ -619,10 +625,10 @@ var $input; var defaultValue; - this.init = function() { + this.init = function () { $input = $('<INPUT type=text class=\'editor-text\' readonly/>') .appendTo(args.container) - .bind('keydown.nav', function(e) { + .bind('keydown.nav', function (e) { if (e.keyCode === $.ui.keyCode.LEFT || e.keyCode === $.ui.keyCode.RIGHT) { e.stopImmediatePropagation(); } @@ -631,19 +637,19 @@ .select(); }; - this.destroy = function() { + this.destroy = function () { $input.remove(); }; - this.focus = function() { + this.focus = function () { $input.focus(); }; - this.getValue = function() { + this.getValue = function () { return $input.val(); }; - this.loadValue = function(item) { + this.loadValue = function (item) { var value = item[args.column.field]; // Check if value is null or undefined @@ -656,19 +662,19 @@ $input.select(); }; - this.serializeValue = function() { + this.serializeValue = function () { return $input.val(); }; - this.applyValue = function(item, state) { + this.applyValue = function (item, state) { item[args.column.field] = state; }; - this.isValueChanged = function() { + this.isValueChanged = function () { return (!($input.val() == '' && defaultValue == null)) && ($input.val() != defaultValue); }; - this.validate = function() { + this.validate = function () { if (args.column.validator) { var validationResults = args.column.validator($input.val()); if (!validationResults.valid) { @@ -689,21 +695,21 @@ var $select; var defaultValue; - this.init = function() { + this.init = function () { $select = $('<INPUT type=checkbox value=\'true\' class=\'editor-checkbox\' hideFocus disabled>'); $select.appendTo(args.container); $select.focus(); }; - this.destroy = function() { + this.destroy = function () { $select.remove(); }; - this.focus = function() { + this.focus = function () { $select.focus(); }; - this.loadValue = function(item) { + this.loadValue = function (item) { defaultValue = item[args.column.pos]; if (_.isNull(defaultValue) || _.isUndefined(defaultValue)) { $select.prop('indeterminate', true); @@ -720,25 +726,25 @@ } }; - this.serializeValue = function() { + this.serializeValue = function () { if ($select.prop('indeterminate')) { return null; } return $select.prop('checked'); }; - this.applyValue = function(item, state) { + this.applyValue = function (item, state) { item[args.column.pos] = state; }; - this.isValueChanged = function() { + this.isValueChanged = function () { // var select_value = this.serializeValue(); var select_value = $select.data('checked'); return (!(select_value === 2 && (defaultValue == null || defaultValue == undefined))) && (select_value !== defaultValue); }; - this.validate = function() { + this.validate = function () { return { valid: true, msg: null, @@ -752,10 +758,10 @@ var $input; var defaultValue; - this.init = function() { + this.init = function () { $input = $('<INPUT type=text class=\'editor-text\' />'); - $input.bind('keydown.nav', function(e) { + $input.bind('keydown.nav', function (e) { if (e.keyCode === $.ui.keyCode.LEFT || e.keyCode === $.ui.keyCode.RIGHT) { e.stopImmediatePropagation(); } @@ -765,15 +771,15 @@ $input.focus().select(); }; - this.destroy = function() { + this.destroy = function () { $input.remove(); }; - this.focus = function() { + this.focus = function () { $input.focus(); }; - this.loadValue = function(item) { + this.loadValue = function (item) { defaultValue = item[args.column.field]; if (args.column.is_array && !_.isNull(defaultValue) && !_.isUndefined(defaultValue)) { @@ -786,7 +792,7 @@ $input.select(); }; - this.serializeValue = function() { + this.serializeValue = function () { var value = $input.val(); if (value === '') { @@ -813,11 +819,11 @@ return value; }; - this.applyValue = function(item, state) { + this.applyValue = function (item, state) { setValue(args, item, state, 'number'); }; - this.isValueChanged = function() { + this.isValueChanged = function () { if ($input.val() == '' && _.isUndefined(defaultValue)) { return false; } else if ($input.val() == '' && defaultValue == '') { @@ -828,7 +834,7 @@ } }; - this.validate = function() { + this.validate = function () { var value = $input.val(); if (!args.column.is_array && isNaN(value)) { return { @@ -885,13 +891,13 @@ var $select, el; var defaultValue, previousState; - this.init = function() { + this.init = function () { $select = $('<div class=\'multi-checkbox\'><span class=\'check\' hideFocus></span></div>'); $select.appendTo(args.container); $select.focus(); // The following code is taken from https://css-tricks.com/indeterminate-checkboxes/ - $select.bind('click', function() { + $select.bind('click', function () { el = $(this); var states = ['unchecked', 'partial', 'checked']; var curState = el.find('.check').data('state'); @@ -903,15 +909,15 @@ }); }; - this.destroy = function() { + this.destroy = function () { $select.remove(); }; - this.focus = function() { + this.focus = function () { $select.focus(); }; - this.loadValue = function(item) { + this.loadValue = function (item) { defaultValue = item[args.column.field]; previousState = 1; if (_.isNull(defaultValue) || _.isUndefined(defaultValue)) { @@ -928,23 +934,23 @@ } }; - this.serializeValue = function() { + this.serializeValue = function () { if ($select.find('.check').data('state') == 1) { return null; } return $select.find('.check').data('state') == 2 ? true : false; }; - this.applyValue = function(item, state) { + this.applyValue = function (item, state) { item[args.column.field] = state; }; - this.isValueChanged = function() { + this.isValueChanged = function () { var currentState = $select.find('.check').data('state'); return currentState !== previousState; }; - this.validate = function() { + this.validate = function () { if (args.column.validator) { var validationResults = args.column.validator(this.serializeValue()); if (!validationResults.valid) { diff --git a/web/pgadmin/static/js/slickgrid/resize_editor.js b/web/pgadmin/static/js/slickgrid/resize_editor.js new file mode 100644 index 00000000..7e781cda --- /dev/null +++ b/web/pgadmin/static/js/slickgrid/resize_editor.js @@ -0,0 +1,63 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +export function resizeContentOnDrag($wrapper, $input) { + // right border, bottom border and right bottom corner of the wrapper are draggable + $wrapper.append('<div class="drag-border" data="right"></div>\ + <div class="drag-border" data="bottom"></div>\ + <div class="drag-border" data="both"></div>'); + + $wrapper.find('.drag-border').on('drag', (event) => { + onDragEvent(event, $input); + }); +} + +function dragEnded(mouseX, mouseY) { + return mouseX === 0 && mouseY === 0; +} + +export function onDragEvent(event, $input) { + event.preventDefault(); + const mouseX = event.clientX; + const mouseY = event.clientY; + + if (dragEnded(mouseX, mouseY)) { + return; + } + + // default spacing between $input and cursor + const paddingBottom = 30; + const paddingRight = 10; + const dir = event.target.getAttribute('data'); + + // size of $input is changed according to cursor position + switch (dir) { + case 'right': + changeWidth($input, mouseX, paddingRight); + break; + case 'bottom': + changeHeight($input, mouseY, paddingBottom); + break; + case 'both': + changeHeight($input, mouseY, paddingBottom); + changeWidth($input, mouseX, paddingRight); + } +} + +function changeWidth($input, mouseX, padding) { + const rect = $input[0].getBoundingClientRect(); + const newWidth = rect.width + mouseX - rect.right - padding; + $input.css('width', newWidth.toString() + 'px'); +} + +function changeHeight($input, mouseY, padding) { + const rect = $input[0].getBoundingClientRect(); + const newHeight = rect.height + mouseY - rect.bottom - padding; + $input.css('height', newHeight.toString() + 'px'); +} diff --git a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css index 46588dce..034e4050 100644 --- a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css +++ b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css @@ -505,6 +505,38 @@ input.editor-checkbox:focus { -moz-border-radius:10px; border-radius:10px; } + +.drag-border{ + background: transparent; + position: absolute; +} + +.drag-border[data=right]{ + cursor: ew-resize; + top: 0; + right: -10px; + bottom: 0; + width: 20px; +} + +.drag-border[data=bottom]{ + cursor: ns-resize; + position: absolute; + left: 0; + right: 0; + bottom: -10px; + height: 20px; +} + +.drag-border[data=both]{ + cursor: move; + position: absolute; + bottom: -10px; + right: -10px; + width: 20px; + height: 20px; +} + .pg_textarea { backround:#fff; width:250px; diff --git a/web/regression/javascript/selection/resize_editor_spec.js b/web/regression/javascript/selection/resize_editor_spec.js new file mode 100644 index 00000000..30080d97 --- /dev/null +++ b/web/regression/javascript/selection/resize_editor_spec.js @@ -0,0 +1,113 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import {onDragEvent} from "../../../pgadmin/static/js/slickgrid/resize_editor"; + +const context = describe; + +describe('onDragEvent', () => { + context('when the drag event ends', () => { + let event; + let input; + beforeEach(() => { + event = {clientX: 0, clientY: 0}; + event.preventDefault = jasmine.createSpy('preventDefault'); + input = { + css: jasmine.createSpy('css'), + }; + + onDragEvent(event, input); + }); + + it('prevents default action from being triggered on event', () => { + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('does not change dimensions of element', () => { + expect(input.css).not.toHaveBeenCalled(); + }); + }); + + context('when the drag event is in progress', () => { + let event; + let input; + beforeEach(() => { + event = { + clientX: 50, + clientY: 101, + target: jasmine.createSpyObj('target', ['getAttribute']), + }; + event.preventDefault = jasmine.createSpy('preventDefault'); + input = [{ + getBoundingClientRect: jasmine.createSpy('getBoundingClientRect') + }]; + input.css = jasmine.createSpy('css'); + }); + + context('drag to the right', () => { + beforeEach(() => { + event.target.getAttribute.and.returnValue('right'); + input[0].getBoundingClientRect.and.returnValue({ + width: 11, + right: 9, + }); + onDragEvent(event, input); + }); + + it('prevents default action from being triggered on event', () => { + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('increases the width of the element', () => { + expect(input.css).toHaveBeenCalledWith('width', '42px'); + }); + }); + + context('drag to the bottom', () => { + beforeEach(() => { + event.target.getAttribute.and.returnValue('bottom'); + input[0].getBoundingClientRect.and.returnValue({ + height: 20, + bottom: 30, + }); + onDragEvent(event, input); + }); + + it('prevents default action from being triggered on event', () => { + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('increases the height of the element', () => { + expect(input.css).toHaveBeenCalledWith('height', '61px'); + }); + }); + + context('drag to the bottom and right at the same time', () => { + beforeEach(() => { + event.target.getAttribute.and.returnValue('both'); + input[0].getBoundingClientRect.and.returnValue({ + width: 10, + height: 19, + right: 10, + bottom: 30, + }); + onDragEvent(event, input); + }); + + it('prevents default action from being triggered on event', () => { + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('increases the height of the element', () => { + expect(input.css).toHaveBeenCalledWith('width', '40px'); + expect(input.css).toHaveBeenCalledWith('height', '60px'); + }); + }); + }); +});