From da4f5e0ac0161cbf7ebe6d78d4ebc92113b8d209 Mon Sep 17 00:00:00 2001
From: Tira + Joao <aodhner+jpereira@pivotal.io>
Date: Tue, 14 Mar 2017 17:11:55 -0400
Subject: [PATCH 03/11] Add RangeBoundaryNavigator

- Update CopyData and move the Spec
- Column selection working (still todo: row/column interaction)
- Corrects regression test
- Remove duplicated checkbox on grid
---
 web/pgadmin/static/js/selection/column_selector.js |   5 +-
 web/pgadmin/static/js/selection/copy_data.js       |  57 ++++-----
 .../js/selection/range_boundary_navigator.js       |  97 ++++++++++++++
 .../static/js/selection/tests/copy_data_spec.js    |  35 ------
 .../javascript/selection}/column_selector_spec.js  |  20 +++
 .../javascript/selection/copy_data_spec.js         |  95 ++++++++++++++
 .../selection/range_boundary_navigator_spec.js     | 139 +++++++++++++++++++++
 7 files changed, 382 insertions(+), 66 deletions(-)
 create mode 100644 web/pgadmin/static/js/selection/range_boundary_navigator.js
 delete mode 100644 web/pgadmin/static/js/selection/tests/copy_data_spec.js
 rename {test/javascript => web/regression/javascript/selection}/column_selector_spec.js (87%)
 create mode 100644 web/regression/javascript/selection/copy_data_spec.js
 create mode 100644 web/regression/javascript/selection/range_boundary_navigator_spec.js

diff --git a/web/pgadmin/static/js/selection/column_selector.js b/web/pgadmin/static/js/selection/column_selector.js
index 845b9a90..e102ae5a 100644
--- a/web/pgadmin/static/js/selection/column_selector.js
+++ b/web/pgadmin/static/js/selection/column_selector.js
@@ -52,8 +52,11 @@ define(['jquery', 'slickgrid'], function ($) {
 
     function getColumnsWithCheckboxes() {
       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: "<div data-test='output-column-header'><input data-id='checkbox-" + columnDefinition.id + "' type='checkbox'>" + columnDefinition.name + "</div>"
+          name: name
         });
       });
     }
