diff --git a/docs/en_US/release_notes_4_25.rst b/docs/en_US/release_notes_4_25.rst index d78006819..a26657571 100644 --- a/docs/en_US/release_notes_4_25.rst +++ b/docs/en_US/release_notes_4_25.rst @@ -30,6 +30,7 @@ Bug fixes | `Issue #4810 `_ - Fixed an issue where the user is not able to save the new row if the table is empty. | `Issue #5429 `_ - Ensure that the Dictionaries drop-down shows all the dictionaries in the FTS configuration dialog. | `Issue #5490 `_ - Make the runtime configuration dialog non-modal. +| `Issue #5526 `_ - Fixed an issue where copying and pasting a cell with multiple line data will result in multiple rows. | `Issue #5632 `_ - Ensure that the user will be able to modify the start value of the Identity column. | `Issue #5646 `_ - Ensure that RLS Policy node should be searchable using search object. | `Issue #5664 `_ - Fixed an issue where 'ALTER VIEW' statement is missing when the user sets the default value of a column for View. diff --git a/web/pgadmin/static/js/selection/range_boundary_navigator.js b/web/pgadmin/static/js/selection/range_boundary_navigator.js index bad5a7395..886fb00b7 100644 --- a/web/pgadmin/static/js/selection/range_boundary_navigator.js +++ b/web/pgadmin/static/js/selection/range_boundary_navigator.js @@ -139,11 +139,16 @@ define(['sources/selection/range_selection_helper', 'json-bignumber'], quoting = CSVOptions.quoting || 'strings', quote_char = CSVOptions.quote_char || '"'; + const escape = (iStr) => { + return (quote_char == '"') ? + iStr.replace(/\"/g, '""') : iStr.replace(/\'/g, '\'\''); + }; + if (quoting == 'all') { if (val && _.isObject(val)) { val = quote_char + JSONBigNumber.stringify(val) + quote_char; } else if (val) { - val = quote_char + val.toString() + quote_char; + val = quote_char + escape(val.toString()) + quote_char; } else if (_.isNull(val) || _.isUndefined(val)) { val = ''; } @@ -152,7 +157,7 @@ define(['sources/selection/range_selection_helper', 'json-bignumber'], if (val && _.isObject(val)) { val = quote_char + JSONBigNumber.stringify(val) + quote_char; } else if (val && cell_type != 'number' && cell_type != 'boolean') { - val = quote_char + val.toString() + quote_char; + val = quote_char + escape(val.toString()) + quote_char; } else if (_.isNull(val) || _.isUndefined(val)) { val = ''; } diff --git a/web/pgadmin/static/js/utils.js b/web/pgadmin/static/js/utils.js index 89b0c5858..79e64fa95 100644 --- a/web/pgadmin/static/js/utils.js +++ b/web/pgadmin/static/js/utils.js @@ -296,3 +296,67 @@ export function sprintf(i_str) { return i_str; } } + +// Modified ref: http://stackoverflow.com/a/1293163/2343 to suite pgAdmin. +// This will parse a delimited string into an array of arrays. +export function CSVToArray( strData, strDelimiter, quoteChar){ + strDelimiter = strDelimiter || ','; + quoteChar = quoteChar || '"'; + + // Create a regular expression to parse the CSV values. + var objPattern = new RegExp( + ( + // Delimiters. + '(\\' + strDelimiter + '|\\r?\\n|\\r|^)' + + // Quoted fields. + (quoteChar == '"' ? '(?:"([^"]*(?:""[^"]*)*)"|' : '(?:\'([^\']*(?:\'\'[^\']*)*)\'|') + + // Standard fields. + (quoteChar == '"' ? '([^"\\' + strDelimiter + '\\r\\n]*))': '([^\'\\' + strDelimiter + '\\r\\n]*))') + ), + 'gi' + ); + + // Create an array to hold our data. Give the array + // a default empty first row. + var arrData = [[]]; + + // Create an array to hold our individual pattern + // matching groups. + var arrMatches = null; + + // Keep looping over the regular expression matches + // until we can no longer find a match. + while ((arrMatches = objPattern.exec( strData ))){ + // Get the delimiter that was found. + var strMatchedDelimiter = arrMatches[ 1 ]; + + // Check to see if the given delimiter has a length + // (is not the start of string) and if it matches + // field delimiter. If id does not, then we know + // that this delimiter is a row delimiter. + if (strMatchedDelimiter.length && strMatchedDelimiter !== strDelimiter){ + // Since we have reached a new row of data, + // add an empty row to our data array. + arrData.push( [] ); + } + + var strMatchedValue; + + // Now that we have our delimiter out of the way, + // let's check to see which kind of value we + // captured (quoted or unquoted). + if (arrMatches[ 2 ]){ + // We found a quoted value. When we capture + // this value, unescape any quotes. + strMatchedValue = arrMatches[ 2 ].replace(new RegExp( quoteChar+quoteChar, 'g' ), quoteChar); + } else { + // We found a non-quoted value. + strMatchedValue = arrMatches[ 3 ]; + } + // Now that we have our value string, let's add + // it to the data array. + arrData[ arrData.length - 1 ].push( strMatchedValue ); + } + // Return the parsed data. + return arrData; +} diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js index b2081b97e..fba42bd8c 100644 --- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js +++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js @@ -10,7 +10,7 @@ define('tools.querytool', [ 'sources/gettext', 'sources/url_for', 'jquery', 'jquery.ui', 'jqueryui.position', 'underscore', 'pgadmin.alertifyjs', - 'sources/pgadmin', 'backbone', 'bundled_codemirror', + 'sources/pgadmin', 'backbone', 'bundled_codemirror', 'sources/utils', 'pgadmin.misc.explain', 'sources/selection/grid_selector', 'sources/selection/active_cell_capture', @@ -48,7 +48,7 @@ define('tools.querytool', [ 'pgadmin.browser', 'pgadmin.tools.user_management', ], function( - gettext, url_for, $, jqueryui, jqueryui_position, _, alertify, pgAdmin, Backbone, codemirror, + gettext, url_for, $, jqueryui, jqueryui_position, _, alertify, pgAdmin, Backbone, codemirror, pgadminUtils, pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent, XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, FilterHandler, GeometryViewer, historyColl, queryHist, querySources, @@ -3739,23 +3739,18 @@ define('tools.querytool', [ // This function will paste the selected row. _paste_row: function() { - var self = this; let rowsText = clipboard.getTextFromClipboard(); - let copied_rows = rowsText.split('\n'); + let copied_rows = pgadminUtils.CSVToArray(rowsText, self.preferences.results_grid_field_separator, self.preferences.results_grid_quote_char); // Do not parse row if rows are copied with headers if(pgAdmin.SqlEditor.copiedInOtherSessionWithHeaders) { copied_rows = copied_rows.slice(1); } - copied_rows = copied_rows.reduce((partial, item) => { + copied_rows = copied_rows.reduce((partial, values) => { // split each row with field separator character - const values = item.split(self.preferences.results_grid_field_separator); let row = {}; for (let col in self.columns) { - let v = null; - if (values.length > col) { - v = values[col].replace(new RegExp(`^\\${self.preferences.results_grid_quote_char}`), '').replace(new RegExp(`\\${self.preferences.results_grid_quote_char}$`), ''); - } + let v = values[col]; // set value to default or null depending on column metadata if(v === '') { diff --git a/web/regression/javascript/pgadmin_utils_spec.js b/web/regression/javascript/pgadmin_utils_spec.js index 31d2cc29b..23ffef4b0 100644 --- a/web/regression/javascript/pgadmin_utils_spec.js +++ b/web/regression/javascript/pgadmin_utils_spec.js @@ -7,7 +7,8 @@ // ////////////////////////////////////////////////////////////// -import { getEpoch, getGCD, getMod, quote_ident, parseFuncParams, getRandomInt, sprintf } from 'sources/utils'; +import { getEpoch, getGCD, getMod, quote_ident, parseFuncParams, + getRandomInt, sprintf, CSVToArray } from 'sources/utils'; describe('getEpoch', function () { it('should return non zero', function () { @@ -168,3 +169,25 @@ describe('sprintf', function () { ); }); }); + +describe('CSVToArray', function() { + it('simple input single record', function() { + expect(CSVToArray('a,b')).toEqual([['a', 'b']]); + }); + + it('simple input delimeter change', function() { + expect(CSVToArray('"a";"b"', ';')).toEqual([['a', 'b']]); + }); + + it('simple input multi records', function() { + expect(CSVToArray('"a","b"\n"c","d"')).toEqual([['a', 'b'], ['c', 'd']]); + }); + + it('multiline input containing double quotes', function() { + expect(CSVToArray('"hello ""a\nb""","c"')).toEqual([['hello "a\nb"','c']]); + }); + + it('multiline input containing single quotes', function() { + expect(CSVToArray('\'hello \'\'a\nb\'\'\',\'c\'', ',', '\'')).toEqual([['hello \'a\nb\'','c']]); + }); +});