Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/mandatory-block-kind.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@platforma-open/milaboratories.clonotype-browser-3.kind': minor
'@platforma-open/milaboratories.clonotype-browser-3.model': minor
'@platforma-open/milaboratories.clonotype-browser-3': minor
---

Add the mandatory block kind and its init-params contract

The block declares a kind carrying its identity and an init-params contract of
`inputAnchor` plus `annotationSpecUi` — the two fields a project template
supplies to seed a new instance. The data model consumes them in `init` and the
block model projects the same pair back through `templateParams`, so export and
apply are inverses.

Also tracks the current column API: `expandByPartition` is now `splitByAxes`
with a defaulted label resolver, and `getLeafColumnData` is replaced by a
`hasReachableData` guard followed by `getData()`.
9 changes: 7 additions & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ jobs:
init:
runs-on: hz-ubuntu-dind
steps:
- uses: milaboratory/github-ci/actions/context/init@v4
- uses: milaboratory/github-ci/actions/context/init@MILAB-6707_changeset-coverage-gate
with:
version-canonize: false
branch-versioning: main
run:
needs:
- init
uses: milaboratory/github-ci/.github/workflows/node-simple-pnpm.yaml@v4
uses: milaboratory/github-ci/.github/workflows/node-simple-pnpm.yaml@MILAB-6707_changeset-coverage-gate
with:
app-name: 'Block: Clonotype Browser 3'
app-name-slug: 'block-clonotype-browser-3'
Expand All @@ -39,6 +39,11 @@ jobs:
package-path: 'block'
create-tag: 'true'

# Require the published `block` package to be bumped by a changeset on
# PRs (empty changeset or the `skip-changelog` label waives it). Needs
# the input to exist on the referenced `github-ci` branch.
require-package-path-bump: true

npmrc-config: |
{
"registries": {
Expand Down
5 changes: 3 additions & 2 deletions block/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,22 @@
"types": "./dist/index.d.ts",
"exports": {
".": {
"sources": "./src/index.ts",
"types": "./dist/index.d.ts",
"sources": "./src/index.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "ts-builder build --target block-facade && block-tools pack",
"prepublishOnly": "block-tools publish -r s3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1 --registry-serve-url https://blocks.pl-open.science",
"prepublishOnly": "block-tools publish --unstable -r s3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1 --registry-serve-url https://blocks.pl-open.science",
"do-pack": "shx rm -f package.tgz && pnpm pack && shx mv *.tgz package.tgz",
"check": "ts-builder type-check --target block-facade"
},
"dependencies": {},
"devDependencies": {
"@milaboratories/ts-builder": "catalog:",
"@milaboratories/ts-configs": "catalog:",
"@platforma-open/milaboratories.clonotype-browser-3.kind": "workspace:*",
"@platforma-open/milaboratories.clonotype-browser-3.model": "workspace:*",
"@platforma-open/milaboratories.clonotype-browser-3.ui": "workspace:*",
"@platforma-open/milaboratories.clonotype-browser-3.workflow": "workspace:*",
Expand Down
4 changes: 4 additions & 0 deletions kind/.oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": ["node_modules/@milaboratories/ts-builder/configs/oxfmt.json"],
"ignorePatterns": ["dist", "coverage", "CHANGELOG.md"]
}
3 changes: 3 additions & 0 deletions kind/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["node_modules/@milaboratories/ts-builder/dist/configs/oxlint-node.json"]
}
39 changes: 39 additions & 0 deletions kind/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@platforma-open/milaboratories.clonotype-browser-3.kind",
"version": "1.0.0",
"private": true,
"description": "Block kind for the clonotype-browser block",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"sources": "./src/index.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"default": "./dist/index.js"
}
},
"scripts": {
"fmt": "ts-builder format",
"watch": "ts-builder build --target block-kind --watch",
"build": "ts-builder build --target block-kind && block-tools build-kind-manifest",
"check": "ts-builder check --target block-kind"
},
"dependencies": {
"@platforma-sdk/block-kind": "catalog:",
"@platforma-sdk/model": "catalog:",
"es-toolkit": "catalog:"
},
"devDependencies": {
"@milaboratories/ts-builder": "catalog:",
"@milaboratories/ts-configs": "catalog:",
"@platforma-sdk/block-tools": "catalog:"
},
"peerDependencies": {
"@types/node": "*",
"typescript": "*"
}
}
70 changes: 70 additions & 0 deletions kind/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { assertParamsObject, defineBlockKind } from "@platforma-sdk/block-kind";
import { invariant, isPlainObject } from "es-toolkit";
import {
isColumnUniversalId,
type AnnotationSpecUi as SdkAnnotationSpecUi,
type ColumnUniversalId,
type FilterSpec as SdkFilterSpec,
type FilterSpecLeaf,
type FilterSpecUi as SdkFilterSpecUi,
} from "@platforma-sdk/model";
import { name, version } from "../package.json" with { type: "json" };

export type FilterSpec = SdkFilterSpec<
FilterSpecLeaf,
{ id: number; name?: string; isExpanded?: boolean }
>;