diff --git a/web/pgadmin/static/js/selection/copy_data.js b/web/pgadmin/static/js/selection/copy_data.js
index 586e2fc5..b640235e 100644
--- a/web/pgadmin/static/js/selection/copy_data.js
+++ b/web/pgadmin/static/js/selection/copy_data.js
@@ -1,7 +1,6 @@
-define(['jquery', 'underscore', 'sources/selection/clipboard'], function ($, _, clipboard) {
+define(['jquery', 'underscore', 'sources/selection/clipboard', 'sources/selection/range_boundary_navigator'], function ($, _, clipboard, rangeBoundaryNavigator) {
   var copyData = function () {
-    var self = this, grid, data, rows, selection, copied_text = '', copied_data = '';
-    self.copied_rows = [];
+    var self = this;
 
     // Disable copy button
     $("#btn-copy-row").prop('disabled', true);
@@ -10,36 +9,34 @@ define(['jquery', 'underscore', 'sources/selection/clipboard'], function ($, _,
       $("#btn-paste-row").prop('disabled', false);
     }
 
-    grid = self.slickgrid;
-    selection = grid.getSelectionModel().getSelectedRanges();
-    data = grid.getData();
-    rows = grid.getSelectedRows();
-    // Iterate over all the selected rows & fetch data
-    for (var i = 0; i < rows.length; i += 1) {
-      var idx = rows[i],
-        _rowData = data[idx],
-        _values = [];
-      self.copied_rows.push(_rowData);
-      // Convert it as CSV for clipboard
-      for (var j = 0; j < self.columns.length; j += 1) {
-
-        var val = _rowData[self.columns[j].pos];
-        if(val && _.isObject(val))
-          val = "'" + JSON.stringify(val) + "'";
-        else if(val && typeof val != "number" && typeof true != "boolean")
-          val = "'" + val.toString() + "'";
-        else if (_.isNull(val) || _.isUndefined(val))
-          val = '';
-        _values.push(val);
-      }
-      // Append to main text string
-      if (_values.length > 0)
-        copied_text += _values.toString() + "\n";
+    var grid = self.slickgrid;
+    var columnDefinitions = grid.getColumns();
+    var selectedRanges = grid.getSelectionModel().getSelectedRanges();
+    var data = grid.getData();
+    var rows = grid.getSelectedRows();
+
+
+    if (allTheRangesAreFullRows(selectedRanges, columnDefinitions)) {
+      self.copied_rows = rows.map(function (rowIndex) {
+        return data[rowIndex];
+      });
+    } else {
+      self.copied_rows = [];
     }
 
+    var csvText = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, selectedRanges);
+
     // If there is something to set into clipboard
-    if (copied_text)
-      clipboard.copyTextToClipboard(copied_text);
+    if (csvText)
+      clipboard.copyTextToClipboard(csvText);
   };
+
+  var allTheRangesAreFullRows = function (ranges, columnDefinitions){
+    var colRangeBounds = ranges.map(function (range) {
+      return [range.fromCell, range.toCell];
+    });
+    return _.isEqual(_.union.apply(null, colRangeBounds), [0, columnDefinitions.length - 1]);
+  };
+
   return copyData
 });
\ No newline at end of file
diff --git a/web/pgadmin/static/js/selection/range_boundary_navigator.js b/web/pgadmin/static/js/selection/range_boundary_navigator.js
new file mode 100644
index 00000000..f7d64b97
--- /dev/null
+++ b/web/pgadmin/static/js/selection/range_boundary_navigator.js
@@ -0,0 +1,97 @@
+define(function () {
+  return {
+    getUnion: function (dimensionBounds) {
+      dimensionBounds.sort();
+      var boundsUnion = [];
+      if (!_.isEmpty(dimensionBounds)) {
+        boundsUnion = [dimensionBounds[0]];
+      }
+
+      dimensionBounds.forEach(function (bound) {
+        var previousBound = _.last(boundsUnion);
+        if (bound[0] <= previousBound[1] + 1) {
+          if (bound[1] > previousBound[1]) {
+            previousBound[1] = bound[1];
+          }
+        } else {
+          boundsUnion.push(bound);
+        }
+      });
+      return boundsUnion;
+    },
+
+    mapDimensionBoundaryUnion: function (unionedDimensionBoundaries, iteratee) {
+      var mapResult = [];
+      unionedDimensionBoundaries.forEach(function (subrange) {
+        for (var index = subrange[0]; index <= subrange[1]; index += 1) {
+          mapResult.push(iteratee(index));
+        }
+      });
+      return mapResult;
+    },
+
+    mapOver2DArray: function (rowRangeBounds, colRangeBounds, processCell, rowCollector) {
+      var unionedRowRanges = this.getUnion(rowRangeBounds);
+      var unionedColRanges = this.getUnion(colRangeBounds);
+
+      return this.mapDimensionBoundaryUnion(unionedRowRanges, function (rowId) {
+        var rowData = this.mapDimensionBoundaryUnion(unionedColRanges, function (colId) {
+          return processCell(rowId, colId);
+        });
+        return rowCollector(rowData);
+      }.bind(this));
+    },
+
+    rangesToCsv: function (data, columnDefinitions, selectedRanges) {
+      var rowRangeBounds = selectedRanges.map(function (range) {
+        return [range.fromRow, range.toRow];
+      });
+      var colRangeBounds = selectedRanges.map(function (range) {
+        return [range.fromCell, range.toCell];
+      });
+
+      if(_.isUndefined(columnDefinitions[0].pos)){
+        colRangeBounds = this.removeFirstColumn(colRangeBounds);
+      }
+
+      var csvRows = this.mapOver2DArray(rowRangeBounds, colRangeBounds, this.csvCell.bind(this, data, columnDefinitions), function (rowData) {
+        return rowData.join(',');
+      });
+      return csvRows.join('\n');
+    },
+
+    removeFirstColumn: function (colRangeBounds) {
+      var unionedColRanges = this.getUnion(colRangeBounds);
+
+      var firstSubrangeStartsAt0 = function () {
+        return unionedColRanges[0][0] == 0;
+      };
+
+      function firstSubrangeIsJustFirstColumn() {
+        return unionedColRanges[0][1] == 0;
+      }
+
+      if(firstSubrangeStartsAt0()){
+        if(firstSubrangeIsJustFirstColumn()){
+          unionedColRanges.shift();
+        } else {
+          unionedColRanges[0][0] = 1;
+        }
+      }
+      return unionedColRanges;
+    },
+
+    csvCell: function (data, columnDefinitions, rowId, colId) {
+      var val = data[rowId][columnDefinitions[colId].pos];
+
+      if (val && _.isObject(val)) {
+        val = "'" + JSON.stringify(val) + "'";
+      } else if (val && typeof val != "number" && typeof val != "boolean") {
+        val = "'" + val.toString() + "'";
+      } else if (_.isNull(val) || _.isUndefined(val)) {
+        val = '';
+      }
+      return val;
+    }
+  };
+});
\ No newline at end of file
diff --git a/web/pgadmin/static/js/selection/tests/copy_data_spec.js b/web/pgadmin/static/js/selection/tests/copy_data_spec.js
deleted file mode 100644
index 4ad6cb0a..00000000
--- a/web/pgadmin/static/js/selection/tests/copy_data_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-define(
-  ["jquery",
-    "slickgrid/slick.grid",
-    "slickgrid/slick.rowselectionmodel",
-    "sources/selection/copy_data",
-    "sources/selection/clipboard"
-  ],
-  function ($, SlickGrid, RowSelectionModel, copyData, clipboard) {
-    describe('copyData', function () {
-      it('copies selected rows', function () {
-        var data = [{"flavor":"leopard","id":"1","color":"purple"},
-          {"flavor":"lion","id":"2","color":"sand"},
-          {"flavor":"puma","id":"3","color":"jet"}];
-        var columns = [{"name":"id","label":"id<br> numeric","cell":"number","can_edit":false,"type":"numeric"},
-          {"name":"flavor","label":"flavor<br> character varying","cell":"string","can_edit":false,"type":"character varying"},
-          {"name":"color","label":"size<br> numeric","cell":"number","can_edit":false,"type":"numeric"}];
-        var gridContainer = $("<div id='grid'></div>");
-        $("body").append(gridContainer);
-        var grid = new Slick.Grid("#grid", data, columns, {});
-        grid.setSelectionModel(new Slick.RowSelectionModel({selectActiveRow: false}));
-
-
-        var sqlEditor = {slickgrid: grid, columns: columns};
-        spyOn(clipboard, 'copyTextToClipboard');
-        spyOn(grid, 'getSelectedRows').and.returnValue([0, 2]);
-
-        copyData.apply(sqlEditor);
-
-        expect(sqlEditor.copied_rows.length).toBe(2);
-        expect(clipboard.copyTextToClipboard).toHaveBeenCalled();
-
-      })
-    })
-  }
-);
\ No newline at end of file
diff --git a/test/javascript/column_selector_spec.js b/web/regression/javascript/selection/column_selector_spec.js
similarity index 87%
rename from test/javascript/column_selector_spec.js
rename to web/regression/javascript/selection/column_selector_spec.js
index 30a98d42..15b34601 100644
--- a/test/javascript/column_selector_spec.js
+++ b/web/regression/javascript/selection/column_selector_spec.js
@@ -25,6 +25,26 @@ define(
           }]
       });
 
