From b6c3d4fe6076a4ee80747b87fffb452e230dce8f Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Fri, 19 Jun 2026 06:44:34 -0700 Subject: [PATCH 01/13] Updates to support filtering sample lineage graph by sample status --- packages/components/releaseNotes/components.md | 7 +++++++ packages/components/src/index.ts | 4 +++- .../internal/components/lineage/constants.tsx | 2 +- .../src/internal/components/lineage/models.ts | 16 ++++++++++------ .../src/internal/components/lineage/types.ts | 4 ++-- .../src/internal/components/samples/models.ts | 2 ++ .../src/internal/components/samples/utils.tsx | 5 +++++ packages/components/src/theme/lineage.scss | 10 ++++++++++ 8 files changed, 40 insertions(+), 10 deletions(-) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index c3fead9149..cb4a94d5c8 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 TBD +*Released*: TBD +- Support for filtering the sample lineage graph by sample status + - Add metric constant + - Add `sampleStatus` field to `LineageNode` + - Add `rowId` field to `SampleStatus` model + ### version 7.42.1 *Released*: 17 June 2026 - Package updates diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 40ce92de9d..189402af3f 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -422,7 +422,7 @@ import { TestLineageAPIWrapper, } from './internal/components/lineage/actions'; import { withLineage } from './internal/components/lineage/withLineage'; -import { DEFAULT_LINEAGE_DISTANCE } from './internal/components/lineage/constants'; +import { DEFAULT_LINEAGE_DISTANCE, LINEAGE_GRAPH_FILTER_METRIC} from './internal/components/lineage/constants'; import { LINEAGE_DIRECTIONS, LINEAGE_GROUPING_GENERATIONS, @@ -1513,6 +1513,7 @@ export { LabelHelpTip, LabelOverlay, LINEAGE_DIRECTIONS, + LINEAGE_GRAPH_FILTER_METRIC, LINEAGE_GROUPING_GENERATIONS, LineageDepthLimitMessage, LineageFilter, @@ -1891,6 +1892,7 @@ export type { SampleStatus, StorageActionStatusCounts, } from './internal/components/samples/models'; +export type { SampleState } from './internal/components/samples/models'; export type { SearchHit, SearchOptions } from './internal/components/search/actions'; export type { EntityFieldFilter } from './internal/components/search/models'; export type { SecurityAPIWrapper } from './internal/components/security/APIWrapper'; diff --git a/packages/components/src/internal/components/lineage/constants.tsx b/packages/components/src/internal/components/lineage/constants.tsx index 90a5fdf559..1742f70ffe 100644 --- a/packages/components/src/internal/components/lineage/constants.tsx +++ b/packages/components/src/internal/components/lineage/constants.tsx @@ -17,7 +17,7 @@ import { LineageURLResolvers, } from './types'; import { SAMPLE_STATE_COLOR_COLUMN_NAME, SAMPLE_STATE_TYPE_COLUMN_NAME } from '../samples/constants'; - +export const LINEAGE_GRAPH_FILTER_METRIC = "LineageGraphFilter"; // Default depth to fetch with the lineage API export const DEFAULT_LINEAGE_DISTANCE = 5; export const DEFAULT_LINEAGE_DIRECTION = LINEAGE_DIRECTIONS.Children; diff --git a/packages/components/src/internal/components/lineage/models.ts b/packages/components/src/internal/components/lineage/models.ts index 7abe9cf0e6..5c19aab5dd 100644 --- a/packages/components/src/internal/components/lineage/models.ts +++ b/packages/components/src/internal/components/lineage/models.ts @@ -291,6 +291,7 @@ export class LineageNode steps: undefined, type: undefined, materialLineageType: undefined, + sampleStatus: undefined, url: undefined, // computed properties @@ -332,6 +333,7 @@ export class LineageNode declare steps: List; declare type: string; declare materialLineageType: string; + declare sampleStatus: number; declare url: string; // computed properties @@ -400,11 +402,11 @@ export class LineageResult extends ImmutableRecord({ }); } - filterIn(field: string, value: string | string[] | undefined): LineageResult { + filterIn(field: string, value: string | string[] | number[] | number | undefined): LineageResult { return LineageResult._filter(this, field, value, true); } - filterOut(field: string, value: string | string[] | undefined): LineageResult { + filterOut(field: string, value: string | string[] | number[] | number | undefined): LineageResult { return LineageResult._filter(this, field, value, false); } @@ -418,7 +420,7 @@ export class LineageResult extends ImmutableRecord({ private static _filter( result: LineageResult, field: string, - value: string | string[] | undefined, + value: number | number[] | string | string[] | undefined, filterIn: boolean ): LineageResult { if (field === undefined) throw new Error('field must not be undefined'); @@ -457,15 +459,15 @@ export class LineageResult extends ImmutableRecord({ /** * When 'filterIn' is true, returns true if the node[field] is equal to the value or any of the array item values. - * When value is undefined, it is treated as a wildcard -- any value is allowed as long as the + * When value is undefined, it is treated as a wildcard -- any value is allowed as long as the field exists * * When 'filterIn' is false, returns true if the node[field] is not equal to the value or any of the array item values. - * When value is undefined, the node must not have contain a value for the field. + * When value is undefined, the node must not contain a value for the field. */ private static _matches( node: LineageNode, field: string, - value: string | string[] | undefined, + value: number | number[] | string | string[] | undefined, filterIn: boolean ): boolean { if (filterIn) { @@ -473,6 +475,7 @@ export class LineageResult extends ImmutableRecord({ // true if the field exists on node return node.has(field); } else if (Array.isArray(value)) { + // @ts-expect-error number or string possible return value.indexOf(node[field]) > -1; } else { return node[field] === value; @@ -482,6 +485,7 @@ export class LineageResult extends ImmutableRecord({ // true if the field does not exist on node return !node.has(field); } else if (Array.isArray(value)) { + // @ts-expect-error number or string possible return value.indexOf(node[field]) === -1; } else { return node[field] !== value; diff --git a/packages/components/src/internal/components/lineage/types.ts b/packages/components/src/internal/components/lineage/types.ts index 58fc4edf19..9171e154a6 100644 --- a/packages/components/src/internal/components/lineage/types.ts +++ b/packages/components/src/internal/components/lineage/types.ts @@ -42,9 +42,9 @@ export interface LineageGroupingOptions { export class LineageFilter { field: string; - value: string[]; + value: number[] | string[]; - constructor(field: string, value: string[]) { + constructor(field: string, value: number[] | string[]) { this.field = field; this.value = value; } diff --git a/packages/components/src/internal/components/samples/models.ts b/packages/components/src/internal/components/samples/models.ts index 49920e67d8..fe0f7daced 100644 --- a/packages/components/src/internal/components/samples/models.ts +++ b/packages/components/src/internal/components/samples/models.ts @@ -86,6 +86,7 @@ export interface SampleStatus { color: string; description?: string; label: string; + rowId: number; statusType: SampleStateType; } @@ -136,6 +137,7 @@ export class SampleState { description: this.description, label: this.label, color: this.color, + rowId: this.rowId ?? undefined, statusType: SampleStateType[this.stateType], }; } diff --git a/packages/components/src/internal/components/samples/utils.tsx b/packages/components/src/internal/components/samples/utils.tsx index 8a76c36e56..edaefbe357 100644 --- a/packages/components/src/internal/components/samples/utils.tsx +++ b/packages/components/src/internal/components/samples/utils.tsx @@ -90,15 +90,19 @@ export function getSampleStatusColor(color: string, stateType: SampleStateType | export function getSampleStatus(row: any): SampleStatus { let label; + let rowId; // Issue 45269. If the state columns are present, don't look at a column named 'Label' let field = caseInsensitive(row, SAMPLE_STATE_COLUMN_NAME); if (field) { + rowId = field.value; label = field.displayValue; } else { field = caseInsensitive(row, 'SampleID/' + SAMPLE_STATE_COLUMN_NAME); if (field) { + rowId = field.value; label = field.displayValue; } else { + rowId = caseInsensitive(row, 'RowId')?.value; label = caseInsensitive(row, 'Label')?.value; } } @@ -128,6 +132,7 @@ export function getSampleStatus(row: any): SampleStatus { } return { label, + rowId, statusType: getSampleStatusType(row), color, description, diff --git a/packages/components/src/theme/lineage.scss b/packages/components/src/theme/lineage.scss index 1ee6de497c..9be407a473 100644 --- a/packages/components/src/theme/lineage.scss +++ b/packages/components/src/theme/lineage.scss @@ -220,3 +220,13 @@ margin-left: 0; padding-left: 0; } + +.lineage-filter__dot { + height: 10px; + width: 10px; + border-radius: 50%; + background-color: $badge-color; + border: 1px solid $white; + display: inline-block; + margin-top: 6px; +} From 437f805609e57ebb1fc701f92ff51c4f1903d443 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Fri, 19 Jun 2026 06:46:37 -0700 Subject: [PATCH 02/13] @labkey/components v7.42.2-lineageStatusFilter.0 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 44bb26167d..bbf9ec2cc3 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.42.1", + "version": "7.42.2-lineageStatusFilter.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.42.1", + "version": "7.42.2-lineageStatusFilter.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index fdb1e9c283..f3d03798c9 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.42.1", + "version": "7.42.2-lineageStatusFilter.0", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From f5ce3100703730b742242bb0cd31f2256fc10bdc Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Fri, 19 Jun 2026 10:31:31 -0700 Subject: [PATCH 03/13] Make rowId optional for better typing --- packages/components/src/internal/components/samples/models.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/internal/components/samples/models.ts b/packages/components/src/internal/components/samples/models.ts index fe0f7daced..06db383707 100644 --- a/packages/components/src/internal/components/samples/models.ts +++ b/packages/components/src/internal/components/samples/models.ts @@ -86,7 +86,7 @@ export interface SampleStatus { color: string; description?: string; label: string; - rowId: number; + rowId?: number; statusType: SampleStateType; } From 5d7d293caf0cbee5fad78f6dbd853a1c40fc7378 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Fri, 19 Jun 2026 10:33:23 -0700 Subject: [PATCH 04/13] @labkey/components v7.42.2-lineageStatusFilter.1 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index bbf9ec2cc3..e1588a647a 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.42.2-lineageStatusFilter.0", + "version": "7.42.2-lineageStatusFilter.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.42.2-lineageStatusFilter.0", + "version": "7.42.2-lineageStatusFilter.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index f3d03798c9..d90ea52061 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.42.2-lineageStatusFilter.0", + "version": "7.42.2-lineageStatusFilter.1", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From b57a16d40f6907bc49a1d3199c3e5a8c093b97f1 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Fri, 19 Jun 2026 11:27:31 -0700 Subject: [PATCH 05/13] Export as class not type --- packages/components/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 189402af3f..7e1d94cce6 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -527,6 +527,7 @@ import { EntityCreationType, INDEPENDENT_SAMPLE_CREATION, POOLED_SAMPLE_CREATION, + SampleState, SampleStateType, } from './internal/components/samples/models'; import { DEFAULT_ALIQUOT_NAMING_PATTERN, SampleTypeModel } from './internal/components/domainproperties/samples/models'; @@ -1658,6 +1659,7 @@ export { SampleParentDataType, SamplePropertyDataType, SamplesEditButtonSections, + SampleState, SampleStateType, SampleStatusLegend, SampleStatusRenderer, @@ -1892,7 +1894,6 @@ export type { SampleStatus, StorageActionStatusCounts, } from './internal/components/samples/models'; -export type { SampleState } from './internal/components/samples/models'; export type { SearchHit, SearchOptions } from './internal/components/search/actions'; export type { EntityFieldFilter } from './internal/components/search/models'; export type { SecurityAPIWrapper } from './internal/components/security/APIWrapper'; From 1743d6faf1213f92d35458657b3c128d302d0c77 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Fri, 19 Jun 2026 11:50:48 -0700 Subject: [PATCH 06/13] @labkey/components v7.42.2-lineageStatusFilter.2 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index e1588a647a..f43eee94b7 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.42.2-lineageStatusFilter.1", + "version": "7.42.2-lineageStatusFilter.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.42.2-lineageStatusFilter.1", + "version": "7.42.2-lineageStatusFilter.2", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index d90ea52061..2ebc879844 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.42.2-lineageStatusFilter.1", + "version": "7.42.2-lineageStatusFilter.2", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 0299965abc1ccfe2ff9b2e431882fc58a9be68d7 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Mon, 22 Jun 2026 07:42:13 -0700 Subject: [PATCH 07/13] Separate methods for getting sample status from a sample and from a status row --- packages/components/src/index.ts | 2 + .../internal/components/samples/utils.test.ts | 55 +++++++++++++++---- .../src/internal/components/samples/utils.tsx | 19 ++++--- .../renderers/SampleStatusRenderer.tsx | 4 +- 4 files changed, 58 insertions(+), 22 deletions(-) diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 7e1d94cce6..f3fe8ea07e 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -395,6 +395,7 @@ import { getSampleStatus, getSampleStatusColor, getSampleStatusContainerFilter, + getSampleStatusFromSampleRow, getSampleStatusType, isAllSamplesSchema, isSampleOperationPermitted, @@ -1408,6 +1409,7 @@ export { getSampleStatus, getSampleStatusColor, getSampleStatusContainerFilter, + getSampleStatusFromSampleRow, getSampleStatusType, getSamplesTestAPIWrapper, getSampleTypeDetails, diff --git a/packages/components/src/internal/components/samples/utils.test.ts b/packages/components/src/internal/components/samples/utils.test.ts index 3c6cb7a628..5f4d56cb8f 100644 --- a/packages/components/src/internal/components/samples/utils.test.ts +++ b/packages/components/src/internal/components/samples/utils.test.ts @@ -28,6 +28,7 @@ import { getOperationNotPermittedMessage, getSampleStatus, getSampleStatusColor, + getSampleStatusFromSampleRow, getSampleStatusLockedMessage, getSampleStatusType, isSampleOperationPermitted, @@ -330,23 +331,59 @@ describe('isSamplesSchema', () => { }); }); -describe('getSampleStatus', () => { - test('getSampleStatusType', () => { +describe('getSampleStatusType', () => { + test('no data', () => { + expect(getSampleStatusType(undefined)).toBeUndefined(); expect(getSampleStatusType({})).toBeUndefined(); + }); + + test('sample row', () => { expect(getSampleStatusType({ 'SampleState/StatusType': { value: undefined } })).toBeUndefined(); expect(getSampleStatusType({ 'SampleState/StatusType': { value: 'Available' } })).toBe('Available'); + }); + + test('sample lookup', () => { expect(getSampleStatusType({ 'SampleID/SampleState/StatusType': { value: undefined } })).toBeUndefined(); expect(getSampleStatusType({ 'SampleID/SampleState/StatusType': { value: 'Consumed' } })).toBe('Consumed'); + }); + + test('status row', () => { expect(getSampleStatusType({ StatusType: { value: undefined } })).toBeUndefined(); expect(getSampleStatusType({ StatusType: { value: 'Locked' } })).toBe('Locked'); }); +}); + +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 + ).toBeUndefined(); + expect(getSampleStatusFromSampleRow({ 'SampleID/SampleState': { displayValue: 'Label2' } }).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({ 'SampleID/SampleState/Description': { value: undefined } }).description + ).toBeUndefined(); + expect( + getSampleStatusFromSampleRow({ 'SampleID/SampleState/Description': { value: 'Desc2' } }).description + ).toBe('Desc2'); + }); +}); + +describe('getSampleStatus', () => { test('label', () => { expect(getSampleStatus({}).label).toBeUndefined(); - expect(getSampleStatus({ SampleState: { displayValue: undefined } }).label).toBeUndefined(); - expect(getSampleStatus({ SampleState: { displayValue: 'Label1' } }).label).toBe('Label1'); - expect(getSampleStatus({ 'SampleID/SampleState': { displayValue: undefined } }).label).toBeUndefined(); - expect(getSampleStatus({ 'SampleID/SampleState': { displayValue: 'Label2' } }).label).toBe('Label2'); expect(getSampleStatus({ Label: { displayValue: undefined } }).label).toBeUndefined(); expect(getSampleStatus({ Label: { displayValue: 'Label3' } }).label).toBeUndefined(); expect(getSampleStatus({ Label: { value: 'Label3' } }).label).toBe('Label3'); @@ -354,12 +391,6 @@ describe('getSampleStatus', () => { test('description', () => { expect(getSampleStatus({}).description).toBeUndefined(); - expect(getSampleStatus({ 'SampleState/Description': { value: undefined } }).description).toBeUndefined(); - expect(getSampleStatus({ 'SampleState/Description': { value: 'Desc1' } }).description).toBe('Desc1'); - expect( - getSampleStatus({ 'SampleID/SampleState/Description': { value: undefined } }).description - ).toBeUndefined(); - expect(getSampleStatus({ 'SampleID/SampleState/Description': { value: 'Desc2' } }).description).toBe('Desc2'); expect(getSampleStatus({ Description: { value: undefined } }).description).toBeUndefined(); expect(getSampleStatus({ Description: { value: 'Desc3' } }).description).toBe('Desc3'); }); diff --git a/packages/components/src/internal/components/samples/utils.tsx b/packages/components/src/internal/components/samples/utils.tsx index edaefbe357..04e7811526 100644 --- a/packages/components/src/internal/components/samples/utils.tsx +++ b/packages/components/src/internal/components/samples/utils.tsx @@ -88,7 +88,7 @@ export function getSampleStatusColor(color: string, stateType: SampleStateType | } } -export function getSampleStatus(row: any): SampleStatus { +export function getSampleStatusFromSampleRow(row: any): SampleStatus { let label; let rowId; // Issue 45269. If the state columns are present, don't look at a column named 'Label' @@ -101,9 +101,6 @@ export function getSampleStatus(row: any): SampleStatus { if (field) { rowId = field.value; label = field.displayValue; - } else { - rowId = caseInsensitive(row, 'RowId')?.value; - label = caseInsensitive(row, 'Label')?.value; } } let color; @@ -114,8 +111,6 @@ export function getSampleStatus(row: any): SampleStatus { col = caseInsensitive(row, 'SampleID/' + SAMPLE_STATE_COLOR_COLUMN_NAME); if (col) { color = col.value; - } else { - color = caseInsensitive(row, 'Color')?.value; } } let description; @@ -126,8 +121,6 @@ export function getSampleStatus(row: any): SampleStatus { col = caseInsensitive(row, 'SampleID/' + SAMPLE_STATE_DESCRIPTION_COLUMN_NAME); if (col) { description = col.value; - } else { - description = caseInsensitive(row, 'Description')?.value; } } return { @@ -139,6 +132,16 @@ export function getSampleStatus(row: any): SampleStatus { }; } +export function getSampleStatus(row: any): SampleStatus { + return { + label: caseInsensitive(row, 'Label')?.value, + rowId: caseInsensitive(row, 'RowId')?.value, + statusType: getSampleStatusType(row), + color: caseInsensitive(row, 'Color')?.value, + description: caseInsensitive(row, 'Description')?.value, + }; +} + export function getFilterForSampleOperation( operation: SampleOperation, allowed = true, diff --git a/packages/components/src/internal/renderers/SampleStatusRenderer.tsx b/packages/components/src/internal/renderers/SampleStatusRenderer.tsx index 9ad3716263..24b540d969 100644 --- a/packages/components/src/internal/renderers/SampleStatusRenderer.tsx +++ b/packages/components/src/internal/renderers/SampleStatusRenderer.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { Map } from 'immutable'; import { SampleStatusTag } from '../components/samples/SampleStatusTag'; -import { getSampleStatus } from '../components/samples/utils'; +import { getSampleStatusFromSampleRow } from '../components/samples/utils'; interface SampleStatusProps { row: Map; @@ -14,6 +14,6 @@ interface SampleStatusProps { export class SampleStatusRenderer extends React.PureComponent { render() { const { row } = this.props; - return + return } } From fcfd10442d83cf35bc1990fd309134abbacb21db Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Mon, 22 Jun 2026 07:44:57 -0700 Subject: [PATCH 08/13] @labkey/components v7.42.2-lineageStatusFilter.3 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index f43eee94b7..8fbd650064 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.42.2-lineageStatusFilter.2", + "version": "7.42.2-lineageStatusFilter.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.42.2-lineageStatusFilter.2", + "version": "7.42.2-lineageStatusFilter.3", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 2ebc879844..c3d98f9375 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.42.2-lineageStatusFilter.2", + "version": "7.42.2-lineageStatusFilter.3", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 476b2c1a261ac455c610ecd9e78ca7facc09c259 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Tue, 23 Jun 2026 08:47:10 -0700 Subject: [PATCH 09/13] GH Issue #1256: Prevent disconnected graph --- packages/components/src/internal/components/lineage/models.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/src/internal/components/lineage/models.ts b/packages/components/src/internal/components/lineage/models.ts index 5c19aab5dd..06e3aeb011 100644 --- a/packages/components/src/internal/components/lineage/models.ts +++ b/packages/components/src/internal/components/lineage/models.ts @@ -1137,7 +1137,8 @@ function applyCombineSize( { aliquotEdges: [] as LineageLink[], nonAliquotEdges: [] as LineageLink[] } ); - if (aliquotEdges.length > 1) { + // GH Issue #1256 + if (aliquotEdges.length > 0) { combinedLineageNodes.push( combineNodes(lsid, aliquotEdges, nodes, options, dir, visEdges, visNodes, nodesInCombinedNode, depth) ); From 0e6b2158b83c7476adf97fd07fbc8f400eb6dbc2 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Tue, 23 Jun 2026 09:37:05 -0700 Subject: [PATCH 10/13] Don't combine for only one node, but add the single edge needed. --- .../components/src/internal/components/lineage/models.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/components/src/internal/components/lineage/models.ts b/packages/components/src/internal/components/lineage/models.ts index 06e3aeb011..eaac360e5e 100644 --- a/packages/components/src/internal/components/lineage/models.ts +++ b/packages/components/src/internal/components/lineage/models.ts @@ -1137,11 +1137,13 @@ function applyCombineSize( { aliquotEdges: [] as LineageLink[], nonAliquotEdges: [] as LineageLink[] } ); - // GH Issue #1256 - if (aliquotEdges.length > 0) { + + if (aliquotEdges.length > 1) { combinedLineageNodes.push( combineNodes(lsid, aliquotEdges, nodes, options, dir, visEdges, visNodes, nodesInCombinedNode, depth) ); + } else { // GH Issue #1256 + addEdges(lsid, null, visEdges, List(aliquotEdges), nodesInCombinedNode, dir); } if (nonAliquotEdges.length >= options.grouping.combineSize) { From e4a65462695f6bd3e6d914f2114df475fc047fa4 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Tue, 23 Jun 2026 09:39:12 -0700 Subject: [PATCH 11/13] @labkey/components v7.42.3-lineageStatusFilter.4 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 45847cbbc6..b26d577517 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.42.3-lineageStatusFilter.3", + "version": "7.42.3-lineageStatusFilter.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.42.3-lineageStatusFilter.3", + "version": "7.42.3-lineageStatusFilter.4", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 2c1e03de8d..3cb32729ae 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.42.3-lineageStatusFilter.3", + "version": "7.42.3-lineageStatusFilter.4", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 34ca8b01a0993fe70ec75515175dd80bfb592af1 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Tue, 23 Jun 2026 09:45:01 -0700 Subject: [PATCH 12/13] prettier --- packages/components/src/internal/components/lineage/models.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/src/internal/components/lineage/models.ts b/packages/components/src/internal/components/lineage/models.ts index eaac360e5e..5295c2c06f 100644 --- a/packages/components/src/internal/components/lineage/models.ts +++ b/packages/components/src/internal/components/lineage/models.ts @@ -1137,12 +1137,12 @@ function applyCombineSize( { aliquotEdges: [] as LineageLink[], nonAliquotEdges: [] as LineageLink[] } ); - if (aliquotEdges.length > 1) { combinedLineageNodes.push( combineNodes(lsid, aliquotEdges, nodes, options, dir, visEdges, visNodes, nodesInCombinedNode, depth) ); - } else { // GH Issue #1256 + } else { + // GH Issue #1256 addEdges(lsid, null, visEdges, List(aliquotEdges), nodesInCombinedNode, dir); } From cffffe4cf43d6b911a440425fa3300168b04f7ec Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Tue, 23 Jun 2026 10:51:34 -0700 Subject: [PATCH 13/13] Regression test --- .../components/lineage/models.test.ts | 95 +++++++++++++++++-- 1 file changed, 89 insertions(+), 6 deletions(-) diff --git a/packages/components/src/internal/components/lineage/models.test.ts b/packages/components/src/internal/components/lineage/models.test.ts index 81d4f4e08d..adc0cc7f75 100644 --- a/packages/components/src/internal/components/lineage/models.test.ts +++ b/packages/components/src/internal/components/lineage/models.test.ts @@ -2,9 +2,9 @@ * Copyright (c) 2020-2026 LabKey Corporation. All rights reserved. No portion of this work may be reproduced * in any form or by any electronic or mechanical means without written permission from LabKey Corporation. */ -import { applyLineageOptions, generateNodesAndEdges, LineageIO, LineageNode, LineageResult } from './models'; +import { applyLineageOptions, generateNodesAndEdges, Lineage, LineageIO, LineageNode, LineageResult } from './models'; import { DEFAULT_LINEAGE_OPTIONS } from './constants'; -import { LineageFilter } from './types'; +import { LineageFilter, LineageOptions } from './types'; describe('lineage model', () => { describe('applyLineageOptions', () => { @@ -33,6 +33,7 @@ describe('lineage model', () => { describe('LineageIO.applyConfig', () => { const lineageObj = { container: 'container', + containerPath: '/container', created: '2022-01-20', createdBy: 'me', modified: '2022-01-21', @@ -235,13 +236,13 @@ describe('lineage model', () => { const parentLsid = 'parent-lsid'; const childNode = LineageNode.create(childLsid, { - parents: [{ lsid: parentLsid, name: '&4[0' }], - name: '&4[0_1001' + parents: [{ lsid: parentLsid }], + name: '&4[0_1001', }); const parentNode = LineageNode.create(parentLsid, { - children: [{ lsid: childLsid, name: '&4[0_1001' }], - name: '&4[0' + children: [{ lsid: childLsid }], + name: '&4[0', }); const result = LineageResult.create({ @@ -262,5 +263,87 @@ describe('lineage model', () => { expect(nodes[childNode.lsid].level).toEqual(0); expect(nodes[childNode.lsid].label).toEqual('&4[0_1001'); }); + + it('GH Issue #1256: Lineage graph can become disconnected with single aliquot and multiple derivatives', () => { + // Parent sample with 2 derived samples and 1 aliquot including derivation runs + const result = LineageResult.create({ + nodes: { + 'aliquot-lsid': { + cpasType: 'sample-type-lsid', + materialLineageType: 'Aliquot', + type: 'Sample', + lsid: 'aliquot-lsid', + name: 'Aliquot-1', + expType: 'Material', + parents: [{ lsid: 'aliquot-run-lsid' }], + }, + 'aliquot-run-lsid': { + cpasType: 'urn:lsid:labkey.org:Protocol:SampleAliquotProtocol', + type: 'Run', + lsid: 'aliquot-run-lsid', + children: [{ lsid: 'aliquot-lsid' }], + name: 'Create aliquot from Parent-1', + expType: 'ExperimentRun', + parents: [{ lsid: 'parent-lsid' }], + }, + 'derived-one-lsid': { + cpasType: 'sample-type-lsid', + materialLineageType: 'Derivative', + type: 'Sample', + lsid: 'derived-one-lsid', + name: 'Derived-1', + expType: 'Material', + parents: [{ lsid: 'derivation-run-lsid' }], + }, + 'derived-two-lsid': { + cpasType: 'sample-type-lsid', + materialLineageType: 'Derivative', + type: 'Sample', + lsid: 'derived-two-lsid', + name: 'Derived-2', + expType: 'Material', + parents: [{ lsid: 'derivation-run-lsid' }], + }, + 'derivation-run-lsid': { + cpasType: 'urn:lsid:labkey.org:Protocol:SampleDerivationProtocol', + type: 'Run', + lsid: 'derivation-run-lsid', + children: [{ lsid: 'derived-two-lsid' }, { lsid: 'derived-one-lsid' }], + name: 'Derive 2 samples from Parent-1', + expType: 'ExperimentRun', + parents: [{ lsid: 'parent-lsid' }], + }, + 'parent-lsid': { + cpasType: 'sample-type-lsid', + materialLineageType: 'RootMaterial', + type: 'Sample', + lsid: 'parent-lsid', + children: [{ lsid: 'aliquot-run-lsid' }, { lsid: 'derivation-run-lsid' }], + name: 'Parent-1', + expType: 'Material', + }, + }, + seed: 'parent-lsid', + }); + + const options: LineageOptions = { + filters: [new LineageFilter('type', ['Sample'])], + }; + + const lineage = new Lineage({ result }); + const nodesAndEdges = generateNodesAndEdges(lineage.filterResult(options), options); + + // Runs have been filtered out + const expectedNodes = new Set(['aliquot-lsid', 'derived-one-lsid', 'derived-two-lsid', 'parent-lsid']); + expect(new Set(Object.keys(nodesAndEdges.nodes))).toEqual(expectedNodes); + + // Previously, this would have only two edges where the edge between the parent and aliquot was missing + const expectedEdges = new Set([ + 'parent-lsid||aliquot-lsid', + 'parent-lsid||derived-one-lsid', + 'parent-lsid||derived-two-lsid', + ]); + expect(new Set(Object.keys(nodesAndEdges.edges))).toEqual(expectedEdges); + }); }); });