Skip to content

Commit d2634cd

Browse files
authored
Merge changes from release26.3-SNAPSHOT through v7.21.1 (#1952)
### version 7.23.1 *Released*: 11 March 2026 - Merge from release26.3-SNAPSHOT to develop - includes changes from 7.21.1 #1946
1 parent 46bcea9 commit d2634cd

10 files changed

Lines changed: 124 additions & 35 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.0",
3+
"version": "7.23.1",
44
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
55
"sideEffects": false,
66
"files": [

packages/components/releaseNotes/components.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# @labkey/components
22
Components, models, actions, and utility functions for LabKey applications and pages
33

4+
### version 7.23.1
5+
*Released*: 11 March 2026
6+
- Merge from release26.3-SNAPSHOT to develop
7+
- includes changes from 7.21.1 #1946
8+
49
### version 7.23.0
510
*Released*: 10 March 2026
611
- Update `tsconfig.json` to specify `delcarationMap`
@@ -18,6 +23,11 @@ Components, models, actions, and utility functions for LabKey applications and p
1823
- Add UnidentifiedPill
1924
- Add EMPTY_SEQUENCE_WARNING constant
2025

26+
### version 7.21.1
27+
*Released*: 4 March 2026
28+
- GitHub Issue 829: Sample type with lookup to list with text primary key where the value contains a comma doesn't map to lookup
29+
- Only do `quoteValueWithDelimiters` on values if the lookup column support multiple values
30+
2131
### version 7.21.0
2232
*Released*: 26 February 2026
2333
- Package updates

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,9 +489,10 @@ async function convertRowToEditorModelData(
489489
const valueDescriptors: ValueDescriptor[] = [];
490490

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

496497
for (const val of values) {
497498
const messageAndValue = await getLookupDisplayValue(col, parseIntIfNumber(val), containerPath);

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

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1170,19 +1170,70 @@ describe('EditorModel', () => {
11701170
raw: 123,
11711171
},
11721172
]),
1173+
[genCellKey(sampleColFk, 2)]: List<ValueDescriptor>([
1174+
{
1175+
display: 'Sample,321',
1176+
raw: 321,
1177+
},
1178+
]),
11731179
});
11741180
const editorModel = modifyEm({
11751181
cellValues: basicEditorModel.cellValues.merge(cellValues),
11761182
columnMap: basicEditorModel.columnMap.set(sampleColFk, sampleColumn),
11771183
orderedColumns: basicEditorModel.orderedColumns.push(sampleColFk),
1178-
rowCount: 2,
1184+
rowCount: 3,
11791185
});
11801186
expect(editorModel.getRowValue(1, false).get('SampleID')).toBe(123);
11811187
expect(editorModel.getRowValue(1, false).get('SampleID/Name')).toBe(undefined);
11821188
expect(editorModel.getRowValue(1, false, () => false).get('SampleID')).toBe(123);
11831189
expect(editorModel.getRowValue(1, false, () => false).get('SampleID/Name')).toBe(undefined);
11841190
expect(editorModel.getRowValue(1, false, () => true).get('SampleID')).toBe(123);
11851191
expect(editorModel.getRowValue(1, false, () => true).get('SampleID/Name')).toBe('Sample-123');
1192+
expect(editorModel.getRowValue(2, false, () => true).get('SampleID/Name')).toBe('Sample,321');
1193+
});
1194+
test('include string list lookup display value', () => {
1195+
const lookColumn = new QueryColumn({
1196+
caption: 'List Look',
1197+
fieldKey: 'ListLook',
1198+
fieldKeyArray: ['ListLook'],
1199+
jsonType: 'string',
1200+
name: 'ListLook',
1201+
shownInInsertView: true,
1202+
shownInUpdateView: true,
1203+
required: true,
1204+
userEditable: true,
1205+
lookup: {
1206+
schemaName: 'lists',
1207+
queryName: 'Location',
1208+
displayColumn: 'Path',
1209+
keyColumn: 'Path',
1210+
},
1211+
});
1212+
const colFk = lookColumn.fieldKey.toLowerCase();
1213+
const cellValues = fromJS({
1214+
[genCellKey(colFk, 0)]: List<ValueDescriptor>([{ display: undefined, raw: undefined }]),
1215+
[genCellKey(colFk, 1)]: List<ValueDescriptor>([
1216+
{
1217+
display: 'Building 123',
1218+
raw: 'Building 123',
1219+
},
1220+
]),
1221+
[genCellKey(colFk, 2)]: List<ValueDescriptor>([
1222+
{
1223+
display: 'Building, 321',
1224+
raw: 'Building, 321',
1225+
},
1226+
]),
1227+
});
1228+
const editorModel = modifyEm({
1229+
cellValues: basicEditorModel.cellValues.merge(cellValues),
1230+
columnMap: basicEditorModel.columnMap.set(colFk, lookColumn),
1231+
orderedColumns: basicEditorModel.orderedColumns.push(colFk),
1232+
rowCount: 3,
1233+
});
1234+
expect(editorModel.getRowValue(0).get('ListLook')).toBe(undefined);
1235+
expect(editorModel.getRowValue(1).get('ListLook')).toBe('Building 123');
1236+
expect(editorModel.getRowValue(2).get('ListLook')).toBe('Building, 321');
11861237
});
11871238
});
11881239

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -352,10 +352,7 @@ export class EditorModel
352352
.toArray();
353353
row = row.set(col.name, valueArray);
354354
} else if (col.lookup.displayColumn === col.lookup.keyColumn) {
355-
row = row.set(
356-
col.name,
357-
values.size === 1 ? quoteValueWithDelimiters(values.first()?.display, ',') : undefined
358-
);
355+
row = row.set(col.name, values.size === 1 ? values.first()?.display : undefined);
359356
} else {
360357
let val;
361358
if (values.size === 1) val = values.first()?.raw;

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Filter, Utils } from '@labkey/api';
1+
import { Filter, QueryKey, Utils } from '@labkey/api';
22

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