export type FilterSpecUI = SdkFilterSpecUi<Extract<FilterSpec, { type: "and" | "or" }>> & {
id: number;
};

export type AnnotationSpecUi = SdkAnnotationSpecUi<FilterSpecUI> & { defaultValue?: string };

export type BlockParams = {
inputAnchor?: ColumnUniversalId;
annotationSpecUi?: AnnotationSpecUi;
};

/**
* Both fields are optional, so a params object that sets neither is valid — a block
* seeded with nothing to browse and nothing annotated is a state the UI reaches too.
* Only the two declared fields are read; anything else in the object is dropped here
* rather than refused, so the returned value is the whole of what the block receives.
*/
function parseInitializationParams(value: unknown): BlockParams {
assertParamsObject(value);

const { inputAnchor, annotationSpecUi } = value;

if (inputAnchor !== undefined && !isColumnUniversalId(inputAnchor)) {
throw new Error("'inputAnchor' must be a column id.");
}
if (annotationSpecUi !== undefined) assertAnnotationSpec(annotationSpecUi);

return { inputAnchor, annotationSpecUi };
}

/**
* The shape of an annotation script, checked only as far as the editor's own states go:
* a step carries a label and a filter from the moment it is added, both still empty
* until the user fills them in. Rejecting an empty label here would refuse a script
* the block itself can produce and export.
*/
function assertAnnotationSpec(spec: unknown): asserts spec is AnnotationSpecUi {
invariant(isPlainObject(spec), "'annotationSpecUi' must be an object.");
invariant(typeof spec.title === "string", "'annotationSpecUi.title' must be a string.");
invariant(
spec.defaultValue === undefined || typeof spec.defaultValue === "string",
"'annotationSpecUi.defaultValue' must be a string.",
);
invariant(Array.isArray(spec.steps), "'annotationSpecUi.steps' must be an array.");

spec.steps.forEach((step: unknown, i: number) => {
invariant(isPlainObject(step), `Annotation step ${i} must be an object.`);
invariant(typeof step.label === "string", `Annotation step ${i} must have a string label.`);
invariant(isPlainObject(step.filter), `Annotation step ${i} must have a filter object.`);
});
}

export const kind = defineBlockKind<BlockParams>({ name, version, parseInitializationParams });
10 changes: 10 additions & 0 deletions kind/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "@milaboratories/ts-configs/block/facade",
"compilerOptions": {
"outDir": "./dist",
"rootDir": ".",
"resolveJsonModule": true
},
"include": ["src/**/*", "package.json"],
"exclude": ["dist", "node_modules"]
}
1 change: 1 addition & 0 deletions model/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
},
"dependencies": {
"@milaboratories/helpers": "catalog:",
"@platforma-open/milaboratories.clonotype-browser-3.kind": "workspace:*",
"@platforma-sdk/model": "catalog:",
"@types/lodash.omit": "^4.5.9",
"lodash.omit": "^4.5.0"
Expand Down
11 changes: 8 additions & 3 deletions model/src/dataModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type PlDataTableStateV2,
type PlRef,
} from "@platforma-sdk/model";
import { kind } from "@platforma-open/milaboratories.clonotype-browser-3.kind";
import type { AnnotationSpecUi, BlockData, LegacyBlockArgs, LegacyUiState } from "./types";

