diff --git a/app/components/Entity/components/ConfigureWorkflowInputs/components/Main/components/Stepper/components/Step/hooks/UseLaunchGalaxy/types.ts b/app/components/Entity/components/ConfigureWorkflowInputs/components/Main/components/Stepper/components/Step/hooks/UseLaunchGalaxy/types.ts index 94348484c..271c567e3 100644 --- a/app/components/Entity/components/ConfigureWorkflowInputs/components/Main/components/Stepper/components/Step/hooks/UseLaunchGalaxy/types.ts +++ b/app/components/Entity/components/ConfigureWorkflowInputs/components/Main/components/Stepper/components/Step/hooks/UseLaunchGalaxy/types.ts @@ -8,21 +8,67 @@ import { UcscTrack } from "../../../../../../../../../../../../utils/ucsc-tracks import { COLUMN_TYPE } from "../../SampleSheetClassificationStep/types"; import { Strandedness } from "../../StrandednessStep/types"; -export interface ConfiguredValue { +// Base configured values shared across all scopes +export interface BaseConfiguredValue { + readRunsPaired: EnaSequencingReads[] | null; + readRunsSingle: EnaSequencingReads[] | null; + tracks: UcscTrack[] | null; +} + +// ASSEMBLY scope: workflows that operate on a specific genome assembly +export interface AssemblyConfiguredValue extends BaseConfiguredValue { + _scope: "ASSEMBLY"; designFormula: string | null; geneModelUrl: string | null; - numberOfHits?: number; primaryContrasts: PrimaryContrasts | null; - readRunsPaired: EnaSequencingReads[] | null; - readRunsSingle: EnaSequencingReads[] | null; referenceAssembly: string; sampleSheet: Record[] | null; sampleSheetClassification: Record | null; - sequence?: string; strandedness: Strandedness | undefined; - tracks: UcscTrack[] | null; } +// ORGANISM scope: workflows that operate at organism level (may not require specific assembly) +// Organism scope workflows may have collection_spec (defined in workflow YAML) and variables. +// fastaCollection can be either: +// - User-selected assembly accessions (string[]) for user-defined FASTA collections +// - Auto-configured via collection_spec in workflow YAML (handled by Galaxy API) +export interface OrganismConfiguredValue extends BaseConfiguredValue { + _scope: "ORGANISM"; + fastaCollection: string[] | null; +} + +// SEQUENCE scope: workflows that operate on user-provided sequences +export interface SequenceConfiguredValue extends BaseConfiguredValue { + _scope: "SEQUENCE"; + numberOfHits?: number; + sequence?: string; +} + +// Type guards for ConfiguredValue discrimination +export function isAssemblyConfiguredValue( + value: ConfiguredValue +): value is AssemblyConfiguredValue { + return value._scope === "ASSEMBLY"; +} + +export function isOrganismConfiguredValue( + value: ConfiguredValue +): value is OrganismConfiguredValue { + return value._scope === "ORGANISM"; +} + +export function isSequenceConfiguredValue( + value: ConfiguredValue +): value is SequenceConfiguredValue { + return value._scope === "SEQUENCE"; +} + +// Union type for all configured values +export type ConfiguredValue = + | AssemblyConfiguredValue + | OrganismConfiguredValue + | SequenceConfiguredValue; + export interface Status { disabled: boolean; error: string | null; diff --git a/app/components/Entity/components/ConfigureWorkflowInputs/components/Main/components/Stepper/components/Step/hooks/UseLaunchGalaxy/useLaunchGalaxy.ts b/app/components/Entity/components/ConfigureWorkflowInputs/components/Main/components/Stepper/components/Step/hooks/UseLaunchGalaxy/useLaunchGalaxy.ts index 2dd523971..719ce4272 100644 --- a/app/components/Entity/components/ConfigureWorkflowInputs/components/Main/components/Stepper/components/Step/hooks/UseLaunchGalaxy/useLaunchGalaxy.ts +++ b/app/components/Entity/components/ConfigureWorkflowInputs/components/Main/components/Stepper/components/Step/hooks/UseLaunchGalaxy/useLaunchGalaxy.ts @@ -11,9 +11,105 @@ import { CUSTOM_WORKFLOW } from "../../../../../../../../../../../../views/Analy import { DIFFERENTIAL_EXPRESSION_ANALYSIS } from "../../../../../../../../../../../../views/AnalyzeWorkflowsView/differentialExpressionAnalysis/constants"; import { LEXICMAP } from "../../../../../../../../../../../../views/AnalyzeWorkflowsView/lexicmap/constants"; import { LOGAN_SEARCH } from "../../../../../../../../../../../../views/AnalyzeWorkflowsView/loganSearch/constants"; -import { Props, UseLaunchGalaxy } from "./types"; +import { Workflow } from "../../../../../../../../../../../../apis/catalog/brc-analytics-catalog/common/entities"; +import { + ConfiguredValue, + isAssemblyConfiguredValue, + isSequenceConfiguredValue, + Props, + UseLaunchGalaxy, +} from "./types"; import { getConfiguredValues, launchGalaxy } from "./utils"; +/** + * Gets the appropriate landing URL based on workflow type. + * @param workflow - Workflow to launch. + * @param configuredValue - Configured values for the workflow. + * @param origin - Origin URL for the Galaxy instance. + * @returns Promise resolving to the Galaxy workflow landing URL. + */ +async function getLandingUrlForWorkflow( + workflow: Workflow, + configuredValue: ConfiguredValue, + origin: string +): Promise { + if (workflow.trsId === CUSTOM_WORKFLOW.trsId) { + if (!isAssemblyConfiguredValue(configuredValue)) { + throw new Error("Invalid configured value for CUSTOM workflow"); + } + return getDataLandingUrl( + configuredValue.referenceAssembly, + configuredValue.geneModelUrl, + configuredValue.readRunsSingle, + configuredValue.readRunsPaired, + null, + configuredValue.tracks, + origin + ); + } + + if ( + workflow.trsId === LOGAN_SEARCH.trsId || + workflow.trsId === LEXICMAP.trsId + ) { + if (!workflow.workflowId) { + throw new Error("Missing workflow ID for LMLS workflow"); + } + if ( + !isSequenceConfiguredValue(configuredValue) || + !configuredValue.numberOfHits || + !configuredValue.sequence + ) { + throw new Error("Missing required values for LMLS workflow"); + } + return getLMLSLandingUrl( + workflow.workflowId, + configuredValue.numberOfHits, + configuredValue.sequence, + origin + ); + } + + if (workflow.trsId === DIFFERENTIAL_EXPRESSION_ANALYSIS.trsId) { + if ( + !workflow.workflowId || + !isAssemblyConfiguredValue(configuredValue) || + !configuredValue.geneModelUrl || + !configuredValue.sampleSheet || + !configuredValue.sampleSheetClassification || + !configuredValue.designFormula + ) { + throw new Error("Missing required values for DE workflow"); + } + return getDeSeq2LandingUrl( + workflow.workflowId, + configuredValue.referenceAssembly, + configuredValue.geneModelUrl, + configuredValue.sampleSheet, + configuredValue.sampleSheetClassification, + configuredValue.designFormula, + configuredValue.primaryContrasts, + configuredValue.strandedness, + origin + ); + } + + return getWorkflowLandingUrl( + workflow.trsId, + isAssemblyConfiguredValue(configuredValue) + ? configuredValue.referenceAssembly + : "", + isAssemblyConfiguredValue(configuredValue) + ? configuredValue.geneModelUrl + : null, + configuredValue.readRunsSingle, + configuredValue.readRunsPaired, + null, + workflow.parameters, + origin + ); +} + export const useLaunchGalaxy = ({ configuredInput, workflow, @@ -27,79 +123,14 @@ export const useLaunchGalaxy = ({ if (!configuredValue) return; const origin = config.browserURL || window.location.origin; - let landingUrl = ""; - - if (workflow.trsId === CUSTOM_WORKFLOW.trsId) { - landingUrl = await run( - getDataLandingUrl( - configuredValue.referenceAssembly, - configuredValue.geneModelUrl, - configuredValue.readRunsSingle, - configuredValue.readRunsPaired, - null, // fastaCollection - not yet supported in UI - configuredValue.tracks, - origin - ) - ); - } else if ( - workflow.trsId === LOGAN_SEARCH.trsId || - workflow.trsId === LEXICMAP.trsId - ) { - // LMLS workflows use stored workflow IDs with sequence and numberOfHits parameters - if (!workflow.workflowId) { - throw new Error("Missing workflow ID for LMLS workflow"); - } - landingUrl = await run( - getLMLSLandingUrl( - workflow.workflowId, - configuredValue.numberOfHits!, - configuredValue.sequence!, - origin - ) - ); - } else if (workflow.trsId === DIFFERENTIAL_EXPRESSION_ANALYSIS.trsId) { - if ( - !workflow.workflowId || - !configuredValue.geneModelUrl || - !configuredValue.sampleSheet || - !configuredValue.sampleSheetClassification || - !configuredValue.designFormula - ) { - throw new Error("Missing required values for DE workflow"); - } - landingUrl = await run( - getDeSeq2LandingUrl( - workflow.workflowId, - configuredValue.referenceAssembly, - configuredValue.geneModelUrl, - configuredValue.sampleSheet, - configuredValue.sampleSheetClassification, - configuredValue.designFormula, - configuredValue.primaryContrasts, - configuredValue.strandedness, - origin - ) - ); - } else { - landingUrl = await run( - getWorkflowLandingUrl( - workflow.trsId, - configuredValue.referenceAssembly, - configuredValue.geneModelUrl, - configuredValue.readRunsSingle, - configuredValue.readRunsPaired, - null, // fastaCollection - not yet supported in UI - workflow.parameters, - origin - ) - ); - } + const landingUrl = await run( + getLandingUrlForWorkflow(workflow, configuredValue, origin) + ); if (!landingUrl) { throw new Error("Failed to retrieve Galaxy workflow launch URL."); } - // Launch the Galaxy workflow. launchGalaxy(landingUrl); }, [config, configuredValue, run, workflow]); diff --git a/app/components/Entity/components/ConfigureWorkflowInputs/components/Main/components/Stepper/components/Step/hooks/UseLaunchGalaxy/utils.ts b/app/components/Entity/components/ConfigureWorkflowInputs/components/Main/components/Stepper/components/Step/hooks/UseLaunchGalaxy/utils.ts index f8cfee59f..12bc1d61b 100644 --- a/app/components/Entity/components/ConfigureWorkflowInputs/components/Main/components/Stepper/components/Step/hooks/UseLaunchGalaxy/utils.ts +++ b/app/components/Entity/components/ConfigureWorkflowInputs/components/Main/components/Stepper/components/Step/hooks/UseLaunchGalaxy/utils.ts @@ -5,8 +5,6 @@ import { import { Workflow } from "../../../../../../../../../../../../apis/catalog/brc-analytics-catalog/common/entities"; import { WORKFLOW_PARAMETER_VARIABLE } from "../../../../../../../../../../../../apis/catalog/brc-analytics-catalog/common/schema-entities"; import { DIFFERENTIAL_EXPRESSION_ANALYSIS } from "../../../../../../../../../../../../views/AnalyzeWorkflowsView/differentialExpressionAnalysis/constants"; -import { LEXICMAP } from "../../../../../../../../../../../../views/AnalyzeWorkflowsView/lexicmap/constants"; -import { LOGAN_SEARCH } from "../../../../../../../../../../../../views/AnalyzeWorkflowsView/loganSearch/constants"; import { ConfiguredInput } from "../../../../../../../../../../../../views/WorkflowInputsView/hooks/UseConfigureInputs/types"; import { ConfiguredValue } from "./types"; @@ -54,6 +52,7 @@ function getDEConfiguredValues( } return { + _scope: "ASSEMBLY", designFormula, geneModelUrl, primaryContrasts: primaryContrasts ?? null, @@ -67,45 +66,13 @@ function getDEConfiguredValues( }; } -/** - * Validates and returns configured values for SEQUENCE scope workflows. - * SEQUENCE scope workflows (like LMLS) require sequence FASTA and numberOfHits from user input. - * @param configuredInput - Configured input. - * @returns Configured values for SEQUENCE workflow or undefined if invalid. - */ -function getLMLSConfiguredValues( - configuredInput: ConfiguredInput -): ConfiguredValue | undefined { - const { numberOfHits, sequence } = configuredInput; - - // Validate required fields for LMLS workflow - if (!sequence || numberOfHits === undefined) { - return; - } - - return { - designFormula: null, - geneModelUrl: null, - numberOfHits, - primaryContrasts: null, - readRunsPaired: null, - readRunsSingle: null, - referenceAssembly: "", - sampleSheet: null, - sampleSheetClassification: null, - sequence, - strandedness: undefined, - tracks: null, - }; -} - /** * Validates and returns configured values for standard workflows. * @param configuredInput - Configured input. * @param workflow - Workflow to check required parameters. - * @returns Configured values for standard workflow or undefined if invalid. + * @returns Configured values for ASSEMBLY workflow or undefined if invalid. */ -function getStandardConfiguredValues( +function getAssemblyScopeConfiguredValues( configuredInput: ConfiguredInput, workflow: Workflow ): ConfiguredValue | undefined { @@ -114,25 +81,25 @@ function getStandardConfiguredValues( // If workflow is not available yet, return undefined if (!workflow?.parameters) return; + // ASSEMBLY-scope workflows always require referenceAssembly + if (!referenceAssembly) return; + // Check which parameters are required by the workflow const requiredParams = getRequiredParameterTypes(workflow); - // Only check for required values - if (requiredParams.ASSEMBLY_FASTA_URL && !referenceAssembly) return; // For geneModelUrl, treat empty string as valid (user skipped or will upload manually) if (requiredParams.GENE_MODEL_URL && geneModelUrl === null) return; if (requiredParams.SANGER_READ_RUN_SINGLE && !readRunsSingle) return; if (requiredParams.SANGER_READ_RUN_PAIRED && !readRunsPaired) return; return { + _scope: "ASSEMBLY", designFormula: null, geneModelUrl: geneModelUrl ?? null, primaryContrasts: null, readRunsPaired: readRunsPaired ?? null, readRunsSingle: readRunsSingle ?? null, - // referenceAssembly is currently always set, but there are workflows that don't require referenceAssembly. - // xref https://github.com/galaxyproject/brc-analytics/issues/652 - referenceAssembly: referenceAssembly!, + referenceAssembly, sampleSheet: null, sampleSheetClassification: null, strandedness: undefined, @@ -140,6 +107,52 @@ function getStandardConfiguredValues( }; } +/** + * Returns default configured values for ORGANISM scope workflows. + * ORGANISM scope workflows may have collection_spec and variables but don't require assembly-specific inputs. + * For Phase 1, return default/empty values to allow launching directly in Galaxy. + * The collection_spec will be automatically passed by the Galaxy API. + * Phase 2 will add stepper UI to populate these values from user input. + * @returns Configured values for ORGANISM workflow. + * (Phase 2): Add validation for required parameters (e.g., check if fastaCollection or + * other organism-specific inputs are required by the workflow and return undefined if missing). + */ +function getOrganismScopeConfiguredValues(): ConfiguredValue { + return { + _scope: "ORGANISM", + fastaCollection: null, + readRunsPaired: null, + readRunsSingle: null, + tracks: null, + }; +} + +/** + * Validates and returns configured values for SEQUENCE scope workflows. + * SEQUENCE scope workflows (like LMLS) require sequence FASTA and numberOfHits from user input. + * @param configuredInput - Configured input. + * @returns Configured values for SEQUENCE workflow or undefined if invalid. + */ +function getSequenceScopeConfiguredValues( + configuredInput: ConfiguredInput +): ConfiguredValue | undefined { + const { numberOfHits, sequence } = configuredInput; + + // Validate required fields for SEQUENCE workflows + if (!sequence || numberOfHits === undefined) { + return; + } + + return { + _scope: "SEQUENCE", + numberOfHits, + readRunsPaired: null, + readRunsSingle: null, + sequence, + tracks: null, + }; +} + /** * Returns the configured values from the configured input. * @param configuredInput - Configured input. @@ -150,20 +163,20 @@ export function getConfiguredValues( configuredInput: ConfiguredInput, workflow: Workflow ): ConfiguredValue | undefined { - // Handle Differential Expression Analysis workflow separately + // Handle Differential Expression Analysis workflow separately (special case - not in IWC yet) if (workflow.trsId === DIFFERENTIAL_EXPRESSION_ANALYSIS.trsId) { return getDEConfiguredValues(configuredInput); } - // Handle LMLS workflows (SEQUENCE scope with no parameters) - if ( - workflow.trsId === LOGAN_SEARCH.trsId || - workflow.trsId === LEXICMAP.trsId - ) { - return getLMLSConfiguredValues(configuredInput); + // For all other workflows, use scope-based logic + switch (workflow.scope) { + case "ASSEMBLY": + return getAssemblyScopeConfiguredValues(configuredInput, workflow); + case "ORGANISM": + return getOrganismScopeConfiguredValues(); + case "SEQUENCE": + return getSequenceScopeConfiguredValues(configuredInput); } - - return getStandardConfiguredValues(configuredInput, workflow); } /** diff --git a/app/views/WorkflowsView/utils.ts b/app/views/WorkflowsView/utils.ts index d47e53689..a02fead72 100644 --- a/app/views/WorkflowsView/utils.ts +++ b/app/views/WorkflowsView/utils.ts @@ -56,18 +56,21 @@ function getTaxonomicLevelRealm(assembly: Assembly | undefined): string { * Filters out workflows that have no compatible assemblies for the current site. * Differential Expression Analysis is always included as an interim measure. * LMLS workflows (Logan Search and Lexicmap) are included when the 'lmls' feature flag is enabled. + * Flu workflow is conditionally included based on the 'flu' feature flag. * Each workflow includes the properties of the workflow itself along with the name of its category and the compatible assembly (if any). * @param workflowCategories - An array of workflow categories, each containing an array of workflows. * @param mappings - Workflow-assembly mappings for the current site. * @param organisms - Organisms. * @param isLmlsEnabled - Whether the 'lmls' feature flag is enabled. + * @param isFluEnabled - Whether the 'flu' feature flag is enabled. * @returns An array of workflows, where each workflow is a combination of a workflow and its category name. */ export function getWorkflows( workflowCategories: WorkflowCategory[], mappings: WorkflowAssemblyMapping[], organisms: Organism[], - isLmlsEnabled = false + isLmlsEnabled = false, + isFluEnabled = false ): WorkflowEntity[] { const workflows: WorkflowEntity[] = []; @@ -83,6 +86,15 @@ export function getWorkflows( for (const category of workflowCategories) { if (!category.workflows) continue; for (const workflow of category.workflows) { + const isFluWorkflow = + workflow.trsId === + "#workflow/github.com/iwc-workflows/influenza-isolates-consensus-and-subtyping/main/versions/v0.3"; + + // Skip flu workflow if feature flag is disabled. + if (isFluWorkflow && !isFluEnabled) { + continue; + } + // Skip workflows with no compatible assemblies. if (!workflowsWithAssemblies.has(workflow.trsId)) { continue; diff --git a/app/views/WorkflowsView/workflowsView.tsx b/app/views/WorkflowsView/workflowsView.tsx index 5a04dbef1..9c613b931 100644 --- a/app/views/WorkflowsView/workflowsView.tsx +++ b/app/views/WorkflowsView/workflowsView.tsx @@ -19,6 +19,7 @@ import { Organism } from "./types"; export const WorkflowsView = (): JSX.Element => { const workflowCategories = getWorkflowCategories(); const organisms = getOrganisms(); + const isFluEnabled = useFeatureFlag("flu"); const isLmlsEnabled = useFeatureFlag("lmls"); const [mappings, setMappings] = useState( null @@ -40,9 +41,15 @@ export const WorkflowsView = (): JSX.Element => { const workflows = useMemo( () => mappings - ? getWorkflows(workflowCategories, mappings, organisms, isLmlsEnabled) + ? getWorkflows( + workflowCategories, + mappings, + organisms, + isLmlsEnabled, + isFluEnabled + ) : [], - [isLmlsEnabled, mappings, organisms, workflowCategories] + [isFluEnabled, isLmlsEnabled, mappings, organisms, workflowCategories] ); return ; diff --git a/catalog/output/qc-report.workflow-mappings.md b/catalog/output/qc-report.workflow-mappings.md index e61fd0e6e..7e718d0f5 100644 --- a/catalog/output/qc-report.workflow-mappings.md +++ b/catalog/output/qc-report.workflow-mappings.md @@ -27,6 +27,7 @@ None | Hi-C Processing: FASTQ to Balanced Cool Files | 4326 | | Hi-C Data Processing: FASTQ to Valid Interaction Pairs | 4326 | | Variant calling and consensus construction from paired end short read data of non-segmented viral genomes | 13 | +| Influenza A isolate subtyping and consensus sequence generation | 7 | | Pox Virus Illumina Amplicon Workflow from half-genomes | 2 | | AMR Gene Detection | 393 | | Genome annotation with Braker3 | 4475 | @@ -35,6 +36,6 @@ None ## Summary Statistics -- Total active workflows: 24 -- Workflows with ≥1 compatible assembly: 24 +- Total active workflows: 25 +- Workflows with ≥1 compatible assembly: 25 - Workflows with 0 compatible assemblies: 0 diff --git a/catalog/output/workflow-assembly-mappings.json b/catalog/output/workflow-assembly-mappings.json index fb4413434..307abd39e 100644 --- a/catalog/output/workflow-assembly-mappings.json +++ b/catalog/output/workflow-assembly-mappings.json @@ -71,6 +71,10 @@ "compatibleAssemblyCount": 4326, "workflowTrsId": "#workflow/github.com/iwc-workflows/hic-hicup-cooler/hic-fastq-to-pairs-hicup/versions/v0.3" }, + { + "compatibleAssemblyCount": 7, + "workflowTrsId": "#workflow/github.com/iwc-workflows/influenza-isolates-consensus-and-subtyping/main/versions/v0.3" + }, { "compatibleAssemblyCount": 2, "workflowTrsId": "#workflow/github.com/iwc-workflows/pox-virus-amplicon/main/versions/v0.4" diff --git a/catalog/output/workflows.json b/catalog/output/workflows.json index b5ee8e0b1..335278bb4 100644 --- a/catalog/output/workflows.json +++ b/catalog/output/workflows.json @@ -534,6 +534,78 @@ "workflowDescription": "Variant calling and consensus sequence generation for batches of Illumina PE sequenced viruses with uncomplicated and stable genome structure (like e.g. Morbilliviruses).", "workflowName": "Variant calling and consensus construction from paired end short read data of non-segmented viral genomes" }, + { + "iwcId": "influenza-isolates-consensus-and-subtyping-main", + "parameters": [ + { + "key": "References per segment collection", + "collection_spec": { + "collection_type": "list", + "name": "Influenza A Segment References", + "elements": [ + { + "ext": "fasta", + "md5": "0cc350480c0ef6d2583b4764adaabb5a", + "src": "url", + "url": "https://zenodo.org/records/14628364/files/ref_1_pb2.fasta" + }, + { + "ext": "fasta", + "md5": "a8c9d6aa3c21ec24c42f480750dc4eca", + "src": "url", + "url": "https://zenodo.org/records/14628364/files/ref_2_pb1.fasta" + }, + { + "ext": "fasta", + "md5": "a5f373e913c0f84ea00cba35ea81c703", + "src": "url", + "url": "https://zenodo.org/records/14628364/files/ref_3_pa.fasta" + }, + { + "ext": "fasta", + "md5": "9476d2c02a0429e6b0e5a70ea2a94ef2", + "src": "url", + "url": "https://zenodo.org/records/14628364/files/ref_4_ha.fasta" + }, + { + "ext": "fasta", + "md5": "3d0e28d72bba2c8b352d64052524473d", + "src": "url", + "url": "https://zenodo.org/records/14628364/files/ref_5_np.fasta" + }, + { + "ext": "fasta", + "md5": "d8c21026a3defe01e020628e5b76227c", + "src": "url", + "url": "https://zenodo.org/records/14628364/files/ref_6_na.fasta" + }, + { + "ext": "fasta", + "md5": "13b0b6900807d5e557348b0e69dc79ad", + "src": "url", + "url": "https://zenodo.org/records/14628364/files/ref_7_mp.fasta" + }, + { + "ext": "fasta", + "md5": "e98fc11ed2c825e54d8d88b295449f41", + "src": "url", + "url": "https://zenodo.org/records/14628364/files/ref_8_ns.fasta" + } + ] + } + }, + { + "key": "Sequenced paired-end data", + "variable": "SANGER_READ_RUN_PAIRED" + } + ], + "ploidy": "ANY", + "scope": "ORGANISM", + "taxonomyId": "11320", + "trsId": "#workflow/github.com/iwc-workflows/influenza-isolates-consensus-and-subtyping/main/versions/v0.3", + "workflowDescription": "This workflow performs subtyping and consensus sequence generation for batches of Illumina PE sequenced Influenza A isolates.", + "workflowName": "Influenza A isolate subtyping and consensus sequence generation" + }, { "iwcId": "pox-virus-amplicon-main", "parameters": [ diff --git a/catalog/py_package/catalog_build/generated_schema/schema.py b/catalog/py_package/catalog_build/generated_schema/schema.py index c862e0812..8eda83229 100644 --- a/catalog/py_package/catalog_build/generated_schema/schema.py +++ b/catalog/py_package/catalog_build/generated_schema/schema.py @@ -120,7 +120,7 @@ class WorkflowCategoryId(str, Enum): class CollectionType(str, Enum): """ - Galaxy collection types supported for workflow parameters. + Galaxy collection types supported for collection_spec in workflow parameters. Currently only 'list' collections are supported, which represent a simple ordered list of datasets. """ diff --git a/catalog/schema/generated/schema.ts b/catalog/schema/generated/schema.ts index f9c54baec..2b2dc5c6c 100644 --- a/catalog/schema/generated/schema.ts +++ b/catalog/schema/generated/schema.ts @@ -47,7 +47,7 @@ export enum WorkflowCategoryId { OTHER = "OTHER", }; /** -* Galaxy collection types supported for workflow parameters. +* Galaxy collection types supported for collection_spec in workflow parameters. Currently only 'list' collections are supported, which represent a simple ordered list of datasets. */ export enum CollectionType { diff --git a/catalog/schema/generated/workflows.json b/catalog/schema/generated/workflows.json index e6fe294d4..7f49f687c 100644 --- a/catalog/schema/generated/workflows.json +++ b/catalog/schema/generated/workflows.json @@ -13,7 +13,7 @@ ] }, "CollectionType": { - "description": "Galaxy collection types supported for workflow parameters.\nCurrently only 'list' collections are supported, which represent a simple ordered list of datasets.", + "description": "Galaxy collection types supported for collection_spec in workflow parameters.\nCurrently only 'list' collections are supported, which represent a simple ordered list of datasets.", "enum": [ "list" ], diff --git a/catalog/source/workflows.yml b/catalog/source/workflows.yml index d6ebad962..55aed9a5d 100644 --- a/catalog/source/workflows.yml +++ b/catalog/source/workflows.yml @@ -902,14 +902,53 @@ workflows: This workflow performs subtyping and consensus sequence generation for batches of Illumina PE sequenced Influenza A isolates. ploidy: ANY + scope: ORGANISM + taxonomy_id: 11320 parameters: - key: References per segment collection + collection_spec: + collection_type: list + name: Influenza A Segment References + elements: + - ext: fasta + md5: 0cc350480c0ef6d2583b4764adaabb5a + src: url + url: https://zenodo.org/records/14628364/files/ref_1_pb2.fasta + - ext: fasta + md5: a8c9d6aa3c21ec24c42f480750dc4eca + src: url + url: https://zenodo.org/records/14628364/files/ref_2_pb1.fasta + - ext: fasta + md5: a5f373e913c0f84ea00cba35ea81c703 + src: url + url: https://zenodo.org/records/14628364/files/ref_3_pa.fasta + - ext: fasta + md5: 9476d2c02a0429e6b0e5a70ea2a94ef2 + src: url + url: https://zenodo.org/records/14628364/files/ref_4_ha.fasta + - ext: fasta + md5: 3d0e28d72bba2c8b352d64052524473d + src: url + url: https://zenodo.org/records/14628364/files/ref_5_np.fasta + - ext: fasta + md5: d8c21026a3defe01e020628e5b76227c + src: url + url: https://zenodo.org/records/14628364/files/ref_6_na.fasta + - ext: fasta + md5: 13b0b6900807d5e557348b0e69dc79ad + src: url + url: https://zenodo.org/records/14628364/files/ref_7_mp.fasta + - ext: fasta + md5: e98fc11ed2c825e54d8d88b295449f41 + src: url + url: https://zenodo.org/records/14628364/files/ref_8_ns.fasta type_guide: class: Collection - key: Sequenced paired-end data + variable: SANGER_READ_RUN_PAIRED type_guide: class: Collection - active: false + active: true iwc_id: influenza-isolates-consensus-and-subtyping-main - trs_id: "#workflow/github.com/iwc-workflows/lncRNAs-annotation/main/versions/v0.1" categories: diff --git a/pages/_app.tsx b/pages/_app.tsx index c2fba459f..7e978b147 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -50,7 +50,7 @@ export type AppPropsWithComponent = AppProps & { pageProps: PageProps; }; -setFeatureFlags(["assistant", "lmls"]); +setFeatureFlags(["assistant", "flu", "lmls"]); const queryClient = new QueryClient(); diff --git a/pages/data/workflows/[trsId]/index.tsx b/pages/data/workflows/[trsId]/index.tsx index 86ca66af5..16a48a661 100644 --- a/pages/data/workflows/[trsId]/index.tsx +++ b/pages/data/workflows/[trsId]/index.tsx @@ -21,9 +21,7 @@ export interface Props { export const getStaticPaths: GetStaticPaths = async () => { const paths = workflowCategories.reduce( - (acc, { showComingSoon, workflows }) => { - // Special case, "showComingSoon", when false, should be skipped. - if (showComingSoon === false) return acc; + (acc, { workflows }) => { for (const { trsId } of workflows) { acc.push({ params: { trsId: formatTrsId(trsId) } }); } diff --git a/tests/components/configureWorkflowInputs/getConfiguredValues.scope.test.ts b/tests/components/configureWorkflowInputs/getConfiguredValues.scope.test.ts new file mode 100644 index 000000000..dd4ef5030 --- /dev/null +++ b/tests/components/configureWorkflowInputs/getConfiguredValues.scope.test.ts @@ -0,0 +1,299 @@ +import type { Workflow } from "../../../app/apis/catalog/brc-analytics-catalog/common/entities"; +import { + WORKFLOW_PARAMETER_VARIABLE, + WORKFLOW_PLOIDY, + WORKFLOW_SCOPE, +} from "../../../app/apis/catalog/brc-analytics-catalog/common/schema-entities"; +import { getConfiguredValues } from "../../../app/components/Entity/components/ConfigureWorkflowInputs/components/Main/components/Stepper/components/Step/hooks/UseLaunchGalaxy/utils"; +import type { ConfiguredInput } from "../../../app/views/WorkflowInputsView/hooks/UseConfigureInputs/types"; + +// Mock workflow constants to avoid pulling in unneeded modules +jest.mock( + "../../../app/views/AnalyzeWorkflowsView/differentialExpressionAnalysis/constants", + () => ({ + DIFFERENTIAL_EXPRESSION_ANALYSIS: { + trsId: "differential-expression-analysis", + }, + }) +); + +jest.mock( + "../../../app/views/AnalyzeWorkflowsView/loganSearch/constants", + () => ({ + LOGAN_SEARCH: { + trsId: "logan-search", + }, + }) +); + +jest.mock("../../../app/views/AnalyzeWorkflowsView/lexicmap/constants", () => ({ + LEXICMAP: { + trsId: "lexicmap", + }, +})); + +describe("getConfiguredValues - scope-based logic", () => { + const BASE_WORKFLOW: Workflow = { + iwcId: "iwc-test", + parameters: [], + ploidy: WORKFLOW_PLOIDY.ANY, + scope: WORKFLOW_SCOPE.ASSEMBLY, + taxonomyId: null, + trsId: "#trs-test", + workflowDescription: "test workflow", + workflowName: "Test Workflow", + }; + + const BASE_CONFIGURED_INPUT: ConfiguredInput = { + designFormula: undefined, + geneModelUrl: null, + primaryContrasts: null, + readRunsPaired: null, + readRunsSingle: null, + referenceAssembly: undefined, + sampleSheet: undefined, + sampleSheetClassification: undefined, + strandedness: undefined, + tracks: null, + }; + + describe("ASSEMBLY scope", () => { + test("returns AssemblyConfiguredValue with _scope discriminant", () => { + const workflow: Workflow = { + ...BASE_WORKFLOW, + parameters: [], + scope: WORKFLOW_SCOPE.ASSEMBLY, + }; + + const configuredInput: ConfiguredInput = { + ...BASE_CONFIGURED_INPUT, + referenceAssembly: "GCF_000001405.40", + }; + + const result = getConfiguredValues(configuredInput, workflow); + + expect(result).toBeDefined(); + expect(result?._scope).toBe("ASSEMBLY"); + expect(result).toHaveProperty("referenceAssembly", "GCF_000001405.40"); + }); + + test("returns undefined when required ASSEMBLY_FASTA_URL param is missing", () => { + const workflow: Workflow = { + ...BASE_WORKFLOW, + parameters: [ + { + key: "Reference genome", + variable: WORKFLOW_PARAMETER_VARIABLE.ASSEMBLY_FASTA_URL, + }, + ], + scope: WORKFLOW_SCOPE.ASSEMBLY, + }; + + const configuredInput: ConfiguredInput = { + ...BASE_CONFIGURED_INPUT, + referenceAssembly: undefined, // Missing required value + }; + + const result = getConfiguredValues(configuredInput, workflow); + + expect(result).toBeUndefined(); + }); + + test("returns value when all required params are provided", () => { + const workflow: Workflow = { + ...BASE_WORKFLOW, + parameters: [ + { + key: "Reference genome", + variable: WORKFLOW_PARAMETER_VARIABLE.ASSEMBLY_FASTA_URL, + }, + { + key: "Gene model", + variable: WORKFLOW_PARAMETER_VARIABLE.GENE_MODEL_URL, + }, + ], + scope: WORKFLOW_SCOPE.ASSEMBLY, + }; + + const configuredInput: ConfiguredInput = { + ...BASE_CONFIGURED_INPUT, + geneModelUrl: "https://example.com/genes.gff", + referenceAssembly: "GCF_000001405.40", + }; + + const result = getConfiguredValues(configuredInput, workflow); + + expect(result).toBeDefined(); + expect(result?._scope).toBe("ASSEMBLY"); + }); + }); + + describe("ORGANISM scope", () => { + test("returns OrganismConfiguredValue with _scope discriminant", () => { + const workflow: Workflow = { + ...BASE_WORKFLOW, + scope: WORKFLOW_SCOPE.ORGANISM, + }; + + const result = getConfiguredValues(BASE_CONFIGURED_INPUT, workflow); + + expect(result).toBeDefined(); + expect(result?._scope).toBe("ORGANISM"); + }); + + test("returns OrganismConfiguredValue with fastaCollection property", () => { + const workflow: Workflow = { + ...BASE_WORKFLOW, + scope: WORKFLOW_SCOPE.ORGANISM, + }; + + const result = getConfiguredValues(BASE_CONFIGURED_INPUT, workflow); + + expect(result).toBeDefined(); + expect(result).toHaveProperty("fastaCollection", null); + }); + + test("always returns value regardless of configuredInput (no validation)", () => { + const workflow: Workflow = { + ...BASE_WORKFLOW, + scope: WORKFLOW_SCOPE.ORGANISM, + }; + + // Even with empty/null inputs, ORGANISM scope should return a value + const result = getConfiguredValues(BASE_CONFIGURED_INPUT, workflow); + + expect(result).toBeDefined(); + }); + }); + + describe("SEQUENCE scope", () => { + test("returns SequenceConfiguredValue with _scope discriminant", () => { + const workflow: Workflow = { + ...BASE_WORKFLOW, + scope: WORKFLOW_SCOPE.SEQUENCE, + }; + + const configuredInput: ConfiguredInput = { + ...BASE_CONFIGURED_INPUT, + numberOfHits: 10, + sequence: ">test\nATCG", + }; + + const result = getConfiguredValues(configuredInput, workflow); + + expect(result).toBeDefined(); + expect(result?._scope).toBe("SEQUENCE"); + }); + + test("returns SequenceConfiguredValue with numberOfHits and sequence", () => { + const workflow: Workflow = { + ...BASE_WORKFLOW, + scope: WORKFLOW_SCOPE.SEQUENCE, + }; + + const configuredInput: ConfiguredInput = { + ...BASE_CONFIGURED_INPUT, + numberOfHits: 10, + sequence: ">test\nATCG", + }; + + const result = getConfiguredValues(configuredInput, workflow); + + expect(result).toBeDefined(); + expect(result).toHaveProperty("numberOfHits", 10); + expect(result).toHaveProperty("sequence", ">test\nATCG"); + }); + + test("returns undefined when required sequence is missing", () => { + const workflow: Workflow = { + ...BASE_WORKFLOW, + scope: WORKFLOW_SCOPE.SEQUENCE, + }; + + const configuredInput: ConfiguredInput = { + ...BASE_CONFIGURED_INPUT, + numberOfHits: 10, + // sequence is missing + }; + + const result = getConfiguredValues(configuredInput, workflow); + + expect(result).toBeUndefined(); + }); + + test("returns undefined when required numberOfHits is missing", () => { + const workflow: Workflow = { + ...BASE_WORKFLOW, + scope: WORKFLOW_SCOPE.SEQUENCE, + }; + + const configuredInput: ConfiguredInput = { + ...BASE_CONFIGURED_INPUT, + sequence: ">test\nATCG", + // numberOfHits is missing + }; + + const result = getConfiguredValues(configuredInput, workflow); + + expect(result).toBeUndefined(); + }); + }); + + describe("type guards", () => { + test("ASSEMBLY scope value passes isAssemblyConfiguredValue", () => { + const workflow: Workflow = { + ...BASE_WORKFLOW, + scope: WORKFLOW_SCOPE.ASSEMBLY, + }; + + const configuredInput: ConfiguredInput = { + ...BASE_CONFIGURED_INPUT, + referenceAssembly: "GCF_000001405.40", + }; + + const result = getConfiguredValues(configuredInput, workflow); + + expect(result?._scope).toBe("ASSEMBLY"); + // Type guard would narrow to AssemblyConfiguredValue + if (result && result._scope === "ASSEMBLY") { + expect(result.referenceAssembly).toBe("GCF_000001405.40"); + } + }); + + test("ORGANISM scope value passes isOrganismConfiguredValue", () => { + const workflow: Workflow = { + ...BASE_WORKFLOW, + scope: WORKFLOW_SCOPE.ORGANISM, + }; + + const result = getConfiguredValues(BASE_CONFIGURED_INPUT, workflow); + + expect(result?._scope).toBe("ORGANISM"); + // Type guard would narrow to OrganismConfiguredValue + if (result && result._scope === "ORGANISM") { + expect(result.fastaCollection).toBeNull(); + } + }); + + test("SEQUENCE scope value passes isSequenceConfiguredValue", () => { + const workflow: Workflow = { + ...BASE_WORKFLOW, + scope: WORKFLOW_SCOPE.SEQUENCE, + }; + + const configuredInput: ConfiguredInput = { + ...BASE_CONFIGURED_INPUT, + numberOfHits: 10, + sequence: ">test\nATCG", + }; + + const result = getConfiguredValues(configuredInput, workflow); + + expect(result?._scope).toBe("SEQUENCE"); + // Type guard would narrow to SequenceConfiguredValue + if (result && result._scope === "SEQUENCE") { + expect(result.numberOfHits).toBe(10); + } + }); + }); +});