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
12 changes: 6 additions & 6 deletions packages/components/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@labkey/components",
"version": "7.13.1",
"version": "7.14.0-fb-mvtc-convert.9",
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
"sideEffects": false,
"files": [
Expand Down Expand Up @@ -50,7 +50,7 @@
"homepage": "https://github.com/LabKey/labkey-ui-components#readme",
"dependencies": {
"@hello-pangea/dnd": "18.0.1",
"@labkey/api": "1.45.0",
"@labkey/api": "1.45.1-fb-mvtc-convert.1",
"@testing-library/dom": "~10.4.1",
"@testing-library/jest-dom": "~6.9.1",
"@testing-library/react": "~16.3.0",
Expand Down
7 changes: 7 additions & 0 deletions packages/components/releaseNotes/components.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# @labkey/components
Components, models, actions, and utility functions for LabKey applications and pages

### version 7.X
*Released*: X January 2026
- Multi value text choices: field type conversion
- Updates the Text Choice options UI to add an “Allow multiple selections” toggle, multi-choice-specific edit restrictions, and improved confirmation messaging/tests for data-type changes involving text/multi-choice.
- Make multi-choice behaves as an internal variant of Text Choice rather than a separate visible type in data type dropdown
- Modified updateDataType and text choice usage counting to correctly handle conversions between string, Text Choice, and Multi Choice fields, clearing validators/flags and tracking multi-value usage where appropriate.

### version 7.13.1
*Released*: 26 January 2026
- Merge from release26.1-SNAPSHOT to develop
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,75 @@ import React from 'react';
import { render } from '@testing-library/react';

import { ConfirmDataTypeChangeModal, getDataTypeConfirmDisplayText } from './ConfirmDataTypeChangeModal';
import { DATE_TYPE, DATETIME_TYPE, PROP_DESC_TYPES, TIME_TYPE } from './PropDescType';
import {
BOOLEAN_TYPE,
DATE_TYPE,
DATETIME_TYPE,
FILE_TYPE,
INTEGER_TYPE,
MULTI_CHOICE_TYPE,
MULTILINE_TYPE,
PROP_DESC_TYPES,
TEXT_CHOICE_TYPE,
TEXT_TYPE,
TIME_TYPE,
} from './PropDescType';
import {
BOOLEAN_RANGE_URI,
DATETIME_RANGE_URI,
FILELINK_RANGE_URI,
INT_RANGE_URI,
MULTI_CHOICE_RANGE_URI,
MULTILINE_RANGE_URI,
TEXT_CHOICE_CONCEPT_URI,
TIME_RANGE_URI,
} from './constants';

describe('ConfirmDataTypeChangeModal', () => {
const intType = {
rangeURI: 'http://www.w3.org/2001/XMLSchema#int',
dataType: INTEGER_TYPE,
};

const multiLineType = {
rangeURI: MULTILINE_RANGE_URI,
dataType: MULTILINE_TYPE,
};

const fileLinkType = {
rangeURI: FILELINK_RANGE_URI,
dataType: FILE_TYPE,
};

const booleanType = {
rangeURI: BOOLEAN_RANGE_URI,
dataType: BOOLEAN_TYPE,
};

const dateTimeType = {
rangeURI: DATETIME_RANGE_URI,
dataType: DATETIME_TYPE,
};

const timeType = {
rangeURI: TIME_RANGE_URI,
dataType: TIME_TYPE,
};

const multiChoiceType = {
rangeURI: MULTI_CHOICE_RANGE_URI,
dataType: MULTI_CHOICE_TYPE,
};

const textChoiceType = {
conceptURI: TEXT_CHOICE_CONCEPT_URI,
dataType: TEXT_CHOICE_TYPE,
};

const DEFAULT_PROPS = {
originalRangeURI: 'http://www.w3.org/2001/XMLSchema#boolean',
original: {
rangeURI: 'http://www.w3.org/2001/XMLSchema#boolean',
dataType: BOOLEAN_TYPE,
},
newDataType: PROP_DESC_TYPES.get(0),
onConfirm: jest.fn,
onCancel: jest.fn,
Expand All @@ -29,49 +85,59 @@ describe('ConfirmDataTypeChangeModal', () => {
});

test('getDataTypeConfirmDisplayText', () => {
expect(getDataTypeConfirmDisplayText(INT_RANGE_URI)).toBe('integer');
expect(getDataTypeConfirmDisplayText(MULTILINE_RANGE_URI)).toBe('string');
expect(getDataTypeConfirmDisplayText(FILELINK_RANGE_URI)).toBe('file');
expect(getDataTypeConfirmDisplayText(BOOLEAN_RANGE_URI)).toBe('boolean');
expect(getDataTypeConfirmDisplayText(DATETIME_RANGE_URI)).toBe('dateTime');
expect(getDataTypeConfirmDisplayText(intType.dataType)).toBe('integer');
expect(getDataTypeConfirmDisplayText(multiLineType.dataType)).toBe('string');
expect(getDataTypeConfirmDisplayText(fileLinkType.dataType)).toBe('file');
expect(getDataTypeConfirmDisplayText(booleanType.dataType)).toBe('boolean');
expect(getDataTypeConfirmDisplayText(dateTimeType.dataType)).toBe('dateTime');
expect(getDataTypeConfirmDisplayText(multiChoiceType.dataType)).toBe('Text Choice (multiple select)');
expect(getDataTypeConfirmDisplayText(textChoiceType.dataType)).toBe('Text Choice (single select)');
});

test('from datetime to time', () => {
render(
<ConfirmDataTypeChangeModal
{...DEFAULT_PROPS}
originalRangeURI={DATETIME_RANGE_URI}
newDataType={TIME_TYPE}
/>
);
render(<ConfirmDataTypeChangeModal {...DEFAULT_PROPS} newDataType={TIME_TYPE} original={dateTimeType} />);
expect(document.body).toHaveTextContent(
'This change will convert the values in the field from dateTime to time. This will cause the Date portion of the value to be removed. Once you save your changes, you will not be able to change it back to dateTime.'
);
});

test('from datetime to date', () => {
render(<ConfirmDataTypeChangeModal {...DEFAULT_PROPS} newDataType={DATE_TYPE} original={dateTimeType} />);
expect(document.body).toHaveTextContent(
'This change will convert the values in the field from dateTime to date. This will cause the Time portion of the value to be removed.'
);
});

test('from date to datetime', () => {
render(<ConfirmDataTypeChangeModal {...DEFAULT_PROPS} newDataType={DATETIME_TYPE} original={timeType} />);
expect(document.body).toHaveTextContent(
'This change will convert the values in the field from time to dateTime. Once you save your changes, you will not be able to change it back to time.'
);
});

test('from text choice to mvtc', () => {
render(
<ConfirmDataTypeChangeModal
{...DEFAULT_PROPS}
originalRangeURI={DATETIME_RANGE_URI}
newDataType={DATE_TYPE}
newDataType={multiChoiceType.dataType}
original={textChoiceType}
/>
);
expect(document.body).toHaveTextContent(
'This change will convert the values in the field from dateTime to date. This will cause the Time portion of the value to be removed.'
'Confirm Data Type ChangeThis change will convert the values in the field from Text Choice (single select) to Text Choice (multiple select). Filters in saved views might not function as expected and any conditional formatting configured for this field will be removed.'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor but when I look at this in the UI the "Single Select" and "Multiple Select" text is capitalized. Does that matter for this test case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I don't think it matters in this case.

);
});

test('from date to datetime', () => {
test('from mvtc to tc', () => {
render(
<ConfirmDataTypeChangeModal
{...DEFAULT_PROPS}
originalRangeURI={TIME_RANGE_URI}
newDataType={DATETIME_TYPE}
newDataType={textChoiceType.dataType}
original={multiChoiceType}
/>
);
expect(document.body).toHaveTextContent(
'This change will convert the values in the field from time to dateTime. Once you save your changes, you will not be able to change it back to time.'
'This change will convert the values in the field from Text Choice (multiple select) to Text Choice (single select). Filters in saved views might not function as expected'
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,40 @@ import {
MULTILINE_RANGE_URI,
TIME_RANGE_URI,
} from './constants';
import { IDomainField } from './models';

interface Props {
originalRangeURI: string;
newDataType: PropDescType;
onConfirm: () => void;
onCancel: () => void;
onConfirm: () => void;
original: Partial<IDomainField>;
}

export const ConfirmDataTypeChangeModal: FC<Props> = memo(props => {
const { originalRangeURI, newDataType, onConfirm, onCancel } = props;
const origTypeLabel = getDataTypeConfirmDisplayText(originalRangeURI);
const newTypeLabel = getDataTypeConfirmDisplayText(newDataType.rangeURI);
const { original, newDataType, onConfirm, onCancel } = props;
const originalRangeURI = original.rangeURI || '';
const origTypeLabel = getDataTypeConfirmDisplayText(original.dataType);
const newTypeLabel = getDataTypeConfirmDisplayText(newDataType);
const newMultiChoice = PropDescType.isMultiChoice(newDataType.rangeURI);
const oldMultiChoice = PropDescType.isMultiChoice(original.dataType.rangeURI);
const newTextChoice = PropDescType.isTextChoice(newDataType.conceptURI);

const reversible =
(PropDescType.isDate(originalRangeURI) && PropDescType.isDateTime(newDataType.rangeURI)) ||
(PropDescType.isDateTime(originalRangeURI) && PropDescType.isDate(newDataType.rangeURI));
(PropDescType.isDateTime(originalRangeURI) && PropDescType.isDate(newDataType.rangeURI)) ||
newMultiChoice;

let dataLossWarning = null;
if (
if (newMultiChoice) {
dataLossWarning = (
<>
Filters in saved views might not function as expected and any conditional formatting configured for this
field will be <span className="bold-text">removed</span>.{' '}
</>
);
} else if (oldMultiChoice && newTextChoice) {
dataLossWarning = <>Filters in saved views might not function as expected. </>;
} else if (
originalRangeURI === DATETIME_RANGE_URI &&
(newDataType.rangeURI === DATE_RANGE_URI || newDataType.rangeURI === TIME_RANGE_URI)
) {
Expand All @@ -43,11 +58,11 @@ export const ConfirmDataTypeChangeModal: FC<Props> = memo(props => {

return (
<Modal
title="Confirm Data Type Change"
onConfirm={onConfirm}
onCancel={onCancel}
confirmClass="btn-danger"
confirmText="Yes, Change Data Type"
onCancel={onCancel}
onConfirm={onConfirm}
title="Confirm Data Type Change"
>
<div>
This change will convert the values in the field from{' '}
Expand All @@ -67,7 +82,12 @@ export const ConfirmDataTypeChangeModal: FC<Props> = memo(props => {
ConfirmDataTypeChangeModal.displayName = 'ConfirmDataTypeChangeModal';

// exported for jest testing
export const getDataTypeConfirmDisplayText = (rangeURI: string): string => {

export const getDataTypeConfirmDisplayText = (dataType: PropDescType): string => {
if (dataType?.longDisplay) {
return dataType.longDisplay;
}
const rangeURI = dataType?.rangeURI || '';
if (rangeURI === INT_RANGE_URI) return 'integer';
if (rangeURI === MULTILINE_RANGE_URI) return 'string';
if (rangeURI === FILELINK_RANGE_URI) return 'file';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1263,6 +1263,7 @@ export class DomainFormImpl extends React.PureComponent<DomainFormProps, State>

return (
<DomainRow
allowMultiChoiceField={domain.allowMultiChoiceProperties}
allowUniqueConstraintProperties={domain.allowUniqueConstraintProperties}
appPropertiesOnly={appPropertiesOnly}
availableTypes={availableTypes}
Expand Down
Loading