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

### version 7.X
*Released*: X March 2026
- GitHub Issue 955: limit text choice option length to 200 characters

### version 7.24.0
*Released*: 24 March 2026
- SchemaQuery.isEqual: add optional includeViewName argument, defaults to true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,33 @@ describe('TextChoiceAddValuesModal', () => {
validateCounterText('2 values', '3 new values');
});

test('value exceeding max length disables apply and shows error', async () => {
render(<TextChoiceAddValuesModal {...DEFAULT_PROPS} />);
const longValue = 'a'.repeat(201);
await userEvent.type(document.querySelector('textarea'), longValue);
expect(document.querySelector('.btn-success').hasAttribute('disabled')).toBeTruthy();
let errorEls = document.querySelectorAll('.domain-text-choices-error');
expect(errorEls).toHaveLength(1);
expect(errorEls[0].textContent).toContain('Value exceeds maximum of 200 characters');

// clear the long value, error should be gone
await userEvent.clear(document.querySelector('textarea'));
expect(document.querySelectorAll('.domain-text-choices-error')).toHaveLength(0);

// enter a valid short value, no error
await userEvent.type(document.querySelector('textarea'), 'short value');
expect(document.querySelector('.btn-success').hasAttribute('disabled')).toBeFalsy();
expect(document.querySelectorAll('.domain-text-choices-error')).toHaveLength(0);

// multiline input where second line exceeds max length
await userEvent.clear(document.querySelector('textarea'));
await userEvent.type(document.querySelector('textarea'), 'valid\n' + 'b'.repeat(201));
expect(document.querySelector('.btn-success').hasAttribute('disabled')).toBeTruthy();
errorEls = document.querySelectorAll('.domain-text-choices-error');
expect(errorEls).toHaveLength(1);
expect(errorEls[0].textContent).toContain('Value exceeds maximum of 200 characters');
});

test('initial already equal to max', async () => {
render(<TextChoiceAddValuesModal {...DEFAULT_PROPS} initialValueCount={2} maxValueCount={2} />);
expect(document.querySelector('.btn-success').hasAttribute('disabled')).toBeTruthy();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Utils } from '@labkey/api';

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

import { MAX_VALID_TEXT_CHOICES } from './constants';
import { MAX_TEXT_CHOICE_VALUE_LENGTH, MAX_VALID_TEXT_CHOICES } from './constants';
import { getValidValuesFromArray } from './models';

interface Props {
Expand All @@ -22,27 +22,31 @@ export const TextChoiceAddValuesModal: FC<Props> = memo(props => {
return valueStr?.trim().length > 0 ? getValidValuesFromArray(valueStr.split('\n').map(v => v.trim())) : [];
}, [valueStr]);
const maxValuesToAdd = useMemo(() => maxValueCount - initialValueCount, [initialValueCount]);
const tooLongValue = useMemo(() => parsedValues.find(v => v.length > MAX_TEXT_CHOICE_VALUE_LENGTH), [parsedValues]);
const hasFieldName = useMemo(() => fieldName?.length > 0, [fieldName]);
const onChange = useCallback(evt => {
setValueStr(evt.target.value);
}, []);
const onConfirm = useCallback(() => {
if (parsedValues.length <= maxValuesToAdd) {
if (parsedValues.length <= maxValuesToAdd && !tooLongValue) {
onApply(parsedValues);
}
}, [parsedValues, maxValuesToAdd, onApply]);
const canConfirm = parsedValues.length > 0 && parsedValues.length <= maxValuesToAdd;
}, [parsedValues, maxValuesToAdd, tooLongValue, onApply]);
const canConfirm = parsedValues.length > 0 && parsedValues.length <= maxValuesToAdd && !tooLongValue;
const title = `Add Text Choice Values${hasFieldName ? ' for ' + fieldName : ''}`;
const valueNoun = Utils.pluralize(maxValuesToAdd, 'value', 'values');
return (
<Modal canConfirm={canConfirm} confirmText="Apply" onCancel={onCancel} onConfirm={onConfirm} title={title}>
<p>Enter each value on a new line. {valueNoun} can be added.</p>
<textarea
rows={8}
cols={50}
aria-label="Text choice values"
aria-describedby={tooLongValue ? 'text-choice-length-error' : undefined}
aria-invalid={!!tooLongValue}
className="form-control textarea-fullwidth"
placeholder="Enter new values..."
cols={50}
onChange={onChange}
placeholder="Enter new values..."
rows={8}
value={valueStr}
/>
<div
Expand All @@ -52,6 +56,12 @@ export const TextChoiceAddValuesModal: FC<Props> = memo(props => {
>
{parsedValues.length === 1 ? '1 new value provided.' : `${parsedValues.length} new values provided.`}
</div>
{tooLongValue && (
<div className="domain-text-choices-error" id="text-choice-length-error" role="alert">
Value exceeds maximum of {MAX_TEXT_CHOICE_VALUE_LENGTH} characters: &quot;
{tooLongValue.substring(0, 50)}...&quot;
</div>
)}
</Modal>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ export const DERIVATION_DATA_SCOPES = {
};

export const MAX_VALID_TEXT_CHOICES = 500;
export const MAX_TEXT_CHOICE_VALUE_LENGTH = 200; // GitHub Issue 955: limit option length to 200

export const LOOKUP_VALIDATOR_VALUES = { type: 'Lookup', name: 'Lookup Validator' };

Expand Down
Loading