@@ -221,7 +221,8 @@ export function getLookupFilters(
221221
}
222222

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

227228
const operation = forUpdate ? Operation.update : Operation.insert;

packages/components/src/internal/components/forms/model.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616
import { fromJS, Record as ImmutableRecord, List, Map, OrderedMap } from 'immutable';
17-
import { Filter, Query } from '@labkey/api';
17+
import { Filter, Query, QueryKey } from '@labkey/api';
1818

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

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

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

157158
// multi-value case
@@ -225,7 +226,8 @@ export function fetchSearchResults(model: QuerySelectModel, input: any): Promise
225226
filterVal,
226227
model.valueColumn,
227228
model.delimiter,
228-
addExactFilter ? displayColumn : undefined
229+
addExactFilter ? displayColumn : undefined,
230+
model.multiple
229231
);
230232
}
231233

@@ -442,7 +444,11 @@ export async function initSelect(props: QuerySelectOwnProps): Promise<Partial<Qu
442444
isInit: true,
443445
queryInfo,
444446
selectedItems: selectedItems
445-
? fromJS(quoteValueColumnWithDelimiters(selectedItems, valueColumn, delimiter).models[selectedItems.key])
447+
? fromJS(
448+
quoteValueColumnWithDelimiters(selectedItems, valueColumn, delimiter, multiple).models[
449+
selectedItems.key
450+
]
451+
)
446452
: Map<string, any>(),
447453
valueColumn,
448454
};

packages/components/src/internal/query/api.test.ts

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -138,23 +138,26 @@ describe('api', () => {
138138
});
139139

