From b587d927feb3000729553730db08910bfa1cc30f Mon Sep 17 00:00:00 2001 From: Tira + Joao 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 = "
" + columnDefinition.name + "
"; return _.extend(columnDefinition, { - name: "
" + columnDefinition.name + "
" + 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
numeric","cell":"number","can_edit":false,"type":"numeric"}, - {"name":"flavor","label":"flavor
character varying","cell":"string","can_edit":false,"type":"character varying"}, - {"name":"color","label":"size
numeric","cell":"number","can_edit":false,"type":"numeric"}]; - var gridContainer = $("
"); - $("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..5102512e --- /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", "9"]]; + + var columns = [ + { + name: "id", + pos: 0, + label: "id
numeric", + cell: "number", + can_edit: false, + type: "numeric" + }, + { + name: "brand", + pos: 1, + label: "flavor
character varying", + cell: "string", + can_edit: false, + type: "character varying" + }, + { + name: "size", + pos: 2, + label: "size
numeric", + cell: "number", + can_edit: false, + type: "numeric" + }]; + var gridContainer = $("
"); + $("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