diff --git a/docs/en_US/images/query_output_data.png b/docs/en_US/images/query_output_data.png
old mode 100755
new mode 100644
index e494523a1..a23815ec7
Binary files a/docs/en_US/images/query_output_data.png and b/docs/en_US/images/query_output_data.png differ
diff --git a/web/pgadmin/feature_tests/copy_selected_query_results_feature_test.py b/web/pgadmin/feature_tests/copy_selected_query_results_feature_test.py
new file mode 100644
index 000000000..223579eee
--- /dev/null
+++ b/web/pgadmin/feature_tests/copy_selected_query_results_feature_test.py
@@ -0,0 +1,76 @@
+import pyperclip
+import time
+
+from selenium.webdriver import ActionChains
+
+from regression.python_test_utils import test_utils
+from regression.feature_utils.base_feature_test import BaseFeatureTest
+
+
+class CopySelectedQueryResultsFeatureTest(BaseFeatureTest):
+ """
+ Tests various ways to copy data from the query results grid.
+ """
+
+
+ scenarios = [
+ ("Test Copying Query Results", dict())
+ ]
+
+ def before(self):
+ connection = test_utils.get_db_connection(self.server['db'],
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ test_utils.drop_database(connection, "acceptance_test_db")
+ test_utils.create_database(self.server, "acceptance_test_db")
+ test_utils.create_table(self.server, "acceptance_test_db", "test_table")
+ self.page.add_server(self.server)
+
+ def runTest(self):
+ self.page.toggle_open_tree_item(self.server['name'])
+ self.page.toggle_open_tree_item('Databases')
+ self.page.toggle_open_tree_item('acceptance_test_db')
+ time.sleep(5)
+ self.page.find_by_partial_link_text("Tools").click()
+ self.page.find_by_partial_link_text("Query Tool").click()
+ self.page.click_tab('Query-1')
+ time.sleep(5)
+ ActionChains(self.page.driver).send_keys("SELECT * FROM test_table").perform()
+ self.page.driver.switch_to_frame(self.page.driver.find_element_by_tag_name("iframe"))
+ self.page.find_by_id("btn-flash").click()
+
+ self._copies_rows()
+ self._copies_columns()
+
+ def _copies_rows(self):
+ pyperclip.copy("old clipboard contents")
+ time.sleep(5)
+ self.page.find_by_xpath("//*[contains(@class, 'sr')]/*[1]/input[@type='checkbox']").click()
+ self.page.find_by_xpath("//*[@id='btn-copy-row']").click()
+
+ self.assertEqual("'Some-Name','6'",
+ pyperclip.paste())
+
+ def _copies_columns(self):
+ pyperclip.copy("old clipboard contents")
+
+ self.page.find_by_xpath("//*[@data-test='output-column-header' and contains(., 'some_column')]/input").click()
+ self.page.find_by_xpath("//*[@id='btn-copy-row']").click()
+
+ self.assertEqual(
+ """'Some-Name'
+'Some-Other-Name'""",
+ pyperclip.paste())
+
+ def after(self):
+ self.page.close_query_tool()
+ self.page.remove_server(self.server)
+
+ connection = test_utils.get_db_connection(self.server['db'],
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ test_utils.drop_database(connection, "acceptance_test_db")
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 000000000..c89b3fa8d
--- /dev/null
+++ b/web/pgadmin/static/js/selection/column_selector.js
@@ -0,0 +1,92 @@
+define(['jquery', 'sources/selection/range_selection_helper', 'slickgrid'], function ($, rangeSelectionHelper) {
+ var ColumnSelector = function () {
+ 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);
+ }
+
+ updateRanges(grid, column.id);
+ }
+ }
+ );
+ grid.getSelectionModel().onSelectedRangesChanged
+ .subscribe(handleSelectedRangesChanged.bind(null, grid));
+ };
+
+ var handleSelectedRangesChanged = function (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 = rangeSelectionHelper.isRangeSelected(ranges, rangeSelectionHelper.rangeForColumn(grid, columnIndex));
+ if (!isStillSelected) {
+ toggleCheckbox($checkbox);
+ }
+ });
+ };
+
+ var updateRanges = function (grid, columnId) {
+ var selectionModel = grid.getSelectionModel();
+ var ranges = selectionModel.getSelectedRanges();
+
+ var columnIndex = grid.getColumnIndex(columnId);
+
+ var columnRange = rangeSelectionHelper.rangeForColumn(grid, columnIndex);
+ var newRanges;
+ if (rangeSelectionHelper.isRangeSelected(ranges, columnRange)) {
+ newRanges = rangeSelectionHelper.removeRange(ranges, columnRange);
+ } else {
+ if (rangeSelectionHelper.areAllRangesColumns(ranges, grid)) {
+ newRanges = rangeSelectionHelper.addRange(ranges, columnRange);
+ } else {
+ newRanges = [columnRange];
+ }
+ }
+ selectionModel.setSelectedRanges(newRanges);
+ };
+
+ 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 =
+ "" +
+ " " +
+ " " + columnDefinition.name + "" +
+ "";
+ return _.extend(columnDefinition, {
+ name: name
+ });
+ } else {
+ return columnDefinition;
+ }
+ });
+ };
+
+ $.extend(this, {
+ "init": init,
+ "getColumnDefinitionsWithCheckboxes": getColumnDefinitionsWithCheckboxes
+ });
+ };
+ return ColumnSelector;
+});
diff --git a/web/pgadmin/static/js/selection/copy_data.js b/web/pgadmin/static/js/selection/copy_data.js
new file mode 100644
index 000000000..018efeadb
--- /dev/null
+++ b/web/pgadmin/static/js/selection/copy_data.js
@@ -0,0 +1,52 @@
+define([
+ 'jquery',
+ 'underscore',
+ 'sources/selection/clipboard',
+ 'sources/selection/range_selection_helper',
+ 'sources/selection/range_boundary_navigator'],
+ function ($, _, clipboard, RangeSelectionHelper, rangeBoundaryNavigator) {
+ var copyData = function () {
+ var self = this;
+
+ 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];
+ });
+ setPasteRowButtonEnablement(self.can_edit, true);
+ } else {
+ self.copied_rows = [];
+ setPasteRowButtonEnablement(self.can_edit, false);
+ }
+
+ var csvText = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, selectedRanges);
+ if (csvText) {
+ clipboard.copyTextToClipboard(csvText);
+ }
+ };
+
+ var setPasteRowButtonEnablement = function (canEditFlag, isEnabled) {
+ if (canEditFlag) {
+ $("#btn-paste-row").prop('disabled', !isEnabled);
+ }
+ };
+
+ var allTheRangesAreFullRows = function (ranges, columnDefinitions) {
+ var colRangeBounds = ranges.map(function (range) {
+ return [range.fromCell, range.toCell];
+ });
+
+ if(RangeSelectionHelper.isFirstColumnData(columnDefinitions)) {
+ return _.isEqual(_.union.apply(null, colRangeBounds), [0, columnDefinitions.length - 1]);
+ }
+ return _.isEqual(_.union.apply(null, colRangeBounds), [1, columnDefinitions.length - 1]);
+ };
+
+ return copyData;
+});
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 000000000..31aee69f1
--- /dev/null
+++ b/web/pgadmin/static/js/selection/grid_selector.js
@@ -0,0 +1,79 @@
+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, eventArguments) {
+ if (eventArguments.column.selectAllOnClick) {
+ toggleSelectAll(grid);
+ }
+ });
+
+ grid.getSelectionModel().onSelectedRangesChanged
+ .subscribe(handleSelectedRangesChanged.bind(null, grid));
+ grid.registerPlugin(rowSelector);
+ grid.registerPlugin(columnSelector);
+ };
+
+ var getColumnDefinitionsWithCheckboxes = function (columnDefinitions) {
+ columnDefinitions = columnSelector.getColumnDefinitionsWithCheckboxes(columnDefinitions);
+ columnDefinitions = rowSelector.getColumnDefinitionsWithCheckboxes(columnDefinitions);
+
+ columnDefinitions[0].selectAllOnClick = true;
+ columnDefinitions[0].name = '' + columnDefinitions[0].name;
+ return columnDefinitions;
+ };
+
+ function handleSelectedRangesChanged(grid) {
+ $("[data-id='checkbox-select-all']").prop("checked", isEntireGridSelected(grid));
+ }
+
+ function isEntireGridSelected(grid) {
+ var selectionModel = grid.getSelectionModel();
+ var selectedRanges = selectionModel.getSelectedRanges();
+ return selectedRanges.length == 1 && isSameRange(selectedRanges[0], getRangeOfWholeGrid(grid));
+ }
+
+ function toggleSelectAll(grid) {
+ if (isEntireGridSelected(grid)) {
+ deselect(grid);
+ } else {
+ selectAll(grid)
+ }
+ }
+
+ var isSameRange = function (range, otherRange) {
+ return range.fromCell == otherRange.fromCell && range.toCell == otherRange.toCell &&
+ range.fromRow == otherRange.fromRow && range.toRow == otherRange.toRow;
+ };
+
+ function getRangeOfWholeGrid(grid) {
+ return new Slick.Range(0, 1, grid.getDataLength() - 1, grid.getColumns().length - 1);
+ }
+
+ function deselect(grid) {
+ var selectionModel = grid.getSelectionModel();
+ selectionModel.setSelectedRanges([]);
+ }
+
+ function selectAll(grid) {
+ var range = getRangeOfWholeGrid(grid);
+ var selectionModel = grid.getSelectionModel();
+
+ selectionModel.setSelectedRanges([range]);
+ }
+
+ $.extend(this, {
+ "init": init,
+ "getColumnDefinitionsWithCheckboxes": getColumnDefinitionsWithCheckboxes
+ });
+ };
+
+ return GridSelector;
+ });
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 000000000..a268d245f
--- /dev/null
+++ b/web/pgadmin/static/js/selection/range_boundary_navigator.js
@@ -0,0 +1,111 @@
+define(['sources/selection/range_selection_helper'], function (RangeSelectionHelper) {
+ return {
+ getUnion: function (allRanges) {
+ if (_.isEmpty(allRanges)) {
+ return [];
+ }
+
+ allRanges.sort(firstElementNumberComparator);
+ var unionedRanges = [allRanges[0]];
+
+ allRanges.forEach(function (range) {
+ var maxBeginningOfRange = _.last(unionedRanges);
+ if (isStartInsideRange(range, maxBeginningOfRange)) {
+ if (!isEndInsideRange(range, maxBeginningOfRange)) {
+ maxBeginningOfRange[1] = range[1];
+ }
+ } else {
+ unionedRanges.push(range);
+ }
+ });
+
+ return unionedRanges;
+
+ function firstElementNumberComparator(a, b) {
+ return a[0] - b[0];
+ }
+
+ function isStartInsideRange(range, surroundingRange) {
+ return range[0] <= surroundingRange[1] + 1;
+ }
+
+ function isEndInsideRange(range, surroundingRange) {
+ return range[1] <= surroundingRange[1];
+ }
+ },
+
+ 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 (!RangeSelectionHelper.isFirstColumnData(columnDefinitions)) {
+ 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/range_selection_helper.js b/web/pgadmin/static/js/selection/range_selection_helper.js
new file mode 100644
index 000000000..31ad3bf79
--- /dev/null
+++ b/web/pgadmin/static/js/selection/range_selection_helper.js
@@ -0,0 +1,78 @@
+define(['slickgrid'], function () {
+ var Slick = window.Slick;
+
+ var isSameRange = function (range, otherRange) {
+ return range.fromCell == otherRange.fromCell && range.toCell == otherRange.toCell &&
+ range.fromRow == otherRange.fromRow && range.toRow == otherRange.toRow;
+ };
+
+ var isRangeSelected = function (selectedRanges, range) {
+ return _.any(selectedRanges, function (selectedRange) {
+ return isSameRange(selectedRange, range)
+ })
+ };
+
+ var removeRange = function (selectedRanges, range) {
+ return _.filter(selectedRanges, function (selectedRange) {
+ return !(isSameRange(selectedRange, range))
+ })
+ };
+
+ var addRange = function (ranges, range) {
+ ranges.push(range);
+ return ranges;
+ };
+
+ var areAllRangesRows = function (ranges, grid) {
+ return _.every(ranges, function (range) {
+ return range.fromRow == range.toRow &&
+ range.fromCell == 1 && range.toCell == grid.getColumns().length - 1
+ })
+ };
+
+ var areAllRangesColumns = function (ranges, grid) {
+ return _.every(ranges, function (range) {
+ return range.fromCell == range.toCell &&
+ range.fromRow == 0 && range.toRow == grid.getDataLength() - 1
+ })
+ };
+
+ var rangeForRow = function (grid, rowId) {
+ var columnDefinitions = grid.getColumns();
+ if(isFirstColumnData(columnDefinitions)) {
+ return new Slick.Range(rowId, 0, rowId, grid.getColumns().length - 1);
+ }
+ return new Slick.Range(rowId, 1, rowId, grid.getColumns().length - 1);
+ };
+
+ function rangeForColumn(grid, columnIndex) {
+ return new Slick.Range(0, columnIndex, grid.getDataLength() - 1, columnIndex)
+ };
+
+ var getRangeOfWholeGrid = function (grid) {
+ return new Slick.Range(0, 1, grid.getDataLength() - 1, grid.getColumns().length - 1);
+ };
+
+ var isEntireGridSelected = function (grid) {
+ var selectionModel = grid.getSelectionModel();
+ var selectedRanges = selectionModel.getSelectedRanges();
+ return selectedRanges.length == 1 && isSameRange(selectedRanges[0], getRangeOfWholeGrid(grid));
+ };
+
+ var isFirstColumnData = function (columnDefinitions) {
+ return !_.isUndefined(columnDefinitions[0].pos);
+ };
+
+ return {
+ addRange: addRange,
+ removeRange: removeRange,
+ isRangeSelected: isRangeSelected,
+ areAllRangesRows: areAllRangesRows,
+ areAllRangesColumns: areAllRangesColumns,
+ rangeForRow: rangeForRow,
+ rangeForColumn: rangeForColumn,
+ isEntireGridSelected: isEntireGridSelected,
+ getRangeOfWholeGrid: getRangeOfWholeGrid,
+ isFirstColumnData: isFirstColumnData
+ }
+});
\ 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
new file mode 100644
index 000000000..76a8c1a7e
--- /dev/null
+++ b/web/pgadmin/static/js/selection/row_selector.js
@@ -0,0 +1,85 @@
+define(['jquery', 'sources/selection/range_selection_helper', 'slickgrid'], function ($, rangeSelectionHelper) {
+ var RowSelector = function () {
+ var Slick = window.Slick;
+
+ var gridEventBus = new Slick.EventHandler();
+
+ var init = function (grid) {
+ grid.getSelectionModel()
+ .onSelectedRangesChanged.subscribe(handleSelectedRangesChanged.bind(null, grid));
+ gridEventBus
+ .subscribe(grid.onClick, handleClick.bind(null, grid))
+ };
+
+ var handleClick = function (grid, event, args) {
+ if (grid.getColumns()[args.cell].id === 'row-header-column') {
+ if (event.target.type != "checkbox") {
+ var checkbox = $(event.target).find('input[type="checkbox"]');
+ toggleCheckbox($(checkbox));
+ }
+ updateRanges(grid, args.row);
+ }
+ }
+
+ var handleSelectedRangesChanged = function (grid, event, ranges) {
+ $('[data-cell-type="row-header-checkbox"]:checked')
+ .each(function (index, checkbox) {
+ var $checkbox = $(checkbox);
+ var row = parseInt($checkbox.data('row'));
+ var isStillSelected = rangeSelectionHelper.isRangeSelected(ranges,
+ rangeSelectionHelper.rangeForRow(grid, row));
+ if (!isStillSelected) {
+ toggleCheckbox($checkbox);
+ }
+ });
+ }
+
+ var updateRanges = function (grid, rowId) {
+ var selectionModel = grid.getSelectionModel();
+ var ranges = selectionModel.getSelectedRanges();
+
+ var rowRange = rangeSelectionHelper.rangeForRow(grid, rowId);
+
+ var newRanges;
+ if (rangeSelectionHelper.isRangeSelected(ranges, rowRange)) {
+ newRanges = rangeSelectionHelper.removeRange(ranges, rowRange);
+ } else {
+ if (rangeSelectionHelper.areAllRangesRows(ranges, grid)) {
+ newRanges = rangeSelectionHelper.addRange(ranges, rowRange);
+ } else {
+ newRanges = [rowRange];
+ }
+ }
+ selectionModel.setSelectedRanges(newRanges);
+ }
+
+ var toggleCheckbox = function (checkbox) {
+ if (checkbox.prop("checked")) {
+ checkbox.prop("checked", false)
+ } else {
+ checkbox.prop("checked", true)
+ }
+ };
+
+ var getColumnDefinitionsWithCheckboxes = function (columnDefinitions) {
+ columnDefinitions.unshift({
+ id: 'row-header-column',
+ name: '',
+ selectable: false,
+ focusable: false,
+ formatter: function (rowIndex) {
+ return ''
+ }
+ });
+ return columnDefinitions;
+ };
+
+ $.extend(this, {
+ "init": init,
+ "getColumnDefinitionsWithCheckboxes": getColumnDefinitionsWithCheckboxes
+ });
+ };
+ return RowSelector;
+});
diff --git a/web/pgadmin/tools/datagrid/templates/datagrid/index.html b/web/pgadmin/tools/datagrid/templates/datagrid/index.html
index 1d71b21e4..727eb777f 100644
--- a/web/pgadmin/tools/datagrid/templates/datagrid/index.html
+++ b/web/pgadmin/tools/datagrid/templates/datagrid/index.html
@@ -96,7 +96,7 @@
-