140140
describe('quoteValueColumnWithDelimiters', () => {
141-
const results: ISelectRowsResult = {
142-
key: 'test',
143-
models: {
144-
test: {
145-
1: { Name: { value: 'one', url: 'http://one/test', randomProperty: 123 } },
146-
2: { Name: { value: 'with, comma', url: 'http://with, comma/test' } },
147-
4: { Name: { value: 'with "quotes", and comma' } },
148-
3: { NoName: { value: 'nonesuch', url: 'http://with, comma/test' } },
149-
5: { Name: { value: ', comma first', displayValue: ',', url: 'http://with, comma/test' } },
141+
function getResults(): ISelectRowsResult {
142+
return {
143+
key: 'test',
144+
models: {
145+
test: {
146+
1: { Name: { value: 'one', url: 'http://one/test', randomProperty: 123 } },
147+
2: { Name: { value: 'with, comma', url: 'http://with, comma/test' } },
148+
4: { Name: { value: 'with "quotes", and comma' } },
149+
3: { NoName: { value: 'nonesuch', url: 'http://with, comma/test' } },
150+
5: { Name: { value: ', comma first', displayValue: ',', url: 'http://with, comma/test' } },
151+
},
150152
},
151-
},
152-
orderedModels: List([1, 2, 3, 4, 5]),
153-
queries: {},
154-
rowCount: 5,
155-
};
156-
test('encode', () => {
157-
expect(quoteValueColumnWithDelimiters(results, 'Name', ',')).toStrictEqual({
153+
orderedModels: List([1, 2, 3, 4, 5]),
154+
queries: {},
155+
rowCount: 5,
156+
};
157+
}
158+
159+
test('encode (multiple=true by default)', () => {
160+
expect(quoteValueColumnWithDelimiters(getResults(), 'Name', ',')).toStrictEqual({
158161
key: 'test',
159162
models: {
160163
test: {
@@ -181,6 +184,24 @@ describe('api', () => {
181184
rowCount: 5,
182185
});
183186
});
187+
188+
test('no encode when multiple=false', () => {
189+
expect(quoteValueColumnWithDelimiters(getResults(), 'Name', ',', false)).toStrictEqual({
190+
key: 'test',
191+
models: {
192+
test: {
193+
1: { Name: { value: 'one', url: 'http://one/test', displayValue: 'one', randomProperty: 123 } },
194+
2: { Name: { value: 'with, comma', url: 'http://with, comma/test', displayValue: 'with, comma' } },
195+
4: { Name: { value: 'with "quotes", and comma', displayValue: 'with "quotes", and comma' } },
196+
3: { NoName: { value: 'nonesuch', url: 'http://with, comma/test' } },
197+
5: { Name: { value: ', comma first', displayValue: ',', url: 'http://with, comma/test' } },
198+
},
199+
},
200+
orderedModels: List([1, 2, 3, 4, 5]),
201+
queries: {},
202+
rowCount: 5,
203+
});
204+
});
184205
});
185206

186207
test('splitRowsByContainer', () => {

packages/components/src/internal/query/api.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -626,15 +626,16 @@ export function handleSelectRowsResponse(response: Query.Response, queryInfo: Qu
626626
export function quoteValueColumnWithDelimiters(
627627
selectRowsResult: ISelectRowsResult,
628628
valueColumn: string,
629-
delimiter: string
629+
delimiter: string,
630+
multiple = true
630631
): ISelectRowsResult {
631632
const rowMap = selectRowsResult.models[selectRowsResult.key];
632633

633634
Object.values(rowMap).forEach(row => {
634635
const cell = row[valueColumn];
635636
if (Utils.isString(cell?.value)) {
636637
cell.displayValue = cell.displayValue ?? cell.value;
637-
cell.value = quoteValueWithDelimiters(cell.value, delimiter);
638+
cell.value = multiple ? quoteValueWithDelimiters(cell.value, delimiter) : cell.value;
638639
}
639640
});
640641

@@ -646,7 +647,8 @@ export function searchRows(
646647
token: any,
647648
valueColumn: string,
648649
delimiter: string,
649-
exactColumn?: string
650+
exactColumn?: string,
651+
multiple?: boolean
650652
): Promise<ISelectRowsResult> {
651653
return new Promise((resolve, reject) => {
652654
let exactFilters, qFilters;
@@ -713,7 +715,7 @@ export function searchRows(
713715
finalResults = queryResults;
714716
}
715717

716-
resolve(quoteValueColumnWithDelimiters(finalResults, valueColumn, delimiter));
718+
resolve(quoteValueColumnWithDelimiters(finalResults, valueColumn, delimiter, multiple));
717719
})
718720
.catch(reason => {
719721
reject(reason);

0 commit comments

Comments
 (0)