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
8 changes: 8 additions & 0 deletions packages/components/releaseNotes/components.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# @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
- Add UnidentifiedPill
- Add EMPTY_SEQUENCE_WARNING constant

### version 7.21.0
*Released*: 26 February 2026
- Package updates
Expand Down
10 changes: 8 additions & 2 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
getValueFromRow,
getValuesSummary,
handleFileInputChange,
hasIdentifiedCol,
isImage,
isInteger,
isIntegerInRange,
Expand Down Expand Up @@ -247,6 +248,7 @@ import {
AssayUploadTabs,
DataViewInfoTypes,
EDIT_METHOD,
EMPTY_SEQUENCE_WARNING,
EXPORT_TYPES,
GRID_CHECKBOX_OPTIONS,
IMPORT_DATA_FORM_TYPES,
Expand Down Expand Up @@ -893,6 +895,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();
Expand Down Expand Up @@ -1263,6 +1266,7 @@ export {
EditInlineField,
EditorMode,
EditorModel,
EMPTY_SEQUENCE_WARNING,
encodeFormDataQuote,
encodePart,
ensureAllFieldsInAllRows,
Expand Down Expand Up @@ -1358,6 +1362,7 @@ export {
getHelpLink,
getInactiveUsers,
getInitialParentChoices,
getIntegerSearchParam,
getJsonDateFormatString,
getJsonDateTimeFormatString,
getJsonFormatString,
Expand All @@ -1369,7 +1374,6 @@ export {
getMetricUnitOptions,
getModuleCustomLabels,
getNonStandardFormatWarning,
getIntegerSearchParam,
getOmittedSampleTypeColumns,
getOperationNotAllowedMessage,
getOperationNotAllowedMessageFromCounts,
Expand Down Expand Up @@ -1413,6 +1417,7 @@ export {
getSelectedRows,
getSourceDomainDefaultSystemFields,
getTestAPIWrapper,
getTextAlignClassName,
getTimelineEntityUrl,
getUniqueIdColumnMetadata,
getUsersWithPermissions,
Expand All @@ -1424,7 +1429,6 @@ export {
GlobalStateContextProvider,
Grid,
GRID_CHECKBOX_OPTIONS,
getTextAlignClassName,
GridColumn,
GridPanel,
GridPanelWithModel,
Expand All @@ -1438,6 +1442,7 @@ export {
hasAnyPermissions,
hasParameter,
hasPermissions,
hasIdentifiedCol,
Help,
HELP_LINK_REFERRER,
HelpIcon,
Expand Down Expand Up @@ -1713,6 +1718,7 @@ export {
Tooltip,
TransactionAuditIdRenderer,
uncapitalizeFirstChar,
UnidentifiedPill,
UNIQUE_ID_FIND_FIELD,
UnitModel,
updateCellKeySampleIdMap,
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/internal/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,7 +30,7 @@ export const Popover: FC<PopoverProps> = props => {
});

return (
<div id={id} className={className_} style={style} ref={overlayRef}>
<div className={className_} id={id} ref={overlayRef} style={style}>
<div className="arrow" />
{title && <h3 className="popover-title">{title}</h3>}
<div className="popover-content">{children}</div>
Expand Down
35 changes: 35 additions & 0 deletions packages/components/src/internal/UnidentifiedPill.tsx
Original file line number Diff line number Diff line change
@@ -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(
() => (
<Popover id="unidentified-sequence-popover" placement="top" targetRef={targetRef}>
<div className="unidentified-sequence-popover">{EMPTY_SEQUENCE_WARNING}</div>
</Popover>
),
[targetRef]
);

return (
<div
className="status-pill info unidentified-sequence-pill"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
ref={targetRef}
>
Unidentified
<span className="label-help-icon fa fa-question-circle" />
{show && createPortal(popover, portalEl)}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ interface PageDetailHeaderProps extends PropsWithChildren {
title: ReactNode;
}

/**
* @deprecated use AppPageHeader in ui-premium instead
*/
export class PageDetailHeader extends PureComponent<PageDetailHeaderProps> {
static defaultProps = {
leftColumns: 6,
Expand Down
11 changes: 11 additions & 0 deletions packages/components/src/internal/components/lineage/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
];
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,72 +1,45 @@
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';
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'];

export interface LineageDetailProps {
item: Experiment.LineageItemBase;
model: QueryModel;
}

const LineageDetailImpl: FC<InjectedQueryModels & LineageDetailProps> = memo(props => {
const { queryModels } = props;
if (queryModels.model.isLoading) return <LoadingSpinner />;
if (queryModels.model.hasLoadErrors) return <Alert>{queryModels.model.loadErrors[0]}</Alert>;
export const LineageDetail: FC<LineageDetailProps> = memo(({ item, model }) => {
if (item.restricted) {
return <Alert bsStyle="info">This {item.name} cannot be viewed.</Alert>;
}

// 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 <LoadingSpinner />;
if (model.hasLoadErrors) return <Alert>{model.loadErrors[0]}</Alert>;

// 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 (
<DetailPanel
detailRenderer={_resolveDetailRenderer}
model={queryModels.model}
model={model}
queryColumns={detailColumns}
tableCls="detail-component--table__auto"
/>
);
});
LineageDetailImpl.displayName = 'LineageDetailImpl';

const LineageDetailWithQueryModels = withQueryModels<LineageDetailProps>(LineageDetailImpl);

export const LineageDetail: FC<LineageDetailProps> = memo(({ item }) => {
const queryConfigs = useMemo<QueryConfigMap>(() => {
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),
// Must specify '*' columns be requested to resolve "properties" columns
requiredColumns: ['*', SAMPLE_STATE_COLOR_COLUMN_NAME, SAMPLE_STATE_TYPE_COLUMN_NAME],
},
};
}, [item]);

if (item.restricted) {
return <Alert bsStyle="info">This {item.name} cannot be viewed.</Alert>;
}

// providing "key" to allow for reload on lsid change
return <LineageDetailWithQueryModels autoLoad item={item} key={item.lsid} queryConfigs={queryConfigs} />;
});
LineageDetail.displayName = 'LineageDetail';

interface RendererProps {
Expand Down
Loading