Skip to content

Commit e8f3480

Browse files
authored
Merge pull request #43 from platforma-open/feat/project-template-migration
Add the mandatory block kind and its params contract
2 parents 6e6d29b + d931cef commit e8f3480

15 files changed

Lines changed: 541 additions & 352 deletions

File tree

.changeset/mandatory-block-kind.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@platforma-open/milaboratories.clonotype-browser-3.kind': minor
3+
'@platforma-open/milaboratories.clonotype-browser-3.model': minor
4+
'@platforma-open/milaboratories.clonotype-browser-3': minor
5+
---
6+
7+
Add the mandatory block kind and its init-params contract
8+
9+
The block declares a kind carrying its identity and an init-params contract of
10+
`inputAnchor` plus `annotationSpecUi` — the two fields a project template
11+
supplies to seed a new instance. The data model consumes them in `init` and the
12+
block model projects the same pair back through `templateParams`, so export and
13+
apply are inverses.
14+
15+
Also tracks the current column API: `expandByPartition` is now `splitByAxes`
16+
with a defaulted label resolver, and `getLeafColumnData` is replaced by a
17+
`hasReachableData` guard followed by `getData()`.

.github/workflows/build.yaml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ jobs:
1313
init:
1414
runs-on: hz-ubuntu-dind
1515
steps:
16-
- uses: milaboratory/github-ci/actions/context/init@v4
16+
- uses: milaboratory/github-ci/actions/context/init@MILAB-6707_changeset-coverage-gate
1717
with:
1818
version-canonize: false
1919
branch-versioning: main
2020
run:
2121
needs:
2222
- init
23-
uses: milaboratory/github-ci/.github/workflows/node-simple-pnpm.yaml@v4
23+
uses: milaboratory/github-ci/.github/workflows/node-simple-pnpm.yaml@MILAB-6707_changeset-coverage-gate
2424
with:
2525
app-name: 'Block: Clonotype Browser 3'
2626
app-name-slug: 'block-clonotype-browser-3'
@@ -39,6 +39,11 @@ jobs:
3939
package-path: 'block'
4040
create-tag: 'true'
4141

42+
# Require the published `block` package to be bumped by a changeset on
43+
# PRs (empty changeset or the `skip-changelog` label waives it). Needs
44+
# the input to exist on the referenced `github-ci` branch.
45+
require-package-path-bump: true
46+
4247
npmrc-config: |
4348
{
4449
"registries": {

block/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,22 @@
1111
"types": "./dist/index.d.ts",
1212
"exports": {
1313
".": {
14-
"sources": "./src/index.ts",
1514
"types": "./dist/index.d.ts",
15+
"sources": "./src/index.ts",
1616
"default": "./dist/index.js"
1717
}
1818
},
1919
"scripts": {
2020
"build": "ts-builder build --target block-facade && block-tools pack",
21-
"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",
21+
"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",
2222
"do-pack": "shx rm -f package.tgz && pnpm pack && shx mv *.tgz package.tgz",
2323
"check": "ts-builder type-check --target block-facade"
2424
},
2525
"dependencies": {},
2626
"devDependencies": {
2727
"@milaboratories/ts-builder": "catalog:",
2828
"@milaboratories/ts-configs": "catalog:",
29+
"@platforma-open/milaboratories.clonotype-browser-3.kind": "workspace:*",
2930
"@platforma-open/milaboratories.clonotype-browser-3.model": "workspace:*",
3031
"@platforma-open/milaboratories.clonotype-browser-3.ui": "workspace:*",
3132
"@platforma-open/milaboratories.clonotype-browser-3.workflow": "workspace:*",

kind/.oxfmtrc.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": ["node_modules/@milaboratories/ts-builder/configs/oxfmt.json"],
3+
"ignorePatterns": ["dist", "coverage", "CHANGELOG.md"]
4+
}

kind/.oxlintrc.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["node_modules/@milaboratories/ts-builder/dist/configs/oxlint-node.json"]
3+
}

kind/package.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "@platforma-open/milaboratories.clonotype-browser-3.kind",
3+
"version": "1.0.0",
4+
"private": true,
5+
"description": "Block kind for the clonotype-browser block",
6+
"type": "module",
7+
"main": "./dist/index.cjs",
8+
"module": "./dist/index.js",
9+
"types": "./dist/index.d.ts",
10+
"exports": {
11+
".": {
12+
"types": "./dist/index.d.ts",
13+
"sources": "./src/index.ts",
14+
"import": "./dist/index.js",
15+
"require": "./dist/index.cjs",
16+
"default": "./dist/index.js"
17+
}
18+
},
19+
"scripts": {
20+
"fmt": "ts-builder format",
21+
"watch": "ts-builder build --target block-kind --watch",
22+
"build": "ts-builder build --target block-kind && block-tools build-kind-manifest",
23+
"check": "ts-builder check --target block-kind"
24+
},
25+
"dependencies": {
26+
"@platforma-sdk/block-kind": "catalog:",
27+
"@platforma-sdk/model": "catalog:",
28+
"es-toolkit": "catalog:"
29+
},
30+
"devDependencies": {
31+
"@milaboratories/ts-builder": "catalog:",
32+
"@milaboratories/ts-configs": "catalog:",
33+
"@platforma-sdk/block-tools": "catalog:"
34+
},
35+
"peerDependencies": {
36+
"@types/node": "*",
37+
"typescript": "*"
38+
}
39+
}

