Subject: Bug #3083 fix

From: Neethu Mariya Joy <[email-masked]>
Date: Tue, 27 Feb 2018 22:47:18 +0530

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/>
diff --git a/web/pgadmin/static/js/slickgrid/editors.js b/web/pgadmin/static/js/slickgrid/editors.js
index 7652bf3..d369928 100644
--- a/web/pgadmin/static/js/slickgrid/editors.js
+++ b/web/pgadmin/static/js/slickgrid/editors.js
@@ -123,6 +123,53 @@
     return position;
   }
 
+  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)=>{
+      event.preventDefault();
+      var mouseX = event.clientX;
+      var mouseY = event.clientY;
+
+      // mouseX == 0 && mouseY == 0 mouse up / end of drag
+      if(mouseX == 0 && mouseY == 0)return;
+      
+      // default spacing between $input and cursor
+      var paddingBottom = 30;
+      var paddingRight = 10;
+      var 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){
+    var rect = $input[0].getBoundingClientRect();    
+    var newWidth = rect.width + mouseX - rect.right - padding;
+    $input.css('width', newWidth.toString() + 'px');
+  }
+
+  function changeHeight($input, mouseY, padding){
+    var rect = $input[0].getBoundingClientRect();
+    var newHeight = rect.height + mouseY - rect.bottom - padding;
+    $input.css('height', newHeight.toString() + 'px');
+  }
+
+
   // Text data type editor
   function pgTextEditor(args) {
     var $input, $wrapper, $buttons;
@@ -140,6 +187,7 @@
       $buttons.find('button:last').on('click', this.cancel);
       $input.bind('keydown', this.handleKeyDown);
 
+      resizeContentOnDrag($wrapper, $input);
       scope.position(args.position);
       $input.focus().select();
     };
@@ -297,6 +345,7 @@
       $buttons.find('button:last').on('click', this.cancel);
       $input.bind('keydown', this.handleKeyDown);
 
+      resizeContentOnDrag($wrapper, $input);
       scope.position(args.position);
       $input.focus().select();
     };
@@ -419,6 +468,7 @@
       $buttons.find('button:first').on('click', this.cancel);
       $input.bind('keydown', this.handleKeyDown);
 
+      resizeContentOnDrag($wrapper, $input);
       scope.position(args.position);
       $input.focus().select();
     };
@@ -518,6 +568,7 @@
       $buttons.find('button:first').on('click', this.cancel);
       $input.bind('keydown', this.handleKeyDown);
 
+      resizeContentOnDrag($wrapper, $input);
       scope.position(args.position);
       $input.focus().select();
     };
diff --git a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
index 1e29c3f..288e57f 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;

---

From: Joao De Almeida Pereira <[email-masked]>
Date: Thu, 01 Mar 2018 22:10:54 +0000

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

Thanks
Joao

On Thu, Mar 1, 2018 at 10:37 AM Neethu Mariya Joy <[email-masked]>
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.
>

---

From: Akshay Joshi <[email-masked]>
Date: Wed, 28 Mar 2018 17:33:03 +0530

Hi

On Fri, Mar 2, 2018 at 3:40 AM, Joao De Almeida Pereira <
[email-masked]> wrote:

> 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.
>
> 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 <
> [email-masked]> wrote:
>
>> Hi,

-- 
*Akshay Joshi*

*Sr. Software Architect *

*Phone: +91 20-3058-9517Mobile: +91 976-788-8246*

---

From: Joao De Almeida Pereira <[email-masked]>
Date: Wed, 28 Mar 2018 18:06:23 +0000

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 <[email-masked]>
wrote:

>     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
> *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);

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

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

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

-    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');
+      });
+    });
+  });
+});

---

From: Akshay Joshi <[email-masked]>
Date: Thu, 29 Mar 2018 16:02:35 +0530

Thanks Joao.

On Wed, Mar 28, 2018 at 11:36 PM, Joao De Almeida Pereira <
[email-masked]> wrote:

> On Wed, Mar 28, 2018 at 8:03 AM Akshay Joshi <
> [email-masked]> 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.
>>>>

-- 
*Akshay Joshi*

---

From: Dave Page <[email-masked]>
Date: Thu, 29 Mar 2018 14:03:24 +0100

Hi

> We ran the patch through the pipelines and all of the tests passed.

I'm consistently seeing the feature test failure below with this patch
applied:

======================================================================
FAIL: runTest
(pgadmin.feature_tests.view_data_dml_queries.CheckForViewDataTest)
Validate Insert, Update operations in View/Edit data with given test data
----------------------------------------------------------------------
Traceback (most recent call last):
  File
"/Users/dpage/git/pgadmin4/web/pgadmin/feature_tests/view_data_dml_queries.py",
line 125, in runTest
    self._verify_row_data(True)
  File
"/Users/dpage/git/pgadmin4/web/pgadmin/feature_tests/view_data_dml_queries.py",
line 325, in _verify_row_data
    self.assertEquals(cells[idx], config_data[str(idx)][1])
AssertionError: u'[null]' != u'1'
- [null]
+ 1

-- 
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake

EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

---

From: Joao De Almeida Pereira <[email-masked]>
Date: Thu, 29 Mar 2018 15:29:20 +0000

Hi Dave,
That looks like in the surrounding area of the change. We run our pipeline
and everything was green.
Can you provide more details, which python version are you using? OS?

Thanks
Joao

On Thu, Mar 29, 2018 at 9:03 AM Dave Page <[email-masked]> wrote:

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

---

From: Dave Page <[email-masked]>
Date: Tue, 3 Apr 2018 13:38:36 +0100

Hi

> Can you provide more details, which python version are you using? OS?

