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
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.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
Expand Down
4 changes: 3 additions & 1 deletion packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ import {
AssayUploadTabs,
DataViewInfoTypes,
EDIT_METHOD,
EMPTY_COMPOUND_WARNING,
EMPTY_NS_SEQUENCE_WARNING,
EXPORT_TYPES,
GRID_CHECKBOX_OPTIONS,
Expand Down Expand Up @@ -1271,6 +1272,7 @@ export {
EditInlineField,
EditorMode,
EditorModel,
EMPTY_COMPOUND_WARNING,
EMPTY_NS_SEQUENCE_WARNING,
encodePart,
ensureAllFieldsInAllRows,
Expand Down Expand Up @@ -1354,8 +1356,8 @@ export {
getEntityTypeOptions,
getEventDataValueDisplay,
getExcludedDataTypeNames,
getFieldDisplayValue,
getExpandQueryInfo,
getFieldDisplayValue,
getFieldFiltersValidationResult,
getFilterForSampleOperation,
getFilterLabKeySql,
Expand Down
84 changes: 84 additions & 0 deletions packages/components/src/internal/UnidentifiedPill.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<UnidentifiedPill schemaQuery={SCHEMAS.DATA_CLASSES.PROTEIN_SEQUENCE} />);
const pill = container.querySelector('.unidentified-sequence-pill');
expect(pill).toBeInTheDocument();
expect(pill).toHaveClass('status-pill', 'info');
});

test('always renders Unidentified text', () => {
const { container } = render(<UnidentifiedPill schemaQuery={new SchemaQuery('other', 'Other')} />);
expect(container.querySelector('.unidentified-sequence-pill')).toHaveTextContent('Unidentified');
});

describe('question mark icon', () => {
test('renders for PROTEIN_SEQUENCE schema', () => {
const { container } = render(<UnidentifiedPill schemaQuery={SCHEMAS.DATA_CLASSES.PROTEIN_SEQUENCE} />);
expect(container.querySelector('.fa-question-circle')).toBeInTheDocument();
});

test('renders for NUC_SEQUENCE schema', () => {
const { container } = render(<UnidentifiedPill schemaQuery={SCHEMAS.DATA_CLASSES.NUC_SEQUENCE} />);
expect(container.querySelector('.fa-question-circle')).toBeInTheDocument();
});

test('renders for COMPOUND schema', () => {
const { container } = render(<UnidentifiedPill schemaQuery={SCHEMAS.DATA_CLASSES.COMPOUND} />);
expect(container.querySelector('.fa-question-circle')).toBeInTheDocument();
});

test('does not render for unrecognized schema', () => {
const { container } = render(<UnidentifiedPill schemaQuery={new SchemaQuery('other', 'Other')} />);
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(<UnidentifiedPill schemaQuery={SCHEMAS.DATA_CLASSES.PROTEIN_SEQUENCE} />);
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(<UnidentifiedPill schemaQuery={SCHEMAS.DATA_CLASSES.NUC_SEQUENCE} />);
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(<UnidentifiedPill schemaQuery={SCHEMAS.DATA_CLASSES.COMPOUND} />);
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(<UnidentifiedPill schemaQuery={new SchemaQuery('other', 'Other')} />);
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(<UnidentifiedPill schemaQuery={SCHEMAS.DATA_CLASSES.PROTEIN_SEQUENCE} />);
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();
});
});
});
22 changes: 16 additions & 6 deletions packages/components/src/internal/UnidentifiedPill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -16,9 +28,7 @@ export const UnidentifiedPill: FC<Props> = ({ 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(
() => (
Expand All @@ -37,8 +47,8 @@ export const UnidentifiedPill: FC<Props> = ({ schemaQuery }) => {
ref={targetRef}
>
Unidentified
<span className="label-help-icon fa fa-question-circle" />
{show && createPortal(popover, portalEl)}
{message && <span className="label-help-icon fa fa-question-circle" />}
{show && message && createPortal(popover, portalEl)}
</div>
);
};
2 changes: 2 additions & 0 deletions packages/components/src/internal/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.';
1 change: 1 addition & 0 deletions packages/components/src/internal/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/internal/util/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading