From f2b2da09df3a78f92bbceb8a4d96121e7c6f32da Mon Sep 17 00:00:00 2001
From: Sarah McAlear and Tira Odhner <pair+smcalear+aodhner@pivotal.io>
Date: Thu, 30 Mar 2017 17:06:07 -0400
Subject: [PATCH 06/11] Select rows and columns reliably

- use new row selection plugin
- adds grid selector plugin
- select all is missing for now
---
 web/pgadmin/static/js/selection/column_selector.js | 212 ++++++++++++---------
 web/pgadmin/static/js/selection/grid_selector.js   |  41 ++++
 web/pgadmin/static/js/selection/row_selector.js    |   6 +-
 .../sqleditor/templates/sqleditor/js/sqleditor.js  |  21 +-
 .../javascript/selection/column_selector_spec.js   |  76 ++++++--
 .../javascript/selection/grid_selector_spec.js     |  80 ++++++++
 .../javascript/selection/row_selector_spec.js      |   4 +-
 7 files changed, 311 insertions(+), 129 deletions(-)
 create mode 100644 web/pgadmin/static/js/selection/grid_selector.js
 create mode 100644 web/regression/javascript/selection/grid_selector_spec.js

diff --git a/web/pgadmin/static/js/selection/column_selector.js b/web/pgadmin/static/js/selection/column_selector.js
index 451625e3..15267d2c 100644
--- a/web/pgadmin/static/js/selection/column_selector.js
+++ b/web/pgadmin/static/js/selection/column_selector.js
@@ -1,95 +1,131 @@
 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;
