Skip to content
Merged
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": "6.62.8",
"version": "6.62.9",
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
"sideEffects": false,
"files": [
Expand Down
6 changes: 6 additions & 0 deletions packages/components/releaseNotes/components.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# @labkey/components
Components, models, actions, and utility functions for LabKey applications and pages

### 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

### version 6.62.8
*Released*: 3 October 2025
- Issue 53328: AssayDefinitionModel.hasLookup to only consider the first sample lookup column for assay import cases
Expand Down
2 changes: 0 additions & 2 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,6 @@ import {
areUnitsCompatible,
getAltUnitKeys,
getMetricUnitOptions,
isValuePrecisionValid,
MEASUREMENT_UNITS,
UnitModel,
} from './internal/util/measurement';
Expand Down Expand Up @@ -1527,7 +1526,6 @@ export {
IssuesListDefDesignerPanels,
IssuesListDefModel,
isValidFilterField,
isValuePrecisionValid,
ItemsLegend,
JavaDocsLink,
joinDateTimeFormat,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
parsePastedLookup,
removeColumn,
removeColumns,
resolveValueDescriptors,
splitPrefixedNumber,
validateAndInsertPastedData,
} from './actions';
Expand Down Expand Up @@ -1168,6 +1169,15 @@ describe('loadEditorModelData', () => {
fieldKeyPath: 'IntField$P$S$C$D$A',
derivationDataScope: 'ParentOnly',
name: 'IntField./,$&',
jsonType: 'int',
}),
new QueryColumn({
fieldKey: 'DecField$P$S$C$D$A',
fieldKeyArray: ['DecField./,$&'],
fieldKeyPath: 'DecField$P$S$C$D$A',
derivationDataScope: 'ParentOnly',
name: 'DecField./,$&',
jsonType: 'float',
}),
new QueryColumn({
fieldKey: 'lkField$P$S$C$D$A',
Expand Down Expand Up @@ -1241,7 +1251,7 @@ describe('loadEditorModelData', () => {
'DtTimeField./,$&': [{ displayValue: '2025-02-07 16:30', value: '2025-02-07 16:30:00.000' }],
'lkField./,$&': [{ displayValue: 'Assay Required File', value: 37721 }],
RowId: 2805931,
'DecField./,$&': 222,
'DecField./,$&': { displayValue: 22.3, value: 22.26 },
'aliqAndParent$,./': '888',
StoredAmount: 99,
Description: '111',
Expand Down Expand Up @@ -1322,6 +1332,8 @@ describe('loadEditorModelData', () => {
'samplefield$p$s$c$d$a&&1': [{ display: '10-1-1', raw: 117334 }],
'intfield$p$s$c$d$a&&0': [{ display: 3, raw: 3 }],
'dtfield$p$s$c$d$a&&0': [{ display: '2025-Feb-04 00:00:11.234', raw: '2025-02-04 00:00:11.234' }],
'decfield$p$s$c$d$a&&0': [{ display: 22, raw: 22 }],
'decfield$p$s$c$d$a&&1': [{ display: 22.26, raw: 22.26 }],
'intfield$p$s$c$d$a&&1': [{ display: 333, raw: 333 }],
'aliqfield$d$c$p$s&&0': [{ display: '123', raw: '123' }],
'dtfield$p$s$c$d$a&&1': [{ display: '2025-Feb-07 00:00', raw: '2025-02-07 00:00:00.000' }],
Expand Down Expand Up @@ -1378,3 +1390,58 @@ describe('loadEditorModelData', () => {
expect(api.query.selectRows).toHaveBeenCalledTimes(2);
});
});

describe('resolveValueDescriptors', () => {
test('default raw and displayValue', () => {
const col = new QueryColumn({ fieldKey: 'col1', name: 'col1' });
expect(resolveValueDescriptors(col, {}, {}, 'cellKey', 1)).toStrictEqual([{ display: 1, raw: 1 }]);
expect(resolveValueDescriptors(col, {}, {}, 'cellKey', 'value')).toStrictEqual([
{ display: 'value', raw: 'value' },
]);
expect(resolveValueDescriptors(col, {}, {}, 'cellKey', true)).toStrictEqual([{ display: true, raw: true }]);
expect(resolveValueDescriptors(col, {}, {}, 'cellKey', false)).toStrictEqual([{ display: false, raw: false }]);
expect(resolveValueDescriptors(col, {}, {}, 'cellKey', null)).toStrictEqual([
{ display: undefined, raw: undefined },
]);
expect(resolveValueDescriptors(col, {}, {}, 'cellKey', undefined)).toStrictEqual([
{ display: undefined, raw: undefined },
]);

expect(resolveValueDescriptors(col, {}, {}, 'cellKey', { value: 1 })).toStrictEqual([{ display: 1, raw: 1 }]);
expect(resolveValueDescriptors(col, {}, {}, 'cellKey', { value: 1, displayValue: '1.00' })).toStrictEqual([
{ display: '1.00', raw: 1 },
]);
});

test('isDecimalJsonType, non lookup', () => {
const intCol = new QueryColumn({ fieldKey: 'col1', name: 'col1', jsonType: 'int' });
const floatCol = new QueryColumn({ fieldKey: 'col1', name: 'col1', jsonType: 'float' });
const intLookupCol = new QueryColumn({
fieldKey: 'col1',
name: 'col1',
jsonType: 'int',
lookup: { keyColumn: 'id', displayColumn: 'name' },
});
const floatLookupCol = new QueryColumn({
fieldKey: 'col1',
name: 'col1',
jsonType: 'float',
lookup: { keyColumn: 'id', displayColumn: 'name' },
});
expect(resolveValueDescriptors(intCol, {}, {}, 'cellKey', { value: 10, displayValue: '010' })).toStrictEqual([
{ display: '010', raw: 10 },
]);
expect(
resolveValueDescriptors(intCol, {}, {}, 'cellKey', { value: 1, displayValue: 'Sample 1' })
).toStrictEqual([{ display: 'Sample 1', raw: 1 }]);
expect(
resolveValueDescriptors(intLookupCol, {}, {}, 'cellKey', { value: 1, displayValue: 'Sample 1' })
).toStrictEqual([{ display: 'Sample 1', raw: 1 }]);
expect(
resolveValueDescriptors(floatCol, {}, {}, 'cellKey', { value: 1.005, displayValue: 1.01 })
).toStrictEqual([{ display: 1.005, raw: 1.005 }]);
expect(
resolveValueDescriptors(floatLookupCol, {}, {}, 'cellKey', { value: 1, displayValue: 'Sample 1' })
).toStrictEqual([{ display: 'Sample 1', raw: 1 }]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ interface MessageAndValue {

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

function resolveValueDescriptors(
// export for jest testing
export function resolveValueDescriptors(
col: QueryColumn,
lookupValues: MessageAndValueMap,
cellMessages: Record<string, CellMessage>,
Expand All @@ -220,6 +221,8 @@ function resolveValueDescriptors(
let display = value?.displayValue ?? raw;
if (col.isTimeOrDateTimeColumn) {
display = getDateTimeDisplayValueFromStr(raw, col);
} else if (!col.isLookup() && col.isDecimalJsonType) {
display = raw; // Issue 53934: don't use displayValue for numeric columns
}

return [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { TEST_PROJECT_CONTAINER } from '../../containerFixtures';

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

import { isPrecisionValid, isValid, SampleAmountEditModal } from './SampleAmountEditModal';
import { isValid, SampleAmountEditModal } from './SampleAmountEditModal';

describe('SampleAmountEditModal', () => {
const testSchemaQuery = new SchemaQuery('schema', 'query', 'view');
Expand All @@ -36,7 +36,9 @@ describe('SampleAmountEditModal', () => {
expect(document.querySelector('textarea').getAttribute('value')).toBe(comment ?? null);
expect(document.querySelectorAll('.alert')).toHaveLength(isNegative ? 1 : 0);
if (isNegative) {
expect(document.querySelectorAll('.alert').item(0).textContent).toBe('Amount must be a non-negative value.');
expect(document.querySelectorAll('.alert').item(0).textContent).toBe(
'Amount must be a non-negative value.'
);
}
validateSubmitButton(noun, canSave);
}
Expand Down Expand Up @@ -206,35 +208,6 @@ describe('SampleAmountEditModal', () => {
});
});

describe('isPrecisionValid', () => {
test('no amount and no units', () => {
expect(isPrecisionValid(undefined, undefined)).toBe(true);
expect(isPrecisionValid(undefined, null)).toBe(true);
expect(isPrecisionValid(undefined, 'bogus')).toBe(true);
expect(isPrecisionValid(undefined, 'mL')).toBe(true);
expect(isPrecisionValid(undefined, 'mg')).toBe(true);
expect(isPrecisionValid(0, undefined)).toBe(true);
expect(isPrecisionValid(1, undefined)).toBe(true);
});

test('with amount and units', () => {
expect(isPrecisionValid(1, 'mg')).toBe(true);
expect(isPrecisionValid(0.1, 'mg')).toBe(true);
expect(isPrecisionValid(0.01, 'mg')).toBe(true);
expect(isPrecisionValid(0.001, 'mg')).toBe(true);
expect(isPrecisionValid(0.0001, 'mg')).toBe(true);
expect(isPrecisionValid(0.00001, 'mg')).toBe(true);
expect(isPrecisionValid(0.000001, 'mg')).toBe(true);
expect(isPrecisionValid(0.0000001, 'mg')).toBe(false);
expect(isPrecisionValid(10.0000001, 'mg')).toBe(false);
});

test('with negative amount', () => {
expect(isPrecisionValid(-1, 'mg')).toBe(false);
expect(isPrecisionValid(-0.001, 'mg')).toBe(false);
});
});

describe('isValid', () => {
test('has neither', () => {
expect(isValid(undefined, undefined)).toBe(true);
Expand All @@ -256,9 +229,6 @@ describe('isValid', () => {
expect(isValid(0, 'uL')).toBe(true);
expect(isValid(10, 'uL')).toBe(true);
expect(isValid(0.1, 'uL')).toBe(true);
expect(isValid(0.01, 'uL')).toBe(true);
expect(isValid(0.001, 'uL')).toBe(true);
expect(isValid(0.0001, 'uL')).toBe(false);
expect(isValid(10.0001, 'uL')).toBe(false);
expect(isValid(10.000000001, 'uL')).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { SchemaQuery } from '../../../public/SchemaQuery';
import { caseInsensitive } from '../../util/utils';
import { Alert } from '../base/Alert';

import { isValuePrecisionValid, MEASUREMENT_UNITS, UnitModel } from '../../util/measurement';
import { UnitModel } from '../../util/measurement';

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

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

import { updateSampleStorageData } from './actions';
import { AMOUNT_PRECISION_ERROR_TEXT, STORED_AMOUNT_FIELDS } from './constants';
import { STORED_AMOUNT_FIELDS } from './constants';
import { StorageAmountInput } from './StorageAmountInput';

interface Props {
Expand All @@ -24,12 +24,6 @@ interface Props {
updateListener: () => void;
}

// exported for jest testing
export const isPrecisionValid = (amount: number, storageUnits: string): boolean => {
const units = MEASUREMENT_UNITS[storageUnits?.toLowerCase()];
return isValuePrecisionValid(amount, units?.displayPrecision);
};

// exported for jest testing
export const isValid = (amount: number, units: string): boolean => {
const hasAmount = amount !== undefined && amount !== null;
Expand All @@ -38,7 +32,7 @@ export const isValid = (amount: number, units: string): boolean => {
const hasNeither = !hasAmount && !hasUnits;

if (hasBoth) {
return amount >= 0 && isPrecisionValid(amount, units);
return amount >= 0;
}
return hasNeither;
};
Expand All @@ -48,16 +42,15 @@ export const SampleAmountEditModal: FC<Props> = memo(props => {

const {
[STORED_AMOUNT_FIELDS.ROWID]: rowId,
[STORED_AMOUNT_FIELDS.UNITS]: Units,
[STORED_AMOUNT_FIELDS.AMOUNT]: initStorageAmount,
[STORED_AMOUNT_FIELDS.UNITS]: units,
[STORED_AMOUNT_FIELDS.AMOUNT]: storedAmount,
[STORED_AMOUNT_FIELDS.SAMPLE_TYPE_UNITS]: sampleTypeUnits,
} = row;

const sampleContainer = caseInsensitive(row, 'Container/Path')?.value;
const initStorageUnits = Units?.value;
const [amount, setStorageAmount] = useState<number>(
initStorageAmount?.displayValue ?? initStorageAmount?.value ?? undefined
);
const initStorageUnits = units?.value;
const initStorageAmount = storedAmount?.value;
const [amount, setStorageAmount] = useState<number>(initStorageAmount);
const [storageUnits, setStorageUnits] = useState<string>(initStorageUnits ?? null);
const [comment, setComment] = useState<string>('');
const [submitting, setSubmitting] = useState(false);
Expand All @@ -71,11 +64,6 @@ export const SampleAmountEditModal: FC<Props> = memo(props => {
}, [onClose]);

const handleUpdateSampleRow = (): Promise<any> => {
// Issue 41931: html input number step value only validates to a certain precision
const precision = storageUnits ? MEASUREMENT_UNITS[storageUnits.toLowerCase()]?.displayPrecision : 2;
if (!isValuePrecisionValid(amount, precision)) {
return Promise.reject(AMOUNT_PRECISION_ERROR_TEXT);
}
const sampleData = [
{
materialId: rowId?.value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,10 @@ import {
getMetricUnitOptions,
getVolumeMinStep,
isMeasurementUnitIgnoreCase,
isValuePrecisionValid,
MEASUREMENT_UNITS,
UnitModel,
} from '../../util/measurement';

import { AMOUNT_PRECISION_ERROR_TEXT } from './constants';

const deltaTooPreciseMessage = (
<Alert bsStyle="danger" className="storage-item-precision-alert">
{AMOUNT_PRECISION_ERROR_TEXT}
</Alert>
);
const negativeValueMessage = (
<Alert bsStyle="danger" className="storage-item-precision-alert">
Amount must be a non-negative value.
Expand All @@ -41,7 +33,6 @@ export const StorageAmountInput: FC<Props> = memo(props => {
props;

const isNegativeValue = model?.value < 0;
const isDeltaValid = isValuePrecisionValid(model?.value, model?.unit?.displayPrecision);
const unitText = model?.unit?.label || model.unitStr;
let preferredUnitMessage;

Expand Down Expand Up @@ -96,7 +87,7 @@ export const StorageAmountInput: FC<Props> = memo(props => {
return (
<>
<div className={containerClassName}>
<div className={'checkin-amount-label ' + (isDeltaValid ? '' : 'has-error ')}>
<div className={'checkin-amount-label'}>
<label htmlFor="checkin-amount">{label}</label>
{tipText && (
<LabelHelpTip title="Stored Amount Delta">
Expand All @@ -119,7 +110,6 @@ export const StorageAmountInput: FC<Props> = memo(props => {
{preferredUnitMessage}
</div>
{isNegativeValue ? negativeValueMessage : undefined}
{!isNegativeValue && !isDeltaValid ? deltaTooPreciseMessage : undefined}
</>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,6 @@ export const SAMPLE_DOMAIN_INVENTORY_SYSTEM_FIELDS = [
{ Name: 'StorageCol', Label: 'Storage Col', DataType: 'Text', Required: false, Description: '', Disableable: true },
];

export const AMOUNT_PRECISION_ERROR_TEXT = 'Amount used is too precise for selected units.';

export const STORED_AMOUNT_FIELDS = {
ROWID: 'RowId',
AMOUNT: 'StoredAmount',
Expand Down
Loading