/**
Expand Down Expand Up @@ -35,7 +36,7 @@ function plRefToUniversalId(ref: PlRef | undefined): ColumnUniversalId | undefin
return ref ? createGlobalPObjectId(ref.blockId, ref.name) : undefined;
}

export const blockDataModel = new DataModelBuilder()
export const blockDataModel = new DataModelBuilder({ kind })
.from<StoredV1>("Ver_2026_04_07")
.upgradeLegacy<LegacyBlockArgs, LegacyUiState>(({ args, uiState }) => ({
inputAnchor: args.inputAnchor,
Expand All @@ -53,10 +54,14 @@ export const blockDataModel = new DataModelBuilder()
...prev,
inputAnchor: plRefToUniversalId(prev.inputAnchor),
}))
.init(() => ({
// Both init params are stored exactly as they arrive. `params` is absent for a
// block created from the UI, so each keeps its empty default — a fresh browser
// with nothing picked and nothing annotated.
.init(({ params }) => ({
inputAnchor: params?.inputAnchor,
settingsOpen: true,
overlapTableState: createPlDataTableStateV2(),
sampleTableState: createPlDataTableStateV2(),
statsTableState: createPlDataTableStateV2(),
annotationSpecUi: { title: "", steps: [] },
annotationSpecUi: params?.annotationSpecUi ?? { title: "", steps: [] },
}));
28 changes: 17 additions & 11 deletions model/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,23 @@ import {
convertFilterSpecsToExpressionSpecs,
createPlDataTableSheet,
createPlDataTableV3,
deriveAxisValuesLabels,
deriveDistinctLabels,
expandByPartition,
splitByAxes,
deriveColumnOptions,
getLeafColumnData,
hasReachableData,
getUniquePartitionKeys,
isLeafColumn,
isPlRef,
TreeNodeAccessor,
parseJsonSafely,
} from "@platforma-sdk/model";
import { kind } from "@platforma-open/milaboratories.clonotype-browser-3.kind";
import { Annotation, isAbundanceColumn, PAxisName, PColumnName, readAnnotation } from "./columns";
import { blockDataModel } from "./dataModel";
import type { BlockArgs, BlockData } from "./types";

export { blockDataModel } from "./dataModel";

export * from "./types";

const inputAnchorSelectors: RelaxedColumnSelector[] = [
Expand All @@ -49,7 +50,15 @@ const inputAnchorSelectors: RelaxedColumnSelector[] = [
},
];

export const platforma = BlockModelV3.create(blockDataModel)
export const platforma = BlockModelV3.create({ dataModel: blockDataModel, kind })

// The inverse of `init`: the two fields a template supplies, handed back exactly
// as they sit in live state. Both carry column ids, whose block ids the SDK
// rewrites on the way out and resolves again on the way in.
.templateParams((data) => ({
inputAnchor: data.inputAnchor,
annotationSpecUi: data.annotationSpecUi,
}))

.args<BlockArgs>((data) => {
if (data.inputAnchor === undefined) throw new Error("No input anchor");
Expand Down Expand Up @@ -146,9 +155,7 @@ export const platforma = BlockModelV3.create(blockDataModel)
}
}

const splitRecipes = expandByPartition(splitInputs, [{ idx: 0 }], {
axisValuesLabels: deriveAxisValuesLabels(),
});
const splitRecipes = splitByAxes(splitInputs, [{ idx: 0 }]);
if (!splitRecipes) return undefined;

return createPlDataTableV3(ctx, {
Expand Down Expand Up @@ -200,7 +207,8 @@ export const platforma = BlockModelV3.create(blockDataModel)
if (ctx.data.inputAnchor === undefined) return undefined;
const anchor = Column(ctx.data.inputAnchor);
if (!anchor) return undefined;
const data = getLeafColumnData(anchor);
if (!hasReachableData(anchor)) return undefined;
const data = anchor.getData();
if (!(data instanceof TreeNodeAccessor)) return undefined;
const samples = getUniquePartitionKeys(data)?.[0];
if (!samples) return undefined;
Expand Down Expand Up @@ -232,9 +240,7 @@ export const platforma = BlockModelV3.create(blockDataModel)
const sampleInputs = sampleRecipes.filter(isLeafColumn);
if (sampleInputs.length !== sampleRecipes.length) return undefined;

const splitSampleRecipes = expandByPartition(sampleInputs, [{ idx: 0 }], {
axisValuesLabels: deriveAxisValuesLabels(),
});
const splitSampleRecipes = splitByAxes(sampleInputs, [{ idx: 0 }]);
if (splitSampleRecipes === undefined) return undefined;

return createPlDataTableV3(ctx, {
Expand Down
25 changes: 12 additions & 13 deletions model/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import type {
AnnotationSpec as _AnnotationSpec,
AnnotationSpecUi as _AnnotationSpecUi,
ColumnUniversalId,
FilterSpec as _FilterSpec,
FilterSpecLeaf,
FilterSpecUi as _FilterSpecUI,
PlDataTableStateV2,
PlRef,
} from "@platforma-sdk/model";
import type { AnnotationSpecUi } from "@platforma-open/milaboratories.clonotype-browser-3.kind";

export type FilterSpec = _FilterSpec<
FilterSpecLeaf,
{ id: number; name?: string; isExpanded?: boolean }
>;

export type FilterSpecUI = _FilterSpecUI<Extract<FilterSpec, { type: "and" | "or" }>> & {
id: number;
};
/**
* The annotation shapes are the block's init-params contract, so they are defined
* in the kind and re-exported here — the model depends on the kind, never the
* reverse, and one definition keeps the contract and the stored state identical.
*/
export type {
AnnotationSpecUi,
FilterSpec,
FilterSpecUI,
} from "@platforma-open/milaboratories.clonotype-browser-3.kind";

export type AnnotationSpecUi = _AnnotationSpecUi<FilterSpecUI> & { defaultValue?: string };
/** The compiled form the workflow consumes — filters lowered to expressions. */
export type AnnotationSpec = _AnnotationSpec & { defaultValue?: string };

/**
Expand Down
12 changes: 1 addition & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,5 @@
"oxfmt": "*",
"oxlint": "*"
},
"packageManager": "pnpm@9.12.0",
"//pnpm": {
"overrides": {
"@milaboratories/uikit": "file:../../core/platforma/lib/ui/uikit/package.tgz",
"@platforma-sdk/ui-vue": "file:../../core/platforma/sdk/ui-vue/package.tgz",
"@platforma-sdk/model": "file:../../core/platforma/sdk/model/package.tgz",
"@milaboratories/helpers": "file:../../core/platforma/lib/util/helpers/package.tgz",
"@platforma-sdk/workflow-tengo": "file:../../core/platforma/sdk/workflow-tengo/package.tgz",
"@milaboratories/pl-model-common": "file:../../core/platforma/lib/model/common/package.tgz"
}
}
"packageManager": "pnpm@9.12.0"
}
Loading
Loading