+      describe("when it is the checkbox column", function () {
+        it("does not create a checkbox", function () {
+          var checkboxColumn = {
+            id: '_checkbox_selector',
+            name: 'checkbox column',
+            selectable: true
+          };
+          columns.push(checkboxColumn);
+
+          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("renders a checkbox in the column header", function () {
         var columnSelector = new ColumnSelector(columns);
         columns = columnSelector.getColumnsWithCheckboxes();
diff --git a/web/regression/javascript/selection/copy_data_spec.js b/web/regression/javascript/selection/copy_data_spec.js
new file mode 100644
index 00000000..42a8a10f
--- /dev/null
+++ b/web/regression/javascript/selection/copy_data_spec.js
@@ -0,0 +1,95 @@
+define(
+  ["jquery",
+    "slickgrid/slick.grid",
+    "slickgrid/slick.rowselectionmodel",
+    "sources/selection/copy_data",
+    "sources/selection/clipboard"
+  ],
+  function ($, SlickGrid, RowSelectionModel, copyData, clipboard) {
+    describe('copyData', function () {
+      var grid, sqlEditor;
+
+      beforeEach(function () {
+        var data = [[1, "leopord", "12"],
+          [2, "lion", "13"],
+          [3, "puma", "14"]];
+
+        var columns = [
+          {
+            name: "id",
+            pos: 0,
+            label: "id<br> numeric",
+            cell: "number",
+            can_edit: false,
+            type: "numeric"
+          },
+          {
+            name: "brand",
+            pos: 1,
+            label: "flavor<br> character varying",
+            cell: "string",
+            can_edit: false,
+            type: "character varying"
+          },
+          {
+            name: "size",
+            pos: 2,
+            label: "size<br> numeric",
+            cell: "number",
+            can_edit: false,
+            type: "numeric"
+          }];
+        var gridContainer = $("<div id='grid'></div>");
+        $("body").append(gridContainer);
+        grid = new Slick.Grid("#grid", data, columns, {});
+        grid.setSelectionModel(new Slick.RowSelectionModel({selectActiveRow: false}));
+        sqlEditor = {slickgrid: grid};
+      });
+
+      describe("when rows are selected", function () {
+        beforeEach(function () {
+          grid.getSelectionModel().setSelectedRows([0, 2])
+        });
+
+        it("copies them", function () {
+          spyOn(clipboard, 'copyTextToClipboard');
+
+          copyData.apply(sqlEditor);
+
+          expect(sqlEditor.copied_rows.length).toBe(2);
+
+          expect(clipboard.copyTextToClipboard).toHaveBeenCalled();
+          expect(clipboard.copyTextToClipboard.calls.mostRecent().args[0]).toContain("1,'leopord','12'");
+          expect(clipboard.copyTextToClipboard.calls.mostRecent().args[0]).toContain("3,'puma','9'");
+        });
+      });
+
+      describe("when a column is selected", function () {
+        beforeEach(function () {
+          var firstColumn = new Slick.Range(0, 0, 2, 0);
+          grid.getSelectionModel().setSelectedRanges([firstColumn])
+        });
+
+        it("copies text to the clipboard", function () {
+          spyOn(clipboard, 'copyTextToClipboard');
+
+          copyData.apply(sqlEditor);
+
+          expect(clipboard.copyTextToClipboard).toHaveBeenCalled();
+
+          var copyArg = clipboard.copyTextToClipboard.calls.mostRecent().args[0];
+          var rowStrings = copyArg.split('\n');
+          expect(rowStrings[0]).toBe("1");
+          expect(rowStrings[1]).toBe("2");
+          expect(rowStrings[2]).toBe("3");
+        });
+
+        it("sets copied_rows to empty", function () {
+          copyData.apply(sqlEditor);
+
+          expect(sqlEditor.copied_rows.length).toBe(0);
+        });
+      });
+    });
+  }
+);
\ No newline at end of file
diff --git a/web/regression/javascript/selection/range_boundary_navigator_spec.js b/web/regression/javascript/selection/range_boundary_navigator_spec.js
new file mode 100644
index 00000000..70fad25f
--- /dev/null
+++ b/web/regression/javascript/selection/range_boundary_navigator_spec.js
@@ -0,0 +1,139 @@
+define(['sources/selection/range_boundary_navigator'], function (rangeBoundaryNavigator) {
+
+  describe("#getUnion", function () {
+    describe("when the ranges completely overlap", function () {
+      it("returns a list with that range", function () {
+        var ranges = [[1, 4], [1, 4], [1, 4]];
+
+        var union = rangeBoundaryNavigator.getUnion(ranges);
+
+        expect(union).toEqual([[1, 4]]);
+      });
+    });
+
+    describe("when the ranges all overlap partially or touch", function () {
+      it("returns one long range", function () {
+        var rangeBounds = [[3, 6], [1, 4], [7, 14]];
+
+        var union = rangeBoundaryNavigator.getUnion(rangeBounds);
+
+        expect(union).toEqual([[1, 14]]);
+      });
+
+      describe("when one range is a subset of another", function () {
+        it("returns the larger range", function () {
+          var rangeBounds = [[2, 6], [1, 14], [8, 10]];
+
+          var union = rangeBoundaryNavigator.getUnion(rangeBounds);
+
+          expect(union).toEqual([[1, 14]]);
+        })
+      })
+    });
+
+    describe("when the ranges do not touch", function () {
+      it("returns them in order from lowest to highest", function () {
+        var rangeBounds = [[3, 6], [1, 1], [8, 10]];
+
+        var union = rangeBoundaryNavigator.getUnion(rangeBounds);
+
+        expect(union).toEqual([[1, 1], [3, 6], [8, 10]]);
+      });
+    });
+  });
+
+
+  describe("#mapDimensionBoundaryUnion", function () {
+    it("returns a list of the results of the callback", function () {
+      var rangeBounds = [[0, 1], [3, 3]];
+      var callback = function () {
+        return 'hello';
+      };
+      var result = rangeBoundaryNavigator.mapDimensionBoundaryUnion(rangeBounds, callback);
+      expect(result).toEqual(['hello', 'hello', 'hello']);
+    });
+
+    it("calls the callback with each index in the dimension", function () {
+      var rangeBounds = [[0, 1], [3, 3]];
+      var callback = jasmine.createSpy('callbackSpy');
+      rangeBoundaryNavigator.mapDimensionBoundaryUnion(rangeBounds, callback);
+      expect(callback.calls.allArgs()).toEqual([[0], [1], [3]]);
+    });
+  });
+
+  describe("#mapOver2DArray", function () {
+    var data, rowCollector, processCell;
+    beforeEach(function () {
+      data = [[0, 1, 2, 3], [2, 2, 2, 2], [4, 5, 6, 7]];
+      processCell = function (rowIndex, columnIndex) {
+        return data[rowIndex][columnIndex];
+      };
+      rowCollector = function (rowData) {
+        return JSON.stringify(rowData);
+      };
+    });
+
+    it("calls the callback for each item in the ranges", function () {
+      var rowRanges = [[0, 0], [2, 2]];
+      var colRanges = [[0, 3]];
+
+      var selectionResult = rangeBoundaryNavigator.mapOver2DArray(rowRanges, colRanges, processCell, rowCollector);
+
+      expect(selectionResult).toEqual(["[0,1,2,3]", "[4,5,6,7]"]);
+    });
+
+    describe("when the ranges are out of order/duplicated", function () {
+      var rowRanges, colRanges;
+      beforeEach(function () {
+        rowRanges = [[2, 2], [2, 2], [0, 0]];
+        colRanges = [[0, 3]];
+      });
+
+      it("uses the union of the ranges", function () {
+        spyOn(rangeBoundaryNavigator, "getUnion").and.callThrough();
+
+        var selectionResult = rangeBoundaryNavigator.mapOver2DArray(rowRanges, colRanges, processCell, rowCollector);
+
+        expect(rangeBoundaryNavigator.getUnion).toHaveBeenCalledWith(rowRanges);
+        expect(rangeBoundaryNavigator.getUnion).toHaveBeenCalledWith(colRanges);
+        expect(selectionResult).toEqual(["[0,1,2,3]", "[4,5,6,7]"]);
+      });
+    });
+  });
+
+  describe("#rangesToCsv", function () {
+    var data, columnDefinitions, ranges;
+    beforeEach(function () {
+      data = [[1, "leopard", "12"],
+        [2, "lion", "13"],
+        [3, "cougar", "9"],
+        [4, "tiger", "10"]];
+      columnDefinitions = [{name: 'id', pos: 0}, {name: 'animal', pos: 1}, {name: 'size', pos: 2}];
+      ranges = [new Slick.Range(0, 0, 0, 2), new Slick.Range(3, 0, 3, 2)];
+    });
+
+    it("returns csv for the provided ranges", function () {
+
+      var csvResult = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, ranges);
+
+      expect(csvResult).toEqual("1,'leopard','12'\n4,'tiger','10'");
+    });
+
+    describe("when there is an extra column with checkboxes", function () {
+      beforeEach(function () {
+        columnDefinitions = [{name: 'not-a-data-column'}, {name: 'id', pos: 0}, {name: 'animal', pos: 1}, {
+          name: 'size',
+          pos: 2
+        }];
+        ranges = [new Slick.Range(0, 0, 0, 3), new Slick.Range(3, 0, 3, 3)];
+
+      });
+
+      it("returns csv for the columns with data", function () {
+        var csvResult = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, ranges);
+
+        expect(csvResult).toEqual("1,'leopard','12'\n4,'tiger','10'");
+      });
+    });
+  });
+});
\ No newline at end of file
-- 
2.12.0

