diff --git a/kubernetes/loculus/templates/_merged-reference-genomes.tpl b/kubernetes/loculus/templates/_merged-reference-genomes.tpl index ae6d685d18..58d6d58fac 100644 --- a/kubernetes/loculus/templates/_merged-reference-genomes.tpl +++ b/kubernetes/loculus/templates/_merged-reference-genomes.tpl @@ -1,60 +1,88 @@ {{- define "loculus.mergeReferenceGenomes" -}} -{{- $referenceGenomes := . -}} +{{- $segmentFirstConfig := . -}} {{- $lapisNucleotideSequences := list -}} {{- $lapisGenes := list -}} -{{- if len $referenceGenomes | eq 1 }} - {{- include "loculus.generateReferenceGenome" (first (values $referenceGenomes)) -}} -{{- else }} - {{- range $suborganismName, $referenceGenomeRaw := $referenceGenomes -}} - {{- $referenceGenome := include "loculus.generateReferenceGenome" $referenceGenomeRaw | fromYaml -}} +{{/* Handle empty reference genomes */}} +{{- if or (not $segmentFirstConfig) (eq (len $segmentFirstConfig) 0) -}} +{{- $result := dict "nucleotideSequences" (list) "genes" (list) -}} +{{- $result | toYaml -}} +{{- else -}} - {{- $nucleotideSequences := $referenceGenome.nucleotideSequences -}} - {{- if $nucleotideSequences -}} - {{- if eq (len $nucleotideSequences) 1 -}} - {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict - "name" $suborganismName - "sequence" (first $nucleotideSequences).sequence) - -}} - {{- else -}} - {{- range $sequence := $nucleotideSequences -}} - {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict - "name" (printf "%s-%s" $suborganismName $sequence.name) - "sequence" $sequence.sequence +{{/* Extract all unique reference names from the first segment */}} +{{- $referenceNames := list -}} +{{- $firstSegment := first (values $segmentFirstConfig) -}} +{{- $referenceNames = keys $firstSegment -}} + +{{/* Check if this is single-reference mode (only one reference across all segments) */}} +{{- if eq (len $referenceNames) 1 -}} + {{/* Single reference mode - no prefixing */}} + {{- $singleRef := first $referenceNames -}} + + {{/* Process each segment */}} + {{- range $segmentName, $refMap := $segmentFirstConfig -}} + {{- $refData := index $refMap $singleRef -}} + {{- if $refData -}} + {{/* Add nucleotide sequence */}} + {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict + "name" $segmentName + "sequence" $refData.sequence + ) -}} + + {{/* Add genes if present */}} + {{- if $refData.genes -}} + {{- range $geneName, $geneData := $refData.genes -}} + {{- $lapisGenes = append $lapisGenes (dict + "name" $geneName + "sequence" $geneData.sequence ) -}} {{- end -}} {{- end -}} {{- end -}} + {{- end -}} + +{{- else -}} +{{/* Multi-reference mode - prefix with reference name */}} + +{{/* Process each reference */}} +{{- range $refName := $referenceNames -}} + {{/* Process each segment */}} + {{- range $segmentName, $refMap := $segmentFirstConfig -}} + {{- $refData := index $refMap $refName -}} + {{- if $refData -}} + {{/* Add nucleotide sequence with reference prefix */}} + {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict + "name" (printf "%s-%s" $refName $segmentName) + "sequence" $refData.sequence + ) -}} - {{- if $referenceGenome.genes -}} - {{- range $gene := $referenceGenome.genes -}} - {{- $lapisGenes = append $lapisGenes (dict - "name" (printf "%s-%s" $suborganismName $gene.name) - "sequence" $gene.sequence) - -}} + {{/* Add genes with reference prefix if present */}} + {{- if $refData.genes -}} + {{- range $geneName, $geneData := $refData.genes -}} + {{- $lapisGenes = append $lapisGenes (dict + "name" (printf "%s-%s" $refName $geneName) + "sequence" $geneData.sequence + ) -}} + {{- end -}} + {{- end -}} {{- end -}} {{- end -}} - {{- end -}} +{{- end -}} - {{- $result := dict "nucleotideSequences" $lapisNucleotideSequences "genes" $lapisGenes -}} - {{- $result | toYaml -}} {{- end -}} +{{- $result := dict "nucleotideSequences" $lapisNucleotideSequences "genes" $lapisGenes -}} +{{- $result | toYaml -}} +{{- end -}} {{- end -}} {{- define "loculus.extractUniqueRawNucleotideSequenceNames" -}} -{{- $referenceGenomes := . -}} -{{- $segmentNames := list -}} +{{- $segmentFirstConfig := . -}} -{{- range $suborganismName, $referenceGenomeRaw := $referenceGenomes -}} - {{- $referenceGenome := include "loculus.generateReferenceGenome" $referenceGenomeRaw | fromYaml -}} - - {{- range $sequence := $referenceGenome.nucleotideSequences -}} - {{- $segmentNames = append $segmentNames $sequence.name -}} - {{- end -}} -{{- end -}} +{{/* Extract segment names directly from top-level keys */}} +{{- $segmentNames := keys $segmentFirstConfig -}} segments: -{{- $segmentNames | uniq | toYaml | nindent 2 -}} +{{- $segmentNames | sortAlpha | toYaml | nindent 2 -}} {{- end -}} diff --git a/kubernetes/loculus/values.schema.json b/kubernetes/loculus/values.schema.json index 4691521b18..4dc049877e 100644 --- a/kubernetes/loculus/values.schema.json +++ b/kubernetes/loculus/values.schema.json @@ -793,70 +793,60 @@ "groups": ["organism"], "docsIncludePrefix": false, "type": "object", - "description": "An object where the keys are the suborganism names and the values are a [Reference Genome](#reference-genome-type). If there is only one suborganism, then the key must be \"singleReference\".", + "description": "Segment-first reference genome structure. The top-level keys are segment names, and each segment maps to reference genomes keyed by reference name (e.g., CV-A16, CV-A10). Each reference contains a nucleotide sequence and optionally genes. All segments must define the same set of reference names.", + "additionalProperties": false, "patternProperties": { "^[a-zA-Z0-9_-]+$": { "type": "object", - "additionalProperties": false, - "properties": { - "nucleotideSequences": { - "groups": ["reference-genome"], - "docsIncludePrefix": false, - "type": "array", - "description": "Array of [Nucleotide sequence (type)](#nucleotidesequence-type)", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "groups": ["nucleotide-sequence"], - "docsIncludePrefix": false, - "type": "string", - "description": "Name of the sequence" - }, - "sequence": { - "groups": ["nucleotide-sequence"], - "docsIncludePrefix": false, - "type": "string" - }, - "insdcAccessionFull": { - "groups": ["nucleotide-sequence"], - "docsIncludePrefix": false, - "type": "string", - "description": "INSDC accession of the sequence" - } + "description": "Segment name (e.g., 'main', 'L', 'M', 'S')", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "type": "object", + "description": "Reference name (e.g., 'CV-A16', 'CV-A10', or 'singleReference')", + "additionalProperties": false, + "properties": { + "sequence": { + "groups": ["nucleotide-sequence"], + "docsIncludePrefix": false, + "type": "string", + "description": "The nucleotide sequence for this segment/reference combination" }, - "required": ["name", "sequence"] - } - }, - "genes": { - "groups": ["reference-genome"], - "docsIncludePrefix": false, - "type": "array", - "description": "Array of [Gene (type)](#gene-type)", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "groups": ["gene"], - "docsIncludePrefix": false, - "type": "string", - "description": "Name of the sequence." - }, - "sequence": { - "groups": ["gene"], - "docsIncludePrefix": false, - "type": "string" - } + "insdcAccessionFull": { + "groups": ["nucleotide-sequence"], + "docsIncludePrefix": false, + "type": "string", + "description": "INSDC accession of the sequence" }, - "required": ["name", "sequence"] - } + "genes": { + "groups": ["gene"], + "docsIncludePrefix": false, + "type": "object", + "description": "Genes for this segment/reference combination", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "type": "object", + "description": "Gene name (e.g., 'VP4', 'NS1')", + "additionalProperties": false, + "properties": { + "sequence": { + "groups": ["gene"], + "docsIncludePrefix": false, + "type": "string", + "description": "The amino acid or nucleotide sequence for this gene" + } + }, + "required": ["sequence"] + } + }, + "additionalProperties": false + } + }, + "required": ["sequence"] } - } + }, + "additionalProperties": false } - }, - "additionalProperties": false + } } }, "required": ["schema"] diff --git a/kubernetes/loculus/values.yaml b/kubernetes/loculus/values.yaml index 701714a8d1..400972d6e7 100644 --- a/kubernetes/loculus/values.yaml +++ b/kubernetes/loculus/values.yaml @@ -1374,30 +1374,29 @@ defaultOrganisms: scientific_name: "Sudan ebolavirus" molecule_type: "genomic RNA" referenceGenomes: - singleReference: - nucleotideSequences: - - name: "main" - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/reference.fasta]]" - insdcAccessionFull: NC_002549.1 - genes: - - name: NP - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/NP.fasta]]" - - name: VP35 - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/VP35.fasta]]" - - name: VP40 - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/VP40.fasta]]" - - name: GP - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/GP.fasta]]" - - name: ssGP - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/ssGP.fasta]]" - - name: sGP - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/sGP.fasta]]" - - name: VP30 - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/VP30.fasta]]" - - name: VP24 - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/VP24.fasta]]" - - name: L - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/L.fasta]]" + main: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/reference.fasta]]" + insdcAccessionFull: NC_002549.1 + genes: + NP: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/NP.fasta]]" + VP35: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/VP35.fasta]]" + VP40: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/VP40.fasta]]" + GP: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/GP.fasta]]" + ssGP: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/ssGP.fasta]]" + sGP: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/sGP.fasta]]" + VP30: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/VP30.fasta]]" + VP24: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/VP24.fasta]]" + L: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/L.fasta]]" west-nile: <<: *defaultOrganismConfig schema: @@ -1455,34 +1454,33 @@ defaultOrganisms: scientific_name: "West Nile virus" molecule_type: "genomic RNA" referenceGenomes: - singleReference: - nucleotideSequences: - - name: main - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/reference.fasta]]" - insdcAccessionFull: NC_009942.1 - genes: - - name: 2K - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/2K.fasta]]" - - name: NS1 - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS1.fasta]]" - - name: NS2A - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS2A.fasta]]" - - name: NS2B - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS2B.fasta]]" - - name: NS3 - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS3.fasta]]" - - name: NS4A - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS4A.fasta]]" - - name: NS4B - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS4B.fasta]]" - - name: NS5 - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS5.fasta]]" - - name: capsid - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/capsid.fasta]]" - - name: env - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/env.fasta]]" - - name: prM - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/prM.fasta]]" + main: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/reference.fasta]]" + insdcAccessionFull: NC_009942.1 + genes: + 2K: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/2K.fasta]]" + NS1: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS1.fasta]]" + NS2A: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS2A.fasta]]" + NS2B: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS2B.fasta]]" + NS3: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS3.fasta]]" + NS4A: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS4A.fasta]]" + NS4B: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS4B.fasta]]" + NS5: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS5.fasta]]" + capsid: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/capsid.fasta]]" + env: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/env.fasta]]" + prM: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/prM.fasta]]" dummy-organism: schema: submissionDataTypes: *defaultSubmissionDataTypes @@ -1566,35 +1564,70 @@ defaultOrganisms: - "--withErrors" - "--randomWarnError" referenceGenomes: - singleReference: - nucleotideSequences: - - name: "main" - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/reference.fasta]]" - genes: - - name: "E" - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/E.fasta]]" - - name: "M" - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/M.fasta]]" - - name: "N" - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/N.fasta]]" - - name: "ORF1a" - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF1a.fasta]]" - - name: "ORF1b" - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF1b.fasta]]" - - name: "ORF3a" - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF3a.fasta]]" - - name: "ORF6" - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF6.fasta]]" - - name: "ORF7a" - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF7a.fasta]]" - - name: "ORF7b" - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF7b.fasta]]" - - name: "ORF8" - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF8.fasta]]" - - name: "ORF9b" - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF9b.fasta]]" - - name: "S" - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/S.fasta]]" + main: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/reference.fasta]]" + genes: + "E": + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/E.fasta]]" + "M": + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/M.fasta]]" + "N": + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/N.fasta]]" + "ORF1a": + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF1a.fasta]]" + "ORF1b": + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF1b.fasta]]" + "ORF3a": + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF3a.fasta]]" + "ORF6": + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF6.fasta]]" + "ORF7a": + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF7a.fasta]]" + "ORF7b": + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF7b.fasta]]" + "ORF8": + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF8.fasta]]" + "ORF9b": + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF9b.fasta]]" + "S": + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/S.fasta]]" + E: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/E.fasta]]" + M: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/M.fasta]]" + N: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/N.fasta]]" + ORF1a: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF1a.fasta]]" + ORF1b: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF1b.fasta]]" + ORF3a: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF3a.fasta]]" + ORF6: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF6.fasta]]" + ORF7a: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF7a.fasta]]" + ORF7b: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF7b.fasta]]" + ORF8: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF8.fasta]]" + ORF9b: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/ORF9b.fasta]]" + S: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/S.fasta]]" dummy-organism-with-files: schema: image: "https://cdn.who.int/media/images/default-source/mca/mca-covid-19/coronavirus-2.tmb-1920v.jpg?sfvrsn=4dba955c_19" @@ -1622,10 +1655,7 @@ defaultOrganisms: args: - "--watch" - "--disableConsensusSequences" - referenceGenomes: - singleReference: - nucleotideSequences: [] - genes: [] + referenceGenomes: {} not-aligned-organism: enabled: true schema: @@ -1706,11 +1736,9 @@ defaultOrganisms: - name: "main" genes: [] referenceGenomes: - singleReference: - nucleotideSequences: - - name: "main" - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/reference.fasta]]" - genes: [] + main: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/reference.fasta]]" cchf: <<: *defaultOrganismConfig schema: @@ -1797,24 +1825,27 @@ defaultOrganisms: scientific_name: "Orthonairovirus haemorrhagiae" molecule_type: "genomic RNA" referenceGenomes: - singleReference: - nucleotideSequences: - - name: L - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/reference_L.fasta]]" - insdcAccessionFull: NC_005301.3 - - name: M - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/reference_M.fasta]]" - insdcAccessionFull: NC_005300.2 - - name: S - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/reference_S.fasta]]" - insdcAccessionFull: NC_005302.1 - genes: - - name: RdRp - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/RdRp.fasta]]" - - name: GPC - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/GPC.fasta]]" - - name: NP - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/NP.fasta]]" + L: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/reference_L.fasta]]" + insdcAccessionFull: NC_005301.3 + genes: + RdRp: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/RdRp.fasta]]" + M: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/reference_M.fasta]]" + insdcAccessionFull: NC_005300.2 + genes: + GPC: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/GPC.fasta]]" + S: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/reference_S.fasta]]" + insdcAccessionFull: NC_005302.1 + genes: + NP: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/NP.fasta]]" enteroviruses: <<: *defaultOrganismConfig enabled: true @@ -1845,7 +1876,7 @@ defaultOrganisms: includeInDownloadsByDefault: true preprocessing: args: - segment: CV-A16 + segment: CV-A16-main inputs: {input: nextclade.clade} - <<: *evMetadataAdd name: clade_cv_a10 @@ -1853,7 +1884,7 @@ defaultOrganisms: onlyForSuborganism: CV-A10 preprocessing: args: - segment: CV-A10 + segment: CV-A10-main inputs: {input: nextclade.clade} - <<: *evMetadataAdd name: clade_ev_a71 @@ -1861,7 +1892,7 @@ defaultOrganisms: onlyForSuborganism: EV-A71 preprocessing: args: - segment: EV-A71 + segment: EV-A71-main inputs: {input: nextclade.clade} - <<: *evMetadataAdd name: clade_ev_d68 @@ -1869,7 +1900,7 @@ defaultOrganisms: onlyForSuborganism: EV-D68 preprocessing: args: - segment: EV-D68 + segment: EV-D68-main inputs: {input: nextclade.clade} - name: genotype displayName: Genotype @@ -1956,118 +1987,112 @@ defaultOrganisms: molecule_type: "genomic RNA" suborganismIdentifierField: genotype referenceGenomes: - CV-A16: - nucleotideSequences: - - name: main - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/reference-cva16.fasta]]" - insdcAccessionFull: U05876.1 - genes: - - name: VP4 - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/VP4-cva16.fasta]]" - - name: VP2 - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/VP2-cva16.fasta]]" - - name: VP3 - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/VP3-cva16.fasta]]" - - name: VP1 - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/VP1-cva16.fasta]]" - - name: 2A - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/2A-cva16.fasta]]" - - name: 2B - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/2B-cva16.fasta]]" - - name: 2C - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/2C-cva16.fasta]]" - - name: 3A - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/3A-cva16.fasta]]" - - name: 3B - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/3B-cva16.fasta]]" - - name: 3C - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/3C-cva16.fasta]]" - - name: 3D - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/3D-cva16.fasta]]" - CV-A10: - nucleotideSequences: - - name: main - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/reference-cva10.fasta]]" - insdcAccessionFull: AY421767.1 - genes: - - name: VP4 - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/VP4-cva10.fasta]]" - - name: VP2 - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/VP2-cva10.fasta]]" - - name: VP3 - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/VP3-cva10.fasta]]" - - name: VP1 - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/VP1-cva10.fasta]]" - - name: 2A - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/2A-cva10.fasta]]" - - name: 2B - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/2B-cva10.fasta]]" - - name: 2C - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/2C-cva10.fasta]]" - - name: 3A - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/3A-cva10.fasta]]" - - name: 3B - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/3B-cva10.fasta]]" - - name: 3C - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/3C-cva10.fasta]]" - - name: 3D - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/3D-cva10.fasta]]" - EV-A71: - nucleotideSequences: - - name: main - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/reference-eva71.fasta]]" - insdcAccessionFull: U22521.1 - genes: - - name: VP4 - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/VP4-eva71.fasta]]" - - name: VP2 - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/VP2-eva71.fasta]]" - - name: VP3 - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/VP3-eva71.fasta]]" - - name: VP1 - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/VP1-eva71.fasta]]" - - name: 2A - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/2A-eva71.fasta]]" - - name: 2B - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/2B-eva71.fasta]]" - - name: 2C - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/2C-eva71.fasta]]" - - name: 3A - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/3A-eva71.fasta]]" - - name: 3B - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/3B-eva71.fasta]]" - - name: 3C - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/3C-eva71.fasta]]" - - name: 3D - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/3D-eva71.fasta]]" - EV-D68: - nucleotideSequences: - - name: main - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/reference-evd68.fasta]]" - insdcAccessionFull: AY426531.1 - genes: - - name: VP4 - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/VP4-evd68.fasta]]" - - name: VP2 - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/VP2-evd68.fasta]]" - - name: VP3 - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/VP3-evd68.fasta]]" - - name: VP1 - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/VP1-evd68.fasta]]" - - name: 2A - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/2A-evd68.fasta]]" - - name: 2B - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/2B-evd68.fasta]]" - - name: 2C - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/2C-evd68.fasta]]" - - name: 3A - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/3A-evd68.fasta]]" - - name: 3B - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/3B-evd68.fasta]]" - - name: 3C - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/3C-evd68.fasta]]" - - name: 3D - sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/3D-evd68.fasta]]" + # NEW: Segment-first structure - each segment (main) contains references (CV-A16, CV-A10, etc.) + main: + CV-A16: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/reference-cva16.fasta]]" + insdcAccessionFull: U05876.1 + genes: + VP4: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/VP4-cva16.fasta]]" + VP2: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/VP2-cva16.fasta]]" + VP3: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/VP3-cva16.fasta]]" + VP1: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/VP1-cva16.fasta]]" + 2A: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/2A-cva16.fasta]]" + 2B: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/2B-cva16.fasta]]" + 2C: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/2C-cva16.fasta]]" + 3A: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/3A-cva16.fasta]]" + 3B: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/3B-cva16.fasta]]" + 3C: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/3C-cva16.fasta]]" + 3D: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva16/3D-cva16.fasta]]" + CV-A10: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/reference-cva10.fasta]]" + insdcAccessionFull: AY421767.1 + genes: + VP4: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/VP4-cva10.fasta]]" + VP2: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/VP2-cva10.fasta]]" + VP3: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/VP3-cva10.fasta]]" + VP1: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/VP1-cva10.fasta]]" + 2A: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/2A-cva10.fasta]]" + 2B: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/2B-cva10.fasta]]" + 2C: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/2C-cva10.fasta]]" + 3A: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/3A-cva10.fasta]]" + 3B: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/3B-cva10.fasta]]" + 3C: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/3C-cva10.fasta]]" + 3D: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/cva10/3D-cva10.fasta]]" + EV-A71: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/reference-eva71.fasta]]" + insdcAccessionFull: U22521.1 + genes: + VP4: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/VP4-eva71.fasta]]" + VP2: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/VP2-eva71.fasta]]" + VP3: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/VP3-eva71.fasta]]" + VP1: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/VP1-eva71.fasta]]" + 2A: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/2A-eva71.fasta]]" + 2B: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/2B-eva71.fasta]]" + 2C: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/2C-eva71.fasta]]" + 3A: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/3A-eva71.fasta]]" + 3B: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/3B-eva71.fasta]]" + 3C: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/3C-eva71.fasta]]" + 3D: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/eva71/3D-eva71.fasta]]" + EV-D68: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/reference-evd68.fasta]]" + insdcAccessionFull: AY426531.1 + genes: + VP4: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/VP4-evd68.fasta]]" + VP2: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/VP2-evd68.fasta]]" + VP3: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/VP3-evd68.fasta]]" + VP1: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/VP1-evd68.fasta]]" + 2A: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/2A-evd68.fasta]]" + 2B: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/2B-evd68.fasta]]" + 2C: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/2C-evd68.fasta]]" + 3A: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/3A-evd68.fasta]]" + 3B: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/3B-evd68.fasta]]" + 3C: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/3C-evd68.fasta]]" + 3D: + sequence: "[[URL:https://raw.githubusercontent.com/alejandra-gonzalezsanchez/loculus-evs/master/enterovirus/evd68/3D-evd68.fasta]]" auth: verifyEmail: false resetPasswordAllowed: true diff --git a/preprocessing/nextclade/src/loculus_preprocessing/prepro.py b/preprocessing/nextclade/src/loculus_preprocessing/prepro.py index 50c0a7c32b..24b42d208b 100644 --- a/preprocessing/nextclade/src/loculus_preprocessing/prepro.py +++ b/preprocessing/nextclade/src/loculus_preprocessing/prepro.py @@ -231,6 +231,7 @@ def processed_entry_no_alignment( # noqa: PLR0913, PLR0917 errors: list[ProcessingAnnotation], warnings: list[ProcessingAnnotation], sequenceNameToFastaId: dict[SegmentName, str], # noqa: N803 + config: Config, ) -> SubmissionData: """Process a single sequence without alignment""" @@ -239,18 +240,26 @@ def processed_entry_no_alignment( # noqa: PLR0913, PLR0917 nucleotide_insertions: dict[SegmentName, list[NucleotideInsertion]] = {} amino_acid_insertions: dict[GeneName, list[AminoAcidInsertion]] = {} + # For minimizer classification, transform segment names by appending "-main" + def transform_segment_dict(d: dict) -> dict: + if config.segment_classification_method == SegmentClassificationMethod.MINIMIZER: + return {f"{k}-main": v for k, v in d.items()} + return d + return SubmissionData( processed_entry=ProcessedEntry( accession=accession_from_str(accession_version), version=version_from_str(accession_version), data=ProcessedData( metadata=output_metadata, - unalignedNucleotideSequences=unprocessed.unalignedNucleotideSequences, + unalignedNucleotideSequences=transform_segment_dict( + unprocessed.unalignedNucleotideSequences + ), alignedNucleotideSequences=aligned_nucleotide_sequences, nucleotideInsertions=nucleotide_insertions, alignedAminoAcidSequences=aligned_aminoacid_sequences, aminoAcidInsertions=amino_acid_insertions, - sequenceNameToFastaId=sequenceNameToFastaId, + sequenceNameToFastaId=transform_segment_dict(sequenceNameToFastaId), ), errors=errors, warnings=warnings, @@ -452,17 +461,26 @@ def process_single( accession_version, unprocessed, config ) + # For minimizer classification, transform segment names by appending "-main" + # This is needed for multi-reference mode where backend expects "{reference}-main" + def transform_segment_dict(d: dict) -> dict: + if config.segment_classification_method == SegmentClassificationMethod.MINIMIZER: + return {f"{k}-main": v for k, v in d.items()} + return d + processed_entry = ProcessedEntry( accession=accession_from_str(accession_version), version=version_from_str(accession_version), data=ProcessedData( metadata=output_metadata, - unalignedNucleotideSequences=unprocessed.unalignedNucleotideSequences, - alignedNucleotideSequences=unprocessed.alignedNucleotideSequences, - nucleotideInsertions=unprocessed.nucleotideInsertions, + unalignedNucleotideSequences=transform_segment_dict( + unprocessed.unalignedNucleotideSequences + ), + alignedNucleotideSequences=transform_segment_dict(unprocessed.alignedNucleotideSequences), + nucleotideInsertions=transform_segment_dict(unprocessed.nucleotideInsertions), alignedAminoAcidSequences=unprocessed.alignedAminoAcidSequences, aminoAcidInsertions=unprocessed.aminoAcidInsertions, - sequenceNameToFastaId=unprocessed.sequenceNameToFastaId, + sequenceNameToFastaId=transform_segment_dict(unprocessed.sequenceNameToFastaId), ), errors=list(set(unprocessed.errors + iupac_errors + alignment_errors + metadata_errors)), warnings=list(set(unprocessed.warnings + alignment_warnings + metadata_warnings)), @@ -500,6 +518,7 @@ def process_single_unaligned( errors=list(set(iupac_errors + metadata_errors + segment_assignment.alert.errors)), warnings=list(set(metadata_warnings)), sequenceNameToFastaId=segment_assignment.sequenceNameToFastaId, + config=config, ) diff --git a/website/src/components/ReviewPage/ReviewPage.spec.tsx b/website/src/components/ReviewPage/ReviewPage.spec.tsx index 84baf9f394..6c861d537d 100644 --- a/website/src/components/ReviewPage/ReviewPage.spec.tsx +++ b/website/src/components/ReviewPage/ReviewPage.spec.tsx @@ -16,7 +16,7 @@ import { errorsProcessingResult, openDataUseTermsOption, } from '../../types/backend.ts'; -import { SINGLE_REFERENCE } from '../../types/referencesGenomes.ts'; +import type { ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes.ts'; const openDataUseTerms = { type: openDataUseTermsOption } as const; @@ -25,6 +25,9 @@ const unreleasedSequencesRegex = /You do not currently have any unreleased seque const testGroup = testGroups[0]; function renderReviewPage() { + const schema: ReferenceGenomesLightweightSchema = { + segments: {}, + }; return render( , ); } diff --git a/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.spec.tsx b/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.spec.tsx index 5d5b600b64..61785e7eb5 100644 --- a/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.spec.tsx +++ b/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.spec.tsx @@ -1,51 +1,60 @@ import { describe, test, expect } from 'vitest'; import { getSegmentAndGeneDisplayNameMap } from './getSegmentAndGeneDisplayNameMap.tsx'; -import { SINGLE_REFERENCE } from '../../types/referencesGenomes.ts'; +import type { ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes.ts'; describe('getSegmentAndGeneDisplayNameMap', () => { - test('should map nothing if there is only a single reference', () => { - const map = getSegmentAndGeneDisplayNameMap({ - [SINGLE_REFERENCE]: { nucleotideSegmentNames: [], geneNames: [], insdcAccessionFull: [] }, - }); + test('should map nothing if there is only a single reference with no segments', () => { + const schema: ReferenceGenomesLightweightSchema = { + segments: {}, + }; + const map = getSegmentAndGeneDisplayNameMap(schema); expect(map.size).equals(0); }); test('should map segments and genes for multiple references', () => { - const map = getSegmentAndGeneDisplayNameMap({ - suborganism1: { - nucleotideSegmentNames: ['segment1', 'segment2'], - geneNames: ['gene1', 'gene2'], - insdcAccessionFull: [], + const schema: ReferenceGenomesLightweightSchema = { + segments: { + segment1: { + references: ['suborganism1', 'suborganism2'], + insdcAccessions: {}, + genesByReference: {}, + }, + segment2: { + references: ['suborganism1', 'suborganism2'], + insdcAccessions: {}, + genesByReference: { + suborganism1: ['gene1', 'gene2'], + suborganism2: ['gene1', 'gene3'], + }, + }, }, - suborganism2: { - nucleotideSegmentNames: ['segment1', 'segment2'], - geneNames: ['gene1', 'gene3'], - insdcAccessionFull: [], - }, - }); + }; + const map = getSegmentAndGeneDisplayNameMap(schema); expect(map.get('suborganism1-segment1')).equals('segment1'); expect(map.get('suborganism2-segment1')).equals('segment1'); + expect(map.get('suborganism2-segment2')).equals('segment2'); expect(map.get('suborganism2-gene3')).equals('gene3'); }); - test('should map segment names to "main" when suborganism only has one segment', () => { - const map = getSegmentAndGeneDisplayNameMap({ - suborganism1: { - nucleotideSegmentNames: ['main'], - geneNames: ['gene1', 'gene2'], - insdcAccessionFull: [], - }, - suborganism2: { - nucleotideSegmentNames: ['main'], - geneNames: ['gene1', 'gene3'], - insdcAccessionFull: [], + test('should not prefix segments when there is only a single reference', () => { + const schema: ReferenceGenomesLightweightSchema = { + segments: { + main: { + references: ['ref1'], + insdcAccessions: {}, + genesByReference: { + ref1: ['gene1', 'gene2'], + }, + }, }, - }); + }; + const map = getSegmentAndGeneDisplayNameMap(schema); - expect(map.get('suborganism1')).equals('main'); - expect(map.get('suborganism2')).equals('main'); + expect(map.get('main')).equals('main'); + expect(map.get('gene1')).equals('gene1'); + expect(map.get('gene2')).equals('gene2'); }); }); diff --git a/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx b/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx index e5f8df6998..3c8175960a 100644 --- a/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx +++ b/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx @@ -1,29 +1,38 @@ -import { type ReferenceGenomesLightweightSchema, SINGLE_REFERENCE } from '../../types/referencesGenomes.ts'; -import { - getMultiPathogenNucleotideSequenceNames, - getMultiPathogenSequenceName, -} from '../../utils/sequenceTypeHelpers.ts'; +import { type ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes.ts'; export function getSegmentAndGeneDisplayNameMap( - referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema, + referenceGenomesLightweightSchema: ReferenceGenomesLightweightSchema, ): Map { - if (SINGLE_REFERENCE in referenceGenomeLightweightSchema) { - return new Map(); - } + const mappingEntries: [string, string][] = []; + + // Iterate through all segments and references + for (const [segmentName, segmentData] of Object.entries(referenceGenomesLightweightSchema.segments)) { + // If only one reference, no prefix needed + if (segmentData.references.length === 1) { + // LAPIS name is just the segment name + mappingEntries.push([segmentName, segmentName]); - const segmentMappingEntries = Object.entries(referenceGenomeLightweightSchema).flatMap( - ([suborganism, suborganismSchema]) => - getMultiPathogenNucleotideSequenceNames(suborganismSchema.nucleotideSegmentNames, suborganism).map( - ({ lapisName, label }) => [lapisName, label] as const, - ), - ); + // Add genes for this segment/reference + const singleRef = segmentData.references[0]; + const genes = segmentData.genesByReference[singleRef] ?? []; + for (const geneName of genes) { + mappingEntries.push([geneName, geneName]); + } + } else { + // Multiple references: use {reference}-{segment} format + for (const referenceName of segmentData.references) { + const lapisSegmentName = `${referenceName}-${segmentName}`; + mappingEntries.push([lapisSegmentName, segmentName]); - const geneMappingEntries = Object.entries(referenceGenomeLightweightSchema).flatMap( - ([suborganism, suborganismSchema]) => - suborganismSchema.geneNames - .map((geneName) => getMultiPathogenSequenceName(geneName, suborganism)) - .map(({ lapisName, label }) => [lapisName, label] as const), - ); + // Add genes for this segment/reference + const genes = segmentData.genesByReference[referenceName] ?? []; + for (const geneName of genes) { + const lapisGeneName = `${referenceName}-${geneName}`; + mappingEntries.push([lapisGeneName, geneName]); + } + } + } + } - return new Map([...segmentMappingEntries, ...geneMappingEntries]); + return new Map(mappingEntries); } diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx index e3cbb2dcf0..61f527486e 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx @@ -9,11 +9,7 @@ import { approxMaxAcceptableUrlLength } from '../../../routes/routes.ts'; import { ACCESSION_VERSION_FIELD, IS_REVOCATION_FIELD, VERSION_STATUS_FIELD } from '../../../settings.ts'; import type { Metadata, Schema } from '../../../types/config.ts'; import { versionStatuses } from '../../../types/lapis'; -import { - type ReferenceGenomesLightweightSchema, - type ReferenceAccession, - SINGLE_REFERENCE, -} from '../../../types/referencesGenomes.ts'; +import { type ReferenceGenomesLightweightSchema, type ReferenceAccession } from '../../../types/referencesGenomes.ts'; import { MetadataFilterSchema } from '../../../utils/search.ts'; const defaultAccession: ReferenceAccession = { @@ -22,23 +18,28 @@ const defaultAccession: ReferenceAccession = { }; const defaultReferenceGenomesLightweightSchema: ReferenceGenomesLightweightSchema = { - [SINGLE_REFERENCE]: { - nucleotideSegmentNames: ['main'], - geneNames: ['gene1', 'gene2'], - insdcAccessionFull: [defaultAccession], + segments: { + main: { + references: ['ref1'], + insdcAccessions: { ref1: defaultAccession }, + genesByReference: { ref1: ['gene1', 'gene2'] }, + }, }, }; const multiPathogenReferenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema = { - suborganism1: { - nucleotideSegmentNames: ['main'], - geneNames: ['gene1', 'gene2'], - insdcAccessionFull: [defaultAccession], - }, - suborganism2: { - nucleotideSegmentNames: ['main'], - geneNames: ['gene1', 'gene2'], - insdcAccessionFull: [defaultAccession], + segments: { + main: { + references: ['suborganism1', 'suborganism2'], + insdcAccessions: { + suborganism1: defaultAccession, + suborganism2: defaultAccession, + }, + genesByReference: { + suborganism1: ['gene1', 'gene2'], + suborganism2: ['gene1', 'gene2'], + }, + }, }, }; @@ -109,7 +110,7 @@ async function renderDialog({ dataUseTermsEnabled={dataUseTermsEnabled} schema={schema} richFastaHeaderFields={richFastaHeaderFields} - selectedSuborganism={selectedSuborganism} + selectedReferenceName={selectedSuborganism} suborganismIdentifierField={suborganismIdentifierField} />, ); @@ -391,7 +392,7 @@ describe('DownloadDialog', () => { suborganismIdentifierField: 'genotype', }); - expect(screen.getByText('select a genotype', { exact: false })).toBeVisible(); + expect(screen.getByText('select a reference', { exact: false })).toBeVisible(); expect(screen.queryByLabelText(alignedNucleotideSequencesLabel)).not.toBeInTheDocument(); expect(screen.queryByLabelText(alignedAminoAcidSequencesLabel)).not.toBeInTheDocument(); }); @@ -465,14 +466,14 @@ describe('DownloadDialog', () => { expectRouteInPathMatches(path, `/sample/alignedAminoAcidSequences/suborganism1-gene2`); }); - const metadataWithOnlyForSuborganism: Metadata[] = [ + const metadataWithOnlyForReferenceName: Metadata[] = [ { name: 'field1', displayName: 'Field 1', type: 'string', header: 'Group 1', includeInDownloadsByDefault: true, - onlyForSuborganism: 'suborganism1', + onlyForReferenceName: 'suborganism1', }, { name: 'field2', @@ -480,7 +481,7 @@ describe('DownloadDialog', () => { type: 'string', header: 'Group 1', includeInDownloadsByDefault: true, - onlyForSuborganism: 'suborganism2', + onlyForReferenceName: 'suborganism2', }, { name: ACCESSION_VERSION_FIELD, @@ -489,12 +490,12 @@ describe('DownloadDialog', () => { }, ]; - test('should include "onlyForSuborganism" selected fields in download if no suborganism is selected', async () => { + test('should include "onlyForReferenceName" selected fields in download if no suborganism is selected', async () => { await renderDialog({ referenceGenomesLightweightSchema: multiPathogenReferenceGenomeLightweightSchema, selectedSuborganism: null, suborganismIdentifierField: 'genotype', - metadata: metadataWithOnlyForSuborganism, + metadata: metadataWithOnlyForReferenceName, }); await checkAgreement(); @@ -510,7 +511,7 @@ describe('DownloadDialog', () => { referenceGenomesLightweightSchema: multiPathogenReferenceGenomeLightweightSchema, selectedSuborganism: 'suborganism2', suborganismIdentifierField: 'genotype', - metadata: metadataWithOnlyForSuborganism, + metadata: metadataWithOnlyForReferenceName, }); await checkAgreement(); diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx index fb10cf6e14..470d0d295b 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx @@ -24,7 +24,7 @@ type DownloadDialogProps = { dataUseTermsEnabled: boolean; schema: Schema; richFastaHeaderFields: Schema['richFastaHeaderFields']; - selectedSuborganism: string | null; + selectedReferenceName: string | null; suborganismIdentifierField: string | undefined; }; @@ -36,7 +36,7 @@ export const DownloadDialog: FC = ({ dataUseTermsEnabled, schema, richFastaHeaderFields, - selectedSuborganism, + selectedReferenceName, suborganismIdentifierField, }) => { const [isOpen, setIsOpen] = useState(false); @@ -45,8 +45,8 @@ export const DownloadDialog: FC = ({ const closeDialog = () => setIsOpen(false); const { nucleotideSequences, genes, useMultiSegmentEndpoint, defaultFastaHeaderTemplate } = useMemo( - () => getSequenceNames(referenceGenomesLightweightSchema, selectedSuborganism), - [referenceGenomesLightweightSchema, selectedSuborganism], + () => getSequenceNames(referenceGenomesLightweightSchema, selectedReferenceName), + [referenceGenomesLightweightSchema, selectedReferenceName], ); const [downloadFormState, setDownloadFormState] = useState( @@ -63,7 +63,7 @@ export const DownloadDialog: FC = ({ return new Map( schema.metadata.map((field) => [ field.name, - new MetadataVisibility(selectedFields.has(field.name), field.onlyForSuborganism), + new MetadataVisibility(selectedFields.has(field.name), field.onlyForReferenceName), ]), ); }, [selectedFields, schema]); @@ -76,7 +76,7 @@ export const DownloadDialog: FC = ({ defaultFastaHeaderTemplate, getVisibleFields: () => [ ...Array.from(downloadFieldVisibilities.entries()) - .filter(([_, visibility]) => visibility.isVisible(selectedSuborganism)) + .filter(([_, visibility]) => visibility.isVisible(selectedReferenceName)) .map(([name]) => name), ], metadata: schema.metadata, @@ -103,7 +103,7 @@ export const DownloadDialog: FC = ({ downloadFieldVisibilities={downloadFieldVisibilities} onSelectedFieldsChange={setSelectedFields} richFastaHeaderFields={richFastaHeaderFields} - selectedSuborganism={selectedSuborganism} + selectedReferenceName={selectedReferenceName} suborganismIdentifierField={suborganismIdentifierField} /> {dataUseTermsEnabled && ( diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx b/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx index 3772eac358..e691333a25 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx @@ -8,7 +8,7 @@ import { DropdownOptionBlock, type OptionBlockOption, RadioOptionBlock } from '. import { routes } from '../../../routes/routes.ts'; import { ACCESSION_VERSION_FIELD } from '../../../settings.ts'; import type { Schema } from '../../../types/config.ts'; -import { type ReferenceGenomesLightweightSchema, SINGLE_REFERENCE } from '../../../types/referencesGenomes.ts'; +import type { ReferenceGenomesLightweightSchema } from '../../../types/referencesGenomes.ts'; import type { MetadataVisibility } from '../../../utils/search.ts'; import { type GeneInfo, @@ -18,8 +18,7 @@ import { isMultiSegmented, type SegmentInfo, } from '../../../utils/sequenceTypeHelpers.ts'; -import { formatLabel } from '../SuborganismSelector.tsx'; -import { stillRequiresSuborganismSelection } from '../stillRequiresSuborganismSelection.tsx'; +import { stillRequiresReferenceNameSelection } from '../stillRequiresReferenceNameSelection.tsx'; export type DownloadFormState = { includeRestricted: boolean; @@ -41,7 +40,7 @@ type DownloadFormProps = { downloadFieldVisibilities: Map; onSelectedFieldsChange: Dispatch>>; richFastaHeaderFields: Schema['richFastaHeaderFields']; - selectedSuborganism: string | null; + selectedReferenceName: string | null; suborganismIdentifierField: string | undefined; }; @@ -55,18 +54,18 @@ export const DownloadForm: FC = ({ downloadFieldVisibilities, onSelectedFieldsChange, richFastaHeaderFields, - selectedSuborganism, + selectedReferenceName, suborganismIdentifierField, }) => { const [isFieldSelectorOpen, setIsFieldSelectorOpen] = useState(false); const { nucleotideSequences, genes } = useMemo( - () => getSequenceNames(referenceGenomesLightweightSchema, selectedSuborganism), - [referenceGenomesLightweightSchema, selectedSuborganism], + () => getSequenceNames(referenceGenomesLightweightSchema, selectedReferenceName), + [referenceGenomesLightweightSchema, selectedReferenceName], ); - const disableAlignedSequences = stillRequiresSuborganismSelection( + const disableAlignedSequences = stillRequiresReferenceNameSelection( referenceGenomesLightweightSchema, - selectedSuborganism, + selectedReferenceName, ); function getDataTypeOptions(): OptionBlockOption[] { @@ -78,7 +77,7 @@ export const DownloadForm: FC = ({ onClick={() => setIsFieldSelectorOpen(true)} selectedFieldsCount={ Array.from(downloadFieldVisibilities.values()).filter((it) => - it.isVisible(selectedSuborganism), + it.isVisible(selectedReferenceName), ).length } disabled={downloadFormState.dataType !== 'metadata'} @@ -234,8 +233,7 @@ export const DownloadForm: FC = ({ /> {disableAlignedSequences && suborganismIdentifierField !== undefined && (
- Or select a {formatLabel(suborganismIdentifierField)} with the search UI to enable download of - aligned sequences. + Or select a reference with the search UI to enable download of aligned sequences.
)} @@ -259,7 +257,7 @@ export const DownloadForm: FC = ({ schema={schema} downloadFieldVisibilities={downloadFieldVisibilities} onSelectedFieldsChange={onSelectedFieldsChange} - selectedSuborganism={selectedSuborganism} + selectedReferenceName={selectedReferenceName} /> ); @@ -267,35 +265,60 @@ export const DownloadForm: FC = ({ export function getSequenceNames( referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema, - selectedSuborganism: string | null, + selectedReferenceName: string | null, ): { nucleotideSequences: SegmentInfo[]; genes: GeneInfo[]; useMultiSegmentEndpoint: boolean; defaultFastaHeaderTemplate?: string; } { - if (SINGLE_REFERENCE in referenceGenomeLightweightSchema) { - const { nucleotideSegmentNames, geneNames } = referenceGenomeLightweightSchema[SINGLE_REFERENCE]; + const segments = Object.keys(referenceGenomeLightweightSchema.segments); + + // Check if single reference mode + const firstSegment = segments[0]; + const firstSegmentRefs = firstSegment ? referenceGenomeLightweightSchema.segments[firstSegment].references : []; + const isSingleReference = firstSegmentRefs.length === 1; + + if (isSingleReference && firstSegmentRefs.length > 0) { + const referenceName = firstSegmentRefs[0]; + const segmentNames = segments; + const allGenes: string[] = []; + + for (const segmentName of segments) { + const segmentData = referenceGenomeLightweightSchema.segments[segmentName]; + const genes = segmentData.genesByReference[referenceName] ?? []; + allGenes.push(...genes); + } + return { - nucleotideSequences: nucleotideSegmentNames.map(getSinglePathogenSequenceName), - genes: geneNames.map(getSinglePathogenSequenceName), - useMultiSegmentEndpoint: isMultiSegmented(nucleotideSegmentNames), + nucleotideSequences: segmentNames.map(getSinglePathogenSequenceName), + genes: allGenes.map(getSinglePathogenSequenceName), + useMultiSegmentEndpoint: isMultiSegmented(segmentNames), }; } - if (selectedSuborganism === null) { + if (selectedReferenceName === null) { return { nucleotideSequences: [], genes: [], - useMultiSegmentEndpoint: false, // When no suborganism is selected, use the "all segments" endpoint to download all available segments, even though LAPIS is multisegmented. That endpoint is available at the same route as the single segmented endpoint. - defaultFastaHeaderTemplate: `{${ACCESSION_VERSION_FIELD}}`, // make sure that the segment does not appear in the fasta header + useMultiSegmentEndpoint: false, + defaultFastaHeaderTemplate: `{${ACCESSION_VERSION_FIELD}}`, }; } - const { nucleotideSegmentNames, geneNames } = referenceGenomeLightweightSchema[selectedSuborganism]; + // Multi-reference mode + const segmentNames = segments; + const allGenes: string[] = []; + + for (const segmentName of segments) { + const segmentData = referenceGenomeLightweightSchema.segments[segmentName]; + const genes = segmentData.genesByReference[selectedReferenceName] ?? []; + allGenes.push(...genes); + } + return { - nucleotideSequences: getMultiPathogenNucleotideSequenceNames(nucleotideSegmentNames, selectedSuborganism), - genes: geneNames.map((name) => getMultiPathogenSequenceName(name, selectedSuborganism)), + nucleotideSequences: getMultiPathogenNucleotideSequenceNames(segmentNames, selectedReferenceName), + genes: allGenes.map((name: string) => getMultiPathogenSequenceName(name, selectedReferenceName)), useMultiSegmentEndpoint: true, }; } diff --git a/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.spec.tsx b/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.spec.tsx index 4a0d0cb682..bc010bc04a 100644 --- a/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.spec.tsx +++ b/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.spec.tsx @@ -174,7 +174,7 @@ describe('FieldSelectorModal', () => { type: 'string', header: 'Group 1', includeInDownloadsByDefault: true, - onlyForSuborganism: 'suborganism1', + onlyForReferenceName: 'suborganism1', }, { name: 'field3', @@ -182,7 +182,7 @@ describe('FieldSelectorModal', () => { type: 'string', header: 'Group 2', includeInDownloadsByDefault: true, - onlyForSuborganism: 'suborganism2', + onlyForReferenceName: 'suborganism2', }, accessionVersionField, ]); @@ -217,12 +217,12 @@ describe('FieldSelectorModal', () => { new Map( metadata.map((field) => [ field.name, - new MetadataVisibility(result.current[0].has(field.name), field.onlyForSuborganism), + new MetadataVisibility(result.current[0].has(field.name), field.onlyForReferenceName), ]), ) } onSelectedFieldsChange={result.current[1]} - selectedSuborganism={selectedSuborganism} + selectedReferenceName={selectedSuborganism} /> ); diff --git a/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.tsx b/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.tsx index f9bcd6d567..479bba4223 100644 --- a/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.tsx +++ b/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.tsx @@ -9,7 +9,7 @@ import { fieldItemDisplayStateType, FieldSelectorModal as CommonFieldSelectorModal, } from '../../../common/FieldSelectorModal.tsx'; -import { isActiveForSelectedSuborganism } from '../../isActiveForSelectedSuborganism.tsx'; +import { isActiveForSelectedReferenceName } from '../../isActiveForSelectedReferenceName.tsx'; type FieldSelectorProps = { isOpen: boolean; @@ -17,7 +17,7 @@ type FieldSelectorProps = { schema: Schema; downloadFieldVisibilities: Map; onSelectedFieldsChange: Dispatch>>; - selectedSuborganism: string | null; + selectedReferenceName: string | null; }; export const FieldSelectorModal: FC = ({ @@ -26,7 +26,7 @@ export const FieldSelectorModal: FC = ({ schema, downloadFieldVisibilities, onSelectedFieldsChange, - selectedSuborganism, + selectedReferenceName, }) => { const handleFieldSelection = (fieldName: string, selected: boolean) => { onSelectedFieldsChange((prevSelectedFields) => { @@ -46,7 +46,7 @@ export const FieldSelectorModal: FC = ({ name: field.name, displayName: field.displayName, header: field.header, - displayState: getDisplayState(field, selectedSuborganism, schema), + displayState: getDisplayState(field, selectedReferenceName, schema), isChecked: downloadFieldVisibilities.get(field.name)?.isChecked ?? false, })); @@ -63,17 +63,17 @@ export const FieldSelectorModal: FC = ({ function getDisplayState( field: Metadata, - selectedSuborganism: string | null, + selectedReferenceName: string | null, schema: Schema, ): FieldItemDisplayState | undefined { if (field.name === ACCESSION_VERSION_FIELD) { return { type: fieldItemDisplayStateType.alwaysChecked }; } - if (!isActiveForSelectedSuborganism(selectedSuborganism, field)) { + if (!isActiveForSelectedReferenceName(selectedReferenceName, field)) { return { type: fieldItemDisplayStateType.disabled, - tooltip: `This is only available when the ${schema.suborganismIdentifierField} ${field.onlyForSuborganism} is selected.`, + tooltip: `This is only available when the ${schema.suborganismIdentifierField} ${field.onlyForReferenceName} is selected.`, }; } diff --git a/website/src/components/SearchPage/DownloadDialog/SequenceFilters.tsx b/website/src/components/SearchPage/DownloadDialog/SequenceFilters.tsx index a5a3248ea0..74bb010453 100644 --- a/website/src/components/SearchPage/DownloadDialog/SequenceFilters.tsx +++ b/website/src/components/SearchPage/DownloadDialog/SequenceFilters.tsx @@ -1,5 +1,5 @@ import { type FieldValues } from '../../../types/config.ts'; -import type { SuborganismSegmentAndGeneInfo } from '../../../utils/getSuborganismSegmentAndGeneInfo.tsx'; +import type { SegmentAndGeneInfo } from '../../../utils/getSegmentAndGeneInfo.tsx'; import { intoMutationSearchParams } from '../../../utils/mutation.ts'; import { MetadataFilterSchema } from '../../../utils/search.ts'; @@ -44,7 +44,7 @@ export class FieldFilterSet implements SequenceFilter { private readonly filterSchema: MetadataFilterSchema; private readonly fieldValues: FieldValues; private readonly hiddenFieldValues: FieldValues; - private readonly suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo | null; + private readonly suborganismSegmentAndGeneInfo: SegmentAndGeneInfo | null; /** * @param filterSchema The {@link MetadataFilterSchema} to use. Provides labels and other @@ -58,7 +58,7 @@ export class FieldFilterSet implements SequenceFilter { filterSchema: MetadataFilterSchema, fieldValues: FieldValues, hiddenFieldValues: FieldValues, - suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo | null, + suborganismSegmentAndGeneInfo: SegmentAndGeneInfo | null, ) { this.filterSchema = filterSchema; this.fieldValues = fieldValues; diff --git a/website/src/components/SearchPage/ReferenceNameSelector.tsx b/website/src/components/SearchPage/ReferenceNameSelector.tsx new file mode 100644 index 0000000000..361297ea98 --- /dev/null +++ b/website/src/components/SearchPage/ReferenceNameSelector.tsx @@ -0,0 +1,2 @@ +// Re-export for backward compatibility +export { SuborganismSelector as ReferenceNameSelector } from './SuborganismSelector.tsx'; diff --git a/website/src/components/SearchPage/SearchForm.spec.tsx b/website/src/components/SearchPage/SearchForm.spec.tsx index 2a5f944934..b1519ba1c9 100644 --- a/website/src/components/SearchPage/SearchForm.spec.tsx +++ b/website/src/components/SearchPage/SearchForm.spec.tsx @@ -1,16 +1,12 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, screen, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { SearchForm } from './SearchForm'; import { testConfig, testOrganism } from '../../../vitest.setup.ts'; import type { MetadataFilter } from '../../types/config.ts'; -import { - type ReferenceGenomesLightweightSchema, - type ReferenceAccession, - SINGLE_REFERENCE, -} from '../../types/referencesGenomes.ts'; +import { type ReferenceGenomesLightweightSchema, type ReferenceAccession } from '../../types/referencesGenomes.ts'; import { MetadataFilterSchema, MetadataVisibility } from '../../utils/search.ts'; global.ResizeObserver = class FakeResizeObserver implements ResizeObserver { @@ -44,23 +40,28 @@ const defaultAccession: ReferenceAccession = { }; const defaultReferenceGenomesLightweightSchema: ReferenceGenomesLightweightSchema = { - [SINGLE_REFERENCE]: { - nucleotideSegmentNames: ['main'], - geneNames: ['gene1', 'gene2'], - insdcAccessionFull: [defaultAccession], + segments: { + main: { + references: ['ref1'], + insdcAccessions: { ref1: defaultAccession }, + genesByReference: { ref1: ['gene1', 'gene2'] }, + }, }, }; const multiPathogenReferenceGenomesLightweightSchema: ReferenceGenomesLightweightSchema = { - suborganism1: { - nucleotideSegmentNames: ['main'], - geneNames: ['gene1', 'gene2'], - insdcAccessionFull: [defaultAccession], - }, - suborganism2: { - nucleotideSegmentNames: ['main'], - geneNames: ['gene1', 'gene2'], - insdcAccessionFull: [defaultAccession], + segments: { + main: { + references: ['suborganism1', 'suborganism2'], + insdcAccessions: { + suborganism1: defaultAccession, + suborganism2: defaultAccession, + }, + genesByReference: { + suborganism1: ['gene1', 'gene2'], + suborganism2: ['gene1', 'gene2'], + }, + }, }, }; @@ -71,7 +72,6 @@ const defaultSearchVisibilities = new Map([ const setSomeFieldValues = vi.fn(); const setASearchVisibility = vi.fn(); -const setSelectedSuborganism = vi.fn(); const renderSearchForm = ({ filterSchema = new MetadataFilterSchema([...defaultSearchFormFilters]), @@ -104,7 +104,8 @@ const renderSearchForm = ({ showMutationSearch: true, suborganismIdentifierField, selectedSuborganism, - setSelectedSuborganism, + setSelectedSuborganism: vi.fn(), + selectedReferences: {}, }; render( @@ -115,6 +116,10 @@ const renderSearchForm = ({ }; describe('SearchForm', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it('renders without crashing', () => { renderSearchForm(); expect(screen.getByText('Field 1')).toBeInTheDocument(); @@ -140,16 +145,32 @@ describe('SearchForm', () => { }); it('should render the suborganism selector in the multi pathogen case', async () => { - renderSearchForm({ - filterSchema: new MetadataFilterSchema([ - ...defaultSearchFormFilters, - { name: 'My genotype', type: 'string' }, - ]), - suborganismIdentifierField: 'My genotype', - referenceGenomeLightweightSchema: multiPathogenReferenceGenomesLightweightSchema, - }); - - const suborganismSelector = screen.getByRole('combobox', { name: 'My genotype' }); + const setSelectedSuborganism = vi.fn(); + render( + + + , + ); + + const suborganismSelector = await screen.findByRole('combobox', { name: 'My genotype' }); expect(suborganismSelector).toBeInTheDocument(); await userEvent.selectOptions(suborganismSelector, 'suborganism1'); @@ -179,14 +200,14 @@ describe('SearchForm', () => { type: 'string', displayName: 'Field 1', initiallyVisible: true, - onlyForSuborganism: 'suborganism1', + onlyForReferenceName: 'suborganism1', }, { name: 'field2', type: 'string', displayName: 'Field 2', initiallyVisible: true, - onlyForSuborganism: 'suborganism2', + onlyForReferenceName: 'suborganism2', }, ]); const searchVisibilities = new Map([ diff --git a/website/src/components/SearchPage/SearchForm.tsx b/website/src/components/SearchPage/SearchForm.tsx index 8932c1a7bd..796d9ffcc8 100644 --- a/website/src/components/SearchPage/SearchForm.tsx +++ b/website/src/components/SearchPage/SearchForm.tsx @@ -21,7 +21,7 @@ import type { FieldValues, GroupedMetadataFilter, MetadataFilter, SetSomeFieldVa import { type ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes.ts'; import type { ClientConfig } from '../../types/runtimeConfig.ts'; import { extractArrayValue, validateSingleValue } from '../../utils/extractFieldValue.ts'; -import { getSuborganismSegmentAndGeneInfo } from '../../utils/getSuborganismSegmentAndGeneInfo.tsx'; +import { getSegmentAndGeneInfo } from '../../utils/getSegmentAndGeneInfo.tsx'; import { type MetadataFilterSchema, MetadataVisibility, MUTATION_KEY } from '../../utils/search.ts'; import { BaseDialog } from '../common/BaseDialog.tsx'; import { type FieldItem, FieldSelectorModal } from '../common/FieldSelectorModal.tsx'; @@ -47,6 +47,7 @@ interface SearchFormProps { suborganismIdentifierField: string | undefined; selectedSuborganism: string | null; setSelectedSuborganism: (newValue: string | null) => void; + selectedReferences: Record; } export const SearchForm = ({ @@ -62,6 +63,7 @@ export const SearchForm = ({ suborganismIdentifierField, selectedSuborganism, setSelectedSuborganism, + selectedReferences, }: SearchFormProps) => { const visibleFields = filterSchema.filters.filter( (field) => searchVisibilities.get(field.name)?.isVisible(selectedSuborganism) ?? false, @@ -107,8 +109,8 @@ export const SearchForm = ({ })); const suborganismSegmentAndGeneInfo = useMemo( - () => getSuborganismSegmentAndGeneInfo(referenceGenomeLightweightSchema, selectedSuborganism), - [referenceGenomeLightweightSchema, selectedSuborganism], + () => getSegmentAndGeneInfo(referenceGenomeLightweightSchema, selectedReferences), + [referenceGenomeLightweightSchema, selectedReferences], ); return ( @@ -185,7 +187,7 @@ export const SearchForm = ({ /> - {showMutationSearch && suborganismSegmentAndGeneInfo !== null && ( + {showMutationSearch && ( { name: 'field1', type: 'string', displayName: 'Field 1', - onlyForSuborganism: 'suborganism1', + onlyForReference: 'suborganism1', initiallyVisible: true, }, { @@ -391,15 +389,18 @@ describe('SearchFullUI', () => { }, ], referenceGenomeLightweightSchema: { - suborganism1: { - nucleotideSegmentNames: ['main'], - geneNames: ['gene1'], - insdcAccessionFull: [defaultAccession], - }, - suborganism2: { - nucleotideSegmentNames: ['main'], - geneNames: ['gene1'], - insdcAccessionFull: [defaultAccession], + segments: { + main: { + references: ['suborganism1', 'suborganism2'], + insdcAccessions: { + suborganism1: defaultAccession, + suborganism2: defaultAccession, + }, + genesByReference: { + suborganism1: ['gene1'], + suborganism2: ['gene1'], + }, + }, }, }, }); diff --git a/website/src/components/SearchPage/SearchFullUI.tsx b/website/src/components/SearchPage/SearchFullUI.tsx index cffbcaadeb..f4ac62a112 100644 --- a/website/src/components/SearchPage/SearchFullUI.tsx +++ b/website/src/components/SearchPage/SearchFullUI.tsx @@ -12,7 +12,7 @@ import { SearchPagination } from './SearchPagination'; import { SeqPreviewModal } from './SeqPreviewModal'; import { Table, type TableSequenceData } from './Table'; import { TableColumnSelectorModal } from './TableColumnSelectorModal.tsx'; -import { stillRequiresSuborganismSelection } from './stillRequiresSuborganismSelection.tsx'; +import { stillRequiresReferenceNameSelection } from './stillRequiresReferenceNameSelection.tsx'; import { useSearchPageState } from './useSearchPageState.ts'; import { type QueryState } from './useStateSyncedWithUrlQueryParams.ts'; import { getLapisUrl } from '../../config.ts'; @@ -25,7 +25,7 @@ import { type OrderBy } from '../../types/lapis.ts'; import type { ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes.ts'; import type { ClientConfig } from '../../types/runtimeConfig.ts'; import { formatNumberWithDefaultLocale } from '../../utils/formatNumber.tsx'; -import { getSuborganismSegmentAndGeneInfo } from '../../utils/getSuborganismSegmentAndGeneInfo.tsx'; +import { getSegmentAndGeneInfo } from '../../utils/getSegmentAndGeneInfo.tsx'; import { getColumnVisibilitiesFromQuery, getFieldVisibilitiesFromQuery, @@ -93,6 +93,7 @@ export const InnerSearchFullUI = ({ setPreviewHalfScreen, selectedSuborganism, setSelectedSuborganism, + selectedReferences, page, setPage, setSomeFieldValues, @@ -156,9 +157,9 @@ export const InnerSearchFullUI = ({ filterSchema, fieldValues, hiddenFieldValues, - getSuborganismSegmentAndGeneInfo(referenceGenomeLightweightSchema, selectedSuborganism), + getSegmentAndGeneInfo(referenceGenomeLightweightSchema, selectedReferences), ), - [fieldValues, hiddenFieldValues, referenceGenomeLightweightSchema, selectedSuborganism, filterSchema], + [fieldValues, hiddenFieldValues, referenceGenomeLightweightSchema, selectedReferences, filterSchema], ); /** @@ -214,7 +215,7 @@ export const InnerSearchFullUI = ({ const showMutationSearch = schema.submissionDataTypes.consensusSequences && - !stillRequiresSuborganismSelection(referenceGenomeLightweightSchema, selectedSuborganism); + !stillRequiresReferenceNameSelection(referenceGenomeLightweightSchema, selectedSuborganism); return (
@@ -224,7 +225,7 @@ export const InnerSearchFullUI = ({ schema={schema} columnVisibilities={columnVisibilities} setAColumnVisibility={setAColumnVisibility} - selectedSuborganism={selectedSuborganism} + selectedReferenceName={selectedSuborganism} />
{linkOuts !== undefined && linkOuts.length > 0 && ( diff --git a/website/src/components/SearchPage/SegmentReferenceSelector.tsx b/website/src/components/SearchPage/SegmentReferenceSelector.tsx new file mode 100644 index 0000000000..e7e3250e8e --- /dev/null +++ b/website/src/components/SearchPage/SegmentReferenceSelector.tsx @@ -0,0 +1,155 @@ +import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'; +import { type FC, useId } from 'react'; + +import type { ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes.ts'; +import type { SegmentReferenceSelections } from '../../utils/sequenceTypeHelpers.ts'; +import DisabledUntilHydrated from '../DisabledUntilHydrated.tsx'; +import { Button } from '../common/Button'; +import MaterialSymbolsClose from '~icons/material-symbols/close'; + +type SegmentReferenceSelectorProps = { + schema: ReferenceGenomesLightweightSchema; + selectedReferences: SegmentReferenceSelections; + setReferenceForSegment: (segment: string, reference: string | null) => void; +}; + +/** + * Segment-first mode selector: allows selecting a reference per segment using a tabbed interface. + * Each tab represents a segment, and within each tab users can select which reference to use. + */ +export const SegmentReferenceSelector: FC = ({ + schema, + selectedReferences, + setReferenceForSegment, +}) => { + const segments = Object.keys(schema.segments); + const isSingleSegment = segments.length === 1; + + // For single segment, show simplified UI without tabs + if (isSingleSegment) { + const segmentName = segments[0]; + const segmentData = schema.segments[segmentName]; + return ( +
+ setReferenceForSegment(segmentName, ref)} + /> +

+ Select a reference to enable mutation search and download of aligned sequences +

+
+ ); + } + + // Multi-segment: show tabs + return ( +
+ + + + {segments.map((segmentName) => { + const hasSelection = selectedReferences[segmentName] !== null; + return ( + + `px-3 py-2 text-sm font-medium rounded-t-md border-b-2 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-200 ${ + selected + ? 'border-primary-500 text-primary-700 bg-white' + : 'border-transparent text-gray-600 hover:text-gray-800 hover:bg-gray-100' + }` + } + > + + {segmentName} + {hasSelection && ( + + )} + + + ); + })} + + + {segments.map((segmentName) => { + const segmentData = schema.segments[segmentName]; + return ( + + setReferenceForSegment(segmentName, ref)} + /> + + ); + })} + + + +

+ Select references for each segment to enable mutation search and download of aligned sequences +

+
+ ); +}; + +type SegmentReferenceDropdownProps = { + segmentName: string; + availableReferences: string[]; + selectedReference: string | null; + onChange: (reference: string | null) => void; +}; + +/** + * Reference dropdown for a single segment. + */ +const SegmentReferenceDropdown: FC = ({ + segmentName, + availableReferences, + selectedReference, + onChange, +}) => { + const selectId = useId(); + + return ( +
+ +
+ + {selectedReference !== null && ( + + )} +
+
+ ); +}; diff --git a/website/src/components/SearchPage/SuborganismSelector.spec.tsx b/website/src/components/SearchPage/SuborganismSelector.spec.tsx index 593d8680f1..42558675b7 100644 --- a/website/src/components/SearchPage/SuborganismSelector.spec.tsx +++ b/website/src/components/SearchPage/SuborganismSelector.spec.tsx @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'; import { describe, expect, it, vi } from 'vitest'; import { SuborganismSelector } from './SuborganismSelector'; -import { type ReferenceGenomesLightweightSchema, SINGLE_REFERENCE } from '../../types/referencesGenomes'; +import { type ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes'; import { MetadataFilterSchema } from '../../utils/search.ts'; const suborganismIdentifierField = 'genotype'; @@ -16,15 +16,24 @@ const filterSchema = new MetadataFilterSchema([ }, ]); -const dummySequences = { - nucleotideSegmentNames: [], - geneNames: [], - insdcAccessionFull: [], +const mockReferenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema = { + segments: { + main: { + references: ['suborganism1', 'suborganism2'], + insdcAccessions: {}, + genesByReference: {}, + }, + }, }; -const mockReferenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema = { - suborganism1: dummySequences, - suborganism2: dummySequences, +const singleReferenceSchema: ReferenceGenomesLightweightSchema = { + segments: { + main: { + references: ['single'], + insdcAccessions: {}, + genesByReference: {}, + }, + }, }; describe('SuborganismSelector', () => { @@ -32,7 +41,7 @@ describe('SuborganismSelector', () => { const { container } = render( = ({ setSelectedSuborganism, }) => { const selectId = useId(); - const suborganismNames = Object.keys(referenceGenomeLightweightSchema); + + // Extract reference names from the segments + const segments = Object.values(referenceGenomeLightweightSchema.segments); + const suborganismNames = segments.length > 0 ? segments[0].references : []; const isSinglePathogen = suborganismNames.length < 2; const label = useMemo(() => { diff --git a/website/src/components/SearchPage/TableColumnSelectorModal.tsx b/website/src/components/SearchPage/TableColumnSelectorModal.tsx index d888611096..a68724a82e 100644 --- a/website/src/components/SearchPage/TableColumnSelectorModal.tsx +++ b/website/src/components/SearchPage/TableColumnSelectorModal.tsx @@ -1,6 +1,6 @@ import { type FC, useMemo } from 'react'; -import { isActiveForSelectedSuborganism } from './isActiveForSelectedSuborganism.tsx'; +import { isActiveForSelectedReferenceName } from './isActiveForSelectedReferenceName.tsx'; import { ACCESSION_VERSION_FIELD } from '../../settings.ts'; import type { Metadata, Schema } from '../../types/config.ts'; import { type MetadataVisibility } from '../../utils/search.ts'; @@ -17,7 +17,7 @@ export type TableColumnSelectorModalProps = { schema: Schema; columnVisibilities: Map; setAColumnVisibility: (fieldName: string, selected: boolean) => void; - selectedSuborganism: string | null; + selectedReferenceName: string | null; }; export const TableColumnSelectorModal: FC = ({ @@ -26,7 +26,7 @@ export const TableColumnSelectorModal: FC = ({ schema, columnVisibilities, setAColumnVisibility, - selectedSuborganism, + selectedReferenceName, }) => { const columnFieldItems: FieldItem[] = useMemo( () => @@ -36,10 +36,10 @@ export const TableColumnSelectorModal: FC = ({ name: field.name, displayName: field.displayName ?? field.name, header: field.header, - displayState: getDisplayState(field, selectedSuborganism, schema.suborganismIdentifierField), + displayState: getDisplayState(field, selectedReferenceName, schema.suborganismIdentifierField), isChecked: columnVisibilities.get(field.name)?.isChecked ?? false, })), - [schema.metadata, schema.suborganismIdentifierField, columnVisibilities, selectedSuborganism], + [schema.metadata, schema.suborganismIdentifierField, columnVisibilities, selectedReferenceName], ); return ( @@ -55,17 +55,17 @@ export const TableColumnSelectorModal: FC = ({ export function getDisplayState( field: Metadata, - selectedSuborganism: string | null, + selectedReferenceName: string | null, suborganismIdentifierField: string | undefined, ): FieldItemDisplayState | undefined { if (field.name === ACCESSION_VERSION_FIELD) { return { type: fieldItemDisplayStateType.alwaysChecked }; } - if (!isActiveForSelectedSuborganism(selectedSuborganism, field)) { + if (!isActiveForSelectedReferenceName(selectedReferenceName, field)) { return { type: fieldItemDisplayStateType.greyedOut, - tooltip: `This is only visible when the ${suborganismIdentifierField ?? 'suborganismIdentifierField'} ${field.onlyForSuborganism} is selected.`, + tooltip: `This is only visible when the ${suborganismIdentifierField ?? 'suborganismIdentifierField'} ${field.onlyForReferenceName} is selected.`, }; } diff --git a/website/src/components/SearchPage/fields/MutationField.spec.tsx b/website/src/components/SearchPage/fields/MutationField.spec.tsx index 7686a82523..f701278edf 100644 --- a/website/src/components/SearchPage/fields/MutationField.spec.tsx +++ b/website/src/components/SearchPage/fields/MutationField.spec.tsx @@ -3,9 +3,9 @@ import userEvent from '@testing-library/user-event'; import { describe, expect, test, vi } from 'vitest'; import { MutationField } from './MutationField.tsx'; -import type { SuborganismSegmentAndGeneInfo } from '../../../utils/getSuborganismSegmentAndGeneInfo.tsx'; +import type { SegmentAndGeneInfo } from '../../../utils/getSegmentAndGeneInfo.tsx'; -const singleReferenceSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo = { +const singleReferenceSegmentAndGeneInfo: SegmentAndGeneInfo = { nucleotideSegmentInfos: [{ lapisName: 'main', label: 'main' }], geneInfos: [ { lapisName: 'gene1', label: 'gene1' }, @@ -14,7 +14,7 @@ const singleReferenceSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo = { isMultiSegmented: false, }; -const multiReferenceGenomeLightweightSchema: SuborganismSegmentAndGeneInfo = { +const multiReferenceGenomeLightweightSchema: SegmentAndGeneInfo = { nucleotideSegmentInfos: [ { lapisName: 'seg1', label: 'seg1' }, { lapisName: 'seg2', label: 'seg2' }, @@ -29,7 +29,7 @@ const multiReferenceGenomeLightweightSchema: SuborganismSegmentAndGeneInfo = { function renderField( value: string, onChange: (mutationFilter: string) => void, - suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo, + suborganismSegmentAndGeneInfo: SegmentAndGeneInfo, ) { render( void; } diff --git a/website/src/components/SearchPage/isActiveForSelectedReferenceName.tsx b/website/src/components/SearchPage/isActiveForSelectedReferenceName.tsx new file mode 100644 index 0000000000..258386508e --- /dev/null +++ b/website/src/components/SearchPage/isActiveForSelectedReferenceName.tsx @@ -0,0 +1,17 @@ +import type { Metadata } from '../../types/config.ts'; + +export function isActiveForSelectedReferenceName(selectedReferenceName: string | null, field: Metadata) { + // Check legacy onlyForReferenceName field + const matchesReferenceName = + selectedReferenceName === null || + field.onlyForReferenceName === undefined || + field.onlyForReferenceName === selectedReferenceName; + + // Check new onlyForReference field (backward compatible) + const matchesReference = + selectedReferenceName === null || + field.onlyForReference === undefined || + field.onlyForReference === selectedReferenceName; + + return matchesReferenceName && matchesReference; +} diff --git a/website/src/components/SearchPage/isActiveForSelectedSuborganism.tsx b/website/src/components/SearchPage/isActiveForSelectedSuborganism.tsx index 7ab6ece0e6..258386508e 100644 --- a/website/src/components/SearchPage/isActiveForSelectedSuborganism.tsx +++ b/website/src/components/SearchPage/isActiveForSelectedSuborganism.tsx @@ -1,9 +1,17 @@ import type { Metadata } from '../../types/config.ts'; -export function isActiveForSelectedSuborganism(selectedSuborganism: string | null, field: Metadata) { - return ( - selectedSuborganism === null || - field.onlyForSuborganism === undefined || - field.onlyForSuborganism === selectedSuborganism - ); +export function isActiveForSelectedReferenceName(selectedReferenceName: string | null, field: Metadata) { + // Check legacy onlyForReferenceName field + const matchesReferenceName = + selectedReferenceName === null || + field.onlyForReferenceName === undefined || + field.onlyForReferenceName === selectedReferenceName; + + // Check new onlyForReference field (backward compatible) + const matchesReference = + selectedReferenceName === null || + field.onlyForReference === undefined || + field.onlyForReference === selectedReferenceName; + + return matchesReferenceName && matchesReference; } diff --git a/website/src/components/SearchPage/stillRequiresReferenceNameSelection.tsx b/website/src/components/SearchPage/stillRequiresReferenceNameSelection.tsx new file mode 100644 index 0000000000..1c1efa04d6 --- /dev/null +++ b/website/src/components/SearchPage/stillRequiresReferenceNameSelection.tsx @@ -0,0 +1,12 @@ +import type { ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes.ts'; + +export function stillRequiresReferenceNameSelection( + referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema, + selectedReferenceName: string | null, +) { + // Check if there are multiple references in any segment + const hasMultipleReferences = Object.values(referenceGenomeLightweightSchema.segments).some( + (segmentData) => segmentData.references.length > 1, + ); + return hasMultipleReferences && selectedReferenceName === null; +} diff --git a/website/src/components/SearchPage/stillRequiresSuborganismSelection.tsx b/website/src/components/SearchPage/stillRequiresSuborganismSelection.tsx index 2c8745f09c..ccca6bf898 100644 --- a/website/src/components/SearchPage/stillRequiresSuborganismSelection.tsx +++ b/website/src/components/SearchPage/stillRequiresSuborganismSelection.tsx @@ -1,8 +1,8 @@ import type { ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes.ts'; -export function stillRequiresSuborganismSelection( +export function stillRequiresReferenceNameSelection( referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema, - selectedSuborganism: string | null, + selectedReferenceName: string | null, ) { - return Object.keys(referenceGenomeLightweightSchema).length > 1 && selectedSuborganism === null; + return Object.keys(referenceGenomeLightweightSchema).length > 1 && selectedReferenceName === null; } diff --git a/website/src/components/SearchPage/useSearchPageState.ts b/website/src/components/SearchPage/useSearchPageState.ts index 29916fc596..65ad0b2d44 100644 --- a/website/src/components/SearchPage/useSearchPageState.ts +++ b/website/src/components/SearchPage/useSearchPageState.ts @@ -19,6 +19,8 @@ type UseSearchPageStateParams = { filterSchema: MetadataFilterSchema; }; +type SegmentReferenceSelections = Record; + export function useSearchPageState({ initialQueryDict, schema, @@ -82,7 +84,7 @@ export function useSearchPageState({ delete newState[MUTATION_KEY]; filterSchema .ungroupedMetadataFilters() - .filter((metadataFilter) => metadataFilter.onlyForSuborganism !== undefined) + .filter((metadataFilter) => metadataFilter.onlyForReference !== undefined) .forEach((metadataFilter) => { delete newState[metadataFilter.name]; }); @@ -121,6 +123,20 @@ export function useSearchPageState({ (value) => value === null, ); + // Compute selectedReferences from selectedSuborganism for backward compatibility + // In the new segment-first mode, all segments use the same reference + const selectedReferences: SegmentReferenceSelections = useMemo(() => { + if (selectedSuborganism === null) { + return {}; + } + // TODO: This assumes all segments use the same reference + // In future, this could be enhanced to support per-segment selection + const refs: SegmentReferenceSelections = {}; + // We don't have segment information here, so return empty object + // The actual segment references will be built in components that have schema access + return refs; + }, [selectedSuborganism]); + const removeFilter = useCallback( (metadataFilterName: string) => { if (Object.keys(hiddenFieldValues).includes(metadataFilterName)) { @@ -218,6 +234,7 @@ export function useSearchPageState({ setPreviewHalfScreen, selectedSuborganism, setSelectedSuborganism, + selectedReferences, page, setPage, setSomeFieldValues, @@ -237,6 +254,7 @@ export function useSearchPageState({ setPreviewHalfScreen, selectedSuborganism, setSelectedSuborganism, + selectedReferences, page, setPage, setSomeFieldValues, diff --git a/website/src/components/SequenceDetailsPage/DataTable.tsx b/website/src/components/SequenceDetailsPage/DataTable.tsx index 4affe81eb0..9b0f41118a 100644 --- a/website/src/components/SequenceDetailsPage/DataTable.tsx +++ b/website/src/components/SequenceDetailsPage/DataTable.tsx @@ -6,18 +6,14 @@ import ReferenceSequenceLinkButton from './ReferenceSequenceLinkButton'; import { type DataTableData } from './getDataTableData'; import { type TableDataEntry } from './types'; import { type DataUseTermsHistoryEntry } from '../../types/backend'; -import { - type ReferenceAccession, - type ReferenceGenomesLightweightSchema, - type Suborganism, -} from '../../types/referencesGenomes'; +import { type ReferenceAccession, type ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes'; import AkarInfo from '~icons/ri/information-line'; interface Props { dataTableData: DataTableData; dataUseTermsHistory: DataUseTermsHistoryEntry[]; referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema; - suborganism: Suborganism | null; + segmentReferences: Record | null; } const ReferenceDisplay = ({ reference }: { reference: ReferenceAccession[] }) => { @@ -40,10 +36,18 @@ const DataTableComponent: React.FC = ({ dataTableData, dataUseTermsHistory, referenceGenomeLightweightSchema, - suborganism, + segmentReferences, }) => { - const reference = suborganism !== null ? referenceGenomeLightweightSchema[suborganism].insdcAccessionFull : null; - const hasReferenceAccession = (reference ?? []).filter((item) => item.insdcAccessionFull !== undefined).length > 0; + // Gather INSDC accessions from all segment/reference combinations + const reference: ReferenceAccession[] = []; + if (segmentReferences !== null) { + for (const [segmentName, referenceName] of Object.entries(segmentReferences)) { + const segmentData = referenceGenomeLightweightSchema.segments[segmentName]; + const accession = segmentData.insdcAccessions[referenceName]; + reference.push(accession); + } + } + const hasReferenceAccession = reference.filter((item) => item.insdcAccessionFull !== undefined).length > 0; return (
@@ -63,11 +67,11 @@ const DataTableComponent: React.FC = ({

{header}

- {reference !== null && hasReferenceAccession && header.includes('Alignment') && ( + {hasReferenceAccession && header.includes('Alignment') && ( )}
- {reference !== null && hasReferenceAccession && header.includes('mutation') && ( + {hasReferenceAccession && header.includes('mutation') && (

Mutations called relative to the reference diff --git a/website/src/components/SequenceDetailsPage/RevocationEntryDataTable.astro b/website/src/components/SequenceDetailsPage/RevocationEntryDataTable.astro index 73291139bc..74adcd311a 100644 --- a/website/src/components/SequenceDetailsPage/RevocationEntryDataTable.astro +++ b/website/src/components/SequenceDetailsPage/RevocationEntryDataTable.astro @@ -17,16 +17,16 @@ import { DATA_USE_TERMS_FIELD, } from '../../settings'; import { type DataUseTermsHistoryEntry } from '../../types/backend'; -import type { ReferenceGenomesLightweightSchema, Suborganism } from '../../types/referencesGenomes'; +import type { ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes'; interface Props { tableData: TableDataEntry[]; dataUseTermsHistory: DataUseTermsHistoryEntry[]; referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema; - suborganism: Suborganism | null; + segmentReferences: Record | null; } -const { tableData, dataUseTermsHistory, referenceGenomeLightweightSchema, suborganism } = Astro.props; +const { tableData, dataUseTermsHistory, referenceGenomeLightweightSchema, segmentReferences } = Astro.props; const relevantFieldsForRevocationVersions = [ ACCESSION_VERSION_FIELD, @@ -50,6 +50,6 @@ const dataTableData = getDataTableData(relevantData); diff --git a/website/src/components/SequenceDetailsPage/SequenceDataUI.tsx b/website/src/components/SequenceDetailsPage/SequenceDataUI.tsx index c4ead3764a..03e9948bf1 100644 --- a/website/src/components/SequenceDetailsPage/SequenceDataUI.tsx +++ b/website/src/components/SequenceDetailsPage/SequenceDataUI.tsx @@ -10,7 +10,7 @@ import { routes } from '../../routes/routes'; import { DATA_USE_TERMS_FIELD } from '../../settings.ts'; import { type DataUseTermsHistoryEntry, type Group, type RestrictedDataUseTerms } from '../../types/backend'; import { type Schema, type SequenceFlaggingConfig } from '../../types/config'; -import { type ReferenceGenomesLightweightSchema, type Suborganism } from '../../types/referencesGenomes'; +import { type ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes'; import { type ClientConfig } from '../../types/runtimeConfig'; import { EditDataUseTermsButton } from '../DataUseTerms/EditDataUseTermsButton'; import RestrictedUseWarning from '../common/RestrictedUseWarning'; @@ -19,7 +19,7 @@ import MdiEye from '~icons/mdi/eye'; interface Props { tableData: TableDataEntry[]; organism: string; - suborganism: Suborganism | null; + segmentReferences: Record | null; accessionVersion: string; dataUseTermsHistory: DataUseTermsHistoryEntry[]; schema: Schema; @@ -33,7 +33,7 @@ interface Props { export const SequenceDataUI: FC = ({ tableData, organism, - suborganism, + segmentReferences, accessionVersion, dataUseTermsHistory, schema, @@ -64,15 +64,15 @@ export const SequenceDataUI: FC = ({ {isRestricted && } - {schema.submissionDataTypes.consensusSequences && suborganism !== null && ( + {schema.submissionDataTypes.consensusSequences && segmentReferences !== null && (
({ @@ -35,7 +34,7 @@ const BUTTON_ROLE = 'button'; function renderSequenceViewer( referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema, - suborganism: string, + segmentReferences: Record, ) { render( @@ -45,7 +44,7 @@ function renderSequenceViewer( clientConfig={testConfig.public} referenceGenomeLightweightSchema={referenceGenomeLightweightSchema} loadSequencesAutomatically={false} - suborganism={suborganism} + segmentReferences={segmentReferences} /> , ); @@ -55,19 +54,29 @@ function renderSingleReferenceSequenceViewer({ nucleotideSegmentNames, genes, }: { - nucleotideSegmentNames: NucleotideSegmentNames; + nucleotideSegmentNames: string[]; genes: string[]; }) { - renderSequenceViewer( + const segments: Record< + string, { - [SINGLE_REFERENCE]: { - geneNames: genes, - nucleotideSegmentNames, - insdcAccessionFull: [], - }, - }, - SINGLE_REFERENCE, - ); + references: string[]; + insdcAccessions: Record; + genesByReference: Record; + } + > = {}; + const segmentReferences: Record = {}; + + for (const segmentName of nucleotideSegmentNames) { + segments[segmentName] = { + references: ['ref1'], + insdcAccessions: {}, + genesByReference: { ref1: genes }, + }; + segmentReferences[segmentName] = 'ref1'; + } + + renderSequenceViewer({ segments }, segmentReferences); } const multiSegmentName = 'main2'; @@ -166,23 +175,24 @@ describe('SequencesContainer', () => { test('should render single segmented sequences', async () => { const alignedSequence = `${suborganism1}AlignedSequence`; const sequence = `${suborganism1}Sequence`; - mockRequest.lapis.alignedNucleotideSequencesMultiSegment(200, `>some\n${alignedSequence}`, suborganism1); - mockRequest.lapis.unalignedNucleotideSequencesMultiSegment(200, `>some\n${sequence}`, suborganism1); + // Single segment uses non-segmented endpoints even in multi-reference mode + mockRequest.lapis.alignedNucleotideSequences(200, `>some\n${alignedSequence}`); + mockRequest.lapis.unalignedNucleotideSequences(200, `>some\n${sequence}`); renderSequenceViewer( { - [suborganism1]: { - nucleotideSegmentNames: ['main'], - geneNames: [], - insdcAccessionFull: [], - }, - [suborganism2]: { - nucleotideSegmentNames: ['main'], - geneNames: [], - insdcAccessionFull: [], + segments: { + main: { + references: [suborganism1, suborganism2], + insdcAccessions: {}, + genesByReference: { + [suborganism1]: [], + [suborganism2]: [], + }, + }, }, }, - suborganism1, + { main: suborganism1 }, ); click(LOAD_SEQUENCES_BUTTON); @@ -219,18 +229,26 @@ describe('SequencesContainer', () => { renderSequenceViewer( { - [suborganism1]: { - nucleotideSegmentNames: ['main'], - geneNames: [], - insdcAccessionFull: [], - }, - [suborganism2]: { - nucleotideSegmentNames: ['segment1', 'segment2'], - geneNames: [], - insdcAccessionFull: [], + segments: { + segment1: { + references: [suborganism1, suborganism2], + insdcAccessions: {}, + genesByReference: { + [suborganism1]: [], + [suborganism2]: [], + }, + }, + segment2: { + references: [suborganism1, suborganism2], + insdcAccessions: {}, + genesByReference: { + [suborganism1]: [], + [suborganism2]: [], + }, + }, }, }, - suborganism2, + { segment1: suborganism2, segment2: suborganism2 }, ); click(LOAD_SEQUENCES_BUTTON); diff --git a/website/src/components/SequenceDetailsPage/SequencesDisplay/SequencesContainer.tsx b/website/src/components/SequenceDetailsPage/SequencesDisplay/SequencesContainer.tsx index 1d614b417e..51bf035a4a 100644 --- a/website/src/components/SequenceDetailsPage/SequencesDisplay/SequencesContainer.tsx +++ b/website/src/components/SequenceDetailsPage/SequencesDisplay/SequencesContainer.tsx @@ -1,9 +1,9 @@ import { type Dispatch, type FC, type SetStateAction, useEffect, useState } from 'react'; import { SequencesViewer } from './SequenceViewer.tsx'; -import { type ReferenceGenomesLightweightSchema, type Suborganism } from '../../../types/referencesGenomes.ts'; +import { type ReferenceGenomesLightweightSchema } from '../../../types/referencesGenomes.ts'; import type { ClientConfig } from '../../../types/runtimeConfig.ts'; -import { getSuborganismSegmentAndGeneInfo } from '../../../utils/getSuborganismSegmentAndGeneInfo.tsx'; +import { getSegmentAndGeneInfo } from '../../../utils/getSegmentAndGeneInfo.tsx'; import { alignedSequenceSegment, type GeneInfo, @@ -22,7 +22,7 @@ import { withQueryProvider } from '../../common/withQueryProvider.tsx'; type SequenceContainerProps = { organism: string; - suborganism: Suborganism; + segmentReferences: Record; accessionVersion: string; clientConfig: ClientConfig; referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema; @@ -31,15 +31,15 @@ type SequenceContainerProps = { export const InnerSequencesContainer: FC = ({ organism, - suborganism, + segmentReferences, accessionVersion, clientConfig, referenceGenomeLightweightSchema, loadSequencesAutomatically, }) => { - const { nucleotideSegmentInfos, geneInfos, isMultiSegmented } = getSuborganismSegmentAndGeneInfo( + const { nucleotideSegmentInfos, geneInfos, isMultiSegmented } = getSegmentAndGeneInfo( referenceGenomeLightweightSchema, - suborganism, + segmentReferences, ); const [loadSequences, setLoadSequences] = useState(() => loadSequencesAutomatically); diff --git a/website/src/components/SequenceDetailsPage/getTableData.spec.ts b/website/src/components/SequenceDetailsPage/getTableData.spec.ts index 329dfd1085..e8464881d0 100644 --- a/website/src/components/SequenceDetailsPage/getTableData.spec.ts +++ b/website/src/components/SequenceDetailsPage/getTableData.spec.ts @@ -8,7 +8,7 @@ import { LapisClient } from '../../services/lapisClient.ts'; import type { ProblemDetail } from '../../types/backend.ts'; import type { Schema } from '../../types/config.ts'; import type { MutationProportionCount } from '../../types/lapis.ts'; -import { type ReferenceGenomes, SINGLE_REFERENCE } from '../../types/referencesGenomes.ts'; +import type { ReferenceGenomes } from '../../types/referencesGenomes.ts'; const schema: Schema = { organismName: 'instance name', @@ -29,22 +29,26 @@ const schema: Schema = { }; const singleReferenceGenomes: ReferenceGenomes = { - [SINGLE_REFERENCE]: { - nucleotideSequences: [], - genes: [], + main: { + ref1: { + sequence: 'ATCG', + genes: {}, + }, }, }; const genome1 = 'genome1'; const genome2 = 'genome2'; const multipleReferenceGenomes: ReferenceGenomes = { - [genome1]: { - nucleotideSequences: [], - genes: [], - }, - [genome2]: { - nucleotideSequences: [], - genes: [], + main: { + [genome1]: { + sequence: 'ATCG', + genes: {}, + }, + [genome2]: { + sequence: 'ATCG', + genes: {}, + }, }, }; @@ -326,22 +330,22 @@ describe('getTableData', () => { expect(mutationTableEntries).toStrictEqual([]); }); - test('should return the suborganism name for a single reference genome', async () => { + test('should return the segmentReferences for a single reference genome', async () => { const result = await getTableData(accessionVersion, schema, singleReferenceGenomes, lapisClient); - const suborganism = result._unsafeUnwrap().suborganism; + const segmentReferences = result._unsafeUnwrap().segmentReferences; - expect(suborganism).equals(SINGLE_REFERENCE); + expect(segmentReferences).toEqual({ main: 'ref1' }); }); - test('should return the suborganism name for multiple reference genomes', async () => { + test('should return the segmentReferences for multiple reference genomes', async () => { mockRequest.lapis.details(200, { info, data: [{ genotype: genome2 }] }); const result = await getTableData(accessionVersion, schema, multipleReferenceGenomes, lapisClient); - const suborganism = result._unsafeUnwrap().suborganism; + const segmentReferences = result._unsafeUnwrap().segmentReferences; - expect(suborganism).equals(genome2); + expect(segmentReferences).toEqual({ main: genome2 }); }); test('should throw when the suborganism name is not in multiple reference genomes', async () => { @@ -360,14 +364,14 @@ describe('getTableData', () => { ); }); - test('should tolerate when suborganism is null (as e.g. for revocation entries)', async () => { + test('should tolerate when genotype is null (as e.g. for revocation entries)', async () => { mockRequest.lapis.details(200, { info, data: [{ genotype: null }] }); const result = await getTableData(accessionVersion, schema, multipleReferenceGenomes, lapisClient); - const suborganism = result._unsafeUnwrap().suborganism; + const segmentReferences = result._unsafeUnwrap().segmentReferences; - expect(suborganism).equals(null); + expect(segmentReferences).equals(null); }); test('should throw when the suborganism name is not in multiple reference genomes', async () => { @@ -377,7 +381,7 @@ describe('getTableData', () => { expect(result).toStrictEqual( err({ - detail: "Suborganism 'unknown suborganism' (value of field 'genotype') not found in reference genomes.", + detail: "ReferenceName 'unknown suborganism' (value of field 'genotype') not found in reference genomes.", instance: '/seq/' + accessionVersion, status: 0, title: 'Invalid suborganism', diff --git a/website/src/components/SequenceDetailsPage/getTableData.ts b/website/src/components/SequenceDetailsPage/getTableData.ts index 87cee3ac43..2c0debc31a 100644 --- a/website/src/components/SequenceDetailsPage/getTableData.ts +++ b/website/src/components/SequenceDetailsPage/getTableData.ts @@ -12,12 +12,12 @@ import { type InsertionCount, type MutationProportionCount, } from '../../types/lapis.ts'; -import { type ReferenceGenomes, SINGLE_REFERENCE, type Suborganism } from '../../types/referencesGenomes.ts'; +import { type ReferenceGenomes } from '../../types/referencesGenomes.ts'; import { parseUnixTimestamp } from '../../utils/parseUnixTimestamp.ts'; export type GetTableDataResult = { data: TableDataEntry[]; - suborganism: Suborganism | null; + segmentReferences: Record | null; isRevocation: boolean; }; @@ -54,31 +54,53 @@ export async function getTableData( }), ) .andThen((data) => { - const suborganismResult = getSuborganism(data.details, schema, referenceGenomes, accessionVersion); - if (suborganismResult.isErr()) { - return err(suborganismResult.error); + const segmentReferencesResult = getSegmentReferences( + data.details, + schema, + referenceGenomes, + accessionVersion, + ); + if (segmentReferencesResult.isErr()) { + return err(segmentReferencesResult.error); } - const suborganism = suborganismResult.value; + const segmentReferences = segmentReferencesResult.value; return ok({ - data: toTableData(schema, suborganism, data), - suborganism, + data: toTableData(schema, segmentReferences, data), + segmentReferences, isRevocation: isRevocationEntry(data.details), }); }), ); } -function getSuborganism( +function getSegmentReferences( details: Details, schema: Schema, referenceGenomes: ReferenceGenomes, accessionVersion: string, -): Result { - if (SINGLE_REFERENCE in referenceGenomes) { - return ok(SINGLE_REFERENCE); +): Result | null, ProblemDetail> { + const segments = Object.keys(referenceGenomes); + + // Check if single reference mode (only one reference per segment) + const firstSegment = segments[0]; + const firstSegmentRefs = firstSegment ? Object.keys(referenceGenomes[firstSegment] ?? {}) : []; + const isSingleReference = firstSegmentRefs.length === 1; + + if (isSingleReference) { + // Build segment references from the single reference + const segmentReferences: Record = {}; + for (const segmentName of segments) { + const refs = Object.keys(referenceGenomes[segmentName] ?? {}); + if (refs.length > 0) { + segmentReferences[segmentName] = refs[0]; + } + } + return ok(segmentReferences); } + + // Multiple references mode - get from metadata field const suborganismField = schema.suborganismIdentifierField; if (suborganismField === undefined) { return err({ @@ -89,6 +111,7 @@ function getSuborganism( instance: '/seq/' + accessionVersion, }); } + const value = details[suborganismField]; const suborganismResult = z.string().nullable().safeParse(value); if (!suborganismResult.success) { @@ -100,17 +123,38 @@ function getSuborganism( instance: '/seq/' + accessionVersion, }); } - const suborganism = suborganismResult.data; - if (suborganism !== null && !(suborganism in referenceGenomes)) { + + const referenceName = suborganismResult.data; + if (referenceName === null) { + return ok(null); + } + + // Validate that the reference exists in at least one segment + let foundInAnySegment = false; + for (const segmentName of segments) { + if (referenceName in (referenceGenomes[segmentName] ?? {})) { + foundInAnySegment = true; + break; + } + } + + if (!foundInAnySegment) { return err({ type: 'about:blank', title: 'Invalid suborganism', status: 0, - detail: `Suborganism '${suborganism}' (value of field '${suborganismField}') not found in reference genomes.`, + detail: `ReferenceName '${referenceName}' (value of field '${suborganismField}') not found in reference genomes.`, instance: '/seq/' + accessionVersion, }); } - return ok(suborganism); + + // Build segment references - all segments use the same reference + const segmentReferences: Record = {}; + for (const segmentName of segments) { + segmentReferences[segmentName] = referenceName; + } + + return ok(segmentReferences); } function isRevocationEntry(details: Details): boolean { @@ -140,7 +184,7 @@ function mutationDetails( aminoAcidMutations: MutationProportionCount[], nucleotideInsertions: InsertionCount[], aminoAcidInsertions: InsertionCount[], - suborganism: Suborganism | null, + segmentReferences: Record | null, ): TableDataEntry[] { const data: TableDataEntry[] = [ { @@ -150,21 +194,21 @@ function mutationDetails( header: 'Nucleotide mutations', customDisplay: { type: 'badge', - value: substitutionsMap(nucleotideMutations, suborganism), + value: substitutionsMap(nucleotideMutations, segmentReferences), }, type: { kind: 'mutation' }, }, { label: 'Deletions', name: 'nucleotideDeletions', - value: deletionsToCommaSeparatedString(nucleotideMutations, suborganism), + value: deletionsToCommaSeparatedString(nucleotideMutations, segmentReferences), header: 'Nucleotide mutations', type: { kind: 'mutation' }, }, { label: 'Insertions', name: 'nucleotideInsertions', - value: insertionsToCommaSeparatedString(nucleotideInsertions, suborganism), + value: insertionsToCommaSeparatedString(nucleotideInsertions, segmentReferences), header: 'Nucleotide mutations', type: { kind: 'mutation' }, }, @@ -175,21 +219,21 @@ function mutationDetails( header: 'Amino acid mutations', customDisplay: { type: 'badge', - value: substitutionsMap(aminoAcidMutations, suborganism), + value: substitutionsMap(aminoAcidMutations, segmentReferences), }, type: { kind: 'mutation' }, }, { label: 'Deletions', name: 'aminoAcidDeletions', - value: deletionsToCommaSeparatedString(aminoAcidMutations, suborganism), + value: deletionsToCommaSeparatedString(aminoAcidMutations, segmentReferences), header: 'Amino acid mutations', type: { kind: 'mutation' }, }, { label: 'Insertions', name: 'aminoAcidInsertions', - value: insertionsToCommaSeparatedString(aminoAcidInsertions, suborganism), + value: insertionsToCommaSeparatedString(aminoAcidInsertions, segmentReferences), header: 'Amino acid mutations', type: { kind: 'mutation' }, }, @@ -199,7 +243,7 @@ function mutationDetails( function toTableData( config: Schema, - suborganism: Suborganism | null, + segmentReferences: Record | null, { details, nucleotideMutations, @@ -233,7 +277,7 @@ function toTableData( aminoAcidMutations, nucleotideInsertions, aminoAcidInsertions, - suborganism, + segmentReferences, ); data.push(...mutations); } @@ -255,7 +299,7 @@ function mapValueToDisplayedValue(value: undefined | null | string | number | bo export function substitutionsMap( mutationData: MutationProportionCount[], - suborganism: Suborganism | null, + segmentReferences: Record | null, ): SegmentedMutations[] { const result: SegmentedMutations[] = []; const substitutionData = mutationData.filter((m) => m.mutationTo !== '-'); @@ -263,7 +307,7 @@ export function substitutionsMap( const segmentMutationsMap = new Map(); for (const entry of substitutionData) { const { sequenceName, mutationFrom, position, mutationTo } = entry; - const sequenceDisplayName = computeSequenceDisplayName(sequenceName, suborganism); + const sequenceDisplayName = computeSequenceDisplayName(sequenceName, segmentReferences); const sequenceKey = sequenceDisplayName ?? ''; if (!segmentMutationsMap.has(sequenceKey)) { @@ -282,29 +326,38 @@ export function substitutionsMap( function computeSequenceDisplayName( originalSequenceName: string | null, - suborganism: Suborganism | null, + segmentReferences: Record | null, ): string | null { - if (originalSequenceName === null || suborganism === SINGLE_REFERENCE || suborganism === null) { + if (originalSequenceName === null || segmentReferences === null) { return originalSequenceName; } - if (originalSequenceName === suborganism) { - // there is only one segment in which case the name should be null - return null; + // Try to strip any reference prefix from the sequence name + for (const referenceName of Object.values(segmentReferences)) { + // Check if the sequence name is just the reference (single segment case) + if (originalSequenceName === referenceName) { + return null; + } + + // Try to strip the reference prefix + const prefixToTrim = `${referenceName}-`; + if (originalSequenceName.startsWith(prefixToTrim)) { + return originalSequenceName.substring(prefixToTrim.length); + } } - const prefixToTrim = `${suborganism}-`; - return originalSequenceName.startsWith(prefixToTrim) - ? originalSequenceName.substring(prefixToTrim.length) - : originalSequenceName; + return originalSequenceName; } -function deletionsToCommaSeparatedString(mutationData: MutationProportionCount[], suborganism: Suborganism | null) { +function deletionsToCommaSeparatedString( + mutationData: MutationProportionCount[], + segmentReferences: Record | null, +) { const segmentPositions = new Map(); mutationData .filter((m) => m.mutationTo === '-') .forEach((m) => { - const segment = computeSequenceDisplayName(m.sequenceName, suborganism); + const segment = computeSequenceDisplayName(m.sequenceName, segmentReferences); const position = m.position; if (!segmentPositions.has(segment)) { segmentPositions.set(segment, []); @@ -348,10 +401,13 @@ function deletionsToCommaSeparatedString(mutationData: MutationProportionCount[] .join(', '); } -function insertionsToCommaSeparatedString(insertionData: InsertionCount[], suborganism: Suborganism | null) { +function insertionsToCommaSeparatedString( + insertionData: InsertionCount[], + segmentReferences: Record | null, +) { return insertionData .map((insertion) => { - const sequenceDisplayName = computeSequenceDisplayName(insertion.sequenceName, suborganism); + const sequenceDisplayName = computeSequenceDisplayName(insertion.sequenceName, segmentReferences); const sequenceNamePart = sequenceDisplayName !== null ? sequenceDisplayName + ':' : ''; return `ins_${sequenceNamePart}${insertion.position}:${insertion.insertedSymbols}`; diff --git a/website/src/config.spec.ts b/website/src/config.spec.ts index 79ca340c00..0045260fda 100644 --- a/website/src/config.spec.ts +++ b/website/src/config.spec.ts @@ -16,7 +16,7 @@ const defaultConfig: WebsiteConfig = { }; describe('validateWebsiteConfig', () => { - it('should fail when "onlyForSuborganism" is not a valid organism', () => { + it('should fail when "onlyForReferenceName" is not a valid organism', () => { const errors = validateWebsiteConfig({ ...defaultConfig, organisms: { @@ -27,7 +27,7 @@ describe('validateWebsiteConfig', () => { { type: 'string', name: 'test field', - onlyForSuborganism: 'nonExistentSuborganism', + onlyForReferenceName: 'nonExistentReferenceName', }, ], inputFields: [], @@ -44,7 +44,7 @@ describe('validateWebsiteConfig', () => { expect(errors).toHaveLength(1); expect(errors[0].message).contains( - `Metadata field 'test field' in organism 'dummyOrganism' references unknown suborganism 'nonExistentSuborganism' in 'onlyForSuborganism'.`, + `Metadata field 'test field' in organism 'dummyOrganism' references unknown suborganism 'nonExistentReferenceName' in 'onlyForReferenceName'.`, ); }); diff --git a/website/src/config.ts b/website/src/config.ts index 60a9fa718b..63597a0161 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -13,9 +13,8 @@ import { websiteConfig, } from './types/config.ts'; import { - type NamedSequence, type ReferenceAccession, - type ReferenceGenomes, + type SegmentFirstReferenceGenomes, type ReferenceGenomesLightweightSchema, } from './types/referencesGenomes.ts'; import { runtimeConfig, type RuntimeConfig, type ServiceUrls } from './types/runtimeConfig.ts'; @@ -47,14 +46,14 @@ export function validateWebsiteConfig(config: WebsiteConfig): Error[] { }); } - const knownSuborganisms = Object.keys(schema.referenceGenomes); + const knownReferenceNames = Object.keys(schema.referenceGenomes); schema.schema.metadata.forEach((metadatum) => { - const onlyForSuborganism = metadatum.onlyForSuborganism; - if (onlyForSuborganism !== undefined && !knownSuborganisms.includes(onlyForSuborganism)) { + const onlyForReferenceName = metadatum.onlyForReferenceName; + if (onlyForReferenceName !== undefined && !knownReferenceNames.includes(onlyForReferenceName)) { errors.push( new Error( - `Metadata field '${metadatum.name}' in organism '${organism}' references unknown suborganism '${onlyForSuborganism}' in 'onlyForSuborganism'.`, + `Metadata field '${metadatum.name}' in organism '${organism}' references unknown suborganism '${onlyForReferenceName}' in 'onlyForReferenceName'.`, ), ); } @@ -278,29 +277,48 @@ export function getLapisUrl(serviceConfig: ServiceUrls, organism: string): strin return serviceConfig.lapisUrls[organism]; } -export function getReferenceGenomes(organism: string): ReferenceGenomes { +export function getReferenceGenomes(organism: string): SegmentFirstReferenceGenomes { return getConfig(organism).referenceGenomes; } -const getAccession = (n: NamedSequence): ReferenceAccession => { - return { - name: n.name, - insdcAccessionFull: n.insdcAccessionFull, - }; -}; - export const getReferenceGenomeLightweightSchema = (organism: string): ReferenceGenomesLightweightSchema => { const referenceGenomes = getReferenceGenomes(organism); - return Object.fromEntries( - Object.entries(referenceGenomes).map(([suborganism, referenceGenome]) => [ - suborganism, - { - nucleotideSegmentNames: referenceGenome.nucleotideSequences.map((n) => n.name), - geneNames: referenceGenome.genes.map((n) => n.name), - insdcAccessionFull: referenceGenome.nucleotideSequences.map((n) => getAccession(n)), - }, - ]), - ); + const segments: Record< + string, + { + references: string[]; + insdcAccessions: Record; + genesByReference: Record; + } + > = {}; + + // Transform segment-first structure to lightweight schema + for (const [segmentName, referenceMap] of Object.entries(referenceGenomes)) { + segments[segmentName] = { + references: Object.keys(referenceMap), + insdcAccessions: {}, + genesByReference: {}, + }; + + for (const [referenceName, referenceData] of Object.entries(referenceMap)) { + // Add INSDC accession + if (referenceData.insdcAccessionFull) { + segments[segmentName].insdcAccessions[referenceName] = { + name: referenceName, + insdcAccessionFull: referenceData.insdcAccessionFull, + }; + } + + // Add genes for this reference + if (referenceData.genes) { + segments[segmentName].genesByReference[referenceName] = Object.keys(referenceData.genes); + } else { + segments[segmentName].genesByReference[referenceName] = []; + } + } + } + + return { segments }; }; export function seqSetsAreEnabled() { diff --git a/website/src/hooks/useUrlParamState.ts b/website/src/hooks/useUrlParamState.ts index 490f7d7d10..82cc248fb5 100644 --- a/website/src/hooks/useUrlParamState.ts +++ b/website/src/hooks/useUrlParamState.ts @@ -3,7 +3,7 @@ import { useCallback, useMemo } from 'react'; import type { QueryState } from '../components/SearchPage/useStateSyncedWithUrlQueryParams.ts'; import type { FieldValueUpdate } from '../types/config.ts'; -type ParamType = 'string' | 'boolean' | 'nullable-string'; +type ParamType = 'string' | 'boolean' | 'nullable-string' | 'json'; /** * A hook that syncs state with URL parameters. @@ -40,14 +40,31 @@ function useUrlParamState( throw Error('Expected string, found array value in state.'); } return (urlValue ?? '') as T; + case 'json': + if (typeof urlValue === 'string') { + try { + return JSON.parse(urlValue) as T; + } catch { + return defaultValue; + } + } + return defaultValue; } } const updateUrlParam = useCallback( (newValue: T) => { - setSomeFieldValues([paramName, shouldRemove(newValue) ? null : String(newValue)]); + let serializedValue: string | null; + if (shouldRemove(newValue)) { + serializedValue = null; + } else if (paramType === 'json') { + serializedValue = JSON.stringify(newValue); + } else { + serializedValue = String(newValue); + } + setSomeFieldValues([paramName, serializedValue]); }, - [paramName, setSomeFieldValues, shouldRemove], + [paramName, setSomeFieldValues, shouldRemove, paramType], ); return [valueState, updateUrlParam]; diff --git a/website/src/pages/seq/[accessionVersion].fa/index.ts b/website/src/pages/seq/[accessionVersion].fa/index.ts index 00b98863ac..b42c762b28 100644 --- a/website/src/pages/seq/[accessionVersion].fa/index.ts +++ b/website/src/pages/seq/[accessionVersion].fa/index.ts @@ -4,7 +4,6 @@ import { getReferenceGenomeLightweightSchema } from '../../../config.ts'; import { routes } from '../../../routes/routes.ts'; import { LapisClient } from '../../../services/lapisClient.ts'; import { ACCESSION_VERSION_FIELD } from '../../../settings.ts'; -import { SINGLE_REFERENCE } from '../../../types/referencesGenomes.ts'; import { createDownloadAPIRoute } from '../../../utils/createDownloadAPIRoute.ts'; export const GET: APIRoute = createDownloadAPIRoute( @@ -16,10 +15,14 @@ export const GET: APIRoute = createDownloadAPIRoute( const referenceGenomeLightweightSchema = getReferenceGenomeLightweightSchema(organism); - if (SINGLE_REFERENCE in referenceGenomeLightweightSchema) { - const { nucleotideSegmentNames } = referenceGenomeLightweightSchema[SINGLE_REFERENCE]; - if (nucleotideSegmentNames.length > 1) { - return lapisClient.getMultiSegmentSequenceFasta(accessionVersion, nucleotideSegmentNames); + // Check if single reference mode (all segments have only one reference) + const segments = Object.entries(referenceGenomeLightweightSchema.segments); + const isSingleReference = segments.every(([_, segmentData]) => segmentData.references.length === 1); + + if (isSingleReference) { + const segmentNames = Object.keys(referenceGenomeLightweightSchema.segments); + if (segmentNames.length > 1) { + return lapisClient.getMultiSegmentSequenceFasta(accessionVersion, segmentNames); } return lapisClient.getSequenceFasta(accessionVersion); diff --git a/website/src/pages/seq/[accessionVersion]/details.json.ts b/website/src/pages/seq/[accessionVersion]/details.json.ts index 4279086014..9b1b34fbeb 100644 --- a/website/src/pages/seq/[accessionVersion]/details.json.ts +++ b/website/src/pages/seq/[accessionVersion]/details.json.ts @@ -41,7 +41,7 @@ export const GET: APIRoute = async (req) => { dataUseTermsHistory: result.dataUseTermsHistory, schema, clientConfig, - suborganism: result.suborganism, + segmentReferences: result.segmentReferences, isRevocation: result.isRevocation, sequenceEntryHistory: result.sequenceEntryHistory, }; diff --git a/website/src/pages/seq/[accessionVersion]/getSequenceDetailsTableData.ts b/website/src/pages/seq/[accessionVersion]/getSequenceDetailsTableData.ts index 78c2eefcbd..462eb5d70e 100644 --- a/website/src/pages/seq/[accessionVersion]/getSequenceDetailsTableData.ts +++ b/website/src/pages/seq/[accessionVersion]/getSequenceDetailsTableData.ts @@ -8,7 +8,6 @@ import { createBackendClient } from '../../../services/backendClientFactory.ts'; import { LapisClient } from '../../../services/lapisClient.ts'; import type { DataUseTermsHistoryEntry, ProblemDetail } from '../../../types/backend.ts'; import type { SequenceEntryHistory } from '../../../types/lapis.ts'; -import type { Suborganism } from '../../../types/referencesGenomes.ts'; import { parseAccessionVersionFromString } from '../../../utils/extractAccessionVersion.ts'; export enum SequenceDetailsTableResultType { @@ -21,7 +20,7 @@ export type TableData = { tableData: TableDataEntry[]; sequenceEntryHistory: SequenceEntryHistory; dataUseTermsHistory: DataUseTermsHistoryEntry[]; - suborganism: Suborganism | null; + segmentReferences: Record | null; isRevocation: boolean; }; @@ -64,7 +63,7 @@ export const getSequenceDetailsTableData = async ( tableData: tableData.data, sequenceEntryHistory, dataUseTermsHistory, - suborganism: tableData.suborganism, + segmentReferences: tableData.segmentReferences, isRevocation: tableData.isRevocation, }), ); diff --git a/website/src/pages/seq/[accessionVersion]/index.astro b/website/src/pages/seq/[accessionVersion]/index.astro index 64c51c0aa9..2c3dd077f9 100644 --- a/website/src/pages/seq/[accessionVersion]/index.astro +++ b/website/src/pages/seq/[accessionVersion]/index.astro @@ -74,13 +74,13 @@ const sequenceFlaggingConfig = getWebsiteConfig().sequenceFlagging; tableData={result.tableData} dataUseTermsHistory={result.dataUseTermsHistory} referenceGenomeLightweightSchema={getReferenceGenomeLightweightSchema(organism)} - suborganism={result.suborganism} + segmentReferences={result.segmentReferences} /> ) : ( ; export const instanceConfig = z.object({ schema, - referenceGenomes, + referenceGenomes: segmentFirstReferenceGenomes, }); export type InstanceConfig = z.infer; diff --git a/website/src/types/detailsJson.ts b/website/src/types/detailsJson.ts index ed4fdca86a..aa935de0f8 100644 --- a/website/src/types/detailsJson.ts +++ b/website/src/types/detailsJson.ts @@ -3,7 +3,6 @@ import { z } from 'zod'; import { dataUseTermsHistoryEntry } from './backend.ts'; import { schema } from './config.ts'; import { parsedSequenceEntryHistoryEntrySchema } from './lapis.ts'; -import { suborganism } from './referencesGenomes.ts'; import { serviceUrls } from './runtimeConfig.ts'; import { tableDataEntrySchema } from '../components/SequenceDetailsPage/types.ts'; @@ -14,7 +13,8 @@ export const detailsJsonSchema = z.object({ dataUseTermsHistory: z.array(dataUseTermsHistoryEntry), schema: schema, clientConfig: serviceUrls, - suborganism: suborganism.nullable(), + // Segment-first mode: map of segment names to reference names + segmentReferences: z.record(z.string(), z.string()).nullable(), isRevocation: z.boolean(), sequenceEntryHistory: z.array(parsedSequenceEntryHistoryEntrySchema), }); diff --git a/website/src/types/referencesGenomes.ts b/website/src/types/referencesGenomes.ts index ba64d446d8..bfb5732927 100644 --- a/website/src/types/referencesGenomes.ts +++ b/website/src/types/referencesGenomes.ts @@ -5,35 +5,48 @@ export type ReferenceAccession = { insdcAccessionFull?: string; }; -const namedSequence = z.object({ - name: z.string(), - sequence: z.string(), - insdcAccessionFull: z.optional(z.string()), -}); -export type NamedSequence = z.infer; +// Segment-first structure types +export type SegmentName = string; +export type ReferenceName = string; +export type GeneName = string; -export const referenceGenome = z.object({ - nucleotideSequences: z.array(namedSequence), - genes: z.array(namedSequence), -}); -export type ReferenceGenome = z.infer; - -export const suborganism = z.string(); -export type Suborganism = z.infer; - -export const referenceGenomes = z - .record(suborganism, referenceGenome) - .refine((value) => Object.entries(value).length > 0, 'The reference genomes must not be empty.'); -export type ReferenceGenomes = z.infer; - -export type NucleotideSegmentNames = string[]; - -export type SuborganismReferenceGenomesLightweightSchema = { - nucleotideSegmentNames: NucleotideSegmentNames; - geneNames: string[]; - insdcAccessionFull: ReferenceAccession[]; +export type GeneSequenceData = { + sequence: string; }; -export type ReferenceGenomesLightweightSchema = Record; +export type ReferenceSequenceData = { + sequence: string; + insdcAccessionFull?: string; + genes?: Record; +}; -export const SINGLE_REFERENCE = 'singleReference'; +// Segment-first reference genomes structure (from values.yaml) +// Structure: referenceGenomes[segmentName][referenceName] = { sequence, insdcAccessionFull?, genes? } +export const segmentFirstReferenceGenomes = z.record( + z.string(), // segment name + z.record( + z.string(), // reference name + z.object({ + sequence: z.string(), + insdcAccessionFull: z.string().optional(), + genes: z.record(z.string(), z.object({ sequence: z.string() })).optional(), + }), + ), +); +export type SegmentFirstReferenceGenomes = z.infer; + +// Type alias for the new segment-first structure +export type ReferenceGenomes = SegmentFirstReferenceGenomes; + +// Lightweight schema for segment-first mode +export type ReferenceGenomesLightweightSchema = { + segments: Record< + SegmentName, + { + references: ReferenceName[]; + insdcAccessions: Record; + // Genes available for each reference in this segment + genesByReference: Record; + } + >; +}; diff --git a/website/src/utils/getSegmentAndGeneInfo.spec.tsx b/website/src/utils/getSegmentAndGeneInfo.spec.tsx new file mode 100644 index 0000000000..9b9d7c4b5b --- /dev/null +++ b/website/src/utils/getSegmentAndGeneInfo.spec.tsx @@ -0,0 +1,185 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { describe, expect, test } from 'vitest'; + +import { getSegmentAndGeneInfo } from './getSegmentAndGeneInfo.tsx'; +import type { ReferenceGenomesLightweightSchema } from '../types/referencesGenomes.ts'; + +describe('getSegmentAndGeneInfo', () => { + describe('with single reference per segment', () => { + test('should return correct names for multi-segmented organism', () => { + const schema: ReferenceGenomesLightweightSchema = { + segments: { + segment1: { + references: ['ref1'], + insdcAccessions: {}, + genesByReference: { + ref1: ['gene1'], + }, + }, + segment2: { + references: ['ref1'], + insdcAccessions: {}, + genesByReference: { + ref1: ['gene2'], + }, + }, + }, + }; + + const selectedReferences = { + segment1: 'ref1', + segment2: 'ref1', + }; + + const result = getSegmentAndGeneInfo(schema, selectedReferences); + + expect(result).toEqual({ + nucleotideSegmentInfos: [ + { lapisName: 'segment1', label: 'segment1' }, + { lapisName: 'segment2', label: 'segment2' }, + ], + geneInfos: [ + { lapisName: 'gene1', label: 'gene1' }, + { lapisName: 'gene2', label: 'gene2' }, + ], + isMultiSegmented: true, + }); + }); + + test('should return correct names for single-segmented organism', () => { + const schema: ReferenceGenomesLightweightSchema = { + segments: { + main: { + references: ['ref1'], + insdcAccessions: {}, + genesByReference: { + ref1: ['gene1'], + }, + }, + }, + }; + + const selectedReferences = { + main: 'ref1', + }; + + const result = getSegmentAndGeneInfo(schema, selectedReferences); + + expect(result).toEqual({ + nucleotideSegmentInfos: [{ lapisName: 'main', label: 'main' }], + geneInfos: [{ lapisName: 'gene1', label: 'gene1' }], + isMultiSegmented: false, + }); + }); + }); + + describe('with multiple references (mixed)', () => { + test('should handle different references for different segments', () => { + const schema: ReferenceGenomesLightweightSchema = { + segments: { + segment1: { + references: ['CV-A16', 'CV-A10'], + insdcAccessions: {}, + genesByReference: { + 'CV-A16': ['gene1'], + 'CV-A10': ['gene1'], + }, + }, + segment2: { + references: ['CV-A16', 'CV-A10'], + insdcAccessions: {}, + genesByReference: { + 'CV-A16': ['gene2'], + 'CV-A10': ['gene2'], + }, + }, + }, + }; + + const selectedReferences = { + segment1: 'CV-A16', + segment2: 'CV-A10', + }; + + const result = getSegmentAndGeneInfo(schema, selectedReferences); + + expect(result).toEqual({ + nucleotideSegmentInfos: [ + { lapisName: 'CV-A16-segment1', label: 'segment1' }, + { lapisName: 'CV-A10-segment2', label: 'segment2' }, + ], + geneInfos: [ + { lapisName: 'CV-A16-gene1', label: 'gene1' }, + { lapisName: 'CV-A10-gene2', label: 'gene2' }, + ], + isMultiSegmented: true, + }); + }); + + test('should handle segments without selected references', () => { + const schema: ReferenceGenomesLightweightSchema = { + segments: { + segment1: { + references: ['CV-A16', 'CV-A10'], + insdcAccessions: {}, + genesByReference: { + 'CV-A16': ['gene1'], + 'CV-A10': ['gene1'], + }, + }, + segment2: { + references: ['CV-A16', 'CV-A10'], + insdcAccessions: {}, + genesByReference: { + 'CV-A16': ['gene2'], + 'CV-A10': ['gene2'], + }, + }, + }, + }; + + const selectedReferences = { + segment1: 'CV-A16', + // segment2 not selected + }; + + const result = getSegmentAndGeneInfo(schema, selectedReferences); + + expect(result).toEqual({ + nucleotideSegmentInfos: [ + { lapisName: 'CV-A16-segment1', label: 'segment1' }, + { lapisName: 'segment2', label: 'segment2' }, // No reference selected + ], + geneInfos: [ + { lapisName: 'CV-A16-gene1', label: 'gene1' }, + // gene2 not included since segment2 has no reference + ], + isMultiSegmented: true, + }); + }); + + test('should handle empty selectedReferences', () => { + const schema: ReferenceGenomesLightweightSchema = { + segments: { + main: { + references: ['ref1'], + insdcAccessions: {}, + genesByReference: { + ref1: ['gene1'], + }, + }, + }, + }; + + const selectedReferences = {}; + + const result = getSegmentAndGeneInfo(schema, selectedReferences); + + expect(result).toEqual({ + nucleotideSegmentInfos: [{ lapisName: 'main', label: 'main' }], + geneInfos: [], + isMultiSegmented: false, + }); + }); + }); +}); diff --git a/website/src/utils/getSegmentAndGeneInfo.tsx b/website/src/utils/getSegmentAndGeneInfo.tsx new file mode 100644 index 0000000000..8fb548b2ec --- /dev/null +++ b/website/src/utils/getSegmentAndGeneInfo.tsx @@ -0,0 +1,57 @@ +import { + type GeneInfo, + type SegmentInfo, + getSegmentInfoWithReference, + getGeneInfoWithReference, + type SegmentReferenceSelections, +} from './sequenceTypeHelpers.ts'; +import { type ReferenceGenomesLightweightSchema } from '../types/referencesGenomes.ts'; + +export type SegmentAndGeneInfo = { + nucleotideSegmentInfos: SegmentInfo[]; + geneInfos: GeneInfo[]; + isMultiSegmented: boolean; +}; + +/** + * Get segment and gene info where each segment can have its own reference. + * @param schema - The reference genome lightweight schema + * @param selectedReferences - Map of segment names to selected references + * @returns SegmentAndGeneInfo with all segments and their genes + */ +export function getSegmentAndGeneInfo( + schema: ReferenceGenomesLightweightSchema, + selectedReferences: SegmentReferenceSelections, +): SegmentAndGeneInfo { + const nucleotideSegmentInfos: SegmentInfo[] = []; + const geneInfos: GeneInfo[] = []; + + // Check if this is single-reference mode (all segments have only one reference) + const segments = Object.values(schema.segments); + const isSingleReference = segments.every((segmentData) => segmentData.references.length === 1); + + // Process each segment + for (const [segmentName, segmentData] of Object.entries(schema.segments)) { + const selectedRef = selectedReferences[segmentName] ?? null; + + // In single-reference mode, don't prefix segment names + const refForNaming = isSingleReference ? null : selectedRef; + + // Add nucleotide sequence info for this segment + nucleotideSegmentInfos.push(getSegmentInfoWithReference(segmentName, refForNaming)); + + // Add gene info if reference is selected + if (selectedRef) { + const geneNames = segmentData.genesByReference[selectedRef]; + for (const geneName of geneNames) { + geneInfos.push(getGeneInfoWithReference(geneName, refForNaming)); + } + } + } + + return { + nucleotideSegmentInfos, + geneInfos, + isMultiSegmented: Object.keys(schema.segments).length > 1, + }; +} diff --git a/website/src/utils/getSuborganismSegmentAndGeneInfo.spec.tsx b/website/src/utils/getSuborganismSegmentAndGeneInfo.spec.tsx deleted file mode 100644 index 905274c76e..0000000000 --- a/website/src/utils/getSuborganismSegmentAndGeneInfo.spec.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import { getSuborganismSegmentAndGeneInfo } from './getSuborganismSegmentAndGeneInfo.tsx'; -import { SINGLE_REFERENCE } from '../types/referencesGenomes.ts'; - -describe('getSuborganismSegmentAndGeneInfo', () => { - describe('with single reference', () => { - test('should return correct names for multi-segmented organism', () => { - const referenceGenomeSequenceNames = { - [SINGLE_REFERENCE]: { - nucleotideSegmentNames: ['segment1', 'segment2'], - geneNames: ['gene1', 'gene2'], - insdcAccessionFull: [], - }, - }; - - const result = getSuborganismSegmentAndGeneInfo(referenceGenomeSequenceNames, SINGLE_REFERENCE); - - expect(result).to.deep.equal({ - nucleotideSegmentInfos: [ - { lapisName: 'segment1', label: 'segment1' }, - { lapisName: 'segment2', label: 'segment2' }, - ], - geneInfos: [ - { lapisName: 'gene1', label: 'gene1' }, - { lapisName: 'gene2', label: 'gene2' }, - ], - isMultiSegmented: true, - }); - }); - - test('should return correct names for single-segmented organism', () => { - const referenceGenomeSequenceNames = { - [SINGLE_REFERENCE]: { - nucleotideSegmentNames: ['main'], - geneNames: ['gene1'], - insdcAccessionFull: [], - }, - }; - - const result = getSuborganismSegmentAndGeneInfo(referenceGenomeSequenceNames, SINGLE_REFERENCE); - - expect(result).to.deep.equal({ - nucleotideSegmentInfos: [{ lapisName: 'main', label: 'main' }], - geneInfos: [{ lapisName: 'gene1', label: 'gene1' }], - isMultiSegmented: false, - }); - }); - }); - - describe('with multiple references', () => { - const suborganism = 'sub1'; - - test('should return correct names for multi-segmented suborganism', () => { - const referenceGenomeSequenceNames = { - [suborganism]: { - nucleotideSegmentNames: ['segment1', 'segment2'], - geneNames: ['gene1', 'gene2'], - insdcAccessionFull: [], - }, - anotherSuborganism: { - nucleotideSegmentNames: ['segmentA', 'segmentB'], - geneNames: ['geneA'], - insdcAccessionFull: [], - }, - }; - - const result = getSuborganismSegmentAndGeneInfo(referenceGenomeSequenceNames, suborganism); - - expect(result).to.deep.equal({ - nucleotideSegmentInfos: [ - { lapisName: 'sub1-segment1', label: 'segment1' }, - { lapisName: 'sub1-segment2', label: 'segment2' }, - ], - geneInfos: [ - { lapisName: 'sub1-gene1', label: 'gene1' }, - { lapisName: 'sub1-gene2', label: 'gene2' }, - ], - isMultiSegmented: true, - }); - }); - - test('should return correct names for single-segmented suborganism', () => { - const referenceGenomeSequenceNames = { - [suborganism]: { - nucleotideSegmentNames: ['main'], - geneNames: ['gene1'], - insdcAccessionFull: [], - }, - anotherSuborganism: { - nucleotideSegmentNames: ['segmentA', 'segmentB'], - geneNames: ['geneA', 'geneB'], - insdcAccessionFull: [], - }, - }; - - const result = getSuborganismSegmentAndGeneInfo(referenceGenomeSequenceNames, suborganism); - - expect(result).to.deep.equal({ - nucleotideSegmentInfos: [{ lapisName: 'sub1', label: 'main' }], - geneInfos: [{ lapisName: 'sub1-gene1', label: 'gene1' }], - isMultiSegmented: true, - }); - }); - - test('should return null when no suborganism is selected', () => { - const referenceGenomeSequenceNames = { - [suborganism]: { - nucleotideSegmentNames: ['main'], - geneNames: ['gene1'], - insdcAccessionFull: [], - }, - anotherSuborganism: { - nucleotideSegmentNames: ['segmentA', 'segmentB'], - geneNames: ['geneA', 'geneB'], - insdcAccessionFull: [], - }, - }; - - const result = getSuborganismSegmentAndGeneInfo(referenceGenomeSequenceNames, null); - - expect(result).toBeNull(); - }); - - test('should return null when unknown suborganism is selected', () => { - const referenceGenomeSequenceNames = { - [suborganism]: { - nucleotideSegmentNames: ['main'], - geneNames: ['gene1'], - insdcAccessionFull: [], - }, - anotherSuborganism: { - nucleotideSegmentNames: ['segmentA', 'segmentB'], - geneNames: ['geneA', 'geneB'], - insdcAccessionFull: [], - }, - }; - - const result = getSuborganismSegmentAndGeneInfo(referenceGenomeSequenceNames, 'unknownSuborganism'); - - expect(result).toBeNull(); - }); - }); -}); diff --git a/website/src/utils/getSuborganismSegmentAndGeneInfo.tsx b/website/src/utils/getSuborganismSegmentAndGeneInfo.tsx deleted file mode 100644 index 4a2dcfd9dc..0000000000 --- a/website/src/utils/getSuborganismSegmentAndGeneInfo.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { - type GeneInfo, - getMultiPathogenNucleotideSequenceNames, - getMultiPathogenSequenceName, - getSinglePathogenSequenceName, - isMultiSegmented, - type SegmentInfo, -} from './sequenceTypeHelpers.ts'; -import { type ReferenceGenomesLightweightSchema, SINGLE_REFERENCE } from '../types/referencesGenomes.ts'; - -export type SuborganismSegmentAndGeneInfo = { - nucleotideSegmentInfos: SegmentInfo[]; - geneInfos: GeneInfo[]; - isMultiSegmented: boolean; -}; - -/** - * If we know that the suborganism is not null, then the result will also be non-null. - */ -export function getSuborganismSegmentAndGeneInfo( - referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema, - suborganism: string, -): SuborganismSegmentAndGeneInfo; - -export function getSuborganismSegmentAndGeneInfo( - referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema, - suborganism: string | null, -): SuborganismSegmentAndGeneInfo | null; - -export function getSuborganismSegmentAndGeneInfo( - referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema, - suborganism: string | null, -): SuborganismSegmentAndGeneInfo | null { - if (SINGLE_REFERENCE in referenceGenomeLightweightSchema) { - const { nucleotideSegmentNames, geneNames } = referenceGenomeLightweightSchema[SINGLE_REFERENCE]; - return { - nucleotideSegmentInfos: nucleotideSegmentNames.map(getSinglePathogenSequenceName), - geneInfos: geneNames.map(getSinglePathogenSequenceName), - isMultiSegmented: isMultiSegmented(nucleotideSegmentNames), - }; - } - - if (suborganism === null || !(suborganism in referenceGenomeLightweightSchema)) { - return null; - } - - const { nucleotideSegmentNames, geneNames } = referenceGenomeLightweightSchema[suborganism]; - - return { - nucleotideSegmentInfos: getMultiPathogenNucleotideSequenceNames(nucleotideSegmentNames, suborganism), - geneInfos: geneNames.map((name) => getMultiPathogenSequenceName(name, suborganism)), - isMultiSegmented: true, // LAPIS treats the suborganisms as multiple nucleotide segments -> always true - }; -} diff --git a/website/src/utils/mutation.spec.ts b/website/src/utils/mutation.spec.ts index d5ebaa9104..9d09fe6c6c 100644 --- a/website/src/utils/mutation.spec.ts +++ b/website/src/utils/mutation.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { SuborganismSegmentAndGeneInfo } from './getSuborganismSegmentAndGeneInfo.tsx'; +import type { SegmentAndGeneInfo } from './getSegmentAndGeneInfo.tsx'; import { intoMutationSearchParams, type MutationQuery, @@ -12,7 +12,7 @@ import { describe('mutation', () => { describe('single segment', () => { - const mockSuborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo = { + const mockSegmentAndGeneInfo: SegmentAndGeneInfo = { nucleotideSegmentInfos: [ { lapisName: 'lapisName-main', @@ -51,7 +51,7 @@ describe('mutation', () => { ], ...aminoAcidInsertionCases, ])('parses the valid mutation string "%s"', (input, expected) => { - const result = parseMutationString(input, mockSuborganismSegmentAndGeneInfo); + const result = parseMutationString(input, mockSegmentAndGeneInfo); expect(result).toEqual(expected); }); @@ -74,13 +74,13 @@ describe('mutation', () => { 'INS_label-GENE1:23:TTT:', 'INS_label-GENE1:23:TTT:INVALID', ])('returns undefined for invalid mutation string %s', (input) => { - const result = parseMutationString(input, mockSuborganismSegmentAndGeneInfo); + const result = parseMutationString(input, mockSegmentAndGeneInfo); expect(result).toBeUndefined(); }); }); describe('single segmented case with multiple suborganism', () => { - const mockSuborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo = { + const mockSegmentAndGeneInfo: SegmentAndGeneInfo = { nucleotideSegmentInfos: [ { lapisName: 'lapisName-main', @@ -153,21 +153,21 @@ describe('mutation', () => { ], ...aminoAcidInsertionCases, ])('parses the valid mutation string "%s"', (input, expected) => { - const result = parseMutationString(input, mockSuborganismSegmentAndGeneInfo); + const result = parseMutationString(input, mockSegmentAndGeneInfo); expect(result).toEqual(expected); }); it.each(['lapisName-main:A123T', 'label-main:A123T'])( 'returns undefined for invalid mutation string %s', (input) => { - const result = parseMutationString(input, mockSuborganismSegmentAndGeneInfo); + const result = parseMutationString(input, mockSegmentAndGeneInfo); expect(result).toBeUndefined(); }, ); }); describe('multi-segment', () => { - const mockSuborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo = { + const mockSegmentAndGeneInfo: SegmentAndGeneInfo = { nucleotideSegmentInfos: [ { lapisName: 'lapisName-SEQ1', @@ -244,7 +244,7 @@ describe('mutation', () => { ], ...aminoAcidInsertionCases, ])('parses the valid mutation string "%s"', (input, expected) => { - const result = parseMutationString(input, mockSuborganismSegmentAndGeneInfo); + const result = parseMutationString(input, mockSegmentAndGeneInfo); expect(result).toEqual(expected); }); @@ -262,12 +262,12 @@ describe('mutation', () => { 'ins_23:A:T', 'INS_4:G:T', ])('returns undefined for invalid mutation string %s', (input) => { - const result = parseMutationString(input, mockSuborganismSegmentAndGeneInfo); + const result = parseMutationString(input, mockSegmentAndGeneInfo); expect(result).toBeUndefined(); }); it('parses a comma-separated mutation string', () => { - const result = parseMutationsString('label-GENE1:A23T, label-SEQ1:123C', mockSuborganismSegmentAndGeneInfo); + const result = parseMutationsString('label-GENE1:A23T, label-SEQ1:123C', mockSegmentAndGeneInfo); expect(result).toEqual([ { baseType: 'aminoAcid', @@ -305,7 +305,7 @@ describe('mutation', () => { it('removes specified mutation queries', () => { const result = removeMutationQueries( 'label-GENE1:A23T, label-SEQ1:123C', - mockSuborganismSegmentAndGeneInfo, + mockSegmentAndGeneInfo, 'aminoAcid', 'substitutionOrDeletion', ); @@ -315,7 +315,7 @@ describe('mutation', () => { it('converts mutations to search params', () => { const params = intoMutationSearchParams( 'label-GENE1:A23T, label-SEQ1:123C, INS_label-SEQ1:100:G', - mockSuborganismSegmentAndGeneInfo, + mockSegmentAndGeneInfo, ); expect(params).toEqual({ nucleotideMutations: ['lapisName-SEQ1:123C'], diff --git a/website/src/utils/mutation.ts b/website/src/utils/mutation.ts index 61c369fdcb..a7720b34a4 100644 --- a/website/src/utils/mutation.ts +++ b/website/src/utils/mutation.ts @@ -1,4 +1,4 @@ -import type { SuborganismSegmentAndGeneInfo } from './getSuborganismSegmentAndGeneInfo.tsx'; +import type { SegmentAndGeneInfo } from './getSegmentAndGeneInfo.tsx'; import { type BaseType, isMultiSegmented, type SegmentInfo } from './sequenceTypeHelpers'; export type MutationType = 'substitutionOrDeletion' | 'insertion'; @@ -26,7 +26,7 @@ export type MutationSearchParams = { export const removeMutationQueries = ( mutations: string, - suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo, + suborganismSegmentAndGeneInfo: SegmentAndGeneInfo, baseType: BaseType, mutationType: MutationType, ): string => { @@ -39,7 +39,7 @@ export const removeMutationQueries = ( export const parseMutationsString = ( value: string, - suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo, + suborganismSegmentAndGeneInfo: SegmentAndGeneInfo, ): MutationQuery[] => { return value .split(',') @@ -53,7 +53,7 @@ export const parseMutationsString = ( */ export const parseMutationString = ( mutation: string, - suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo, + suborganismSegmentAndGeneInfo: SegmentAndGeneInfo, ): MutationQuery | undefined => { const tests = [ { baseType: 'nucleotide', mutationType: 'substitutionOrDeletion', test: isValidNucleotideMutationQuery }, @@ -76,7 +76,7 @@ export const serializeMutationQueries = (selectedOptions: MutationQuery[]): stri export const intoMutationSearchParams = ( mutation: string | undefined, - suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo, + suborganismSegmentAndGeneInfo: SegmentAndGeneInfo, ): MutationSearchParams => { const mutationFilter = parseMutationsString(mutation ?? '', suborganismSegmentAndGeneInfo); @@ -102,7 +102,7 @@ const INVALID: MutationTestResult = { valid: false }; const isValidAminoAcidInsertionQuery = ( text: string, - suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo, + suborganismSegmentAndGeneInfo: SegmentAndGeneInfo, ): MutationTestResult => { try { const textUpper = text.toUpperCase(); @@ -136,7 +136,7 @@ const isValidAminoAcidInsertionQuery = ( const isValidAminoAcidMutationQuery = ( text: string, - suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo, + suborganismSegmentAndGeneInfo: SegmentAndGeneInfo, ): MutationTestResult => { try { const textUpper = text.toUpperCase(); @@ -169,7 +169,7 @@ const isValidAminoAcidMutationQuery = ( const isValidNucleotideInsertionQuery = ( text: string, - suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo, + suborganismSegmentAndGeneInfo: SegmentAndGeneInfo, ): MutationTestResult => { try { const multiSegmented = isMultiSegmented(suborganismSegmentAndGeneInfo.nucleotideSegmentInfos); @@ -210,7 +210,7 @@ const isValidNucleotideInsertionQuery = ( const isValidNucleotideMutationQuery = ( text: string, - suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo, + suborganismSegmentAndGeneInfo: SegmentAndGeneInfo, ): MutationTestResult => { try { const multiSegmented = isMultiSegmented(suborganismSegmentAndGeneInfo.nucleotideSegmentInfos); diff --git a/website/src/utils/search.spec.ts b/website/src/utils/search.spec.ts index c33ee43452..86e2109dc4 100644 --- a/website/src/utils/search.spec.ts +++ b/website/src/utils/search.spec.ts @@ -36,7 +36,7 @@ describe('MetadataVisibility', () => { expect(visibility.isVisible('suborganism1')).toBe(false); }); - it('should return true when isChecked is true and onlyForSuborganism is undefined', () => { + it('should return true when isChecked is true and onlyForReferenceName is undefined', () => { const visibility = new MetadataVisibility(true, undefined); expect(visibility.isVisible(null)).toBe(true); diff --git a/website/src/utils/search.ts b/website/src/utils/search.ts index abf3910bfc..a9a887535e 100644 --- a/website/src/utils/search.ts +++ b/website/src/utils/search.ts @@ -33,23 +33,23 @@ type VisiblitySelectableAccessor = (field: MetadataFilter) => boolean; export class MetadataVisibility { public readonly isChecked: boolean; - private readonly onlyForSuborganism: string | undefined; + private readonly onlyForReferenceName: string | undefined; - constructor(isChecked: boolean, onlyForSuborganism: string | undefined) { + constructor(isChecked: boolean, onlyForReferenceName: string | undefined) { this.isChecked = isChecked; - this.onlyForSuborganism = onlyForSuborganism; + this.onlyForReferenceName = onlyForReferenceName; } - public isVisible(selectedSuborganism: string | null) { + public isVisible(selectedReferenceName: string | null) { if (!this.isChecked) { return false; } - if (this.onlyForSuborganism === undefined || selectedSuborganism === null) { + if (this.onlyForReferenceName === undefined || selectedReferenceName === null) { return true; } - return this.onlyForSuborganism === selectedSuborganism; + return this.onlyForReferenceName === selectedReferenceName; } } @@ -80,7 +80,7 @@ const getFieldOrColumnVisibilitiesFromQuery = ( const visibility = new MetadataVisibility( explicitVisibilitiesInUrlByFieldName.get(fieldName) ?? initiallyVisibleAccessor(field), - field.onlyForSuborganism, + field.onlyForReferenceName, ); visibilities.set(fieldName, visibility); diff --git a/website/src/utils/sequenceTypeHelpers.ts b/website/src/utils/sequenceTypeHelpers.ts index 36510c597d..7a3616fa81 100644 --- a/website/src/utils/sequenceTypeHelpers.ts +++ b/website/src/utils/sequenceTypeHelpers.ts @@ -62,3 +62,48 @@ export const isUnalignedSequence = (type: SequenceType): boolean => type.type == export const isAlignedSequence = (type: SequenceType): boolean => type.type === 'nucleotide' && type.aligned; export const isGeneSequence = (segmentOrGeneInfo: SegmentInfo | GeneInfo, type: SequenceType): boolean => type.type === 'aminoAcid' && type.name.lapisName === segmentOrGeneInfo.lapisName; + +// NEW: Segment-first mode helpers +export type SegmentReferenceSelections = Record; + +/** + * Get segment info for segment-first mode where each segment can have its own reference. + * @param segmentName - The segment name (e.g., "main", "VP4") + * @param referenceName - The selected reference for this segment (e.g., "CV-A16"), or null + * @returns SegmentInfo with appropriate LAPIS naming + */ +export function getSegmentInfoWithReference(segmentName: string, referenceName: string | null): SegmentInfo { + if (referenceName === null) { + // No reference selected - use segment name as-is + return { + lapisName: segmentName, + label: segmentName, + }; + } + // Reference selected - prefix with reference name for LAPIS + return { + lapisName: `${referenceName}-${segmentName}`, + label: segmentName, + }; +} + +/** + * Get gene info for segment-first mode. + * @param geneName - The gene name (e.g., "VP4") + * @param referenceName - The reference name (e.g., "CV-A16") + * @returns GeneInfo with appropriate LAPIS naming + */ +export function getGeneInfoWithReference(geneName: string, referenceName: string | null): GeneInfo { + if (referenceName === null) { + // No reference selected - use gene name as-is + return { + lapisName: geneName, + label: geneName, + }; + } + // Reference selected - prefix with reference name for LAPIS + return { + lapisName: `${referenceName}-${geneName}`, + label: geneName, + }; +} diff --git a/website/src/utils/serversideSearch.ts b/website/src/utils/serversideSearch.ts index 08f5352b4b..e2f51a049d 100644 --- a/website/src/utils/serversideSearch.ts +++ b/website/src/utils/serversideSearch.ts @@ -1,5 +1,5 @@ import { validateSingleValue } from './extractFieldValue'; -import { getSuborganismSegmentAndGeneInfo } from './getSuborganismSegmentAndGeneInfo.tsx'; +import { getSegmentAndGeneInfo } from './getSegmentAndGeneInfo.tsx'; import { getColumnVisibilitiesFromQuery, MetadataFilterSchema, @@ -23,11 +23,19 @@ export const performLapisSearchQueries = async ( hiddenFieldValues: FieldValues, organism: string, ): Promise => { - const suborganism = extractSuborganism(schema, state); + const suborganism = extractReferenceName(schema, state); - const suborganismSegmentAndGeneInfo = getSuborganismSegmentAndGeneInfo( + // Build segment references - all segments use the same reference + const segmentReferences: Record = {}; + if (suborganism !== null) { + for (const segmentName of Object.keys(referenceGenomeLightweightSchema.segments)) { + segmentReferences[segmentName] = suborganism; + } + } + + const suborganismSegmentAndGeneInfo = getSegmentAndGeneInfo( referenceGenomeLightweightSchema, - suborganism, + Object.keys(segmentReferences).length > 0 ? segmentReferences : {}, ); const filterSchema = new MetadataFilterSchema(schema.metadata); @@ -78,7 +86,7 @@ export const performLapisSearchQueries = async ( }; }; -function extractSuborganism(schema: Schema, state: QueryState): string | null { +function extractReferenceName(schema: Schema, state: QueryState): string | null { if (schema.suborganismIdentifierField === undefined) { return null; }