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

### version 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.2
*Released*: 22 June 2026
- Styling update for user comment on large storage modal
Expand Down
7 changes: 6 additions & 1 deletion packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ import {
getSampleStatus,
getSampleStatusColor,
getSampleStatusContainerFilter,
getSampleStatusFromSampleRow,
getSampleStatusType,
isAllSamplesSchema,
isSampleOperationPermitted,
Expand Down Expand Up @@ -422,7 +423,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,
Expand Down Expand Up @@ -527,6 +528,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';
Expand Down Expand Up @@ -1407,6 +1409,7 @@ export {
getSampleStatus,
getSampleStatusColor,
getSampleStatusContainerFilter,
getSampleStatusFromSampleRow,
getSampleStatusType,
getSamplesTestAPIWrapper,
getSampleTypeDetails,
Expand Down Expand Up @@ -1513,6 +1516,7 @@ export {
LabelHelpTip,
LabelOverlay,
LINEAGE_DIRECTIONS,
LINEAGE_GRAPH_FILTER_METRIC,
LINEAGE_GROUPING_GENERATIONS,
LineageDepthLimitMessage,
LineageFilter,
Expand Down Expand Up @@ -1657,6 +1661,7 @@ export {
SampleParentDataType,
SamplePropertyDataType,
SamplesEditButtonSections,
SampleState,
SampleStateType,
SampleStatusLegend,
SampleStatusRenderer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
95 changes: 89 additions & 6 deletions packages/components/src/internal/components/lineage/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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({
Expand All @@ -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);
});
});
});
19 changes: 13 additions & 6 deletions packages/components/src/internal/components/lineage/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ export class LineageNode
steps: undefined,
type: undefined,
materialLineageType: undefined,
sampleStatus: undefined,
url: undefined,

// computed properties
Expand Down Expand Up @@ -332,6 +333,7 @@ export class LineageNode
declare steps: List<LineageRunStep>;
declare type: string;
declare materialLineageType: string;
declare sampleStatus: number;
declare url: string;

// computed properties
Expand Down Expand Up @@ -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);
}

Expand All @@ -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');
Expand Down Expand Up @@ -457,22 +459,23 @@ 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) {
if (value === undefined) {
// 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;
Expand All @@ -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;
Expand Down Expand Up @@ -1137,6 +1141,9 @@ function applyCombineSize(
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) {
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/internal/components/lineage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/components/src/internal/components/samples/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export interface SampleStatus {
color: string;
description?: string;
label: string;
rowId?: number;
statusType: SampleStateType;
}

Expand Down Expand Up @@ -136,6 +137,7 @@ export class SampleState {
description: this.description,
label: this.label,
color: this.color,
rowId: this.rowId ?? undefined,
statusType: SampleStateType[this.stateType],
};
}
Expand Down
Loading