Skip to content

Commit ee488eb

Browse files
committed
Use papaParse, support multi-line paste
1 parent 28e40cd commit ee488eb

7 files changed

Lines changed: 204 additions & 125 deletions

File tree

packages/components/package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/components/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@
7575
"react-select": "~5.10.2",
7676
"react-treebeard": "~3.2.4",
7777
"vis-data": "~8.0.3",
78-
"vis-network": "~10.0.2"
78+
"vis-network": "~10.0.2",
79+
"papaparse": "5.5.3"
7980
},
8081
"devDependencies": {
8182
"@labkey/build": "9.0.0",

packages/components/src/internal/components/editable/actions.test.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,31 @@ describe('insertPastedData', () => {
11371137
);
11381138
});
11391139

1140+
test('pasting multi-line value', async () => {
1141+
const em = baseEditorModel.applyChanges({
1142+
selectionCells: [genCellKey(fkOne, 0)],
1143+
selectedColIdx: 0,
1144+
selectedRowIdx: 2,
1145+
});
1146+
const changes = await validateAndInsertPastedData(
1147+
em,
1148+
'"line1\nline2"',
1149+
undefined,
1150+
true,
1151+
true,
1152+
undefined,
1153+
true
1154+
);
1155+
const cellValues = changes.cellValues;
1156+
expect(cellValues.get(genCellKey(fkOne, 2))).toEqual(
1157+
List([
1158+
{ display: 'line1\nline2', raw: 'line1\nline2' }
1159+
]));
1160+
1161+
const cellMessages = changes.cellMessages;
1162+
expect(cellMessages.get(genCellKey(fkOne, 2))).toBeUndefined();
1163+
});
1164+
11401165
test('pasting multi values', async () => {
11411166
const em = baseEditorModel.applyChanges({
11421167
selectionCells: [genCellKey(mvtc, 0)],
@@ -1248,24 +1273,33 @@ describe('insertPastedData', () => {
12481273
selectedColIdx: 3,
12491274
selectedRowIdx: 0,
12501275
});
1251-
const changes = await validateAndInsertPastedData(em, 'A,B', undefined, true, true, undefined, true);
1252-
// 'A,B' exactly matches a validValue, treated as a single value (not split on comma)
1276+
const changes = await validateAndInsertPastedData(em, '"A,B"', undefined, true, true, undefined, true);
12531277
expect(changes.cellValues.get(genCellKey(mvtc, 0))).toEqual(List([{ display: 'A,B', raw: 'A,B' }]));
12541278
expect(changes.cellMessages.get(genCellKey(mvtc, 0))).toBeUndefined();
12551279
});
12561280

1257-
test('pasting exactly A,B,C without quote into mvtc matches single valid value', async () => {
1281+
test('pasting exactly A,B,C into mvtc matches single valid value', async () => {
12581282
const em = baseEditorModel.applyChanges({
12591283
selectionCells: [genCellKey(mvtc, 0)],
12601284
selectedColIdx: 3,
12611285
selectedRowIdx: 0,
12621286
});
1263-
const changes = await validateAndInsertPastedData(em, 'A,B,C', undefined, true, true, undefined, true);
1264-
// 'A,B' exactly matches a validValue, treated as a single value (not split on comma)
1287+
const changes = await validateAndInsertPastedData(em, '"A,B,C"', undefined, true, true, undefined, true);
12651288
expect(changes.cellValues.get(genCellKey(mvtc, 0))).toEqual(List([{ display: 'A,B,C', raw: 'A,B,C' }]));
12661289
expect(changes.cellMessages.get(genCellKey(mvtc, 0))).toBeUndefined();
12671290
});
12681291

1292+
test('pasting escaped "A,B,C" into mvtc matches single valid value', async () => {
1293+
const em = baseEditorModel.applyChanges({
1294+
selectionCells: [genCellKey(mvtc, 0)],
1295+
selectedColIdx: 3,
1296+
selectedRowIdx: 0,
1297+
});
1298+
const changes = await validateAndInsertPastedData(em, '"""A,B,C"""', undefined, true, true, undefined, true);
1299+
expect(changes.cellValues.get(genCellKey(mvtc, 0))).toEqual(List([{ display: '"A,B,C"', raw: '"A,B,C"' }]));
1300+
expect(changes.cellMessages.get(genCellKey(mvtc, 0))).toBeUndefined();
1301+
});
1302+
12691303
test('pasting A, B with space into mvtc parses as two values', async () => {
12701304
const em = baseEditorModel.applyChanges({
12711305
selectionCells: [genCellKey(mvtc, 0)],
@@ -1317,7 +1351,7 @@ describe('insertPastedData', () => {
13171351
});
13181352
const changes = await validateAndInsertPastedData(
13191353
em,
1320-
'A,B\n"A,B",cc\nA,B,cc',
1354+
'"A,B"\n"A,B",cc\nA,B,cc',
13211355
undefined,
13221356
true,
13231357
true,

packages/components/src/internal/components/editable/actions.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Filter, getServerContext, QueryKey, Utils } from '@labkey/api';
22
import { fromJS, List, Map, OrderedMap } from 'immutable';
33
import { addDays, subDays } from 'date-fns';
4+
import Papa from 'papaparse';
45

56
import { ExtendedMap } from '../../../public/ExtendedMap';
67
import { QueryColumn, QueryLookup } from '../../../public/QueryColumn';
@@ -11,6 +12,7 @@ import {
1112
caseInsensitive,
1213
isFloat,
1314
isInteger,
15+
isSimpleQuotedMultiLine,
1416
joinMultiValueForExport,
1517
parseScientificInt,
1618
quoteValueWithDelimiters,
@@ -1303,7 +1305,8 @@ export function dragFillEvent(
13031305
forUpdate,
13041306
targetContainerPath,
13051307
false,
1306-
selectionToFill
1308+
selectionToFill,
1309+
true
13071310
);
13081311
}
13091312

@@ -1418,20 +1421,40 @@ function parsePaste(value: string): ParsePastePayload {
14181421
let numCols = 0;
14191422
let data = List<List<string>>();
14201423

1421-
if (value === undefined || value === null || typeof value !== 'string') {
1424+
if (value === undefined || value == null || typeof value !== 'string') {
14221425
return { data, numCols, numRows: 0 };
14231426
}
14241427

14251428
// remove trailing newline from pasted data to avoid creating an empty row of cells
14261429
if (value.endsWith('\n')) value = value.substring(0, value.length - 1);
14271430

1428-
value.split('\n').forEach(rv => {
1429-
const columns = List(rv.split('\t'));
1430-
if (numCols < columns.size) {
1431-
numCols = columns.size;
1431+
if (value.indexOf('"') === -1 || isSimpleQuotedMultiLine(value)) {
1432+
// parse tsv ONLY if the copied string doesn't contain "
1433+
// quoteChar will be stripped during TSV parsing, resulting in incorrect parsed data
1434+
const rows = Papa.parse(value, { delimiter: '\t' }).data;
1435+
if (!rows || rows.length === 0) {
1436+
return { data, numCols, numRows: 0 };
14321437
}
1433-
data = data.push(columns);
1434-
});
1438+
1439+
rows.forEach(row => {
1440+
const columns : List<string> = List(row)
1441+
if (numCols < columns.size) {
1442+
numCols = columns.size;
1443+
}
1444+
data = data.push(columns);
1445+
});
1446+
}
1447+
else {
1448+
// fall back to line by line processing without parsing, to preserver quotes
1449+
value.split('\n').forEach(rv => {
1450+
const columns = List(rv.split('\t'));
1451+
if (numCols < columns.size) {
1452+
numCols = columns.size;
1453+
}
1454+
data = data.push(columns);
1455+
});
1456+
}
1457+
14351458

14361459
// Normalize the number columns in each row in case a user pasted rows with different numbers of columns in them
14371460
data = data
@@ -1530,7 +1553,7 @@ async function insertPastedData(
15301553
const unmatched: string[] = [];
15311554
const values: ValueDescriptor[] = [];
15321555

1533-
const parsedValues = splitMultiValueForImport(val).sort(caseSensitiveNaturalSort);
1556+
const parsedValues = splitMultiValueForImport(val, ',', true, true).sort(caseSensitiveNaturalSort);
15341557
const foundValues = new Set<string>();
15351558

15361559
// GitHub Issue 942: Add error for duplicate values

packages/components/src/internal/events.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ export const isSelectAll = (event: KeyboardEvent<any>): boolean => isMetaKeyEven
4747

4848
export function setCopyValue(event: any, value: string): boolean {
4949
if (isEvent(event)) {
50-
console.log(value);
5150
(event.clipboardData || window['clipboardData']).setData('text/plain', value);
5251
return true;
5352
}

0 commit comments

Comments
 (0)