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
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export default class TableClipboardHelper {
includeHeaders: boolean
): {data: Data; columns: Columns} | void {
const text = Clipboard.get(ev);
console.log(text);
amy-morrill marked this conversation as resolved.
Show resolved Hide resolved
Logger.trace('TableClipboard -- get clipboard data: ', text);

if (!text) {
Expand All @@ -98,7 +99,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 +112,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');
});
});
88 changes: 87 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 @@ -171,8 +185,14 @@ def test_tbcp004_copy_9_and_10(test):
ActionChains(test.driver).send_keys(Keys.DOWN).perform()

test.copy()
# Uncomment the following two lines if using a Mac computer
# with test.hold(Keys.COMMAND):
# ActionChains(test.driver).send_keys("c").perform()
target.cell(0, 0).click()
test.paste()
# Uncomment the following two lines if using a Mac computer
# with test.hold(Keys.COMMAND):
# ActionChains(test.driver).send_keys("v").perform()

for row in range(2):
for col in range(1):
Expand Down Expand Up @@ -300,8 +320,14 @@ def test_tbcp009_copy_9_and_10_click(test):
source.cell(10, 0).click()

test.copy()
# Uncomment the following two lines if using a Mac computer
# with test.hold(Keys.COMMAND):
# ActionChains(test.driver).send_keys("c").perform()
target.cell(0, 0).click()
test.paste()
# Uncomment the following two lines if using a Mac computer
# with test.hold(Keys.COMMAND):
# ActionChains(test.driver).send_keys("v").perform()

for row in range(2):
for col in range(1):
Expand All @@ -322,15 +348,75 @@ 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()
# Uncomment the following two lines if using a Mac computer
# with test.hold(Keys.COMMAND):
# ActionChains(test.driver).send_keys("c").perform()
amy-morrill marked this conversation as resolved.
Show resolved Hide resolved

# assert the target cell value is different before paste
target.cell(1, 1).click()
assert target.cell(1, 1).get_text() != source.cell(2, 2).get_text()

# assert the target cell value has changed to the pasted value
test.paste()
# Uncomment the following two lines if using a Mac computer
# with test.hold(Keys.COMMAND):
# ActionChains(test.driver).send_keys("v").perform()
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()
# Uncomment the following two lines if using a Mac computer
# with test.hold(Keys.COMMAND):
# ActionChains(test.driver).send_keys("c").perform()
target.cell(0, 0).click()
test.paste()
# Uncomment the following two lines if using a Mac computer
# with test.hold(Keys.COMMAND):
# ActionChains(test.driver).send_keys("v").perform()

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()
# Uncomment the following two lines if using a Mac computer
# with test.hold(Keys.COMMAND):
# ActionChains(test.driver).send_keys("c").perform()
target.cell(1, 0).click()
test.paste()
# Uncomment the following two lines if using a Mac computer
# with test.hold(Keys.COMMAND):
# ActionChains(test.driver).send_keys("v").perform()

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() == []