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.21.0",
"version": "7.21.1-fb-lookupComma.3",
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
"sideEffects": false,
"files": [
Expand Down
5 changes: 5 additions & 0 deletions packages/components/releaseNotes/components.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# @labkey/components
Components, models, actions, and utility functions for LabKey applications and pages

### version 7.X
*Released*: X February 2026
- GitHub Issue 829: Sample type with lookup to list with text primary key where the value contains a comma doesn't map to lookup
- Only do `quoteValueWithDelimiters` on values if the lookup column support multiple values

### version 7.21.0
*Released*: 26 February 2026
- Package updates
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -489,9 +489,10 @@ async function convertRowToEditorModelData(
const valueDescriptors: ValueDescriptor[] = [];

if (data && col?.isPublicLookup()) {
const multiple = col.isJunctionLookup() || col.isExpInput();
// value had better be the rowId here, but it may be several in a comma-separated list.
// If it's the display value, which happens to be a number, much confusion will arise.
const values = data.toString().split(',');
const values = multiple ? data.toString().split(',') : [data.toString()];

for (const val of values) {
const messageAndValue = await getLookupDisplayValue(col, parseIntIfNumber(val), containerPath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1170,19 +1170,70 @@ describe('EditorModel', () => {
raw: 123,
},
]),
[genCellKey(sampleColFk, 2)]: List<ValueDescriptor>([
{
display: 'Sample,321',
raw: 321,
},
]),
});
const editorModel = modifyEm({
cellValues: basicEditorModel.cellValues.merge(cellValues),
columnMap: basicEditorModel.columnMap.set(sampleColFk, sampleColumn),
orderedColumns: basicEditorModel.orderedColumns.push(sampleColFk),
rowCount: 2,
rowCount: 3,
});
expect(editorModel.getRowValue(1, false).get('SampleID')).toBe(123);
expect(editorModel.getRowValue(1, false).get('SampleID/Name')).toBe(undefined);
expect(editorModel.getRowValue(1, false, () => false).get('SampleID')).toBe(123);
expect(editorModel.getRowValue(1, false, () => false).get('SampleID/Name')).toBe(undefined);
expect(editorModel.getRowValue(1, false, () => true).get('SampleID')).toBe(123);
expect(editorModel.getRowValue(1, false, () => true).get('SampleID/Name')).toBe('Sample-123');
expect(editorModel.getRowValue(2, false, () => true).get('SampleID/Name')).toBe('Sample,321');
});
test('include string list lookup display value', () => {
const lookColumn = new QueryColumn({
caption: 'List Look',
fieldKey: 'ListLook',
fieldKeyArray: ['ListLook'],
jsonType: 'string',
name: 'ListLook',
shownInInsertView: true,
shownInUpdateView: true,
required: true,
userEditable: true,
lookup: {
schemaName: 'lists',
queryName: 'Location',
displayColumn: 'Path',
keyColumn: 'Path',
},
});
const colFk = lookColumn.fieldKey.toLowerCase();
const cellValues = fromJS({
[genCellKey(colFk, 0)]: List<ValueDescriptor>([{ display: undefined, raw: undefined }]),
[genCellKey(colFk, 1)]: List<ValueDescriptor>([
{
display: 'Building 123',
raw: 'Building 123',
},
]),
[genCellKey(colFk, 2)]: List<ValueDescriptor>([
{
display: 'Building, 321',
raw: 'Building, 321',
},
]),
});
const editorModel = modifyEm({
cellValues: basicEditorModel.cellValues.merge(cellValues),
columnMap: basicEditorModel.columnMap.set(colFk, lookColumn),
orderedColumns: basicEditorModel.orderedColumns.push(colFk),
rowCount: 3,
});
expect(editorModel.getRowValue(0).get('ListLook')).toBe(undefined);
expect(editorModel.getRowValue(1).get('ListLook')).toBe('Building 123');
expect(editorModel.getRowValue(2).get('ListLook')).toBe('Building, 321');
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,10 +352,7 @@ export class EditorModel
.toArray();
row = row.set(col.name, valueArray);
} else if (col.lookup.displayColumn === col.lookup.keyColumn) {
row = row.set(
col.name,
values.size === 1 ? quoteValueWithDelimiters(values.first()?.display, ',') : undefined
);
row = row.set(col.name, values.size === 1 ? values.first()?.display : undefined);
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: the ? after first() seems unnecessary.

} else {
let val;
if (values.size === 1) val = values.first()?.raw;
Expand Down
5 changes: 3 additions & 2 deletions packages/components/src/internal/components/editable/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Filter, Utils } from '@labkey/api';
import { Filter, QueryKey, Utils } from '@labkey/api';

import { Operation, QueryColumn } from '../../../public/QueryColumn';

Expand Down Expand Up @@ -221,7 +221,8 @@ export function getLookupFilters(
}

if (lookupKeyValues) {
filters.push(Filter.create(lookup.keyColumn, lookupKeyValues, Filter.Types.IN));
// lookup.keyColumn is column name, needs to encode to handle cases when the column name contains special characters.
filters.push(Filter.create(QueryKey.encodePart(lookup.keyColumn), lookupKeyValues, Filter.Types.IN));
}

const operation = forUpdate ? Operation.update : Operation.insert;
Expand Down
14 changes: 10 additions & 4 deletions packages/components/src/internal/components/forms/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/
import { fromJS, Record as ImmutableRecord, List, Map, OrderedMap } from 'immutable';
import { Filter, Query } from '@labkey/api';
import { Filter, Query, QueryKey } from '@labkey/api';

import { QueryInfo } from '../../../public/QueryInfo';

Expand Down Expand Up @@ -151,7 +151,8 @@ function getSelectedOptions(model: QuerySelectModel, value: any): Map<string, an
return Map<string, any>();
}