That was on my travel laptop, which is macOS Sierra with the Apple supplied
Python 2.7.

Interestingly, I'm on my dev laptop today (same OS and Python) and it's
working just fine. The difference is that the travel machine is a 12"
Macbook, whilst the dev machine is

>> FAIL: runTest (pgadmin.feature_tests.view_data_dml_queries.
>> CheckForViewDataTest)
>>   File "/Users/dpage/git/pgadmin4/web/pgadmin/feature_tests/view_data_dml_queries.py",
>> line 125, in runTest
>>   File "/Users/dpage/git/pgadmin4/web/pgadmin/feature_tests/view_data_dml_queries.py",

---

From: Dave Page <[email-masked]>
Date: Tue, 3 Apr 2018 13:40:18 +0100

Argh, managed to send before I finished typing...

On Tue, Apr 3, 2018 at 1:38 PM, Dave Page <[email-masked]> wrote:

>> That looks like in the surrounding area of the change. We run our
>> pipeline and everything was green.
> That was on my travel laptop, which is macOS Sierra with the Apple
> supplied Python 2.7.
>

a 15" MacBook Pro with 2 24" external monitors. That makes me wonder if the
small screen size is causing a problem with this test, something we have
seen before.

>>> FAIL: runTest (pgadmin.feature_tests.view_da
>>> ta_dml_queries.CheckForViewDataTest)

---

From: Harshal Dhumal <[email-masked]>
Date: Tue, 3 Apr 2018 18:33:55 +0530

-- 
*Harshal Dhumal*
*Sr. Software Engineer*

EnterpriseDB India: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

On Tue, Apr 3, 2018 at 6:10 PM, Dave Page <[email-masked]> wrote:

> a 15" MacBook Pro with 2 24" external monitors. That makes me wonder if
> the small screen size is causing a problem with this test, something we
> have seen before.
>
>

Yes, screen size does cause problem. Slick grid does not render all columns
if viewport is not wide enough (like it does for rows).
Remaining columns would render when user scrolls right.

To avoid similar problem in datatype feature test (commit:
88bcd3b5129db88975421e26c1bf188daf4892f9
<https://git.postgresql.org/gitweb/?p=pgadmin4.git;a=commitdiff;h=88bcd3b5129db88975421e26c1bf188daf4892f9>)
I have executed
queries in batch to limit number of columns in single query result.

---

From: Dave Page <[email-masked]>
Date: Wed, 11 Apr 2018 10:56:04 +0100

Joao, are you looking at this?

On Tue, Apr 3, 2018 at 2:03 PM, Harshal Dhumal <
[email-masked]> wrote:

> Yes, screen size does cause problem. Slick grid does not render all
> columns if viewport is not wide enough (like it does for rows).

---

From: Joao De Almeida Pereira <[email-masked]>
Date: Wed, 11 Apr 2018 15:46:35 +0000

Hi Dave,
Do you still have issues in your environment?

Thanks
Joao

On Wed, Apr 11, 2018 at 5:56 AM Dave Page <[email-masked]> wrote:

>>>>>>> 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.
>>>>>>>
>>>>>> I'm consistently seeing the feature test failure below with this
>>>>>> patch applied:
>>>>>>

---

From: Dave Page <[email-masked]>
Date: Wed, 11 Apr 2018 17:04:17 +0100

Hi

> Do you still have issues in your environment?

I just tested on my MBP, and got the following failure:

======================================================================
ERROR: runTest (pgadmin.feature_tests.query_tool_tests.QueryToolFeatureTest)
Query tool feature test
----------------------------------------------------------------------
Traceback (most recent call last):
  File
"/Users/dpage/git/pgadmin4/web/regression/feature_utils/base_feature_test.py",
line 43, in setUp
    self.before()
  File
"/Users/dpage/git/pgadmin4/web/pgadmin/feature_tests/query_tool_tests.py",
line 46, in before
    self._reset_options()
  File
"/Users/dpage/git/pgadmin4/web/pgadmin/feature_tests/query_tool_tests.py",
line 113, in _reset_options
    self.page.fill_codemirror_area_with('')
  File
"/Users/dpage/git/pgadmin4/web/regression/feature_utils/pgadmin_page.py",
line 227, in fill_codemirror_area_with
    until(find_codemirror, "Timed out waiting for codemirror "
  File
"/Users/dpage/.virtualenvs/pgadmin4/lib/python2.7/site-packages/selenium/webdriver/remote/webelement.py",
line 80, in click
    self._execute(Command.CLICK_ELEMENT)
  File
"/Users/dpage/.virtualenvs/pgadmin4/lib/python2.7/site-packages/selenium/webdriver/remote/webelement.py",
line 628, in _execute
    return self._parent.execute(command, params)
  File
"/Users/dpage/.virtualenvs/pgadmin4/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py",
line 312, in execute
    self.error_handler.check_response(response)
  File
"/Users/dpage/.virtualenvs/pgadmin4/lib/python2.7/site-packages/selenium/webdriver/remote/errorhandler.py",
line 242, in check_response
    raise exception_class(message, screen, stacktrace)
WebDriverException: Message: unknown error: Element <iframe
cd_frame_id_="6af0765089f43a73f733471ef04cb792">...</iframe> is not
clickable at point (813, 155). Other element would receive the click: <div
class="wcLoadingIconContainer">...</div>
  (Session info: chrome=65.0.3325.181)
  (Driver info: chromedriver=2.37.544337
(8c0344a12e552148c185f7d5117db1f28d6c9e85),platform=Mac OS X 10.12.6 x86_64)

>>>>> Interestingly, I'm on my dev laptop today (same OS and Python) and
>>>>> it's working just fine. The difference is that the travel machine is a 12"