diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 223032bc9d..05a1cc40b9 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.62.0", + "version": "7.62.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.62.0", + "version": "7.62.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 79697f08c9..b686176154 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.62.0", + "version": "7.62.1", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/src/internal/app/products.ts b/packages/components/src/internal/app/products.ts index d07b2fa944..7e7c00479b 100644 --- a/packages/components/src/internal/app/products.ts +++ b/packages/components/src/internal/app/products.ts @@ -11,13 +11,8 @@ import { ModuleContext, resolveModuleContext } from '../components/base/ServerCo // These ids should match what is used by the MenuProviders in the Java code, so we can avoid toLowerCase comparisons. export const LKS_PRODUCT_ID = 'LabKeyServer'; export const BIOLOGICS_PRODUCT_ID = 'Biologics'; -export const BIOLOGICS_ENTERPRISE_PRODUCT_KEY = 'limsEnterprise'; -export const BIOLOGICS_STARTER_PRODUCT_KEY = 'limsStarter' export const LIMS_PRODUCT_ID = 'LIMS'; -export const LIMS_PRODUCT_KEY = 'labkeyLims'; export const SAMPLE_MANAGER_PRODUCT_ID = 'SampleManager'; -export const SAMPLE_MANAGER_STARTER_PRODUCT_KEY = 'sampleManagerStarter'; -export const SAMPLE_MANAGER_PROFESSIONAL_PRODUCT_KEY = 'sampleManagerProfessional' export const FREEZER_MANAGER_PRODUCT_ID = 'FreezerManager'; export function isFreezerManagementEnabled(moduleContext?: ModuleContext): boolean { diff --git a/packages/components/src/internal/components/editable/LookupCell.tsx b/packages/components/src/internal/components/editable/LookupCell.tsx index ff2ec4acbf..6523086d49 100644 --- a/packages/components/src/internal/components/editable/LookupCell.tsx +++ b/packages/components/src/internal/components/editable/LookupCell.tsx @@ -148,12 +148,10 @@ export const LookupCell: FC = memo(props => { autoFocus defaultInputValue={defaultInputValue} disabled={disabled} - multiple={col.isMultiChoice} onBlur={onBlur} onChange={onSelectChange} onKeyDown={onKeyDown} queryColumn={col} - skipJoinValues={true} value={col.isMultiChoice ? rawValues : rawValues[0]} /> ); diff --git a/packages/components/src/internal/components/forms/BulkUpdateForm.test.tsx b/packages/components/src/internal/components/forms/BulkUpdateForm.test.tsx index f4072436fa..b14853d831 100644 --- a/packages/components/src/internal/components/forms/BulkUpdateForm.test.tsx +++ b/packages/components/src/internal/components/forms/BulkUpdateForm.test.tsx @@ -56,6 +56,8 @@ const QUERY_INFO = QueryInfo.fromJsonForTests({ const DEFAULT_PROPS: BulkUpdateFormProps = { api: getTestAPIWrapper(jest.fn), + nounPlural: QUERY, + nounSingular: QUERY, onComplete: jest.fn(), onCancel: jest.fn(), queryInfo: QUERY_INFO, @@ -105,8 +107,8 @@ describe('BulkUpdateForm', () => { expect(document.querySelectorAll('.query-info-form')).toHaveLength(1); }); expect(document.querySelectorAll('.toggle-group-icon')).toHaveLength(2); - expect(document.querySelectorAll('input#update')).toHaveLength(1); - expect(document.querySelector('input#update').getAttribute('value')).toBe('abc'); + expect(document.querySelectorAll('input[name="update"]')).toHaveLength(1); + expect(document.querySelector('input[name="update"]')).toHaveValue('abc'); expect(document.querySelectorAll('.attachment-card__name')).toHaveLength(1); expect(document.querySelector('.attachment-card__name')).toHaveTextContent('test.txt'); }); @@ -118,7 +120,7 @@ describe('BulkUpdateForm', () => { expect(document.querySelectorAll('.query-info-form')).toHaveLength(1); }); expect(document.querySelectorAll('.toggle-group-icon')).toHaveLength(1); - expect(document.querySelectorAll('input#update')).toHaveLength(0); + expect(document.querySelectorAll('input[name="update"]')).toHaveLength(0); expect(document.querySelectorAll('.attachment-card__name')).toHaveLength(1); }); }); diff --git a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx index 7ea8e33ef8..fba7861795 100644 --- a/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx +++ b/packages/components/src/internal/components/forms/input/AmountUnitInput.tsx @@ -94,14 +94,13 @@ export const AmountUnitInput: FC = memo(props => { /> diff --git a/packages/components/src/internal/components/forms/input/AppendUnitsInput.test.tsx b/packages/components/src/internal/components/forms/input/AppendUnitsInput.test.tsx index 6e90ebe495..0c3746d5ab 100644 --- a/packages/components/src/internal/components/forms/input/AppendUnitsInput.test.tsx +++ b/packages/components/src/internal/components/forms/input/AppendUnitsInput.test.tsx @@ -22,7 +22,7 @@ describe('AppendUnitsInput', () => { test('without formsy', () => { // Without Formsy it should not crash the page renderWithAppContext(); - expect(document.querySelector('#appendUnitsColumn')).toBeNull(); + expect(document.querySelector('input[name="appendUnitsColumn"]')).not.toBeInTheDocument(); }); test('with formsy', () => { @@ -31,6 +31,6 @@ describe('AppendUnitsInput', () => { ); - expect(document.querySelector('#appendUnitsColumn')).toBeInTheDocument(); + expect(document.querySelector('input[name="appendUnitsColumn"]')).toBeInTheDocument(); }); }); diff --git a/packages/components/src/internal/components/forms/input/DisableableInput.test.tsx b/packages/components/src/internal/components/forms/input/DisableableInput.test.tsx index aba0aa9cf9..3205eff1d6 100644 --- a/packages/components/src/internal/components/forms/input/DisableableInput.test.tsx +++ b/packages/components/src/internal/components/forms/input/DisableableInput.test.tsx @@ -31,6 +31,36 @@ describe('DisableableInput', () => { expect(notDisableable.result.current.inputValue).toBe('fromProps'); }); + test('localValue reports local edits whether or not allowDisable is set', () => { + const disableable = renderHook(() => + useDisableableInput({ allowDisable: true, value: 'fromProps' }) + ); + const notDisableable = renderHook(() => useDisableableInput({ value: 'fromProps' })); + + expect(disableable.result.current.localValue).toBe('fromProps'); + expect(notDisableable.result.current.localValue).toBe('fromProps'); + + act(() => { + disableable.result.current.setInputValue('edited'); + }); + act(() => { + notDisableable.result.current.setInputValue('edited'); + }); + + expect(disableable.result.current.localValue).toBe('edited'); + + // Where inputValue falls back to the value from props, localValue still reports the edit + expect(notDisableable.result.current.inputValue).toBe('fromProps'); + expect(notDisableable.result.current.localValue).toBe('edited'); + + // Disabling discards the edit, so localValue tracks the value the input reverts to + act(() => { + disableable.result.current.toggleDisabled(); + }); + + expect(disableable.result.current.localValue).toBe('fromProps'); + }); + test('discards local edits when the input is disabled', () => { const { result } = renderHook(() => useDisableableInput({ allowDisable: true, value: 'fromProps' }) diff --git a/packages/components/src/internal/components/forms/input/DisableableInput.tsx b/packages/components/src/internal/components/forms/input/DisableableInput.tsx index 95c95907d6..533c13fa47 100644 --- a/packages/components/src/internal/components/forms/input/DisableableInput.tsx +++ b/packages/components/src/internal/components/forms/input/DisableableInput.tsx @@ -59,6 +59,11 @@ export interface UseDisableableInput { */ inputValue: V; isDisabled: boolean; + /** + * The locally tracked value. Unlike inputValue this never falls back to props, so it still reflects what the user + * has typed into an input whose parent does not echo edits back through value. + */ + localValue: V; /** * Records the value as the user edits it so it can be reverted when the input is subsequently disabled. * Call this from the input's onChange handler. @@ -117,6 +122,7 @@ export function useDisableableInput(props: DisableableInputProps): UseDisa return { inputValue: !allowDisable || inputValue === undefined ? value : inputValue, isDisabled, + localValue: inputValue, setInputValue, toggleDisabled, }; diff --git a/packages/components/src/internal/components/forms/input/TextAreaInput.test.tsx b/packages/components/src/internal/components/forms/input/TextAreaInput.test.tsx new file mode 100644 index 0000000000..a1fddc1f8e --- /dev/null +++ b/packages/components/src/internal/components/forms/input/TextAreaInput.test.tsx @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2026 LabKey Corporation. All rights reserved. No portion of this work may be reproduced + * in any form or by any electronic or mechanical means without written permission from LabKey Corporation. + */ +import React from 'react'; +import { render, RenderResult } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { QueryColumn } from '../../../../public/QueryColumn'; +import { Formsy } from '../formsy'; +import { INPUT_LABEL_CLASS_NAME, INPUT_WRAPPER_CLASS_NAME, MIXED_VALUE_DISPLAY } from '../constants'; + +import { TextAreaInput, TextAreaInputProps } from './TextAreaInput'; + +const COLUMN = new QueryColumn({ caption: 'Description', fieldKey: 'description', name: 'description' }); +const REQUIRED_COLUMN = COLUMN.mutate({ required: true }) as QueryColumn; + +const ENABLED_FIELD_SELECTOR = `input[name="${COLUMN.fieldKey}::enabled"]`; + +function renderInForm(props?: Partial): RenderResult { + return render( + + + + ); +} + +function textArea(): HTMLTextAreaElement { + return document.querySelector('textarea'); +} + +function clickToggle(): Promise { + return userEvent.click(document.querySelector('.control-label-toggle-input button:last-child')); +} + +describe('TextAreaInput', () => { + describe('label', () => { + test('renders the column caption by default', () => { + const { container } = renderInForm(); + + const label = container.querySelector('label'); + expect(label).toHaveTextContent('Description'); + expect(label).toHaveClass(...INPUT_LABEL_CLASS_NAME.split(' '), 'textarea-control-label'); + // With a visible label the textarea is labeled by the