const keyPath = [model.valueColumn, 'value'];
// model.valueColumn is fieldKey, not column name
const keyPath = [QueryKey.decodePart(model.valueColumn), 'value'];
const sources = model.allResults.merge(model.selectedItems);

// multi-value case
Expand Down Expand Up @@ -225,7 +226,8 @@ export function fetchSearchResults(model: QuerySelectModel, input: any): Promise
filterVal,
model.valueColumn,
model.delimiter,
addExactFilter ? displayColumn : undefined
addExactFilter ? displayColumn : undefined,
model.multiple
);
}

Expand Down Expand Up @@ -442,7 +444,11 @@ export async function initSelect(props: QuerySelectOwnProps): Promise<Partial<Qu
isInit: true,
queryInfo,
selectedItems: selectedItems
? fromJS(quoteValueColumnWithDelimiters(selectedItems, valueColumn, delimiter).models[selectedItems.key])
? fromJS(
quoteValueColumnWithDelimiters(selectedItems, valueColumn, delimiter, multiple).models[
selectedItems.key
]
)
: Map<string, any>(),
valueColumn,
};
Expand Down
53 changes: 37 additions & 16 deletions packages/components/src/internal/query/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,23 +138,26 @@ describe('api', () => {
});

describe('quoteValueColumnWithDelimiters', () => {
const results: ISelectRowsResult = {
key: 'test',
models: {
test: {
1: { Name: { value: 'one', url: 'http://one/test', randomProperty: 123 } },
2: { Name: { value: 'with, comma', url: 'http://with, comma/test' } },
4: { Name: { value: 'with "quotes", and comma' } },
3: { NoName: { value: 'nonesuch', url: 'http://with, comma/test' } },
5: { Name: { value: ', comma first', displayValue: ',', url: 'http://with, comma/test' } },
function getResults(): ISelectRowsResult {
return {
key: 'test',
models: {
test: {
1: { Name: { value: 'one', url: 'http://one/test', randomProperty: 123 } },
2: { Name: { value: 'with, comma', url: 'http://with, comma/test' } },
4: { Name: { value: 'with "quotes", and comma' } },
3: { NoName: { value: 'nonesuch', url: 'http://with, comma/test' } },
5: { Name: { value: ', comma first', displayValue: ',', url: 'http://with, comma/test' } },
},
},
},
orderedModels: List([1, 2, 3, 4, 5]),
queries: {},
rowCount: 5,
};
test('encode', () => {
expect(quoteValueColumnWithDelimiters(results, 'Name', ',')).toStrictEqual({
orderedModels: List([1, 2, 3, 4, 5]),
queries: {},
rowCount: 5,
};
}

test('encode (multiple=true by default)', () => {
expect(quoteValueColumnWithDelimiters(getResults(), 'Name', ',')).toStrictEqual({
key: 'test',
models: {
test: {
Expand All @@ -181,6 +184,24 @@ describe('api', () => {
rowCount: 5,
});
});

test('no encode when multiple=false', () => {
expect(quoteValueColumnWithDelimiters(getResults(), 'Name', ',', false)).toStrictEqual({
key: 'test',
models: {
test: {
1: { Name: { value: 'one', url: 'http://one/test', displayValue: 'one', randomProperty: 123 } },
2: { Name: { value: 'with, comma', url: 'http://with, comma/test', displayValue: 'with, comma' } },
4: { Name: { value: 'with "quotes", and comma', displayValue: 'with "quotes", and comma' } },
3: { NoName: { value: 'nonesuch', url: 'http://with, comma/test' } },
5: { Name: { value: ', comma first', displayValue: ',', url: 'http://with, comma/test' } },
},
},
orderedModels: List([1, 2, 3, 4, 5]),
queries: {},
rowCount: 5,
});
});
});

test('splitRowsByContainer', () => {
Expand Down
10 changes: 6 additions & 4 deletions packages/components/src/internal/query/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,15 +626,16 @@ export function handleSelectRowsResponse(response: Query.Response, queryInfo: Qu
export function quoteValueColumnWithDelimiters(
selectRowsResult: ISelectRowsResult,
valueColumn: string,
delimiter: string
delimiter: string,
multiple = true
): ISelectRowsResult {
const rowMap = selectRowsResult.models[selectRowsResult.key];

Object.values(rowMap).forEach(row => {
const cell = row[valueColumn];
if (Utils.isString(cell?.value)) {
cell.displayValue = cell.displayValue ?? cell.value;
cell.value = quoteValueWithDelimiters(cell.value, delimiter);
cell.value = multiple ? quoteValueWithDelimiters(cell.value, delimiter) : cell.value;
}
});

Expand All @@ -646,7 +647,8 @@ export function searchRows(
token: any,
valueColumn: string,
delimiter: string,
exactColumn?: string
exactColumn?: string,
multiple?: boolean
): Promise<ISelectRowsResult> {
return new Promise((resolve, reject) => {
let exactFilters, qFilters;
Expand Down Expand Up @@ -713,7 +715,7 @@ export function searchRows(
finalResults = queryResults;
}

resolve(quoteValueColumnWithDelimiters(finalResults, valueColumn, delimiter));
resolve(quoteValueColumnWithDelimiters(finalResults, valueColumn, delimiter, multiple));
})
.catch(reason => {
reject(reason);
Expand Down