Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed double quotes copy and paste bug #2202

Merged
merged 11 commits into from
Sep 7, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).

- [#2152](https://github.com/plotly/dash/pull/2152) Fix bug [#2128](https://github.com/plotly/dash/issues/2128) preventing rendering of multiple components inside a dictionary.
- [#2187](https://github.com/plotly/dash/pull/2187) Fix confusing error message when trying to use pytest fixtures but `dash[testing]` is not installed.
- [#2202](https://github.com/plotly/dash/pull/2202) Fix bug [#2185](https://github.com/plotly/dash/issues/2185) when you copy text with multiple quotes into a table

## [2.6.1] - 2022-08-01

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export default class TableClipboardHelper {
? TableClipboardHelper.localCopyWithoutHeaders
: TableClipboardHelper.lastLocalCopy;
const values =
localDf === text ? localCopy : SheetClip.prototype.parse(text);
localDf === text ? localCopy : TableClipboardHelper.parse(text);

return applyClipboardToData(
values,
Expand All @@ -111,4 +111,63 @@ export default class TableClipboardHelper {
overflowRows
);
}

private static parse(str: string) {
let r,
rlen,
a = 0,
c,
clen,
multiline,
last,
arr: string[][] = [[]];
const rows = str.split('\n');
if (rows.length > 1 && rows[rows.length - 1] === '') {
rows.pop();
}
arr = [];
for (r = 0, rlen = rows.length; r < rlen; r += 1) {
const row = rows[r].split('\t');
for (c = 0, clen = row.length; c < clen; c += 1) {
if (!arr[a]) {
arr[a] = [];
}
if (multiline && c === 0) {
last = arr[a].length - 1;
arr[a][last] =
arr[a][last] + '\n' + row[0].replace(/""/g, '"');
if (
multiline &&
TableClipboardHelper.countQuotes(row[0]) & 1
) {
multiline = false;
arr[a][last] = arr[a][last].substring(
0,
arr[a][last].length - 1
);
}
} else {
if (
c === clen - 1 &&
row[c].indexOf('"') === 0 &&
TableClipboardHelper.countQuotes(row[c]) & 1
) {
arr[a].push(row[c].substring(1).replace(/""/g, '"'));
multiline = true;
} else {
arr[a].push(row[c]);
multiline = false;
}
}
}
if (!multiline) {
a += 1;
}
}
return arr;
}

private static countQuotes(str: string) {
return str.split('"').length - 1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {expect} from 'chai';

import TableClipboardHelper from 'dash-table/utils/TableClipboardHelper';

describe('table clipboard helper tests', () => {
it('test parse basic', () => {
const res = TableClipboardHelper.parse('abc\tefg\n123\t456');
expect(res.length).to.equal(2);
expect(res[0].length).to.equal(2);
expect(res[1].length).to.equal(2);
expect(res[0][0]).to.equal('abc');
expect(res[0][1]).to.equal('efg');
expect(res[1][0]).to.equal('123');
expect(res[1][1]).to.equal('456');
});

it('test parse with double quotes', () => {
const res = TableClipboardHelper.parse('a""bc\tefg\n123\t456');
expect(res.length).to.equal(2);
expect(res[0].length).to.equal(2);
expect(res[1].length).to.equal(2);
expect(res[0][0]).to.equal('a""bc');
expect(res[0][1]).to.equal('efg');
expect(res[1][0]).to.equal('123');
expect(res[1][1]).to.equal('456');
});

it('test with multiline', () => {
const res = TableClipboardHelper.parse('"a\nb\nc"\tefg\n123\t456');
expect(res.length).to.equal(2);
expect(res[0].length).to.equal(2);
expect(res[1].length).to.equal(2);
expect(res[0][0]).to.equal('a\nb\nc');
expect(res[0][1]).to.equal('efg');
expect(res[1][0]).to.equal('123');
expect(res[1][1]).to.equal('456');
});

it('test with multiline and double quotes', () => {
const res = TableClipboardHelper.parse('"a\nb""c"\te""fg\n123\t456');
expect(res.length).to.equal(2);
expect(res[0].length).to.equal(2);
expect(res[1].length).to.equal(2);
expect(res[0][0]).to.equal('a\nb"c');
expect(res[0][1]).to.equal('e""fg');
expect(res[1][0]).to.equal('123');
expect(res[1][1]).to.equal('456');
});
});
8 changes: 4 additions & 4 deletions components/dash-table/tests/selenium/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
_ANY = ".dash-spreadsheet"
_TIMEOUT = 10

CMD = Keys.COMMAND if platform.system() == "Darwin" else Keys.CONTROL


class HoldKeyContext:
@preconditions(_validate_mixin, _validate_key)
Expand Down Expand Up @@ -268,8 +270,6 @@ def filter(self):
)

def filter_clear(self):
CMD = Keys.COMMAND if platform.system() == "Darwin" else Keys.CONTROL

self.filter().find_element(By.CSS_SELECTOR, "input").click()
ac = ActionChains(self.mixin.driver)
ac.key_down(CMD)
Expand Down Expand Up @@ -518,11 +518,11 @@ def get_table_ids(self):
)

def copy(self):
with self.hold(Keys.CONTROL):
with self.hold(CMD):
self.send_keys("c")

def paste(self):
with self.hold(Keys.CONTROL):
with self.hold(CMD):
self.send_keys("v")

@preconditions(_validate_key)
Expand Down
58 changes: 57 additions & 1 deletion components/dash-table/tests/selenium/test_basic_copy_paste.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ def get_app():
cell_selectable=False,
sort_action="native",
),
DataTable(
id="table4",
data=[
{"string": 'a""b', "int": 10},
{"string": 'hello\n""hi', "int": 11},
],
columns=[
{"name": "string", "id": "string"},
{"name": "int", "id": "int"},
],
editable=True,
sort_action="native",
include_headers_on_copy_paste=True,
),
]
)

Expand Down Expand Up @@ -322,7 +336,7 @@ def test_tbcp010_copy_from_unselectable_cells_table(test):
source.cell(2, 2).double_click()
assert source.cell(2, 2).get_text() == test.get_selected_text()

# copy the source text to clipboard using CTRL+C
# copy the source text to clipboard using CTRL+C or COMMAND+C
test.copy()

# assert the target cell value is different before paste
Expand All @@ -334,3 +348,45 @@ def test_tbcp010_copy_from_unselectable_cells_table(test):
assert target.cell(1, 1).get_text() == source.cell(2, 2).get_text()

assert test.get_log_errors() == []


def test_tbcp011_copy_double_quotes(test):
test.start_server(get_app())

source = test.table("table4")
target = test.table("table2")

source.cell(0, 0).click()
with test.hold(Keys.SHIFT):
source.cell(0, 1).click()

test.copy()
target.cell(0, 0).click()
test.paste()

for row in range(1):
for col in range(2):
assert target.cell(row, col).get_text() == source.cell(row, col).get_text()

assert test.get_log_errors() == []


def test_tbcp011_copy_multiline(test):
test.start_server(get_app())

source = test.table("table4")
target = test.table("table2")

source.cell(1, 0).click()
with test.hold(Keys.SHIFT):
source.cell(1, 1).click()

test.copy()
target.cell(1, 0).click()
test.paste()

for row in range(1, 2):
for col in range(2):
assert target.cell(row, col).get_text() == source.cell(row, col).get_text()

assert test.get_log_errors() == []