Skip to content

Commit efa7f8e

Browse files
authored
Issue 53934: Remove stored amount "too precise" validation check on setting amount modal (#1861)
### version 6.62.9 *Released*: 7 October 2025 - Issue 53934: Remove stored amount "too precise" validation check on setting amount modal - remove isValuePrecisionValid() and the related isPrecisionValid() - use amount value instead of displayValue for EditableGrid and SampleAmountEditModal
1 parent 88b4442 commit efa7f8e

14 files changed

Lines changed: 119 additions & 141 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": "6.62.8",
3+
"version": "6.62.9",
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: 6 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 6.62.9
5+
*Released*: 7 October 2025
6+
- Issue 53934: Remove stored amount "too precise" validation check on setting amount modal
7+
- remove isValuePrecisionValid() and the related isPrecisionValid()
8+
- use amount value instead of displayValue for EditableGrid and SampleAmountEditModal
9+
410
### version 6.62.8
511
*Released*: 3 October 2025
612
- Issue 53328: AssayDefinitionModel.hasLookup to only consider the first sample lookup column for assay import cases

packages/components/src/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -885,7 +885,6 @@ import {
885885
areUnitsCompatible,
886886
getAltUnitKeys,
887887
getMetricUnitOptions,
888-
isValuePrecisionValid,
889888
MEASUREMENT_UNITS,
890889
UnitModel,
891890
} from './internal/util/measurement';
@@ -1527,7 +1526,6 @@ export {
15271526
IssuesListDefDesignerPanels,
15281527
IssuesListDefModel,
15291528
isValidFilterField,
1530-
isValuePrecisionValid,
15311529
ItemsLegend,
15321530
JavaDocsLink,
15331531
joinDateTimeFormat,

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

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
parsePastedLookup,
2424
removeColumn,
2525
removeColumns,
26+
resolveValueDescriptors,
2627
splitPrefixedNumber,
2728
validateAndInsertPastedData,
2829
} from './actions';
@@ -1168,6 +1169,15 @@ describe('loadEditorModelData', () => {
11681169
fieldKeyPath: 'IntField$P$S$C$D$A',
11691170
derivationDataScope: 'ParentOnly',
11701171
name: 'IntField./,$&',
1172+
jsonType: 'int',
1173+
}),
1174+
new QueryColumn({
1175+
fieldKey: 'DecField$P$S$C$D$A',
1176+
fieldKeyArray: ['DecField./,$&'],
1177+
fieldKeyPath: 'DecField$P$S$C$D$A',
1178+
derivationDataScope: 'ParentOnly',
1179+
name: 'DecField./,$&',
1180+
jsonType: 'float',
11711181
}),
11721182
new QueryColumn({
11731183
fieldKey: 'lkField$P$S$C$D$A',
@@ -1241,7 +1251,7 @@ describe('loadEditorModelData', () => {
12411251
'DtTimeField./,$&': [{ displayValue: '2025-02-07 16:30', value: '2025-02-07 16:30:00.000' }],
12421252
'lkField./,$&': [{ displayValue: 'Assay Required File', value: 37721 }],
12431253
RowId: 2805931,
1244-
'DecField./,$&': 222,
1254+
'DecField./,$&': { displayValue: 22.3, value: 22.26 },
12451255
'aliqAndParent$,./': '888',
12461256
StoredAmount: 99,
12471257
Description: '111',
@@ -1322,6 +1332,8 @@ describe('loadEditorModelData', () => {
13221332
'samplefield$p$s$c$d$a&&1': [{ display: '10-1-1', raw: 117334 }],
13231333
'intfield$p$s$c$d$a&&0': [{ display: 3, raw: 3 }],
13241334
'dtfield$p$s$c$d$a&&0': [{ display: '2025-Feb-04 00:00:11.234', raw: '2025-02-04 00:00:11.234' }],
1335+
'decfield$p$s$c$d$a&&0': [{ display: 22, raw: 22 }],
1336+
'decfield$p$s$c$d$a&&1': [{ display: 22.26, raw: 22.26 }],
13251337
'intfield$p$s$c$d$a&&1': [{ display: 333, raw: 333 }],
13261338
'aliqfield$d$c$p$s&&0': [{ display: '123', raw: '123' }],
13271339
'dtfield$p$s$c$d$a&&1': [{ display: '2025-Feb-07 00:00', raw: '2025-02-07 00:00:00.000' }],
@@ -1378,3 +1390,58 @@ describe('loadEditorModelData', () => {
13781390
expect(api.query.selectRows).toHaveBeenCalledTimes(2);
13791391
});
13801392
});
1393+
1394+
describe('resolveValueDescriptors', () => {
1395+
test('default raw and displayValue', () => {
1396+
const col = new QueryColumn({ fieldKey: 'col1', name: 'col1' });
1397+
expect(resolveValueDescriptors(col, {}, {}, 'cellKey', 1)).toStrictEqual([{ display: 1, raw: 1 }]);
1398+
expect(resolveValueDescriptors(col, {}, {}, 'cellKey', 'value')).toStrictEqual([
1399+
{ display: 'value', raw: 'value' },
1400+
]);
1401+
expect(resolveValueDescriptors(col, {}, {}, 'cellKey', true)).toStrictEqual([{ display: true, raw: true }]);
1402+
expect(resolveValueDescriptors(col, {}, {}, 'cellKey', false)).toStrictEqual([{ display: false, raw: false }]);
1403+
expect(resolveValueDescriptors(col, {}, {}, 'cellKey', null)).toStrictEqual([
1404+
{ display: undefined, raw: undefined },
1405+
]);
1406+
expect(resolveValueDescriptors(col, {}, {}, 'cellKey', undefined)).toStrictEqual([
1407+
{ display: undefined, raw: undefined },
1408+
]);
1409+
1410+
expect(resolveValueDescriptors(col, {}, {}, 'cellKey', { value: 1 })).toStrictEqual([{ display: 1, raw: 1 }]);
1411+
expect(resolveValueDescriptors(col, {}, {}, 'cellKey', { value: 1, displayValue: '1.00' })).toStrictEqual([
1412+
{ display: '1.00', raw: 1 },
1413+
]);
1414+
});
1415+
1416+
test('isDecimalJsonType, non lookup', () => {
1417+
const intCol = new QueryColumn({ fieldKey: 'col1', name: 'col1', jsonType: 'int' });
1418+
const floatCol = new QueryColumn({ fieldKey: 'col1', name: 'col1', jsonType: 'float' });
1419+
const intLookupCol = new QueryColumn({
1420+
fieldKey: 'col1',
1421+
name: 'col1',
1422+
jsonType: 'int',
1423+
lookup: { keyColumn: 'id', displayColumn: 'name' },
1424+
});
1425+
const floatLookupCol = new QueryColumn({
1426+
fieldKey: 'col1',
1427+
name: 'col1',
1428+
jsonType: 'float',
1429+
lookup: { keyColumn: 'id', displayColumn: 'name' },
1430+
});
1431+
expect(resolveValueDescriptors(intCol, {}, {}, 'cellKey', { value: 10, displayValue: '010' })).toStrictEqual([
1432+
{ display: '010', raw: 10 },
1433+
]);
1434+
expect(
1435+
resolveValueDescriptors(intCol, {}, {}, 'cellKey', { value: 1, displayValue: 'Sample 1' })
1436+
).toStrictEqual([{ display: 'Sample 1', raw: 1 }]);
1437+
expect(
1438+
resolveValueDescriptors(intLookupCol, {}, {}, 'cellKey', { value: 1, displayValue: 'Sample 1' })
1439+
).toStrictEqual([{ display: 'Sample 1', raw: 1 }]);
1440+
expect(
1441+
resolveValueDescriptors(floatCol, {}, {}, 'cellKey', { value: 1.005, displayValue: 1.01 })
1442+
).toStrictEqual([{ display: 1.005, raw: 1.005 }]);
1443+
expect(
1444+
resolveValueDescriptors(floatLookupCol, {}, {}, 'cellKey', { value: 1, displayValue: 'Sample 1' })
1445+
).toStrictEqual([{ display: 'Sample 1', raw: 1 }]);
1446+
});
1447+
});

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,8 @@ interface MessageAndValue {
193193

194194
type MessageAndValueMap = Record<string, MessageAndValue[]>;
195195

196-
function resolveValueDescriptors(
196+
// export for jest testing
197+
export function resolveValueDescriptors(
197198
col: QueryColumn,
198199
lookupValues: MessageAndValueMap,
199200
cellMessages: Record<string, CellMessage>,
@@ -220,6 +221,8 @@ function resolveValueDescriptors(
220221
let display = value?.displayValue ?? raw;
221222
if (col.isTimeOrDateTimeColumn) {
222223
display = getDateTimeDisplayValueFromStr(raw, col);
224+
} else if (!col.isLookup() && col.isDecimalJsonType) {
225+
display = raw; // Issue 53934: don't use displayValue for numeric columns
223226
}
224227

225228
return [

packages/components/src/internal/components/samples/SampleAmountEditModal.test.tsx

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { TEST_PROJECT_CONTAINER } from '../../containerFixtures';
1111

1212
import { renderWithAppContext } from '../../test/reactTestLibraryHelpers';
1313

14-
import { isPrecisionValid, isValid, SampleAmountEditModal } from './SampleAmountEditModal';
14+
import { isValid, SampleAmountEditModal } from './SampleAmountEditModal';
1515

1616
describe('SampleAmountEditModal', () => {
1717
const testSchemaQuery = new SchemaQuery('schema', 'query', 'view');
@@ -36,7 +36,9 @@ describe('SampleAmountEditModal', () => {
3636
expect(document.querySelector('textarea').getAttribute('value')).toBe(comment ?? null);
3737
expect(document.querySelectorAll('.alert')).toHaveLength(isNegative ? 1 : 0);
3838
if (isNegative) {
39-
expect(document.querySelectorAll('.alert').item(0).textContent).toBe('Amount must be a non-negative value.');
39+
expect(document.querySelectorAll('.alert').item(0).textContent).toBe(
40+
'Amount must be a non-negative value.'
41+
);
4042
}
4143
validateSubmitButton(noun, canSave);
4244
}
@@ -206,35 +208,6 @@ describe('SampleAmountEditModal', () => {
206208
});
207209
});
208210

209-
describe('isPrecisionValid', () => {
210-
test('no amount and no units', () => {
211-
expect(isPrecisionValid(undefined, undefined)).toBe(true);
212-
expect(isPrecisionValid(undefined, null)).toBe(true);
213-
expect(isPrecisionValid(undefined, 'bogus')).toBe(true);
214-
expect(isPrecisionValid(undefined, 'mL')).toBe(true);
215-
expect(isPrecisionValid(undefined, 'mg')).toBe(true);
216-
expect(isPrecisionValid(0, undefined)).toBe(true);
217-
expect(isPrecisionValid(1, undefined)).toBe(true);
218-
});
219-
220-
test('with amount and units', () => {
221-
expect(isPrecisionValid(1, 'mg')).toBe(true);
222-
expect(isPrecisionValid(0.1, 'mg')).toBe(true);
223-
expect(isPrecisionValid(0.01, 'mg')).toBe(true);
224-
expect(isPrecisionValid(0.001, 'mg')).toBe(true);
225-
expect(isPrecisionValid(0.0001, 'mg')).toBe(true);
226-
expect(isPrecisionValid(0.00001, 'mg')).toBe(true);
227-
expect(isPrecisionValid(0.000001, 'mg')).toBe(true);
228-
expect(isPrecisionValid(0.0000001, 'mg')).toBe(false);
229-
expect(isPrecisionValid(10.0000001, 'mg')).toBe(false);
230-
});
231-
232-
test('with negative amount', () => {
233-
expect(isPrecisionValid(-1, 'mg')).toBe(false);
234-
expect(isPrecisionValid(-0.001, 'mg')).toBe(false);
235-
});
236-
});
237-
238211
describe('isValid', () => {
239212
test('has neither', () => {
240213
expect(isValid(undefined, undefined)).toBe(true);
@@ -256,9 +229,6 @@ describe('isValid', () => {
256229
expect(isValid(0, 'uL')).toBe(true);
257230
expect(isValid(10, 'uL')).toBe(true);
258231
expect(isValid(0.1, 'uL')).toBe(true);
259-
expect(isValid(0.01, 'uL')).toBe(true);
260-
expect(isValid(0.001, 'uL')).toBe(true);
261-
expect(isValid(0.0001, 'uL')).toBe(false);
262-
expect(isValid(10.0001, 'uL')).toBe(false);
232+
expect(isValid(10.000000001, 'uL')).toBe(true);
263233
});
264234
});

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

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { SchemaQuery } from '../../../public/SchemaQuery';
44
import { caseInsensitive } from '../../util/utils';
55
import { Alert } from '../base/Alert';
66

7-
import { isValuePrecisionValid, MEASUREMENT_UNITS, UnitModel } from '../../util/measurement';
7+
import { UnitModel } from '../../util/measurement';
88

99
import { Modal } from '../../Modal';
1010

@@ -13,7 +13,7 @@ import { CommentTextArea } from '../forms/input/CommentTextArea';
1313
import { useDataChangeCommentsRequired } from '../forms/input/useDataChangeCommentsRequired';
1414

1515
import { updateSampleStorageData } from './actions';
16-
import { AMOUNT_PRECISION_ERROR_TEXT, STORED_AMOUNT_FIELDS } from './constants';
16+
import { STORED_AMOUNT_FIELDS } from './constants';
1717
import { StorageAmountInput } from './StorageAmountInput';
1818

1919
interface Props {
@@ -24,12 +24,6 @@ interface Props {
2424
updateListener: () => void;
2525
}
2626

27-
// exported for jest testing
28-
export const isPrecisionValid = (amount: number, storageUnits: string): boolean => {
29-
const units = MEASUREMENT_UNITS[storageUnits?.toLowerCase()];
30-
return isValuePrecisionValid(amount, units?.displayPrecision);
31-
};
32-
3327
// exported for jest testing
3428
export const isValid = (amount: number, units: string): boolean => {
3529
const hasAmount = amount !== undefined && amount !== null;
@@ -38,7 +32,7 @@ export const isValid = (amount: number, units: string): boolean => {
3832
const hasNeither = !hasAmount && !hasUnits;
3933

4034
if (hasBoth) {
41-
return amount >= 0 && isPrecisionValid(amount, units);
35+
return amount >= 0;
4236
}
4337
return hasNeither;
4438
};
@@ -48,16 +42,15 @@ export const SampleAmountEditModal: FC<Props> = memo(props => {
4842

4943
const {
5044
[STORED_AMOUNT_FIELDS.ROWID]: rowId,
51-
[STORED_AMOUNT_FIELDS.UNITS]: Units,
52-
[STORED_AMOUNT_FIELDS.AMOUNT]: initStorageAmount,
45+
[STORED_AMOUNT_FIELDS.UNITS]: units,
46+
[STORED_AMOUNT_FIELDS.AMOUNT]: storedAmount,
5347
[STORED_AMOUNT_FIELDS.SAMPLE_TYPE_UNITS]: sampleTypeUnits,
5448
} = row;
5549

5650
const sampleContainer = caseInsensitive(row, 'Container/Path')?.value;
57-
const initStorageUnits = Units?.value;
58-
const [amount, setStorageAmount] = useState<number>(
59-
initStorageAmount?.displayValue ?? initStorageAmount?.value ?? undefined
60-
);
51+
const initStorageUnits = units?.value;
52+
const initStorageAmount = storedAmount?.value;
53+
const [amount, setStorageAmount] = useState<number>(initStorageAmount);
6154
const [storageUnits, setStorageUnits] = useState<string>(initStorageUnits ?? null);
6255
const [comment, setComment] = useState<string>('');
6356
const [submitting, setSubmitting] = useState(false);
@@ -71,11 +64,6 @@ export const SampleAmountEditModal: FC<Props> = memo(props => {
7164
}, [onClose]);
7265

7366
const handleUpdateSampleRow = (): Promise<any> => {
74-
// Issue 41931: html input number step value only validates to a certain precision
75-
const precision = storageUnits ? MEASUREMENT_UNITS[storageUnits.toLowerCase()]?.displayPrecision : 2;
76-
if (!isValuePrecisionValid(amount, precision)) {
77-
return Promise.reject(AMOUNT_PRECISION_ERROR_TEXT);
78-
}
7967
const sampleData = [
8068
{
8169
materialId: rowId?.value,

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

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,10 @@ import {
77
getMetricUnitOptions,
88
getVolumeMinStep,
99
isMeasurementUnitIgnoreCase,
10-
isValuePrecisionValid,
1110
MEASUREMENT_UNITS,
1211
UnitModel,
1312
} from '../../util/measurement';
1413

15-
import { AMOUNT_PRECISION_ERROR_TEXT } from './constants';
16-
17-
const deltaTooPreciseMessage = (
18-
<Alert bsStyle="danger" className="storage-item-precision-alert">
19-
{AMOUNT_PRECISION_ERROR_TEXT}
20-
</Alert>
21-
);
2214
const negativeValueMessage = (
2315
<Alert bsStyle="danger" className="storage-item-precision-alert">
2416
Amount must be a non-negative value.
@@ -41,7 +33,6 @@ export const StorageAmountInput: FC<Props> = memo(props => {
4133
props;
4234

4335
const isNegativeValue = model?.value < 0;
44-
const isDeltaValid = isValuePrecisionValid(model?.value, model?.unit?.displayPrecision);
4536
const unitText = model?.unit?.label || model.unitStr;
4637
let preferredUnitMessage;
4738

@@ -96,7 +87,7 @@ export const StorageAmountInput: FC<Props> = memo(props => {
9687
return (
9788
<>
9889
<div className={containerClassName}>
99-
<div className={'checkin-amount-label ' + (isDeltaValid ? '' : 'has-error ')}>
90+
<div className={'checkin-amount-label'}>
10091
<label htmlFor="checkin-amount">{label}</label>
10192
{tipText && (
10293
<LabelHelpTip title="Stored Amount Delta">
@@ -119,7 +110,6 @@ export const StorageAmountInput: FC<Props> = memo(props => {
119110
{preferredUnitMessage}
120111
</div>
121112
{isNegativeValue ? negativeValueMessage : undefined}
122-
{!isNegativeValue && !isDeltaValid ? deltaTooPreciseMessage : undefined}
123113
</>
124114
);
125115
});

packages/components/src/internal/components/samples/constants.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,6 @@ export const SAMPLE_DOMAIN_INVENTORY_SYSTEM_FIELDS = [
307307
{ Name: 'StorageCol', Label: 'Storage Col', DataType: 'Text', Required: false, Description: '', Disableable: true },
308308
];
309309

310-
export const AMOUNT_PRECISION_ERROR_TEXT = 'Amount used is too precise for selected units.';
311-
312310
export const STORED_AMOUNT_FIELDS = {
313311
ROWID: 'RowId',
314312
AMOUNT: 'StoredAmount',

0 commit comments

Comments
 (0)