diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..d5cdfea --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "oxc.oxc-vscode" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e5bdb6c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,40 @@ +{ + "editor.defaultFormatter": "oxc.oxc-vscode", + "[typescript]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.tabSize": 2 + }, + "[vue]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.tabSize": 2 + }, + "[javascript]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.tabSize": 2 + }, + "[css]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.tabSize": 2 + }, + "[scss]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.tabSize": 2 + }, + "[html]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.tabSize": 2 + }, + "[yaml]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.tabSize": 2 + }, + "[markdown]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.tabSize": 2 + }, + "[json]": { + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.tabSize": 2 + }, + "typescript.tsdk": "./node_modules/typescript/lib" +} diff --git a/model/.oxfmtrc.json b/model/.oxfmtrc.json new file mode 100644 index 0000000..ba9bb8f --- /dev/null +++ b/model/.oxfmtrc.json @@ -0,0 +1,3 @@ +{ + "ignorePatterns": ["dist", "CHANGELOG.md"] +} diff --git a/model/.oxlintrc.json b/model/.oxlintrc.json new file mode 100644 index 0000000..70ab6b8 --- /dev/null +++ b/model/.oxlintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["node_modules/@milaboratories/ts-builder/dist/configs/oxlint-block-model.json"] +} diff --git a/model/eslint.config.mjs b/model/eslint.config.mjs deleted file mode 100644 index 0c8c7ff..0000000 --- a/model/eslint.config.mjs +++ /dev/null @@ -1,4 +0,0 @@ -import { model } from '@platforma-sdk/eslint-config'; - -/** @type {import('eslint').Linter.Config[]} */ -export default [...model]; \ No newline at end of file diff --git a/model/package.json b/model/package.json index 907f3a6..df13805 100644 --- a/model/package.json +++ b/model/package.json @@ -7,17 +7,17 @@ "types": "dist/index.d.ts", "exports": { ".": { + "types": "./dist/index.d.ts", "sources": "./src/index.ts", - "import": "./dist/index.js", - "types": "./dist/index.d.ts" + "import": "./dist/index.js" }, "./dist/*": "./dist/*" }, "scripts": { - "lint": "eslint .", + "fmt": "ts-builder format", + "check": "ts-builder check --target block-model", "watch": "ts-builder build --target block-model --watch", "build": "ts-builder build --target block-model && block-tools build-model", - "type-check": "ts-builder type-check --target block-model", "test": "vitest --run --passWithNoTests", "do-pack": "rm -f *.tgz && pnpm pack && mv *.tgz package.tgz" }, @@ -31,7 +31,7 @@ "@platforma-sdk/block-tools": "catalog:", "@platforma-sdk/eslint-config": "catalog:", "eslint": "catalog:", - "vitest": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/model/src/index.ts b/model/src/index.ts index d616d4b..02e1dde 100644 --- a/model/src/index.ts +++ b/model/src/index.ts @@ -1,4 +1,9 @@ -import type { ImportFileHandle, InferHrefType, PlDataTableStateV2, PlRef } from '@platforma-sdk/model'; +import type { + ImportFileHandle, + InferHrefType, + PlDataTableStateV2, + PlRef, +} from "@platforma-sdk/model"; import { BlockModel, createPlDataTableStateV2, @@ -6,13 +11,13 @@ import { isPColumnSpec, parseResourceMap, type InferOutputsType, -} from '@platforma-sdk/model'; -import { ProgressPrefix } from './progress'; +} from "@platforma-sdk/model"; +import { ProgressPrefix } from "./progress"; -export type CloneClusteringMode = 'relaxed' | 'default' | 'off'; +export type CloneClusteringMode = "relaxed" | "default" | "off"; export type AssemblingFeature = string; -export type StopCodonType = 'amber' | 'ochre' | 'opal'; -export type ReferenceInputMode = 'fastaFile' | 'fastaSequence' | 'libraryFile' | 'buildLibrary'; +export type StopCodonType = "amber" | "ochre" | "opal"; +export type ReferenceInputMode = "fastaFile" | "fastaSequence" | "libraryFile" | "buildLibrary"; export interface VAnchorPoints { fr1Begin: number; @@ -85,101 +90,105 @@ export interface BlockArgsValid extends BlockArgs { librarySequence: string; } -export const platforma = BlockModel.create('Heavy') +export const platforma = BlockModel.create("Heavy") .withArgs({ - defaultBlockLabel: '', - customBlockLabel: '', - chains: 'IGHeavy', - cloneClusteringMode: 'relaxed', - tagPattern: '', - assemblingFeature: 'VDJRegion', + defaultBlockLabel: "", + customBlockLabel: "", + chains: "IGHeavy", + cloneClusteringMode: "relaxed", + tagPattern: "", + assemblingFeature: "VDJRegion", imputeGermline: false, }) .withUiState({ - referenceInputMode: 'fastaSequence', + referenceInputMode: "fastaSequence", tableState: createPlDataTableStateV2(), }) - .output('qc', (ctx) => { - const acc = ctx.outputs?.resolve('qc'); + .output("qc", (ctx) => { + const acc = ctx.outputs?.resolve("qc"); if (!acc || !acc.getInputsLocked()) return undefined; return parseResourceMap(acc, (acc) => acc.getFileHandle(), true); }) - .output('reports', (ctx) => - parseResourceMap( - ctx.outputs?.resolve('reports'), - (acc) => acc.getFileHandle(), - false, - ), + .output("reports", (ctx) => + parseResourceMap(ctx.outputs?.resolve("reports"), (acc) => acc.getFileHandle(), false), ) - .output('logs', (ctx) => { + .output("logs", (ctx) => { return ctx.outputs !== undefined - ? parseResourceMap( - ctx.outputs?.resolve('logs'), - (acc) => acc.getLogHandle(), - false, - ) + ? parseResourceMap(ctx.outputs?.resolve("logs"), (acc) => acc.getLogHandle(), false) : undefined; }) - .output('progress', (ctx) => { + .output("progress", (ctx) => { return ctx.outputs !== undefined ? parseResourceMap( - ctx.outputs?.resolve('logs'), + ctx.outputs?.resolve("logs"), (acc) => acc.getProgressLog(ProgressPrefix), false, ) : undefined; }) - .output('referenceLibrary', (ctx) => { + .output("referenceLibrary", (ctx) => { return ctx.outputs !== undefined - ? ctx.outputs?.resolve({ field: 'referenceLibrary', assertFieldType: 'Input', allowPermanentAbsence: true })?.getRemoteFileHandle() + ? ctx.outputs + ?.resolve({ + field: "referenceLibrary", + assertFieldType: "Input", + allowPermanentAbsence: true, + }) + ?.getRemoteFileHandle() : undefined; }) - .output('debugOutput', (ctx) => { + .output("debugOutput", (ctx) => { return ctx.outputs !== undefined - ? ctx.outputs?.resolve({ field: 'debugOutput', assertFieldType: 'Input', allowPermanentAbsence: true })?.getLogHandle() + ? ctx.outputs + ?.resolve({ field: "debugOutput", assertFieldType: "Input", allowPermanentAbsence: true }) + ?.getLogHandle() : undefined; }) - .output('started', (ctx) => ctx.outputs !== undefined) + .output("started", (ctx) => ctx.outputs !== undefined) - .output('done', (ctx) => { + .output("done", (ctx) => { return ctx.outputs !== undefined - ? parseResourceMap( - ctx.outputs?.resolve('clns'), - (_acc) => true, - false, - ).data.map((e) => e.key[0] as string) + ? parseResourceMap(ctx.outputs?.resolve("clns"), (_acc) => true, false).data.map( + (e) => e.key[0] as string, + ) : undefined; }) - .output('prerunLibrary', (ctx) => - ctx.prerun?.resolve({ field: 'referenceLibrary', assertFieldType: 'Input', allowPermanentAbsence: true })?.getFileHandle(), + .output("prerunLibrary", (ctx) => + ctx.prerun + ?.resolve({ + field: "referenceLibrary", + assertFieldType: "Input", + allowPermanentAbsence: true, + }) + ?.getFileHandle(), ) - .retentiveOutput('inputOptions', (ctx) => { + .retentiveOutput("inputOptions", (ctx) => { return ctx.resultPool.getOptions((v) => { if (!isPColumnSpec(v)) return false; const domain = v.domain; return ( - v.name === 'pl7.app/sequencing/data' - && (v.valueType as string) === 'File' - && domain !== undefined - && (domain['pl7.app/fileExtension'] === 'fasta' - || domain['pl7.app/fileExtension'] === 'fasta.gz' - || domain['pl7.app/fileExtension'] === 'fastq' - || domain['pl7.app/fileExtension'] === 'fastq.gz') + v.name === "pl7.app/sequencing/data" && + (v.valueType as string) === "File" && + domain !== undefined && + (domain["pl7.app/fileExtension"] === "fasta" || + domain["pl7.app/fileExtension"] === "fasta.gz" || + domain["pl7.app/fileExtension"] === "fastq" || + domain["pl7.app/fileExtension"] === "fastq.gz") ); }); }) - .output('sampleLabels', (ctx): Record | undefined => { + .output("sampleLabels", (ctx): Record | undefined => { const inputRef = ctx.args.datasetRef; if (inputRef === undefined) return undefined; @@ -189,71 +198,79 @@ export const platforma = BlockModel.create('Heavy') return ctx.resultPool.findLabelsForColumnAxis(spec, 0); }) - .output('rawTsvs', (ctx) => { - if (ctx.outputs === undefined) - return undefined; - const pCols = ctx.outputs?.resolve('clonotypeTables')?.getPColumns(); + .output("rawTsvs", (ctx) => { + if (ctx.outputs === undefined) return undefined; + const pCols = ctx.outputs?.resolve("clonotypeTables")?.getPColumns(); if (pCols === undefined) { return undefined; } - return pCols.map((pCol) => { - return { - ...pCol, - id: (JSON.parse(pCol.id) as { name: string }).name, - data: parseResourceMap(pCol.data, (acc) => acc.getRemoteFileHandle(), false), - }; - }).filter((pCol) => pCol.data.isComplete).map((pCol) => { - return { - ...pCol, - data: pCol.data.data, - }; - }); + return pCols + .map((pCol) => { + return { + ...pCol, + id: (JSON.parse(pCol.id) as { name: string }).name, + data: parseResourceMap(pCol.data, (acc) => acc.getRemoteFileHandle(), false), + }; + }) + .filter((pCol) => pCol.data.isComplete) + .map((pCol) => { + return { + ...pCol, + data: pCol.data.data, + }; + }); }) - .outputWithStatus('pt', (ctx) => { - const pCols = ctx.outputs?.resolve({ field: 'qcReportTable', assertFieldType: 'Input', allowPermanentAbsence: true })?.getPColumns(); + .outputWithStatus("pt", (ctx) => { + const pCols = ctx.outputs + ?.resolve({ field: "qcReportTable", assertFieldType: "Input", allowPermanentAbsence: true }) + ?.getPColumns(); if (pCols === undefined) { return undefined; } - return createPlDataTableV2( - ctx, - pCols, - ctx.uiState.tableState, - ); + return createPlDataTableV2(ctx, pCols, ctx.uiState.tableState); }) .sections((_ctx) => { return [ - { type: 'link', href: '/', label: 'Main' }, - { type: 'link', href: '/qc-report-table', label: 'QC Report Table' }, + { type: "link", href: "/", label: "Main" }, + { type: "link", href: "/qc-report-table", label: "QC Report Table" }, ]; }) .argsValid((ctx) => { - const mode = ctx.uiState.referenceInputMode ?? 'fastaSequence'; + const mode = ctx.uiState.referenceInputMode ?? "fastaSequence"; const hasDataset = ctx.args.datasetRef !== undefined; - if (mode === 'libraryFile') { + if (mode === "libraryFile") { return hasDataset && ctx.args.libraryFile !== undefined; } - if (mode === 'buildLibrary') { + if (mode === "buildLibrary") { return hasDataset && (ctx.args.libraryEntries?.length ?? 0) > 0; } - return hasDataset && (ctx.uiState.librarySequence !== undefined || ctx.args.vGenes !== undefined); + return ( + hasDataset && (ctx.uiState.librarySequence !== undefined || ctx.args.vGenes !== undefined) + ); }) - .output('isRunning', (ctx) => ctx.outputs?.getIsReadyOrError() === false) + .output("isRunning", (ctx) => ctx.outputs?.getIsReadyOrError() === false) - .output('libraryUploadProgress', (ctx) => - ctx.outputs?.resolve({ field: 'libraryImportHandle', allowPermanentAbsence: true })?.getImportProgress(), { isActive: true }) + .output( + "libraryUploadProgress", + (ctx) => + ctx.outputs + ?.resolve({ field: "libraryImportHandle", allowPermanentAbsence: true }) + ?.getImportProgress(), + { isActive: true }, + ) - .title(() => 'MiXCR Amplicon Alignment') + .title(() => "MiXCR Amplicon Alignment") - .subtitle((ctx) => ctx.args.customBlockLabel || ctx.args.defaultBlockLabel || '') + .subtitle((ctx) => ctx.args.customBlockLabel || ctx.args.defaultBlockLabel || "") .done(2); export type BlockOutputs = InferOutputsType; export type Href = InferHrefType; -export * from './progress'; -export * from './qc'; -export * from './reports'; +export * from "./progress"; +export * from "./qc"; +export * from "./reports"; diff --git a/model/src/progress.ts b/model/src/progress.ts index 040b307..63b6bb4 100644 --- a/model/src/progress.ts +++ b/model/src/progress.ts @@ -1,3 +1,4 @@ -export const ProgressPrefix = '[==PROGRESS==]'; +export const ProgressPrefix = "[==PROGRESS==]"; -export const ProgressPattern = /(?[^:]*):(?: *(?[0-9.]+)%)?(?: *ETA: *(?.+))?/; +export const ProgressPattern = + /(?[^:]*):(?: *(?[0-9.]+)%)?(?: *ETA: *(?.+))?/; diff --git a/model/src/qc.ts b/model/src/qc.ts index 7001f47..2bc2fe9 100644 --- a/model/src/qc.ts +++ b/model/src/qc.ts @@ -1,10 +1,6 @@ -import { z } from 'zod'; +import { z } from "zod"; -export const QcStatus = z.union([ - z.literal('OK'), - z.literal('WARN'), - z.literal('ALERT'), -]); +export const QcStatus = z.union([z.literal("OK"), z.literal("WARN"), z.literal("ALERT")]); export const QcCheck = z.object({ type: z.string(), diff --git a/model/src/reports.ts b/model/src/reports.ts index 5e78a19..c75c620 100644 --- a/model/src/reports.ts +++ b/model/src/reports.ts @@ -1,79 +1,79 @@ -import { z } from 'zod'; +import { z } from "zod"; export const ImmuneChain = z.union([ - z.literal(''), - z.literal('TRA'), - z.literal('TRAD'), - z.literal('TRB'), - z.literal('TRG'), - z.literal('TRD'), - z.literal('IGH'), - z.literal('IGK'), - z.literal('IGL'), + z.literal(""), + z.literal("TRA"), + z.literal("TRAD"), + z.literal("TRB"), + z.literal("TRG"), + z.literal("TRD"), + z.literal("IGH"), + z.literal("IGK"), + z.literal("IGL"), ]); export type ImmuneChain = z.infer; export const NotAlignedReason = z.union([ - z.literal('NoHits'), - z.literal('FailedAfterAOverlap'), - z.literal('NoCDR3Parts'), - z.literal('NoVHits'), - z.literal('NoJHits'), - z.literal('VAndJOnDifferentTargets'), - z.literal('LowTotalScore'), - z.literal('NoBarcode'), - z.literal('SampleNotMatched'), + z.literal("NoHits"), + z.literal("FailedAfterAOverlap"), + z.literal("NoCDR3Parts"), + z.literal("NoVHits"), + z.literal("NoJHits"), + z.literal("VAndJOnDifferentTargets"), + z.literal("LowTotalScore"), + z.literal("NoBarcode"), + z.literal("SampleNotMatched"), ]); export type NotAlignedReason = z.infer; export const AlignmentChannel = z.union([ - z.literal('Success'), - z.literal('NoHits'), - z.literal('NoCDR3Parts'), - z.literal('NoVHits'), - z.literal('NoJHits'), - z.literal('VAndJOnDifferentTargets'), - z.literal('LowTotalScore'), - z.literal('NoBarcode'), + z.literal("Success"), + z.literal("NoHits"), + z.literal("NoCDR3Parts"), + z.literal("NoVHits"), + z.literal("NoJHits"), + z.literal("VAndJOnDifferentTargets"), + z.literal("LowTotalScore"), + z.literal("NoBarcode"), ]); export type AlignmentChannel = z.infer; export const AlignmentChannels = [ - 'Success', - 'NoHits', - 'NoCDR3Parts', - 'NoVHits', - 'NoJHits', - 'VAndJOnDifferentTargets', - 'LowTotalScore', - 'NoBarcode', + "Success", + "NoHits", + "NoCDR3Parts", + "NoVHits", + "NoJHits", + "VAndJOnDifferentTargets", + "LowTotalScore", + "NoBarcode", ] satisfies AlignmentChannel[]; export const AlignmentChannelLabels = { - Success: 'Successfully aligned', - NoHits: 'No hits (not TCR/IG?)', - FailedAfterAOverlap: 'Failed after alignment-overlap', - NoCDR3Parts: 'No CDR3 parts', - NoVHits: 'No V hits', - NoJHits: 'No J hits', - VAndJOnDifferentTargets: 'No target with both V and J', - LowTotalScore: 'Low total score', - NoBarcode: 'Absent barcode', - SampleNotMatched: 'Sample not matched', + Success: "Successfully aligned", + NoHits: "No hits (not TCR/IG?)", + FailedAfterAOverlap: "Failed after alignment-overlap", + NoCDR3Parts: "No CDR3 parts", + NoVHits: "No V hits", + NoJHits: "No J hits", + VAndJOnDifferentTargets: "No target with both V and J", + LowTotalScore: "Low total score", + NoBarcode: "Absent barcode", + SampleNotMatched: "Sample not matched", } satisfies Record; export const AlignmentChannelColors = { - Success: '#6BD67D', - NoHits: '#FEE27A', - FailedAfterAOverlap: 'red', - NoCDR3Parts: '#FEBF51', - NoVHits: '#FB9361', - NoJHits: '#E75B64', - VAndJOnDifferentTargets: '#B8397A', - LowTotalScore: '#7E2583', - NoBarcode: '#4B1979', - SampleNotMatched: '#2B125C', + Success: "#6BD67D", + NoHits: "#FEE27A", + FailedAfterAOverlap: "red", + NoCDR3Parts: "#FEBF51", + NoVHits: "#FB9361", + NoJHits: "#E75B64", + VAndJOnDifferentTargets: "#B8397A", + LowTotalScore: "#7E2583", + NoBarcode: "#4B1979", + SampleNotMatched: "#2B125C", } satisfies Record; const ChainUsageEntry = z.object({ @@ -90,7 +90,7 @@ const ChainUsage = z.object({ }); export const AlignReport = z.object({ - type: z.literal('alignerReport'), + type: z.literal("alignerReport"), totalReadsProcessed: z.number().int(), aligned: z.number().int(), notAligned: z.number().int(), @@ -112,11 +112,9 @@ export const AlignReport = z.object({ }); export type AlignReport = z.infer; -export function extractAlignmentChannels( - report: AlignReport, -): [AlignmentChannel, number][] { +export function extractAlignmentChannels(report: AlignReport): [AlignmentChannel, number][] { return AlignmentChannels.map((cId) => [ cId, - cId === 'Success' ? report.aligned : report.notAlignedReasons[cId] ?? 0, + cId === "Success" ? report.aligned : (report.notAlignedReasons[cId] ?? 0), ]); } diff --git a/model/vitest.config.mts b/model/vitest.config.mts index 49a0dd8..609455d 100644 --- a/model/vitest.config.mts +++ b/model/vitest.config.mts @@ -1,7 +1,7 @@ -import { defineConfig } from 'vitest/config'; +import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - watch: false - } + watch: false, + }, }); diff --git a/package.json b/package.json index 399b3c8..5e8e2bc 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "scripts": { + "fmt": "turbo run fmt", + "check": "turbo run check", "build": "turbo run build", "build:dev": "env PL_PKG_DEV=local turbo run build", - "lint": "turbo run lint", - "type-check": "turbo run type-check", "test": "env PL_PKG_DEV=local turbo run test --concurrency 1 --env-mode=loose", "test:dry-run": "env PL_PKG_DEV=local turbo run test --dry-run=json", "mark-stable": "turbo run mark-stable", @@ -14,10 +14,15 @@ "update-sdk": "block-tools update-deps" }, "devDependencies": { + "@milaboratories/ts-builder": "catalog:", "@changesets/cli": "catalog:", "@platforma-sdk/block-tools": "catalog:", "turbo": "catalog:", "typescript": "catalog:" }, - "packageManager": "pnpm@9.12.0" -} \ No newline at end of file + "packageManager": "pnpm@9.12.0", + "peerDependencies": { + "oxlint": "*", + "oxfmt": "*" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c3bcfc..7443202 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,10 +88,20 @@ catalogs: importers: .: + dependencies: + oxfmt: + specifier: '*' + version: 0.35.0 + oxlint: + specifier: '*' + version: 1.43.0 devDependencies: '@changesets/cli': specifier: 'catalog:' version: 2.29.8(@types/node@25.0.2) + '@milaboratories/ts-builder': + specifier: 'catalog:' + version: 1.3.0(@types/node@25.0.2)(esbuild@0.27.1)(rollup@4.53.4)(vue@3.5.25(typescript@5.6.3))(yaml@2.8.2) '@platforma-sdk/block-tools': specifier: 'catalog:' version: 2.6.70 diff --git a/test/.oxfmtrc.json b/test/.oxfmtrc.json new file mode 100644 index 0000000..ba9bb8f --- /dev/null +++ b/test/.oxfmtrc.json @@ -0,0 +1,3 @@ +{ + "ignorePatterns": ["dist", "CHANGELOG.md"] +} diff --git a/test/.oxlintrc.json b/test/.oxlintrc.json new file mode 100644 index 0000000..b1a1390 --- /dev/null +++ b/test/.oxlintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["node_modules/@milaboratories/ts-builder/dist/configs/oxlint-node.json"] +} diff --git a/test/eslint.config.mjs b/test/eslint.config.mjs deleted file mode 100644 index bf5dbe7..0000000 --- a/test/eslint.config.mjs +++ /dev/null @@ -1,4 +0,0 @@ -import { test } from '@platforma-sdk/eslint-config'; - -/** @type {import('eslint').Linter.Config[]} */ -export default [...test]; diff --git a/test/package.json b/test/package.json index 124f604..d745710 100644 --- a/test/package.json +++ b/test/package.json @@ -1,22 +1,22 @@ { "name": "@platforma-open/milaboratories.mixcr-amplicon-alignment.test", - "private": true, "version": "1.7.10", + "private": true, "description": "MiXCR Amplicon Alignment Tests", + "keywords": [], + "files": [], "type": "module", "scripts": { + "fmt": "ts-builder format", "test": "vitest --run --passWithNoTests", - "lint": "eslint .", - "type-check": "ts-builder type-check --target block-test" + "check": "ts-builder check --target block-test" }, - "files": [], - "keywords": [], "dependencies": { - "this-block": "workspace:@platforma-open/milaboratories.mixcr-amplicon-alignment@*", "@platforma-open/milaboratories.mixcr-amplicon-alignment.model": "workspace:*", - "@platforma-open/milaboratories.samples-and-data.model": "catalog:", "@platforma-open/milaboratories.samples-and-data": "catalog:", - "@platforma-sdk/model": "catalog:" + "@platforma-open/milaboratories.samples-and-data.model": "catalog:", + "@platforma-sdk/model": "catalog:", + "this-block": "workspace:@platforma-open/milaboratories.mixcr-amplicon-alignment@*" }, "devDependencies": { "@milaboratories/ts-builder": "catalog:", @@ -27,4 +27,4 @@ "typescript": "catalog:", "vitest": "catalog:" } -} \ No newline at end of file +} diff --git a/test/src/alignmentChartSettings.test.ts b/test/src/alignmentChartSettings.test.ts index 1cc414e..f1ec4a4 100644 --- a/test/src/alignmentChartSettings.test.ts +++ b/test/src/alignmentChartSettings.test.ts @@ -1,10 +1,10 @@ -import { test, expect } from 'vitest'; -import type { AlignReport } from '@platforma-open/milaboratories.mixcr-amplicon-alignment.model'; +import { test, expect } from "vitest"; +import type { AlignReport } from "@platforma-open/milaboratories.mixcr-amplicon-alignment.model"; // Test the core logic without importing UI components -test('alignment chart data extraction logic', () => { +test("alignment chart data extraction logic", () => { const mockAlignReport: Partial = { - type: 'alignerReport', + type: "alignerReport", totalReadsProcessed: 1000, aligned: 850, notAligned: 150, @@ -28,7 +28,9 @@ test('alignment chart data extraction logic', () => { const aligned = alignReport.aligned || 0; const notAlignedReasons = alignReport.notAlignedReasons || {}; - const result: Array<{ category: string; value: number }> = [{ category: 'Success', value: aligned }]; + const result: Array<{ category: string; value: number }> = [ + { category: "Success", value: aligned }, + ]; // Add not aligned reasons for (const [reason, count] of Object.entries(notAlignedReasons)) { @@ -45,12 +47,12 @@ test('alignment chart data extraction logic', () => { expect(result).toHaveLength(10); // Success + 9 not aligned reasons // Check that Success category exists and has correct value - const successCategory = result.find((item) => item.category === 'Success'); + const successCategory = result.find((item) => item.category === "Success"); expect(successCategory).toBeDefined(); expect(successCategory?.value).toBe(850); // Check that NoHits category exists and has correct value - const noHitsCategory = result.find((item) => item.category === 'NoHits'); + const noHitsCategory = result.find((item) => item.category === "NoHits"); expect(noHitsCategory).toBeDefined(); expect(noHitsCategory?.value).toBe(50); @@ -60,7 +62,7 @@ test('alignment chart data extraction logic', () => { }); }); -test('alignment chart data extraction with undefined report', () => { +test("alignment chart data extraction with undefined report", () => { const extractAlignmentData = (alignReport: Partial | undefined) => { if (alignReport === undefined) return []; // ... rest of logic @@ -71,9 +73,9 @@ test('alignment chart data extraction with undefined report', () => { expect(result).toHaveLength(0); }); -test('alignment chart data extraction with empty notAlignedReasons', () => { +test("alignment chart data extraction with empty notAlignedReasons", () => { const mockAlignReport: Partial = { - type: 'alignerReport', + type: "alignerReport", totalReadsProcessed: 1000, aligned: 1000, notAligned: 0, @@ -86,7 +88,9 @@ test('alignment chart data extraction with empty notAlignedReasons', () => { const aligned = alignReport.aligned || 0; const notAlignedReasons = alignReport.notAlignedReasons || {}; - const result: Array<{ category: string; value: number }> = [{ category: 'Success', value: aligned }]; + const result: Array<{ category: string; value: number }> = [ + { category: "Success", value: aligned }, + ]; // Add not aligned reasons for (const [reason, count] of Object.entries(notAlignedReasons)) { @@ -100,6 +104,6 @@ test('alignment chart data extraction with empty notAlignedReasons', () => { const result = extractAlignmentData(mockAlignReport); expect(result).toHaveLength(1); // Only Success category - expect(result[0].category).toBe('Success'); + expect(result[0].category).toBe("Success"); expect(result[0].value).toBe(1000); }); diff --git a/test/src/debugOutput.test.ts b/test/src/debugOutput.test.ts index e2d6f86..4be443a 100644 --- a/test/src/debugOutput.test.ts +++ b/test/src/debugOutput.test.ts @@ -1,19 +1,19 @@ -import { test, expect } from 'vitest'; +import { test, expect } from "vitest"; // Test the debug output functionality -test('debug output structure', () => { +test("debug output structure", () => { // Mock debug output structure const mockDebugOutput = { isComplete: true, - data: 'Repseqio library generation completed successfully', + data: "Repseqio library generation completed successfully", }; expect(mockDebugOutput.isComplete).toBe(true); - expect(typeof mockDebugOutput.data).toBe('string'); - expect(mockDebugOutput.data).toContain('Repseqio'); + expect(typeof mockDebugOutput.data).toBe("string"); + expect(mockDebugOutput.data).toContain("Repseqio"); }); -test('debug output availability', () => { +test("debug output availability", () => { // Test that debug output can be undefined initially const debugOutput = undefined; expect(debugOutput).toBeUndefined(); @@ -21,7 +21,7 @@ test('debug output availability', () => { // Test that debug output can be available later const debugOutputAvailable = { isComplete: true, - data: 'Library generation logs', + data: "Library generation logs", }; expect(debugOutputAvailable).toBeDefined(); expect(debugOutputAvailable.isComplete).toBe(true); diff --git a/test/src/exportSpecs.test.ts b/test/src/exportSpecs.test.ts index a1fa4d4..a2d8619 100644 --- a/test/src/exportSpecs.test.ts +++ b/test/src/exportSpecs.test.ts @@ -1,4 +1,4 @@ -import { test, expect, describe } from 'vitest'; +import { test, expect, describe } from "vitest"; /** * These tests replicate the key logic from calculate-export-specs.lib.tengo @@ -12,8 +12,8 @@ import { test, expect, describe } from 'vitest'; // Mirrors formatAssemblingFeature in calculate-export-specs.lib.tengo function formatAssemblingFeature(fstr: string): string { - if (fstr === 'VDJRegion' || fstr === 'CDR3') return fstr; - const parts = fstr.split(':'); + if (fstr === "VDJRegion" || fstr === "CDR3") return fstr; + const parts = fstr.split(":"); if (parts.length === 1) return `{${parts[0]}Begin:${parts[0]}End}`; return `{${parts[0]}Begin:${parts[1]}End}`; } @@ -22,9 +22,9 @@ function formatAssemblingFeature(fstr: string): string { // MiXCR has named aliases for ranges ending at FR4; other ranges use {XBegin:YEnd} function outputProductiveFeature(assemblingFeature: string): string { const productive = formatAssemblingFeature(assemblingFeature); - if (assemblingFeature !== 'VDJRegion' && assemblingFeature !== 'CDR3') { - const parts = assemblingFeature.split(':'); - if (parts.length === 2 && parts[1] === 'FR4') { + if (assemblingFeature !== "VDJRegion" && assemblingFeature !== "CDR3") { + const parts = assemblingFeature.split(":"); + if (parts.length === 2 && parts[1] === "FR4") { return `${parts[0]}_TO_FR4`; } } @@ -33,17 +33,18 @@ function outputProductiveFeature(assemblingFeature: string): string { // Mirrors parseAssemblingFeature function parseAssemblingFeature(assemblingFeature: string) { - if (assemblingFeature === 'VDJRegion' || assemblingFeature === 'CDR3') { + if (assemblingFeature === "VDJRegion" || assemblingFeature === "CDR3") { return { imputed: [] as string[], - nonImputed: assemblingFeature === 'CDR3' - ? ['CDR3'] - : ['CDR1', 'FR1', 'FR2', 'CDR2', 'FR3', 'CDR3', 'FR4', 'VDJRegion'], + nonImputed: + assemblingFeature === "CDR3" + ? ["CDR3"] + : ["CDR1", "FR1", "FR2", "CDR2", "FR3", "CDR3", "FR4", "VDJRegion"], }; } - const features = ['FR1', 'CDR1', 'FR2', 'CDR2', 'FR3', 'CDR3', 'FR4']; - const [begin, end] = assemblingFeature.split(':'); + const features = ["FR1", "CDR1", "FR2", "CDR2", "FR3", "CDR3", "FR4"]; + const [begin, end] = assemblingFeature.split(":"); const iBegin = features.indexOf(begin); const iEnd = features.indexOf(end); @@ -54,10 +55,10 @@ function parseAssemblingFeature(assemblingFeature: string) { for (let i = iEnd + 1; i < features.length; i++) imputed.push(features[i]); for (let i = iBegin; i <= iEnd; i++) nonImputed.push(features[i]); - if (begin === 'FR1' && end === 'FR4') { - nonImputed.push('VDJRegion'); + if (begin === "FR1" && end === "FR4") { + nonImputed.push("VDJRegion"); } else { - imputed.push('VDJRegion'); + imputed.push("VDJRegion"); } return { imputed, nonImputed }; @@ -81,24 +82,24 @@ function computeClonotypeKeyAndExport( let clonotypeKeyColumns: string[]; - if (assemblingFeature === 'CDR3') { - clonotypeKeyColumns = ['nSeqCDR3', 'bestVGene', 'bestJGene']; + if (assemblingFeature === "CDR3") { + clonotypeKeyColumns = ["nSeqCDR3", "bestVGene", "bestJGene"]; } else { // VDJRegion is the assembling feature itself only when it's NOT in the imputed list - const vdjIsAssemblingFeature = imputedFeaturesMap['VDJRegion'] === undefined; + const vdjIsAssemblingFeature = imputedFeaturesMap["VDJRegion"] === undefined; if (vdjIsAssemblingFeature) { // VDJRegion IS the assembling feature, use it directly - clonotypeKeyColumns = ['nSeqVDJRegion', 'bestVGene', 'bestJGene']; + clonotypeKeyColumns = ["nSeqVDJRegion", "bestVGene", "bestJGene"]; } else { // Range feature: always use assembling feature as key (not imputed VDJRegion) const keyColName = `nSeq${outputProductive}`; - clonotypeKeyColumns = [keyColName, 'bestVGene', 'bestJGene']; + clonotypeKeyColumns = [keyColName, "bestVGene", "bestJGene"]; } } - const isRangeFeature = assemblingFeature !== 'CDR3' && assemblingFeature !== 'VDJRegion'; - const vdjIsImputed = imputedFeaturesMap['VDJRegion'] === true; + const isRangeFeature = assemblingFeature !== "CDR3" && assemblingFeature !== "VDJRegion"; + const vdjIsImputed = imputedFeaturesMap["VDJRegion"] === true; const needsAssemblingFeatureExport = isRangeFeature && vdjIsImputed; let assemblingFeatureColumn: string | undefined; @@ -115,161 +116,161 @@ function computeClonotypeKeyAndExport( // --- Tests --- -describe('formatAssemblingFeature', () => { - test('CDR3 returns CDR3', () => { - expect(formatAssemblingFeature('CDR3')).toBe('CDR3'); +describe("formatAssemblingFeature", () => { + test("CDR3 returns CDR3", () => { + expect(formatAssemblingFeature("CDR3")).toBe("CDR3"); }); - test('VDJRegion returns VDJRegion', () => { - expect(formatAssemblingFeature('VDJRegion')).toBe('VDJRegion'); + test("VDJRegion returns VDJRegion", () => { + expect(formatAssemblingFeature("VDJRegion")).toBe("VDJRegion"); }); - test('range feature returns {XBegin:YEnd}', () => { - expect(formatAssemblingFeature('CDR1:FR4')).toBe('{CDR1Begin:FR4End}'); - expect(formatAssemblingFeature('FR2:FR4')).toBe('{FR2Begin:FR4End}'); - expect(formatAssemblingFeature('CDR1:CDR3')).toBe('{CDR1Begin:CDR3End}'); + test("range feature returns {XBegin:YEnd}", () => { + expect(formatAssemblingFeature("CDR1:FR4")).toBe("{CDR1Begin:FR4End}"); + expect(formatAssemblingFeature("FR2:FR4")).toBe("{FR2Begin:FR4End}"); + expect(formatAssemblingFeature("CDR1:CDR3")).toBe("{CDR1Begin:CDR3End}"); }); }); -describe('outputProductiveFeature (MiXCR column naming)', () => { - test('CDR3 returns CDR3', () => { - expect(outputProductiveFeature('CDR3')).toBe('CDR3'); +describe("outputProductiveFeature (MiXCR column naming)", () => { + test("CDR3 returns CDR3", () => { + expect(outputProductiveFeature("CDR3")).toBe("CDR3"); }); - test('VDJRegion returns VDJRegion', () => { - expect(outputProductiveFeature('VDJRegion')).toBe('VDJRegion'); + test("VDJRegion returns VDJRegion", () => { + expect(outputProductiveFeature("VDJRegion")).toBe("VDJRegion"); }); - test('ranges ending at FR4 use named aliases (X_TO_FR4)', () => { - expect(outputProductiveFeature('CDR1:FR4')).toBe('CDR1_TO_FR4'); - expect(outputProductiveFeature('FR2:FR4')).toBe('FR2_TO_FR4'); - expect(outputProductiveFeature('CDR2:FR4')).toBe('CDR2_TO_FR4'); - expect(outputProductiveFeature('FR3:FR4')).toBe('FR3_TO_FR4'); + test("ranges ending at FR4 use named aliases (X_TO_FR4)", () => { + expect(outputProductiveFeature("CDR1:FR4")).toBe("CDR1_TO_FR4"); + expect(outputProductiveFeature("FR2:FR4")).toBe("FR2_TO_FR4"); + expect(outputProductiveFeature("CDR2:FR4")).toBe("CDR2_TO_FR4"); + expect(outputProductiveFeature("FR3:FR4")).toBe("FR3_TO_FR4"); }); - test('ranges NOT ending at FR4 use {XBegin:YEnd} format', () => { - expect(outputProductiveFeature('CDR1:CDR3')).toBe('{CDR1Begin:CDR3End}'); - expect(outputProductiveFeature('FR2:CDR3')).toBe('{FR2Begin:CDR3End}'); - expect(outputProductiveFeature('CDR1:FR3')).toBe('{CDR1Begin:FR3End}'); + test("ranges NOT ending at FR4 use {XBegin:YEnd} format", () => { + expect(outputProductiveFeature("CDR1:CDR3")).toBe("{CDR1Begin:CDR3End}"); + expect(outputProductiveFeature("FR2:CDR3")).toBe("{FR2Begin:CDR3End}"); + expect(outputProductiveFeature("CDR1:FR3")).toBe("{CDR1Begin:FR3End}"); }); }); -describe('parseAssemblingFeature', () => { - test('CDR3 has no imputed features', () => { - const result = parseAssemblingFeature('CDR3'); +describe("parseAssemblingFeature", () => { + test("CDR3 has no imputed features", () => { + const result = parseAssemblingFeature("CDR3"); expect(result.imputed).toEqual([]); - expect(result.nonImputed).toEqual(['CDR3']); + expect(result.nonImputed).toEqual(["CDR3"]); }); - test('VDJRegion includes all features as nonImputed', () => { - const result = parseAssemblingFeature('VDJRegion'); + test("VDJRegion includes all features as nonImputed", () => { + const result = parseAssemblingFeature("VDJRegion"); expect(result.imputed).toEqual([]); - expect(result.nonImputed).toContain('VDJRegion'); + expect(result.nonImputed).toContain("VDJRegion"); }); - test('CDR1:CDR3 puts VDJRegion and FR1,FR4 in imputed', () => { - const result = parseAssemblingFeature('CDR1:CDR3'); - expect(result.imputed).toContain('VDJRegion'); - expect(result.imputed).toContain('FR1'); - expect(result.imputed).toContain('FR4'); - expect(result.nonImputed).toContain('CDR1'); - expect(result.nonImputed).toContain('CDR3'); + test("CDR1:CDR3 puts VDJRegion and FR1,FR4 in imputed", () => { + const result = parseAssemblingFeature("CDR1:CDR3"); + expect(result.imputed).toContain("VDJRegion"); + expect(result.imputed).toContain("FR1"); + expect(result.imputed).toContain("FR4"); + expect(result.nonImputed).toContain("CDR1"); + expect(result.nonImputed).toContain("CDR3"); }); - test('FR1:FR4 (full range) puts VDJRegion in nonImputed', () => { - const result = parseAssemblingFeature('FR1:FR4'); - expect(result.nonImputed).toContain('VDJRegion'); + test("FR1:FR4 (full range) puts VDJRegion in nonImputed", () => { + const result = parseAssemblingFeature("FR1:FR4"); + expect(result.nonImputed).toContain("VDJRegion"); expect(result.imputed).toHaveLength(0); }); - test('FR2:FR4 puts FR1, CDR1 and VDJRegion in imputed', () => { - const result = parseAssemblingFeature('FR2:FR4'); - expect(result.imputed).toContain('FR1'); - expect(result.imputed).toContain('CDR1'); - expect(result.imputed).toContain('VDJRegion'); + test("FR2:FR4 puts FR1, CDR1 and VDJRegion in imputed", () => { + const result = parseAssemblingFeature("FR2:FR4"); + expect(result.imputed).toContain("FR1"); + expect(result.imputed).toContain("CDR1"); + expect(result.imputed).toContain("VDJRegion"); }); }); -describe('clonotype key columns', () => { - test('CDR3: uses nSeqCDR3 as key', () => { - const r = computeClonotypeKeyAndExport('CDR3', false); - expect(r.clonotypeKeyColumns[0]).toBe('nSeqCDR3'); +describe("clonotype key columns", () => { + test("CDR3: uses nSeqCDR3 as key", () => { + const r = computeClonotypeKeyAndExport("CDR3", false); + expect(r.clonotypeKeyColumns[0]).toBe("nSeqCDR3"); expect(r.needsAssemblingFeatureExport).toBe(false); }); - test('VDJRegion: uses nSeqVDJRegion as key', () => { - const r = computeClonotypeKeyAndExport('VDJRegion', false); - expect(r.clonotypeKeyColumns[0]).toBe('nSeqVDJRegion'); + test("VDJRegion: uses nSeqVDJRegion as key", () => { + const r = computeClonotypeKeyAndExport("VDJRegion", false); + expect(r.clonotypeKeyColumns[0]).toBe("nSeqVDJRegion"); expect(r.needsAssemblingFeatureExport).toBe(false); }); - test('FR1:FR4 without imputation: VDJRegion non-imputed, uses nSeqVDJRegion', () => { - const r = computeClonotypeKeyAndExport('FR1:FR4', false); - expect(r.clonotypeKeyColumns[0]).toBe('nSeqVDJRegion'); + test("FR1:FR4 without imputation: VDJRegion non-imputed, uses nSeqVDJRegion", () => { + const r = computeClonotypeKeyAndExport("FR1:FR4", false); + expect(r.clonotypeKeyColumns[0]).toBe("nSeqVDJRegion"); expect(r.needsAssemblingFeatureExport).toBe(false); }); // Range ending at FR4 → MiXCR alias - test('CDR1:FR4 without imputation: uses nSeqCDR1_TO_FR4 (MiXCR alias)', () => { - const r = computeClonotypeKeyAndExport('CDR1:FR4', false); - expect(r.clonotypeKeyColumns[0]).toBe('nSeqCDR1_TO_FR4'); + test("CDR1:FR4 without imputation: uses nSeqCDR1_TO_FR4 (MiXCR alias)", () => { + const r = computeClonotypeKeyAndExport("CDR1:FR4", false); + expect(r.clonotypeKeyColumns[0]).toBe("nSeqCDR1_TO_FR4"); expect(r.needsAssemblingFeatureExport).toBe(true); - expect(r.assemblingFeatureColumn).toBe('nSeqCDR1_TO_FR4'); + expect(r.assemblingFeatureColumn).toBe("nSeqCDR1_TO_FR4"); }); - test('FR2:FR4 without imputation: uses nSeqFR2_TO_FR4 (MiXCR alias)', () => { - const r = computeClonotypeKeyAndExport('FR2:FR4', false); - expect(r.clonotypeKeyColumns[0]).toBe('nSeqFR2_TO_FR4'); + test("FR2:FR4 without imputation: uses nSeqFR2_TO_FR4 (MiXCR alias)", () => { + const r = computeClonotypeKeyAndExport("FR2:FR4", false); + expect(r.clonotypeKeyColumns[0]).toBe("nSeqFR2_TO_FR4"); expect(r.needsAssemblingFeatureExport).toBe(true); }); // Range NOT ending at FR4 → {XBegin:YEnd} format - test('CDR1:CDR3 without imputation: uses nSeq{CDR1Begin:CDR3End}', () => { - const r = computeClonotypeKeyAndExport('CDR1:CDR3', false); - expect(r.clonotypeKeyColumns[0]).toBe('nSeq{CDR1Begin:CDR3End}'); + test("CDR1:CDR3 without imputation: uses nSeq{CDR1Begin:CDR3End}", () => { + const r = computeClonotypeKeyAndExport("CDR1:CDR3", false); + expect(r.clonotypeKeyColumns[0]).toBe("nSeq{CDR1Begin:CDR3End}"); expect(r.needsAssemblingFeatureExport).toBe(true); - expect(r.assemblingFeatureColumn).toBe('nSeq{CDR1Begin:CDR3End}'); + expect(r.assemblingFeatureColumn).toBe("nSeq{CDR1Begin:CDR3End}"); }); // With imputation → still uses assembling feature key (imputed VDJRegion is NOT unique per clone) - test('CDR1:CDR3 WITH imputation: still uses nSeq{CDR1Begin:CDR3End} (not imputed VDJRegion)', () => { - const r = computeClonotypeKeyAndExport('CDR1:CDR3', true); - expect(r.clonotypeKeyColumns[0]).toBe('nSeq{CDR1Begin:CDR3End}'); + test("CDR1:CDR3 WITH imputation: still uses nSeq{CDR1Begin:CDR3End} (not imputed VDJRegion)", () => { + const r = computeClonotypeKeyAndExport("CDR1:CDR3", true); + expect(r.clonotypeKeyColumns[0]).toBe("nSeq{CDR1Begin:CDR3End}"); expect(r.needsAssemblingFeatureExport).toBe(true); }); - test('FR2:FR4 WITH imputation: still uses nSeqFR2_TO_FR4 (not imputed VDJRegion)', () => { - const r = computeClonotypeKeyAndExport('FR2:FR4', true); - expect(r.clonotypeKeyColumns[0]).toBe('nSeqFR2_TO_FR4'); + test("FR2:FR4 WITH imputation: still uses nSeqFR2_TO_FR4 (not imputed VDJRegion)", () => { + const r = computeClonotypeKeyAndExport("FR2:FR4", true); + expect(r.clonotypeKeyColumns[0]).toBe("nSeqFR2_TO_FR4"); expect(r.needsAssemblingFeatureExport).toBe(true); }); - test('FR1:FR4 WITH imputation: VDJRegion non-imputed, uses nSeqVDJRegion', () => { - const r = computeClonotypeKeyAndExport('FR1:FR4', true); - expect(r.clonotypeKeyColumns[0]).toBe('nSeqVDJRegion'); + test("FR1:FR4 WITH imputation: VDJRegion non-imputed, uses nSeqVDJRegion", () => { + const r = computeClonotypeKeyAndExport("FR1:FR4", true); + expect(r.clonotypeKeyColumns[0]).toBe("nSeqVDJRegion"); expect(r.needsAssemblingFeatureExport).toBe(false); }); }); -describe('isProductive column naming (must match MiXCR output)', () => { - test('CDR3: isProductiveCDR3', () => { - expect(`isProductive${outputProductiveFeature('CDR3')}`).toBe('isProductiveCDR3'); +describe("isProductive column naming (must match MiXCR output)", () => { + test("CDR3: isProductiveCDR3", () => { + expect(`isProductive${outputProductiveFeature("CDR3")}`).toBe("isProductiveCDR3"); }); - test('VDJRegion: isProductiveVDJRegion', () => { - expect(`isProductive${outputProductiveFeature('VDJRegion')}`).toBe('isProductiveVDJRegion'); + test("VDJRegion: isProductiveVDJRegion", () => { + expect(`isProductive${outputProductiveFeature("VDJRegion")}`).toBe("isProductiveVDJRegion"); }); - test('FR2:FR4: isProductiveFR2_TO_FR4 (MiXCR named alias)', () => { - expect(`isProductive${outputProductiveFeature('FR2:FR4')}`).toBe('isProductiveFR2_TO_FR4'); + test("FR2:FR4: isProductiveFR2_TO_FR4 (MiXCR named alias)", () => { + expect(`isProductive${outputProductiveFeature("FR2:FR4")}`).toBe("isProductiveFR2_TO_FR4"); }); - test('CDR1:FR4: isProductiveCDR1_TO_FR4 (MiXCR named alias)', () => { - expect(`isProductive${outputProductiveFeature('CDR1:FR4')}`).toBe('isProductiveCDR1_TO_FR4'); + test("CDR1:FR4: isProductiveCDR1_TO_FR4 (MiXCR named alias)", () => { + expect(`isProductive${outputProductiveFeature("CDR1:FR4")}`).toBe("isProductiveCDR1_TO_FR4"); }); - test('CDR1:CDR3: isProductive{CDR1Begin:CDR3End} (no alias)', () => { - const col = `isProductive${outputProductiveFeature('CDR1:CDR3')}`; - expect(col).toBe('isProductive{CDR1Begin:CDR3End}'); + test("CDR1:CDR3: isProductive{CDR1Begin:CDR3End} (no alias)", () => { + const col = `isProductive${outputProductiveFeature("CDR1:CDR3")}`; + expect(col).toBe("isProductive{CDR1Begin:CDR3End}"); }); }); diff --git a/test/src/wf.test.ts b/test/src/wf.test.ts index 2c73a83..dedd705 100644 --- a/test/src/wf.test.ts +++ b/test/src/wf.test.ts @@ -1,21 +1,22 @@ -import type { BlockArgs, BlockOutputs, platforma } from '@platforma-open/milaboratories.mixcr-amplicon-alignment.model'; -import { - AlignReport, - Qc, -} from '@platforma-open/milaboratories.mixcr-amplicon-alignment.model'; -import { awaitStableState, blockTest } from '@platforma-sdk/test'; -import { blockSpec as samplesAndDataBlockSpec } from '@platforma-open/milaboratories.samples-and-data'; -import type { BlockArgs as SamplesAndDataBlockArgs } from '@platforma-open/milaboratories.samples-and-data.model'; -import { uniquePlId } from '@platforma-open/milaboratories.samples-and-data.model'; -import { blockSpec as myBlockSpec } from 'this-block'; -import type { InferBlockState } from '@platforma-sdk/model'; -import { wrapOutputs } from '@platforma-sdk/model'; +import type { + BlockArgs, + BlockOutputs, + platforma, +} from "@platforma-open/milaboratories.mixcr-amplicon-alignment.model"; +import { AlignReport, Qc } from "@platforma-open/milaboratories.mixcr-amplicon-alignment.model"; +import { awaitStableState, blockTest } from "@platforma-sdk/test"; +import { blockSpec as samplesAndDataBlockSpec } from "@platforma-open/milaboratories.samples-and-data"; +import type { BlockArgs as SamplesAndDataBlockArgs } from "@platforma-open/milaboratories.samples-and-data.model"; +import { uniquePlId } from "@platforma-open/milaboratories.samples-and-data.model"; +import { blockSpec as myBlockSpec } from "this-block"; +import type { InferBlockState } from "@platforma-sdk/model"; +import { wrapOutputs } from "@platforma-sdk/model"; // prettier-ignore const referenceSequence = 'GAGGTGCAGCTCGTGGAGTCTGGGGGAGGCTTGGTCCAGCCTGGGGGGTCCCTGACACTTTCCTGTGCAGCCTCTGGATTCACCTTTAACACCTATTGGATGACCTGGGTCCGCCAGGCTCCAGGGAAGGGGCTGGAGTGGGTGGCCAATATAAATGAAGATGGAAGTGAAAACTACTATGCGGACTCTGTGAGGGGCCGATTCACCATTTTCAGAGACAACGCCAAGAACTCACTGTATCTGCAACTGAGCAGCCTGAGAGCCGAGGACACGTCTGTGTATTACTGTGCGAGATTCCGCGGGGGCCTTTGGGGCCAGGGAACCCTGGTCATTGTCTCCTCA'; -blockTest('empty inputs', { timeout: 20000 }, async ({ rawPrj: project, expect }) => { - const blockId = await project.addBlock('Block', myBlockSpec); +blockTest("empty inputs", { timeout: 20000 }, async ({ rawPrj: project, expect }) => { + const blockId = await project.addBlock("Block", myBlockSpec); const stableState = (await awaitStableState( project.getBlockState(blockId), 15000, @@ -26,30 +27,30 @@ blockTest('empty inputs', { timeout: 20000 }, async ({ rawPrj: project, expect } }); blockTest( - 'simple project', + "simple project", { timeout: 300000 }, async ({ rawPrj: project, ml, helpers, expect }) => { - const sndBlockId = await project.addBlock('Samples & Data', samplesAndDataBlockSpec); - const alignBlockId = await project.addBlock('MiXCR Amplicon Alignment', myBlockSpec); + const sndBlockId = await project.addBlock("Samples & Data", samplesAndDataBlockSpec); + const alignBlockId = await project.addBlock("MiXCR Amplicon Alignment", myBlockSpec); const sample1Id = uniquePlId(); const dataset1Id = uniquePlId(); - const r1Handle = await helpers.getLocalFileHandle('./assets/s1_R1.fastq.gz'); - const r2Handle = await helpers.getLocalFileHandle('./assets/s1_R2.fastq.gz'); + const r1Handle = await helpers.getLocalFileHandle("./assets/s1_R1.fastq.gz"); + const r2Handle = await helpers.getLocalFileHandle("./assets/s1_R2.fastq.gz"); await project.setBlockArgs(sndBlockId, { metadata: [], sampleIds: [sample1Id], - sampleLabelColumnLabel: 'Sample Name', - sampleLabels: { [sample1Id]: 'Sample 1' }, + sampleLabelColumnLabel: "Sample Name", + sampleLabels: { [sample1Id]: "Sample 1" }, datasets: [ { id: dataset1Id, - label: 'Dataset 1', + label: "Dataset 1", content: { - type: 'Fastq', - readIndices: ['R1', 'R2'], + type: "Fastq", + readIndices: ["R1", "R2"], gzipped: true, data: { [sample1Id]: { @@ -74,17 +75,16 @@ blockTest( // Wait for input options to propagate const alignBlockState = project.getBlockState(alignBlockId); - const alignStableState1 = (await awaitStableState( - alignBlockState, - 25000, - )) as InferBlockState; + const alignStableState1 = (await awaitStableState(alignBlockState, 25000)) as InferBlockState< + typeof platforma + >; expect(alignStableState1.outputs).toMatchObject({ inputOptions: { ok: true, value: [ { - label: 'Dataset 1', + label: "Dataset 1", }, ], }, @@ -98,12 +98,12 @@ blockTest( await project.setBlockArgs(alignBlockId, { datasetRef: alignOutputs1.inputOptions[0].ref, - chains: 'IGHeavy', - tagPattern: '', + chains: "IGHeavy", + tagPattern: "", vGenes: vGenesFasta, jGenes: jGenesFasta, - assemblingFeature: 'VDJRegion', - cloneClusteringMode: 'relaxed', + assemblingFeature: "VDJRegion", + cloneClusteringMode: "relaxed", } satisfies BlockArgs); const alignStableState2 = (await awaitStableState( @@ -129,7 +129,7 @@ blockTest( const reportEntries = outputs3.reports.data; const alignJsonReportEntry = reportEntries.find( - (entry) => entry.key[1] === 'align' && entry.key[2] === 'json', + (entry) => entry.key[1] === "align" && entry.key[2] === "json", ); expect(alignJsonReportEntry).toBeDefined(); @@ -141,7 +141,7 @@ blockTest( typeof ml.driverKit.blobDriver.getContent >[0], ), - ).toString('utf8'), + ).toString("utf8"), ), ); expect(alignReport).toBeDefined(); @@ -157,7 +157,7 @@ blockTest( await ml.driverKit.blobDriver.getContent( qcEntry.value!.handle as Parameters[0], ), - ).toString('utf8'), + ).toString("utf8"), ), ); expect(qc).toBeDefined(); @@ -165,30 +165,30 @@ blockTest( ); blockTest( - 'FR2:FR4 with imputation', + "FR2:FR4 with imputation", { timeout: 300000 }, async ({ rawPrj: project, ml, helpers, expect }) => { - const sndBlockId = await project.addBlock('Samples & Data', samplesAndDataBlockSpec); - const alignBlockId = await project.addBlock('MiXCR Amplicon Alignment', myBlockSpec); + const sndBlockId = await project.addBlock("Samples & Data", samplesAndDataBlockSpec); + const alignBlockId = await project.addBlock("MiXCR Amplicon Alignment", myBlockSpec); const sample1Id = uniquePlId(); const dataset1Id = uniquePlId(); - const r1Handle = await helpers.getLocalFileHandle('./assets/s1_R1.fastq.gz'); - const r2Handle = await helpers.getLocalFileHandle('./assets/s1_R2.fastq.gz'); + const r1Handle = await helpers.getLocalFileHandle("./assets/s1_R1.fastq.gz"); + const r2Handle = await helpers.getLocalFileHandle("./assets/s1_R2.fastq.gz"); await project.setBlockArgs(sndBlockId, { metadata: [], sampleIds: [sample1Id], - sampleLabelColumnLabel: 'Sample Name', - sampleLabels: { [sample1Id]: 'Sample 1' }, + sampleLabelColumnLabel: "Sample Name", + sampleLabels: { [sample1Id]: "Sample 1" }, datasets: [ { id: dataset1Id, - label: 'Dataset 1', + label: "Dataset 1", content: { - type: 'Fastq', - readIndices: ['R1', 'R2'], + type: "Fastq", + readIndices: ["R1", "R2"], gzipped: true, data: { [sample1Id]: { @@ -218,13 +218,13 @@ blockTest( await project.setBlockArgs(alignBlockId, { datasetRef: alignOutputs1.inputOptions[0].ref, - chains: 'IGHeavy', - tagPattern: '', + chains: "IGHeavy", + tagPattern: "", vGenes: vGenesFasta, jGenes: jGenesFasta, - assemblingFeature: 'FR2:FR4', + assemblingFeature: "FR2:FR4", imputeGermline: true, - cloneClusteringMode: 'relaxed', + cloneClusteringMode: "relaxed", } satisfies BlockArgs); await project.runBlock(alignBlockId); @@ -241,7 +241,7 @@ blockTest( const reportEntries = outputs3.reports.data; const alignJsonReportEntry = reportEntries.find( - (entry) => entry.key[1] === 'align' && entry.key[2] === 'json', + (entry) => entry.key[1] === "align" && entry.key[2] === "json", ); expect(alignJsonReportEntry).toBeDefined(); @@ -253,7 +253,7 @@ blockTest( typeof ml.driverKit.blobDriver.getContent >[0], ), - ).toString('utf8'), + ).toString("utf8"), ), ); expect(alignReport).toBeDefined(); @@ -265,30 +265,30 @@ blockTest( ); blockTest( - 'CDR1:CDR3 without imputation', + "CDR1:CDR3 without imputation", { timeout: 300000 }, async ({ rawPrj: project, ml, helpers, expect }) => { - const sndBlockId = await project.addBlock('Samples & Data', samplesAndDataBlockSpec); - const alignBlockId = await project.addBlock('MiXCR Amplicon Alignment', myBlockSpec); + const sndBlockId = await project.addBlock("Samples & Data", samplesAndDataBlockSpec); + const alignBlockId = await project.addBlock("MiXCR Amplicon Alignment", myBlockSpec); const sample1Id = uniquePlId(); const dataset1Id = uniquePlId(); - const r1Handle = await helpers.getLocalFileHandle('./assets/s1_R1.fastq.gz'); - const r2Handle = await helpers.getLocalFileHandle('./assets/s1_R2.fastq.gz'); + const r1Handle = await helpers.getLocalFileHandle("./assets/s1_R1.fastq.gz"); + const r2Handle = await helpers.getLocalFileHandle("./assets/s1_R2.fastq.gz"); await project.setBlockArgs(sndBlockId, { metadata: [], sampleIds: [sample1Id], - sampleLabelColumnLabel: 'Sample Name', - sampleLabels: { [sample1Id]: 'Sample 1' }, + sampleLabelColumnLabel: "Sample Name", + sampleLabels: { [sample1Id]: "Sample 1" }, datasets: [ { id: dataset1Id, - label: 'Dataset 1', + label: "Dataset 1", content: { - type: 'Fastq', - readIndices: ['R1', 'R2'], + type: "Fastq", + readIndices: ["R1", "R2"], gzipped: true, data: { [sample1Id]: { @@ -318,13 +318,13 @@ blockTest( await project.setBlockArgs(alignBlockId, { datasetRef: alignOutputs1.inputOptions[0].ref, - chains: 'IGHeavy', - tagPattern: '', + chains: "IGHeavy", + tagPattern: "", vGenes: vGenesFasta, jGenes: jGenesFasta, - assemblingFeature: 'CDR1:CDR3', + assemblingFeature: "CDR1:CDR3", imputeGermline: false, - cloneClusteringMode: 'relaxed', + cloneClusteringMode: "relaxed", } satisfies BlockArgs); await project.runBlock(alignBlockId); @@ -341,7 +341,7 @@ blockTest( const reportEntries = outputs3.reports.data; const alignJsonReportEntry = reportEntries.find( - (entry) => entry.key[1] === 'align' && entry.key[2] === 'json', + (entry) => entry.key[1] === "align" && entry.key[2] === "json", ); expect(alignJsonReportEntry).toBeDefined(); @@ -353,7 +353,7 @@ blockTest( typeof ml.driverKit.blobDriver.getContent >[0], ), - ).toString('utf8'), + ).toString("utf8"), ), ); expect(alignReport).toBeDefined(); diff --git a/test/test_config.json b/test/test_config.json index e454791..b38f4ce 100644 --- a/test/test_config.json +++ b/test/test_config.json @@ -1,3 +1,3 @@ { "address": "http://127.0.0.1:6345?tx-delay=5&force-sync=true" -} \ No newline at end of file +} diff --git a/test/vitest.config.mts b/test/vitest.config.mts index 35a4fb9..0977962 100644 --- a/test/vitest.config.mts +++ b/test/vitest.config.mts @@ -1,9 +1,9 @@ -import { defineConfig } from 'vitest/config'; +import { defineConfig } from "vitest/config"; export default defineConfig({ test: { watch: false, testTimeout: 10000, - retry: 2 - } -}); \ No newline at end of file + retry: 2, + }, +}); diff --git a/turbo.json b/turbo.json index 2400ba7..c6f0fab 100644 --- a/turbo.json +++ b/turbo.json @@ -2,11 +2,10 @@ "$schema": "https://turbo.build/schema.json", "globalDependencies": ["tsconfig.json"], "tasks": { - "lint": { - "outputs": [], - "dependsOn": ["^build"] + "fmt": { + "outputs": [] }, - "type-check": { + "check": { "outputs": [], "dependsOn": ["^build"] }, @@ -14,7 +13,7 @@ "inputs": ["$TURBO_DEFAULT$"], "env": ["PL_PKG_DEV"], "outputs": ["./dist/**", "./block-pack/**", "./pkg-*.tgz"], - "dependsOn": ["type-check", "lint", "^build"] + "dependsOn": ["check","^build"] }, "do-pack": { "dependsOn": ["build"], diff --git a/ui/.oxfmtrc.json b/ui/.oxfmtrc.json new file mode 100644 index 0000000..ba9bb8f --- /dev/null +++ b/ui/.oxfmtrc.json @@ -0,0 +1,3 @@ +{ + "ignorePatterns": ["dist", "CHANGELOG.md"] +} diff --git a/ui/.oxlintrc.json b/ui/.oxlintrc.json new file mode 100644 index 0000000..5cb5522 --- /dev/null +++ b/ui/.oxlintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["node_modules/@milaboratories/ts-builder/dist/configs/oxlint-block-ui.json"] +} diff --git a/ui/eslint.config.mjs b/ui/eslint.config.mjs deleted file mode 100644 index 30e2871..0000000 --- a/ui/eslint.config.mjs +++ /dev/null @@ -1,4 +0,0 @@ -import { ui } from '@platforma-sdk/eslint-config'; - -/** @type {import('eslint').Linter.Config[]} */ -export default [...ui]; \ No newline at end of file diff --git a/ui/index.html b/ui/index.html index e37ab12..c604936 100644 --- a/ui/index.html +++ b/ui/index.html @@ -2,11 +2,11 @@ - +
- \ No newline at end of file + diff --git a/ui/package.json b/ui/package.json index 81884b3..7a7d251 100644 --- a/ui/package.json +++ b/ui/package.json @@ -3,31 +3,31 @@ "version": "1.19.1", "type": "module", "scripts": { + "fmt": "ts-builder format", + "check": "ts-builder check --target block-ui", "dev": "ts-builder serve --target block-ui", "watch": "ts-builder build --target block-ui --watch", "build": "ts-builder build --target block-ui", - "type-check": "ts-builder type-check --target block-ui", - "lint": "eslint .", "do-pack": "rm -f *.tgz && pnpm pack && mv *.tgz package.tgz" }, "dependencies": { + "@milaboratories/helpers": "catalog:", "@platforma-open/milaboratories.mixcr-amplicon-alignment.model": "workspace:*", - "@platforma-sdk/ui-vue": "catalog:", "@platforma-sdk/model": "catalog:", - "@milaboratories/helpers": "catalog:", - "vue": "catalog:", + "@platforma-sdk/ui-vue": "catalog:", "@vueuse/core": "catalog:", + "@zip.js/zip.js": "catalog:", "ag-grid-enterprise": "catalog:", "ag-grid-vue3": "catalog:", - "@zip.js/zip.js": "catalog:" + "vue": "catalog:" }, "devDependencies": { "@milaboratories/ts-builder": "catalog:", "@milaboratories/ts-configs": "catalog:", "@platforma-sdk/eslint-config": "catalog:", + "@types/wicg-file-system-access": "catalog:", "eslint": "catalog:", - "vitest": "catalog:", "typescript": "catalog:", - "@types/wicg-file-system-access": "catalog:" + "vitest": "catalog:" } } diff --git a/ui/src/ChunkedStreamReader.ts b/ui/src/ChunkedStreamReader.ts index daa4af0..c0daa4e 100644 --- a/ui/src/ChunkedStreamReader.ts +++ b/ui/src/ChunkedStreamReader.ts @@ -1,6 +1,6 @@ -import type { RemoteBlobHandle } from '@platforma-sdk/model'; -import { getRawPlatformaInstance } from '@platforma-sdk/model'; -import { simpleRetry } from './simpleRetry.ts'; +import type { RemoteBlobHandle } from "@platforma-sdk/model"; +import { getRawPlatformaInstance } from "@platforma-sdk/model"; +import { simpleRetry } from "./simpleRetry.ts"; export class ChunkedStreamReader { private readonly handle: RemoteBlobHandle; @@ -10,10 +10,10 @@ export class ChunkedStreamReader { constructor(handle: RemoteBlobHandle, totalSize: number, chunkSize: number = 16 * 1024 * 1024) { if (totalSize < 0) { - throw new Error('Total size must be non-negative'); + throw new Error("Total size must be non-negative"); } if (chunkSize <= 0) { - throw new Error('Chunk size must be positive'); + throw new Error("Chunk size must be positive"); } this.handle = handle; @@ -24,7 +24,10 @@ export class ChunkedStreamReader { createStream(): ReadableStream { return new ReadableStream({ start: () => { - console.debug('[ChunkedStreamReader] start', { totalSize: this.totalSize, chunkSize: this.chunkSize }); + console.debug("[ChunkedStreamReader] start", { + totalSize: this.totalSize, + chunkSize: this.chunkSize, + }); }, pull: async (controller) => { @@ -36,29 +39,39 @@ export class ChunkedStreamReader { const endPosition = Math.min(this.currentPosition + this.chunkSize, this.totalSize); - const data = await simpleRetry(async () => getRawPlatformaInstance().blobDriver.getContent( - this.handle, - { from: this.currentPosition, to: endPosition }, - ), { - maxAttempts: 3, - delay: 500, - }); + const data = await simpleRetry( + async () => + getRawPlatformaInstance().blobDriver.getContent(this.handle, { + from: this.currentPosition, + to: endPosition, + }), + { + maxAttempts: 3, + delay: 500, + }, + ); controller.enqueue(data); this.currentPosition = endPosition; - if (this.currentPosition % (64 * 1024 * 1024) === 0 || this.currentPosition >= this.totalSize) { - console.debug('[ChunkedStreamReader] progress', { current: this.currentPosition, total: this.totalSize }); + if ( + this.currentPosition % (64 * 1024 * 1024) === 0 || + this.currentPosition >= this.totalSize + ) { + console.debug("[ChunkedStreamReader] progress", { + current: this.currentPosition, + total: this.totalSize, + }); } } catch (error) { - console.error('[ChunkedStreamReader] error', error); + console.error("[ChunkedStreamReader] error", error); controller.error(error); } }, cancel: (reason) => { this.currentPosition = 0; - console.debug('[ChunkedStreamReader] cancelled', reason); + console.debug("[ChunkedStreamReader] cancelled", reason); }, }); } diff --git a/ui/src/ExportRawBtn/ExportRawBtn.vue b/ui/src/ExportRawBtn/ExportRawBtn.vue index 2dd6d9c..c05052c 100644 --- a/ui/src/ExportRawBtn/ExportRawBtn.vue +++ b/ui/src/ExportRawBtn/ExportRawBtn.vue @@ -1,17 +1,12 @@ diff --git a/ui/src/ExportRawBtn/index.ts b/ui/src/ExportRawBtn/index.ts index 375b8f9..3583e3d 100644 --- a/ui/src/ExportRawBtn/index.ts +++ b/ui/src/ExportRawBtn/index.ts @@ -1 +1 @@ -export { default as ExportRawBtn } from './ExportRawBtn.vue'; +export { default as ExportRawBtn } from "./ExportRawBtn.vue"; diff --git a/ui/src/ExportRawBtn/types.ts b/ui/src/ExportRawBtn/types.ts index d810751..c87122d 100644 --- a/ui/src/ExportRawBtn/types.ts +++ b/ui/src/ExportRawBtn/types.ts @@ -2,7 +2,7 @@ export type ExportItem = { fileName: string; current: number; size: number; - status: 'pending' | 'in-progress' | 'completed'; + status: "pending" | "in-progress" | "completed"; }; export type ExportsMap = Map; diff --git a/ui/src/app.ts b/ui/src/app.ts index e3eacc0..45d1473 100644 --- a/ui/src/app.ts +++ b/ui/src/app.ts @@ -1,14 +1,14 @@ -import { platforma } from '@platforma-open/milaboratories.mixcr-amplicon-alignment.model'; -import { defineApp } from '@platforma-sdk/ui-vue'; -import MainPage from './pages/MainPage.vue'; -import QcReportTablePage from './pages/QcReportTablePage.vue'; -import { watch } from 'vue'; +import { platforma } from "@platforma-open/milaboratories.mixcr-amplicon-alignment.model"; +import { defineApp } from "@platforma-sdk/ui-vue"; +import MainPage from "./pages/MainPage.vue"; +import QcReportTablePage from "./pages/QcReportTablePage.vue"; +import { watch } from "vue"; export const sdkPlugin = defineApp(platforma, () => { return { routes: { - '/': () => MainPage, - '/qc-report-table': () => QcReportTablePage, + "/": () => MainPage, + "/qc-report-table": () => QcReportTablePage, }, }; }); @@ -19,7 +19,7 @@ export const useApp = sdkPlugin.useApp; const unwatch = watch(sdkPlugin, ({ loaded }) => { if (!loaded) return; const app = useApp(); - app.model.args.customBlockLabel ??= ''; - app.model.args.defaultBlockLabel ??= 'Select Clonotype Definition'; + app.model.args.customBlockLabel ??= ""; + app.model.args.defaultBlockLabel ??= "Select Clonotype Definition"; unwatch(); }); diff --git a/ui/src/charts/AlignmentsChart.vue b/ui/src/charts/AlignmentsChart.vue index 915fabc..b82aa49 100644 --- a/ui/src/charts/AlignmentsChart.vue +++ b/ui/src/charts/AlignmentsChart.vue @@ -1,8 +1,8 @@ @@ -215,16 +238,20 @@ watch( @update:model-value="onBuildLibraryFastaUpload" > -
- Inferring anchor points... -
+
Inferring anchor points...
- {{ expandedEntries.has(index) ? '\u25BC' : '\u25B6' }} - {{ entry.name || `Entry ${index + 1}` }} + {{ expandedEntries.has(index) ? "\u25BC" : "\u25B6" }} + {{ entry.name || `Entry ${index + 1}` }} {{ getEntryError(entry) }}
@@ -233,48 +260,100 @@ watch(
-
V gene ({{ entry.name ? entry.name + '_Vgene' : '...' }})
+
+ V gene ({{ entry.name ? entry.name + "_Vgene" : "..." }}) +
FR1
- -
{{ translateDNA(getVRegions(entry).fr1) || '-' }}
+ +
{{ translateDNA(getVRegions(entry).fr1) || "-" }}
CDR1
- -
{{ translateDNA(getVRegions(entry).cdr1) || '-' }}
+ +
{{ translateDNA(getVRegions(entry).cdr1) || "-" }}
FR2
- -
{{ translateDNA(getVRegions(entry).fr2) || '-' }}
+ +
{{ translateDNA(getVRegions(entry).fr2) || "-" }}
CDR2
- -
{{ translateDNA(getVRegions(entry).cdr2) || '-' }}
+ +
{{ translateDNA(getVRegions(entry).cdr2) || "-" }}
FR3
- -
{{ translateDNA(getVRegions(entry).fr3) || '-' }}
+ +
{{ translateDNA(getVRegions(entry).fr3) || "-" }}
V part CDR3
- -
{{ translateDNA(getVRegions(entry).vPartCdr3) || '-' }}
+ +
{{ translateDNA(getVRegions(entry).vPartCdr3) || "-" }}
-
J gene ({{ entry.name ? entry.name + '_Jgene' : '...' }})
+
+ J gene ({{ entry.name ? entry.name + "_Jgene" : "..." }}) +
J part CDR3
- -
{{ translateDNA(getJRegions(entry).jPartCdr3) || '-' }}
+ +
{{ translateDNA(getJRegions(entry).jPartCdr3) || "-" }}
FR4
- -
{{ translateDNA(getJRegions(entry).fr4) || '-' }}
+ +
{{ translateDNA(getJRegions(entry).fr4) || "-" }}
diff --git a/ui/src/pages/LogsPanel.vue b/ui/src/pages/LogsPanel.vue index 48c87e2..96045da 100644 --- a/ui/src/pages/LogsPanel.vue +++ b/ui/src/pages/LogsPanel.vue @@ -1,7 +1,7 @@