Skip to content

Commit 28e40cd

Browse files
committed
Rewrite parseCsvString
1 parent 16c901b commit 28e40cd

9 files changed

Lines changed: 504 additions & 30 deletions

File tree

packages/components/package-lock.json

Lines changed: 2 additions & 2 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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@labkey/components",
3-
"version": "7.23.2",
3+
"version": "7.23.3-fb-parseComma.0",
44
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
55
"sideEffects": false,
66
"files": [

packages/components/src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,11 @@ import {
6666
isNonNegativeFloat,
6767
isNonNegativeInteger,
6868
isSetEqual,
69+
joinMultiValueForExport,
6970
makeCommaSeparatedString,
70-
parseCsvString,
7171
parseScientificInt,
7272
quoteValueWithDelimiters,
73+
splitMultiValueForImport,
7374
setIsTestEnv,
7475
uncapitalizeFirstChar,
7576
valueIsEmpty,
@@ -1503,6 +1504,7 @@ export {
15031504
JavaDocsLink,
15041505
JobOperation,
15051506
joinDateTimeFormat,
1507+
joinMultiValueForExport,
15061508
Key,
15071509
LabelColorRenderer,
15081510
LabelHelpTip,
@@ -1575,7 +1577,6 @@ export {
15751577
ParentEntityRequiredColumns,
15761578
ParentImportAliasRenderer,
15771579
parseCellKey,
1578-
parseCsvString,
15791580
parseDate,
15801581
parseEntityParentKey,
15811582
parseScientificInt,
@@ -1698,6 +1699,7 @@ export {
16981699
spliceURL,
16991700
SplitButton,
17001701
splitDateTimeFormat,
1702+
splitMultiValueForImport,
17011703
STORAGE_UNIQUE_ID_CONCEPT_URI,
17021704
StorageAmountInput,
17031705
StorageStatusRenderer,

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

Lines changed: 231 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { genCellKey } from './utils';
3434
import sampleSetQueryInfoJSON from '../../../test/data/sampleSetAllFieldTypes-getQueryDetails.json';
3535
import { MockEditableGridLoader } from './utils.test';
3636
import { MULTI_CHOICE_TYPE } from '../domainproperties/PropDescType';
37+
import { joinMultiValueForExport } from '../../util/utils';
3738

3839
describe('column mutation actions', () => {
3940
const queryInfo = QueryInfo.fromJsonForTests(sampleSet2QueryInfo);
@@ -859,7 +860,7 @@ describe('insertPastedData', () => {
859860
fieldKey: mvtc,
860861
jsonType: 'ARRAY',
861862
rangeURI: MULTI_CHOICE_TYPE.rangeURI,
862-
validValues: ['a', 'ab', 'cc', 'cD', 'A,B', 'de'],
863+
validValues: ['a', 'ab', 'cc', 'cD', 'A,B', 'de', 'A', 'B', 'C', 'A,B,C', '"A"', '"A",B', '"A,B,C"'],
863864
}),
864865
},
865866
});
@@ -1175,6 +1176,235 @@ describe('insertPastedData', () => {
11751176
expect(cellMessages.get(genCellKey(mvtc, 1))).toBeUndefined();
11761177
expect(cellMessages.get(genCellKey(mvtc, 2))).toEqual({ message: 'Could not find "bad"' });
11771178
});
1179+
1180+
test('pasting string values with special characters, fromDragFill false', async () => {
1181+
const em = baseEditorModel.applyChanges({
1182+
selectionCells: [genCellKey(fkOne, 0), genCellKey(fkOne, 1), genCellKey(fkOne, 2)],
1183+
selectedColIdx: 0,
1184+
selectedRowIdx: 2,
1185+
});
1186+
const changes = await validateAndInsertPastedData(
1187+
em,
1188+
'hello world\n"hello, world"\n"say ""hello"""',
1189+
undefined,
1190+
true,
1191+
true,
1192+
undefined,
1193+
true
1194+
);
1195+
const cellValues = changes.cellValues;
1196+
// Space is preserved as-is
1197+
expect(cellValues.get(genCellKey(fkOne, 0))).toEqual(List([{ display: 'hello world', raw: 'hello world' }]));
1198+
// Quoted comma: without fromDragFill, CSV quoting is NOT stripped
1199+
expect(cellValues.get(genCellKey(fkOne, 1))).toEqual(
1200+
List([{ display: '"hello, world"', raw: '"hello, world"' }])
1201+
);
1202+
// Escaped double quotes: without fromDragFill, CSV escaping is NOT processed
1203+
expect(cellValues.get(genCellKey(fkOne, 2))).toEqual(
1204+
List([{ display: '"say ""hello"""', raw: '"say ""hello"""' }])
1205+
);
1206+
});
1207+
1208+
test('drag fill string values with special characters, fromDragFill true', async () => {
1209+
const em = baseEditorModel.applyChanges({
1210+
selectionCells: [
1211+
genCellKey(fkOne, 0),
1212+
genCellKey(fkOne, 1),
1213+
genCellKey(fkOne, 2),
1214+
genCellKey(fkOne, 3),
1215+
genCellKey(fkOne, 4),
1216+
genCellKey(fkOne, 5),
1217+
],
1218+
selectedColIdx: 0,
1219+
selectedRowIdx: 5,
1220+
});
1221+
const changes = await validateAndInsertPastedData(
1222+
em,
1223+
'hello world\n"hello, world"\n"say ""hello"""',
1224+
undefined,
1225+
true,
1226+
true,
1227+
undefined,
1228+
false,
1229+
[[genCellKey(fkOne, 3), genCellKey(fkOne, 4), genCellKey(fkOne, 5)]],
1230+
true
1231+
);
1232+
const cellValues = changes.cellValues;
1233+
// Original values unchanged
1234+
expect(cellValues.get(genCellKey(fkOne, 0))).toEqual(List([{ display: 'qwer', raw: 'qwer' }]));
1235+
expect(cellValues.get(genCellKey(fkOne, 1))).toEqual(List([{ display: 'asdf', raw: 'asdf' }]));
1236+
expect(cellValues.get(genCellKey(fkOne, 2))).toEqual(List([{ display: 'zxcv', raw: 'zxcv' }]));
1237+
// Space: no CSV quoting to strip
1238+
expect(cellValues.get(genCellKey(fkOne, 3))).toEqual(List([{ display: 'hello world', raw: 'hello world' }]));
1239+
// Quoted comma: fromDragFill strips CSV quoting, comma preserved in value
1240+
expect(cellValues.get(genCellKey(fkOne, 4))).toEqual(List([{ display: 'hello, world', raw: 'hello, world' }]));
1241+
// Escaped double quotes: fromDragFill strips CSV quoting and unescapes ""
1242+
expect(cellValues.get(genCellKey(fkOne, 5))).toEqual(List([{ display: 'say "hello"', raw: 'say "hello"' }]));
1243+
});
1244+
1245+
test('pasting exactly A,B into mvtc matches single valid value', async () => {
1246+
const em = baseEditorModel.applyChanges({
1247+
selectionCells: [genCellKey(mvtc, 0)],
1248+
selectedColIdx: 3,
1249+
selectedRowIdx: 0,
1250+
});
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)
1253+
expect(changes.cellValues.get(genCellKey(mvtc, 0))).toEqual(List([{ display: 'A,B', raw: 'A,B' }]));
1254+
expect(changes.cellMessages.get(genCellKey(mvtc, 0))).toBeUndefined();
1255+
});
1256+
1257+
test('pasting exactly A,B,C without quote into mvtc matches single valid value', async () => {
1258+
const em = baseEditorModel.applyChanges({
1259+
selectionCells: [genCellKey(mvtc, 0)],
1260+
selectedColIdx: 3,
1261+
selectedRowIdx: 0,
1262+
});
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)
1265+
expect(changes.cellValues.get(genCellKey(mvtc, 0))).toEqual(List([{ display: 'A,B,C', raw: 'A,B,C' }]));
1266+
expect(changes.cellMessages.get(genCellKey(mvtc, 0))).toBeUndefined();
1267+
});
1268+
1269+
test('pasting A, B with space into mvtc parses as two values', async () => {
1270+
const em = baseEditorModel.applyChanges({
1271+
selectionCells: [genCellKey(mvtc, 0)],
1272+
selectedColIdx: 3,
1273+
selectedRowIdx: 0,
1274+
});
1275+
const changes = await validateAndInsertPastedData(em, 'A, B', undefined, true, true, undefined, true);
1276+
// 'A, B' does not match any validValue, so parsed as CSV → ' B' and 'A' (sorted with leading space)
1277+
expect(changes.cellValues.get(genCellKey(mvtc, 0))).toEqual(
1278+
List([
1279+
{ display: 'A', raw: 'A' },
1280+
{ display: 'B', raw: 'B' }
1281+
])
1282+
);
1283+
expect(changes.cellMessages.get(genCellKey(mvtc, 0))).toBeUndefined();
1284+
});
1285+
1286+
test('pasting quoted "A,B" into mvtc treats as single value', async () => {
1287+
const em = baseEditorModel.applyChanges({
1288+
selectionCells: [genCellKey(mvtc, 0)],
1289+
selectedColIdx: 3,
1290+
selectedRowIdx: 0,
1291+
});
1292+
const changes = await validateAndInsertPastedData(em, '"A,B"', undefined, true, true, undefined, true);
1293+
// Quoted '"A,B"' is CSV-parsed to 'A,B' which is a valid value
1294+
expect(changes.cellValues.get(genCellKey(mvtc, 0))).toEqual(List([{ display: 'A,B', raw: 'A,B' }]));
1295+
expect(changes.cellMessages.get(genCellKey(mvtc, 0))).toBeUndefined();
1296+
});
1297+
1298+
test('pasting quoted "A, B" into mvtc, invalid', async () => {
1299+
const em = baseEditorModel.applyChanges({
1300+
selectionCells: [genCellKey(mvtc, 0)],
1301+
selectedColIdx: 3,
1302+
selectedRowIdx: 0,
1303+
});
1304+
const changes = await validateAndInsertPastedData(em, '"A, B"', undefined, true, true, undefined, true);
1305+
// Quoted '"A,B"' is CSV-parsed to 'A,B' which is a valid value
1306+
expect(changes.cellValues.get(genCellKey(mvtc, 0))).toEqual(List([{ display: 'A, B', raw: 'A, B' }]));
1307+
expect(changes.cellMessages.get(genCellKey(mvtc, 0))).toEqual({
1308+
message: 'Could not find "A, B". Please make sure values that contain commas are properly quoted.',
1309+
});
1310+
});
1311+
1312+
test('pasting mvtc values combined with other valid values', async () => {
1313+
const em = baseEditorModel.applyChanges({
1314+
selectionCells: [genCellKey(mvtc, 0), genCellKey(mvtc, 1), genCellKey(mvtc, 2)],
1315+
selectedColIdx: 3,
1316+
selectedRowIdx: 2,
1317+
});
1318+
const changes = await validateAndInsertPastedData(
1319+
em,
1320+
'A,B\n"A,B",cc\nA,B,cc',
1321+
undefined,
1322+
true,
1323+
true,
1324+
undefined,
1325+
true
1326+
);
1327+
const cellValues = changes.cellValues;
1328+
// Row 0: 'A,B' exactly matches single validValue
1329+
expect(cellValues.get(genCellKey(mvtc, 0))).toEqual(List([{ display: 'A,B', raw: 'A,B' }]));
1330+
expect(changes.cellMessages.get(genCellKey(mvtc, 0))).toBeUndefined();
1331+
// Row 1: '"A,B",cc' → CSV parsed to ['A,B', 'cc'], both valid
1332+
expect(cellValues.get(genCellKey(mvtc, 1))).toEqual(
1333+
List([
1334+
{ display: 'A,B', raw: 'A,B' },
1335+
{ display: 'cc', raw: 'cc' },
1336+
])
1337+
);
1338+
expect(changes.cellMessages.get(genCellKey(mvtc, 1))).toBeUndefined();
1339+
// Row 2: 'A,B,cc' without quotes → CSV parsed to ['A', 'B', 'cc'], all valid
1340+
expect(cellValues.get(genCellKey(mvtc, 2))).toEqual(
1341+
List([
1342+
{ display: 'A', raw: 'A' },
1343+
{ display: 'B', raw: 'B' },
1344+
{ display: 'cc', raw: 'cc' },
1345+
])
1346+
);
1347+
expect(changes.cellMessages.get(genCellKey(mvtc, 2))).toBeUndefined();
1348+
});
1349+
1350+
test('pasting mvtc values combined with invalid values', async () => {
1351+
const em = baseEditorModel.applyChanges({
1352+
selectionCells: [genCellKey(mvtc, 0), genCellKey(mvtc, 1)],
1353+
selectedColIdx: 3,
1354+
selectedRowIdx: 1,
1355+
});
1356+
const changes = await validateAndInsertPastedData(
1357+
em,
1358+
'A, B, bad\n"A,B",bad',
1359+
undefined,
1360+
true,
1361+
true,
1362+
undefined,
1363+
true
1364+
);
1365+
const cellValues = changes.cellValues;
1366+
const cellMessages = changes.cellMessages;
1367+
// Row 0: 'A,B,bad' → CSV parsed to ['A', 'B', 'bad'], 'bad' invalid
1368+
expect(cellValues.get(genCellKey(mvtc, 0))).toEqual(
1369+
List([
1370+
{ display: 'A', raw: 'A' },
1371+
{ display: 'B', raw: 'B' },
1372+
{ display: 'bad', raw: 'bad' },
1373+
])
1374+
);
1375+
expect(cellMessages.get(genCellKey(mvtc, 0))).toEqual({ message: 'Could not find "bad"' });
1376+
// Row 1: '"A,B",bad' → CSV parsed to ['A,B', 'bad'], 'bad' invalid
1377+
expect(cellValues.get(genCellKey(mvtc, 1))).toEqual(
1378+
List([
1379+
{ display: 'A,B', raw: 'A,B' },
1380+
{ display: 'bad', raw: 'bad' },
1381+
])
1382+
);
1383+
expect(cellMessages.get(genCellKey(mvtc, 1))).toEqual({ message: 'Could not find "bad"' });
1384+
});
1385+
1386+
test.each([
1387+
{ values: ['A', 'B', 'C'], desc: 'simple values' },
1388+
{ values: ['"A",B'], desc: 'single value with quotes and comma' },
1389+
{ values: ['"A,B,C"'], desc: 'single value with quotes wrapping commas' },
1390+
{ values: ['"A",B', 'B'], desc: 'tricky + simple' },
1391+
{ values: ['A', '"A"', 'B'], desc: 'quotes among simple' },
1392+
{ values: ['A', 'A,B,C', '"A,B,C"'], desc: 'plain + comma + quote-containing' },
1393+
{ values: ['"A"', '"A",B', '"A,B,C"'], desc: 'all contain quotes' },
1394+
])('pasting multi values round-trip: $desc', async ({ values }) => {
1395+
const exported = joinMultiValueForExport(values);
1396+
const em = baseEditorModel.applyChanges({
1397+
selectionCells: [genCellKey(mvtc, 0)],
1398+
selectedColIdx: 3,
1399+
selectedRowIdx: 0,
1400+
});
1401+
const changes = await validateAndInsertPastedData(em, exported, undefined, true, true, undefined, true);
1402+
const cellValues = changes.cellValues.get(genCellKey(mvtc, 0));
1403+
const sortedExpected = [...values].sort();
1404+
const actualValues = cellValues.toArray().map(v => v.raw);
1405+
expect(actualValues).toStrictEqual(sortedExpected);
1406+
expect(changes.cellMessages.get(genCellKey(mvtc, 0))).toBeUndefined();
1407+
});
11781408
});
11791409

11801410
describe('loadEditorModelData', () => {

0 commit comments

Comments
 (0)