From b6a558bf8be23e75f26d1c757bf32295828aeca5 Mon Sep 17 00:00:00 2001 From: alanv Date: Thu, 16 Apr 2026 17:45:02 -0500 Subject: [PATCH 1/3] Add support for compound schema to hasIdentifiedCol and UnidentifiedPill --- .../components/releaseNotes/components.md | 7 ++++++ packages/components/src/index.ts | 4 +++- .../src/internal/UnidentifiedPill.tsx | 22 ++++++++++++++----- packages/components/src/internal/constants.ts | 2 ++ packages/components/src/internal/schemas.ts | 1 + .../components/src/internal/util/utils.ts | 3 ++- 6 files changed, 31 insertions(+), 8 deletions(-) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 3babdfdd16..0aea45adb4 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -1,6 +1,13 @@ # @labkey/components Components, models, actions, and utility functions for LabKey applications and pages +### version 7.x.x +*Released*: ? April 2026 +- Add EMPTY_COMPOUND_WARNING +- Add COMPOUND to SCHEMAS.DATA_CLASSES +- hasIdentifiedCol: check for Compound schema +- UnidentifiedPill: handle schemas that don't have defined messages, add support for Compound schema + ### version 7.30.0 *Released*: 17 April 2026 - GitHub Issue 848: Add the ability to reset TOTP settings in the apps diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 4c01381ea3..0f0f549394 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -250,6 +250,7 @@ import { AssayUploadTabs, DataViewInfoTypes, EDIT_METHOD, + EMPTY_COMPOUND_WARNING, EMPTY_NS_SEQUENCE_WARNING, EXPORT_TYPES, GRID_CHECKBOX_OPTIONS, @@ -1271,6 +1272,7 @@ export { EditInlineField, EditorMode, EditorModel, + EMPTY_COMPOUND_WARNING, EMPTY_NS_SEQUENCE_WARNING, encodePart, ensureAllFieldsInAllRows, @@ -1354,8 +1356,8 @@ export { getEntityTypeOptions, getEventDataValueDisplay, getExcludedDataTypeNames, - getFieldDisplayValue, getExpandQueryInfo, + getFieldDisplayValue, getFieldFiltersValidationResult, getFilterForSampleOperation, getFilterLabKeySql, diff --git a/packages/components/src/internal/UnidentifiedPill.tsx b/packages/components/src/internal/UnidentifiedPill.tsx index a65b37bd99..7e28f57625 100644 --- a/packages/components/src/internal/UnidentifiedPill.tsx +++ b/packages/components/src/internal/UnidentifiedPill.tsx @@ -3,10 +3,22 @@ import { createPortal } from 'react-dom'; import { generateId } from './util/utils'; import { useOverlayTriggerState } from './OverlayTrigger'; import { Popover } from './Popover'; -import { EMPTY_NS_SEQUENCE_WARNING, EMPTY_PS_SEQUENCE_WARNING } from './constants'; +import { EMPTY_COMPOUND_WARNING, EMPTY_NS_SEQUENCE_WARNING, EMPTY_PS_SEQUENCE_WARNING } from './constants'; import { SchemaQuery } from '../public/SchemaQuery'; import { SCHEMAS } from './schemas'; +function getPopoverMessage(schemaQuery: SchemaQuery): string | undefined { + if (schemaQuery.isEqual(SCHEMAS.DATA_CLASSES.PROTEIN_SEQUENCE, false)) { + return EMPTY_PS_SEQUENCE_WARNING; + } else if (schemaQuery.isEqual(SCHEMAS.DATA_CLASSES.NUC_SEQUENCE, false)) { + return EMPTY_NS_SEQUENCE_WARNING; + } else if (schemaQuery.isEqual(SCHEMAS.DATA_CLASSES.COMPOUND, false)) { + return EMPTY_COMPOUND_WARNING; + } + + return undefined; +} + interface Props { schemaQuery: SchemaQuery; } @@ -16,9 +28,7 @@ export const UnidentifiedPill: FC = ({ schemaQuery }) => { // Note: we use useOverlayTriggerState instead of OverlayTrigger because the wrapping div from OverlayTrigger // causes layout problems. const { onMouseEnter, onMouseLeave, portalEl, show, targetRef } = useOverlayTriggerState(id, true, false); - const message = schemaQuery.isEqual(SCHEMAS.DATA_CLASSES.PROTEIN_SEQUENCE, false) - ? EMPTY_PS_SEQUENCE_WARNING - : EMPTY_NS_SEQUENCE_WARNING; + const message = getPopoverMessage(schemaQuery); const popover = useMemo( () => ( @@ -37,8 +47,8 @@ export const UnidentifiedPill: FC = ({ schemaQuery }) => { ref={targetRef} > Unidentified - - {show && createPortal(popover, portalEl)} + {message && } + {show && message && createPortal(popover, portalEl)} ); }; diff --git a/packages/components/src/internal/constants.ts b/packages/components/src/internal/constants.ts index 7b9684f115..34e84fdcaa 100644 --- a/packages/components/src/internal/constants.ts +++ b/packages/components/src/internal/constants.ts @@ -265,3 +265,5 @@ export const EMPTY_NS_SEQUENCE_WARNING = 'Without a sequence, Protein sequence translations cannot be done automatically, and the system cannot prevent duplicates.'; export const EMPTY_PS_SEQUENCE_WARNING = 'No sequence added. The structure format and physical properties of molecules using this sequence cannot be calculated.'; +export const EMPTY_COMPOUND_WARNING = + 'Without SMILES Molecule component translation cannot be done automatically, and the system cannot prevent duplicates.'; diff --git a/packages/components/src/internal/schemas.ts b/packages/components/src/internal/schemas.ts index 3250fcab20..62fa907eef 100644 --- a/packages/components/src/internal/schemas.ts +++ b/packages/components/src/internal/schemas.ts @@ -66,6 +66,7 @@ export const DATA_CLASSES = { SCHEMA: DATA_CLASS_SCHEMA, CELL_LINE: new SchemaQuery(DATA_CLASS_SCHEMA, 'CellLine'), CONSTRUCT: new SchemaQuery(DATA_CLASS_SCHEMA, 'Construct'), + COMPOUND: new SchemaQuery(DATA_CLASS_SCHEMA, 'Compound'), EXPRESSION_SYSTEM: new SchemaQuery(DATA_CLASS_SCHEMA, 'ExpressionSystem'), MOLECULE: new SchemaQuery(DATA_CLASS_SCHEMA, 'Molecule'), MOLECULE_SET: new SchemaQuery(DATA_CLASS_SCHEMA, 'MoleculeSet'), diff --git a/packages/components/src/internal/util/utils.ts b/packages/components/src/internal/util/utils.ts index 9885a16695..070d80500e 100644 --- a/packages/components/src/internal/util/utils.ts +++ b/packages/components/src/internal/util/utils.ts @@ -919,5 +919,6 @@ export function hasIdentifiedCol(schemaQuery: SchemaQuery): boolean { const isNucSeq = schemaQuery.isEqual(SCHEMAS.DATA_CLASSES.NUC_SEQUENCE, false); const isProtSeq = schemaQuery.isEqual(SCHEMAS.DATA_CLASSES.PROTEIN_SEQUENCE, false); const isMolecule = schemaQuery.isEqual(SCHEMAS.DATA_CLASSES.MOLECULE, false); - return isNucSeq || isProtSeq || isMolecule; + const isCompound = schemaQuery.isEqual(SCHEMAS.DATA_CLASSES.COMPOUND, false); + return isNucSeq || isProtSeq || isMolecule || isCompound; } From 053f3109834c8fbb66c4e705aa12703b28422aa9 Mon Sep 17 00:00:00 2001 From: alanv Date: Fri, 17 Apr 2026 16:41:08 -0500 Subject: [PATCH 2/3] constants.ts - Add comma to EMPTY_COMPOUND_WARNING --- packages/components/src/internal/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/internal/constants.ts b/packages/components/src/internal/constants.ts index 34e84fdcaa..c6802553e3 100644 --- a/packages/components/src/internal/constants.ts +++ b/packages/components/src/internal/constants.ts @@ -266,4 +266,4 @@ export const EMPTY_NS_SEQUENCE_WARNING = export const EMPTY_PS_SEQUENCE_WARNING = 'No sequence added. The structure format and physical properties of molecules using this sequence cannot be calculated.'; export const EMPTY_COMPOUND_WARNING = - 'Without SMILES Molecule component translation cannot be done automatically, and the system cannot prevent duplicates.'; + 'Without SMILES, Molecule component translation cannot be done automatically, and the system cannot prevent duplicates.'; From 36107363eda086400ef1ce5c10fc29da8520e6f4 Mon Sep 17 00:00:00 2001 From: alanv Date: Fri, 17 Apr 2026 16:47:01 -0500 Subject: [PATCH 3/3] Add UnidentifiedPill.test.tsx --- .../src/internal/UnidentifiedPill.test.tsx | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 packages/components/src/internal/UnidentifiedPill.test.tsx diff --git a/packages/components/src/internal/UnidentifiedPill.test.tsx b/packages/components/src/internal/UnidentifiedPill.test.tsx new file mode 100644 index 0000000000..24b1cd60b3 --- /dev/null +++ b/packages/components/src/internal/UnidentifiedPill.test.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { SchemaQuery } from '../public/SchemaQuery'; +import { EMPTY_COMPOUND_WARNING, EMPTY_NS_SEQUENCE_WARNING, EMPTY_PS_SEQUENCE_WARNING } from './constants'; +import { SCHEMAS } from './schemas'; +import { UnidentifiedPill } from './UnidentifiedPill'; + +describe('UnidentifiedPill', () => { + test('renders with correct CSS classes', () => { + const { container } = render(); + const pill = container.querySelector('.unidentified-sequence-pill'); + expect(pill).toBeInTheDocument(); + expect(pill).toHaveClass('status-pill', 'info'); + }); + + test('always renders Unidentified text', () => { + const { container } = render(); + expect(container.querySelector('.unidentified-sequence-pill')).toHaveTextContent('Unidentified'); + }); + + describe('question mark icon', () => { + test('renders for PROTEIN_SEQUENCE schema', () => { + const { container } = render(); + expect(container.querySelector('.fa-question-circle')).toBeInTheDocument(); + }); + + test('renders for NUC_SEQUENCE schema', () => { + const { container } = render(); + expect(container.querySelector('.fa-question-circle')).toBeInTheDocument(); + }); + + test('renders for COMPOUND schema', () => { + const { container } = render(); + expect(container.querySelector('.fa-question-circle')).toBeInTheDocument(); + }); + + test('does not render for unrecognized schema', () => { + const { container } = render(); + expect(container.querySelector('.fa-question-circle')).not.toBeInTheDocument(); + }); + }); + + describe('popover on hover', () => { + test('shows EMPTY_PS_SEQUENCE_WARNING for PROTEIN_SEQUENCE', async () => { + const user = userEvent.setup(); + const { container } = render(); + await user.hover(container.querySelector('.unidentified-sequence-pill')); + expect(screen.getByText(EMPTY_PS_SEQUENCE_WARNING)).toBeInTheDocument(); + }); + + test('shows EMPTY_NS_SEQUENCE_WARNING for NUC_SEQUENCE', async () => { + const user = userEvent.setup(); + const { container } = render(); + await user.hover(container.querySelector('.unidentified-sequence-pill')); + expect(screen.getByText(EMPTY_NS_SEQUENCE_WARNING)).toBeInTheDocument(); + }); + + test('shows EMPTY_COMPOUND_WARNING for COMPOUND', async () => { + const user = userEvent.setup(); + const { container } = render(); + await user.hover(container.querySelector('.unidentified-sequence-pill')); + expect(screen.getByText(EMPTY_COMPOUND_WARNING)).toBeInTheDocument(); + }); + + test('does not show popover for unrecognized schema', async () => { + const user = userEvent.setup(); + const { container } = render(); + await user.hover(container.querySelector('.unidentified-sequence-pill')); + expect(document.querySelector('.unidentified-sequence-popover')).not.toBeInTheDocument(); + }); + + test('hides popover when mouse leaves', async () => { + const user = userEvent.setup(); + const { container } = render(); + const pill = container.querySelector('.unidentified-sequence-pill'); + await user.hover(pill); + expect(screen.getByText(EMPTY_PS_SEQUENCE_WARNING)).toBeInTheDocument(); + await user.unhover(pill); + expect(screen.queryByText(EMPTY_PS_SEQUENCE_WARNING)).not.toBeInTheDocument(); + }); + }); +});