From e9af25663ae0ca1c8d3f42d999a7779ad7849c6e Mon Sep 17 00:00:00 2001
From: Tira + Joao <aodhner+jpereira@pivotal.io>
Date: Tue, 14 Mar 2017 16:54:22 -0400
Subject: [PATCH 01/11] Add column selector to SQLEditor

- Update README with xsel package to enable testing with Pyperclip on linux
---
 test/javascript/column_selector_spec.js            | 137 +++++++++++++++++++++
 web/pgadmin/static/js/selection/column_selector.js |  67 ++++++++++
 .../sqleditor/templates/sqleditor/js/sqleditor.js  |   8 +-
 web/regression/README                              |   6 +
 web/regression/requirements.txt                    |   1 +
 5 files changed, 217 insertions(+), 2 deletions(-)
 create mode 100644 test/javascript/column_selector_spec.js
 create mode 100644 web/pgadmin/static/js/selection/column_selector.js

diff --git a/test/javascript/column_selector_spec.js b/test/javascript/column_selector_spec.js
new file mode 100644
index 00000000..30a98d42
--- /dev/null
+++ b/test/javascript/column_selector_spec.js
@@ -0,0 +1,137 @@
+define(
+  ["jquery",
+    "underscore",
+    "slickgrid/slick.grid",
+    "sources/selection/column_selector",
+    "slickgrid/slick.rowselectionmodel"
+  ],
+  function ($, _, SlickGrid, ColumnSelector, RowSelectionModel) {
+    describe("ColumnSelector", function () {
+      var container, data, columns, options;
+      beforeEach(function () {
+        container = $("<div></div>");
+        data = [{'some-column-name': 'first value', 'second column': 'second value'}];
+
+        columns = [
+          {
+            id: '1',
+            name: 'some-column-name',
+            selectable: true
+          },
+          {
+            id: '2',
+            name: 'second column',
+            selectable: true
+          }]
+      });
+
+      it("renders a checkbox in the column header", function () {
+        var columnSelector = new ColumnSelector(columns);
+        columns = columnSelector.getColumnsWithCheckboxes();
+        var grid = new SlickGrid(container, data, columns, options);
+
+        grid.registerPlugin(columnSelector);
+        grid.invalidate();
+
+        expect(container.find('.slick-header-columns input').length).toBe(2)
+      });
+
+      it("displays the name of the column", function () {
+        var columnSelector = new ColumnSelector(columns);
+        columns = columnSelector.getColumnsWithCheckboxes();
+        var grid = new SlickGrid(container, data, columns, options);
+
+        grid.registerPlugin(columnSelector);
+        grid.invalidate();
+
+        expect($(container.find('.slick-header-columns .slick-column-name')[0]).text()).toBe('some-column-name');
+        expect($(container.find('.slick-header-columns .slick-column-name')[1]).text()).toBe('second column');
+      });
+
+      it("preserves the other attributes of column definitions", function () {
+        var columnSelector = new ColumnSelector(columns);
+        var selectableColumns = columnSelector.getColumnsWithCheckboxes();
+
+        expect(selectableColumns[0].id).toBe('1');
+        expect(selectableColumns[0].selectable).toBe(true);
+      });
+
+      describe("when the user clicks on a column header", function () {
+        var grid, rowSelectionModel;
+        beforeEach(function () {
+          var columnSelector = new ColumnSelector(columns);
+          columns = columnSelector.getColumnsWithCheckboxes();
+          data = [];
+          for (var i = 0; i < 10; i++) {
+            data.push({'some-column-name': 'some-value-' + i, 'second column': 'second value ' + i});
+          }
+          grid = new SlickGrid(container, data, columns, options);
+
+          rowSelectionModel = new RowSelectionModel();
+          grid.setSelectionModel(rowSelectionModel);
+          grid.registerPlugin(columnSelector);
+          grid.invalidate();
+          $("body").append(container);
+          container.find('.slick-header-column')[1].click();
+        });
+
+        afterEach(function () {
+          $("body").find(container).remove();
+        });
+
+        it("selects the column", function () {
+          var selectedRanges = rowSelectionModel.getSelectedRanges();
+          var column = selectedRanges[0];
+
+          expect(selectedRanges.length).toEqual(1);
+          expect(column.fromCell).toBe(1);
+          expect(column.toCell).toBe(1);
+          expect(column.fromRow).toBe(0);
+          expect(column.toRow).toBe(9);
+        });
+
+        it("selects another column", function () {
+          container.find('.slick-header-column')[0].click();
+
+          var selectedRanges = rowSelectionModel.getSelectedRanges();
+          var column1 = selectedRanges[0];
+
+          expect(selectedRanges.length).toEqual(2);
+          expect(column1.fromCell).toBe(1);
+          expect(column1.toCell).toBe(1);
+
+          var column2 = selectedRanges[1];
+
+          expect(column2.fromCell).toBe(0);
+          expect(column2.toCell).toBe(0);
+        });
+
+        it("allows clicking on the checkbox", function () {
+          container.find('.slick-header-columns input')[1].click();
+
+          expect($(container.find('.slick-header-columns input')[1]).is(':checked')).toBeFalsy();
+
+        });
+
+        it("checks the checkbox", function () {
+          expect($(container.find('.slick-header-columns input')[1]).is(':checked')).toBeTruthy();
+        });
+
+        describe("click a second time", function () {
+          beforeEach(function () {
+            container.find('.slick-header-column')[1].click();
+          });
+
+          it("unchecks checkbox", function () {
+            expect($(container.find('.slick-header-columns input')[1]).is(':checked')).toBeFalsy();
+          });
+
+          it("unselects the column", function () {
+            var selectedRanges = rowSelectionModel.getSelectedRanges();
+
+            expect(selectedRanges.length).toEqual(0);
+          })
+        });
+      });
+    });
+  });
\ No newline at end of file
diff --git a/web/pgadmin/static/js/selection/column_selector.js b/web/pgadmin/static/js/selection/column_selector.js
new file mode 100644
index 00000000..845b9a90
--- /dev/null
+++ b/web/pgadmin/static/js/selection/column_selector.js
@@ -0,0 +1,67 @@
+define(['jquery', 'slickgrid'], function ($) {
+  var ColumnSelector = function (columnDefinitions) {
+    var Slick = window.Slick;
+
+    var init = function (grid) {
+      grid.onHeaderClick.subscribe(function (e, eventArgument) {
+        var column = eventArgument.column;
+        var _grid = eventArgument.grid;
+
+        updateRanges(_grid, column);
+
+        if(!clickedCheckbox(e)) {
+          var $checkbox = $("[data-id='checkbox-" + column.id + "']");
+          toggleCheckbox($checkbox);
+        }
+      });
+    };
+
+    function updateRanges(_grid, column) {
+      var selectionModel = _grid.getSelectionModel();
+      var ranges = selectionModel.getSelectedRanges();
+
+      var columnIndex = _grid.getColumnIndex(column.id);
+
+      var filteredRanges = ranges.filter(function (range) {
+        return !range.contains(0, columnIndex);
+      });
+
+      if (isNotPreviouslySelectedColumn(filteredRanges, ranges)) {
+        var range = new Slick.Range(0, columnIndex, _grid.getDataLength() - 1, columnIndex);
+        filteredRanges.push(range);
+      }
+
+      selectionModel.setSelectedRanges(filteredRanges);
+    }
+
+    function clickedCheckbox(e) {
+      return e.target.type == "checkbox"
+    }
+
+    function isNotPreviouslySelectedColumn(filteredRanges, ranges) {
+      return filteredRanges.length == ranges.length
+    }
+
+    function toggleCheckbox(checkbox) {
+      if (checkbox.prop("checked")) {
+        checkbox.prop("checked", false)
+      } else {
+        checkbox.prop("checked", true)
+      }
+    }
+
+    function getColumnsWithCheckboxes() {
+      return _.map(columnDefinitions, function (columnDefinition) {
+        return _.extend(columnDefinition, {
+          name: "<div data-test='output-column-header'><input data-id='checkbox-" + columnDefinition.id + "' type='checkbox'>" + columnDefinition.name + "</div>"
+        });
+      });
+    }
+
+    $.extend(this, {
+      "init": init,
+      "getColumnsWithCheckboxes": getColumnsWithCheckboxes
+    });
+  };
+  return ColumnSelector;
+});
diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js b/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js
index 1bda0679..40974d9b 100644
--- a/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js
+++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js
@@ -2,7 +2,7 @@ define(
   [
     'jquery', 'underscore', 'underscore.string', 'alertify', 'pgadmin',
     'backbone', 'backgrid', 'codemirror', 'pgadmin.misc.explain',
-    'sources/selection/clipboard',
+    'sources/selection/column_selector', 'sources/selection/clipboard',
 
     'slickgrid', 'bootstrap', 'pgadmin.browser', 'wcdocker',
     'codemirror/mode/sql/sql', 'codemirror/addon/selection/mark-selection',
@@ -27,7 +27,7 @@ define(
     'slickgrid/slick.grid'
   ],
   function(
-    $, _, S, alertify, pgAdmin, Backbone, Backgrid, CodeMirror, pgExplain, clipboard
+    $, _, S, alertify, pgAdmin, Backbone, Backgrid, CodeMirror, pgExplain, ColumnSelector, clipboard
   ) {
     /* Return back, this has been called more than once */
     if (pgAdmin.SqlEditor)
@@ -592,6 +592,9 @@ define(
            grid_columns.push(options)
         });
 
+        var columnSelector = new ColumnSelector(grid_columns);
+        grid_columns = columnSelector.getColumnsWithCheckboxes();
+
         var grid_options = {
           editable: true,
           enableAddRow: is_editable,
@@ -636,6 +639,7 @@ define(
         grid.registerPlugin( new Slick.AutoTooltips({ enableForHeaderCells: false }) );
         grid.setSelectionModel(new Slick.RowSelectionModel({selectActiveRow: false}));
         grid.registerPlugin(checkboxSelector);
+        grid.registerPlugin(columnSelector);
 
         var editor_data = {
           keys: self.handler.primary_keys,
diff --git a/web/regression/README b/web/regression/README
index 2eb5c65a..72edaaf8 100644
--- a/web/regression/README
+++ b/web/regression/README
@@ -22,6 +22,12 @@ installed with:
 
 (pgadmin4) $ pip install -r $PGADMIN4_SRC/web/regression/requirements.txt
 
+While running in Linux environments install:
+sudo apt-get install xsel
+
+Otherwise the following error happens:
+"Pyperclip could not find a copy/paste mechanism for your system"
+
 General Information
 -------------------
 
diff --git a/web/regression/requirements.txt b/web/regression/requirements.txt
index f644c12a..693ea177 100644
--- a/web/regression/requirements.txt
+++ b/web/regression/requirements.txt
@@ -1,4 +1,5 @@
 chromedriver_installer==0.0.6
+pyperclip~=1.5.27
 selenium==3.3.1
 testscenarios==0.5.0
 testtools==2.0.0
-- 
2.12.0

