From 5166c878776ad50b74840c71aeae29ba9b62377e Mon Sep 17 00:00:00 2001 From: alanv Date: Wed, 18 Feb 2026 16:37:30 -0600 Subject: [PATCH 01/12] Remove app-page styles (moved to ui-premium) --- packages/components/src/theme/app/app.scss | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/components/src/theme/app/app.scss b/packages/components/src/theme/app/app.scss index 592b29aca3..f7bf6e1fd6 100644 --- a/packages/components/src/theme/app/app.scss +++ b/packages/components/src/theme/app/app.scss @@ -19,18 +19,6 @@ scroll-margin: 97px; } -.app-page { - padding-top: 20px; - - // GitHub Issue #734: inline-comment footer can take up to 125px on small screens - @media (max-width: 774px) { - padding-bottom: 130px; - } - @media (min-width: 775px) { - padding-bottom: 80px; - } -} - .page-header { margin: 0 0 15px 0; padding: 0; From 0ddb50916c7a7763741883c11572e71d66de8911 Mon Sep 17 00:00:00 2001 From: alanv Date: Wed, 18 Feb 2026 16:37:39 -0600 Subject: [PATCH 02/12] Add ta-left, ta-right util styles --- packages/components/src/theme/utils.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/components/src/theme/utils.scss b/packages/components/src/theme/utils.scss index 910bf343b1..b320d93e85 100644 --- a/packages/components/src/theme/utils.scss +++ b/packages/components/src/theme/utils.scss @@ -184,6 +184,14 @@ font-size: $font-size-large; } +.ta-left { + text-align: left; +} + +.ta-right { + text-align: right; +} + /** Other **/ .pointer { From a7ad8ec9a720fa4ad286e6eeb38b5e45fce86cf3 Mon Sep 17 00:00:00 2001 From: alanv Date: Wed, 18 Feb 2026 16:37:45 -0600 Subject: [PATCH 03/12] Updat release notes --- packages/components/releaseNotes/components.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index d1a361d5e3..ad08db95b4 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -1,6 +1,12 @@ # @labkey/components Components, models, actions, and utility functions for LabKey applications and pages +### version 7.?.? +*Released*: ???? +- Remove styles for `.app-page` + - Component was moved to ui-premium a while ago +- Add `ta-right`, `ta-left` util styles + ### version 7.21.0 *Released*: 26 February 2026 - Package updates From 12df3c70698f5c7a7adbd4d9db6134798b30a071 Mon Sep 17 00:00:00 2001 From: alanv Date: Mon, 23 Feb 2026 09:26:30 -0600 Subject: [PATCH 04/12] Popover: lint file --- packages/components/src/internal/Popover.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/src/internal/Popover.tsx b/packages/components/src/internal/Popover.tsx index 88dfecb242..8952cd7577 100644 --- a/packages/components/src/internal/Popover.tsx +++ b/packages/components/src/internal/Popover.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import { useOverlayPositioning } from './useOverlayPositioning'; import { TooltipProps } from './Tooltip'; -interface PopoverProps extends TooltipProps, PropsWithChildren { +interface PopoverProps extends PropsWithChildren, TooltipProps { className?: string; isFlexPlacement?: boolean; title?: string; @@ -30,7 +30,7 @@ export const Popover: FC = props => { }); return ( -
+
{title &&

{title}

}
{children}
From 68efec7b6f150b15b858835767c9f71c3fa25bfd Mon Sep 17 00:00:00 2001 From: alanv Date: Mon, 23 Feb 2026 09:26:52 -0600 Subject: [PATCH 05/12] PageDetailHeader: add deprecation comment --- .../src/internal/components/forms/PageDetailHeader.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/components/src/internal/components/forms/PageDetailHeader.tsx b/packages/components/src/internal/components/forms/PageDetailHeader.tsx index 3a819196a9..3d000a13c8 100644 --- a/packages/components/src/internal/components/forms/PageDetailHeader.tsx +++ b/packages/components/src/internal/components/forms/PageDetailHeader.tsx @@ -28,6 +28,9 @@ interface PageDetailHeaderProps extends PropsWithChildren { title: ReactNode; } +/** + * @deprecated use AppPageHeader in ui-premium instead + */ export class PageDetailHeader extends PureComponent { static defaultProps = { leftColumns: 6, From 7520ed498afff3b6af3777c84f8b2febf3919ceb Mon Sep 17 00:00:00 2001 From: alanv Date: Tue, 24 Feb 2026 13:40:38 -0600 Subject: [PATCH 06/12] Update build to use pyproject.toml and hatch Remove setup.py --- .../components/lineage/node/LineageDetail.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/components/src/internal/components/lineage/node/LineageDetail.tsx b/packages/components/src/internal/components/lineage/node/LineageDetail.tsx index a1ff6a7255..003daf0724 100644 --- a/packages/components/src/internal/components/lineage/node/LineageDetail.tsx +++ b/packages/components/src/internal/components/lineage/node/LineageDetail.tsx @@ -13,6 +13,14 @@ import { InjectedQueryModels, QueryConfigMap, withQueryModels } from '../../../. import { SAMPLE_STATE_COLOR_COLUMN_NAME, SAMPLE_STATE_TYPE_COLUMN_NAME } from '../../samples/constants'; const ADDITIONAL_DETAIL_FIELDS = ['properties']; +const IDENTIFIED_COLUMN_NAME = 'identified'; +// Must specify '*' columns be requested to resolve "properties" columns +const LINEAGE_DETAIL_REQUIRED_COLS = [ + '*', + SAMPLE_STATE_COLOR_COLUMN_NAME, + SAMPLE_STATE_TYPE_COLUMN_NAME, + IDENTIFIED_COLUMN_NAME, +]; export interface LineageDetailProps { item: Experiment.LineageItemBase; @@ -54,8 +62,7 @@ export const LineageDetail: FC = memo(({ item }) => { containerPath: item.containerPath, // Issue 45028: Display details view columns in lineage schemaQuery: new SchemaQuery(item.schemaName, item.queryName, ViewInfo.DETAIL_NAME), - // Must specify '*' columns be requested to resolve "properties" columns - requiredColumns: ['*', SAMPLE_STATE_COLOR_COLUMN_NAME, SAMPLE_STATE_TYPE_COLUMN_NAME], + requiredColumns: LINEAGE_DETAIL_REQUIRED_COLS, }, }; }, [item]); From 8bd476f14bc27503b992965989c53138abc0579f Mon Sep 17 00:00:00 2001 From: alanv Date: Wed, 25 Feb 2026 12:44:47 -0600 Subject: [PATCH 07/12] Add UnidentifiedPill, EMPTY_SEQUENCE_WARNING --- .../components/releaseNotes/components.md | 2 ++ packages/components/src/index.ts | 8 +++-- .../src/internal/UnidentifiedPill.tsx | 35 +++++++++++++++++++ packages/components/src/internal/constants.ts | 3 ++ packages/components/src/theme/pills.scss | 13 +++++++ 5 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 packages/components/src/internal/UnidentifiedPill.tsx diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index ad08db95b4..fa084bd0ce 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -6,6 +6,8 @@ Components, models, actions, and utility functions for LabKey applications and p - Remove styles for `.app-page` - Component was moved to ui-premium a while ago - Add `ta-right`, `ta-left` util styles +- Add UnidentifiedPill +- Add EMPTY_SEQUENCE_WARNING constant ### version 7.21.0 *Released*: 26 February 2026 diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 65b3fab597..0eef8f5d20 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -247,6 +247,7 @@ import { AssayUploadTabs, DataViewInfoTypes, EDIT_METHOD, + EMPTY_SEQUENCE_WARNING, EXPORT_TYPES, GRID_CHECKBOX_OPTIONS, IMPORT_DATA_FORM_TYPES, @@ -893,6 +894,7 @@ import { ArchivedFolderTag } from './internal/components/folder/ArchivedFolderTa import { FilterCriteriaRenderer } from './internal/FilterCriteriaRenderer'; import { getQueryTestAPIWrapper } from './internal/query/APIWrapper'; import { useLoadableState } from './internal/useLoadableState'; +import { UnidentifiedPill } from './internal/UnidentifiedPill'; // See Immer docs for why we do this: https://immerjs.github.io/immer/docs/installation#pick-your-immer-version enableMapSet(); @@ -1263,6 +1265,7 @@ export { EditInlineField, EditorMode, EditorModel, + EMPTY_SEQUENCE_WARNING, encodeFormDataQuote, encodePart, ensureAllFieldsInAllRows, @@ -1358,6 +1361,7 @@ export { getHelpLink, getInactiveUsers, getInitialParentChoices, + getIntegerSearchParam, getJsonDateFormatString, getJsonDateTimeFormatString, getJsonFormatString, @@ -1369,7 +1373,6 @@ export { getMetricUnitOptions, getModuleCustomLabels, getNonStandardFormatWarning, - getIntegerSearchParam, getOmittedSampleTypeColumns, getOperationNotAllowedMessage, getOperationNotAllowedMessageFromCounts, @@ -1413,6 +1416,7 @@ export { getSelectedRows, getSourceDomainDefaultSystemFields, getTestAPIWrapper, + getTextAlignClassName, getTimelineEntityUrl, getUniqueIdColumnMetadata, getUsersWithPermissions, @@ -1424,7 +1428,6 @@ export { GlobalStateContextProvider, Grid, GRID_CHECKBOX_OPTIONS, - getTextAlignClassName, GridColumn, GridPanel, GridPanelWithModel, @@ -1713,6 +1716,7 @@ export { Tooltip, TransactionAuditIdRenderer, uncapitalizeFirstChar, + UnidentifiedPill, UNIQUE_ID_FIND_FIELD, UnitModel, updateCellKeySampleIdMap, diff --git a/packages/components/src/internal/UnidentifiedPill.tsx b/packages/components/src/internal/UnidentifiedPill.tsx new file mode 100644 index 0000000000..4ba3c8f45d --- /dev/null +++ b/packages/components/src/internal/UnidentifiedPill.tsx @@ -0,0 +1,35 @@ +import React, { FC, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { generateId } from './util/utils'; +import { useOverlayTriggerState } from './OverlayTrigger'; +import { Popover } from './Popover'; +import { EMPTY_SEQUENCE_WARNING } from './constants'; + +export const UnidentifiedPill: FC = () => { + const id = useMemo(() => generateId('unidentified-sequence-overlay-trigger'), []); + // 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 popover = useMemo( + () => ( + +
{EMPTY_SEQUENCE_WARNING}
+
+ ), + [targetRef] + ); + + return ( +
+ Unidentified + + {show && createPortal(popover, portalEl)} +
+ ); +}; diff --git a/packages/components/src/internal/constants.ts b/packages/components/src/internal/constants.ts index 7a0cbf2acc..81cd83b81d 100644 --- a/packages/components/src/internal/constants.ts +++ b/packages/components/src/internal/constants.ts @@ -260,3 +260,6 @@ export const VIEW_NOT_FOUND_EXCEPTION_CLASS = 'org.labkey.api.view.NotFoundExcep export const APP_FIELD_CANNOT_BE_REMOVED_MESSAGE = 'This application field cannot be removed.'; export const CELL_SELECTION_HANDLE_CLASSNAME = 'cell-selection-handle'; + +export const EMPTY_SEQUENCE_WARNING = + "Without a sequence, Protein sequence translations can't be done automatically, and the system can't prevent duplicates."; diff --git a/packages/components/src/theme/pills.scss b/packages/components/src/theme/pills.scss index 706cc059f9..12969c7365 100644 --- a/packages/components/src/theme/pills.scss +++ b/packages/components/src/theme/pills.scss @@ -6,7 +6,11 @@ white-space: break-spaces; border-style: solid; border-width: 1px; + cursor: default; + // It's important to set font-size and weight because we often use status-pill components in headers, titles, or + // other areas where the font-size and weight are set to something large and bold. font-size: 12px; + font-weight: normal; &.disabled { background-color: $gray-lighter; @@ -65,3 +69,12 @@ text-align:center; margin-left: 0.2em; } + +// The styles below are in the pills.scss file because they're part of the UnidentifiedPill component +.unidentified-sequence-pill > .fa { + margin-left: 4px; +} + +.unidentified-sequence-popover { + max-width: 250px; +} From 007592b0801aa5a8bce6c63d0abbdda407b0f88f Mon Sep 17 00:00:00 2001 From: alanv Date: Wed, 25 Feb 2026 12:53:15 -0600 Subject: [PATCH 08/12] NodeDetailHeader: Render UnidentifiedPill for unidentified sequences LineageDetail: don't load query model for node LineageNodeDetail: load query model for node --- .../components/lineage/node/LineageDetail.tsx | 60 ++++------------ .../lineage/node/LineageNodeDetail.tsx | 71 ++++++++++++++++--- .../lineage/node/NodeDetailHeader.tsx | 23 +++++- packages/components/src/theme/lineage.scss | 4 ++ 4 files changed, 99 insertions(+), 59 deletions(-) diff --git a/packages/components/src/internal/components/lineage/node/LineageDetail.tsx b/packages/components/src/internal/components/lineage/node/LineageDetail.tsx index 003daf0724..29ee2e7c73 100644 --- a/packages/components/src/internal/components/lineage/node/LineageDetail.tsx +++ b/packages/components/src/internal/components/lineage/node/LineageDetail.tsx @@ -6,74 +6,40 @@ import { Renderer, resolveDetailRenderer } from '../../forms/detail/DetailDispla import { LoadingSpinner } from '../../base/LoadingSpinner'; import { Alert } from '../../base/Alert'; import { DetailPanel } from '../../../../public/QueryModel/DetailPanel'; -import { SchemaQuery } from '../../../../public/SchemaQuery'; -import { ViewInfo } from '../../../ViewInfo'; import { QueryColumn } from '../../../../public/QueryColumn'; -import { InjectedQueryModels, QueryConfigMap, withQueryModels } from '../../../../public/QueryModel/withQueryModels'; -import { SAMPLE_STATE_COLOR_COLUMN_NAME, SAMPLE_STATE_TYPE_COLUMN_NAME } from '../../samples/constants'; +import { QueryModel } from '../../../../public/QueryModel/QueryModel'; const ADDITIONAL_DETAIL_FIELDS = ['properties']; -const IDENTIFIED_COLUMN_NAME = 'identified'; -// Must specify '*' columns be requested to resolve "properties" columns -const LINEAGE_DETAIL_REQUIRED_COLS = [ - '*', - SAMPLE_STATE_COLOR_COLUMN_NAME, - SAMPLE_STATE_TYPE_COLUMN_NAME, - IDENTIFIED_COLUMN_NAME, -]; export interface LineageDetailProps { item: Experiment.LineageItemBase; + model: QueryModel; } -const LineageDetailImpl: FC = memo(props => { - const { queryModels } = props; - if (queryModels.model.isLoading) return ; - if (queryModels.model.hasLoadErrors) return {queryModels.model.loadErrors[0]}; +export const LineageDetail: FC = memo(({ item, model }) => { + if (item.restricted) { + return This {item.name} cannot be viewed.; + } - // Issue 50537: only show the "Properties" column in the detail view for the exp schema - const isExpSchema = queryModels.model.schemaName === 'exp'; + if (model.isLoading) return ; + if (model.hasLoadErrors) return {model.loadErrors[0]}; + // Issue 50537: only show the "Properties" column in the detail view for the exp schema + const isExpSchema = model.schemaName === 'exp'; const additionalCols = isExpSchema - ? queryModels.model.allColumns.filter(col => ADDITIONAL_DETAIL_FIELDS.indexOf(col.fieldKey?.toLowerCase()) > -1) + ? model.allColumns.filter(col => ADDITIONAL_DETAIL_FIELDS.indexOf(col.fieldKey?.toLowerCase()) > -1) : []; - const detailColumns = [...queryModels.model.detailColumns, ...additionalCols]; + const detailColumns = [...model.detailColumns, ...additionalCols]; return ( ); }); -LineageDetailImpl.displayName = 'LineageDetailImpl'; - -const LineageDetailWithQueryModels = withQueryModels(LineageDetailImpl); - -export const LineageDetail: FC = memo(({ item }) => { - const queryConfigs = useMemo(() => { - if (item.restricted) return {}; - - return { - model: { - baseFilters: item.pkFilters.map(pkFilter => Filter.create(pkFilter.fieldKey, pkFilter.value)), - containerPath: item.containerPath, - // Issue 45028: Display details view columns in lineage - schemaQuery: new SchemaQuery(item.schemaName, item.queryName, ViewInfo.DETAIL_NAME), - requiredColumns: LINEAGE_DETAIL_REQUIRED_COLS, - }, - }; - }, [item]); - - if (item.restricted) { - return This {item.name} cannot be viewed.; - } - - // providing "key" to allow for reload on lsid change - return ; -}); LineageDetail.displayName = 'LineageDetail'; interface RendererProps { diff --git a/packages/components/src/internal/components/lineage/node/LineageNodeDetail.tsx b/packages/components/src/internal/components/lineage/node/LineageNodeDetail.tsx index be76843b96..0d82240051 100644 --- a/packages/components/src/internal/components/lineage/node/LineageNodeDetail.tsx +++ b/packages/components/src/internal/components/lineage/node/LineageNodeDetail.tsx @@ -25,6 +25,22 @@ import { hasModule } from '../../../app/utils'; import { LineageDetail } from './LineageDetail'; import { DetailHeader, NodeDetailHeader } from './NodeDetailHeader'; import { DetailsListLineageIO, DetailsListNodes, DetailsListSteps } from './DetailsList'; +import { InjectedQueryModels, QueryConfigMap, withQueryModels } from '../../../../public/QueryModel/withQueryModels'; +import { Filter } from '@labkey/api'; +import { SchemaQuery } from '../../../../public/SchemaQuery'; +import { ViewInfo } from '../../../ViewInfo'; +import { SAMPLE_STATE_COLOR_COLUMN_NAME, SAMPLE_STATE_TYPE_COLUMN_NAME } from '../../samples/constants'; +import { QueryModel } from '../../../../public/QueryModel/QueryModel'; + +const IDENTIFIED_COLUMN_NAME = 'identified'; + +// Must specify '*' columns be requested to resolve "properties" columns +const LINEAGE_DETAIL_REQUIRED_COLS = [ + '*', + SAMPLE_STATE_COLOR_COLUMN_NAME, + SAMPLE_STATE_TYPE_COLUMN_NAME, + IDENTIFIED_COLUMN_NAME, +]; interface LineageNodeDetailProps { highlightNode?: string; @@ -33,27 +49,28 @@ interface LineageNodeDetailProps { seed: string; } -export const LineageNodeDetail: FC = memo(props => { - const { seed, node, highlightNode, lineageOptions } = props; - const { isRun, restricted } = node; +const LineageNodeDetailImpl: FC = memo(props => { + const { seed, node, highlightNode, lineageOptions, queryModels } = props; + const { containerPath, isRun, lsid, restricted } = node; + const model = queryModels.model; const [stepIdx, setStepIdx] = useState(undefined); const [tabKey, setTabKey] = useState('details'); const onBack = useCallback(() => setStepIdx(undefined), []); if (isRun && stepIdx !== undefined) { - return ; + return ; } const nodeDetails = ( <> - + {!restricted && ( )} @@ -62,7 +79,7 @@ export const LineageNodeDetail: FC = memo(props => { return (
- + {isRun && !restricted ? ( @@ -79,6 +96,37 @@ export const LineageNodeDetail: FC = memo(props => {
); }); +LineageNodeDetailImpl.displayName = 'LineageNodeDetailImpl'; + +const LineageNodeDetailWithModels = withQueryModels(LineageNodeDetailImpl); + +export const LineageNodeDetail: FC = memo(props => { + const { highlightNode, lineageOptions, node, seed } = props; + const queryConfigs = useMemo(() => { + if (node.restricted) return {}; + + return { + model: { + baseFilters: node.pkFilters.map(pkFilter => Filter.create(pkFilter.fieldKey, pkFilter.value)), + containerPath: node.containerPath, + // Issue 45028: Display details view columns in lineage + schemaQuery: new SchemaQuery(node.schemaName, node.queryName, ViewInfo.DETAIL_NAME), + requiredColumns: LINEAGE_DETAIL_REQUIRED_COLS, + }, + }; + }, [node]); + + return ( + + ); +}); LineageNodeDetail.displayName = 'LineageNodeDetail'; interface ClusterNodeDetailProps { @@ -128,13 +176,14 @@ export const ClusterNodeDetail: FC = memo(props => { ClusterNodeDetail.displayName = 'ClusterNodeDetail'; interface RunStepNodeDetailProps { + model: QueryModel; node: LineageNode; onBack: () => void; stepIdx: number; } const RunStepNodeDetail: FC = memo(props => { - const { node, onBack, stepIdx } = props; + const { model, node, onBack, stepIdx } = props; const [tabKey, setTabKey] = useState('details'); const step = node.steps.get(stepIdx); const stepName = step.protocol?.name || step.name; @@ -155,7 +204,7 @@ const RunStepNodeDetail: FC = memo(props => { - + {hasProvenanceModule && ( diff --git a/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx b/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx index 5fcf1b0684..fa5447bc17 100644 --- a/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx +++ b/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx @@ -3,6 +3,11 @@ import React, { FC, memo, PropsWithChildren, ReactNode } from 'react'; import { LineageNode } from '../models'; import { LineageDataLink } from '../LineageDataLink'; import { SVGIcon, Theme } from '../../base/SVGIcon'; +import { QueryModel } from '../../../../public/QueryModel/QueryModel'; +import { caseInsensitive } from '../../../util/utils'; +import { SCHEMAS } from '../../../schemas'; +import { SchemaQuery } from '../../../../public/SchemaQuery'; +import { UnidentifiedPill } from '../../../UnidentifiedPill'; export interface DetailHeaderProps extends PropsWithChildren { header: ReactNode; @@ -25,11 +30,12 @@ export const DetailHeader: FC = memo(({ children, header, ico DetailHeader.displayName = 'DetailHeader'; export interface NodeDetailHeaderProps { + model: QueryModel; node: LineageNode; seed?: string; } -export const NodeDetailHeader: FC = memo(({ node, seed }) => { +export const NodeDetailHeader: FC = memo(({ model, node, seed }) => { const { links, meta, name } = node; const lineageUrl = links.lineage; const isSeed = seed === node.lsid; @@ -37,6 +43,19 @@ export const NodeDetailHeader: FC = memo(({ node, seed }) const aliases = meta?.aliases; const description = meta?.description; const displayType = meta?.displayType; + let identified: boolean; + + if (model && !model.isLoading && !model.hasLoadErrors) { + // Drop the viewName from the schemaQuery so we can properly compare + const sq = new SchemaQuery(model.schemaQuery.schemaName, model.schemaQuery.queryName); + const isNucSeq = sq.isEqual(SCHEMAS.DATA_CLASSES.NUC_SEQUENCE); + const isProtSeq = sq.isEqual(SCHEMAS.DATA_CLASSES.PROTEIN_SEQUENCE); + const isMolSpecSeq = sq.isEqual(SCHEMAS.DATA_CLASSES.MOLECULAR_SPECIES_SEQ); + + if (isNucSeq || isProtSeq || isMolSpecSeq) { + identified = caseInsensitive(model.getRow(), 'identified')?.value; + } + } const header = ( <> @@ -53,6 +72,8 @@ export const NodeDetailHeader: FC = memo(({ node, seed }) {displayType &&
{displayType}
} {aliases &&
{aliases.join(', ')}
} {description &&
{description}
} + {/* Triple eq is important here; we only want false, not falsey values */} + {identified === false && } ); }); diff --git a/packages/components/src/theme/lineage.scss b/packages/components/src/theme/lineage.scss index 3ebbaf4982..8308bf6519 100644 --- a/packages/components/src/theme/lineage.scss +++ b/packages/components/src/theme/lineage.scss @@ -149,6 +149,10 @@ width: 100%; } +.lineage-detail-header .unidentified-sequence-pill { + margin-top: 8px; +} + .lineage-sm-icon { height: 1.2em; margin: 0.1em; From eb0335ddad43fe4e19fa27d217caba1c903778a9 Mon Sep 17 00:00:00 2001 From: alanv Date: Wed, 25 Feb 2026 13:04:14 -0600 Subject: [PATCH 09/12] LineageNodeMetadata: use value for alias column --- packages/components/src/internal/components/lineage/models.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/internal/components/lineage/models.ts b/packages/components/src/internal/components/lineage/models.ts index 46936ac84f..459a422538 100644 --- a/packages/components/src/internal/components/lineage/models.ts +++ b/packages/components/src/internal/components/lineage/models.ts @@ -104,7 +104,7 @@ export class LineageNodeMetadata extends ImmutableRecord({ let aliases; if (selectRowsMetadata.has('Alias')) { - aliases = selectRowsMetadata.get('Alias').map(alias => alias.get('displayValue')); + aliases = selectRowsMetadata.get('Alias').map(alias => alias.get('value')); } return new LineageNodeMetadata({ From f02217d3dc7368af363ea898f8c46fc0b33ff32f Mon Sep 17 00:00:00 2001 From: alanv Date: Wed, 25 Feb 2026 15:02:25 -0600 Subject: [PATCH 10/12] Add hasSequenceCol, use it in NodeDetailHeader --- packages/components/src/index.ts | 2 ++ .../components/lineage/node/NodeDetailHeader.tsx | 16 +++------------- packages/components/src/internal/util/utils.ts | 11 +++++++++++ 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 0eef8f5d20..f761e02432 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -59,6 +59,7 @@ import { getValueFromRow, getValuesSummary, handleFileInputChange, + hasSequenceCol, isImage, isInteger, isIntegerInRange, @@ -1441,6 +1442,7 @@ export { hasAnyPermissions, hasParameter, hasPermissions, + hasSequenceCol, Help, HELP_LINK_REFERRER, HelpIcon, diff --git a/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx b/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx index fa5447bc17..2ada5a6ca4 100644 --- a/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx +++ b/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx @@ -4,9 +4,7 @@ import { LineageNode } from '../models'; import { LineageDataLink } from '../LineageDataLink'; import { SVGIcon, Theme } from '../../base/SVGIcon'; import { QueryModel } from '../../../../public/QueryModel/QueryModel'; -import { caseInsensitive } from '../../../util/utils'; -import { SCHEMAS } from '../../../schemas'; -import { SchemaQuery } from '../../../../public/SchemaQuery'; +import { caseInsensitive, hasSequenceCol } from '../../../util/utils'; import { UnidentifiedPill } from '../../../UnidentifiedPill'; export interface DetailHeaderProps extends PropsWithChildren { @@ -45,16 +43,8 @@ export const NodeDetailHeader: FC = memo(({ model, node, const displayType = meta?.displayType; let identified: boolean; - if (model && !model.isLoading && !model.hasLoadErrors) { - // Drop the viewName from the schemaQuery so we can properly compare - const sq = new SchemaQuery(model.schemaQuery.schemaName, model.schemaQuery.queryName); - const isNucSeq = sq.isEqual(SCHEMAS.DATA_CLASSES.NUC_SEQUENCE); - const isProtSeq = sq.isEqual(SCHEMAS.DATA_CLASSES.PROTEIN_SEQUENCE); - const isMolSpecSeq = sq.isEqual(SCHEMAS.DATA_CLASSES.MOLECULAR_SPECIES_SEQ); - - if (isNucSeq || isProtSeq || isMolSpecSeq) { - identified = caseInsensitive(model.getRow(), 'identified')?.value; - } + if (model && !model.isLoading && !model.hasLoadErrors && hasSequenceCol(model.schemaQuery)) { + identified = caseInsensitive(model.getRow(), 'identified')?.value; } const header = ( diff --git a/packages/components/src/internal/util/utils.ts b/packages/components/src/internal/util/utils.ts index 378b2fb605..d2ee95ef0a 100644 --- a/packages/components/src/internal/util/utils.ts +++ b/packages/components/src/internal/util/utils.ts @@ -20,6 +20,8 @@ import { ChangeEvent, CSSProperties } from 'react'; import { hasParameter, toggleParameter } from '../url/ActionURL'; import { QueryInfo } from '../../public/QueryInfo'; import { STORED_AMOUNT_FIELDS } from '../components/samples/constants'; +import { SchemaQuery } from '../../public/SchemaQuery'; +import { SCHEMAS } from '../schemas'; // Case-insensitive Object reference. Returns undefined if either object or prop does not resolve. // If both casings exist (e.g. 'x' and 'X' are props) then either value may be returned. @@ -900,3 +902,12 @@ export const setIsTestEnv = (isTestEnv: boolean): void => { }; export const isTestEnv = (): boolean => IS_NODE_TEST_ENV || IS_TEST_ENV; + +export function hasSequenceCol(schemaQuery: SchemaQuery): boolean { + // Drop the viewName from the schemaQuery so we can properly compare + const sqNoView = new SchemaQuery(schemaQuery.schemaName, schemaQuery.queryName); + const isNucSeq = sqNoView.isEqual(SCHEMAS.DATA_CLASSES.NUC_SEQUENCE); + const isProtSeq = sqNoView.isEqual(SCHEMAS.DATA_CLASSES.PROTEIN_SEQUENCE); + const isMolecule = sqNoView.isEqual(SCHEMAS.DATA_CLASSES.MOLECULE); + return isNucSeq || isProtSeq || isMolecule; +} From ac23a2c57c664a9517e1474a3888c00de0a373c7 Mon Sep 17 00:00:00 2001 From: alanv Date: Thu, 26 Feb 2026 16:01:29 -0600 Subject: [PATCH 11/12] NodeDetailHeader: don't render alias, description. Move unidentified pill --- .../src/internal/components/lineage/constants.tsx | 11 +++++++++++ .../components/lineage/node/LineageNodeDetail.tsx | 11 +---------- .../components/lineage/node/NodeDetailHeader.tsx | 9 +++------ packages/components/src/theme/lineage.scss | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/components/src/internal/components/lineage/constants.tsx b/packages/components/src/internal/components/lineage/constants.tsx index 7f9598d92d..10622ea946 100644 --- a/packages/components/src/internal/components/lineage/constants.tsx +++ b/packages/components/src/internal/components/lineage/constants.tsx @@ -16,6 +16,7 @@ import { LineageOptions, LineageURLResolvers, } from './types'; +import { SAMPLE_STATE_COLOR_COLUMN_NAME, SAMPLE_STATE_TYPE_COLUMN_NAME } from '../samples/constants'; // Default depth to fetch with the lineage API export const DEFAULT_LINEAGE_DISTANCE = 5; @@ -159,3 +160,13 @@ export const LINEAGE_GRID_COLUMNS = List([ title: 'Alias', }), ]); + +export const IDENTIFIED_COLUMN_NAME = 'identified'; + +// Must specify '*' columns be requested to resolve "properties" columns +export const LINEAGE_DETAIL_REQUIRED_COLS = [ + '*', + SAMPLE_STATE_COLOR_COLUMN_NAME, + SAMPLE_STATE_TYPE_COLUMN_NAME, + IDENTIFIED_COLUMN_NAME, +]; diff --git a/packages/components/src/internal/components/lineage/node/LineageNodeDetail.tsx b/packages/components/src/internal/components/lineage/node/LineageNodeDetail.tsx index 0d82240051..9151396cfb 100644 --- a/packages/components/src/internal/components/lineage/node/LineageNodeDetail.tsx +++ b/packages/components/src/internal/components/lineage/node/LineageNodeDetail.tsx @@ -29,18 +29,9 @@ import { InjectedQueryModels, QueryConfigMap, withQueryModels } from '../../../. import { Filter } from '@labkey/api'; import { SchemaQuery } from '../../../../public/SchemaQuery'; import { ViewInfo } from '../../../ViewInfo'; -import { SAMPLE_STATE_COLOR_COLUMN_NAME, SAMPLE_STATE_TYPE_COLUMN_NAME } from '../../samples/constants'; import { QueryModel } from '../../../../public/QueryModel/QueryModel'; -const IDENTIFIED_COLUMN_NAME = 'identified'; - -// Must specify '*' columns be requested to resolve "properties" columns -const LINEAGE_DETAIL_REQUIRED_COLS = [ - '*', - SAMPLE_STATE_COLOR_COLUMN_NAME, - SAMPLE_STATE_TYPE_COLUMN_NAME, - IDENTIFIED_COLUMN_NAME, -]; +import { LINEAGE_DETAIL_REQUIRED_COLS } from '../constants'; interface LineageNodeDetailProps { highlightNode?: string; diff --git a/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx b/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx index 2ada5a6ca4..b25f447944 100644 --- a/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx +++ b/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx @@ -6,6 +6,7 @@ import { SVGIcon, Theme } from '../../base/SVGIcon'; import { QueryModel } from '../../../../public/QueryModel/QueryModel'; import { caseInsensitive, hasSequenceCol } from '../../../util/utils'; import { UnidentifiedPill } from '../../../UnidentifiedPill'; +import { IDENTIFIED_COLUMN_NAME } from '../constants'; export interface DetailHeaderProps extends PropsWithChildren { header: ReactNode; @@ -38,13 +39,11 @@ export const NodeDetailHeader: FC = memo(({ model, node, const lineageUrl = links.lineage; const isSeed = seed === node.lsid; - const aliases = meta?.aliases; - const description = meta?.description; const displayType = meta?.displayType; let identified: boolean; if (model && !model.isLoading && !model.hasLoadErrors && hasSequenceCol(model.schemaQuery)) { - identified = caseInsensitive(model.getRow(), 'identified')?.value; + identified = caseInsensitive(model.getRow(), IDENTIFIED_COLUMN_NAME)?.value; } const header = ( @@ -59,11 +58,9 @@ export const NodeDetailHeader: FC = memo(({ model, node, return ( - {displayType &&
{displayType}
} - {aliases &&
{aliases.join(', ')}
} - {description &&
{description}
} {/* Triple eq is important here; we only want false, not falsey values */} {identified === false && } + {displayType &&
{displayType}
}
); }); diff --git a/packages/components/src/theme/lineage.scss b/packages/components/src/theme/lineage.scss index 8308bf6519..d3f8928967 100644 --- a/packages/components/src/theme/lineage.scss +++ b/packages/components/src/theme/lineage.scss @@ -150,7 +150,7 @@ } .lineage-detail-header .unidentified-sequence-pill { - margin-top: 8px; + margin: 4px 0; } .lineage-sm-icon { From 67ff3a9751c0c89b36e655c5173496b782878b69 Mon Sep 17 00:00:00 2001 From: alanv Date: Thu, 26 Feb 2026 16:04:40 -0600 Subject: [PATCH 12/12] PR feedback --- packages/components/src/index.ts | 4 ++-- .../src/internal/components/lineage/node/LineageDetail.tsx | 4 ++-- .../src/internal/components/lineage/node/NodeDetailHeader.tsx | 4 ++-- packages/components/src/internal/constants.ts | 2 +- packages/components/src/internal/util/utils.ts | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index f761e02432..55247ab938 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -59,7 +59,7 @@ import { getValueFromRow, getValuesSummary, handleFileInputChange, - hasSequenceCol, + hasIdentifiedCol, isImage, isInteger, isIntegerInRange, @@ -1442,7 +1442,7 @@ export { hasAnyPermissions, hasParameter, hasPermissions, - hasSequenceCol, + hasIdentifiedCol, Help, HELP_LINK_REFERRER, HelpIcon, diff --git a/packages/components/src/internal/components/lineage/node/LineageDetail.tsx b/packages/components/src/internal/components/lineage/node/LineageDetail.tsx index 29ee2e7c73..9ec01ae4ec 100644 --- a/packages/components/src/internal/components/lineage/node/LineageDetail.tsx +++ b/packages/components/src/internal/components/lineage/node/LineageDetail.tsx @@ -1,5 +1,5 @@ -import React, { FC, memo, useMemo } from 'react'; -import { Experiment, Filter } from '@labkey/api'; +import React, { FC, memo } from 'react'; +import { Experiment } from '@labkey/api'; import { List, Map } from 'immutable'; import { Renderer, resolveDetailRenderer } from '../../forms/detail/DetailDisplay'; diff --git a/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx b/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx index b25f447944..51ee2dd1bd 100644 --- a/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx +++ b/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx @@ -4,7 +4,7 @@ import { LineageNode } from '../models'; import { LineageDataLink } from '../LineageDataLink'; import { SVGIcon, Theme } from '../../base/SVGIcon'; import { QueryModel } from '../../../../public/QueryModel/QueryModel'; -import { caseInsensitive, hasSequenceCol } from '../../../util/utils'; +import { caseInsensitive, hasIdentifiedCol } from '../../../util/utils'; import { UnidentifiedPill } from '../../../UnidentifiedPill'; import { IDENTIFIED_COLUMN_NAME } from '../constants'; @@ -42,7 +42,7 @@ export const NodeDetailHeader: FC = memo(({ model, node, const displayType = meta?.displayType; let identified: boolean; - if (model && !model.isLoading && !model.hasLoadErrors && hasSequenceCol(model.schemaQuery)) { + if (model && !model.isLoading && !model.hasLoadErrors && hasIdentifiedCol(model.schemaQuery)) { identified = caseInsensitive(model.getRow(), IDENTIFIED_COLUMN_NAME)?.value; } diff --git a/packages/components/src/internal/constants.ts b/packages/components/src/internal/constants.ts index 81cd83b81d..ef5683e637 100644 --- a/packages/components/src/internal/constants.ts +++ b/packages/components/src/internal/constants.ts @@ -262,4 +262,4 @@ export const APP_FIELD_CANNOT_BE_REMOVED_MESSAGE = 'This application field canno export const CELL_SELECTION_HANDLE_CLASSNAME = 'cell-selection-handle'; export const EMPTY_SEQUENCE_WARNING = - "Without a sequence, Protein sequence translations can't be done automatically, and the system can't prevent duplicates."; + "Without a sequence, Protein sequence translations cannot be done automatically, and the system can't prevent duplicates."; diff --git a/packages/components/src/internal/util/utils.ts b/packages/components/src/internal/util/utils.ts index d2ee95ef0a..f1efe6d3fd 100644 --- a/packages/components/src/internal/util/utils.ts +++ b/packages/components/src/internal/util/utils.ts @@ -903,7 +903,7 @@ export const setIsTestEnv = (isTestEnv: boolean): void => { export const isTestEnv = (): boolean => IS_NODE_TEST_ENV || IS_TEST_ENV; -export function hasSequenceCol(schemaQuery: SchemaQuery): boolean { +export function hasIdentifiedCol(schemaQuery: SchemaQuery): boolean { // Drop the viewName from the schemaQuery so we can properly compare const sqNoView = new SchemaQuery(schemaQuery.schemaName, schemaQuery.queryName); const isNucSeq = sqNoView.isEqual(SCHEMAS.DATA_CLASSES.NUC_SEQUENCE);