[pgAdmin4][Patch] - RM 3009 - Right click to copy from data grid, optionally with headers.

Khushboo Vashi <[email protected]>
Newsgroups gmane.comp.db.postgresql.pgadmin.devel
Message-ID <CAFOhELeP1KDqLM8hhgMP+9spsycgnmpxnmLmgXjQKTnBc2ffFg@mail.gmail.com>
Hi,

Please find the attached patch for RM #3009 - Right click to copy from data
grid, optionally with headers.

Query Tool / View data:

Currently the result-set can be copied without header. With this patch the
result-set can be copied with the header also and that is optional.

To copy the result-set with header, the option '*Copy with header'* is
given next with the Copy button in the toolbar in the form of dropdown.

Thanks,
Khushboo
RM_3009.patch (application/octet-stream, 11.6 KB)
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..6f1b8ab68 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\tvalue\tdetails
+\"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..c28d2c30b 100644
--- a/web/pgadmin/static/js/selection/column_selector.js
+++ b/web/pgadmin/static/js/selection/column_selector.js
@@ -49,6 +49,21 @@ define([
         if (!(event.isPropagationStopped() || event.isImmediatePropagationStopped())) {
           updateRanges(grid, columnDefinition.id);
         }
+      } else if(!$('.copy-with-header').hasClass('visibility-hidden')) {
+        var selRowCnt = grid.getSelectedRows();
+        $('.slick-header-column').each(function (index, columnHeader) {
+          if (selRowCnt == 0) {
+            $(columnHeader).removeClass('selected');
+            grid.getColumns()[index].selected = false;
+          }
+          else {
+            if (index > 0) {
+              $(columnHeader).addClass('selected');
+              grid.getColumns()[index].selected = true;
+            }
+          }
+
+        });
       }
     };
 
@@ -59,8 +74,10 @@ define([
 
         if (isColumnSelected(grid, selectedRanges, columnIndex)) {
           $(columnHeader).addClass('selected');
+          if (columnIndex) grid.getColumns()[columnIndex].selected = true;
         } else {
           $(columnHeader).removeClass('selected');
+          if (columnIndex) grid.getColumns()[columnIndex].selected = false;
         }
       });
     };
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/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>
                     <i class="fa fa-files-o sql-icon-lg" aria-hidden="true"></i>
                 </button>
+                <button id="btn-copy-row-dropdown" type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split"
+                        data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
+                        tabindex="0">
+                </button>
+                <ul class="dropdown-menu">
+                    <li>
+                        <a class="dropdown-item" id="btn-copy-with-header" href="#" tabindex="0">
+                            <i class="copy-with-header fa fa-check visibility-hidden" aria-hidden="true"></i>
+                            <span> {{ _('Copy with headers') }} </span>
+                        </a>
+                    </li>
+                </ul>
                 <button id="btn-paste-row" type="button" class="btn btn-sm btn-secondary"
                         title=""
                         accesskey=""
diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
index 37d15a9c9..ffa882881 100644
--- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
+++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
@@ -108,6 +108,7 @@ define('tools.querytool', [
       'click #btn-remove-filter': 'on_remove_filter',
       'click #btn-cancel': 'on_cancel',
       'click #btn-copy-row': 'on_copy_row',
+      'click #btn-copy-with-header': 'on_copy_row_with_header',
       'click #btn-paste-row': 'on_paste_row',
       'click #btn-flash': 'on_flash',
       'click #btn-flash-menu': 'on_flash',
@@ -1606,6 +1607,19 @@ define('tools.querytool', [
 
     },
 
+    // Callback function for copy with header button click.
+    on_copy_row_with_header: function() {
+      var self = this;
+
+      // Toggle the button
+      self.handler.trigger(
+        'pgadmin-sqleditor:button:copy_row_with_header',
+        self,
+        self.handler
+      );
+
+    },
+
     // Callback function for paste button click.
     on_paste_row: function() {
       var self = this;
@@ -2314,6 +2328,7 @@ define('tools.querytool', [
         self.on('pgadmin-sqleditor:button:exclude_filter', self._exclude_filter, self);
         self.on('pgadmin-sqleditor:button:remove_filter', self._remove_filter, self);
         self.on('pgadmin-sqleditor:button:copy_row', self._copy_row, self);
+        self.on('pgadmin-sqleditor:button:copy_row_with_header', self._copy_row_with_header, self);
         self.on('pgadmin-sqleditor:button:paste_row', self._paste_row, self);
         self.on('pgadmin-sqleditor:button:limit', self._set_limit, self);
         self.on('pgadmin-sqleditor:button:cancel-query', self._cancel_query, self);
@@ -3644,6 +3659,10 @@ define('tools.querytool', [
       // This function will copy the selected row.
       _copy_row: copyData,
 
+      _copy_row_with_header: function() {
+        $('.copy-with-header').toggleClass('visibility-hidden');
+      },
+
       // This function will paste the selected row.
       _paste_row: function() {
         var self = this,
diff --git a/web/regression/javascript/selection/copy_data_spec.js b/web/regression/javascript/selection/copy_data_spec.js
index 3ce49e927..9b87752ab 100644
--- a/web/regression/javascript/selection/copy_data_spec.js
+++ b/web/regression/javascript/selection/copy_data_spec.js
@@ -17,7 +17,7 @@ import copyData from '../../../pgadmin/static/js/selection/copy_data';
 import RangeSelectionHelper from 'sources/selection/range_selection_helper';
 import XCellSelectionModel from 'sources/selection/xcell_selection_model';
 describe('copyData', function () {
-  var grid, sqlEditor, gridContainer, buttonPasteRow;
+  var grid, sqlEditor, gridContainer, buttonPasteRow, buttonCopyWithHeader;
   var SlickGrid;
 
   beforeEach(function () {
@@ -65,7 +65,9 @@ describe('copyData', function () {
     gridContainer = $('<div id="grid"></div>');
     $('body').append(gridContainer);
     buttonPasteRow = $('<button id="btn-paste-row" disabled></button>');
+    buttonCopyWithHeader = $('<button class="copy-with-header visibility-hidden"></button>');
     $('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 = $('<button class="copy-with-header visibility-hidden"></button>');
+    $('body').append(buttonCopyWithHeader);
+
     spyOn(clipboard, 'copyTextToClipboard');
     handleQueryOutputKeyboardEvent = HandleQueryOutputKeyboardEvent.bind(window);
   });
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.