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

### version 7.45.5
*Released*: 7 July 2026
- Export DetailDisplay
- ExpandableContainer: Fix style issues
- Remove .component-detail and .detail-components styles
- These have been moved to Biologics where they are used exclusively

### version 7.45.4
*Released*: 7 July 2026
- GitHub Issue #1846: Add role-restriction option to API key dialog

### version 7.45.3
*Released*: 4 July 2026
- GitHub Issue #1134: Support plating from find by samples

### version 7.45.2
*Released*: 29 June 2026
- GitHub Issue #1023: Add redirect() helper that uses core-safeRedirect when necessary to check url before redirecting
Expand Down
5 changes: 4 additions & 1 deletion packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ import { ColorIcon } from './internal/components/base/ColorIcon';
import { QuerySelect } from './internal/components/forms/QuerySelect';
import { PageDetailHeader } from './internal/components/forms/PageDetailHeader';
import { DetailPanelHeader } from './internal/components/forms/detail/DetailPanelHeader';
import { resolveDetailRenderer } from './internal/components/forms/detail/DetailDisplay';
import { DetailDisplay, resolveDetailRenderer } from './internal/components/forms/detail/DetailDisplay';
import { useDataChangeCommentsRequired } from './internal/components/forms/input/useDataChangeCommentsRequired';
import {
InputRenderContext,
Expand Down Expand Up @@ -398,6 +398,7 @@ import {
getSampleStatusFromSampleRow,
getSampleStatusType,
isAllSamplesSchema,
isFindBySampleSchema,
isSampleOperationPermitted,
isSamplesSchema,
SamplesEditButtonSections,
Expand Down Expand Up @@ -1237,6 +1238,7 @@ export {
DERIVATION_DATA_SCOPES,
DERIVATIVE_CREATION,
DesignerDetailTooltip,
DetailDisplay,
DetailPanel,
DetailPanelHeader,
DetailPanelWithModel,
Expand Down Expand Up @@ -1488,6 +1490,7 @@ export {
isCtrlOrMetaKey,
isDateBetween,
isDateTimeInPast,
isFindBySampleSchema,
isImage,
isInteger,
isIntegerInRange,
Expand Down
9 changes: 8 additions & 1 deletion packages/components/src/internal/UnidentifiedPill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import { createPortal } from 'react-dom';
import { generateId } from './util/utils';
import { useOverlayTriggerState } from './OverlayTrigger';
import { Popover } from './Popover';
import { EMPTY_COMPOUND_WARNING, EMPTY_NS_SEQUENCE_WARNING, EMPTY_PS_SEQUENCE_WARNING } from './constants';
import {
EMPTY_COMPOUND_WARNING,
EMPTY_NS_SEQUENCE_WARNING,
EMPTY_PS_SEQUENCE_WARNING,
UNIDENTIFIED_MOLECULE_WARNING,
} from './constants';
import { SchemaQuery } from '../public/SchemaQuery';
import { SCHEMAS } from './schemas';

Expand All @@ -18,6 +23,8 @@ function getPopoverMessage(schemaQuery: SchemaQuery): string | undefined {
return EMPTY_NS_SEQUENCE_WARNING;
} else if (schemaQuery.isEqual(SCHEMAS.DATA_CLASSES.COMPOUND, false)) {
return EMPTY_COMPOUND_WARNING;
} else if (schemaQuery.isEqual(SCHEMAS.DATA_CLASSES.MOLECULE, false)) {
return UNIDENTIFIED_MOLECULE_WARNING;
}

return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,10 @@ export const TrendlineOption: FC<TrendlineOptionProps> = memo(props => {
);

const trendlineOptions = useMemo(() => {
return TRENDLINE_OPTIONS.filter(option => {
return !option.schemaPrefix || schemaQuery.schemaName.startsWith(option.schemaPrefix);
});
}, [TRENDLINE_OPTIONS, schemaQuery.schemaName]);
return TRENDLINE_OPTIONS.filter(
option => !option.schemaPrefix || schemaQuery.schemaStartsWith(option.schemaPrefix)
);
}, [TRENDLINE_OPTIONS, schemaQuery]);

const onAsymptoteTypeChange = useCallback(
(selected: string) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export const DetailDisplay: FC<DetailDisplayProps> = memo(props => {
// key safety
const newRow = row.reduce((newRow, value, key) => {
return newRow.set(key.toLowerCase(), value);
}, OrderedMap<string, any>());
}, OrderedMap<string, unknown>());

return (
<table className={classNames(DETAIL_TABLE_CLASSES, tableCls)} key={i}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,9 @@ export const DetailPanelHeader: FC<DetailPanelHeaderProps> = memo(props => {
<span>{title}</span>
<span className="detail__edit--heading">
{isEditable && (
<>
<button className="clickable-text detail__edit-button" onClick={onClick} type="button">
<Icon iconClass="fa fa-pencil-square-o" srText={'Edit ' + title} />
</button>
<div className="clearfix" />
</>
<button className="clickable-text detail__edit-button" onClick={onClick} type="button">
<Icon iconClass="fa fa-pencil-square-o" srText={'Edit ' + title} />
</button>
)}
</span>
</h2>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface DetailHeaderProps extends PropsWithChildren {

export const DetailHeader: FC<DetailHeaderProps> = memo(({ children, header, iconSrc }) => (
<div className="lineage-detail-header margin-bottom">
<i className="component-detail--child--img">
<i className="pull-left">
<SVGIcon height="50px" iconSrc={iconSrc} theme={Theme.ORANGE} width="50px" />
</i>
<div className="text__truncate">
Expand Down
105 changes: 91 additions & 14 deletions packages/components/src/internal/components/samples/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,15 @@ import {
getSampleStatusFromSampleRow,
getSampleStatusLockedMessage,
getSampleStatusType,
isAllSamplesSchema,
isFindBySampleSchema,
isSampleOperationPermitted,
isSamplesSchema,
isWorkflowInputSamplesSchema,
} from './utils';
import { SampleState, SampleStateType } from './models';
import { TEST_LK_LIMS_MODULE_CONTEXT } from '../../productFixtures';
import { ViewInfo } from '../../ViewInfo';

const CHECKED_OUT_BY_FIELD = SCHEMAS.INVENTORY.CHECKED_OUT_BY_FIELD;
const INVENTORY_COLS = SCHEMAS.INVENTORY.INVENTORY_COLS;
Expand Down Expand Up @@ -69,13 +74,13 @@ describe('isSampleOperationPermitted', () => {
});

test('enabled, no status provided', () => {
const moduleContext = { api: { moduleNames: ['samplemanagement'] } };
const moduleContext = TEST_LK_LIMS_MODULE_CONTEXT;
expect(isSampleOperationPermitted(undefined, SampleOperation.EditMetadata, moduleContext)).toBeTruthy();
expect(isSampleOperationPermitted(null, SampleOperation.EditLineage, moduleContext)).toBeTruthy();
});

test('enabled, with status type provided', () => {
const moduleContext = { api: { moduleNames: ['samplemanagement'] } };
const moduleContext = TEST_LK_LIMS_MODULE_CONTEXT;
expect(
isSampleOperationPermitted(SampleStateType.Locked, SampleOperation.EditMetadata, moduleContext)
).toBeFalsy();
Expand All @@ -96,16 +101,16 @@ describe('isSampleOperationPermitted', () => {

describe('getFilterForSampleOperation', () => {
test('status not enabled', () => {
expect(getFilterForSampleOperation(SampleOperation.EditMetadata, true, {})).toBeNull();
expect(getFilterForSampleOperation(SampleOperation.EditMetadata, true, {})).toBeUndefined();
});

test('enabled, all allowed', () => {
const moduleContext = { api: { moduleNames: ['samplemanagement'] } };
expect(getFilterForSampleOperation(SampleOperation.AddToPicklist, true, moduleContext)).toBeNull();
const moduleContext = TEST_LK_LIMS_MODULE_CONTEXT;
expect(getFilterForSampleOperation(SampleOperation.AddToPicklist, true, moduleContext)).toBeUndefined();
});

test('enabled, some status does not allow', () => {
const moduleContext = { api: { moduleNames: ['samplemanagement'] } };
const moduleContext = TEST_LK_LIMS_MODULE_CONTEXT;
expect(getFilterForSampleOperation(SampleOperation.EditLineage, true, moduleContext)).toStrictEqual(
Filter.create(SAMPLE_STATE_TYPE_COLUMN_NAME, [SampleStateType.Locked], Filter.Types.NOT_IN)
);
Expand Down Expand Up @@ -308,6 +313,52 @@ describe('getOperationNotPermittedMessage', () => {
});
});

describe('isAllSamplesSchema', () => {
test('invalid values', () => {
expect(isAllSamplesSchema(undefined)).toBeFalsy();
expect(isAllSamplesSchema(null)).toBeFalsy();
expect(isAllSamplesSchema(SCHEMAS.EXP_TABLES.DATA)).toBeFalsy();
});
test('exp.materials query', () => {
const sq = SCHEMAS.EXP_TABLES.MATERIALS;
const details = new SchemaQuery(sq.schemaName, sq.queryName, ViewInfo.DETAIL_NAME);
const otherView = new SchemaQuery(sq.schemaName.toUpperCase(), sq.queryName.toUpperCase(), 'otherView');
expect(isAllSamplesSchema(sq)).toBeTruthy();
expect(isAllSamplesSchema(details)).toBeTruthy();
expect(isAllSamplesSchema(otherView)).toBeTruthy();
});
test('other supported schemas', () => {
expect(isAllSamplesSchema(SCHEMAS.SAMPLE_MANAGEMENT.SOURCE_SAMPLES)).toBeTruthy();
expect(isAllSamplesSchema(SCHEMAS.SAMPLE_MANAGEMENT.SAMPLE_STATUS_COUNTS)).toBeFalsy();
expect(new SchemaQuery(SCHEMAS.EXP_TABLES.SCHEMA, 'exp_temp_123')).toBeTruthy();
expect(isAllSamplesSchema(SCHEMAS.WORKFLOW.JOB_INPUT_SAMPLES)).toBeTruthy();
});
});

describe('isFindBySampleSchema', () => {
const expSQ = (queryName: string, viewName?: string): SchemaQuery =>
new SchemaQuery(SCHEMAS.EXP_TABLES.SCHEMA, queryName, viewName);

test('invalid values', () => {
expect(isFindBySampleSchema(undefined)).toBeFalsy();
expect(isFindBySampleSchema(null)).toBeFalsy();
});
test('other experiment queries', () => {
expect(isWorkflowInputSamplesSchema(SCHEMAS.EXP_TABLES.DATA)).toBeFalsy();
expect(isWorkflowInputSamplesSchema(SCHEMAS.EXP_TABLES.MATERIALS)).toBeFalsy();
});
test('expected query prefix', () => {
const prefix = 'exp_temp_';
expect(isFindBySampleSchema(expSQ(prefix))).toBeTruthy();
expect(isFindBySampleSchema(expSQ(prefix.toUpperCase()))).toBeTruthy();
expect(isFindBySampleSchema(expSQ(`${prefix}Hello`))).toBeTruthy();
expect(isFindBySampleSchema(expSQ(`${prefix}${prefix}`))).toBeTruthy();
expect(isFindBySampleSchema(expSQ(`${prefix}${prefix}`))).toBeTruthy();
expect(isFindBySampleSchema(expSQ(`${prefix}`, ViewInfo.DETAIL_NAME))).toBeTruthy();
expect(isFindBySampleSchema(expSQ(`_${prefix}`))).toBeFalsy();
});
});

describe('isSamplesSchema', () => {
test('not sample schema', () => {
expect(isSamplesSchema(SCHEMAS.EXP_TABLES.DATA)).toBeFalsy();
Expand All @@ -331,6 +382,25 @@ describe('isSamplesSchema', () => {
});
});

describe('isWorkflowInputSamplesSchema', () => {
test('invalid values', () => {
expect(isWorkflowInputSamplesSchema(undefined)).toBeFalsy();
expect(isWorkflowInputSamplesSchema(null)).toBeFalsy();
});
test('other workflow queries', () => {
expect(isWorkflowInputSamplesSchema(SCHEMAS.WORKFLOW.JOB)).toBeFalsy();
expect(isWorkflowInputSamplesSchema(SCHEMAS.WORKFLOW.JOB_PRIORITY)).toBeFalsy();
});
test('job input samples', () => {
const sq = SCHEMAS.WORKFLOW.JOB_INPUT_SAMPLES;
const details = new SchemaQuery(sq.schemaName, sq.queryName, ViewInfo.DETAIL_NAME);
const otherView = new SchemaQuery(sq.schemaName.toUpperCase(), sq.queryName.toUpperCase(), 'otherView');
expect(isWorkflowInputSamplesSchema(sq)).toBeTruthy();
expect(isWorkflowInputSamplesSchema(details)).toBeTruthy();
expect(isWorkflowInputSamplesSchema(otherView)).toBeTruthy();
});
});

describe('getSampleStatusType', () => {
test('no data', () => {
expect(getSampleStatusType(undefined)).toBeUndefined();
Expand All @@ -356,22 +426,29 @@ describe('getSampleStatusType', () => {
describe('getSampleStatusFromSampleRow', () => {
test('label', () => {
expect(getSampleStatusFromSampleRow({}).label).toBeUndefined();
expect(getSampleStatusFromSampleRow({ SampleState: { displayValue: undefined } }).label).toBeUndefined();
expect(getSampleStatusFromSampleRow({ SampleState: { displayValue: 'Label1' } }).label).toBe('Label1');
expect(
getSampleStatusFromSampleRow({ 'SampleID/SampleState': { displayValue: undefined } }).label
getSampleStatusFromSampleRow({ SampleState: { displayValue: undefined, value: undefined } }).label
).toBeUndefined();
expect(getSampleStatusFromSampleRow({ 'SampleID/SampleState': { displayValue: 'Label2' } }).label).toBe(
'Label2'
expect(getSampleStatusFromSampleRow({ SampleState: { displayValue: 'Label1', value: undefined } }).label).toBe(
'Label1'
);
expect(
getSampleStatusFromSampleRow({ 'SampleID/SampleState': { displayValue: undefined, value: undefined } })
.label
).toBeUndefined();
expect(
getSampleStatusFromSampleRow({ 'SampleID/SampleState': { displayValue: 'Label2', value: undefined } }).label
).toBe('Label2');
});

test('description', () => {
expect(getSampleStatusFromSampleRow({}).description).toBeUndefined();
expect(
getSampleStatusFromSampleRow({ 'SampleState/Description': { value: undefined } }).description
).toBeUndefined();
expect(getSampleStatusFromSampleRow({ 'SampleState/Description': { value: 'Desc1' } }).description).toBe('Desc1');
expect(getSampleStatusFromSampleRow({ 'SampleState/Description': { value: 'Desc1' } }).description).toBe(
'Desc1'
);
expect(
getSampleStatusFromSampleRow({ 'SampleID/SampleState/Description': { value: undefined } }).description
).toBeUndefined();
Expand All @@ -384,8 +461,8 @@ describe('getSampleStatusFromSampleRow', () => {
describe('getSampleStatus', () => {
test('label', () => {
expect(getSampleStatus({}).label).toBeUndefined();
expect(getSampleStatus({ Label: { displayValue: undefined } }).label).toBeUndefined();
expect(getSampleStatus({ Label: { displayValue: 'Label3' } }).label).toBeUndefined();
expect(getSampleStatus({ Label: { displayValue: undefined, value: undefined } }).label).toBeUndefined();
expect(getSampleStatus({ Label: { displayValue: 'Label3', value: undefined } }).label).toBeUndefined();
expect(getSampleStatus({ Label: { value: 'Label3' } }).label).toBe('Label3');
});

Expand Down
Loading