Skip to content

Commit 16c901b

Browse files
authored
Merge changes from release26.3-SNAPSHOT through v7.21.3 (#1956)
### version 7.23.2 *Released*: 18 March 2026 - Merge from release26.3-SNAPSHOT to develop - includes changes from 7.21.2 #1949 - includes changes from 7.21.3 #1947
1 parent d2634cd commit 16c901b

11 files changed

Lines changed: 124 additions & 14 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.1",
3+
"version": "7.23.2",
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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# @labkey/components
22
Components, models, actions, and utility functions for LabKey applications and pages
33

4+
### version 7.23.2
5+
*Released*: 18 March 2026
6+
- Merge from release26.3-SNAPSHOT to develop
7+
- includes changes from 7.21.2 #1949
8+
- includes changes from 7.21.3 #1947
9+
410
### version 7.23.1
511
*Released*: 11 March 2026
612
- Merge from release26.3-SNAPSHOT to develop
@@ -23,6 +29,18 @@ Components, models, actions, and utility functions for LabKey applications and p
2329
- Add UnidentifiedPill
2430
- Add EMPTY_SEQUENCE_WARNING constant
2531

32+
### version 7.21.3
33+
*Released*: 12 March 2026
34+
- GitHub Issue #790: Sample check-in and discard should not allow amount/unit input for differing units
35+
- Update areUnitsCompatible to account for different "Count" unit labels
36+
- getMetricUnitOptions() to allow optional filterFn parameter for the "Count" unit case
37+
38+
### version 7.21.2
39+
*Released*: 11 March 2026
40+
- GitHub Issue 710: Include columns set as Identifying Fields within the 'Search for Samples' grid when adding samples to a storage location
41+
- update saveGridView() to take hidden prop (default false)
42+
- saveSettingsToLocalStorage() fix for checking model.useSavedSettings === SavedSettings.none
43+
2644
### version 7.21.1
2745
*Released*: 4 March 2026
2846
- GitHub Issue 829: Sample type with lookup to list with text primary key where the value contains a comma doesn't map to lookup

packages/components/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,7 @@ import {
865865
getMetricUnitOptions,
866866
MEASUREMENT_UNITS,
867867
UnitModel,
868+
UNITS_KIND,
868869
} from './internal/util/measurement';
869870
import { DELIMITER, DETAIL_TABLE_CLASSES } from './internal/components/forms/constants';
870871
import {
@@ -1536,6 +1537,7 @@ export {
15361537
MAX_EDITABLE_GRID_ROWS,
15371538
MAX_SELECTION_ACTION_ROWS,
15381539
MEASUREMENT_UNITS,
1540+
UNITS_KIND,
15391541
MemberType,
15401542
MenuDivider,
15411543
MenuHeader,

packages/components/src/internal/ViewInfo.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export class ViewInfo {
9797
static UPDATE_NAME = '~~UPDATE~~';
9898
static SAMPLE_FINDER_VIEW_NAME = '~~samplefinder~~';
9999
static IDENTIFYING_FIELDS_VIEW_NAME = '~~identifyingfields~~';
100+
static UNASSIGNED_SAMPLES_VIEW_NAME = '~~unassignedsamples~~';
100101
// TODO seems like this should not be in the generic model, but we'll need a good way
101102
// to define the override detail name.
102103
static BIO_DETAIL_NAME = 'BiologicsDetails';

packages/components/src/internal/actions.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -613,14 +613,15 @@ export function saveGridView(
613613
replace: boolean,
614614
session: boolean,
615615
inherit: boolean,
616-
shared: boolean
616+
shared: boolean,
617+
hidden = false
617618
): Promise<void> {
618619
return new Promise((resolve, reject) => {
619620
Query.saveQueryViews({
620621
schemaName: schemaQuery.schemaName,
621622
queryName: schemaQuery.queryName,
622623
containerPath,
623-
views: [{ ...ViewInfo.serialize(viewInfo), replace, session, inherit, shared, hidden: false }],
624+
views: [{ ...ViewInfo.serialize(viewInfo), replace, session, inherit, shared, hidden }],
624625
success: () => {
625626
invalidateQueryDetailsCache(schemaQuery, containerPath);
626627
resolve();

packages/components/src/internal/components/samples/StorageAmountInput.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,25 @@ interface Props {
1616
className?: string;
1717
inputName?: string;
1818
label: string;
19+
metricUnitsFilterFn?: (option: { label: string; value: string }) => boolean;
1920
model: UnitModel;
2021
preferredUnit: string;
2122
tipText?: string;
2223
unitsChangedHandler?: (units: string) => void;
2324
}
2425

2526
export const StorageAmountInput: FC<Props> = memo(props => {
26-
const { className, model, preferredUnit, inputName, label, tipText, amountChangedHandler, unitsChangedHandler } =
27-
props;
28-
27+
const {
28+
className,
29+
model,
30+
preferredUnit,
31+
inputName,
32+
label,
33+
tipText,
34+
amountChangedHandler,
35+
unitsChangedHandler,
36+
metricUnitsFilterFn,
37+
} = props;
2938
const [amountInput, setAmountInput] = useState<string>(model?.value?.toString() || '');
3039

3140
const unitText = model?.unit?.label || model.unitStr;
@@ -49,6 +58,8 @@ export const StorageAmountInput: FC<Props> = memo(props => {
4958
);
5059
} else {
5160
// IFF preferred units nor provided or are a supported type, then show possible conversions
61+
62+
const options = getMetricUnitOptions(preferredUnit, false, metricUnitsFilterFn);
5263
unitDisplay = (
5364
<SelectInput
5465
containerClass="checkin-unit-select-container"
@@ -57,7 +68,7 @@ export const StorageAmountInput: FC<Props> = memo(props => {
5768
onChange={(name, formValue, option: SelectInputOption) => {
5869
unitsChangedHandler(formValue === undefined && option ? option.id : formValue);
5970
}}
60-
options={getMetricUnitOptions(preferredUnit)}
71+
options={options}
6172
value={model.unit?.label}
6273
/>
6374
);

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ export interface QueryAPIWrapper {
125125
replace: boolean,
126126
session: boolean,
127127
inherit: boolean,
128-
shared: boolean
128+
shared: boolean,
129+
hidden?: boolean
129130
) => Promise<void>;
130131
saveRows: (options: SaveRowsOptions) => Promise<Query.SaveRowsResponse>;
131132
saveRowsByContainer: (options: SaveRowsOptions, containerField?: string) => Promise<Query.SaveRowsResponse>;

packages/components/src/internal/util/measurement.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,49 @@ describe('MetricUnit utils', () => {
139139
expect(getMetricUnitOptions('bad').length).toBe(18);
140140
});
141141

142+
test('getMetricUnitOptions with filterFn', () => {
143+
let options = getMetricUnitOptions('kg', false);
144+
expect(options).toEqual([
145+
{ label: 'g', value: 'g' },
146+
{ label: 'mg', value: 'mg' },
147+
{ label: 'kg', value: 'kg' },
148+
{ label: 'ug', value: 'ug' },
149+
{ label: 'ng', value: 'ng' },
150+
]);
151+
152+
let filterFn = option => option.value !== 'mg';
153+
options = getMetricUnitOptions('kg', false, filterFn);
154+
expect(options).toEqual([
155+
{ label: 'g', value: 'g' },
156+
{ label: 'kg', value: 'kg' },
157+
{ label: 'ug', value: 'ug' },
158+
{ label: 'ng', value: 'ng' },
159+
]);
160+
161+
filterFn = option => option.value === 'mg';
162+
options = getMetricUnitOptions('kg', false, filterFn);
163+
expect(options).toEqual([{ label: 'mg', value: 'mg' }]);
164+
165+
filterFn = option => option.value === 'mg';
166+
options = getMetricUnitOptions('mg', false, filterFn);
167+
expect(options).toEqual([{ label: 'mg', value: 'mg' }]);
168+
169+
// incompatible units
170+
filterFn = option => option.value === 'mg';
171+
options = getMetricUnitOptions('mL', false, filterFn);
172+
expect(options).toEqual([]);
173+
174+
// no unit
175+
filterFn = option => option.value === 'mg';
176+
options = getMetricUnitOptions(undefined, false, filterFn);
177+
expect(options).toEqual([{ label: 'mg', value: 'mg' }]);
178+
179+
// bogus unit will be treated as no unit
180+
filterFn = option => option.value === 'mg';
181+
options = getMetricUnitOptions('bogus', false, filterFn);
182+
expect(options).toEqual([{ label: 'mg', value: 'mg' }]);
183+
});
184+
142185
test('getAltUnitKeys', () => {
143186
const expectedUlOptions = ['mL', 'uL', 'L'];
144187
expect(getAltUnitKeys('uL')).toEqual(expectedUlOptions);
@@ -234,4 +277,20 @@ describe('areUnitsCompatible', () => {
234277
expect(areUnitsCompatible('mL', 'bogus')).toBeFalsy();
235278
expect(areUnitsCompatible('kg', 'bogus')).toBeFalsy();
236279
});
280+
281+
test('comparison of Count units with different labels but same kind', () => {
282+
expect(areUnitsCompatible('count', 'count')).toBeTruthy();
283+
expect(areUnitsCompatible('blocks', 'blocks')).toBeTruthy();
284+
expect(areUnitsCompatible('boxes', 'cells')).toBeTruthy();
285+
expect(areUnitsCompatible('kits', 'packs')).toBeTruthy();
286+
expect(areUnitsCompatible('pieces', 'unit')).toBeTruthy();
287+
expect(areUnitsCompatible('unit', 'unit')).toBeTruthy();
288+
289+
expect(areUnitsCompatible('count', 'count', true)).toBeTruthy();
290+
expect(areUnitsCompatible('blocks', 'blocks', true)).toBeTruthy();
291+
expect(areUnitsCompatible('boxes', 'cells', true)).toBeFalsy();
292+
expect(areUnitsCompatible('kits', 'packs', true)).toBeFalsy();
293+
expect(areUnitsCompatible('pieces', 'unit', true)).toBeFalsy();
294+
expect(areUnitsCompatible('unit', 'unit', true)).toBeTruthy();
295+
});
237296
});

packages/components/src/internal/util/measurement.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ export function getMeasurementUnit(unitStr: string): MeasurementUnit | null {
204204
return null;
205205
}
206206

207-
export function areUnitsCompatible(unitAStr: string, unitBStr: string): boolean {
207+
export function areUnitsCompatible(unitAStr: string, unitBStr: string, compareCountUnitLabels = false): boolean {
208208
if (unitAStr == unitBStr) {
209209
return true;
210210
}
@@ -222,10 +222,21 @@ export function areUnitsCompatible(unitAStr: string, unitBStr: string): boolean
222222
if (!unitA || !unitB) {
223223
return false;
224224
}
225-
return unitA.kind === unitB.kind;
225+
226+
// GitHub Issue #790: for "Count" kind, the specific label must also match
227+
const matchingKinds = unitA.kind === unitB.kind;
228+
if (compareCountUnitLabels && matchingKinds && unitA.kind === UNITS_KIND.COUNT) {
229+
return unitA.label === unitB.label;
230+
}
231+
232+
return matchingKinds;
226233
}
227234

228-
export function getMetricUnitOptions(metricUnit?: string, showLongLabel?: boolean): { label: string; value: string }[] {
235+
export function getMetricUnitOptions(
236+
metricUnit?: string,
237+
showLongLabel?: boolean,
238+
filterFn?: (option: { label: string; value: string }) => boolean
239+
): { label: string; value: string }[] {
229240
const unit = getMeasurementUnit(metricUnit);
230241

231242
const options = [];
@@ -248,6 +259,12 @@ export function getMetricUnitOptions(metricUnit?: string, showLongLabel?: boolea
248259
}
249260
}
250261
}
262+
263+
// apply additional filter if provided
264+
if (filterFn) {
265+
return options.filter(filterFn);
266+
}
267+
251268
return options;
252269
}
253270

0 commit comments

Comments
 (0)