From e55295700093e0f6587849e3a79ffd433e3524ff Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 17 Nov 2025 09:54:24 -0800 Subject: [PATCH 01/22] Disallow negative sample amount --- .../src/internal/components/domainproperties/constants.ts | 1 + packages/components/src/internal/components/editable/utils.ts | 3 +++ .../src/internal/components/forms/QueryFormInputs.tsx | 1 + 3 files changed, 5 insertions(+) diff --git a/packages/components/src/internal/components/domainproperties/constants.ts b/packages/components/src/internal/components/domainproperties/constants.ts index a914003a0a..7525f0dce1 100644 --- a/packages/components/src/internal/components/domainproperties/constants.ts +++ b/packages/components/src/internal/components/domainproperties/constants.ts @@ -158,6 +158,7 @@ export const MODIFIED_TIMESTAMP_CONCEPT_URI = 'http://www.labkey.org/types#modif export const SMILES_CONCEPT_URI = 'http://www.labkey.org/exp/xml#smiles'; export const AUTO_INT_CONCEPT_URI = 'http://www.labkey.org/types#autoInt'; export const CALCULATED_CONCEPT_URI = 'http://www.labkey.org/exp/xml#calculated'; +export const NON_NEGATIVE_NUMBER_CONCEPT_URI = 'http://www.labkey.org/types#nonNegativeNumber'; export const UNLIMITED_TEXT_LENGTH = 2147483647; // Integer.MAX_VALUE export const MAX_TEXT_LENGTH = 4000; diff --git a/packages/components/src/internal/components/editable/utils.ts b/packages/components/src/internal/components/editable/utils.ts index 957ebed635..845840af86 100644 --- a/packages/components/src/internal/components/editable/utils.ts +++ b/packages/components/src/internal/components/editable/utils.ts @@ -20,6 +20,7 @@ import { incrementClientSideMetricCount } from '../../actions'; import { CellMessage } from './models'; import { CellActions, MODIFICATION_TYPES } from './constants'; +import { NON_NEGATIVE_NUMBER_CONCEPT_URI } from '../domainproperties/constants'; interface ValidatedValue { message: CellMessage; @@ -63,6 +64,8 @@ export const getValidatedEditableGridValue = (origValue: any, col: QueryColumn): message = 'Invalid integer'; } else if (jsonType === 'float' && !isFloat(value)) { message = 'Invalid decimal'; + } else if (NON_NEGATIVE_NUMBER_CONCEPT_URI === col?.conceptURI && Number(value) < 0) { + message = 'Invalid ' + col.caption + ', must be non-negative'; } else if (jsonType === 'string' && scale) { if (value.toString().trim().length > scale) message = value.toString().trim().length + '/' + scale + ' characters'; diff --git a/packages/components/src/internal/components/forms/QueryFormInputs.tsx b/packages/components/src/internal/components/forms/QueryFormInputs.tsx index 7ae2f034b7..0f29ef8610 100644 --- a/packages/components/src/internal/components/forms/QueryFormInputs.tsx +++ b/packages/components/src/internal/components/forms/QueryFormInputs.tsx @@ -236,6 +236,7 @@ export class QueryFormInputs extends React.Component ); } From a243ef57f2a8b526cbe542bd0e2f3ec17ae45135 Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 17 Nov 2025 11:05:50 -0800 Subject: [PATCH 02/22] jest --- packages/components/package.json | 2 +- .../components/releaseNotes/components.md | 5 +++ .../components/editable/utils.test.ts | 34 ++++++++++++++++++- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/components/package.json b/packages/components/package.json index b156cc69c9..cefc347d12 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "6.70.5", + "version": "6.70.6-fb-amountValidation.1", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index f3f084653c..e2fb928197 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -1,6 +1,11 @@ # @labkey/components Components, models, actions, and utility functions for LabKey applications and pages +### version 6.X +*Released*: X November 2025 +- Disallow negative sample amounts + - Update `getValidatedEditableGridValue` to check for negative amount values + ### version 6.70.5 *Released*: 13 November 2025 - Issue 54186: App actions for picklists and assay run delete don't get TransactionAuditEvent diff --git a/packages/components/src/internal/components/editable/utils.test.ts b/packages/components/src/internal/components/editable/utils.test.ts index c10dfcb870..d0d200e6fa 100644 --- a/packages/components/src/internal/components/editable/utils.test.ts +++ b/packages/components/src/internal/components/editable/utils.test.ts @@ -3,7 +3,7 @@ import { List, Map } from 'immutable'; import { QueryInfo } from '../../../public/QueryInfo'; import { QueryColumn, QueryLookup } from '../../../public/QueryColumn'; -import { DATE_RANGE_URI } from '../domainproperties/constants'; +import { DATE_RANGE_URI, NON_NEGATIVE_NUMBER_CONCEPT_URI } from '../domainproperties/constants'; import sampleSetQueryInfoJSON from '../../../test/data/sampleSetAllFieldTypes-getQueryDetails.json'; @@ -273,6 +273,38 @@ describe('getValidatedEditableGridValue', () => { }); }); + test('amount column', () => { + const amountCol = new QueryColumn({ jsonType: 'float', conceptURI: NON_NEGATIVE_NUMBER_CONCEPT_URI, caption: 'Amount' }); + + const validValues = [null, undefined, '', ' ', 0, 1.1e3, '100', '0.0', 1.11, '1.11', 123.456e2]; + validValues.forEach(value => { + expect(getValidatedEditableGridValue(value, amountCol)).toStrictEqual({ + message: undefined, + value: Utils.isString(value) ? value.trim() : value, + }); + }); + + const invalidValues = ['Bogus', true, NaN]; + invalidValues.forEach(value => { + expect(getValidatedEditableGridValue(value, amountCol)).toStrictEqual({ + message: { + message: 'Invalid decimal', + }, + value, + }); + }); + + const invalidNegative = ['-1', '-1.1', -1, -1.1, -1.1e3, -123.456e2]; + invalidNegative.forEach(value => { + expect(getValidatedEditableGridValue(value, amountCol)).toStrictEqual({ + message: { + message: 'Invalid Amount, must be non-negative', + }, + value, + }); + }); + }); + test('boolean column', () => { const boolCol = new QueryColumn({ jsonType: 'boolean' }); From e9193d744ec9ed77d59e8bb47cb51ecc71f6c9ac Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 17 Nov 2025 11:15:56 -0800 Subject: [PATCH 03/22] publish --- packages/components/package-lock.json | 4 ++-- .../src/internal/components/editable/utils.test.ts | 6 +++++- .../src/internal/components/forms/QueryFormInputs.tsx | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 5804df043c..d54b218219 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "6.70.5", + "version": "6.70.6-fb-amountValidation.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "6.70.5", + "version": "6.70.6-fb-amountValidation.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/src/internal/components/editable/utils.test.ts b/packages/components/src/internal/components/editable/utils.test.ts index d0d200e6fa..bc8fcf397b 100644 --- a/packages/components/src/internal/components/editable/utils.test.ts +++ b/packages/components/src/internal/components/editable/utils.test.ts @@ -274,7 +274,11 @@ describe('getValidatedEditableGridValue', () => { }); test('amount column', () => { - const amountCol = new QueryColumn({ jsonType: 'float', conceptURI: NON_NEGATIVE_NUMBER_CONCEPT_URI, caption: 'Amount' }); + const amountCol = new QueryColumn({ + jsonType: 'float', + conceptURI: NON_NEGATIVE_NUMBER_CONCEPT_URI, + caption: 'Amount', + }); const validValues = [null, undefined, '', ' ', 0, 1.1e3, '100', '0.0', 1.11, '1.11', 123.456e2]; validValues.forEach(value => { diff --git a/packages/components/src/internal/components/forms/QueryFormInputs.tsx b/packages/components/src/internal/components/forms/QueryFormInputs.tsx index 0f29ef8610..10fd1c13e8 100644 --- a/packages/components/src/internal/components/forms/QueryFormInputs.tsx +++ b/packages/components/src/internal/components/forms/QueryFormInputs.tsx @@ -225,6 +225,7 @@ export class QueryFormInputs extends React.Component ); } From a64608cc7082ea20a1e2b47d204a98c614798a96 Mon Sep 17 00:00:00 2001 From: XingY Date: Tue, 18 Nov 2025 15:41:12 -0800 Subject: [PATCH 04/22] Show amount and unit in same row during edit --- .../components/forms/QueryFormInputs.tsx | 3 +- .../forms/input/AmountUnitInput.tsx | 87 +++++++++++++++++++ .../forms/input/InputRenderFactory.ts | 2 + .../components/forms/input/SelectInput.tsx | 3 +- .../components/forms/input/TextInput.tsx | 3 +- .../internal/components/forms/input/types.ts | 2 + packages/components/src/public/QueryColumn.ts | 1 + 7 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 packages/components/src/internal/components/forms/input/AmountUnitInput.tsx diff --git a/packages/components/src/internal/components/forms/QueryFormInputs.tsx b/packages/components/src/internal/components/forms/QueryFormInputs.tsx index 10fd1c13e8..cefc4c35bd 100644 --- a/packages/components/src/internal/components/forms/QueryFormInputs.tsx +++ b/packages/components/src/internal/components/forms/QueryFormInputs.tsx @@ -194,7 +194,7 @@ export class QueryFormInputs extends React.Component filter(col)) + .filter(col => filter(col) && !col.removeFromFormInput) .map(col => { const { fieldKey, name, required } = col; const shouldDisableField = initiallyDisableFields || disabledFields.contains(name.toLowerCase()); @@ -237,6 +237,7 @@ export class QueryFormInputs extends React.Component ); } diff --git a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx new file mode 100644 index 0000000000..a9163b7b42 --- /dev/null +++ b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx @@ -0,0 +1,87 @@ +import React, { FC, memo, useCallback, useState } from 'react'; + +import { TextInput } from './TextInput'; +import { QuerySelect } from '../QuerySelect'; +import { getContainerFilterForLookups } from '../../../query/api'; +import { FieldLabel } from '../FieldLabel'; +import { InputRendererProps } from './types'; +import { caseInsensitive, generateId } from '../../../util/utils'; +import { FormsyInput } from './FormsyReactComponents'; + +export const AmountUnitInput: FC = memo(props => { + const { allowFieldDisable, onSelectChange, + onToggleDisable, initiallyDisabled, + containerFilter, containerPath, + allColumns, data } = props; + const [disabled, setDisabled] = useState(initiallyDisabled && allowFieldDisable); + + const id = generateId('selectinput-'); + const amountCol = allColumns.filter(col => col.name.toLowerCase() === 'storedamount').valueArray?.[0]; + const unitCol = allColumns.filter(col => col.name.toLowerCase() === 'units').valueArray?.[0]; + const amountValue = caseInsensitive(data, amountCol.name); + const unitValue = caseInsensitive(data, unitCol.name); + + const onToggleChange = useCallback(() => { + setDisabled(prevDisabled => { + const newDisabled = !prevDisabled; + onToggleDisable?.(newDisabled); + return newDisabled; + }); + }, [setDisabled]); + + return ( +
+ + + + {allowFieldDisable && !disabled && ( + + )} +
+ ); +}); + +AmountUnitInput.displayName = 'AmountUnitInput'; diff --git a/packages/components/src/internal/components/forms/input/InputRenderFactory.ts b/packages/components/src/internal/components/forms/input/InputRenderFactory.ts index fd69a13e4e..68945769b6 100644 --- a/packages/components/src/internal/components/forms/input/InputRenderFactory.ts +++ b/packages/components/src/internal/components/forms/input/InputRenderFactory.ts @@ -6,6 +6,7 @@ import { InputRendererProps } from './types'; import { AliasGridInput, AliasInput } from './AliasInput'; import { AppendUnitsInput } from './AppendUnitsInput'; import { SampleStatusInputRenderer } from './SampleStatusInput'; +import { AmountUnitInput } from './AmountUnitInput'; export type InputRendererComponent = ComponentType; export type InputRendererFactory = (col: QueryColumn, isGridInput?: boolean) => InputRendererComponent; @@ -48,4 +49,5 @@ export function registerInputRenderers(): void { registerInputRenderer('ExperimentAlias', AliasGridInput, InputRenderContext.Grid); registerInputRenderer('ExperimentAlias', AliasInput, InputRenderContext.Form); registerInputRenderer('SampleStatusInput', SampleStatusInputRenderer); + registerInputRenderer('AmountUnitInput', AmountUnitInput, InputRenderContext.Form); } diff --git a/packages/components/src/internal/components/forms/input/SelectInput.tsx b/packages/components/src/internal/components/forms/input/SelectInput.tsx index 9e1ef55381..1bf5ec0a80 100644 --- a/packages/components/src/internal/components/forms/input/SelectInput.tsx +++ b/packages/components/src/internal/components/forms/input/SelectInput.tsx @@ -267,6 +267,7 @@ export interface SelectInputProps { valueKey?: string; valueRenderer?: any; warning?: ReactNode; + disableInput?: boolean; } type SelectInputImplProps = SelectInputProps & FormsyInjectedProps; @@ -686,7 +687,7 @@ export class SelectInputImpl extends Component { id: this.getId(), inputId, isClearable: clearable, - isDisabled: disabled || this.state.isDisabled, + isDisabled: disabled || this.state.isDisabled || this.props.disableInput, isLoading, isMulti: multiple, isValidNewOption, diff --git a/packages/components/src/internal/components/forms/input/TextInput.tsx b/packages/components/src/internal/components/forms/input/TextInput.tsx index 8d081a7c8f..46b6f41859 100644 --- a/packages/components/src/internal/components/forms/input/TextInput.tsx +++ b/packages/components/src/internal/components/forms/input/TextInput.tsx @@ -34,6 +34,7 @@ export interface TextInputProps extends DisableableInputProps, Omit ReactNode; showLabel?: boolean; startFocused?: boolean; + disableInput?: boolean; } interface TextInputState extends DisableableInputState { @@ -142,7 +143,7 @@ export class TextInput extends DisableableInput required={queryColumn.required} {...inputProps} componentRef={this.textInput} - disabled={this.state.isDisabled} + disabled={this.state.isDisabled || this.props.disableInput} help={help} label={this.renderLabel()} labelClassName={showLabel ? labelClassName : 'hide-label'} diff --git a/packages/components/src/internal/components/forms/input/types.ts b/packages/components/src/internal/components/forms/input/types.ts index 7a3abedf33..d1ef46b6c1 100644 --- a/packages/components/src/internal/components/forms/input/types.ts +++ b/packages/components/src/internal/components/forms/input/types.ts @@ -5,6 +5,7 @@ import { Query } from '@labkey/api'; import { QueryColumn } from '../../../../public/QueryColumn'; import { SelectInputChange, SelectInputProps } from './SelectInput'; +import { ExtendedMap } from '../../../../public/ExtendedMap'; export interface InputRendererProps { allowFieldDisable?: boolean; @@ -25,4 +26,5 @@ export interface InputRendererProps { showLabel?: boolean; value: any; values?: any; + allColumns?: ExtendedMap; } diff --git a/packages/components/src/public/QueryColumn.ts b/packages/components/src/public/QueryColumn.ts index e241ecb1b5..151103af0f 100644 --- a/packages/components/src/public/QueryColumn.ts +++ b/packages/components/src/public/QueryColumn.ts @@ -189,6 +189,7 @@ export class QueryColumn implements IQueryColumn { declare inputRenderer: string; declare sorts: '+' | '-'; declare removeFromViews: boolean; // strips this column from all ViewInfo definitions + declare removeFromFormInput: boolean; // strips this column from QueryFormInputs declare units: string; declare derivationDataScope: string; From f3f6e79a451b0c8823128a0c5459558eb55f7dfc Mon Sep 17 00:00:00 2001 From: XingY Date: Tue, 18 Nov 2025 15:46:27 -0800 Subject: [PATCH 05/22] Show amount and unit in same row during edit --- packages/components/package-lock.json | 4 +- .../components/releaseNotes/components.md | 5 ++- packages/components/src/index.ts | 2 + .../components/forms/QueryFormInputs.tsx | 2 +- .../forms/input/AmountUnitInput.tsx | 44 ++++++++++--------- .../components/forms/input/SelectInput.tsx | 12 ++--- .../components/forms/input/TextInput.tsx | 9 ++-- .../internal/components/forms/input/types.ts | 2 +- 8 files changed, 42 insertions(+), 38 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index ffeca881d7..7cd205bba9 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "6.70.7", + "version": "6.70.8-fb-amountValidation.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "6.70.7", + "version": "6.70.8-fb-amountValidation.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 59069bbd31..39da4621c6 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -3,8 +3,9 @@ Components, models, actions, and utility functions for LabKey applications and p ### version 6.X *Released*: X November 2025 -- Disallow negative sample amounts - - Update `getValidatedEditableGridValue` to check for negative amount values +- Sample Amount/Units polish + - Disallow negative sample amounts: Update `getValidatedEditableGridValue` to check for negative amount values + - Added `AmountUnitInput` to show amount/units input side-by-side on bulk add/edit ### version 6.70.7 *Released*: 18 November 2025 diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index d2dee15827..a1e7354d2c 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -474,6 +474,7 @@ import { RANGE_URIS, SAMPLE_TYPE_CONCEPT_URI, STORAGE_UNIQUE_ID_CONCEPT_URI, + NON_NEGATIVE_NUMBER_CONCEPT_URI, } from './internal/components/domainproperties/constants'; import { ExpandableContainer } from './internal/components/ExpandableContainer'; import { Principal, SecurityAssignment, SecurityPolicy, SecurityRole } from './internal/components/permissions/models'; @@ -1678,6 +1679,7 @@ export { SplitButton, splitDateTimeFormat, STORAGE_UNIQUE_ID_CONCEPT_URI, + NON_NEGATIVE_NUMBER_CONCEPT_URI, StorageAmountInput, StorageStatusRenderer, STORED_AMOUNT_FIELDS, diff --git a/packages/components/src/internal/components/forms/QueryFormInputs.tsx b/packages/components/src/internal/components/forms/QueryFormInputs.tsx index cefc4c35bd..d6874d6c6f 100644 --- a/packages/components/src/internal/components/forms/QueryFormInputs.tsx +++ b/packages/components/src/internal/components/forms/QueryFormInputs.tsx @@ -223,6 +223,7 @@ export class QueryFormInputs extends React.Component ); } diff --git a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx index a9163b7b42..6af3b13268 100644 --- a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx +++ b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx @@ -9,10 +9,16 @@ import { caseInsensitive, generateId } from '../../../util/utils'; import { FormsyInput } from './FormsyReactComponents'; export const AmountUnitInput: FC = memo(props => { - const { allowFieldDisable, onSelectChange, - onToggleDisable, initiallyDisabled, - containerFilter, containerPath, - allColumns, data } = props; + const { + allowFieldDisable, + onSelectChange, + onToggleDisable, + initiallyDisabled, + containerFilter, + containerPath, + allColumns, + data, + } = props; const [disabled, setDisabled] = useState(initiallyDisabled && allowFieldDisable); const id = generateId('selectinput-'); @@ -32,50 +38,46 @@ export const AmountUnitInput: FC = memo(props => { return (
{allowFieldDisable && !disabled && ( diff --git a/packages/components/src/internal/components/forms/input/SelectInput.tsx b/packages/components/src/internal/components/forms/input/SelectInput.tsx index 1bf5ec0a80..5003879301 100644 --- a/packages/components/src/internal/components/forms/input/SelectInput.tsx +++ b/packages/components/src/internal/components/forms/input/SelectInput.tsx @@ -145,7 +145,7 @@ export type SelectInputChange = ( // Copied from @types/react-select/src/Select.d.ts export type FilterOption = ((option: SelectInputOption, rawInput: string) => boolean) | null; -function initOptionFromPrimitive(value: string | number, props: SelectInputProps): SelectInputOption { +function initOptionFromPrimitive(value: number | string, props: SelectInputProps): SelectInputOption { const { labelKey = 'label', options, valueKey = 'value' } = props; const result = options?.find(o => o[valueKey] === value); if (result) return result; @@ -205,8 +205,8 @@ export interface SelectInputProps { autoValue?: boolean; backspaceRemovesValue?: boolean; cacheOptions?: boolean; - clearCacheOnChange?: boolean; clearable?: boolean; + clearCacheOnChange?: boolean; closeMenuOnSelect?: boolean; containerClass?: string; customStyles?: Record; @@ -216,6 +216,7 @@ export interface SelectInputProps { delimiter?: string; description?: string; disabled?: boolean; + disableInput?: boolean; filterOption?: FilterOption; formatCreateLabel?: (inputValue: string) => ReactNode; formatGroupLabel?: (data: any) => ReactNode; @@ -267,10 +268,9 @@ export interface SelectInputProps { valueKey?: string; valueRenderer?: any; warning?: ReactNode; - disableInput?: boolean; } -type SelectInputImplProps = SelectInputProps & FormsyInjectedProps; +type SelectInputImplProps = FormsyInjectedProps & SelectInputProps; interface State { // This state property is used in conjunction with the prop "clearCacheOnChange" which when true @@ -538,8 +538,9 @@ export class SelectInputImpl extends Component { return ( { }} showLabel={showLabel} showToggle={allowDisable} - isDisabled={isDisabled} toggleProps={{ onClick: toggleDisabledTooltip ? undefined : this.onToggleChange, toolTip: toggleDisabledTooltip, diff --git a/packages/components/src/internal/components/forms/input/TextInput.tsx b/packages/components/src/internal/components/forms/input/TextInput.tsx index 46b6f41859..a0e136a032 100644 --- a/packages/components/src/internal/components/forms/input/TextInput.tsx +++ b/packages/components/src/internal/components/forms/input/TextInput.tsx @@ -27,6 +27,7 @@ import { InternalSpacesWarning } from '../InternalSpacesWarning'; export interface TextInputProps extends DisableableInputProps, Omit { addLabelAsterisk?: boolean; + disableInput?: boolean; includeSpacesWarning?: boolean; isUpdate?: boolean; onChange?: (value: any) => void; @@ -34,7 +35,6 @@ export interface TextInputProps extends DisableableInputProps, Omit ReactNode; showLabel?: boolean; startFocused?: boolean; - disableInput?: boolean; } interface TextInputState extends DisableableInputState { @@ -92,12 +92,12 @@ export class TextInput extends DisableableInput return ( ...inputProps } = rest; - let help: string; // Issue 52367: Don't show the message if we have a name that can be edited if (queryColumn.nameExpression && !isUpdate) { diff --git a/packages/components/src/internal/components/forms/input/types.ts b/packages/components/src/internal/components/forms/input/types.ts index d1ef46b6c1..1ecbed14b3 100644 --- a/packages/components/src/internal/components/forms/input/types.ts +++ b/packages/components/src/internal/components/forms/input/types.ts @@ -8,6 +8,7 @@ import { SelectInputChange, SelectInputProps } from './SelectInput'; import { ExtendedMap } from '../../../../public/ExtendedMap'; export interface InputRendererProps { + allColumns?: ExtendedMap; allowFieldDisable?: boolean; col: QueryColumn; containerFilter?: Query.ContainerFilter; @@ -26,5 +27,4 @@ export interface InputRendererProps { showLabel?: boolean; value: any; values?: any; - allColumns?: ExtendedMap; } From 061973aff0e23052b35b597b2fd443810d040544 Mon Sep 17 00:00:00 2001 From: XingY Date: Tue, 18 Nov 2025 16:08:48 -0800 Subject: [PATCH 06/22] Show amount and unit in same row during edit --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- packages/components/src/index.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 7cd205bba9..9fbe9c7630 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "6.70.8-fb-amountValidation.1", + "version": "6.70.8-fb-amountValidation.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "6.70.8-fb-amountValidation.1", + "version": "6.70.8-fb-amountValidation.2", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 51435d4cd7..205338ca4e 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "6.70.8-fb-amountValidation.1", + "version": "6.70.8-fb-amountValidation.2", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index a1e7354d2c..da22ed2dd6 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -471,10 +471,10 @@ import { DOMAIN_FIELD_REQUIRED, DOMAIN_FIELD_TYPE, DOMAIN_RANGE_VALIDATOR, + NON_NEGATIVE_NUMBER_CONCEPT_URI, RANGE_URIS, SAMPLE_TYPE_CONCEPT_URI, STORAGE_UNIQUE_ID_CONCEPT_URI, - NON_NEGATIVE_NUMBER_CONCEPT_URI, } from './internal/components/domainproperties/constants'; import { ExpandableContainer } from './internal/components/ExpandableContainer'; import { Principal, SecurityAssignment, SecurityPolicy, SecurityRole } from './internal/components/permissions/models'; @@ -1536,6 +1536,7 @@ export { NavigationBar, NO_UPDATES_MESSAGE, NoLinkRenderer, + NON_NEGATIVE_NUMBER_CONCEPT_URI, NOT_ANY_FILTER_TYPE, NOT_IN_EXP_DESCENDANTS_OF_FILTER_TYPE, Notifications, @@ -1679,7 +1680,6 @@ export { SplitButton, splitDateTimeFormat, STORAGE_UNIQUE_ID_CONCEPT_URI, - NON_NEGATIVE_NUMBER_CONCEPT_URI, StorageAmountInput, StorageStatusRenderer, STORED_AMOUNT_FIELDS, From 69becc7ea6f14f0f255c2f7413669f30213ef6c3 Mon Sep 17 00:00:00 2001 From: XingY Date: Tue, 18 Nov 2025 17:44:46 -0800 Subject: [PATCH 07/22] Enable Amount and Units together in Sample Type Designer --- .../components/domainproperties/DomainForm.tsx | 10 ++++++++-- .../domainproperties/samples/SampleTypeDesigner.tsx | 1 + .../src/internal/components/forms/QueryFormInputs.tsx | 1 + .../components/forms/input/AmountUnitInput.tsx | 7 +++++++ .../src/internal/components/forms/input/types.ts | 4 +++- 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/components/src/internal/components/domainproperties/DomainForm.tsx b/packages/components/src/internal/components/domainproperties/DomainForm.tsx index 14b1faa9d7..176dbf119c 100644 --- a/packages/components/src/internal/components/domainproperties/DomainForm.tsx +++ b/packages/components/src/internal/components/domainproperties/DomainForm.tsx @@ -248,6 +248,8 @@ export interface DomainFormProps extends PropsWithChildren { systemFields?: SystemField[]; todoIconHelpMsg?: string; validate?: boolean; + // Map of grouped system fields, that should be enabled/disabled together + groupedSystemFields?: Record; } interface State { @@ -701,8 +703,12 @@ export class DomainFormImpl extends React.PureComponent }; onSystemFieldEnable = (field: string, enable: boolean): void => { - const { domain } = this.props; - this.onDomainChange(handleSystemFieldUpdates(domain, field, enable)); + const { domain, groupedSystemFields } = this.props; + let updatedDomain = handleSystemFieldUpdates(domain, field, enable); + groupedSystemFields?.[field.toLowerCase()]?.forEach(groupedField => { + updatedDomain = handleSystemFieldUpdates(updatedDomain, groupedField, enable); + }); + this.onDomainChange(updatedDomain); }; onFieldsChange = (changes: List, index: number, expand: boolean, skipDirtyCheck = false): void => { diff --git a/packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.tsx b/packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.tsx index 81a6077c80..1e100664f4 100644 --- a/packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.tsx +++ b/packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.tsx @@ -765,6 +765,7 @@ export class SampleTypeDesignerImpl extends React.PureComponent ); } diff --git a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx index 6af3b13268..3cbc33f750 100644 --- a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx +++ b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx @@ -1,4 +1,5 @@ import React, { FC, memo, useCallback, useState } from 'react'; +import { List } from 'immutable'; import { TextInput } from './TextInput'; import { QuerySelect } from '../QuerySelect'; @@ -7,6 +8,7 @@ import { FieldLabel } from '../FieldLabel'; import { InputRendererProps } from './types'; import { caseInsensitive, generateId } from '../../../util/utils'; import { FormsyInput } from './FormsyReactComponents'; +import { Operation } from '../../../../public/QueryColumn'; export const AmountUnitInput: FC = memo(props => { const { @@ -18,6 +20,7 @@ export const AmountUnitInput: FC = memo(props => { containerPath, allColumns, data, + queryFilters, } = props; const [disabled, setDisabled] = useState(initiallyDisabled && allowFieldDisable); @@ -26,6 +29,9 @@ export const AmountUnitInput: FC = memo(props => { const unitCol = allColumns.filter(col => col.name.toLowerCase() === 'units').valueArray?.[0]; const amountValue = caseInsensitive(data, amountCol.name); const unitValue = caseInsensitive(data, unitCol.name); + const queryFilter = unitCol.lookup.hasQueryFilters(Operation.insert) + ? List(unitCol.lookup.getQueryFilters(Operation.insert)) + : queryFilters?.[unitCol.fieldKey]; const onToggleChange = useCallback(() => { setDisabled(prevDisabled => { @@ -78,6 +84,7 @@ export const AmountUnitInput: FC = memo(props => { showLabel={false} value={unitValue} valueColumn={unitCol.lookup.keyColumn} + queryFilters={queryFilter} /> {allowFieldDisable && !disabled && ( diff --git a/packages/components/src/internal/components/forms/input/types.ts b/packages/components/src/internal/components/forms/input/types.ts index 1ecbed14b3..4e1e54727f 100644 --- a/packages/components/src/internal/components/forms/input/types.ts +++ b/packages/components/src/internal/components/forms/input/types.ts @@ -1,6 +1,7 @@ import { ReactNode } from 'react'; +import { List } from 'immutable'; -import { Query } from '@labkey/api'; +import { Filter, Query } from '@labkey/api'; import { QueryColumn } from '../../../../public/QueryColumn'; @@ -27,4 +28,5 @@ export interface InputRendererProps { showLabel?: boolean; value: any; values?: any; + queryFilters?: Record>; } From 53ab68ecd643fe101343d517d70683e4f64587b6 Mon Sep 17 00:00:00 2001 From: XingY Date: Tue, 18 Nov 2025 17:48:34 -0800 Subject: [PATCH 08/22] Enable Amount and Units together in Sample Type Designer --- packages/components/package-lock.json | 4 +- packages/components/package.json | 2 +- .../domainproperties/DomainForm.tsx | 124 ++++++++-------- .../samples/SampleTypeDesigner.tsx | 132 +++++++++--------- .../components/forms/QueryFormInputs.tsx | 2 +- .../forms/input/AmountUnitInput.tsx | 2 +- .../internal/components/forms/input/types.ts | 2 +- 7 files changed, 134 insertions(+), 134 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 9fbe9c7630..0c3e844166 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "6.70.8-fb-amountValidation.2", + "version": "6.70.8-fb-amountValidation.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "6.70.8-fb-amountValidation.2", + "version": "6.70.8-fb-amountValidation.3", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 205338ca4e..3acd23f1e7 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "6.70.8-fb-amountValidation.2", + "version": "6.70.8-fb-amountValidation.3", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/src/internal/components/domainproperties/DomainForm.tsx b/packages/components/src/internal/components/domainproperties/DomainForm.tsx index 176dbf119c..d60544fc96 100644 --- a/packages/components/src/internal/components/domainproperties/DomainForm.tsx +++ b/packages/components/src/internal/components/domainproperties/DomainForm.tsx @@ -145,27 +145,27 @@ const DomainFormToolbar: FC = memo(props => {
{!domainFormDisplayOptions?.hideAddFieldsButton && ( )} Delete {shouldShowImportExport && ( Export @@ -175,26 +175,26 @@ const DomainFormToolbar: FC = memo(props => {
{!valueIsEmpty(search) && ( - Showing {fields.filter(f => f.visible).size} of {fields.size} {' '} - field{fields.size > 1 ? 's' : ''}. + Showing {fields.filter(f => f.visible).size} of {fields.size} field + {fields.size > 1 ? 's' : ''}. )}
Mode:
@@ -215,6 +215,8 @@ export interface DomainFormProps extends PropsWithChildren { domainFormDisplayOptions?: IDomainFormDisplayOptions; domainIndex?: number; fieldsAdditionalRenderer?: () => ReactNode; + // Map of grouped system fields, that should be enabled/disabled together + groupedSystemFields?: Record; headerPrefix?: string; // used as a string to remove from the heading when using the domain.name headerTitle?: string; helpNoun?: string; @@ -248,8 +250,6 @@ export interface DomainFormProps extends PropsWithChildren { systemFields?: SystemField[]; todoIconHelpMsg?: string; validate?: boolean; - // Map of grouped system fields, that should be enabled/disabled together - groupedSystemFields?: Record; } interface State { @@ -602,10 +602,10 @@ export class DomainFormImpl extends React.PureComponent if (deletableSelectedFieldsCount === 0) { return (

None of the selected fields can be deleted.

@@ -621,11 +621,11 @@ export class DomainFormImpl extends React.PureComponent return (

{howManyDeleted} will be deleted.

@@ -927,9 +927,9 @@ export class DomainFormImpl extends React.PureComponent
@@ -961,11 +961,11 @@ export class DomainFormImpl extends React.PureComponent const fieldName = field && field.name && field.name.trim().length > 0 ? {field.name} : 'this field'; return ( this.onDeleteConfirm(confirmDeleteRowIndex)} - onCancel={this.onConfirmCancel} confirmClass="btn-danger" confirmText="Yes, Remove Field" + onCancel={this.onConfirmCancel} + onConfirm={() => this.onDeleteConfirm(confirmDeleteRowIndex)} + title="Confirm Remove Field" >
Are you sure you want to remove {fieldName}?{' '} @@ -1094,11 +1094,11 @@ export class DomainFormImpl extends React.PureComponent <> domainKindName: domain.domainKindName, } } - fileSpecificCallback={Map({ '.json': this.importFieldsFromJson })} + showAcceptedFormats /> {shouldShowInferFromFile && this.state.filePreviewMsg && ( {this.state.filePreviewMsg} @@ -1203,7 +1203,7 @@ export class DomainFormImpl extends React.PureComponent visibleFieldsCount !== 0 && visibleSelection.size === visibleFieldsCount ? 'Clear All' : 'Clear'; return ( - +
{reservedFieldsMsg}
@@ -1223,10 +1223,10 @@ export class DomainFormImpl extends React.PureComponent
@@ -1261,39 +1261,39 @@ export class DomainFormImpl extends React.PureComponent return ( { - this.refsArray[i] = ref; - }} + allowUniqueConstraintProperties={domain.allowUniqueConstraintProperties} + appPropertiesOnly={appPropertiesOnly} + availableTypes={availableTypes} + defaultDefaultValueType={domain.defaultDefaultValueType} + defaultValueOptions={domain.defaultValueOptions} + domainContainerPath={domain.container} + domainFormDisplayOptions={domainFormDisplayOptions} domainId={domain.domainId} - helpNoun={helpNoun} - key={key} + domainIndex={domainIndex} + dragging={dragId === i} + expanded={expandedRowIndex === i} field={field} + fieldDetailsInfo={fieldDetails.detailsInfo} fieldError={this.getFieldError(domain, i)} getDomainFields={this.getDomainFields} - fieldDetailsInfo={fieldDetails.detailsInfo} - domainIndex={domainIndex} + helpNoun={helpNoun} index={i} - expanded={expandedRowIndex === i} - onChange={this.onFieldsChange} - onExpand={this.onFieldExpandToggle} - onDelete={this.onDeleteField} - maxPhiLevel={maxPhiLevel} - dragging={dragId === i} - availableTypes={availableTypes} - allowUniqueConstraintProperties={domain.allowUniqueConstraintProperties} - showDefaultValueSettings={domain.showDefaultValueSettings} - defaultDefaultValueType={domain.defaultDefaultValueType} - defaultValueOptions={domain.defaultValueOptions} - appPropertiesOnly={appPropertiesOnly} isDragDisabled={ !valueIsEmpty(search) || domainFormDisplayOptions.isDragDisabled || field.isCalculatedField() } - domainFormDisplayOptions={domainFormDisplayOptions} - domainContainerPath={domain.container} - schemaName={schemaName ?? domain.schemaName} + key={key} + maxPhiLevel={maxPhiLevel} + onChange={this.onFieldsChange} + onDelete={this.onDeleteField} + onExpand={this.onFieldExpandToggle} queryName={queryName ?? domain.queryName} + ref={ref => { + this.refsArray[i] = ref; + }} + schemaName={schemaName ?? domain.schemaName} + showDefaultValueSettings={domain.showDefaultValueSettings} /> ); }) @@ -1360,17 +1360,17 @@ export class DomainFormImpl extends React.PureComponent
{showHeader && ( {children} @@ -1383,8 +1383,8 @@ export class DomainFormImpl extends React.PureComponent <> {systemFields && ( )} @@ -1410,7 +1410,7 @@ export class DomainFormImpl extends React.PureComponent
{helpTopic && (
- + Learn more about this tool
@@ -1457,10 +1457,10 @@ export class DomainFormImpl extends React.PureComponent {filePreviewData && !domainFormDisplayOptions?.hideImportData && ( )}
@@ -1468,8 +1468,8 @@ export class DomainFormImpl extends React.PureComponent
{hasException && domain.domainException.severity === SEVERITY_LEVEL_ERROR && (
{domain.domainException.exception}
diff --git a/packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.tsx b/packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.tsx index 1e100664f4..15c7885e52 100644 --- a/packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.tsx +++ b/packages/components/src/internal/components/domainproperties/samples/SampleTypeDesigner.tsx @@ -29,7 +29,7 @@ import { ComponentsAPIWrapper, getDefaultAPIWrapper } from '../../../APIWrapper' import { GENID_SYNTAX_STRING } from '../NameExpressionGenIdBanner'; -import { IImportAlias, IParentAlias, IParentOption, FolderConfigurableDataType } from '../../entities/models'; +import { FolderConfigurableDataType, IImportAlias, IParentAlias, IParentOption } from '../../entities/models'; import { SCHEMAS } from '../../../schemas'; import { getHelpLink, @@ -115,10 +115,10 @@ interface Props { onCancel: () => void; onChange?: (model: SampleTypeModel) => void; onComplete: (response: DomainDesign) => void; + sampleAliasCaption?: string; sampleTypeCaption?: string; saveBtnText?: string; showAliquotOptions?: boolean; - sampleAliasCaption?: string; showLinkToStudy?: boolean; showParentLabelPrefix?: boolean; useSeparateDataClassesAliasMenu?: boolean; @@ -138,7 +138,7 @@ interface State { uniqueIdsConfirmed: boolean; } // Exported for testing -export class SampleTypeDesignerImpl extends React.PureComponent { +export class SampleTypeDesignerImpl extends React.PureComponent { static defaultProps = { api: getDefaultAPIWrapper(), defaultSampleFieldConfig: DEFAULT_SAMPLE_FIELD_CONFIG, @@ -156,7 +156,7 @@ export class SampleTypeDesignerImpl extends React.PureComponent; + return ; }; getNumNewUniqueIdFields(): number { @@ -558,7 +558,7 @@ export class SampleTypeDesignerImpl extends React.PureComponent field.isNew() && field.isUniqueIdField()).count(); } - getDomainDetails = (): { [key: string]: any } => { + getDomainDetails = (): Record => { const { model } = this.state; const { @@ -683,56 +683,31 @@ export class SampleTypeDesignerImpl extends React.PureComponent - + {appPropertiesOnly && allowFolderExclusion && ( // appPropertiesOnly check will prevent this panel from showing in LKS and in LKB media types )} {error &&
{error && {error}}
} {showUniqueIdConfirmation && ( {confirmModalMessage} )} ); diff --git a/packages/components/src/internal/components/forms/QueryFormInputs.tsx b/packages/components/src/internal/components/forms/QueryFormInputs.tsx index a62672c71f..152f44a6bb 100644 --- a/packages/components/src/internal/components/forms/QueryFormInputs.tsx +++ b/packages/components/src/internal/components/forms/QueryFormInputs.tsx @@ -234,11 +234,11 @@ export class QueryFormInputs extends React.Component ); } diff --git a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx index 3cbc33f750..a4f7ce202f 100644 --- a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx +++ b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx @@ -80,11 +80,11 @@ export const AmountUnitInput: FC = memo(props => { name={unitCol.fieldKey} onQSChange={onSelectChange} placeholder="Select or type to search..." + queryFilters={queryFilter} schemaQuery={unitCol.lookup.schemaQuery} showLabel={false} value={unitValue} valueColumn={unitCol.lookup.keyColumn} - queryFilters={queryFilter} /> {allowFieldDisable && !disabled && ( diff --git a/packages/components/src/internal/components/forms/input/types.ts b/packages/components/src/internal/components/forms/input/types.ts index 4e1e54727f..abfe2a7200 100644 --- a/packages/components/src/internal/components/forms/input/types.ts +++ b/packages/components/src/internal/components/forms/input/types.ts @@ -22,11 +22,11 @@ export interface InputRendererProps { onAdditionalFormDataChange?: (name: string, value: any) => void; onSelectChange?: SelectInputChange; onToggleDisable?: (disabled: boolean) => void; + queryFilters?: Record>; renderLabelField?: (col: QueryColumn) => ReactNode; selectInputProps?: Omit; showAsteriskSymbol?: boolean; showLabel?: boolean; value: any; values?: any; - queryFilters?: Record>; } From 38bb8f783a2885587e0b55b90b98880aa595d06f Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 19 Nov 2025 14:51:13 -0800 Subject: [PATCH 09/22] Fix bulk update error --- .../src/internal/components/forms/input/AmountUnitInput.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx index a4f7ce202f..01719ea2b0 100644 --- a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx +++ b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx @@ -49,7 +49,7 @@ export const AmountUnitInput: FC = memo(props => { isDisabled={disabled} labelOverlayProps={{ inputId: amountCol.name, - description: 'TODO', + description: 'The amount of this sample, in the display unit for the sample type, currently on hand.', label: 'Amount and Units', isFormsy: false, }} @@ -86,8 +86,8 @@ export const AmountUnitInput: FC = memo(props => { value={unitValue} valueColumn={unitCol.lookup.keyColumn} /> - {allowFieldDisable && !disabled && ( - + {allowFieldDisable && ( + )}
); From d5a1986bd143aaae0a40ba5fb2b83cd413301080 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 19 Nov 2025 14:57:18 -0800 Subject: [PATCH 10/22] merge from develop --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 9e1d8eea96..141a6802f1 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "6.70.8", + "version": "6.70.9-fb-amountValidation.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "6.70.8", + "version": "6.70.9-fb-amountValidation.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 30d6f24d4c..c9b526ced1 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "6.70.8", + "version": "6.70.9-fb-amountValidation.1", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 78af88b6362711056538b6512c3850962d9fed61 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 19 Nov 2025 16:32:19 -0800 Subject: [PATCH 11/22] bug fixes --- .../components/src/internal/components/editable/Cell.tsx | 5 ++++- .../src/internal/components/forms/input/AmountUnitInput.tsx | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/components/src/internal/components/editable/Cell.tsx b/packages/components/src/internal/components/editable/Cell.tsx index 95839e694c..5c9f949393 100644 --- a/packages/components/src/internal/components/editable/Cell.tsx +++ b/packages/components/src/internal/components/editable/Cell.tsx @@ -44,6 +44,7 @@ import { } from './utils'; import { LookupCell } from './LookupCell'; import { DateInputCell } from './DateInputCell'; +import { NON_NEGATIVE_NUMBER_CONCEPT_URI } from '../domainproperties/constants'; // CSS Order: top, right, bottom, left export type BorderMask = [boolean, boolean, boolean, boolean]; @@ -532,6 +533,8 @@ export class Cell extends React.PureComponent { .filter(vd => vd && vd.display !== undefined) .reduce((v, vd, i) => v + (i > 0 ? ', ' : '') + vd.display, ''); + const showMenu = (showLookup || !!col.inputRenderer) && (NON_NEGATIVE_NUMBER_CONCEPT_URI !== col?.conceptURI); + return ( <> { placeholder={placeholder} selected={selected} selection={selection} - showMenu={showLookup || !!col.inputRenderer} + showMenu={showMenu} targetRef={this.displayEl} /> {renderDragHandle && !this.isReadOnly && ( diff --git a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx index 01719ea2b0..fafd414aae 100644 --- a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx +++ b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx @@ -41,6 +41,10 @@ export const AmountUnitInput: FC = memo(props => { }); }, [setDisabled]); + if (!amountCol || !unitCol) { + return null; + } + return (
= memo(props => { isDisabled={disabled} labelOverlayProps={{ inputId: amountCol.name, - description: 'The amount of this sample, in the display unit for the sample type, currently on hand.', + description: 'The amount and units of this sample, currently on hand.', label: 'Amount and Units', isFormsy: false, }} From 1cf5e19434a1ab1ee9703007c0471206403a5a99 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 19 Nov 2025 16:39:28 -0800 Subject: [PATCH 12/22] bug fixes --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 141a6802f1..aee94b22cd 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "6.70.9-fb-amountValidation.1", + "version": "6.70.9-fb-amountValidation.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "6.70.9-fb-amountValidation.1", + "version": "6.70.9-fb-amountValidation.2", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index c9b526ced1..1f7fea8ffc 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "6.70.9-fb-amountValidation.1", + "version": "6.70.9-fb-amountValidation.2", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From b5a909dc3be564dc50e032368d853382eb27427a Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 19 Nov 2025 18:01:21 -0800 Subject: [PATCH 13/22] Fix media detail edit --- .../src/internal/components/forms/detail/DetailDisplay.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/src/internal/components/forms/detail/DetailDisplay.tsx b/packages/components/src/internal/components/forms/detail/DetailDisplay.tsx index cf3e08ad87..8b20917062 100644 --- a/packages/components/src/internal/components/forms/detail/DetailDisplay.tsx +++ b/packages/components/src/internal/components/forms/detail/DetailDisplay.tsx @@ -40,7 +40,7 @@ import { UserDetailsRenderer } from '../../../renderers/UserDetailsRenderer'; import { ExpirationDateColumnRenderer } from '../../../renderers/ExpirationDateColumnRenderer'; import { getContainerFilterForLookups } from '../../../query/api'; import { FolderColumnRenderer } from '../../../renderers/FolderColumnRenderer'; -import { FILELINK_RANGE_URI } from '../../domainproperties/constants'; +import { FILELINK_RANGE_URI, NON_NEGATIVE_NUMBER_CONCEPT_URI } from '../../domainproperties/constants'; import { LOOKUP_DEFAULT_SIZE } from '../../../constants'; export type Renderer = (data: any, row?: any) => ReactNode; @@ -294,7 +294,7 @@ export function resolveDetailEditRenderer( } let value = resolveDetailFieldValue(data); - const ColumnInputRenderer = resolveInputRenderer(col); + const ColumnInputRenderer = col.conceptURI === NON_NEGATIVE_NUMBER_CONCEPT_URI ? null /* Use standard forminput for media amount/unit */ : resolveInputRenderer(col); if (ColumnInputRenderer) { return ( Date: Wed, 19 Nov 2025 18:10:40 -0800 Subject: [PATCH 14/22] fix tests --- packages/components/package-lock.json | 4 +- packages/components/package.json | 2 +- .../domainproperties/DomainForm.tsx | 120 +++++++++--------- .../components/forms/detail/DetailDisplay.tsx | 5 +- .../forms/input/AmountUnitInput.tsx | 2 +- 5 files changed, 68 insertions(+), 65 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index aee94b22cd..f7df9b6c16 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "6.70.9-fb-amountValidation.2", + "version": "6.70.9-fb-amountValidation.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "6.70.9-fb-amountValidation.2", + "version": "6.70.9-fb-amountValidation.3", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 1f7fea8ffc..1a9707d263 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "6.70.9-fb-amountValidation.2", + "version": "6.70.9-fb-amountValidation.3", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/src/internal/components/domainproperties/DomainForm.tsx b/packages/components/src/internal/components/domainproperties/DomainForm.tsx index df4248d1ab..182c5756c7 100644 --- a/packages/components/src/internal/components/domainproperties/DomainForm.tsx +++ b/packages/components/src/internal/components/domainproperties/DomainForm.tsx @@ -146,28 +146,28 @@ const DomainFormToolbar: FC = memo(props => {
{!domainFormDisplayOptions?.hideAddFieldsButton && ( )} {!domainFormDisplayOptions?.hideAddFieldsButton && ( Delete )} {shouldShowImportExport && ( Export @@ -177,26 +177,26 @@ const DomainFormToolbar: FC = memo(props => {
{!valueIsEmpty(search) && ( - Showing {fields.filter(f => f.visible).size} of {fields.size} {' '} - field{fields.size > 1 ? 's' : ''}. + Showing {fields.filter(f => f.visible).size} of {fields.size} field + {fields.size > 1 ? 's' : ''}. )}
Mode:
@@ -604,10 +604,10 @@ export class DomainFormImpl extends React.PureComponent if (deletableSelectedFieldsCount === 0) { return (

None of the selected fields can be deleted.

@@ -623,11 +623,11 @@ export class DomainFormImpl extends React.PureComponent return (

{howManyDeleted} will be deleted.

@@ -929,9 +929,9 @@ export class DomainFormImpl extends React.PureComponent
@@ -963,11 +963,11 @@ export class DomainFormImpl extends React.PureComponent const fieldName = field && field.name && field.name.trim().length > 0 ? {field.name} : 'this field'; return ( this.onDeleteConfirm(confirmDeleteRowIndex)} - onCancel={this.onConfirmCancel} confirmClass="btn-danger" confirmText="Yes, Remove Field" + onCancel={this.onConfirmCancel} + onConfirm={() => this.onDeleteConfirm(confirmDeleteRowIndex)} + title="Confirm Remove Field" >
Are you sure you want to remove {fieldName}?{' '} @@ -1096,11 +1096,11 @@ export class DomainFormImpl extends React.PureComponent <> domainKindName: domain.domainKindName, } } - fileSpecificCallback={Map({ '.json': this.importFieldsFromJson })} + showAcceptedFormats /> {shouldShowInferFromFile && this.state.filePreviewMsg && ( {this.state.filePreviewMsg} @@ -1205,7 +1205,7 @@ export class DomainFormImpl extends React.PureComponent visibleFieldsCount !== 0 && visibleSelection.size === visibleFieldsCount ? 'Clear All' : 'Clear'; return ( - +
{reservedFieldsMsg}
@@ -1225,10 +1225,10 @@ export class DomainFormImpl extends React.PureComponent
@@ -1263,39 +1263,39 @@ export class DomainFormImpl extends React.PureComponent return ( { - this.refsArray[i] = ref; - }} + allowUniqueConstraintProperties={domain.allowUniqueConstraintProperties} + appPropertiesOnly={appPropertiesOnly} + availableTypes={availableTypes} + defaultDefaultValueType={domain.defaultDefaultValueType} + defaultValueOptions={domain.defaultValueOptions} + domainContainerPath={domain.container} + domainFormDisplayOptions={domainFormDisplayOptions} domainId={domain.domainId} - helpNoun={helpNoun} - key={key} + domainIndex={domainIndex} + dragging={dragId === i} + expanded={expandedRowIndex === i} field={field} + fieldDetailsInfo={fieldDetails.detailsInfo} fieldError={this.getFieldError(domain, i)} getDomainFields={this.getDomainFields} - fieldDetailsInfo={fieldDetails.detailsInfo} - domainIndex={domainIndex} + helpNoun={helpNoun} index={i} - expanded={expandedRowIndex === i} - onChange={this.onFieldsChange} - onExpand={this.onFieldExpandToggle} - onDelete={this.onDeleteField} - maxPhiLevel={maxPhiLevel} - dragging={dragId === i} - availableTypes={availableTypes} - allowUniqueConstraintProperties={domain.allowUniqueConstraintProperties} - showDefaultValueSettings={domain.showDefaultValueSettings} - defaultDefaultValueType={domain.defaultDefaultValueType} - defaultValueOptions={domain.defaultValueOptions} - appPropertiesOnly={appPropertiesOnly} isDragDisabled={ !valueIsEmpty(search) || domainFormDisplayOptions.isDragDisabled || field.isCalculatedField() } - domainFormDisplayOptions={domainFormDisplayOptions} - domainContainerPath={domain.container} - schemaName={schemaName ?? domain.schemaName} + key={key} + maxPhiLevel={maxPhiLevel} + onChange={this.onFieldsChange} + onDelete={this.onDeleteField} + onExpand={this.onFieldExpandToggle} queryName={queryName ?? domain.queryName} + ref={ref => { + this.refsArray[i] = ref; + }} + schemaName={schemaName ?? domain.schemaName} + showDefaultValueSettings={domain.showDefaultValueSettings} /> ); }) @@ -1362,17 +1362,17 @@ export class DomainFormImpl extends React.PureComponent
{showHeader && ( {children} @@ -1385,8 +1385,8 @@ export class DomainFormImpl extends React.PureComponent <> {systemFields && ( )} @@ -1413,7 +1413,7 @@ export class DomainFormImpl extends React.PureComponent
{helpTopic && (
- + Learn more about this tool
@@ -1460,10 +1460,10 @@ export class DomainFormImpl extends React.PureComponent {filePreviewData && !domainFormDisplayOptions?.hideImportData && ( )}
@@ -1471,8 +1471,8 @@ export class DomainFormImpl extends React.PureComponent
{hasException && domain.domainException.severity === SEVERITY_LEVEL_ERROR && (
{domain.domainException.exception}
diff --git a/packages/components/src/internal/components/forms/detail/DetailDisplay.tsx b/packages/components/src/internal/components/forms/detail/DetailDisplay.tsx index 8b20917062..1902527e2b 100644 --- a/packages/components/src/internal/components/forms/detail/DetailDisplay.tsx +++ b/packages/components/src/internal/components/forms/detail/DetailDisplay.tsx @@ -294,7 +294,10 @@ export function resolveDetailEditRenderer( } let value = resolveDetailFieldValue(data); - const ColumnInputRenderer = col.conceptURI === NON_NEGATIVE_NUMBER_CONCEPT_URI ? null /* Use standard forminput for media amount/unit */ : resolveInputRenderer(col); + const ColumnInputRenderer = + col.conceptURI === NON_NEGATIVE_NUMBER_CONCEPT_URI + ? null /* Use standard forminput for media amount/unit */ + : resolveInputRenderer(col); if (ColumnInputRenderer) { return ( = memo(props => { valueColumn={unitCol.lookup.keyColumn} /> {allowFieldDisable && ( - + )}
); From 0acbce3c0562b760681a974b41ed1ac6de82e292 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 19 Nov 2025 18:54:56 -0800 Subject: [PATCH 15/22] jest --- .../forms/input/AmountUnitInput.test.tsx | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 packages/components/src/internal/components/forms/input/AmountUnitInput.test.tsx diff --git a/packages/components/src/internal/components/forms/input/AmountUnitInput.test.tsx b/packages/components/src/internal/components/forms/input/AmountUnitInput.test.tsx new file mode 100644 index 0000000000..5543c4e25d --- /dev/null +++ b/packages/components/src/internal/components/forms/input/AmountUnitInput.test.tsx @@ -0,0 +1,139 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { AmountUnitInput } from './AmountUnitInput'; +import { ExtendedMap } from '../../../../public/ExtendedMap'; +import { QueryColumn } from '../../../../public/QueryColumn'; +import { Formsy } from '../formsy/index'; + +describe('AmountUnitInput', () => { + + const amountCol = { name: 'StoredAmount', caption: 'amount', fieldKey: 'amountKey' }; + const unitCol = { name: 'Units', caption: 'unit', fieldKey: 'unitKey', + lookup: { + hasQueryFilters: jest.fn(), + displayColumn: new QueryColumn({caption: 'test'}) + } + }; + const data = { StoredAmount: 12.5, Units: 'mg' }; + const allColumns = new ExtendedMap({ [amountCol.fieldKey]: amountCol, [unitCol.fieldKey]: unitCol }); + + const CAN_DISABLE: any = { + allowFieldDisable: true, + onSelectChange: jest.fn(), + onToggleDisable: jest.fn(), + initiallyDisabled: false, + containerFilter: undefined, + containerPath: undefined, + allColumns, + data, + queryFilters: {}, + }; + + const DISABLED: any = { + ...CAN_DISABLE, + initiallyDisabled: true + }; + + const NOT_DISABLABLE: any = { + ...CAN_DISABLE, + allowFieldDisable: false + }; + + test('returns null when required columns are missing', () => { + // Missing unit column + + const someColumns = new ExtendedMap({ [amountCol.fieldKey]: amountCol }); + const { container } = render( + + + + ); + + // Component should render nothing when unit or amount column can't be found + expect(container.querySelector('.inner-test')).toBeEmptyDOMElement(); + }); + + test('with amount and unit column, can disable', () => { + + const { container } = render( + + + + ); + expect(document.querySelectorAll('.form-group.row')).toHaveLength(1); + expect(document.querySelectorAll('label')).toHaveLength(2); + expect(document.querySelectorAll('label')[0].textContent).toBe('Amount and Units '); + expect(document.querySelectorAll('label')[1].textContent).toBe(''); + expect(document.querySelectorAll('.fa-toggle-on')).toHaveLength(1); + expect(document.querySelectorAll('.fa-toggle-off')).toHaveLength(0); + const inputs = document.querySelectorAll('input'); + expect(inputs).toHaveLength(4); + expect(inputs[0].getAttribute('value')).toBe('true'); + expect(inputs[0].getAttribute('type')).toBe('hidden'); + expect(inputs[0].getAttribute('name')).toBe('StoredAmount::enabled'); + expect(inputs[1].getAttribute('value')).toBe('12.5'); + expect(inputs[1].getAttribute('name')).toBe('amountKey'); + expect(inputs[2].getAttribute('role')).toBe('combobox'); + expect(inputs[3].getAttribute('name')).toBe('Units::enabled'); + expect(inputs[3].getAttribute('value')).toBe('true'); + expect(inputs[3].getAttribute('type')).toBe('hidden'); + + }); + + test('with amount and unit column, can disable and disabled', () => { + + const { container } = render( + + + + ); + expect(document.querySelectorAll('.form-group.row')).toHaveLength(1); + expect(document.querySelectorAll('label')).toHaveLength(2); + expect(document.querySelectorAll('label')[0].textContent).toBe('Amount and Units '); + expect(document.querySelectorAll('label')[1].textContent).toBe(''); + expect(document.querySelectorAll('.fa-toggle-on')).toHaveLength(0); + expect(document.querySelectorAll('.fa-toggle-off')).toHaveLength(1); + const inputs = document.querySelectorAll('input'); + expect(inputs).toHaveLength(4); + expect(inputs[0].getAttribute('value')).toBe('false'); + expect(inputs[0].getAttribute('type')).toBe('hidden'); + expect(inputs[0].getAttribute('name')).toBe('StoredAmount::enabled'); + expect(inputs[1].getAttribute('value')).toBe('12.5'); + expect(inputs[1].getAttribute('name')).toBe('amountKey'); + expect(inputs[2].getAttribute('role')).toBe('combobox'); + expect(inputs[3].getAttribute('name')).toBe('Units::enabled'); + expect(inputs[3].getAttribute('value')).toBe('false'); + expect(inputs[3].getAttribute('type')).toBe('hidden'); + + }); + + test('with amount and unit column, cannot disable', () => { + + const { container } = render( + + + + ); + expect(document.querySelectorAll('.form-group.row')).toHaveLength(1); + expect(document.querySelectorAll('label')).toHaveLength(2); + expect(document.querySelectorAll('label')[0].textContent).toBe('Amount and Units '); + expect(document.querySelectorAll('label')[1].textContent).toBe(''); + expect(document.querySelectorAll('.fa-toggle-on')).toHaveLength(0); + expect(document.querySelectorAll('.fa-toggle-off')).toHaveLength(0); + const inputs = document.querySelectorAll('input'); + expect(inputs).toHaveLength(2); + expect(inputs[0].getAttribute('value')).toBe('12.5'); + expect(inputs[0].getAttribute('name')).toBe('amountKey'); + expect(inputs[1].getAttribute('role')).toBe('combobox'); + }); +}) From ae2374a86c5a35dcd3affa595a0a9c974a03b498 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 19 Nov 2025 18:55:04 -0800 Subject: [PATCH 16/22] fix null --- .../components/forms/input/AmountUnitInput.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx index 452f9083cd..68829d05cd 100644 --- a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx +++ b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx @@ -27,11 +27,11 @@ export const AmountUnitInput: FC = memo(props => { const id = generateId('selectinput-'); const amountCol = allColumns.filter(col => col.name.toLowerCase() === 'storedamount').valueArray?.[0]; const unitCol = allColumns.filter(col => col.name.toLowerCase() === 'units').valueArray?.[0]; - const amountValue = caseInsensitive(data, amountCol.name); - const unitValue = caseInsensitive(data, unitCol.name); - const queryFilter = unitCol.lookup.hasQueryFilters(Operation.insert) - ? List(unitCol.lookup.getQueryFilters(Operation.insert)) - : queryFilters?.[unitCol.fieldKey]; + const amountValue = caseInsensitive(data, amountCol?.name); + const unitValue = caseInsensitive(data, unitCol?.name); + const queryFilter = unitCol?.lookup.hasQueryFilters(Operation.insert) + ? List(unitCol?.lookup.getQueryFilters(Operation.insert)) + : queryFilters?.[unitCol?.fieldKey]; const onToggleChange = useCallback(() => { setDisabled(prevDisabled => { From 6f70a53d0a7839a2094b890bedcd813f27a36551 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 19 Nov 2025 18:55:37 -0800 Subject: [PATCH 17/22] jest --- .../forms/input/AmountUnitInput.test.tsx | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/components/src/internal/components/forms/input/AmountUnitInput.test.tsx b/packages/components/src/internal/components/forms/input/AmountUnitInput.test.tsx index 5543c4e25d..bede93660a 100644 --- a/packages/components/src/internal/components/forms/input/AmountUnitInput.test.tsx +++ b/packages/components/src/internal/components/forms/input/AmountUnitInput.test.tsx @@ -6,16 +6,21 @@ import { QueryColumn } from '../../../../public/QueryColumn'; import { Formsy } from '../formsy/index'; describe('AmountUnitInput', () => { - const amountCol = { name: 'StoredAmount', caption: 'amount', fieldKey: 'amountKey' }; - const unitCol = { name: 'Units', caption: 'unit', fieldKey: 'unitKey', + const unitCol = { + name: 'Units', + caption: 'unit', + fieldKey: 'unitKey', lookup: { hasQueryFilters: jest.fn(), - displayColumn: new QueryColumn({caption: 'test'}) - } + displayColumn: new QueryColumn({ caption: 'test' }), + }, }; const data = { StoredAmount: 12.5, Units: 'mg' }; - const allColumns = new ExtendedMap({ [amountCol.fieldKey]: amountCol, [unitCol.fieldKey]: unitCol }); + const allColumns = new ExtendedMap({ + [amountCol.fieldKey]: amountCol, + [unitCol.fieldKey]: unitCol, + }); const CAN_DISABLE: any = { allowFieldDisable: true, @@ -31,12 +36,12 @@ describe('AmountUnitInput', () => { const DISABLED: any = { ...CAN_DISABLE, - initiallyDisabled: true + initiallyDisabled: true, }; const NOT_DISABLABLE: any = { ...CAN_DISABLE, - allowFieldDisable: false + allowFieldDisable: false, }; test('returns null when required columns are missing', () => { @@ -44,16 +49,16 @@ describe('AmountUnitInput', () => { const someColumns = new ExtendedMap({ [amountCol.fieldKey]: amountCol }); const { container } = render( - + @@ -64,12 +69,11 @@ describe('AmountUnitInput', () => { }); test('with amount and unit column, can disable', () => { - const { container } = render( - ); + ); expect(document.querySelectorAll('.form-group.row')).toHaveLength(1); expect(document.querySelectorAll('label')).toHaveLength(2); expect(document.querySelectorAll('label')[0].textContent).toBe('Amount and Units '); @@ -87,11 +91,9 @@ describe('AmountUnitInput', () => { expect(inputs[3].getAttribute('name')).toBe('Units::enabled'); expect(inputs[3].getAttribute('value')).toBe('true'); expect(inputs[3].getAttribute('type')).toBe('hidden'); - }); test('with amount and unit column, can disable and disabled', () => { - const { container } = render( @@ -114,11 +116,9 @@ describe('AmountUnitInput', () => { expect(inputs[3].getAttribute('name')).toBe('Units::enabled'); expect(inputs[3].getAttribute('value')).toBe('false'); expect(inputs[3].getAttribute('type')).toBe('hidden'); - }); test('with amount and unit column, cannot disable', () => { - const { container } = render( @@ -136,4 +136,4 @@ describe('AmountUnitInput', () => { expect(inputs[0].getAttribute('name')).toBe('amountKey'); expect(inputs[1].getAttribute('role')).toBe('combobox'); }); -}) +}); From d920de438df57f8485b5305489d77f9349eaceef Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 19 Nov 2025 18:57:01 -0800 Subject: [PATCH 18/22] jest --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index f7df9b6c16..1607c4473f 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "6.70.9-fb-amountValidation.3", + "version": "6.70.9-fb-amountValidation.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "6.70.9-fb-amountValidation.3", + "version": "6.70.9-fb-amountValidation.4", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 1a9707d263..acac2bb703 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "6.70.9-fb-amountValidation.3", + "version": "6.70.9-fb-amountValidation.4", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From eb0192603f4cba36991c762c77e9b45338982bd3 Mon Sep 17 00:00:00 2001 From: XingY Date: Thu, 20 Nov 2025 10:00:24 -0800 Subject: [PATCH 19/22] code review changes --- .../src/internal/components/editable/Cell.tsx | 2 +- .../src/internal/components/editable/utils.test.ts | 2 +- .../src/internal/components/editable/utils.ts | 2 +- .../components/forms/input/AmountUnitInput.tsx | 10 ++++++---- packages/components/src/internal/query/api.ts | 4 ++++ 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/components/src/internal/components/editable/Cell.tsx b/packages/components/src/internal/components/editable/Cell.tsx index 5c9f949393..3f3e2f0c15 100644 --- a/packages/components/src/internal/components/editable/Cell.tsx +++ b/packages/components/src/internal/components/editable/Cell.tsx @@ -533,7 +533,7 @@ export class Cell extends React.PureComponent { .filter(vd => vd && vd.display !== undefined) .reduce((v, vd, i) => v + (i > 0 ? ', ' : '') + vd.display, ''); - const showMenu = (showLookup || !!col.inputRenderer) && (NON_NEGATIVE_NUMBER_CONCEPT_URI !== col?.conceptURI); + const showMenu = (showLookup || !!col.inputRenderer) && (NON_NEGATIVE_NUMBER_CONCEPT_URI !== col?.conceptURI /*storedamount has inputRenderer but shouldn't show menu*/); return ( <> diff --git a/packages/components/src/internal/components/editable/utils.test.ts b/packages/components/src/internal/components/editable/utils.test.ts index bc8fcf397b..8895584578 100644 --- a/packages/components/src/internal/components/editable/utils.test.ts +++ b/packages/components/src/internal/components/editable/utils.test.ts @@ -302,7 +302,7 @@ describe('getValidatedEditableGridValue', () => { invalidNegative.forEach(value => { expect(getValidatedEditableGridValue(value, amountCol)).toStrictEqual({ message: { - message: 'Invalid Amount, must be non-negative', + message: 'Amount must be non-negative', }, value, }); diff --git a/packages/components/src/internal/components/editable/utils.ts b/packages/components/src/internal/components/editable/utils.ts index 845840af86..b5c1ee6f13 100644 --- a/packages/components/src/internal/components/editable/utils.ts +++ b/packages/components/src/internal/components/editable/utils.ts @@ -65,7 +65,7 @@ export const getValidatedEditableGridValue = (origValue: any, col: QueryColumn): } else if (jsonType === 'float' && !isFloat(value)) { message = 'Invalid decimal'; } else if (NON_NEGATIVE_NUMBER_CONCEPT_URI === col?.conceptURI && Number(value) < 0) { - message = 'Invalid ' + col.caption + ', must be non-negative'; + message = col.caption + ' must be non-negative'; } else if (jsonType === 'string' && scale) { if (value.toString().trim().length > scale) message = value.toString().trim().length + '/' + scale + ' characters'; diff --git a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx index 68829d05cd..37f1c19b56 100644 --- a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx +++ b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx @@ -1,4 +1,4 @@ -import React, { FC, memo, useCallback, useState } from 'react'; +import React, { FC, memo, useCallback, useMemo, useState } from 'react'; import { List } from 'immutable'; import { TextInput } from './TextInput'; @@ -9,6 +9,7 @@ import { InputRendererProps } from './types'; import { caseInsensitive, generateId } from '../../../util/utils'; import { FormsyInput } from './FormsyReactComponents'; import { Operation } from '../../../../public/QueryColumn'; +import { STORED_AMOUNT_FIELDS } from '../../samples/constants'; export const AmountUnitInput: FC = memo(props => { const { @@ -24,15 +25,16 @@ export const AmountUnitInput: FC = memo(props => { } = props; const [disabled, setDisabled] = useState(initiallyDisabled && allowFieldDisable); - const id = generateId('selectinput-'); - const amountCol = allColumns.filter(col => col.name.toLowerCase() === 'storedamount').valueArray?.[0]; - const unitCol = allColumns.filter(col => col.name.toLowerCase() === 'units').valueArray?.[0]; + const amountCol = allColumns.find(col => col.name.toLowerCase() === STORED_AMOUNT_FIELDS.AMOUNT.toLowerCase()); + const unitCol = allColumns.find(col => col.name.toLowerCase() === STORED_AMOUNT_FIELDS.UNITS.toLowerCase()); const amountValue = caseInsensitive(data, amountCol?.name); const unitValue = caseInsensitive(data, unitCol?.name); const queryFilter = unitCol?.lookup.hasQueryFilters(Operation.insert) ? List(unitCol?.lookup.getQueryFilters(Operation.insert)) : queryFilters?.[unitCol?.fieldKey]; + const id = useMemo(() => generateId('amount-unit-input-'), []); + const onToggleChange = useCallback(() => { setDisabled(prevDisabled => { const newDisabled = !prevDisabled; diff --git a/packages/components/src/internal/query/api.ts b/packages/components/src/internal/query/api.ts index 47a8768d0a..45a210c4e3 100644 --- a/packages/components/src/internal/query/api.ts +++ b/packages/components/src/internal/query/api.ts @@ -373,6 +373,10 @@ function applyViewColumns( export class Renderers { static _check(columnMetadata, rawColumn, field, metadata) { + const columnValue = columnMetadata[field]; + if (columnValue) + return columnValue; + if (columnMetadata.conceptURI || rawColumn.conceptURI) { const concept = metadata.getIn([ 'concepts', From ee7df7204b4985a41b3963c012e132ffb224fb9e Mon Sep 17 00:00:00 2001 From: XingY Date: Thu, 20 Nov 2025 10:02:07 -0800 Subject: [PATCH 20/22] code review changes --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 1607c4473f..5d0661d9b2 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "6.70.9-fb-amountValidation.4", + "version": "6.70.9-fb-amountValidation.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "6.70.9-fb-amountValidation.4", + "version": "6.70.9-fb-amountValidation.5", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index acac2bb703..226e63096b 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "6.70.9-fb-amountValidation.4", + "version": "6.70.9-fb-amountValidation.5", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 8bf400fb724cf522d6066f37661396588d1dbba6 Mon Sep 17 00:00:00 2001 From: XingY Date: Thu, 20 Nov 2025 13:40:50 -0800 Subject: [PATCH 21/22] remove comma --- .../src/internal/components/forms/input/AmountUnitInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx index 37f1c19b56..06797ffe05 100644 --- a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx +++ b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx @@ -55,7 +55,7 @@ export const AmountUnitInput: FC = memo(props => { isDisabled={disabled} labelOverlayProps={{ inputId: amountCol.name, - description: 'The amount and units of this sample, currently on hand.', + description: 'The amount and units of this sample currently on hand.', label: 'Amount and Units', isFormsy: false, }} From e3b74e58becb0c8606d3f0f435ad606974865bce Mon Sep 17 00:00:00 2001 From: XingY Date: Thu, 20 Nov 2025 13:49:05 -0800 Subject: [PATCH 22/22] publish --- packages/components/package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index bccd357bb4..d83fb8e684 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "6.71.0", + "version": "6.71.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "6.71.0", + "version": "6.71.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1",