Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/components/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@labkey/components",
"version": "7.29.1",
"version": "7.29.2-fb-duplicateParents.0",
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
"sideEffects": false,
"files": [
Expand Down
4 changes: 4 additions & 0 deletions packages/components/releaseNotes/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Components, models, actions, and utility functions for LabKey applications and p
*Released*: 7 April 2026
- Update `FREEZER_ITEM_SAMPLE_MAPPER` to return undefined if not matched, for consistency with other mappers

### version 7.X
*Released*: X April 2026
- GitHub Issue 954: Add error for duplicate values for parent inputs

### version 7.28.1
*Released*: 3 April 2026
- GitHub Issue 613: Use "Status" instead of "SampleState" in error messaging.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,48 @@ describe('parsePastedLookup', () => {
]),
});
});

test('duplicate values', () => {
// single duplicate
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than one test with comments on the test cases, this could be better done as separate tests with the comments acting as the test name.

expect(parsePastedLookup(multiValueLookup, stringLookupValues, 'A,A')).toStrictEqual({
message: { message: 'Duplicate values not allowed: "A".' },
valueDescriptors: List([
{ display: 'A', raw: 'a' },
{ display: 'A', raw: 'a' },
]),
});

// multiple duplicates
expect(parsePastedLookup(multiValueLookup, stringLookupValues, 'A,b,A,b')).toStrictEqual({
message: { message: 'Duplicate values not allowed: "A", "b".' },
valueDescriptors: List([
{ display: 'A', raw: 'a' },
{ display: 'b', raw: 'B' },
{ display: 'A', raw: 'a' },
{ display: 'b', raw: 'B' },
]),
});

// duplicate with other valid values
expect(parsePastedLookup(multiValueLookup, stringLookupValues, 'A,C,A')).toStrictEqual({
message: { message: 'Duplicate values not allowed: "A".' },
valueDescriptors: List([
{ display: 'A', raw: 'a' },
{ display: 'C', raw: 'C' },
{ display: 'A', raw: 'a' },
]),
});

// unmatched takes precedence over duplicates
expect(parsePastedLookup(multiValueLookup, stringLookupValues, 'A,A,notfound')).toStrictEqual({
message: { message: 'Could not find "notfound"' },
valueDescriptors: List([
{ display: 'A', raw: 'a' },
{ display: 'A', raw: 'a' },
{ display: 'notfound', raw: 'notfound' },
]),
});
});
});

describe('insertPastedData', () => {
Expand Down
12 changes: 12 additions & 0 deletions packages/components/src/internal/components/editable/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1118,6 +1118,7 @@ export function parsePastedLookup(
let message: CellMessage;
let values: ValueDescriptor[];
const unmatched: string[] = [];
const dupValues = new Set<string>();

// Parse pasted strings to split properly around quoted values.
// Remove the quotes for storing the actual values in the grid.
Expand All @@ -1129,10 +1130,15 @@ export function parsePastedLookup(
unmatched.push(vt);
values = [{ display: vt, raw: vt }];
} else {
const foundValues = new Set<string>();
values = parsedValues.flatMap(v => {
const vt = v.trim();
if (!vt) return [];

if (foundValues.has(vt)) {
dupValues.add(vt);
}
foundValues.add(vt);
const vl = vt.toLowerCase();
const vd = descriptors.find(d => d.display && d.display.toString().toLowerCase() === vl);
if (vd) return [vd];
Expand All @@ -1148,6 +1154,12 @@ export function parsePastedLookup(
.map(u => '"' + u + '"')
.join(', ');
message = { message: lookupValidationErrorMessage(valueStr, true) };
} else if (dupValues.size > 0) {
const valueStr = Array.from(dupValues)
.slice(0, 4)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the slicing is to keep the message size small and see that it's done in the previous block too, but it seems somewhat magical.

.map(u => '"' + u + '"')
.join(', ');
message = { message: `Duplicate values not allowed: ${valueStr}.` };
}

return { message, valueDescriptors: List(values) };
Expand Down
Loading