kind/src/index.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { assertParamsObject, defineBlockKind } from "@platforma-sdk/block-kind";
2+
import { invariant, isPlainObject } from "es-toolkit";
3+
import {
4+
isColumnUniversalId,
5+
type AnnotationSpecUi as SdkAnnotationSpecUi,
6+
type ColumnUniversalId,
7+
type FilterSpec as SdkFilterSpec,
8+
type FilterSpecLeaf,
9+
type FilterSpecUi as SdkFilterSpecUi,
10+
} from "@platforma-sdk/model";
11+
import { name, version } from "../package.json" with { type: "json" };
12+
13+
export type FilterSpec = SdkFilterSpec<
14+
FilterSpecLeaf,
15+
{ id: number; name?: string; isExpanded?: boolean }
16+
>;
17+
18+
export type FilterSpecUI = SdkFilterSpecUi<Extract<FilterSpec, { type: "and" | "or" }>> & {
19+
id: number;
20+
};
21+
22+
export type AnnotationSpecUi = SdkAnnotationSpecUi<FilterSpecUI> & { defaultValue?: string };
23+
24+
export type BlockParams = {
25+
inputAnchor?: ColumnUniversalId;
26+
annotationSpecUi?: AnnotationSpecUi;
27+
};
28+
29+
/**
30+
* Both fields are optional, so a params object that sets neither is valid — a block
31+
* seeded with nothing to browse and nothing annotated is a state the UI reaches too.
32+
* Only the two declared fields are read; anything else in the object is dropped here
33+
* rather than refused, so the returned value is the whole of what the block receives.
34+
*/
35+
function parseInitializationParams(value: unknown): BlockParams {
36+
assertParamsObject(value);
37+
38+
const { inputAnchor, annotationSpecUi } = value;
39+
40+
if (inputAnchor !== undefined && !isColumnUniversalId(inputAnchor)) {
41+
throw new Error("'inputAnchor' must be a column id.");
42+
}
43+
if (annotationSpecUi !== undefined) assertAnnotationSpec(annotationSpecUi);
44+
45+
return { inputAnchor, annotationSpecUi };
46+
}
47+
48+
/**
49+
* The shape of an annotation script, checked only as far as the editor's own states go:
50+
* a step carries a label and a filter from the moment it is added, both still empty
51+
* until the user fills them in. Rejecting an empty label here would refuse a script
52+
* the block itself can produce and export.
53+
*/
54+
function assertAnnotationSpec(spec: unknown): asserts spec is AnnotationSpecUi {
55+
invariant(isPlainObject(spec), "'annotationSpecUi' must be an object.");
56+
invariant(typeof spec.title === "string", "'annotationSpecUi.title' must be a string.");
57+
invariant(
58+
spec.defaultValue === undefined || typeof spec.defaultValue === "string",
59+
"'annotationSpecUi.defaultValue' must be a string.",
60+
);
61+
invariant(Array.isArray(spec.steps), "'annotationSpecUi.steps' must be an array.");
62+
63+
spec.steps.forEach((step: unknown, i: number) => {
64+
invariant(isPlainObject(step), `Annotation step ${i} must be an object.`);
65+
invariant(typeof step.label === "string", `Annotation step ${i} must have a string label.`);
66+
invariant(isPlainObject(step.filter), `Annotation step ${i} must have a filter object.`);
67+
});
68+
}
69+
70+
export const kind = defineBlockKind<BlockParams>({ name, version, parseInitializationParams });

kind/tsconfig.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "@milaboratories/ts-configs/block/facade",
3+
"compilerOptions": {
4+
"outDir": "./dist",
5+
"rootDir": ".",
6+
"resolveJsonModule": true
7+
},
8+
"include": ["src/**/*", "package.json"],
9+
"exclude": ["dist", "node_modules"]
10+
}

model/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
},
2828
"dependencies": {
2929
"@milaboratories/helpers": "catalog:",
30+
"@platforma-open/milaboratories.clonotype-browser-3.kind": "workspace:*",
3031
"@platforma-sdk/model": "catalog:",
3132
"@types/lodash.omit": "^4.5.9",
3233
"lodash.omit": "^4.5.0"

model/src/dataModel.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type PlDataTableStateV2,
77
type PlRef,
88
} from "@platforma-sdk/model";
9+
import { kind } from "@platforma-open/milaboratories.clonotype-browser-3.kind";
910
import type { AnnotationSpecUi, BlockData, LegacyBlockArgs, LegacyUiState } from "./types";
1011

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

38-
export const blockDataModel = new DataModelBuilder()
39+
export const blockDataModel = new DataModelBuilder({ kind })
3940
.from<StoredV1>("Ver_2026_04_07")
4041
.upgradeLegacy<LegacyBlockArgs, LegacyUiState>(({ args, uiState }) => ({
4142
inputAnchor: args.inputAnchor,
@@ -53,10 +54,14 @@ export const blockDataModel = new DataModelBuilder()
5354
...prev,
5455
inputAnchor: plRefToUniversalId(prev.inputAnchor),
5556
}))
56-
.init(() => ({
57+
// Both init params are stored exactly as they arrive. `params` is absent for a
58+
// block created from the UI, so each keeps its empty default — a fresh browser
59+
// with nothing picked and nothing annotated.
60+
.init(({ params }) => ({
61+
inputAnchor: params?.inputAnchor,
5762
settingsOpen: true,
5863
overlapTableState: createPlDataTableStateV2(),
5964
sampleTableState: createPlDataTableStateV2(),
6065
statsTableState: createPlDataTableStateV2(),
61-
annotationSpecUi: { title: "", steps: [] },
66+
annotationSpecUi: params?.annotationSpecUi ?? { title: "", steps: [] },
6267
}));

0 commit comments

Comments
 (0)