-
-                updateRanges(grid, column.id);
-
-                if (!clickedCheckbox(e)) {
-                    var $checkbox = $("[data-id='checkbox-" + column.id + "']");
-                    toggleCheckbox($checkbox);
-                }
-            });
-        };
-
-        function updateRanges(grid, columnId) {
-            var selectionModel = grid.getSelectionModel();
-            var ranges = selectionModel.getSelectedRanges();
-
-            var columnIndex = grid.getColumnIndex(columnId);
-
-            var columnRange = new Slick.Range(0, columnIndex, grid.getDataLength() - 1, columnIndex);
-            var newRanges;
-            if (isColumnSelected(ranges, columnRange)) {
-                newRanges = removeColumn(ranges, columnRange);
-            } else {
-                if (!allRangesAreColumns(ranges, grid)) {
-                    newRanges = [columnRange];
-                } else {
-                    newRanges = addColumn(ranges, columnRange);
-                }
+  var ColumnSelector = function () {
+    var Slick = window.Slick;
+
+    var init = function (grid) {
+      grid.onHeaderClick.subscribe(function (event, eventArgument) {
+          var column = eventArgument.column;
+
+          if (column.selectable !== false) {
+
+            if (!clickedCheckbox(event)) {
+              var $checkbox = $("[data-id='checkbox-" + column.id + "']");
+              toggleCheckbox($checkbox);
             }
-            selectionModel.setSelectedRanges(newRanges);
+
+            updateRanges(grid, column.id);
+          }
         }
+      );
+      grid.getSelectionModel().onSelectedRangesChanged
+        .subscribe(handleSelectedRangesChanged.bind(null, grid));
+    };
 
-        var isColumnSelected = function (ranges, columnRange) {
-            return _.any(ranges, function (range) {
-                return isSameRange(range, columnRange)
-            })
-        };
-
-        var isSameRange = function (range, otherRange) {
-            return range.fromCell == otherRange.fromCell && range.toCell == otherRange.toCell &&
-                range.fromRow == otherRange.fromRow && range.toRow == otherRange.toRow;
-        };
-
-        var removeColumn = function (ranges, columnRange) {
-            return _.filter(ranges, function (range) {
-                return !(isSameRange(range, columnRange))
-            })
-        };
-        var addColumn = function (ranges, column) {
-            ranges.push(column);
-            return ranges;
-        };
-
-        var allRangesAreColumns = function (ranges, grid) {
-            return _.every(ranges, function (range) {
-                return range.fromCell == range.toCell &&
-                range.fromRow == 0 && range.toRow == grid.getDataLength() - 1
-            })
-        };
-
-        var clickedCheckbox = function (e) {
-            return e.target.type == "checkbox"
-        };
-
-        var toggleCheckbox = function (checkbox) {
-            if (checkbox.prop("checked")) {
-                checkbox.prop("checked", false)
-            } else {
-                checkbox.prop("checked", true)
-            }
-        };
-
-        var getColumnsWithCheckboxes = function () {
-            return _.map(columnDefinitions, function (columnDefinition) {
-                var name = columnDefinition.name;
-                if (columnDefinition.id != "_checkbox_selector")
-                    name = "<div data-test='output-column-header'><input data-id='checkbox-" + columnDefinition.id + "' type='checkbox'> " + columnDefinition.name + "</div>";
-                return _.extend(columnDefinition, {
-                    name: name
-                });
-            });
-        };
-
-        $.extend(this, {
-            "init": init,
-            "getColumnsWithCheckboxes": getColumnsWithCheckboxes
+    function rangeForColumn(grid, columnIndex) {
+      return new Slick.Range(0, columnIndex, grid.getDataLength() - 1, columnIndex)
+    }
+
+    function handleSelectedRangesChanged(grid, event, ranges) {
+      $('[data-cell-type="column-header-row"] input:checked')
+        .each(function (index, checkbox) {
+          var $checkbox = $(checkbox);
+          var columnIndex = grid.getColumnIndex($checkbox.data('column-id'));
+          var isStillSelected = isRangeSelected(ranges, rangeForColumn(grid, columnIndex));
+          if (!isStillSelected) {
+            toggleCheckbox($checkbox);
+          }
         });
+    }
+
+    var isRangeSelected = function (selectedRanges, range) {
+      return _.any(selectedRanges, function (selectedRange) {
+        return isSameRange(selectedRange, range)
+      })
     };
-    return ColumnSelector;
+
+    function updateRanges(grid, columnId) {
+      var selectionModel = grid.getSelectionModel();
+      var ranges = selectionModel.getSelectedRanges();
+
+      var columnIndex = grid.getColumnIndex(columnId);
+
+      var columnRange = rangeForColumn(grid, columnIndex);
+      var newRanges;
+      if (isColumnSelected(ranges, columnRange)) {
+        newRanges = removeColumn(ranges, columnRange);
+      } else {
+        if (allRangesAreColumns(ranges, grid)) {
+          newRanges = addColumn(ranges, columnRange);
+        } else {
+          newRanges = [columnRange];
+        }
+      }
+      selectionModel.setSelectedRanges(newRanges);
+    }
+
+    var isColumnSelected = function (ranges, columnRange) {
+      return _.any(ranges, function (range) {
+        return isSameRange(range, columnRange)
+      })
+    };
+
+    var isSameRange = function (range, otherRange) {
+      return range.fromCell == otherRange.fromCell && range.toCell == otherRange.toCell &&
+        range.fromRow == otherRange.fromRow && range.toRow == otherRange.toRow;
+    };
+
+    var removeColumn = function (ranges, columnRange) {
+      return _.filter(ranges, function (range) {
+        return !(isSameRange(range, columnRange))
+      })
+    };
+    var addColumn = function (ranges, column) {
+      ranges.push(column);
+      return ranges;
+    };
+
+    var allRangesAreColumns = function (ranges, grid) {
+      return _.every(ranges, function (range) {
+        return range.fromCell == range.toCell &&
+          range.fromRow == 0 && range.toRow == grid.getDataLength() - 1
+      })
+    };
+
+    var clickedCheckbox = function (e) {
+      return e.target.type == "checkbox"
+    };
+
+    var toggleCheckbox = function (checkbox) {
+      if (checkbox.prop("checked")) {
+        checkbox.prop("checked", false)
+      } else {
+        checkbox.prop("checked", true)
+      }
+    };
+
+    var getColumnDefinitionsWithCheckboxes = function (columnDefinitions) {
+      return _.map(columnDefinitions, function (columnDefinition) {
+        if (columnDefinition.selectable !== false) {
+          var name = "<div " +
+            "data-cell-type='column-header-row' " +
+            "data-test='output-column-header'>" +
+            "<input data-id='checkbox-" + columnDefinition.id + "' " +
+            "data-column-id='" + columnDefinition.id + "' type='checkbox'> "
+            + columnDefinition.name +
+            "</div>";
+          return _.extend(columnDefinition, {
+            name: name
+          });
+        } else {
+          return columnDefinition;
+        }
+      });
+    };
+
+    $.extend(this, {
+      "init": init,
+      "getColumnDefinitionsWithCheckboxes": getColumnDefinitionsWithCheckboxes
+    });
+  };
+  return ColumnSelector;
 });
diff --git a/web/pgadmin/static/js/selection/grid_selector.js b/web/pgadmin/static/js/selection/grid_selector.js
new file mode 100644
index 00000000..0d9e1e78
--- /dev/null
+++ b/web/pgadmin/static/js/selection/grid_selector.js
@@ -0,0 +1,41 @@
+define(['jquery', 'sources/selection/column_selector', 'sources/selection/row_selector'],
+  function ($, ColumnSelector, RowSelector) {
+    var Slick = window.Slick;
+
+    var GridSelector = function (columnDefinitions) {
+      var rowSelector = new RowSelector(columnDefinitions);
+      var columnSelector = new ColumnSelector(columnDefinitions);
+
+      var init = function (grid) {
+        this.grid = grid;
+        grid.onHeaderClick.subscribe(function (event, eventArgument) {
+          if (event.target == $("[data-id='checkbox-select-all']")[0]) {
+            toggleSelectAll(grid);
+          }
+        });
+        grid.registerPlugin(rowSelector);
+        grid.registerPlugin(columnSelector);
+      };
+
+      var getColumnDefinitionsWithCheckboxes = function (columnDefinitions) {
+        columnDefinitions = columnSelector.getColumnDefinitionsWithCheckboxes(columnDefinitions);
+        columnDefinitions = rowSelector.getColumnDefinitionsWithCheckboxes(columnDefinitions);
+
+        return columnDefinitions;
+      };
+
+      function toggleSelectAll(grid) {
+        var range = new Slick.Range(0, 1, grid.getDataLength() - 1, grid.getColumns().length - 1);
+        var selectionModel = grid.getSelectionModel();
+
+        selectionModel.setSelectedRanges([range]);
+      }
+
+      $.extend(this, {
+        "init": init,
+        "getColumnDefinitionsWithCheckboxes": getColumnDefinitionsWithCheckboxes
+      });
+    };
+
+    return GridSelector;
+  });
\ No newline at end of file
diff --git a/web/pgadmin/static/js/selection/row_selector.js b/web/pgadmin/static/js/selection/row_selector.js
index 29a37588..353efdc8 100644
--- a/web/pgadmin/static/js/selection/row_selector.js
+++ b/web/pgadmin/static/js/selection/row_selector.js
@@ -1,12 +1,12 @@
 define(['jquery', 'slickgrid'], function ($) {
-  var RowSelector = function (columnDefinitions) {
+  var RowSelector = function () {
     var Slick = window.Slick;
 
     var gridEventBus = new Slick.EventHandler();
 
     var init = function (grid) {
       grid.getSelectionModel()
-        .onSelectedRangesChanged.subscribe(handleSelectedRangesChanged.bind(null, grid))
+        .onSelectedRangesChanged.subscribe(handleSelectedRangesChanged.bind(null, grid));
       gridEventBus
         .subscribe(grid.onClick, handleClick.bind(null, grid))
     };
@@ -92,7 +92,7 @@ define(['jquery', 'slickgrid'], function ($) {
       }
     };
 
-    var getColumnDefinitionsWithCheckboxes = function () {
+    var getColumnDefinitionsWithCheckboxes = function (columnDefinitions) {
       columnDefinitions.unshift({
         id: 'row-header-column',
         selectable: false,
diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js b/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js
index c8049b62..70412a5f 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/column_selector', 'sources/selection/clipboard',
+    'sources/selection/grid_selector', 'sources/selection/clipboard',
     'sources/selection/copy_data',
 
     'slickgrid', 'bootstrap', 'pgadmin.browser', 'wcdocker',
@@ -22,13 +22,12 @@ define(
     'slickgrid/plugins/slick.cellrangedecorator',
     'slickgrid/plugins/slick.cellrangeselector',
     'slickgrid/plugins/slick.cellselectionmodel',
-    'slickgrid/plugins/slick.checkboxselectcolumn',
     'slickgrid/plugins/slick.cellcopymanager',
     'slickgrid/plugins/slick.rowselectionmodel',
     'slickgrid/slick.grid'
   ],
   function(
-    $, _, S, alertify, pgAdmin, Backbone, Backgrid, CodeMirror, pgExplain, ColumnSelector, clipboard, copyData
+    $, _, S, alertify, pgAdmin, Backbone, Backgrid, CodeMirror, pgExplain, GridSelector, clipboard, copyData
   ) {
     /* Return back, this has been called more than once */
     if (pgAdmin.SqlEditor)
@@ -550,14 +549,7 @@ define(
           collection = [];
         }
 
-        var grid_columns = new Array(),
-          checkboxSelector;
-
-          checkboxSelector = new Slick.CheckboxSelectColumn({
-            cssClass: "sc-cb"
-          });
-
-          grid_columns.push(checkboxSelector.getColumnDefinition());
+        var grid_columns = [];
 
         var grid_width = $($('#editor-panel').find('.wcFrame')[1]).width()
         _.each(columns, function(c) {
@@ -593,8 +585,8 @@ define(
            grid_columns.push(options)
         });
 
-        var columnSelector = new ColumnSelector(grid_columns);
-        grid_columns = columnSelector.getColumnsWithCheckboxes();
+        var gridSelector = new GridSelector();
+        grid_columns = gridSelector.getColumnDefinitionsWithCheckboxes(grid_columns);
 
         var grid_options = {
           editable: true,
@@ -639,8 +631,7 @@ define(
         var grid = new Slick.Grid($data_grid, collection, grid_columns, grid_options);
         grid.registerPlugin( new Slick.AutoTooltips({ enableForHeaderCells: false }) );
         grid.setSelectionModel(new Slick.RowSelectionModel({selectActiveRow: false}));
-        grid.registerPlugin(checkboxSelector);
-        grid.registerPlugin(columnSelector);
+        grid.registerPlugin(gridSelector);
 
         var editor_data = {
           keys: self.handler.primary_keys,
diff --git a/web/regression/javascript/selection/column_selector_spec.js b/web/regression/javascript/selection/column_selector_spec.js
index 5c0e1c1f..ff991cd1 100644
--- a/web/regression/javascript/selection/column_selector_spec.js
+++ b/web/regression/javascript/selection/column_selector_spec.js
@@ -3,41 +3,48 @@ define(
     "underscore",
     "slickgrid/slick.grid",
     "sources/selection/column_selector",
-    "slickgrid/slick.rowselectionmodel"
+    "slickgrid/slick.rowselectionmodel",
+    "slickgrid"
   ],
-  function ($, _, SlickGrid, ColumnSelector, RowSelectionModel) {
+  function ($, _, SlickGrid, ColumnSelector, RowSelectionModel, Slick) {
     describe("ColumnSelector", function () {
       var container, data, columns, options;
       beforeEach(function () {
         container = $("<div></div>");
+        container.height(9999);
+
         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
-          }]
+          },
+          {
+            name: 'some-non-selectable-column',
+            selectable: false
+          }
+        ]
       });
 
-      describe("when it is the checkbox column", function () {
-        it("does not create a checkbox", function () {
+      describe("when a column is not selectable", function () {
+        it("does not create a checkbox for selecting the column", function () {
           var checkboxColumn = {
-            id: '_checkbox_selector',
-            name: 'checkbox column',
-            selectable: true
+            name: 'some-column-name-4',
+            selectable: false
           };
           columns.push(checkboxColumn);
 
-          var columnSelector = new ColumnSelector(columns);
-          columns = columnSelector.getColumnsWithCheckboxes();
+          var columnSelector = new ColumnSelector();
+          columns = columnSelector.getColumnDefinitionsWithCheckboxes(columns);
           var grid = new SlickGrid(container, data, columns, options);
 
+          var rowSelectionModel = new RowSelectionModel();
+          grid.setSelectionModel(rowSelectionModel);
           grid.registerPlugin(columnSelector);
           grid.invalidate();
 
@@ -46,10 +53,13 @@ define(
       });
 
       it("renders a checkbox in the column header", function () {
-        var columnSelector = new ColumnSelector(columns);
-        columns = columnSelector.getColumnsWithCheckboxes();
+        var columnSelector = new ColumnSelector();
+        columns = columnSelector.getColumnDefinitionsWithCheckboxes(columns);
         var grid = new SlickGrid(container, data, columns, options);
 
+        var rowSelectionModel = new RowSelectionModel();
+        grid.setSelectionModel(rowSelectionModel);
+
         grid.registerPlugin(columnSelector);
         grid.invalidate();
 
@@ -57,10 +67,12 @@ define(
       });
 
       it("displays the name of the column", function () {
-        var columnSelector = new ColumnSelector(columns);
-        columns = columnSelector.getColumnsWithCheckboxes();
+        var columnSelector = new ColumnSelector();
+        columns = columnSelector.getColumnDefinitionsWithCheckboxes(columns);
         var grid = new SlickGrid(container, data, columns, options);
 
+        var rowSelectionModel = new RowSelectionModel();
+        grid.setSelectionModel(rowSelectionModel);
         grid.registerPlugin(columnSelector);
         grid.invalidate();
 
@@ -69,18 +81,17 @@ define(
       });
 
       it("preserves the other attributes of column definitions", function () {
-        var columnSelector = new ColumnSelector(columns);
-        var selectableColumns = columnSelector.getColumnsWithCheckboxes();
+        var columnSelector = new ColumnSelector();
+        var selectableColumns = columnSelector.getColumnDefinitionsWithCheckboxes(columns);
 
         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();
+          var columnSelector = new ColumnSelector();
+          columns = columnSelector.getColumnDefinitionsWithCheckboxes(columns);
           data = [];
           for (var i = 0; i < 10; i++) {
             data.push({'some-column-name': 'some-value-' + i, 'second column': 'second value ' + i});
@@ -89,6 +100,7 @@ define(
 
           rowSelectionModel = new RowSelectionModel();
           grid.setSelectionModel(rowSelectionModel);
+
           grid.registerPlugin(columnSelector);
           grid.invalidate();
           $("body").append(container);
@@ -178,6 +190,28 @@ define(
             expect(selectedRanges.length).toEqual(0);
           })
         });
+
+        describe("and the column is not selectable", function () {
+          it("does not select the column", function () {
+            $(container.find('.slick-header-column:contains(some-non-selectable-column)')).click();
+            var selectedRanges = rowSelectionModel.getSelectedRanges();
+
+            expect(selectedRanges.length).toEqual(0);
+          });
+        });
+
+        describe("when the column is deselected through setSelectedRanges", function () {
+          beforeEach(function () {
+            container.find('.slick-header-column')[1].click();
+          });
+
+          it("should uncheck the checkbox", function () {
+            rowSelectionModel.setSelectedRanges([]);
+
+            expect($(container.find('.slick-header-columns input')[1])
+              .is(':checked')).toBeFalsy();
+          });
+        });
       });
     });
   });
\ No newline at end of file
diff --git a/web/regression/javascript/selection/grid_selector_spec.js b/web/regression/javascript/selection/grid_selector_spec.js
new file mode 100644
index 00000000..df71049a
--- /dev/null
+++ b/web/regression/javascript/selection/grid_selector_spec.js
@@ -0,0 +1,80 @@
+define(["jquery",
+    "underscore",
+    "slickgrid/slick.grid",
+    "slickgrid/slick.rowselectionmodel",
+    "sources/selection/grid_selector"
+  ],
+  function ($, _, SlickGrid, RowSelectionModel, GridSelector) {
+    describe("GridSelector", function () {
+      var container, data, columns, gridSelector, rowSelectionModel;
+
+      beforeEach(function () {
+        container = $("<div></div>");
+        container.height(9999);
+        columns = [{
+          id: '1',
+          name: 'some-column-name',
+        }, {
+          id: '2',
+          name: 'second column',
+        }];
+
+        gridSelector = new GridSelector();
+        columns = gridSelector.getColumnDefinitionsWithCheckboxes(columns);
+
+        data = [];
+        for (var i = 0; i < 10; i++) {
+          data.push({'some-column-name': 'some-value-' + i, 'second column': 'second value ' + i});
+        }
+        var grid = new SlickGrid(container, data, columns);
+
+        rowSelectionModel = new RowSelectionModel();
+        grid.setSelectionModel(rowSelectionModel);
+
+        grid.registerPlugin(gridSelector);
+        grid.invalidate();
+
+        $("body").append(container);
+      });
+
+      afterEach(function () {
+        $("body").find(container).remove();
+      });
+
+      it("renders an additional column on the left for selecting rows", function () {
+        expect(columns.length).toBe(3);
+
+        var leftmostColumn = columns[0];
+        expect(leftmostColumn.id).toBe('row-header-column');
+      });
+
+      it("renders checkboxes for selecting columns", function () {
+        expect(container.find('[data-test="output-column-header"] input').length).toBe(2)
+      });
+
+      xit("renders a checkbox for selecting all the cells", function () {
+          expect(container.find("[title='Select/Deselect All']").length).toBe(1);
+      });
+
+      xdescribe("when the main checkbox in the corner gets selected", function () {
+        it("unchecks all the columns", function () {
+          container.find("[title='Select/Deselect All']").click();
+
+          expect($(container.find('.slick-header-columns input')[1]).is(':checked')).toBeFalsy();
+          expect($(container.find('.slick-header-columns input')[2]).is(':checked')).toBeFalsy();
+        });
+
+        it("selects all the cells", function () {
+          container.find("[title='Select/Deselect All']").click();
+
+          var selectedRanges = rowSelectionModel.getSelectedRanges();
+          expect(selectedRanges.length).toBe(1);
+          var selectedRange = selectedRanges[0];
+          expect(selectedRange.fromCell).toBe(1);
+          expect(selectedRange.toCell).toBe(2);
+          expect(selectedRange.fromRow).toBe(0);
+          expect(selectedRange.toRow).toBe(9);
+        })
+      });
+    });
+  });
\ No newline at end of file
diff --git a/web/regression/javascript/selection/row_selector_spec.js b/web/regression/javascript/selection/row_selector_spec.js
index 30f155c9..10697e6a 100644
--- a/web/regression/javascript/selection/row_selector_spec.js
+++ b/web/regression/javascript/selection/row_selector_spec.js
@@ -24,12 +24,12 @@ define(
           selectable: true
         }];
 
-        var rowSelector = new RowSelector(columnDefinitions);
+        var rowSelector = new RowSelector();
         data = [];
         for (var i = 0; i < 10; i++) {
           data.push(['some-value-' + i, 'second value ' + i]);
         }
-        columnDefinitions = rowSelector.getColumnDefinitionsWithCheckboxes();
+        columnDefinitions = rowSelector.getColumnDefinitionsWithCheckboxes(columnDefinitions);
         grid = new SlickGrid(container, data, columnDefinitions);
 
         rowSelectionModel = new RowSelectionModel();
-- 
2.12.0

