diff --git a/docs/en_US/release_notes_4_14.rst b/docs/en_US/release_notes_4_14.rst index 579b9f01f..10a4770e4 100644 --- a/docs/en_US/release_notes_4_14.rst +++ b/docs/en_US/release_notes_4_14.rst @@ -9,6 +9,8 @@ This release contains a number of bug fixes and new features since the release o New features ************ +| `Issue #3009 `_ - Added Copy with headers functionality when copy data from Query Tool/View Data. + Housekeeping ************ 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 index a3a55f32c..9bb458710 100644 --- a/web/pgadmin/feature_tests/copy_selected_query_results_feature_test.py +++ b/web/pgadmin/feature_tests/copy_selected_query_results_feature_test.py @@ -58,6 +58,7 @@ class CopySelectedQueryResultsFeatureTest(BaseFeatureTest): self._shift_resizes_rectangular_selection() self._shift_resizes_column_selection() self._mouseup_outside_grid_still_makes_a_selection() + self._copies_rows_with_header() def _copies_rows(self): pyperclip.copy("old clipboard contents") @@ -72,6 +73,24 @@ class CopySelectedQueryResultsFeatureTest(BaseFeatureTest): self.assertEqual('"Some-Name"\t"6"\t"some info"', pyperclip.paste()) + def _copies_rows_with_header(self): + self.page.find_by_css_selector('#btn-copy-row-dropdown').click() + self.page.find_by_css_selector('a#btn-copy-with-header').click() + + pyperclip.copy("old clipboard contents") + select_all = self.page.find_by_xpath( + QueryToolLocators.select_all_column) + select_all.click() + + copy_button = self.page.find_by_css_selector( + QueryToolLocators.copy_button_css) + copy_button.click() + + self.assertEqual("""\"some_column"\t"value"\t"details" +\"Some-Name"\t"6"\t"some info" +\"Some-Other-Name"\t"22"\t"some other info" +\"Yet-Another-Name"\t"14"\t"cool info\"""", pyperclip.paste()) + def _copies_columns(self): pyperclip.copy("old clipboard contents") column = self.page.find_by_css_selector( diff --git a/web/pgadmin/static/js/selection/column_selector.js b/web/pgadmin/static/js/selection/column_selector.js index a8c8c9c2b..7bb9411ea 100644 --- a/web/pgadmin/static/js/selection/column_selector.js +++ b/web/pgadmin/static/js/selection/column_selector.js @@ -49,18 +49,44 @@ define([ if (!(event.isPropagationStopped() || event.isImmediatePropagationStopped())) { updateRanges(grid, columnDefinition.id); } + } else { + toggleColumnHeaderForCopyHeader(grid); } }; + var toggleColumnHeaderForCopyHeader = function(grid) { + if(!$('.copy-with-header').hasClass('visibility-hidden')) { + var selRowCnt = grid.getSelectedRows(); + $('.slick-header-column').each(function (index, columnHeader) { + if (selRowCnt.length == 0) { + $(columnHeader).removeClass('selected'); + grid.getColumns()[index].selected = false; + } + else { + if (index > 0 && grid.getColumns()[index].selectable) { + $(columnHeader).addClass('selected'); + grid.getColumns()[index].selected = true; + } + } + + }); + } else { + $('.slick-header-column').each(function (index, columnHeader) { + $(columnHeader).removeClass('selected'); + }); + } + }.bind(RangeSelectionHelper); + var handleSelectedRangesChanged = function (grid, event, selectedRanges) { $('.slick-header-column').each(function (index, columnHeader) { var $spanHeaderColumn = $(columnHeader).find('[data-cell-type="column-header-row"]'); var columnIndex = grid.getColumnIndex($spanHeaderColumn.data('column-id')); - if (isColumnSelected(grid, selectedRanges, columnIndex)) { $(columnHeader).addClass('selected'); - } else { + if (columnIndex) grid.getColumns()[columnIndex].selected = true; + } else if(!RangeSelectionHelper.areAllRangesCompleteRows(grid, selectedRanges)){ $(columnHeader).removeClass('selected'); + if (columnIndex) grid.getColumns()[columnIndex].selected = false; } }); }; @@ -132,6 +158,7 @@ define([ 'getColumnDefinitions': getColumnDefinitions, 'onBeforeColumnSelectAll': onBeforeColumnSelectAll, 'onColumnSelectAll': onColumnSelectAll, + 'toggleColumnHeaderForCopyHeader': toggleColumnHeaderForCopyHeader, }); }; return ColumnSelector; diff --git a/web/pgadmin/static/js/selection/copy_data.js b/web/pgadmin/static/js/selection/copy_data.js index c6a18b8a8..2fc89485f 100644 --- a/web/pgadmin/static/js/selection/copy_data.js +++ b/web/pgadmin/static/js/selection/copy_data.js @@ -34,12 +34,17 @@ function ($, _, clipboard, RangeSelectionHelper, rangeBoundaryNavigator) { self.copied_rows = []; setPasteRowButtonEnablement(self.can_edit, false); } - var csvText = rangeBoundaryNavigator.rangesToCsv(dataView.getItems(), columnDefinitions, selectedRanges, CSVOptions); + var csvText = rangeBoundaryNavigator.rangesToCsv(dataView.getItems(), columnDefinitions, + selectedRanges, CSVOptions, copyWithHeader()); if (csvText) { clipboard.copyTextToClipboard(csvText); } }; + var copyWithHeader = function () { + return !$('.copy-with-header').hasClass('visibility-hidden'); + }; + var setPasteRowButtonEnablement = function (canEditFlag, isEnabled) { if (canEditFlag) { $('#btn-paste-row').prop('disabled', !isEnabled); diff --git a/web/pgadmin/static/js/selection/range_boundary_navigator.js b/web/pgadmin/static/js/selection/range_boundary_navigator.js index c810431d1..aed5ae60c 100644 --- a/web/pgadmin/static/js/selection/range_boundary_navigator.js +++ b/web/pgadmin/static/js/selection/range_boundary_navigator.js @@ -66,7 +66,21 @@ define(['sources/selection/range_selection_helper'], }.bind(this)); }, - rangesToCsv: function (data, columnDefinitions, selectedRanges, CSVOptions) { + getHeaderData: function (columnDefinitions, CSVOptions) { + var headerData = [], + field_separator = CSVOptions.field_separator || '\t', + quote_char = CSVOptions.quote_char || '"'; + + _.each(columnDefinitions, function(col) { + if(col.display_name && col.selected) { + headerData.push(quote_char + col.display_name + quote_char); + } + }); + + return headerData.join(field_separator); + }, + + rangesToCsv: function (data, columnDefinitions, selectedRanges, CSVOptions, copyWithHeader) { var rowRangeBounds = selectedRanges.map(function (range) { return [range.fromRow, range.toRow]; @@ -84,6 +98,13 @@ define(['sources/selection/range_selection_helper'], return rowData.join(field_separator); }); + if (copyWithHeader) { + var headerData = ''; + headerData = this.getHeaderData(columnDefinitions, CSVOptions); + + return headerData + '\n' + csvRows.join('\n'); + } + return csvRows.join('\n'); }, diff --git a/web/pgadmin/static/js/selection/row_selector.js b/web/pgadmin/static/js/selection/row_selector.js index 7cee0b1fa..913832a3a 100644 --- a/web/pgadmin/static/js/selection/row_selector.js +++ b/web/pgadmin/static/js/selection/row_selector.js @@ -10,12 +10,14 @@ define([ 'jquery', 'sources/selection/range_selection_helper', + 'sources/selection/column_selector', 'slickgrid', -], function ($, RangeSelectionHelper) { +], function ($, RangeSelectionHelper, ColumnSelector) { var RowSelector = function () { var Slick = window.Slick; var gridEventBus = new Slick.EventHandler(); + var columnSelector = new ColumnSelector(); var init = function (grid) { grid.getSelectionModel().onSelectedRangesChanged @@ -34,6 +36,7 @@ define([ $rowHeaderSpan.parent().toggleClass('selected'); updateRanges(grid, args.row); + columnSelector.toggleColumnHeaderForCopyHeader(grid); } }; diff --git a/web/pgadmin/tools/datagrid/templates/datagrid/index.html b/web/pgadmin/tools/datagrid/templates/datagrid/index.html index 3b3e3508d..3eb02e57e 100644 --- a/web/pgadmin/tools/datagrid/templates/datagrid/index.html +++ b/web/pgadmin/tools/datagrid/templates/datagrid/index.html @@ -121,6 +121,18 @@ tabindex="0" disabled> + + '); + buttonCopyWithHeader = $(''); $('body').append(buttonPasteRow); + $('body').append(buttonCopyWithHeader); grid = new SlickGrid('#grid', dataView, columns, {}); grid.CSVOptions = CSVOptions; dataView.setItems(data, '__temp_PK'); @@ -77,6 +79,7 @@ describe('copyData', function () { grid.destroy(); gridContainer.remove(); buttonPasteRow.remove(); + buttonCopyWithHeader.remove(); }); describe('when rows are selected', function () { diff --git a/web/regression/javascript/slickgrid/event_handlers/handle_query_output_keyboard_event_spec.js b/web/regression/javascript/slickgrid/event_handlers/handle_query_output_keyboard_event_spec.js index 407d5efe5..644db37a9 100644 --- a/web/regression/javascript/slickgrid/event_handlers/handle_query_output_keyboard_event_spec.js +++ b/web/regression/javascript/slickgrid/event_handlers/handle_query_output_keyboard_event_spec.js @@ -18,7 +18,7 @@ import $ from 'jquery'; describe('#handleQueryOutputKeyboardEvent', function () { var event, grid, slickEvent; - var handleQueryOutputKeyboardEvent; + var handleQueryOutputKeyboardEvent, buttonCopyWithHeader; beforeEach(function () { event = { @@ -47,6 +47,9 @@ describe('#handleQueryOutputKeyboardEvent', function () { grid: grid, }; + buttonCopyWithHeader = $(''); + $('body').append(buttonCopyWithHeader); + spyOn(clipboard, 'copyTextToClipboard'); handleQueryOutputKeyboardEvent = HandleQueryOutputKeyboardEvent.bind(window); });