From ff94dc607906487a3db262b38f1cd8c62cd5599f Mon Sep 17 00:00:00 2001 From: Theo Sanderson Date: Mon, 15 Dec 2025 00:17:29 +0000 Subject: [PATCH 01/71] restructure --- .../templates/_merged-reference-genomes.tpl | 95 +++++--- kubernetes/loculus/values.schema.json | 104 ++++----- kubernetes/loculus/values.yaml | 218 +++++++++--------- .../components/ReviewPage/ReviewPage.spec.tsx | 9 +- .../getSegmentAndGeneDisplayNameMap.spec.tsx | 69 +++--- .../getSegmentAndGeneDisplayNameMap.tsx | 53 +++-- .../DownloadDialog/DownloadDialog.spec.tsx | 48 ++-- .../DownloadDialog/DownloadDialog.tsx | 14 +- .../DownloadDialog/DownloadForm.tsx | 73 ++++-- .../FieldSelector/FieldSelectorModal.spec.tsx | 8 +- .../FieldSelector/FieldSelectorModal.tsx | 14 +- .../DownloadDialog/SequenceFilters.tsx | 6 +- .../SearchPage/ReferenceNameSelector.tsx | 2 + .../components/SearchPage/SearchForm.spec.tsx | 43 ++-- .../src/components/SearchPage/SearchForm.tsx | 8 +- .../SearchPage/SearchFullUI.spec.tsx | 34 +-- .../components/SearchPage/SearchFullUI.tsx | 16 +- .../SearchPage/SegmentReferenceSelector.tsx | 152 ++++++++++++ .../SearchPage/SuborganismSelector.spec.tsx | 27 ++- .../SearchPage/SuborganismSelector.tsx | 5 +- .../SearchPage/TableColumnSelectorModal.tsx | 16 +- .../SearchPage/fields/MutationField.spec.tsx | 8 +- .../SearchPage/fields/MutationField.tsx | 4 +- .../isActiveForSelectedReferenceName.tsx | 17 ++ .../isActiveForSelectedSuborganism.tsx | 20 +- .../stillRequiresReferenceNameSelection.tsx | 12 + .../stillRequiresSuborganismSelection.tsx | 6 +- .../SearchPage/useSearchPageState.ts | 20 +- .../SequenceDetailsPage/DataTable.tsx | 19 +- .../RevocationEntryDataTable.astro | 8 +- .../SequenceDetailsPage/SequenceDataUI.tsx | 12 +- .../SequenceContainer.spec.tsx | 82 ++++--- .../SequencesDisplay/SequencesContainer.tsx | 12 +- .../SequenceDetailsPage/getTableData.spec.ts | 46 ++-- .../SequenceDetailsPage/getTableData.ts | 125 ++++++---- website/src/config.spec.ts | 6 +- website/src/config.ts | 63 +++-- website/src/hooks/useUrlParamState.ts | 23 +- .../pages/seq/[accessionVersion].fa/index.ts | 13 +- .../seq/[accessionVersion]/details.json.ts | 2 +- .../getSequenceDetailsTableData.ts | 5 +- .../pages/seq/[accessionVersion]/index.astro | 4 +- website/src/types/config.ts | 7 +- website/src/types/detailsJson.ts | 4 +- website/src/types/referencesGenomes.ts | 66 +++--- .../src/utils/getSegmentAndGeneInfo.spec.tsx | 182 +++++++++++++++ website/src/utils/getSegmentAndGeneInfo.tsx | 57 +++++ .../getSuborganismSegmentAndGeneInfo.spec.tsx | 144 ------------ .../getSuborganismSegmentAndGeneInfo.tsx | 54 ----- website/src/utils/mutation.spec.ts | 26 +-- website/src/utils/mutation.ts | 18 +- website/src/utils/search.spec.ts | 2 +- website/src/utils/search.ts | 14 +- website/src/utils/sequenceTypeHelpers.ts | 45 ++++ website/src/utils/serversideSearch.ts | 18 +- 55 files changed, 1326 insertions(+), 832 deletions(-) create mode 100644 website/src/components/SearchPage/ReferenceNameSelector.tsx create mode 100644 website/src/components/SearchPage/SegmentReferenceSelector.tsx create mode 100644 website/src/components/SearchPage/isActiveForSelectedReferenceName.tsx create mode 100644 website/src/components/SearchPage/stillRequiresReferenceNameSelection.tsx create mode 100644 website/src/utils/getSegmentAndGeneInfo.spec.tsx create mode 100644 website/src/utils/getSegmentAndGeneInfo.tsx delete mode 100644 website/src/utils/getSuborganismSegmentAndGeneInfo.spec.tsx delete mode 100644 website/src/utils/getSuborganismSegmentAndGeneInfo.tsx diff --git a/kubernetes/loculus/templates/_merged-reference-genomes.tpl b/kubernetes/loculus/templates/_merged-reference-genomes.tpl index ae6d685d18..b1cd416536 100644 --- a/kubernetes/loculus/templates/_merged-reference-genomes.tpl +++ b/kubernetes/loculus/templates/_merged-reference-genomes.tpl @@ -1,60 +1,83 @@ {{- 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 -}} +{{/* Extract all unique reference names from the first segment */}} +{{- $referenceNames := list -}} +{{- if $segmentFirstConfig -}} + {{- $firstSegment := first (values $segmentFirstConfig) -}} + {{- $referenceNames = keys $firstSegment -}} +{{- end -}} - {{- $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 +{{/* 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 */}} - {{- if $referenceGenome.genes -}} - {{- range $gene := $referenceGenome.genes -}} - {{- $lapisGenes = append $lapisGenes (dict - "name" (printf "%s-%s" $suborganismName $gene.name) - "sequence" $gene.sequence) - -}} + {{/* 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 + ) -}} + + {{/* 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 -}} - {{- $result := dict "nucleotideSequences" $lapisNucleotideSequences "genes" $lapisGenes -}} - {{- $result | toYaml -}} {{- end -}} +{{- $result := dict "nucleotideSequences" $lapisNucleotideSequences "genes" $lapisGenes -}} +{{- $result | toYaml -}} {{- 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 93ae9aca47..ef788e030a 100644 --- a/kubernetes/loculus/values.schema.json +++ b/kubernetes/loculus/values.schema.json @@ -805,70 +805,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 2b26905c2f..ba5d7a84f1 100644 --- a/kubernetes/loculus/values.yaml +++ b/kubernetes/loculus/values.yaml @@ -1970,118 +1970,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/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..e22ab42fc3 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..62d878718e 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx @@ -12,7 +12,6 @@ import { versionStatuses } from '../../../types/lapis'; import { type ReferenceGenomesLightweightSchema, type ReferenceAccession, - SINGLE_REFERENCE, } from '../../../types/referencesGenomes.ts'; import { MetadataFilterSchema } from '../../../utils/search.ts'; @@ -22,23 +21,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 +113,7 @@ async function renderDialog({ dataUseTermsEnabled={dataUseTermsEnabled} schema={schema} richFastaHeaderFields={richFastaHeaderFields} - selectedSuborganism={selectedSuborganism} + selectedReferenceName={selectedSuborganism} suborganismIdentifierField={suborganismIdentifierField} />, ); @@ -391,7 +395,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 +469,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 +484,7 @@ describe('DownloadDialog', () => { type: 'string', header: 'Group 1', includeInDownloadsByDefault: true, - onlyForSuborganism: 'suborganism2', + onlyForReferenceName: 'suborganism2', }, { name: ACCESSION_VERSION_FIELD, @@ -489,12 +493,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 +514,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..8589cdcc72 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..35da8811db 100644 --- a/website/src/components/SearchPage/SearchForm.spec.tsx +++ b/website/src/components/SearchPage/SearchForm.spec.tsx @@ -9,7 +9,6 @@ import type { MetadataFilter } from '../../types/config.ts'; import { type ReferenceGenomesLightweightSchema, type ReferenceAccession, - SINGLE_REFERENCE, } from '../../types/referencesGenomes.ts'; import { MetadataFilterSchema, MetadataVisibility } from '../../utils/search.ts'; @@ -44,23 +43,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 +75,7 @@ const defaultSearchVisibilities = new Map([ const setSomeFieldValues = vi.fn(); const setASearchVisibility = vi.fn(); -const setSelectedSuborganism = vi.fn(); +const setSelectedReferenceName = vi.fn(); const renderSearchForm = ({ filterSchema = new MetadataFilterSchema([...defaultSearchFormFilters]), @@ -104,7 +108,8 @@ const renderSearchForm = ({ showMutationSearch: true, suborganismIdentifierField, selectedSuborganism, - setSelectedSuborganism, + setSelectedSuborganism: vi.fn(), + selectedReferences: {}, }; render( @@ -153,7 +158,7 @@ describe('SearchForm', () => { expect(suborganismSelector).toBeInTheDocument(); await userEvent.selectOptions(suborganismSelector, 'suborganism1'); - expect(setSelectedSuborganism).toHaveBeenCalledWith('suborganism1'); + expect(setSelectedReferenceName).toHaveBeenCalledWith('suborganism1'); }); it('opens advanced options modal with version status and revocation fields', async () => { @@ -179,14 +184,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..f1d820533f 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 ( diff --git a/website/src/components/SearchPage/SearchFullUI.spec.tsx b/website/src/components/SearchPage/SearchFullUI.spec.tsx index db84f4b872..c09e92e37b 100644 --- a/website/src/components/SearchPage/SearchFullUI.spec.tsx +++ b/website/src/components/SearchPage/SearchFullUI.spec.tsx @@ -10,7 +10,6 @@ import type { FieldValues, MetadataFilter, Schema } from '../../types/config.ts' import { type ReferenceAccession, type ReferenceGenomesLightweightSchema, - SINGLE_REFERENCE, } from '../../types/referencesGenomes.ts'; import type { ClientConfig } from '../../types/runtimeConfig.ts'; import { ACTIVE_FILTER_BADGE_TEST_ID } from '../common/ActiveFilters.tsx'; @@ -73,10 +72,12 @@ 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'] }, + }, }, }; @@ -381,7 +382,7 @@ describe('SearchFullUI', () => { name: 'field1', type: 'string', displayName: 'Field 1', - onlyForSuborganism: 'suborganism1', + onlyForReferenceName: 'suborganism1', initiallyVisible: true, }, { @@ -391,15 +392,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..bbf9abde96 --- /dev/null +++ b/website/src/components/SearchPage/SegmentReferenceSelector.tsx @@ -0,0 +1,152 @@ +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] !== undefined && 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..bed726521b --- /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 940559ef95..83d40de8fe 100644 --- a/website/src/components/SearchPage/useSearchPageState.ts +++ b/website/src/components/SearchPage/useSearchPageState.ts @@ -24,6 +24,8 @@ type UseSearchPageStateParams = { filterSchema: MetadataFilterSchema; }; +type SegmentReferenceSelections = Record; + export function useSearchPageState({ initialQueryDict, schema, @@ -90,7 +92,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]; }); @@ -134,6 +136,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)) { @@ -231,6 +247,7 @@ export function useSearchPageState({ setPreviewHalfScreen, selectedSuborganism, setSelectedSuborganism, + selectedReferences, page, setPage, setSomeFieldValues, @@ -250,6 +267,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..27e8dae6bd 100644 --- a/website/src/components/SequenceDetailsPage/DataTable.tsx +++ b/website/src/components/SequenceDetailsPage/DataTable.tsx @@ -9,7 +9,7 @@ import { type DataUseTermsHistoryEntry } from '../../types/backend'; import { type ReferenceAccession, type ReferenceGenomesLightweightSchema, - type Suborganism, + type ReferenceName, } from '../../types/referencesGenomes'; import AkarInfo from '~icons/ri/information-line'; @@ -17,7 +17,7 @@ interface Props { dataTableData: DataTableData; dataUseTermsHistory: DataUseTermsHistoryEntry[]; referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema; - suborganism: Suborganism | null; + segmentReferences: Record | null; } const ReferenceDisplay = ({ reference }: { reference: ReferenceAccession[] }) => { @@ -40,10 +40,19 @@ 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]; + if (segmentData && segmentData.insdcAccessions[referenceName]) { + reference.push(segmentData.insdcAccessions[referenceName]); + } + } + } + const hasReferenceAccession = reference.filter((item) => item.insdcAccessionFull !== undefined).length > 0; return (
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..fb5405e878 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, type ReferenceName } 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 && (
({ getLapisUrl: vi.fn().mockReturnValue('http://lapis.dummy'), @@ -35,7 +31,7 @@ const BUTTON_ROLE = 'button'; function renderSequenceViewer( referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema, - suborganism: string, + segmentReferences: Record, ) { render( @@ -45,7 +41,7 @@ function renderSequenceViewer( clientConfig={testConfig.public} referenceGenomeLightweightSchema={referenceGenomeLightweightSchema} loadSequencesAutomatically={false} - suborganism={suborganism} + segmentReferences={segmentReferences} /> , ); @@ -55,18 +51,24 @@ function renderSingleReferenceSequenceViewer({ nucleotideSegmentNames, genes, }: { - nucleotideSegmentNames: NucleotideSegmentNames; + nucleotideSegmentNames: string[]; genes: string[]; }) { + const segments: Record }> = {}; + const segmentReferences: Record = {}; + + for (const segmentName of nucleotideSegmentNames) { + segments[segmentName] = { + references: ['ref1'], + insdcAccessions: {}, + genesByReference: { ref1: genes }, + }; + segmentReferences[segmentName] = 'ref1'; + } + renderSequenceViewer( - { - [SINGLE_REFERENCE]: { - geneNames: genes, - nucleotideSegmentNames, - insdcAccessionFull: [], - }, - }, - SINGLE_REFERENCE, + { segments }, + segmentReferences, ); } @@ -171,18 +173,18 @@ describe('SequencesContainer', () => { 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 +221,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..cdbd761b7a 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, type ReferenceName } 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..c8e2b19502 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, type ReferenceName } 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,48 @@ 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 +106,7 @@ function getSuborganism( instance: '/seq/' + accessionVersion, }); } + const value = details[suborganismField]; const suborganismResult = z.string().nullable().safeParse(value); if (!suborganismResult.success) { @@ -100,17 +118,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 +179,7 @@ function mutationDetails( aminoAcidMutations: MutationProportionCount[], nucleotideInsertions: InsertionCount[], aminoAcidInsertions: InsertionCount[], - suborganism: Suborganism | null, + segmentReferences: Record | null, ): TableDataEntry[] { const data: TableDataEntry[] = [ { @@ -150,21 +189,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 +214,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 +238,7 @@ function mutationDetails( function toTableData( config: Schema, - suborganism: Suborganism | null, + segmentReferences: Record | null, { details, nucleotideMutations, @@ -233,7 +272,7 @@ function toTableData( aminoAcidMutations, nucleotideInsertions, aminoAcidInsertions, - suborganism, + segmentReferences, ); data.push(...mutations); } @@ -255,7 +294,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 +302,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 +321,35 @@ 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 +393,10 @@ 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..eb30559e1b 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,45 @@ 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; + 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..eacce7ace8 100644 --- a/website/src/types/referencesGenomes.ts +++ b/website/src/types/referencesGenomes.ts @@ -5,35 +5,45 @@ 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; + // 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..648069fa8e --- /dev/null +++ b/website/src/utils/getSegmentAndGeneInfo.spec.tsx @@ -0,0 +1,182 @@ +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: ['ref1'], + insdcAccessions: {}, + genesByReference: { + ref1: ['gene1'], + }, + }, + segment2: { + references: ['ref1'], + insdcAccessions: {}, + genesByReference: { + ref1: ['gene2'], + }, + }, + }, + }; + + const selectedReferences = { + segment1: 'ref1', + // 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..3ed88e9435 --- /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 0791ce9de6..b63d8f5418 100644 --- a/website/src/utils/search.ts +++ b/website/src/utils/search.ts @@ -37,23 +37,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; } } @@ -84,7 +84,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; } From c7bebc329028478650a4bfae1edecfbfbbecfabe Mon Sep 17 00:00:00 2001 From: Theo Sanderson Date: Mon, 15 Dec 2025 09:01:45 +0000 Subject: [PATCH 02/71] update --- kubernetes/loculus/values.yaml | 314 ++++++++++++------ .../getSegmentAndGeneDisplayNameMap.tsx | 4 +- .../components/SearchPage/SearchForm.spec.tsx | 48 ++- .../SearchPage/SearchFullUI.spec.tsx | 2 +- .../SequenceDetailsPage/DataTable.tsx | 1 - .../SequenceDetailsPage/SequenceDataUI.tsx | 2 +- .../SequenceContainer.spec.tsx | 5 +- .../SequencesDisplay/SequencesContainer.tsx | 2 +- .../SequenceDetailsPage/getTableData.ts | 2 +- .../src/utils/getSegmentAndGeneInfo.spec.tsx | 13 +- website/src/utils/getSegmentAndGeneInfo.tsx | 2 +- 11 files changed, 259 insertions(+), 136 deletions(-) diff --git a/kubernetes/loculus/values.yaml b/kubernetes/loculus/values.yaml index ba5d7a84f1..bd9ad101dc 100644 --- a/kubernetes/loculus/values.yaml +++ b/kubernetes/loculus/values.yaml @@ -1379,30 +1379,56 @@ 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]]" + NP: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/NP.fasta]]" + VP35: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/VP35.fasta]]" + VP40: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/VP40.fasta]]" + GP: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/GP.fasta]]" + ssGP: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/ssGP.fasta]]" + sGP: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/sGP.fasta]]" + VP30: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/VP30.fasta]]" + VP24: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/VP24.fasta]]" + L: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/L.fasta]]" west-nile: <<: *defaultOrganismConfig schema: @@ -1463,34 +1489,66 @@ 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]]" + 2K: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/2K.fasta]]" + NS1: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS1.fasta]]" + NS2A: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS2A.fasta]]" + NS2B: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS2B.fasta]]" + NS3: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS3.fasta]]" + NS4A: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS4A.fasta]]" + NS4B: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS4B.fasta]]" + NS5: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS5.fasta]]" + capsid: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/capsid.fasta]]" + env: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/env.fasta]]" + prM: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/prM.fasta]]" dummy-organism: schema: submissionDataTypes: *defaultSubmissionDataTypes @@ -1574,35 +1632,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" @@ -1630,10 +1723,7 @@ defaultOrganisms: args: - "--watch" - "--disableConsensusSequences" - referenceGenomes: - singleReference: - nucleotideSequences: [] - genes: [] + referenceGenomes: {} not-aligned-organism: enabled: true schema: @@ -1716,11 +1806,9 @@ defaultOrganisms: - reference_name: "singleReference" 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: @@ -1813,24 +1901,34 @@ 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]]" + GPC: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/GPC.fasta]]" + NP: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/NP.fasta]]" + M: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/reference_M.fasta]]" + insdcAccessionFull: NC_005300.2 + S: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/reference_S.fasta]]" + insdcAccessionFull: NC_005302.1 + RdRp: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/RdRp.fasta]]" + GPC: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/GPC.fasta]]" + NP: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/NP.fasta]]" enteroviruses: <<: *defaultOrganismConfig enabled: true diff --git a/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx b/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx index e22ab42fc3..721c665910 100644 --- a/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx +++ b/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx @@ -14,7 +14,7 @@ export function getSegmentAndGeneDisplayNameMap( // Add genes for this segment/reference const singleRef = segmentData.references[0]; - const genes = segmentData.genesByReference[singleRef] || []; + const genes = segmentData.genesByReference[singleRef]; for (const geneName of genes) { mappingEntries.push([geneName, geneName]); } @@ -25,7 +25,7 @@ export function getSegmentAndGeneDisplayNameMap( mappingEntries.push([lapisSegmentName, segmentName]); // Add genes for this segment/reference - const genes = segmentData.genesByReference[referenceName] || []; + const genes = segmentData.genesByReference[referenceName]; for (const geneName of genes) { const lapisGeneName = `${referenceName}-${geneName}`; mappingEntries.push([lapisGeneName, geneName]); diff --git a/website/src/components/SearchPage/SearchForm.spec.tsx b/website/src/components/SearchPage/SearchForm.spec.tsx index 35da8811db..60b4e0aafc 100644 --- a/website/src/components/SearchPage/SearchForm.spec.tsx +++ b/website/src/components/SearchPage/SearchForm.spec.tsx @@ -1,7 +1,7 @@ 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'; @@ -75,7 +75,6 @@ const defaultSearchVisibilities = new Map([ const setSomeFieldValues = vi.fn(); const setASearchVisibility = vi.fn(); -const setSelectedReferenceName = vi.fn(); const renderSearchForm = ({ filterSchema = new MetadataFilterSchema([...defaultSearchFormFilters]), @@ -120,6 +119,10 @@ const renderSearchForm = ({ }; describe('SearchForm', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it('renders without crashing', () => { renderSearchForm(); expect(screen.getByText('Field 1')).toBeInTheDocument(); @@ -145,20 +148,39 @@ 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'); - expect(setSelectedReferenceName).toHaveBeenCalledWith('suborganism1'); + expect(setSelectedSuborganism).toHaveBeenCalledWith('suborganism1'); }); it('opens advanced options modal with version status and revocation fields', async () => { diff --git a/website/src/components/SearchPage/SearchFullUI.spec.tsx b/website/src/components/SearchPage/SearchFullUI.spec.tsx index c09e92e37b..b371a31d32 100644 --- a/website/src/components/SearchPage/SearchFullUI.spec.tsx +++ b/website/src/components/SearchPage/SearchFullUI.spec.tsx @@ -382,7 +382,7 @@ describe('SearchFullUI', () => { name: 'field1', type: 'string', displayName: 'Field 1', - onlyForReferenceName: 'suborganism1', + onlyForReference: 'suborganism1', initiallyVisible: true, }, { diff --git a/website/src/components/SequenceDetailsPage/DataTable.tsx b/website/src/components/SequenceDetailsPage/DataTable.tsx index 27e8dae6bd..6d760841c8 100644 --- a/website/src/components/SequenceDetailsPage/DataTable.tsx +++ b/website/src/components/SequenceDetailsPage/DataTable.tsx @@ -9,7 +9,6 @@ import { type DataUseTermsHistoryEntry } from '../../types/backend'; import { type ReferenceAccession, type ReferenceGenomesLightweightSchema, - type ReferenceName, } from '../../types/referencesGenomes'; import AkarInfo from '~icons/ri/information-line'; diff --git a/website/src/components/SequenceDetailsPage/SequenceDataUI.tsx b/website/src/components/SequenceDetailsPage/SequenceDataUI.tsx index fb5405e878..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 ReferenceName } 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'; diff --git a/website/src/components/SequenceDetailsPage/SequencesDisplay/SequenceContainer.spec.tsx b/website/src/components/SequenceDetailsPage/SequencesDisplay/SequenceContainer.spec.tsx index 02e79de65a..8001dc69c6 100644 --- a/website/src/components/SequenceDetailsPage/SequencesDisplay/SequenceContainer.spec.tsx +++ b/website/src/components/SequenceDetailsPage/SequencesDisplay/SequenceContainer.spec.tsx @@ -168,8 +168,9 @@ 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( { diff --git a/website/src/components/SequenceDetailsPage/SequencesDisplay/SequencesContainer.tsx b/website/src/components/SequenceDetailsPage/SequencesDisplay/SequencesContainer.tsx index cdbd761b7a..51bf035a4a 100644 --- a/website/src/components/SequenceDetailsPage/SequencesDisplay/SequencesContainer.tsx +++ b/website/src/components/SequenceDetailsPage/SequencesDisplay/SequencesContainer.tsx @@ -1,7 +1,7 @@ import { type Dispatch, type FC, type SetStateAction, useEffect, useState } from 'react'; import { SequencesViewer } from './SequenceViewer.tsx'; -import { type ReferenceGenomesLightweightSchema, type ReferenceName } from '../../../types/referencesGenomes.ts'; +import { type ReferenceGenomesLightweightSchema } from '../../../types/referencesGenomes.ts'; import type { ClientConfig } from '../../../types/runtimeConfig.ts'; import { getSegmentAndGeneInfo } from '../../../utils/getSegmentAndGeneInfo.tsx'; import { diff --git a/website/src/components/SequenceDetailsPage/getTableData.ts b/website/src/components/SequenceDetailsPage/getTableData.ts index c8e2b19502..40870f6d10 100644 --- a/website/src/components/SequenceDetailsPage/getTableData.ts +++ b/website/src/components/SequenceDetailsPage/getTableData.ts @@ -12,7 +12,7 @@ import { type InsertionCount, type MutationProportionCount, } from '../../types/lapis.ts'; -import { type ReferenceGenomes, type ReferenceName } from '../../types/referencesGenomes.ts'; +import { type ReferenceGenomes } from '../../types/referencesGenomes.ts'; import { parseUnixTimestamp } from '../../utils/parseUnixTimestamp.ts'; export type GetTableDataResult = { diff --git a/website/src/utils/getSegmentAndGeneInfo.spec.tsx b/website/src/utils/getSegmentAndGeneInfo.spec.tsx index 648069fa8e..9b9d7c4b5b 100644 --- a/website/src/utils/getSegmentAndGeneInfo.spec.tsx +++ b/website/src/utils/getSegmentAndGeneInfo.spec.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import { describe, expect, test } from 'vitest'; import { getSegmentAndGeneInfo } from './getSegmentAndGeneInfo.tsx'; @@ -119,24 +120,26 @@ describe('getSegmentAndGeneInfo', () => { const schema: ReferenceGenomesLightweightSchema = { segments: { segment1: { - references: ['ref1'], + references: ['CV-A16', 'CV-A10'], insdcAccessions: {}, genesByReference: { - ref1: ['gene1'], + 'CV-A16': ['gene1'], + 'CV-A10': ['gene1'], }, }, segment2: { - references: ['ref1'], + references: ['CV-A16', 'CV-A10'], insdcAccessions: {}, genesByReference: { - ref1: ['gene2'], + 'CV-A16': ['gene2'], + 'CV-A10': ['gene2'], }, }, }, }; const selectedReferences = { - segment1: 'ref1', + segment1: 'CV-A16', // segment2 not selected }; diff --git a/website/src/utils/getSegmentAndGeneInfo.tsx b/website/src/utils/getSegmentAndGeneInfo.tsx index 3ed88e9435..8fb548b2ec 100644 --- a/website/src/utils/getSegmentAndGeneInfo.tsx +++ b/website/src/utils/getSegmentAndGeneInfo.tsx @@ -42,7 +42,7 @@ export function getSegmentAndGeneInfo( // Add gene info if reference is selected if (selectedRef) { - const geneNames = segmentData.genesByReference[selectedRef] || []; + const geneNames = segmentData.genesByReference[selectedRef]; for (const geneName of geneNames) { geneInfos.push(getGeneInfoWithReference(geneName, refForNaming)); } From 84d32f429796fdab7aac34a0e6864b52dd1d91f2 Mon Sep 17 00:00:00 2001 From: Theo Sanderson Date: Mon, 15 Dec 2025 10:31:21 +0000 Subject: [PATCH 03/71] fixup --- .../getSegmentAndGeneDisplayNameMap.tsx | 4 ++-- .../DownloadDialog/DownloadDialog.spec.tsx | 5 +---- .../DownloadDialog/DownloadForm.tsx | 4 ++-- .../components/SearchPage/SearchForm.spec.tsx | 14 ++++---------- .../src/components/SearchPage/SearchForm.tsx | 2 +- .../SearchPage/SearchFullUI.spec.tsx | 5 +---- .../SearchPage/SegmentReferenceSelector.tsx | 7 +++++-- .../stillRequiresReferenceNameSelection.tsx | 2 +- .../SequenceDetailsPage/DataTable.tsx | 14 +++++--------- .../SequenceContainer.spec.tsx | 19 +++++++++++++------ .../SequenceDetailsPage/getTableData.ts | 17 ++++++++++++++--- website/src/config.ts | 13 ++++++++----- website/src/types/config.ts | 4 ++-- website/src/types/referencesGenomes.ts | 19 +++++++++++-------- 14 files changed, 70 insertions(+), 59 deletions(-) diff --git a/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx b/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx index 721c665910..3c8175960a 100644 --- a/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx +++ b/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx @@ -14,7 +14,7 @@ export function getSegmentAndGeneDisplayNameMap( // Add genes for this segment/reference const singleRef = segmentData.references[0]; - const genes = segmentData.genesByReference[singleRef]; + const genes = segmentData.genesByReference[singleRef] ?? []; for (const geneName of genes) { mappingEntries.push([geneName, geneName]); } @@ -25,7 +25,7 @@ export function getSegmentAndGeneDisplayNameMap( mappingEntries.push([lapisSegmentName, segmentName]); // Add genes for this segment/reference - const genes = segmentData.genesByReference[referenceName]; + const genes = segmentData.genesByReference[referenceName] ?? []; for (const geneName of genes) { const lapisGeneName = `${referenceName}-${geneName}`; mappingEntries.push([lapisGeneName, geneName]); diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx index 62d878718e..61f527486e 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx @@ -9,10 +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, -} from '../../../types/referencesGenomes.ts'; +import { type ReferenceGenomesLightweightSchema, type ReferenceAccession } from '../../../types/referencesGenomes.ts'; import { MetadataFilterSchema } from '../../../utils/search.ts'; const defaultAccession: ReferenceAccession = { diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx b/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx index 8589cdcc72..e691333a25 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx @@ -286,7 +286,7 @@ export function getSequenceNames( for (const segmentName of segments) { const segmentData = referenceGenomeLightweightSchema.segments[segmentName]; - const genes = segmentData.genesByReference[referenceName] || []; + const genes = segmentData.genesByReference[referenceName] ?? []; allGenes.push(...genes); } @@ -312,7 +312,7 @@ export function getSequenceNames( for (const segmentName of segments) { const segmentData = referenceGenomeLightweightSchema.segments[segmentName]; - const genes = segmentData.genesByReference[selectedReferenceName] || []; + const genes = segmentData.genesByReference[selectedReferenceName] ?? []; allGenes.push(...genes); } diff --git a/website/src/components/SearchPage/SearchForm.spec.tsx b/website/src/components/SearchPage/SearchForm.spec.tsx index 60b4e0aafc..b1519ba1c9 100644 --- a/website/src/components/SearchPage/SearchForm.spec.tsx +++ b/website/src/components/SearchPage/SearchForm.spec.tsx @@ -6,10 +6,7 @@ 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, -} 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 { @@ -154,21 +151,18 @@ describe('SearchForm', () => {
- {showMutationSearch && suborganismSegmentAndGeneInfo !== null && ( + {showMutationSearch && ( = ({ {segments.map((segmentName) => { - const hasSelection = selectedReferences[segmentName] !== undefined && selectedReferences[segmentName] !== null; + const hasSelection = selectedReferences[segmentName] !== null; return ( = ({ {segmentName} {hasSelection && ( - + )} diff --git a/website/src/components/SearchPage/stillRequiresReferenceNameSelection.tsx b/website/src/components/SearchPage/stillRequiresReferenceNameSelection.tsx index bed726521b..1c1efa04d6 100644 --- a/website/src/components/SearchPage/stillRequiresReferenceNameSelection.tsx +++ b/website/src/components/SearchPage/stillRequiresReferenceNameSelection.tsx @@ -6,7 +6,7 @@ export function stillRequiresReferenceNameSelection( ) { // Check if there are multiple references in any segment const hasMultipleReferences = Object.values(referenceGenomeLightweightSchema.segments).some( - (segmentData) => segmentData.references.length > 1 + (segmentData) => segmentData.references.length > 1, ); return hasMultipleReferences && selectedReferenceName === null; } diff --git a/website/src/components/SequenceDetailsPage/DataTable.tsx b/website/src/components/SequenceDetailsPage/DataTable.tsx index 6d760841c8..9b0f41118a 100644 --- a/website/src/components/SequenceDetailsPage/DataTable.tsx +++ b/website/src/components/SequenceDetailsPage/DataTable.tsx @@ -6,10 +6,7 @@ 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, -} from '../../types/referencesGenomes'; +import { type ReferenceAccession, type ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes'; import AkarInfo from '~icons/ri/information-line'; interface Props { @@ -46,9 +43,8 @@ const DataTableComponent: React.FC = ({ if (segmentReferences !== null) { for (const [segmentName, referenceName] of Object.entries(segmentReferences)) { const segmentData = referenceGenomeLightweightSchema.segments[segmentName]; - if (segmentData && segmentData.insdcAccessions[referenceName]) { - reference.push(segmentData.insdcAccessions[referenceName]); - } + const accession = segmentData.insdcAccessions[referenceName]; + reference.push(accession); } } const hasReferenceAccession = reference.filter((item) => item.insdcAccessionFull !== undefined).length > 0; @@ -71,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/SequencesDisplay/SequenceContainer.spec.tsx b/website/src/components/SequenceDetailsPage/SequencesDisplay/SequenceContainer.spec.tsx index 8001dc69c6..6ff96465aa 100644 --- a/website/src/components/SequenceDetailsPage/SequencesDisplay/SequenceContainer.spec.tsx +++ b/website/src/components/SequenceDetailsPage/SequencesDisplay/SequenceContainer.spec.tsx @@ -5,7 +5,10 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { SequencesContainer } from './SequencesContainer.tsx'; import { mockRequest, testConfig, testOrganism } from '../../../../vitest.setup.ts'; -import type { ReferenceGenomesLightweightSchema } from '../../../types/referencesGenomes.ts'; +import type { + ReferenceAccession, + ReferenceGenomesLightweightSchema, +} from '../../../types/referencesGenomes.ts'; vi.mock('../../config', () => ({ getLapisUrl: vi.fn().mockReturnValue('http://lapis.dummy'), @@ -54,7 +57,14 @@ function renderSingleReferenceSequenceViewer({ nucleotideSegmentNames: string[]; genes: string[]; }) { - const segments: Record }> = {}; + const segments: Record< + string, + { + references: string[]; + insdcAccessions: Record; + genesByReference: Record; + } + > = {}; const segmentReferences: Record = {}; for (const segmentName of nucleotideSegmentNames) { @@ -66,10 +76,7 @@ function renderSingleReferenceSequenceViewer({ segmentReferences[segmentName] = 'ref1'; } - renderSequenceViewer( - { segments }, - segmentReferences, - ); + renderSequenceViewer({ segments }, segmentReferences); } const multiSegmentName = 'main2'; diff --git a/website/src/components/SequenceDetailsPage/getTableData.ts b/website/src/components/SequenceDetailsPage/getTableData.ts index 40870f6d10..2c0debc31a 100644 --- a/website/src/components/SequenceDetailsPage/getTableData.ts +++ b/website/src/components/SequenceDetailsPage/getTableData.ts @@ -54,7 +54,12 @@ export async function getTableData( }), ) .andThen((data) => { - const segmentReferencesResult = getSegmentReferences(data.details, schema, referenceGenomes, accessionVersion); + const segmentReferencesResult = getSegmentReferences( + data.details, + schema, + referenceGenomes, + accessionVersion, + ); if (segmentReferencesResult.isErr()) { return err(segmentReferencesResult.error); } @@ -344,7 +349,10 @@ function computeSequenceDisplayName( return originalSequenceName; } -function deletionsToCommaSeparatedString(mutationData: MutationProportionCount[], segmentReferences: Record | null) { +function deletionsToCommaSeparatedString( + mutationData: MutationProportionCount[], + segmentReferences: Record | null, +) { const segmentPositions = new Map(); mutationData .filter((m) => m.mutationTo === '-') @@ -393,7 +401,10 @@ function deletionsToCommaSeparatedString(mutationData: MutationProportionCount[] .join(', '); } -function insertionsToCommaSeparatedString(insertionData: InsertionCount[], segmentReferences: Record | null) { +function insertionsToCommaSeparatedString( + insertionData: InsertionCount[], + segmentReferences: Record | null, +) { return insertionData .map((insertion) => { const sequenceDisplayName = computeSequenceDisplayName(insertion.sequenceName, segmentReferences); diff --git a/website/src/config.ts b/website/src/config.ts index eb30559e1b..63597a0161 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -283,11 +283,14 @@ export function getReferenceGenomes(organism: string): SegmentFirstReferenceGeno export const getReferenceGenomeLightweightSchema = (organism: string): ReferenceGenomesLightweightSchema => { const referenceGenomes = getReferenceGenomes(organism); - const segments: Record; - genesByReference: Record; - }> = {}; + 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)) { diff --git a/website/src/types/config.ts b/website/src/types/config.ts index 98f3078919..f966a3b546 100644 --- a/website/src/types/config.ts +++ b/website/src/types/config.ts @@ -74,8 +74,8 @@ export const metadata = z.object({ order: z.number().optional(), orderOnDetailsPage: z.number().optional(), includeInDownloadsByDefault: z.boolean().optional(), - onlyForReferenceName: z.string().optional(), // DEPRECATED: Use onlyForReference instead - onlyForReference: z.string().optional(), // NEW: Scopes field to a specific reference (replaces onlyForReferenceName) + onlyForReferenceName: z.string().optional(), // DEPRECATED: Use onlyForReference instead + onlyForReference: z.string().optional(), // NEW: Scopes field to a specific reference (replaces onlyForReferenceName) }); export const inputFieldOption = z.object({ diff --git a/website/src/types/referencesGenomes.ts b/website/src/types/referencesGenomes.ts index eacce7ace8..bfb5732927 100644 --- a/website/src/types/referencesGenomes.ts +++ b/website/src/types/referencesGenomes.ts @@ -30,8 +30,8 @@ export const segmentFirstReferenceGenomes = z.record( sequence: z.string(), insdcAccessionFull: z.string().optional(), genes: z.record(z.string(), z.object({ sequence: z.string() })).optional(), - }) - ) + }), + ), ); export type SegmentFirstReferenceGenomes = z.infer; @@ -40,10 +40,13 @@ export type ReferenceGenomes = SegmentFirstReferenceGenomes; // Lightweight schema for segment-first mode export type ReferenceGenomesLightweightSchema = { - segments: Record; - // Genes available for each reference in this segment - genesByReference: Record; - }>; + segments: Record< + SegmentName, + { + references: ReferenceName[]; + insdcAccessions: Record; + // Genes available for each reference in this segment + genesByReference: Record; + } + >; }; From 4d1fb0f22370d8151473fa0f019fe68a79f734e3 Mon Sep 17 00:00:00 2001 From: Theo Sanderson Date: Mon, 15 Dec 2025 10:57:58 +0000 Subject: [PATCH 04/71] update --- .../loculus/templates/_merged-reference-genomes.tpl | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/kubernetes/loculus/templates/_merged-reference-genomes.tpl b/kubernetes/loculus/templates/_merged-reference-genomes.tpl index b1cd416536..f92b938ae9 100644 --- a/kubernetes/loculus/templates/_merged-reference-genomes.tpl +++ b/kubernetes/loculus/templates/_merged-reference-genomes.tpl @@ -3,12 +3,16 @@ {{- $lapisNucleotideSequences := list -}} {{- $lapisGenes := list -}} +{{/* Handle empty reference genomes */}} +{{- if or (not $segmentFirstConfig) (eq (len $segmentFirstConfig) 0) -}} +{{- $result := dict "nucleotideSequences" (list) "genes" (list) -}} +{{- $result | toYaml -}} +{{- else -}} + {{/* Extract all unique reference names from the first segment */}} {{- $referenceNames := list -}} -{{- if $segmentFirstConfig -}} - {{- $firstSegment := first (values $segmentFirstConfig) -}} - {{- $referenceNames = keys $firstSegment -}} -{{- end -}} +{{- $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 -}} @@ -70,6 +74,7 @@ {{- $result := dict "nucleotideSequences" $lapisNucleotideSequences "genes" $lapisGenes -}} {{- $result | toYaml -}} {{- end -}} +{{- end -}} {{- define "loculus.extractUniqueRawNucleotideSequenceNames" -}} From 8bce4f65af6a1edc2edbbb624ca2fc74b8a05089 Mon Sep 17 00:00:00 2001 From: Theo Sanderson Date: Mon, 15 Dec 2025 11:39:49 +0000 Subject: [PATCH 05/71] fixup --- .../loculus/templates/_merged-reference-genomes.tpl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kubernetes/loculus/templates/_merged-reference-genomes.tpl b/kubernetes/loculus/templates/_merged-reference-genomes.tpl index f92b938ae9..58d6d58fac 100644 --- a/kubernetes/loculus/templates/_merged-reference-genomes.tpl +++ b/kubernetes/loculus/templates/_merged-reference-genomes.tpl @@ -42,10 +42,10 @@ {{- end -}} {{- else -}} - {{/* Multi-reference mode - prefix with reference name */}} +{{/* Multi-reference mode - prefix with reference name */}} - {{/* Process each reference */}} - {{- range $refName := $referenceNames -}} +{{/* Process each reference */}} +{{- range $refName := $referenceNames -}} {{/* Process each segment */}} {{- range $segmentName, $refMap := $segmentFirstConfig -}} {{- $refData := index $refMap $refName -}} @@ -67,7 +67,7 @@ {{- end -}} {{- end -}} {{- end -}} - {{- end -}} +{{- end -}} {{- end -}} From 3faee2555575a012e431da710db1d9046934f0f5 Mon Sep 17 00:00:00 2001 From: Theo Sanderson Date: Mon, 15 Dec 2025 11:49:45 +0000 Subject: [PATCH 06/71] up --- kubernetes/loculus/values.yaml | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/kubernetes/loculus/values.yaml b/kubernetes/loculus/values.yaml index bd9ad101dc..8667572975 100644 --- a/kubernetes/loculus/values.yaml +++ b/kubernetes/loculus/values.yaml @@ -1402,33 +1402,6 @@ defaultOrganisms: 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]]" - NP: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/NP.fasta]]" - VP35: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/VP35.fasta]]" - VP40: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/VP40.fasta]]" - GP: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/GP.fasta]]" - ssGP: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/ssGP.fasta]]" - sGP: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/sGP.fasta]]" - VP30: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/VP30.fasta]]" - VP24: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/VP24.fasta]]" - L: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/ebola-sudan/L.fasta]]" west-nile: <<: *defaultOrganismConfig schema: From b5b7a3a165850bc1f718d5e5246542e54db349e4 Mon Sep 17 00:00:00 2001 From: Theo Sanderson Date: Mon, 15 Dec 2025 11:53:38 +0000 Subject: [PATCH 07/71] fixup --- kubernetes/loculus/values.yaml | 52 ++++------------------------------ 1 file changed, 6 insertions(+), 46 deletions(-) diff --git a/kubernetes/loculus/values.yaml b/kubernetes/loculus/values.yaml index 8667572975..a4490d357f 100644 --- a/kubernetes/loculus/values.yaml +++ b/kubernetes/loculus/values.yaml @@ -1489,39 +1489,6 @@ defaultOrganisms: 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]]" - 2K: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/2K.fasta]]" - NS1: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS1.fasta]]" - NS2A: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS2A.fasta]]" - NS2B: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS2B.fasta]]" - NS3: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS3.fasta]]" - NS4A: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS4A.fasta]]" - NS4B: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS4B.fasta]]" - NS5: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/NS5.fasta]]" - capsid: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/capsid.fasta]]" - env: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/env.fasta]]" - prM: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/west-nile/prM.fasta]]" dummy-organism: schema: submissionDataTypes: *defaultSubmissionDataTypes @@ -1881,27 +1848,20 @@ defaultOrganisms: genes: RdRp: sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/RdRp.fasta]]" - GPC: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/GPC.fasta]]" - NP: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/NP.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 - RdRp: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/RdRp.fasta]]" - GPC: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/GPC.fasta]]" - NP: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/NP.fasta]]" + genes: + NP: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/NP.fasta]]" enteroviruses: <<: *defaultOrganismConfig enabled: true From eb3d998c4ab58e928eedf30d7b9af4b02c45ddec Mon Sep 17 00:00:00 2001 From: Theo Sanderson Date: Mon, 15 Dec 2025 12:08:59 +0000 Subject: [PATCH 08/71] fixup --- kubernetes/loculus/values.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/kubernetes/loculus/values.yaml b/kubernetes/loculus/values.yaml index a4490d357f..cc66a56542 100644 --- a/kubernetes/loculus/values.yaml +++ b/kubernetes/loculus/values.yaml @@ -1868,7 +1868,7 @@ defaultOrganisms: schema: <<: *schema organismName: "Enterovirus" - nucleotideSequences: [CV-A16, CV-A10, EV-A71, EV-D68] + nucleotideSequences: [CV-A16-main, CV-A10-main, EV-A71-main, EV-D68-main] image: "/images/organisms/enterovirus.jpg" linkOuts: - name: "Nextclade (CV-A16)" @@ -1892,7 +1892,7 @@ defaultOrganisms: includeInDownloadsByDefault: true preprocessing: args: - segment: CV-A16 + segment: CV-A16-main inputs: {input: nextclade.clade} - <<: *evMetadataAdd name: clade_cv_a10 @@ -1900,7 +1900,7 @@ defaultOrganisms: onlyForSuborganism: CV-A10 preprocessing: args: - segment: CV-A10 + segment: CV-A10-main inputs: {input: nextclade.clade} - <<: *evMetadataAdd name: clade_ev_a71 @@ -1908,7 +1908,7 @@ defaultOrganisms: onlyForSuborganism: EV-A71 preprocessing: args: - segment: EV-A71 + segment: EV-A71-main inputs: {input: nextclade.clade} - <<: *evMetadataAdd name: clade_ev_d68 @@ -1916,7 +1916,7 @@ defaultOrganisms: onlyForSuborganism: EV-D68 preprocessing: args: - segment: EV-D68 + segment: EV-D68-main inputs: {input: nextclade.clade} - name: genotype displayName: Genotype From 94e803d4e83dfe19929f42edd74352e5f83f8b6f Mon Sep 17 00:00:00 2001 From: Theo Sanderson Date: Mon, 15 Dec 2025 14:44:40 +0000 Subject: [PATCH 09/71] upd --- .../src/loculus_preprocessing/nextclade.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/preprocessing/nextclade/src/loculus_preprocessing/nextclade.py b/preprocessing/nextclade/src/loculus_preprocessing/nextclade.py index d0554d6964..0dbfe58170 100644 --- a/preprocessing/nextclade/src/loculus_preprocessing/nextclade.py +++ b/preprocessing/nextclade/src/loculus_preprocessing/nextclade.py @@ -1,4 +1,5 @@ import csv +import dataclasses import json import logging import os @@ -752,6 +753,12 @@ def enrich_with_nextclade( # noqa: PLR0914 # Add aligned sequences to aligned_nucleotide_sequences # Modifies aligned_nucleotide_sequences in place + # For minimizer classification, append "-main" to segment name for backend submission + segment_output_name = ( + f"{segment}-main" + if config.segment_classification_method == SegmentClassificationMethod.MINIMIZER + else segment + ) aligned_nucleotide_sequences = load_aligned_nuc_sequences( result_dir_seg, name, aligned_nucleotide_sequences ) @@ -761,11 +768,15 @@ def enrich_with_nextclade( # noqa: PLR0914 nextclade_metadata = parse_nextclade_json( result_dir_seg, nextclade_metadata, name, unaligned_nucleotide_sequences ) # this includes the "annotation" field + # Pass segment_output_name to parse_nextclade_tsv by temporarily modifying sequence_and_dataset + sequence_and_dataset_copy = dataclasses.replace( + sequence_and_dataset, name=segment_output_name + ) amino_acid_insertions, nucleotide_insertions = parse_nextclade_tsv( amino_acid_insertions, nucleotide_insertions, result_dir_seg, - sequence_and_dataset, + sequence_and_dataset_copy, ) return { From 0bf193e691f0fa2f19e9926397772050fd8a0eb2 Mon Sep 17 00:00:00 2001 From: Theo Sanderson Date: Mon, 15 Dec 2025 14:54:58 +0000 Subject: [PATCH 10/71] up --- kubernetes/loculus/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/loculus/values.yaml b/kubernetes/loculus/values.yaml index cc66a56542..b7032a5020 100644 --- a/kubernetes/loculus/values.yaml +++ b/kubernetes/loculus/values.yaml @@ -1868,7 +1868,7 @@ defaultOrganisms: schema: <<: *schema organismName: "Enterovirus" - nucleotideSequences: [CV-A16-main, CV-A10-main, EV-A71-main, EV-D68-main] + nucleotideSequences: [CV-A16, CV-A10, EV-A71, EV-D68] image: "/images/organisms/enterovirus.jpg" linkOuts: - name: "Nextclade (CV-A16)" From d3e9f0ea3f6a12a5895673a2f2a4f9ced429c39a Mon Sep 17 00:00:00 2001 From: Theo Sanderson Date: Mon, 15 Dec 2025 16:06:09 +0000 Subject: [PATCH 11/71] u --- .../src/loculus_preprocessing/nextclade.py | 13 +------- .../src/loculus_preprocessing/prepro.py | 30 +++++++++++++++---- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/preprocessing/nextclade/src/loculus_preprocessing/nextclade.py b/preprocessing/nextclade/src/loculus_preprocessing/nextclade.py index 0dbfe58170..d0554d6964 100644 --- a/preprocessing/nextclade/src/loculus_preprocessing/nextclade.py +++ b/preprocessing/nextclade/src/loculus_preprocessing/nextclade.py @@ -1,5 +1,4 @@ import csv -import dataclasses import json import logging import os @@ -753,12 +752,6 @@ def enrich_with_nextclade( # noqa: PLR0914 # Add aligned sequences to aligned_nucleotide_sequences # Modifies aligned_nucleotide_sequences in place - # For minimizer classification, append "-main" to segment name for backend submission - segment_output_name = ( - f"{segment}-main" - if config.segment_classification_method == SegmentClassificationMethod.MINIMIZER - else segment - ) aligned_nucleotide_sequences = load_aligned_nuc_sequences( result_dir_seg, name, aligned_nucleotide_sequences ) @@ -768,15 +761,11 @@ def enrich_with_nextclade( # noqa: PLR0914 nextclade_metadata = parse_nextclade_json( result_dir_seg, nextclade_metadata, name, unaligned_nucleotide_sequences ) # this includes the "annotation" field - # Pass segment_output_name to parse_nextclade_tsv by temporarily modifying sequence_and_dataset - sequence_and_dataset_copy = dataclasses.replace( - sequence_and_dataset, name=segment_output_name - ) amino_acid_insertions, nucleotide_insertions = parse_nextclade_tsv( amino_acid_insertions, nucleotide_insertions, result_dir_seg, - sequence_and_dataset_copy, + sequence_and_dataset, ) return { diff --git a/preprocessing/nextclade/src/loculus_preprocessing/prepro.py b/preprocessing/nextclade/src/loculus_preprocessing/prepro.py index a52ecf03ae..93fe502786 100644 --- a/preprocessing/nextclade/src/loculus_preprocessing/prepro.py +++ b/preprocessing/nextclade/src/loculus_preprocessing/prepro.py @@ -239,18 +239,26 @@ def processed_entry_no_alignment( # noqa: PLR0913, PLR0917 nucleotide_insertions: dict[SequenceName, 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, @@ -447,17 +455,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)), @@ -495,6 +512,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, ) From 2de515104eeb4655484d06ef872de267d47126bb Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:12:09 +0100 Subject: [PATCH 12/71] format --- preprocessing/nextclade/src/loculus_preprocessing/prepro.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/preprocessing/nextclade/src/loculus_preprocessing/prepro.py b/preprocessing/nextclade/src/loculus_preprocessing/prepro.py index 93fe502786..abef2d040b 100644 --- a/preprocessing/nextclade/src/loculus_preprocessing/prepro.py +++ b/preprocessing/nextclade/src/loculus_preprocessing/prepro.py @@ -470,7 +470,9 @@ def transform_segment_dict(d: dict) -> dict: unalignedNucleotideSequences=transform_segment_dict( unprocessed.unalignedNucleotideSequences ), - alignedNucleotideSequences=transform_segment_dict(unprocessed.alignedNucleotideSequences), + alignedNucleotideSequences=transform_segment_dict( + unprocessed.alignedNucleotideSequences + ), nucleotideInsertions=transform_segment_dict(unprocessed.nucleotideInsertions), alignedAminoAcidSequences=unprocessed.alignedAminoAcidSequences, aminoAcidInsertions=unprocessed.aminoAcidInsertions, From 30f9586cda8e92cad0cbf478d53b1305f791e8f2 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:13:05 +0100 Subject: [PATCH 13/71] remove duplication --- kubernetes/loculus/values.yaml | 36 ---------------------------------- 1 file changed, 36 deletions(-) diff --git a/kubernetes/loculus/values.yaml b/kubernetes/loculus/values.yaml index b7032a5020..a9a5227b1e 100644 --- a/kubernetes/loculus/values.yaml +++ b/kubernetes/loculus/values.yaml @@ -1600,42 +1600,6 @@ defaultOrganisms: 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" From 22b423e7a672fed6687d826d061df7099a1d2acf Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:15:06 +0100 Subject: [PATCH 14/71] delete unused file --- website/src/components/SearchPage/ReferenceNameSelector.tsx | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 website/src/components/SearchPage/ReferenceNameSelector.tsx diff --git a/website/src/components/SearchPage/ReferenceNameSelector.tsx b/website/src/components/SearchPage/ReferenceNameSelector.tsx deleted file mode 100644 index 361297ea98..0000000000 --- a/website/src/components/SearchPage/ReferenceNameSelector.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export for backward compatibility -export { SuborganismSelector as ReferenceNameSelector } from './SuborganismSelector.tsx'; From 64dade139ab4b1d49270f98c4da9ff65bca1ad0c Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:34:53 +0100 Subject: [PATCH 15/71] fix order of reference and segments --- .../templates/_merged-reference-genomes.tpl | 54 +++++++++---------- kubernetes/loculus/values.yaml | 8 +-- .../src/loculus_preprocessing/prepro.py | 32 +++-------- 3 files changed, 35 insertions(+), 59 deletions(-) diff --git a/kubernetes/loculus/templates/_merged-reference-genomes.tpl b/kubernetes/loculus/templates/_merged-reference-genomes.tpl index 58d6d58fac..940d4a59bb 100644 --- a/kubernetes/loculus/templates/_merged-reference-genomes.tpl +++ b/kubernetes/loculus/templates/_merged-reference-genomes.tpl @@ -9,18 +9,11 @@ {{- $result | toYaml -}} {{- else -}} -{{/* Extract all unique reference names from the first segment */}} -{{- $referenceNames := list -}} -{{- $firstSegment := first (values $segmentFirstConfig) -}} -{{- $referenceNames = keys $firstSegment -}} +{{- range $segmentName, $refMap := $segmentFirstConfig -}} + {{- if eq (len $refMap) 1 -}} + {{/* Single reference mode - no suffix */}} + {{- $singleRef := first $refMap -}} -{{/* 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 */}} @@ -42,31 +35,34 @@ {{- 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 */}} +{{/* Multi-reference mode - suffix with reference name */}} + {{- range $refName, $refData := $refMap -}} + {{- if $refData -}} + {{- if eq len($segmentFirstConfig) 1-}} + {{/* Add nucleotide sequence without segmentName */}} {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict - "name" (printf "%s-%s" $refName $segmentName) + "name" $refName "sequence" $refData.sequence ) -}} + {{- else -}} + {{/* Add nucleotide sequence with reference suffix */}} + {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict + "name" (printf "%s-%s" $segmentName $refName) + "sequence" $refData.sequence + ) -}} + {{- end -}} - {{/* 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 -}} + {{/* Add genes with reference prefix if present */}} + {{- if $refData.genes -}} + {{- range $geneName, $geneData := $refData.genes -}} + {{- $lapisGenes = append $lapisGenes (dict + "name" (printf "%s-%s" $geneName $refName) + "sequence" $geneData.sequence + ) -}} {{- end -}} {{- end -}} {{- end -}} + {{- end -}} {{- end -}} {{- end -}} diff --git a/kubernetes/loculus/values.yaml b/kubernetes/loculus/values.yaml index a9a5227b1e..a59f8079b9 100644 --- a/kubernetes/loculus/values.yaml +++ b/kubernetes/loculus/values.yaml @@ -1856,7 +1856,7 @@ defaultOrganisms: includeInDownloadsByDefault: true preprocessing: args: - segment: CV-A16-main + segment: CV-A16 inputs: {input: nextclade.clade} - <<: *evMetadataAdd name: clade_cv_a10 @@ -1864,7 +1864,7 @@ defaultOrganisms: onlyForSuborganism: CV-A10 preprocessing: args: - segment: CV-A10-main + segment: CV-A10 inputs: {input: nextclade.clade} - <<: *evMetadataAdd name: clade_ev_a71 @@ -1872,7 +1872,7 @@ defaultOrganisms: onlyForSuborganism: EV-A71 preprocessing: args: - segment: EV-A71-main + segment: EV-A71 inputs: {input: nextclade.clade} - <<: *evMetadataAdd name: clade_ev_d68 @@ -1880,7 +1880,7 @@ defaultOrganisms: onlyForSuborganism: EV-D68 preprocessing: args: - segment: EV-D68-main + segment: EV-D68 inputs: {input: nextclade.clade} - name: genotype displayName: Genotype diff --git a/preprocessing/nextclade/src/loculus_preprocessing/prepro.py b/preprocessing/nextclade/src/loculus_preprocessing/prepro.py index abef2d040b..a52ecf03ae 100644 --- a/preprocessing/nextclade/src/loculus_preprocessing/prepro.py +++ b/preprocessing/nextclade/src/loculus_preprocessing/prepro.py @@ -239,26 +239,18 @@ def processed_entry_no_alignment( # noqa: PLR0913, PLR0917 nucleotide_insertions: dict[SequenceName, 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=transform_segment_dict( - unprocessed.unalignedNucleotideSequences - ), + unalignedNucleotideSequences=unprocessed.unalignedNucleotideSequences, alignedNucleotideSequences=aligned_nucleotide_sequences, nucleotideInsertions=nucleotide_insertions, alignedAminoAcidSequences=aligned_aminoacid_sequences, aminoAcidInsertions=amino_acid_insertions, - sequenceNameToFastaId=transform_segment_dict(sequenceNameToFastaId), + sequenceNameToFastaId=sequenceNameToFastaId, ), errors=errors, warnings=warnings, @@ -455,28 +447,17 @@ 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=transform_segment_dict( - unprocessed.unalignedNucleotideSequences - ), - alignedNucleotideSequences=transform_segment_dict( - unprocessed.alignedNucleotideSequences - ), - nucleotideInsertions=transform_segment_dict(unprocessed.nucleotideInsertions), + unalignedNucleotideSequences=unprocessed.unalignedNucleotideSequences, + alignedNucleotideSequences=unprocessed.alignedNucleotideSequences, + nucleotideInsertions=unprocessed.nucleotideInsertions, alignedAminoAcidSequences=unprocessed.alignedAminoAcidSequences, aminoAcidInsertions=unprocessed.aminoAcidInsertions, - sequenceNameToFastaId=transform_segment_dict(unprocessed.sequenceNameToFastaId), + sequenceNameToFastaId=unprocessed.sequenceNameToFastaId, ), errors=list(set(unprocessed.errors + iupac_errors + alignment_errors + metadata_errors)), warnings=list(set(unprocessed.warnings + alignment_warnings + metadata_warnings)), @@ -514,7 +495,6 @@ 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, ) From b12bbecf06f1a0f45e379ad252f03f5a5b8dbda8 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:41:51 +0100 Subject: [PATCH 16/71] fixup --- .../templates/_merged-reference-genomes.tpl | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/kubernetes/loculus/templates/_merged-reference-genomes.tpl b/kubernetes/loculus/templates/_merged-reference-genomes.tpl index 940d4a59bb..3956dec2ab 100644 --- a/kubernetes/loculus/templates/_merged-reference-genomes.tpl +++ b/kubernetes/loculus/templates/_merged-reference-genomes.tpl @@ -12,23 +12,22 @@ {{- range $segmentName, $refMap := $segmentFirstConfig -}} {{- if eq (len $refMap) 1 -}} {{/* Single reference mode - no suffix */}} - {{- $singleRef := first $refMap -}} - - {{- $refData := index $refMap $singleRef -}} - {{- if $refData -}} - {{/* Add nucleotide sequence */}} - {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict - "name" $segmentName - "sequence" $refData.sequence - ) -}} + {{- range $refName, $refData := $refMap -}} + {{- 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 - ) -}} + {{/* 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 -}} @@ -38,7 +37,7 @@ {{/* Multi-reference mode - suffix with reference name */}} {{- range $refName, $refData := $refMap -}} {{- if $refData -}} - {{- if eq len($segmentFirstConfig) 1-}} + {{- if eq (len $segmentFirstConfig) 1 -}} {{/* Add nucleotide sequence without segmentName */}} {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict "name" $refName @@ -70,7 +69,6 @@ {{- $result := dict "nucleotideSequences" $lapisNucleotideSequences "genes" $lapisGenes -}} {{- $result | toYaml -}} {{- end -}} -{{- end -}} {{- define "loculus.extractUniqueRawNucleotideSequenceNames" -}} From a68c52f0e22900a83a4edd4027275bb06a311479 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:54:06 +0100 Subject: [PATCH 17/71] change prepro config as well --- preprocessing/nextclade/src/loculus_preprocessing/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/preprocessing/nextclade/src/loculus_preprocessing/config.py b/preprocessing/nextclade/src/loculus_preprocessing/config.py index faca4f991d..2726f3eac2 100644 --- a/preprocessing/nextclade/src/loculus_preprocessing/config.py +++ b/preprocessing/nextclade/src/loculus_preprocessing/config.py @@ -83,6 +83,9 @@ class NextcladeSequenceAndDataset(BaseModel): genes: list[str] = Field(default_factory=list) +type SegmentName = str + + class Config(BaseModel): log_level: str = "DEBUG" keep_tmp_dir: bool = False From 8b687eea690014067161b4533d801748f13aa69f Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Fri, 9 Jan 2026 20:16:33 +0100 Subject: [PATCH 18/71] fixup --- preprocessing/nextclade/src/loculus_preprocessing/config.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/preprocessing/nextclade/src/loculus_preprocessing/config.py b/preprocessing/nextclade/src/loculus_preprocessing/config.py index 2726f3eac2..faca4f991d 100644 --- a/preprocessing/nextclade/src/loculus_preprocessing/config.py +++ b/preprocessing/nextclade/src/loculus_preprocessing/config.py @@ -83,9 +83,6 @@ class NextcladeSequenceAndDataset(BaseModel): genes: list[str] = Field(default_factory=list) -type SegmentName = str - - class Config(BaseModel): log_level: str = "DEBUG" keep_tmp_dir: bool = False From 432a4e42f8b7eaa89b5826fe10c83b1f128aafde Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Fri, 9 Jan 2026 20:18:27 +0100 Subject: [PATCH 19/71] feat(website): format --- website/.prettierrc | 32 +++++++++---------- website/README.md | 26 +++++++-------- .../SequenceContainer.spec.tsx | 5 +-- website/src/pages/-testpage/index.mdx | 8 ++--- website/tests/config/README.md | 2 +- 5 files changed, 34 insertions(+), 39 deletions(-) diff --git a/website/.prettierrc b/website/.prettierrc index b4af6d7013..a1e3b6cf86 100644 --- a/website/.prettierrc +++ b/website/.prettierrc @@ -1,18 +1,18 @@ { - "printWidth": 120, - "tabWidth": 4, - "trailingComma": "all", - "semi": true, - "jsxSingleQuote": true, - "singleQuote": true, - "quoteProps": "consistent", - "plugins": ["prettier-plugin-astro"], - "overrides": [ - { - "files": "*.astro", - "options": { - "parser": "astro" - } - } - ] + "printWidth": 120, + "tabWidth": 4, + "trailingComma": "all", + "semi": true, + "jsxSingleQuote": true, + "singleQuote": true, + "quoteProps": "consistent", + "plugins": ["prettier-plugin-astro"], + "overrides": [ + { + "files": "*.astro", + "options": { + "parser": "astro" + } + } + ] } diff --git a/website/README.md b/website/README.md index 4da8d28dc4..75114a3333 100644 --- a/website/README.md +++ b/website/README.md @@ -9,11 +9,11 @@ In order to run the website locally you will need to install [nodejs](https://no ### Local Development -- Set up your `.env` file, e.g. by copying `.env.example` with `cp .env.example .env` -- Install packages: `npm ci` (`ci` as opposed to `install` makes sure to install the exact versions specified in `package-lock.json`) -- Generate config files for local testing (requires Helm installed): `../generate_local_test_config.sh`. If you are not running the backend locally, run `../generate_local_test_config.sh --from-live` to point to the backend from the live server (preview of the `main` branch) or `../generate_local_test_config.sh --from-live --live-host main.loculus.org` to specify a particular host which can also be a preview. -- Run `npm run start` to start a local development server with hot reloading. -- Run `npm run format-fast` to format the code. +- Set up your `.env` file, e.g. by copying `.env.example` with `cp .env.example .env` +- Install packages: `npm ci` (`ci` as opposed to `install` makes sure to install the exact versions specified in `package-lock.json`) +- Generate config files for local testing (requires Helm installed): `../generate_local_test_config.sh`. If you are not running the backend locally, run `../generate_local_test_config.sh --from-live` to point to the backend from the live server (preview of the `main` branch) or `../generate_local_test_config.sh --from-live --live-host main.loculus.org` to specify a particular host which can also be a preview. +- Run `npm run start` to start a local development server with hot reloading. +- Run `npm run format-fast` to format the code. ### Unit Tests @@ -32,9 +32,9 @@ See `.env.docker` for the required variables. Furthermore, the website requires config files that need to be present at runtime in the directory specified in the `CONFIG_DIR` environment variable: -- `website_config.json`: Contains configuration on the underlying organism. It's similar to the database config file that LAPIS uses. -- `reference_genomes.json`: Defines names for segments of the genome and amino acids. It's equal to the file that LAPIS uses. -- `runtime_config.json`: Contains configuration that specific for a deployed instance of the website. +- `website_config.json`: Contains configuration on the underlying organism. It's similar to the database config file that LAPIS uses. +- `reference_genomes.json`: Defines names for segments of the genome and amino acids. It's equal to the file that LAPIS uses. +- `runtime_config.json`: Contains configuration that specific for a deployed instance of the website. Check our tests and examples for working config files. @@ -47,17 +47,17 @@ If the environment variable LOG_DIR is set, it will also store them in `LOG_DIR/ ### Editor -- [Astro](https://docs.astro.build/en/editor-setup/) +- [Astro](https://docs.astro.build/en/editor-setup/) ### Setup -- Install node version from `.nvmrc` with `nvm install` +- Install node version from `.nvmrc` with `nvm install` ### General tips -- Available scripts can be browsed in [`package.json`](./package.json) or by running `npm run` -- For VS code, use the ESlint extension which must be configured with `"eslint.workingDirectories": ["./website"],` in the settings.json -- Tips & Tricks for using icons from MUI https://mui.com/material-ui/guides/minimizing-bundle-size/ +- Available scripts can be browsed in [`package.json`](./package.json) or by running `npm run` +- For VS code, use the ESlint extension which must be configured with `"eslint.workingDirectories": ["./website"],` in the settings.json +- Tips & Tricks for using icons from MUI https://mui.com/material-ui/guides/minimizing-bundle-size/ ### Codemods diff --git a/website/src/components/SequenceDetailsPage/SequencesDisplay/SequenceContainer.spec.tsx b/website/src/components/SequenceDetailsPage/SequencesDisplay/SequenceContainer.spec.tsx index 6ff96465aa..e93515d749 100644 --- a/website/src/components/SequenceDetailsPage/SequencesDisplay/SequenceContainer.spec.tsx +++ b/website/src/components/SequenceDetailsPage/SequencesDisplay/SequenceContainer.spec.tsx @@ -5,10 +5,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { SequencesContainer } from './SequencesContainer.tsx'; import { mockRequest, testConfig, testOrganism } from '../../../../vitest.setup.ts'; -import type { - ReferenceAccession, - ReferenceGenomesLightweightSchema, -} from '../../../types/referencesGenomes.ts'; +import type { ReferenceAccession, ReferenceGenomesLightweightSchema } from '../../../types/referencesGenomes.ts'; vi.mock('../../config', () => ({ getLapisUrl: vi.fn().mockReturnValue('http://lapis.dummy'), diff --git a/website/src/pages/-testpage/index.mdx b/website/src/pages/-testpage/index.mdx index 3e661b4752..3520bf7b30 100644 --- a/website/src/pages/-testpage/index.mdx +++ b/website/src/pages/-testpage/index.mdx @@ -1,17 +1,15 @@ --- layout: ../../layouts/MdLayout.astro -title: "Governance" +title: 'Governance' --- - # Governance - Governance page goes here Bold -_italic_ etc. +_italic_ etc. ``` code blocks @@ -20,4 +18,4 @@ are great and `commands` -[Link](http://google.com) \ No newline at end of file +[Link](http://google.com) diff --git a/website/tests/config/README.md b/website/tests/config/README.md index 5aadd310e7..b72e28f35c 100644 --- a/website/tests/config/README.md +++ b/website/tests/config/README.md @@ -1,3 +1,3 @@ Config files are written to this directory by the deploy script. -[TODO (https://github.com/loculus-project/loculus/issues/5541): use a different location] \ No newline at end of file +[TODO (https://github.com/loculus-project/loculus/issues/5541): use a different location] From 0d6795d780df00d2c86bf285f6bdfc9c797462aa Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Fri, 9 Jan 2026 21:04:14 +0100 Subject: [PATCH 20/71] testing --- kubernetes/loculus/values.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kubernetes/loculus/values.yaml b/kubernetes/loculus/values.yaml index a59f8079b9..74fb11327b 100644 --- a/kubernetes/loculus/values.yaml +++ b/kubernetes/loculus/values.yaml @@ -1627,7 +1627,10 @@ defaultOrganisms: args: - "--watch" - "--disableConsensusSequences" - referenceGenomes: {} + referenceGenomes: + main: + singleReference: + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/reference.fasta]]" not-aligned-organism: enabled: true schema: From 5b4e253aaf31acc9cf4216f5324dfbf1b524cb60 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:16:30 +0100 Subject: [PATCH 21/71] feat(config): update the config to use format decided on in slack --- .../loculus/templates/_common-metadata.tpl | 8 +- .../templates/_merged-reference-genomes.tpl | 94 ++-- .../templates/_preprocessingFromValues.tpl | 2 +- .../loculus/templates/_siloDatabaseConfig.tpl | 2 +- .../loculus/templates/ingest-config.yaml | 2 +- kubernetes/loculus/values.schema.json | 64 ++- kubernetes/loculus/values.yaml | 432 +++++++++--------- 7 files changed, 319 insertions(+), 285 deletions(-) diff --git a/kubernetes/loculus/templates/_common-metadata.tpl b/kubernetes/loculus/templates/_common-metadata.tpl index 7542f0be1d..6bdeda43cb 100644 --- a/kubernetes/loculus/templates/_common-metadata.tpl +++ b/kubernetes/loculus/templates/_common-metadata.tpl @@ -331,7 +331,7 @@ organisms: {{/* Generate website metadata from passed metadata array */}} {{- define "loculus.generateWebsiteMetadata" }} -{{- $rawUniqueSegments := (include "loculus.extractUniqueRawNucleotideSequenceNames" .referenceGenomes | fromYaml).segments }} +{{- $rawUniqueSegments := (include "loculus.getNucleotideSegmentNames" .referenceGenomes | fromYaml).segments }} {{- $isSegmented := gt (len $rawUniqueSegments) 1 }} {{- $metadataList := .metadata }} fields: @@ -440,7 +440,7 @@ fields: {{/* Generate backend metadata from passed metadata array */}} {{- define "loculus.generateBackendMetadata" }} -{{- $rawUniqueSegments := (include "loculus.extractUniqueRawNucleotideSequenceNames" .referenceGenomes | fromYaml).segments }} +{{- $rawUniqueSegments := (include "loculus.getNucleotideSegmentNames" .referenceGenomes | fromYaml).segments }} {{- $isSegmented := gt (len $rawUniqueSegments) 1 }} {{- $metadataList := .metadata }} fields: @@ -464,7 +464,7 @@ fields: {{/* Generate backend metadata from passed metadata array */}} {{- define "loculus.generateBackendExternalMetadata" }} -{{- $rawUniqueSegments := (include "loculus.extractUniqueRawNucleotideSequenceNames" .referenceGenomes | fromYaml).segments }} +{{- $rawUniqueSegments := (include "loculus.getNucleotideSegmentNames" .referenceGenomes | fromYaml).segments }} {{- $isSegmented := gt (len $rawUniqueSegments) 1 }} {{- $metadataList := .metadata }} fields: @@ -531,7 +531,7 @@ enaOrganisms: suborganismIdentifierField: {{ quote $configFile.suborganismIdentifierField }} {{- end }} organismName: {{ quote .organismName }} - {{- $rawUniqueSegments := (include "loculus.extractUniqueRawNucleotideSequenceNames" $instance.referenceGenomes | fromYaml).segments }} + {{- $rawUniqueSegments := (include "loculus.getNucleotideSegmentNames" $instance.referenceGenomes | fromYaml).segments }} segments: {{ $rawUniqueSegments | toYaml | nindent 6 }} externalMetadata: {{- $args := dict diff --git a/kubernetes/loculus/templates/_merged-reference-genomes.tpl b/kubernetes/loculus/templates/_merged-reference-genomes.tpl index 3956dec2ab..6cb13a45e6 100644 --- a/kubernetes/loculus/templates/_merged-reference-genomes.tpl +++ b/kubernetes/loculus/templates/_merged-reference-genomes.tpl @@ -1,63 +1,68 @@ {{- define "loculus.mergeReferenceGenomes" -}} -{{- $segmentFirstConfig := . -}} +{{- $segmentWithReferencesList := . -}} {{- $lapisNucleotideSequences := list -}} {{- $lapisGenes := list -}} -{{/* Handle empty reference genomes */}} -{{- if or (not $segmentFirstConfig) (eq (len $segmentFirstConfig) 0) -}} +{{- if or (not $segmentWithReferencesList) (eq (len $segmentWithReferencesList) 0) -}} {{- $result := dict "nucleotideSequences" (list) "genes" (list) -}} {{- $result | toYaml -}} {{- else -}} -{{- range $segmentName, $refMap := $segmentFirstConfig -}} - {{- if eq (len $refMap) 1 -}} - {{/* Single reference mode - no suffix */}} - {{- range $refName, $refData := $refMap -}} - {{- if $refData -}} - {{/* Add nucleotide sequence */}} +{{- $singleSegment := eq (len $segmentWithReferencesList) 1 -}} + +{{- range $segment := $segmentWithReferencesList -}} + {{- $segmentName := $segment.name -}} + {{- $singleReference := eq (len $segment.references) 1 -}} + {{- range $reference := $segment.references -}} + {{- if $singleReference -}} + {{/* Single reference mode - no suffix */}} + {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict + "name" $segmentName + "sequence" $reference.sequence + ) -}} + + {{/* Add genes if present */}} + {{- if $reference.genes -}} + {{- range $geneName, $geneData := $reference.genes -}} + {{- $lapisGenes = append $lapisGenes (dict + "name" $geneName + "sequence" $geneData.sequence + ) -}} + {{- end -}} + {{- end -}} + {{- else -}} + {{- if $singleSegment -}} {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict - "name" $segmentName - "sequence" $refData.sequence + "name" $reference.reference_name + "sequence" $reference.sequence ) -}} {{/* Add genes if present */}} - {{- if $refData.genes -}} - {{- range $geneName, $geneData := $refData.genes -}} + {{- if $reference.genes -}} + {{- $referenceSuffix := printf "_%s" $reference.reference_name -}} + {{- range $geneName, $geneData := $reference.genes -}} {{- $lapisGenes = append $lapisGenes (dict - "name" $geneName + "name" (printf "%s%s" $geneName $referenceSuffix) "sequence" $geneData.sequence ) -}} {{- end -}} {{- end -}} - {{- end -}} - {{- end -}} - {{- end -}} - -{{- else -}} -{{/* Multi-reference mode - suffix with reference name */}} - {{- range $refName, $refData := $refMap -}} - {{- if $refData -}} - {{- if eq (len $segmentFirstConfig) 1 -}} - {{/* Add nucleotide sequence without segmentName */}} - {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict - "name" $refName - "sequence" $refData.sequence - ) -}} {{- else -}} - {{/* Add nucleotide sequence with reference suffix */}} + {{/* Multiple references mode - add suffix to names */}} + {{- $referenceSuffix := printf "_%s" $reference.reference_name -}} {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict - "name" (printf "%s-%s" $segmentName $refName) - "sequence" $refData.sequence + "name" (printf "%s%s" $segmentName $referenceSuffix) + "sequence" $reference.sequence ) -}} - {{- end -}} - {{/* Add genes with reference prefix if present */}} - {{- if $refData.genes -}} - {{- range $geneName, $geneData := $refData.genes -}} - {{- $lapisGenes = append $lapisGenes (dict - "name" (printf "%s-%s" $geneName $refName) - "sequence" $geneData.sequence - ) -}} + {{/* Add genes if present */}} + {{- if $reference.genes -}} + {{- range $geneName, $geneData := $reference.genes -}} + {{- $lapisGenes = append $lapisGenes (dict + "name" (printf "%s%s" $geneName $referenceSuffix) + "sequence" $geneData.sequence + ) -}} + {{- end -}} {{- end -}} {{- end -}} {{- end -}} @@ -71,11 +76,14 @@ {{- end -}} -{{- define "loculus.extractUniqueRawNucleotideSequenceNames" -}} -{{- $segmentFirstConfig := . -}} +{{- define "loculus.getNucleotideSegmentNames" -}} +{{- $segmentWithReferencesList := . -}} -{{/* Extract segment names directly from top-level keys */}} -{{- $segmentNames := keys $segmentFirstConfig -}} +{{/* Extract segment names directly from .name */}} +{{- $segmentNames := list -}} +{{- range $segment := $segmentWithReferencesList -}} + {{- $segmentNames = append $segmentNames $segment.name -}} +{{- end -}} segments: {{- $segmentNames | sortAlpha | toYaml | nindent 2 -}} diff --git a/kubernetes/loculus/templates/_preprocessingFromValues.tpl b/kubernetes/loculus/templates/_preprocessingFromValues.tpl index d9506b1d4f..62ae8a56a2 100644 --- a/kubernetes/loculus/templates/_preprocessingFromValues.tpl +++ b/kubernetes/loculus/templates/_preprocessingFromValues.tpl @@ -58,7 +58,7 @@ {{- $metadata := .metadata }} {{- $referenceGenomes := .referenceGenomes}} -{{- $rawUniqueSegments := (include "loculus.extractUniqueRawNucleotideSequenceNames" $referenceGenomes | fromYaml).segments }} +{{- $rawUniqueSegments := (include "loculus.getNucleotideSegmentNames" $referenceGenomes | fromYaml).segments }} {{- $isSegmented := gt (len $rawUniqueSegments) 1 }} {{- range $metadata }} diff --git a/kubernetes/loculus/templates/_siloDatabaseConfig.tpl b/kubernetes/loculus/templates/_siloDatabaseConfig.tpl index b1c4e0bc45..596c87f7ae 100644 --- a/kubernetes/loculus/templates/_siloDatabaseConfig.tpl +++ b/kubernetes/loculus/templates/_siloDatabaseConfig.tpl @@ -13,7 +13,7 @@ {{- define "loculus.siloDatabaseConfig" }} {{- $schema := .schema }} -{{- $rawUniqueSegments := (include "loculus.extractUniqueRawNucleotideSequenceNames" .referenceGenomes | fromYaml).segments }} +{{- $rawUniqueSegments := (include "loculus.getNucleotideSegmentNames" .referenceGenomes | fromYaml).segments }} {{- $isSegmented := gt (len $rawUniqueSegments) 1 }} schema: instanceName: {{ $schema.organismName }} diff --git a/kubernetes/loculus/templates/ingest-config.yaml b/kubernetes/loculus/templates/ingest-config.yaml index 8951b43759..e8ff5dd135 100644 --- a/kubernetes/loculus/templates/ingest-config.yaml +++ b/kubernetes/loculus/templates/ingest-config.yaml @@ -8,7 +8,7 @@ {{- $values := $item.contents }} {{- if $values.ingest }} {{- $metadata := (include "loculus.patchMetadataSchema" $values.schema | fromYaml).metadata }} -{{- $rawUniqueSegments := (include "loculus.extractUniqueRawNucleotideSequenceNames" $values.referenceGenomes | fromYaml).segments }} +{{- $rawUniqueSegments := (include "loculus.getNucleotideSegmentNames" $values.referenceGenomes | fromYaml).segments }} --- apiVersion: v1 kind: ConfigMap diff --git a/kubernetes/loculus/values.schema.json b/kubernetes/loculus/values.schema.json index ef788e030a..5bdadb5ae4 100644 --- a/kubernetes/loculus/values.schema.json +++ b/kubernetes/loculus/values.schema.json @@ -804,19 +804,33 @@ "referenceGenomes": { "groups": ["organism"], "docsIncludePrefix": false, - "type": "object", - "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", - "description": "Segment name (e.g., 'main', 'L', 'M', 'S')", - "patternProperties": { - "^[a-zA-Z0-9_-]+$": { + "type": "array", + "description": "Segment-first reference genome structure. Each segment must have a name and a list of reference genomes. Each reference contains a nucleotide sequence and optionally genes.", + "items": { + "type": "object", + "description": "Segment details, including name and references", + "additionalProperties": false, + "properties": { + "name": { + "groups": ["referenceGenomes"], + "docsIncludePrefix": false, + "type": "string", + "description": "Name of the segment (e.g., 'S', 'M', 'L', or 'main' for single-segmented organisms)" + }, + "references": { + "type": "array", + "description": "Array of reference genomes for this segment", + "items": { "type": "object", - "description": "Reference name (e.g., 'CV-A16', 'CV-A10', or 'singleReference')", + "description": "Reference genome details", "additionalProperties": false, "properties": { + "reference_name": { + "groups": ["nucleotide-sequence"], + "docsIncludePrefix": false, + "type": "string", + "description": "Name of the reference genome" + }, "sequence": { "groups": ["nucleotide-sequence"], "docsIncludePrefix": false, @@ -832,28 +846,32 @@ "genes": { "groups": ["gene"], "docsIncludePrefix": false, - "type": "object", + "type": "array", "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": { + "items": { + "type": "object", + "description": "Gene details, including name and references", + "additionalProperties": false, + "properties": { + "name": { "groups": ["gene"], "docsIncludePrefix": false, "type": "string", - "description": "The amino acid or nucleotide sequence for this gene" - } + "description": "The name of the gene" + }, + "sequence": { + "groups": ["gene"], + "docsIncludePrefix": false, + "type": "string", + "description": "The amino acid or nucleotide sequence for this gene" + } }, - "required": ["sequence"] - } + "required": ["name", "sequence"] }, "additionalProperties": false } }, - "required": ["sequence"] + "required": ["reference_name", "sequence"] } }, "additionalProperties": false diff --git a/kubernetes/loculus/values.yaml b/kubernetes/loculus/values.yaml index 74fb11327b..563d7b0452 100644 --- a/kubernetes/loculus/values.yaml +++ b/kubernetes/loculus/values.yaml @@ -1379,29 +1379,30 @@ defaultOrganisms: scientific_name: "Sudan ebolavirus" molecule_type: "genomic RNA" referenceGenomes: - 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]]" + - name: main + references: + - reference_name: singleReference + 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]]" west-nile: <<: *defaultOrganismConfig schema: @@ -1462,33 +1463,34 @@ defaultOrganisms: scientific_name: "West Nile virus" molecule_type: "genomic RNA" referenceGenomes: - 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]]" + - name: main + references: + - reference_name: singleReference + 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]]" dummy-organism: schema: submissionDataTypes: *defaultSubmissionDataTypes @@ -1572,34 +1574,35 @@ defaultOrganisms: - "--withErrors" - "--randomWarnError" referenceGenomes: - 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]]" + - name: main + references: + - reference_name: singleReference + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/reference.fasta]]" + genes: + - name: NP + 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]]" 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" @@ -1628,9 +1631,10 @@ defaultOrganisms: - "--watch" - "--disableConsensusSequences" referenceGenomes: - main: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/reference.fasta]]" + - name: main + references: + - reference_name: singleReference + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/reference.fasta]]" not-aligned-organism: enabled: true schema: @@ -1710,12 +1714,13 @@ defaultOrganisms: segments: - name: main references: - - reference_name: "singleReference" + - reference_name: singleReference genes: [] referenceGenomes: - main: - singleReference: - sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/reference.fasta]]" + - name: main + references: + - reference_name: singleReference + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/sars-cov-2/reference.fasta]]" cchf: <<: *defaultOrganismConfig schema: @@ -1808,27 +1813,30 @@ defaultOrganisms: scientific_name: "Orthonairovirus haemorrhagiae" molecule_type: "genomic RNA" referenceGenomes: - 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]]" + - name: L + references: + - reference_name: singleReference + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/reference_L.fasta]]" + insdcAccessionFull: NC_005301.3 + genes: + - name: RdRp + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/RdRp.fasta]]" + - name: M + references: + - reference_name: singleReference + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/reference_M.fasta]]" + insdcAccessionFull: NC_005300.2 + genes: + - name: GPC + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/GPC.fasta]]" + - name: S + references: + - reference_name: singleReference + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/reference_S.fasta]]" + insdcAccessionFull: NC_005302.1 + genes: + - name: NP + sequence: "[[URL:https://corneliusroemer.github.io/seqs/artefacts/cchf/NP.fasta]]" enteroviruses: <<: *defaultOrganismConfig enabled: true @@ -1968,112 +1976,112 @@ defaultOrganisms: molecule_type: "genomic RNA" suborganismIdentifierField: genotype referenceGenomes: - # 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]]" + - name: main + references: + - reference_name: CV-A16 + 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]]" + - reference_name: CV-A10 + 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]]" + - reference_name: EV-A71 + 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]]" + - reference_name: EV-D68 + 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]]" auth: verifyEmail: false resetPasswordAllowed: true From 02f2693bbcb7a63a74fa5e799b20819cf8f52a37 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:19:25 +0100 Subject: [PATCH 22/71] fixup --- kubernetes/loculus/values.schema.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/kubernetes/loculus/values.schema.json b/kubernetes/loculus/values.schema.json index 5bdadb5ae4..e2637e9f30 100644 --- a/kubernetes/loculus/values.schema.json +++ b/kubernetes/loculus/values.schema.json @@ -854,19 +854,19 @@ "additionalProperties": false, "properties": { "name": { - "groups": ["gene"], - "docsIncludePrefix": false, - "type": "string", - "description": "The name of the gene" - }, + "groups": ["gene"], + "docsIncludePrefix": false, + "type": "string", + "description": "The name of the gene" + }, "sequence": { "groups": ["gene"], "docsIncludePrefix": false, "type": "string", "description": "The amino acid or nucleotide sequence for this gene" } - }, - "required": ["name", "sequence"] + }, + "required": ["name", "sequence"] }, "additionalProperties": false } From b58d1e05e6e4c7ee60fea5354c598135bf352ac4 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:02:07 +0100 Subject: [PATCH 23/71] fix reading in config --- .../components/ReviewPage/ReviewPage.spec.tsx | 6 +- .../src/components/ReviewPage/ReviewPage.tsx | 10 +-- .../getSegmentAndGeneDisplayNameMap.spec.tsx | 8 +-- .../getSegmentAndGeneDisplayNameMap.tsx | 6 +- .../DownloadDialog/DownloadDialog.spec.tsx | 28 ++++---- .../DownloadDialog/DownloadDialog.tsx | 12 ++-- .../DownloadDialog/DownloadForm.tsx | 22 +++--- .../components/SearchPage/SearchForm.spec.tsx | 14 ++-- .../src/components/SearchPage/SearchForm.tsx | 12 ++-- .../SearchPage/SearchFullUI.spec.tsx | 12 ++-- .../components/SearchPage/SearchFullUI.tsx | 18 ++--- .../SearchPage/SegmentReferenceSelector.tsx | 4 +- .../components/SearchPage/SeqPreviewModal.tsx | 8 +-- .../SearchPage/SuborganismSelector.spec.tsx | 16 ++--- .../SearchPage/SuborganismSelector.tsx | 8 +-- .../SearchPage/fields/MutationField.spec.tsx | 4 +- .../stillRequiresReferenceNameSelection.tsx | 6 +- .../stillRequiresSuborganismSelection.tsx | 6 +- .../SequenceDetailsPage/DataTable.tsx | 8 +-- .../RevocationEntryDataTable.astro | 8 +-- .../SequenceDetailsPage/SequenceDataUI.tsx | 8 +-- .../SequenceContainer.spec.tsx | 6 +- .../SequencesDisplay/SequencesContainer.tsx | 8 +-- website/src/config.ts | 50 ++------------ .../src/pages/[organism]/search/index.astro | 8 +-- .../submission/[groupId]/released.astro | 8 +-- .../submission/[groupId]/review.astro | 6 +- .../pages/seq/[accessionVersion].fa/index.ts | 8 +-- .../pages/seq/[accessionVersion]/index.astro | 6 +- website/src/types/config.ts | 4 +- website/src/types/referencesGenomes.ts | 67 +++++++++++++------ .../src/utils/getSegmentAndGeneInfo.spec.tsx | 12 ++-- website/src/utils/getSegmentAndGeneInfo.tsx | 4 +- website/src/utils/serversideSearch.ts | 8 +-- 34 files changed, 202 insertions(+), 217 deletions(-) diff --git a/website/src/components/ReviewPage/ReviewPage.spec.tsx b/website/src/components/ReviewPage/ReviewPage.spec.tsx index 6c861d537d..aa279f9d6d 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 type { ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes.ts'; +import type { ReferenceGenomesMap } from '../../types/referencesGenomes.ts'; const openDataUseTerms = { type: openDataUseTermsOption } as const; @@ -25,7 +25,7 @@ const unreleasedSequencesRegex = /You do not currently have any unreleased seque const testGroup = testGroups[0]; function renderReviewPage() { - const schema: ReferenceGenomesLightweightSchema = { + const schema: ReferenceGenomesMap = { segments: {}, }; return render( @@ -36,7 +36,7 @@ function renderReviewPage() { accessToken={testAccessToken} clientConfig={testConfig.public} filesEnabled={false} - referenceGenomeLightweightSchema={schema} + referenceGenomesMap={schema} />, ); } diff --git a/website/src/components/ReviewPage/ReviewPage.tsx b/website/src/components/ReviewPage/ReviewPage.tsx index 74abb18d95..ca913c9c38 100644 --- a/website/src/components/ReviewPage/ReviewPage.tsx +++ b/website/src/components/ReviewPage/ReviewPage.tsx @@ -21,7 +21,7 @@ import { type SequenceEntryStatus, warningsProcessingResult, } from '../../types/backend.ts'; -import { type ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes.ts'; +import { type ReferenceGenomesMap } from '../../types/referencesGenomes.ts'; import { type ClientConfig } from '../../types/runtimeConfig.ts'; import { getAccessionVersionString } from '../../utils/extractAccessionVersion.ts'; import { displayConfirmationDialog } from '../ConfirmationDialog.tsx'; @@ -46,7 +46,7 @@ type ReviewPageProps = { accessToken: string; metadataDisplayNames: Map; filesEnabled: boolean; - referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema; + referenceGenomesMap: ReferenceGenomesMap; }; const pageSizeOptions = [10, 20, 50, 100] as const; @@ -85,7 +85,7 @@ const InnerReviewPage: FC = ({ accessToken, metadataDisplayNames, filesEnabled, - referenceGenomeLightweightSchema, + referenceGenomesMap, }) => { const [pageQuery, setPageQuery] = useState({ pageOneIndexed: 1, size: pageSizeOptions[2] }); @@ -129,8 +129,8 @@ const InnerReviewPage: FC = ({ }; const segmentAndGeneDisplayNameMap = useMemo( - () => getSegmentAndGeneDisplayNameMap(referenceGenomeLightweightSchema), - [referenceGenomeLightweightSchema], + () => getSegmentAndGeneDisplayNameMap(referenceGenomesMap), + [referenceGenomesMap], ); let sequencesData = hooks.getSequences.data; diff --git a/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.spec.tsx b/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.spec.tsx index 61785e7eb5..51026df418 100644 --- a/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.spec.tsx +++ b/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.spec.tsx @@ -1,11 +1,11 @@ import { describe, test, expect } from 'vitest'; import { getSegmentAndGeneDisplayNameMap } from './getSegmentAndGeneDisplayNameMap.tsx'; -import type { ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes.ts'; +import type { ReferenceGenomesMap } from '../../types/referencesGenomes.ts'; describe('getSegmentAndGeneDisplayNameMap', () => { test('should map nothing if there is only a single reference with no segments', () => { - const schema: ReferenceGenomesLightweightSchema = { + const schema: ReferenceGenomesMap = { segments: {}, }; const map = getSegmentAndGeneDisplayNameMap(schema); @@ -14,7 +14,7 @@ describe('getSegmentAndGeneDisplayNameMap', () => { }); test('should map segments and genes for multiple references', () => { - const schema: ReferenceGenomesLightweightSchema = { + const schema: ReferenceGenomesMap = { segments: { segment1: { references: ['suborganism1', 'suborganism2'], @@ -40,7 +40,7 @@ describe('getSegmentAndGeneDisplayNameMap', () => { }); test('should not prefix segments when there is only a single reference', () => { - const schema: ReferenceGenomesLightweightSchema = { + const schema: ReferenceGenomesMap = { segments: { main: { references: ['ref1'], diff --git a/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx b/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx index 3c8175960a..685d9e88fb 100644 --- a/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx +++ b/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx @@ -1,12 +1,12 @@ -import { type ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes.ts'; +import { type ReferenceGenomesMap } from '../../types/referencesGenomes.ts'; export function getSegmentAndGeneDisplayNameMap( - referenceGenomesLightweightSchema: ReferenceGenomesLightweightSchema, + ReferenceGenomesMap: ReferenceGenomesMap, ): Map { const mappingEntries: [string, string][] = []; // Iterate through all segments and references - for (const [segmentName, segmentData] of Object.entries(referenceGenomesLightweightSchema.segments)) { + for (const [segmentName, segmentData] of Object.entries(ReferenceGenomesMap.segments)) { // If only one reference, no prefix needed if (segmentData.references.length === 1) { // LAPIS name is just the segment name diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx index 61f527486e..eff32e7079 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx @@ -9,7 +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 } from '../../../types/referencesGenomes.ts'; +import { type ReferenceGenomesMap, type ReferenceAccession } from '../../../types/referencesGenomes.ts'; import { MetadataFilterSchema } from '../../../utils/search.ts'; const defaultAccession: ReferenceAccession = { @@ -17,7 +17,7 @@ const defaultAccession: ReferenceAccession = { insdcAccessionFull: undefined, }; -const defaultReferenceGenomesLightweightSchema: ReferenceGenomesLightweightSchema = { +const defaultReferenceGenomesMap: ReferenceGenomesMap = { segments: { main: { references: ['ref1'], @@ -27,7 +27,7 @@ const defaultReferenceGenomesLightweightSchema: ReferenceGenomesLightweightSchem }, }; -const multiPathogenReferenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema = { +const multiPathogenreferenceGenomesMap: ReferenceGenomesMap = { segments: { main: { references: ['suborganism1', 'suborganism2'], @@ -75,7 +75,7 @@ async function renderDialog({ metadata = mockMetadata, selectedSuborganism = null, suborganismIdentifierField, - referenceGenomesLightweightSchema = defaultReferenceGenomesLightweightSchema, + ReferenceGenomesMap = defaultReferenceGenomesMap, }: { downloadParams?: SequenceFilter; allowSubmissionOfConsensusSequences?: boolean; @@ -84,7 +84,7 @@ async function renderDialog({ metadata?: Metadata[]; selectedSuborganism?: string | null; suborganismIdentifierField?: string; - referenceGenomesLightweightSchema?: ReferenceGenomesLightweightSchema; + ReferenceGenomesMap?: ReferenceGenomesMap; } = {}) { const schema: Schema = { defaultOrder: 'ascending', @@ -105,7 +105,7 @@ async function renderDialog({ new DownloadUrlGenerator(defaultOrganism, defaultLapisUrl, dataUseTermsEnabled, richFastaHeaderFields) } sequenceFilter={downloadParams} - referenceGenomesLightweightSchema={referenceGenomesLightweightSchema} + ReferenceGenomesMap={ReferenceGenomesMap} allowSubmissionOfConsensusSequences={allowSubmissionOfConsensusSequences} dataUseTermsEnabled={dataUseTermsEnabled} schema={schema} @@ -387,7 +387,7 @@ describe('DownloadDialog', () => { describe('multi pathogen case', () => { test('should disable the aligned sequence downloads when no suborganism is selected', async () => { await renderDialog({ - referenceGenomesLightweightSchema: multiPathogenReferenceGenomeLightweightSchema, + ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: null, suborganismIdentifierField: 'genotype', }); @@ -399,7 +399,7 @@ describe('DownloadDialog', () => { test('should download all raw segments when no suborganism is selected', async () => { await renderDialog({ - referenceGenomesLightweightSchema: multiPathogenReferenceGenomeLightweightSchema, + ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: null, suborganismIdentifierField: 'genotype', }); @@ -414,7 +414,7 @@ describe('DownloadDialog', () => { test('should enable the aligned sequence downloads when suborganism is selected', async () => { await renderDialog({ - referenceGenomesLightweightSchema: multiPathogenReferenceGenomeLightweightSchema, + ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: 'suborganism1', suborganismIdentifierField: 'genotype', }); @@ -425,7 +425,7 @@ describe('DownloadDialog', () => { test('should download only the selected raw suborganism sequences when suborganism is selected', async () => { await renderDialog({ - referenceGenomesLightweightSchema: multiPathogenReferenceGenomeLightweightSchema, + ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: 'suborganism1', suborganismIdentifierField: 'genotype', }); @@ -439,7 +439,7 @@ describe('DownloadDialog', () => { test('should download only the selected aligned suborganism sequences when suborganism is selected', async () => { await renderDialog({ - referenceGenomesLightweightSchema: multiPathogenReferenceGenomeLightweightSchema, + ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: 'suborganism1', suborganismIdentifierField: 'genotype', }); @@ -453,7 +453,7 @@ describe('DownloadDialog', () => { test('should download only the selected aligned suborganism amino acid sequences when suborganism is selected', async () => { await renderDialog({ - referenceGenomesLightweightSchema: multiPathogenReferenceGenomeLightweightSchema, + ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: 'suborganism1', suborganismIdentifierField: 'genotype', }); @@ -492,7 +492,7 @@ describe('DownloadDialog', () => { test('should include "onlyForReferenceName" selected fields in download if no suborganism is selected', async () => { await renderDialog({ - referenceGenomesLightweightSchema: multiPathogenReferenceGenomeLightweightSchema, + ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: null, suborganismIdentifierField: 'genotype', metadata: metadataWithOnlyForReferenceName, @@ -508,7 +508,7 @@ describe('DownloadDialog', () => { test('should exclude selected fields from download if they are not for selected suborganism', async () => { await renderDialog({ - referenceGenomesLightweightSchema: multiPathogenReferenceGenomeLightweightSchema, + ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: 'suborganism2', suborganismIdentifierField: 'genotype', metadata: metadataWithOnlyForReferenceName, diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx index 470d0d295b..5da18219ca 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx @@ -10,7 +10,7 @@ import type { SequenceFilter } from './SequenceFilters.tsx'; import { routes } from '../../../routes/routes.ts'; import { ACCESSION_VERSION_FIELD } from '../../../settings.ts'; import type { Metadata, Schema } from '../../../types/config.ts'; -import type { ReferenceGenomesLightweightSchema } from '../../../types/referencesGenomes.ts'; +import type { ReferenceGenomesMap } from '../../../types/referencesGenomes.ts'; import { MetadataVisibility } from '../../../utils/search.ts'; import type { GeneInfo, SegmentInfo } from '../../../utils/sequenceTypeHelpers.ts'; import { ActiveFilters } from '../../common/ActiveFilters.tsx'; @@ -19,7 +19,7 @@ import { BaseDialog } from '../../common/BaseDialog.tsx'; type DownloadDialogProps = { downloadUrlGenerator: DownloadUrlGenerator; sequenceFilter: SequenceFilter; - referenceGenomesLightweightSchema: ReferenceGenomesLightweightSchema; + ReferenceGenomesMap: ReferenceGenomesMap; allowSubmissionOfConsensusSequences: boolean; dataUseTermsEnabled: boolean; schema: Schema; @@ -31,7 +31,7 @@ type DownloadDialogProps = { export const DownloadDialog: FC = ({ downloadUrlGenerator, sequenceFilter, - referenceGenomesLightweightSchema, + ReferenceGenomesMap, allowSubmissionOfConsensusSequences, dataUseTermsEnabled, schema, @@ -45,8 +45,8 @@ export const DownloadDialog: FC = ({ const closeDialog = () => setIsOpen(false); const { nucleotideSequences, genes, useMultiSegmentEndpoint, defaultFastaHeaderTemplate } = useMemo( - () => getSequenceNames(referenceGenomesLightweightSchema, selectedReferenceName), - [referenceGenomesLightweightSchema, selectedReferenceName], + () => getSequenceNames(ReferenceGenomesMap, selectedReferenceName), + [ReferenceGenomesMap, selectedReferenceName], ); const [downloadFormState, setDownloadFormState] = useState( @@ -94,7 +94,7 @@ export const DownloadDialog: FC = ({

)} >; allowSubmissionOfConsensusSequences: boolean; @@ -45,7 +45,7 @@ type DownloadFormProps = { }; export const DownloadForm: FC = ({ - referenceGenomesLightweightSchema, + ReferenceGenomesMap, downloadFormState, setDownloadFormState, allowSubmissionOfConsensusSequences, @@ -59,12 +59,12 @@ export const DownloadForm: FC = ({ }) => { const [isFieldSelectorOpen, setIsFieldSelectorOpen] = useState(false); const { nucleotideSequences, genes } = useMemo( - () => getSequenceNames(referenceGenomesLightweightSchema, selectedReferenceName), - [referenceGenomesLightweightSchema, selectedReferenceName], + () => getSequenceNames(ReferenceGenomesMap, selectedReferenceName), + [ReferenceGenomesMap, selectedReferenceName], ); const disableAlignedSequences = stillRequiresReferenceNameSelection( - referenceGenomesLightweightSchema, + ReferenceGenomesMap, selectedReferenceName, ); @@ -264,7 +264,7 @@ export const DownloadForm: FC = ({ }; export function getSequenceNames( - referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema, + referenceGenomesMap: ReferenceGenomesMap, selectedReferenceName: string | null, ): { nucleotideSequences: SegmentInfo[]; @@ -272,11 +272,11 @@ export function getSequenceNames( useMultiSegmentEndpoint: boolean; defaultFastaHeaderTemplate?: string; } { - const segments = Object.keys(referenceGenomeLightweightSchema.segments); + const segments = Object.keys(referenceGenomesMap.segments); // Check if single reference mode const firstSegment = segments[0]; - const firstSegmentRefs = firstSegment ? referenceGenomeLightweightSchema.segments[firstSegment].references : []; + const firstSegmentRefs = firstSegment ? referenceGenomesMap.segments[firstSegment].references : []; const isSingleReference = firstSegmentRefs.length === 1; if (isSingleReference && firstSegmentRefs.length > 0) { @@ -285,7 +285,7 @@ export function getSequenceNames( const allGenes: string[] = []; for (const segmentName of segments) { - const segmentData = referenceGenomeLightweightSchema.segments[segmentName]; + const segmentData = referenceGenomesMap.segments[segmentName]; const genes = segmentData.genesByReference[referenceName] ?? []; allGenes.push(...genes); } @@ -311,7 +311,7 @@ export function getSequenceNames( const allGenes: string[] = []; for (const segmentName of segments) { - const segmentData = referenceGenomeLightweightSchema.segments[segmentName]; + const segmentData = referenceGenomesMap.segments[segmentName]; const genes = segmentData.genesByReference[selectedReferenceName] ?? []; allGenes.push(...genes); } diff --git a/website/src/components/SearchPage/SearchForm.spec.tsx b/website/src/components/SearchPage/SearchForm.spec.tsx index b1519ba1c9..ce043bc781 100644 --- a/website/src/components/SearchPage/SearchForm.spec.tsx +++ b/website/src/components/SearchPage/SearchForm.spec.tsx @@ -6,7 +6,7 @@ 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 } from '../../types/referencesGenomes.ts'; +import { type ReferenceGenomesMap, type ReferenceAccession } from '../../types/referencesGenomes.ts'; import { MetadataFilterSchema, MetadataVisibility } from '../../utils/search.ts'; global.ResizeObserver = class FakeResizeObserver implements ResizeObserver { @@ -39,7 +39,7 @@ const defaultAccession: ReferenceAccession = { insdcAccessionFull: undefined, }; -const defaultReferenceGenomesLightweightSchema: ReferenceGenomesLightweightSchema = { +const defaultReferenceGenomesMap: ReferenceGenomesMap = { segments: { main: { references: ['ref1'], @@ -49,7 +49,7 @@ const defaultReferenceGenomesLightweightSchema: ReferenceGenomesLightweightSchem }, }; -const multiPathogenReferenceGenomesLightweightSchema: ReferenceGenomesLightweightSchema = { +const multiPathogenReferenceGenomesMap: ReferenceGenomesMap = { segments: { main: { references: ['suborganism1', 'suborganism2'], @@ -76,7 +76,7 @@ const setASearchVisibility = vi.fn(); const renderSearchForm = ({ filterSchema = new MetadataFilterSchema([...defaultSearchFormFilters]), fieldValues = {}, - referenceGenomeLightweightSchema = defaultReferenceGenomesLightweightSchema, + referenceGenomesMap = defaultReferenceGenomesMap, lapisSearchParameters = {}, suborganismIdentifierField = undefined, selectedSuborganism = null, @@ -84,7 +84,7 @@ const renderSearchForm = ({ }: { filterSchema?: MetadataFilterSchema; fieldValues?: Record; - referenceGenomeLightweightSchema?: ReferenceGenomesLightweightSchema; + referenceGenomesMap?: ReferenceGenomesMap; lapisSearchParameters?: Record; suborganismIdentifierField?: string; selectedSuborganism?: string | null; @@ -99,7 +99,7 @@ const renderSearchForm = ({ lapisUrl: 'http://lapis.dummy.url', searchVisibilities, setASearchVisibility, - referenceGenomeLightweightSchema, + referenceGenomesMap, lapisSearchParameters, showMutationSearch: true, suborganismIdentifierField, @@ -159,7 +159,7 @@ describe('SearchForm', () => { lapisUrl='http://lapis.dummy.url' searchVisibilities={defaultSearchVisibilities} setASearchVisibility={setASearchVisibility} - referenceGenomeLightweightSchema={multiPathogenReferenceGenomesLightweightSchema} + referenceGenomesMap={multiPathogenReferenceGenomesMap} lapisSearchParameters={{}} showMutationSearch={true} suborganismIdentifierField='My genotype' diff --git a/website/src/components/SearchPage/SearchForm.tsx b/website/src/components/SearchPage/SearchForm.tsx index 796d9ffcc8..50f7905706 100644 --- a/website/src/components/SearchPage/SearchForm.tsx +++ b/website/src/components/SearchPage/SearchForm.tsx @@ -18,7 +18,7 @@ import { searchFormHelpDocsUrl } from './searchFormHelpDocsUrl.ts'; import { useOffCanvas } from '../../hooks/useOffCanvas.ts'; import { ACCESSION_FIELD, IS_REVOCATION_FIELD, VERSION_STATUS_FIELD } from '../../settings.ts'; import type { FieldValues, GroupedMetadataFilter, MetadataFilter, SetSomeFieldValues } from '../../types/config.ts'; -import { type ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes.ts'; +import { type ReferenceGenomesMap } from '../../types/referencesGenomes.ts'; import type { ClientConfig } from '../../types/runtimeConfig.ts'; import { extractArrayValue, validateSingleValue } from '../../utils/extractFieldValue.ts'; import { getSegmentAndGeneInfo } from '../../utils/getSegmentAndGeneInfo.tsx'; @@ -41,7 +41,7 @@ interface SearchFormProps { lapisUrl: string; searchVisibilities: Map; setASearchVisibility: (fieldName: string, value: boolean) => void; - referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema; + referenceGenomesMap: ReferenceGenomesMap; lapisSearchParameters: LapisSearchParameters; showMutationSearch: boolean; suborganismIdentifierField: string | undefined; @@ -57,7 +57,7 @@ export const SearchForm = ({ lapisUrl, searchVisibilities, setASearchVisibility, - referenceGenomeLightweightSchema, + referenceGenomesMap, lapisSearchParameters, showMutationSearch, suborganismIdentifierField, @@ -109,8 +109,8 @@ export const SearchForm = ({ })); const suborganismSegmentAndGeneInfo = useMemo( - () => getSegmentAndGeneInfo(referenceGenomeLightweightSchema, selectedReferences), - [referenceGenomeLightweightSchema, selectedReferences], + () => getSegmentAndGeneInfo(referenceGenomesMap, selectedReferences), + [referenceGenomesMap, selectedReferences], ); return ( @@ -174,7 +174,7 @@ export const SearchForm = ({ {suborganismIdentifierField !== undefined && ( { displayName: 'suborganism', }, ], - referenceGenomeLightweightSchema: { + referenceGenomesMap: { segments: { main: { references: ['suborganism1', 'suborganism2'], diff --git a/website/src/components/SearchPage/SearchFullUI.tsx b/website/src/components/SearchPage/SearchFullUI.tsx index f4ac62a112..613df0894b 100644 --- a/website/src/components/SearchPage/SearchFullUI.tsx +++ b/website/src/components/SearchPage/SearchFullUI.tsx @@ -22,7 +22,7 @@ import type { Group } from '../../types/backend.ts'; import type { LinkOut } from '../../types/config.ts'; import { type FieldValues, type Schema, type SequenceFlaggingConfig } from '../../types/config.ts'; import { type OrderBy } from '../../types/lapis.ts'; -import type { ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes.ts'; +import type { ReferenceGenomesMap } from '../../types/referencesGenomes.ts'; import type { ClientConfig } from '../../types/runtimeConfig.ts'; import { formatNumberWithDefaultLocale } from '../../utils/formatNumber.tsx'; import { getSegmentAndGeneInfo } from '../../utils/getSegmentAndGeneInfo.tsx'; @@ -37,7 +37,7 @@ import ErrorBox from '../common/ErrorBox.tsx'; export interface InnerSearchFullUIProps { accessToken?: string; - referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema; + referenceGenomesMap: ReferenceGenomesMap; myGroups: Group[]; organism: string; clientConfig: ClientConfig; @@ -64,7 +64,7 @@ const buildSequenceCountText = (totalSequences: number | undefined, oldCount: nu /* eslint-disable @typescript-eslint/no-unsafe-member-access -- TODO(#3451) this component is a mess a needs to be refactored */ export const InnerSearchFullUI = ({ accessToken, - referenceGenomeLightweightSchema, + referenceGenomesMap, myGroups, organism, clientConfig, @@ -157,9 +157,9 @@ export const InnerSearchFullUI = ({ filterSchema, fieldValues, hiddenFieldValues, - getSegmentAndGeneInfo(referenceGenomeLightweightSchema, selectedReferences), + getSegmentAndGeneInfo(referenceGenomesMap, selectedReferences), ), - [fieldValues, hiddenFieldValues, referenceGenomeLightweightSchema, selectedReferences, filterSchema], + [fieldValues, hiddenFieldValues, referenceGenomesMap, selectedReferences, filterSchema], ); /** @@ -215,7 +215,7 @@ export const InnerSearchFullUI = ({ const showMutationSearch = schema.submissionDataTypes.consensusSequences && - !stillRequiresReferenceNameSelection(referenceGenomeLightweightSchema, selectedSuborganism); + !stillRequiresReferenceNameSelection(referenceGenomesMap, selectedSuborganism); return (
@@ -233,7 +233,7 @@ export const InnerSearchFullUI = ({ accessToken={accessToken} isOpen={Boolean(previewedSeqId)} onClose={() => setPreviewedSeqId(null)} - referenceGenomeLightweightSchema={referenceGenomeLightweightSchema} + referenceGenomesMap={referenceGenomesMap} myGroups={myGroups} isHalfScreen={previewHalfScreen} setIsHalfScreen={setPreviewHalfScreen} @@ -244,7 +244,7 @@ export const InnerSearchFullUI = ({ void; }; diff --git a/website/src/components/SearchPage/SeqPreviewModal.tsx b/website/src/components/SearchPage/SeqPreviewModal.tsx index c6d44738d5..34bbf5cc56 100644 --- a/website/src/components/SearchPage/SeqPreviewModal.tsx +++ b/website/src/components/SearchPage/SeqPreviewModal.tsx @@ -6,7 +6,7 @@ import { routes } from '../../routes/routes'; import { type Group } from '../../types/backend'; import type { SequenceFlaggingConfig } from '../../types/config.ts'; import { type DetailsJson, detailsJsonSchema } from '../../types/detailsJson.ts'; -import { type ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes'; +import { type ReferenceGenomesMap } from '../../types/referencesGenomes'; import { SequenceDataUI } from '../SequenceDetailsPage/SequenceDataUI'; import { SequenceEntryHistoryMenu } from '../SequenceDetailsPage/SequenceEntryHistoryMenu'; import SequencesBanner from '../SequenceDetailsPage/SequencesBanner.tsx'; @@ -26,7 +26,7 @@ interface SeqPreviewModalProps { accessToken?: string; isOpen: boolean; onClose: () => void; - referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema; + referenceGenomesMap: ReferenceGenomesMap; sequenceFlaggingConfig: SequenceFlaggingConfig | undefined; myGroups: Group[]; isHalfScreen?: boolean; @@ -41,7 +41,7 @@ export const SeqPreviewModal: React.FC = ({ accessToken, isOpen, onClose, - referenceGenomeLightweightSchema, + referenceGenomesMap, sequenceFlaggingConfig, myGroups, isHalfScreen = false, @@ -88,7 +88,7 @@ export const SeqPreviewModal: React.FC = ({
{ const { container } = render( { render( { render( { render( { render( void; @@ -21,7 +21,7 @@ type SuborganismSelectorProps = { */ export const SuborganismSelector: FC = ({ filterSchema, - referenceGenomeLightweightSchema, + referenceGenomesMap, suborganismIdentifierField, selectedSuborganism, setSelectedSuborganism, @@ -29,7 +29,7 @@ export const SuborganismSelector: FC = ({ const selectId = useId(); // Extract reference names from the segments - const segments = Object.values(referenceGenomeLightweightSchema.segments); + const segments = Object.values(referenceGenomesMap.segments); const suborganismNames = segments.length > 0 ? segments[0].references : []; const isSinglePathogen = suborganismNames.length < 2; diff --git a/website/src/components/SearchPage/fields/MutationField.spec.tsx b/website/src/components/SearchPage/fields/MutationField.spec.tsx index f701278edf..9df70d578d 100644 --- a/website/src/components/SearchPage/fields/MutationField.spec.tsx +++ b/website/src/components/SearchPage/fields/MutationField.spec.tsx @@ -14,7 +14,7 @@ const singleReferenceSegmentAndGeneInfo: SegmentAndGeneInfo = { isMultiSegmented: false, }; -const multiReferenceGenomeLightweightSchema: SegmentAndGeneInfo = { +const multireferenceGenomesMap: SegmentAndGeneInfo = { nucleotideSegmentInfos: [ { lapisName: 'seg1', label: 'seg1' }, { lapisName: 'seg2', label: 'seg2' }, @@ -59,7 +59,7 @@ describe('MutationField', () => { test('should accept input and dispatch events (multi-segmented)', async () => { const handleChange = vi.fn(); - renderField('', handleChange, multiReferenceGenomeLightweightSchema); + renderField('', handleChange, multireferenceGenomesMap); await userEvent.type(screen.getByLabelText('Mutations'), 'seg1:G100A{enter}'); expect(handleChange).toHaveBeenCalledWith('seg1:G100A'); diff --git a/website/src/components/SearchPage/stillRequiresReferenceNameSelection.tsx b/website/src/components/SearchPage/stillRequiresReferenceNameSelection.tsx index 1c1efa04d6..0b49aacbd3 100644 --- a/website/src/components/SearchPage/stillRequiresReferenceNameSelection.tsx +++ b/website/src/components/SearchPage/stillRequiresReferenceNameSelection.tsx @@ -1,11 +1,11 @@ -import type { ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes.ts'; +import type { ReferenceGenomesMap } from '../../types/referencesGenomes.ts'; export function stillRequiresReferenceNameSelection( - referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema, + referenceGenomesMap: ReferenceGenomesMap, selectedReferenceName: string | null, ) { // Check if there are multiple references in any segment - const hasMultipleReferences = Object.values(referenceGenomeLightweightSchema.segments).some( + const hasMultipleReferences = Object.values(referenceGenomesMap.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 ccca6bf898..1a9fa56c4b 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'; +import type { ReferenceGenomesMap } from '../../types/referencesGenomes.ts'; export function stillRequiresReferenceNameSelection( - referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema, + referenceGenomesMap: ReferenceGenomesMap, selectedReferenceName: string | null, ) { - return Object.keys(referenceGenomeLightweightSchema).length > 1 && selectedReferenceName === null; + return Object.keys(referenceGenomesMap).length > 1 && selectedReferenceName === null; } diff --git a/website/src/components/SequenceDetailsPage/DataTable.tsx b/website/src/components/SequenceDetailsPage/DataTable.tsx index 9b0f41118a..7db4bf95e3 100644 --- a/website/src/components/SequenceDetailsPage/DataTable.tsx +++ b/website/src/components/SequenceDetailsPage/DataTable.tsx @@ -6,13 +6,13 @@ 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 } from '../../types/referencesGenomes'; +import { type ReferenceAccession, type ReferenceGenomesMap } from '../../types/referencesGenomes'; import AkarInfo from '~icons/ri/information-line'; interface Props { dataTableData: DataTableData; dataUseTermsHistory: DataUseTermsHistoryEntry[]; - referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema; + referenceGenomesMap: ReferenceGenomesMap; segmentReferences: Record | null; } @@ -35,14 +35,14 @@ const ReferenceDisplay = ({ reference }: { reference: ReferenceAccession[] }) => const DataTableComponent: React.FC = ({ dataTableData, dataUseTermsHistory, - referenceGenomeLightweightSchema, + referenceGenomesMap, segmentReferences, }) => { // 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 segmentData = referenceGenomesMap.segments[segmentName]; const accession = segmentData.insdcAccessions[referenceName]; reference.push(accession); } diff --git a/website/src/components/SequenceDetailsPage/RevocationEntryDataTable.astro b/website/src/components/SequenceDetailsPage/RevocationEntryDataTable.astro index 74adcd311a..75d8dbb34c 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 } from '../../types/referencesGenomes'; +import type { ReferenceGenomesMap } from '../../types/referencesGenomes'; interface Props { tableData: TableDataEntry[]; dataUseTermsHistory: DataUseTermsHistoryEntry[]; - referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema; + referenceGenomesMap: ReferenceGenomesMap; segmentReferences: Record | null; } -const { tableData, dataUseTermsHistory, referenceGenomeLightweightSchema, segmentReferences } = Astro.props; +const { tableData, dataUseTermsHistory, referenceGenomesMap, segmentReferences } = Astro.props; const relevantFieldsForRevocationVersions = [ ACCESSION_VERSION_FIELD, @@ -51,5 +51,5 @@ const dataTableData = getDataTableData(relevantData); dataTableData={dataTableData} dataUseTermsHistory={dataUseTermsHistory} segmentReferences={segmentReferences} - referenceGenomeLightweightSchema={referenceGenomeLightweightSchema} + referenceGenomesMap={referenceGenomesMap} /> diff --git a/website/src/components/SequenceDetailsPage/SequenceDataUI.tsx b/website/src/components/SequenceDetailsPage/SequenceDataUI.tsx index 03e9948bf1..9f9d4a5239 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 } from '../../types/referencesGenomes'; +import { type ReferenceGenomesMap } from '../../types/referencesGenomes'; import { type ClientConfig } from '../../types/runtimeConfig'; import { EditDataUseTermsButton } from '../DataUseTerms/EditDataUseTermsButton'; import RestrictedUseWarning from '../common/RestrictedUseWarning'; @@ -27,7 +27,7 @@ interface Props { myGroups: Group[]; accessToken: string | undefined; sequenceFlaggingConfig: SequenceFlaggingConfig | undefined; - referenceGenomeSequenceNames: ReferenceGenomesLightweightSchema; + referenceGenomeSequenceNames: ReferenceGenomesMap; } export const SequenceDataUI: FC = ({ @@ -66,7 +66,7 @@ export const SequenceDataUI: FC = ({ dataTableData={dataTableData} segmentReferences={segmentReferences} dataUseTermsHistory={dataUseTermsHistory} - referenceGenomeLightweightSchema={referenceGenomeSequenceNames} + referenceGenomesMap={referenceGenomeSequenceNames} /> {schema.submissionDataTypes.consensusSequences && segmentReferences !== null && (
@@ -75,7 +75,7 @@ export const SequenceDataUI: FC = ({ segmentReferences={segmentReferences} accessionVersion={accessionVersion} clientConfig={clientConfig} - referenceGenomeLightweightSchema={referenceGenomeSequenceNames} + referenceGenomesMap={referenceGenomeSequenceNames} loadSequencesAutomatically={loadSequencesAutomatically} />
diff --git a/website/src/components/SequenceDetailsPage/SequencesDisplay/SequenceContainer.spec.tsx b/website/src/components/SequenceDetailsPage/SequencesDisplay/SequenceContainer.spec.tsx index e93515d749..d68110a60a 100644 --- a/website/src/components/SequenceDetailsPage/SequencesDisplay/SequenceContainer.spec.tsx +++ b/website/src/components/SequenceDetailsPage/SequencesDisplay/SequenceContainer.spec.tsx @@ -5,7 +5,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { SequencesContainer } from './SequencesContainer.tsx'; import { mockRequest, testConfig, testOrganism } from '../../../../vitest.setup.ts'; -import type { ReferenceAccession, ReferenceGenomesLightweightSchema } from '../../../types/referencesGenomes.ts'; +import type { ReferenceAccession, ReferenceGenomesMap } from '../../../types/referencesGenomes.ts'; vi.mock('../../config', () => ({ getLapisUrl: vi.fn().mockReturnValue('http://lapis.dummy'), @@ -30,7 +30,7 @@ const getUnalignedSegmentLabel = (segment: string) => `${segment} (unaligned)`; const BUTTON_ROLE = 'button'; function renderSequenceViewer( - referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema, + referenceGenomesMap: ReferenceGenomesMap, segmentReferences: Record, ) { render( @@ -39,7 +39,7 @@ function renderSequenceViewer( organism={testOrganism} accessionVersion={accessionVersion} clientConfig={testConfig.public} - referenceGenomeLightweightSchema={referenceGenomeLightweightSchema} + referenceGenomesMap={referenceGenomesMap} loadSequencesAutomatically={false} segmentReferences={segmentReferences} /> diff --git a/website/src/components/SequenceDetailsPage/SequencesDisplay/SequencesContainer.tsx b/website/src/components/SequenceDetailsPage/SequencesDisplay/SequencesContainer.tsx index 51bf035a4a..78b6c56fcf 100644 --- a/website/src/components/SequenceDetailsPage/SequencesDisplay/SequencesContainer.tsx +++ b/website/src/components/SequenceDetailsPage/SequencesDisplay/SequencesContainer.tsx @@ -1,7 +1,7 @@ import { type Dispatch, type FC, type SetStateAction, useEffect, useState } from 'react'; import { SequencesViewer } from './SequenceViewer.tsx'; -import { type ReferenceGenomesLightweightSchema } from '../../../types/referencesGenomes.ts'; +import { type ReferenceGenomesMap } from '../../../types/referencesGenomes.ts'; import type { ClientConfig } from '../../../types/runtimeConfig.ts'; import { getSegmentAndGeneInfo } from '../../../utils/getSegmentAndGeneInfo.tsx'; import { @@ -25,7 +25,7 @@ type SequenceContainerProps = { segmentReferences: Record; accessionVersion: string; clientConfig: ClientConfig; - referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema; + referenceGenomesMap: ReferenceGenomesMap; loadSequencesAutomatically: boolean; }; @@ -34,11 +34,11 @@ export const InnerSequencesContainer: FC = ({ segmentReferences, accessionVersion, clientConfig, - referenceGenomeLightweightSchema, + referenceGenomesMap, loadSequencesAutomatically, }) => { const { nucleotideSegmentInfos, geneInfos, isMultiSegmented } = getSegmentAndGeneInfo( - referenceGenomeLightweightSchema, + referenceGenomesMap, segmentReferences, ); diff --git a/website/src/config.ts b/website/src/config.ts index 63597a0161..425eafc67c 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -14,8 +14,8 @@ import { } from './types/config.ts'; import { type ReferenceAccession, - type SegmentFirstReferenceGenomes, - type ReferenceGenomesLightweightSchema, + toReferenceGenomesMap, + ReferenceGenomesMap, } from './types/referencesGenomes.ts'; import { runtimeConfig, type RuntimeConfig, type ServiceUrls } from './types/runtimeConfig.ts'; @@ -46,7 +46,7 @@ export function validateWebsiteConfig(config: WebsiteConfig): Error[] { }); } - const knownReferenceNames = Object.keys(schema.referenceGenomes); + const knownReferenceNames = Object.keys(toReferenceGenomesMap(schema.referenceGenomes)); schema.schema.metadata.forEach((metadatum) => { const onlyForReferenceName = metadatum.onlyForReferenceName; @@ -277,50 +277,10 @@ export function getLapisUrl(serviceConfig: ServiceUrls, organism: string): strin return serviceConfig.lapisUrls[organism]; } -export function getReferenceGenomes(organism: string): SegmentFirstReferenceGenomes { - return getConfig(organism).referenceGenomes; +export function getReferenceGenomes(organism: string): ReferenceGenomesMap { + return toReferenceGenomesMap(getConfig(organism).referenceGenomes); } -export const getReferenceGenomeLightweightSchema = (organism: string): ReferenceGenomesLightweightSchema => { - const referenceGenomes = getReferenceGenomes(organism); - 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() { return getWebsiteConfig().enableSeqSets; } diff --git a/website/src/pages/[organism]/search/index.astro b/website/src/pages/[organism]/search/index.astro index c2eec6866b..331c637ddd 100644 --- a/website/src/pages/[organism]/search/index.astro +++ b/website/src/pages/[organism]/search/index.astro @@ -3,7 +3,7 @@ import { cleanOrganism } from '../../../components/Navigation/cleanOrganism'; import { SearchFullUI } from '../../../components/SearchPage/SearchFullUI'; import { dataUseTermsAreEnabled, - getReferenceGenomeLightweightSchema, + getReferenceGenomes, getRuntimeConfig, getSchema, getWebsiteConfig, @@ -37,14 +37,14 @@ const schema = getSchema(cleanedOrganism.key); const accessToken = getAccessToken(Astro.locals.session); const myGroups = accessToken !== undefined ? await getMyGroups(accessToken) : []; -const referenceGenomeLightweightSchema = getReferenceGenomeLightweightSchema(cleanedOrganism.key); +const referenceGenomes = getReferenceGenomes(cleanedOrganism.key); // Build initialQueryDict with support for multi-value parameters const initialQueryDict = parseUrlSearchParams(Astro.url.searchParams); const { data, totalCount } = await performLapisSearchQueries( initialQueryDict, schema, - referenceGenomeLightweightSchema, + referenceGenomes, hiddenFieldValues, cleanedOrganism.key, ); @@ -60,7 +60,7 @@ const sequenceFlaggingConfig = getWebsiteConfig().sequenceFlagging; schema={schema} myGroups={myGroups} accessToken={accessToken} - referenceGenomeLightweightSchema={referenceGenomeLightweightSchema} + getReferenceGenomes={getReferenceGenomes} hiddenFieldValues={hiddenFieldValues} initialData={data} initialCount={totalCount} diff --git a/website/src/pages/[organism]/submission/[groupId]/released.astro b/website/src/pages/[organism]/submission/[groupId]/released.astro index 230036edf7..cb8ac68af2 100644 --- a/website/src/pages/[organism]/submission/[groupId]/released.astro +++ b/website/src/pages/[organism]/submission/[groupId]/released.astro @@ -4,7 +4,7 @@ import { SearchFullUI } from '../../../../components/SearchPage/SearchFullUI'; import SubmissionPageWrapper from '../../../../components/Submission/SubmissionPageWrapper.astro'; import { dataUseTermsAreEnabled, - getReferenceGenomeLightweightSchema, + getReferenceGenomes, getRuntimeConfig, getSchema, getWebsiteConfig, @@ -36,7 +36,7 @@ const schema = getSchema(cleanedOrganism.key); const accessToken = getAccessToken(Astro.locals.session); -const referenceGenomeLightweightSchema = getReferenceGenomeLightweightSchema(cleanedOrganism.key); +const referenceGenomesMap = getReferenceGenomes(cleanedOrganism.key); const hiddenFieldValues = { [VERSION_STATUS_FIELD]: versionStatuses.latestVersion, @@ -47,7 +47,7 @@ const initialQueryDict = parseUrlSearchParams(Astro.url.searchParams); const { data, totalCount } = await performLapisSearchQueries( initialQueryDict, schema, - referenceGenomeLightweightSchema, + referenceGenomesMap, hiddenFieldValues, cleanedOrganism.key, ); @@ -63,7 +63,7 @@ const sequenceFlaggingConfig = getWebsiteConfig().sequenceFlagging; schema={schema} myGroups={[group]} accessToken={accessToken} - referenceGenomeLightweightSchema={referenceGenomeLightweightSchema} + referenceGenomesMap={referenceGenomesMap} hiddenFieldValues={hiddenFieldValues} initialData={data} initialCount={totalCount} diff --git a/website/src/pages/[organism]/submission/[groupId]/review.astro b/website/src/pages/[organism]/submission/[groupId]/review.astro index 1971181112..f030116dcf 100644 --- a/website/src/pages/[organism]/submission/[groupId]/review.astro +++ b/website/src/pages/[organism]/submission/[groupId]/review.astro @@ -3,7 +3,7 @@ import { ReviewPage } from '../../../../components/ReviewPage/ReviewPage'; import SubmissionPageWrapper from '../../../../components/Submission/SubmissionPageWrapper.astro'; import { getMetadataDisplayNames, - getReferenceGenomeLightweightSchema, + getReferenceGenomes, getRuntimeConfig, outputFilesEnabled, } from '../../../../config'; @@ -17,7 +17,7 @@ const groupsResult = await getGroupsAndCurrentGroup(Astro.params, Astro.locals.s const clientConfig: ClientConfig = getRuntimeConfig().public; const filesEnabled = outputFilesEnabled(organism); const metadataDisplayNames: Map = getMetadataDisplayNames(organism); -const referenceGenomeLightweightSchema = getReferenceGenomeLightweightSchema(organism); +const referenceGenomesMap = getReferenceGenomes(organism); --- @@ -31,7 +31,7 @@ const referenceGenomeLightweightSchema = getReferenceGenomeLightweightSchema(org accessToken={getAccessToken(Astro.locals.session)!} metadataDisplayNames={metadataDisplayNames} filesEnabled={filesEnabled} - referenceGenomeLightweightSchema={referenceGenomeLightweightSchema} + referenceGenomesMap={referenceGenomesMap} client:load /> ), diff --git a/website/src/pages/seq/[accessionVersion].fa/index.ts b/website/src/pages/seq/[accessionVersion].fa/index.ts index b42c762b28..7e221a1d2d 100644 --- a/website/src/pages/seq/[accessionVersion].fa/index.ts +++ b/website/src/pages/seq/[accessionVersion].fa/index.ts @@ -1,6 +1,6 @@ import type { APIRoute } from 'astro'; -import { getReferenceGenomeLightweightSchema } from '../../../config.ts'; +import { getReferenceGenomes } from '../../../config.ts'; import { routes } from '../../../routes/routes.ts'; import { LapisClient } from '../../../services/lapisClient.ts'; import { ACCESSION_VERSION_FIELD } from '../../../settings.ts'; @@ -13,14 +13,14 @@ export const GET: APIRoute = createDownloadAPIRoute( async (accessionVersion: string, organism: string) => { const lapisClient = LapisClient.createForOrganism(organism); - const referenceGenomeLightweightSchema = getReferenceGenomeLightweightSchema(organism); + const referenceGenomesMap = getReferenceGenomes(organism); // Check if single reference mode (all segments have only one reference) - const segments = Object.entries(referenceGenomeLightweightSchema.segments); + const segments = Object.entries(referenceGenomesMap.segments); const isSingleReference = segments.every(([_, segmentData]) => segmentData.references.length === 1); if (isSingleReference) { - const segmentNames = Object.keys(referenceGenomeLightweightSchema.segments); + const segmentNames = Object.keys(referenceGenomesMap.segments); if (segmentNames.length > 1) { return lapisClient.getMultiSegmentSequenceFasta(accessionVersion, segmentNames); } diff --git a/website/src/pages/seq/[accessionVersion]/index.astro b/website/src/pages/seq/[accessionVersion]/index.astro index 2c3dd077f9..e75414baf4 100644 --- a/website/src/pages/seq/[accessionVersion]/index.astro +++ b/website/src/pages/seq/[accessionVersion]/index.astro @@ -6,7 +6,7 @@ import { SequenceDataUI } from '../../../components/SequenceDetailsPage/Sequence import SequencesBanner from '../../../components/SequenceDetailsPage/SequencesBanner.tsx'; import SequencesDataTableTitle from '../../../components/SequenceDetailsPage/SequencesDataTableTitle.astro'; import ErrorBox from '../../../components/common/ErrorBox.tsx'; -import { getSchema, getRuntimeConfig, getReferenceGenomeLightweightSchema } from '../../../config'; +import { getSchema, getRuntimeConfig, getReferenceGenomes } from '../../../config'; import { getWebsiteConfig } from '../../../config'; import BaseLayout from '../../../layouts/BaseLayout.astro'; import { type Group } from '../../../types/backend'; @@ -73,7 +73,7 @@ const sequenceFlaggingConfig = getWebsiteConfig().sequenceFlagging; ) : ( @@ -81,7 +81,7 @@ const sequenceFlaggingConfig = getWebsiteConfig().sequenceFlagging; tableData={result.tableData} organism={organism} segmentReferences={result.segmentReferences} - referenceGenomeSequenceNames={getReferenceGenomeLightweightSchema(organism)} + referenceGenomeSequenceNames={getReferenceGenomes(organism)} accessionVersion={accessionVersion} dataUseTermsHistory={result.dataUseTermsHistory} schema={getSchema(organism)} diff --git a/website/src/types/config.ts b/website/src/types/config.ts index f966a3b546..51e1ebda1e 100644 --- a/website/src/types/config.ts +++ b/website/src/types/config.ts @@ -1,7 +1,7 @@ import z from 'zod'; import { mutationProportionCount, orderDirection } from './lapis.ts'; -import { segmentFirstReferenceGenomes } from './referencesGenomes.ts'; +import { referenceGenomesSchema } from './referencesGenomes.ts'; export const FASTA_IDS_SEPARATOR = ' '; @@ -170,7 +170,7 @@ export type Schema = z.infer; export const instanceConfig = z.object({ schema, - referenceGenomes: segmentFirstReferenceGenomes, + referenceGenomes: referenceGenomesSchema, }); export type InstanceConfig = z.infer; diff --git a/website/src/types/referencesGenomes.ts b/website/src/types/referencesGenomes.ts index bfb5732927..e23a4b2bc2 100644 --- a/website/src/types/referencesGenomes.ts +++ b/website/src/types/referencesGenomes.ts @@ -5,7 +5,7 @@ export type ReferenceAccession = { insdcAccessionFull?: string; }; -// Segment-first structure types + export type SegmentName = string; export type ReferenceName = string; export type GeneName = string; @@ -20,9 +20,7 @@ export type ReferenceSequenceData = { genes?: Record; }; -// Segment-first reference genomes structure (from values.yaml) -// Structure: referenceGenomes[segmentName][referenceName] = { sequence, insdcAccessionFull?, genes? } -export const segmentFirstReferenceGenomes = z.record( +export const ReferenceGenomesMap = z.record( z.string(), // segment name z.record( z.string(), // reference name @@ -33,20 +31,47 @@ export const segmentFirstReferenceGenomes = z.record( }), ), ); -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; - } - >; -}; +export type ReferenceGenomesMap = z.infer; + +export const referenceGenomesSchema = z + .array( + z.object({ + name: z.string(), + references: z.array( + z.object({ + reference_name: z.string(), + sequence: z.string(), + insdcAccessionFull: z.string().optional(), + genes: z.array(z.object({ name: z.string(), sequence: z.string() })).optional(), + }), + ), + }), + ) + .optional(); +export type ReferenceGenomes = z.infer; + +export function toReferenceGenomesMap(values: ReferenceGenomes): ReferenceGenomesMap { + const out: ReferenceGenomesMap = {}; + + for (const genome of values ?? []) { + const segmentName = genome.name; + + out[segmentName] ??= {}; + + for (const ref of genome.references) { + out[segmentName][ref.reference_name] = { + sequence: ref.sequence, + ...(ref.insdcAccessionFull ? { insdcAccessionFull: ref.insdcAccessionFull } : {}), + ...(ref.genes + ? { + genes: Object.fromEntries( + ref.genes.map((g) => [g.name, { sequence: g.sequence }]), + ), + } + : {}), + }; + } + } + + return ReferenceGenomesMap.parse(out); +} \ No newline at end of file diff --git a/website/src/utils/getSegmentAndGeneInfo.spec.tsx b/website/src/utils/getSegmentAndGeneInfo.spec.tsx index 9b9d7c4b5b..29b4829a4f 100644 --- a/website/src/utils/getSegmentAndGeneInfo.spec.tsx +++ b/website/src/utils/getSegmentAndGeneInfo.spec.tsx @@ -2,12 +2,12 @@ import { describe, expect, test } from 'vitest'; import { getSegmentAndGeneInfo } from './getSegmentAndGeneInfo.tsx'; -import type { ReferenceGenomesLightweightSchema } from '../types/referencesGenomes.ts'; +import type { ReferenceGenomesMap } from '../types/referencesGenomes.ts'; describe('getSegmentAndGeneInfo', () => { describe('with single reference per segment', () => { test('should return correct names for multi-segmented organism', () => { - const schema: ReferenceGenomesLightweightSchema = { + const schema: ReferenceGenomesMap = { segments: { segment1: { references: ['ref1'], @@ -47,7 +47,7 @@ describe('getSegmentAndGeneInfo', () => { }); test('should return correct names for single-segmented organism', () => { - const schema: ReferenceGenomesLightweightSchema = { + const schema: ReferenceGenomesMap = { segments: { main: { references: ['ref1'], @@ -75,7 +75,7 @@ describe('getSegmentAndGeneInfo', () => { describe('with multiple references (mixed)', () => { test('should handle different references for different segments', () => { - const schema: ReferenceGenomesLightweightSchema = { + const schema: ReferenceGenomesMap = { segments: { segment1: { references: ['CV-A16', 'CV-A10'], @@ -117,7 +117,7 @@ describe('getSegmentAndGeneInfo', () => { }); test('should handle segments without selected references', () => { - const schema: ReferenceGenomesLightweightSchema = { + const schema: ReferenceGenomesMap = { segments: { segment1: { references: ['CV-A16', 'CV-A10'], @@ -159,7 +159,7 @@ describe('getSegmentAndGeneInfo', () => { }); test('should handle empty selectedReferences', () => { - const schema: ReferenceGenomesLightweightSchema = { + const schema: ReferenceGenomesMap = { segments: { main: { references: ['ref1'], diff --git a/website/src/utils/getSegmentAndGeneInfo.tsx b/website/src/utils/getSegmentAndGeneInfo.tsx index 8fb548b2ec..7b7df802fd 100644 --- a/website/src/utils/getSegmentAndGeneInfo.tsx +++ b/website/src/utils/getSegmentAndGeneInfo.tsx @@ -5,7 +5,7 @@ import { getGeneInfoWithReference, type SegmentReferenceSelections, } from './sequenceTypeHelpers.ts'; -import { type ReferenceGenomesLightweightSchema } from '../types/referencesGenomes.ts'; +import { type ReferenceGenomesMap } from '../types/referencesGenomes.ts'; export type SegmentAndGeneInfo = { nucleotideSegmentInfos: SegmentInfo[]; @@ -20,7 +20,7 @@ export type SegmentAndGeneInfo = { * @returns SegmentAndGeneInfo with all segments and their genes */ export function getSegmentAndGeneInfo( - schema: ReferenceGenomesLightweightSchema, + schema: ReferenceGenomesMap, selectedReferences: SegmentReferenceSelections, ): SegmentAndGeneInfo { const nucleotideSegmentInfos: SegmentInfo[] = []; diff --git a/website/src/utils/serversideSearch.ts b/website/src/utils/serversideSearch.ts index e2f51a049d..b55e565c52 100644 --- a/website/src/utils/serversideSearch.ts +++ b/website/src/utils/serversideSearch.ts @@ -14,12 +14,12 @@ import type { QueryState } from '../components/SearchPage/useStateSyncedWithUrlQ import { LapisClient } from '../services/lapisClient'; import { pageSize } from '../settings'; import type { FieldValues, Schema } from '../types/config'; -import type { ReferenceGenomesLightweightSchema } from '../types/referencesGenomes.ts'; +import type { ReferenceGenomesMap } from '../types/referencesGenomes.ts'; export const performLapisSearchQueries = async ( state: QueryState, schema: Schema, - referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema, + referenceGenomesMap: ReferenceGenomesMap, hiddenFieldValues: FieldValues, organism: string, ): Promise => { @@ -28,13 +28,13 @@ export const performLapisSearchQueries = async ( // Build segment references - all segments use the same reference const segmentReferences: Record = {}; if (suborganism !== null) { - for (const segmentName of Object.keys(referenceGenomeLightweightSchema.segments)) { + for (const segmentName of Object.keys(referenceGenomesMap.segments)) { segmentReferences[segmentName] = suborganism; } } const suborganismSegmentAndGeneInfo = getSegmentAndGeneInfo( - referenceGenomeLightweightSchema, + referenceGenomesMap, Object.keys(segmentReferences).length > 0 ? segmentReferences : {}, ); From d9ada3a1c1a995cdf6a5a2f338939ba6b701718d Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 12 Jan 2026 17:19:50 +0100 Subject: [PATCH 24/71] more changes --- backend/docs/organismWithSuborganisms.md | 12 +- .../scripts/get_ena_submission_list.py | 12 +- ena-submission/src/ena_deposition/config.py | 2 +- .../loculus/templates/_common-metadata.tpl | 8 +- kubernetes/loculus/values.schema.json | 4 +- kubernetes/loculus/values.yaml | 18 +-- .../DownloadDialog/DownloadDialog.spec.tsx | 22 ++-- .../DownloadDialog/DownloadDialog.tsx | 6 +- .../DownloadDialog/DownloadForm.tsx | 108 ++++++++---------- .../FieldSelector/FieldSelectorModal.tsx | 2 +- .../components/SearchPage/SearchForm.spec.tsx | 12 +- .../src/components/SearchPage/SearchForm.tsx | 12 +- .../SearchPage/SearchFullUI.spec.tsx | 8 +- .../components/SearchPage/SearchFullUI.tsx | 22 ++-- .../SearchPage/SuborganismSelector.spec.tsx | 14 +-- .../SearchPage/SuborganismSelector.tsx | 10 +- .../SearchPage/TableColumnSelectorModal.tsx | 8 +- .../stillRequiresReferenceNameSelection.tsx | 12 -- .../stillRequiresSuborganismSelection.tsx | 8 -- .../SearchPage/useSearchPageState.ts | 28 +---- .../SequenceDetailsPage/getTableData.spec.ts | 2 +- .../SequenceDetailsPage/getTableData.ts | 4 +- website/src/config.spec.ts | 6 +- website/src/config.ts | 8 +- website/src/pages/[organism]/index.astro | 1 + .../src/pages/[organism]/search/index.astro | 2 +- website/src/types/config.ts | 2 +- website/src/utils/search.ts | 2 +- website/src/utils/sequenceTypeHelpers.ts | 12 +- website/src/utils/serversideSearch.ts | 4 +- 30 files changed, 165 insertions(+), 206 deletions(-) delete mode 100644 website/src/components/SearchPage/stillRequiresReferenceNameSelection.tsx delete mode 100644 website/src/components/SearchPage/stillRequiresSuborganismSelection.tsx diff --git a/backend/docs/organismWithSuborganisms.md b/backend/docs/organismWithSuborganisms.md index c03262f79a..a07f7892a9 100644 --- a/backend/docs/organismWithSuborganisms.md +++ b/backend/docs/organismWithSuborganisms.md @@ -1,6 +1,6 @@ -# Solution Design - Organisms With Suborganisms +# Solution Design - Organisms With Multiple References ("Subtypes") -The purpose of this feature is to allow a single top level organism to contain multiple suborganisms. +The purpose of this feature is to allow a single top level organism to contain multiple references, or multiple "suborganisms". Motivation: @@ -39,7 +39,7 @@ defaultOrganisms: metadataAdd: - name: clade_cv_a10 # tells the website to only show this field on the search page when CV-A10 is selected - onlyForSuborganism: CV-A10 + onlyForReference: CV-A10 preprocessing: args: segment: CV-A10 @@ -62,10 +62,10 @@ defaultOrganisms: perSegment: true website: <<: *website - # When the website needs to know which suborganism a sequence entry belongs to, - # it will look up the value of this metadata field. + # When the website needs to know which suborganism a sequence entry belongs to (i.e. which reference it aligns to), + # it will look up the value of this metadata field (this metadata field will exist for each segment, e.g. genotype_L, genotype_M). # Preprocessing must make sure that this field is always populated. - suborganismIdentifierField: genotype + referenceIdentifierField: genotype preprocessing: - <<: *preprocessing configFile: diff --git a/ena-submission/scripts/get_ena_submission_list.py b/ena-submission/scripts/get_ena_submission_list.py index 0dd30fe0b5..e242eae8b4 100644 --- a/ena-submission/scripts/get_ena_submission_list.py +++ b/ena-submission/scripts/get_ena_submission_list.py @@ -43,16 +43,16 @@ class SubmissionResults: @dataclass class ENAOrganism: enaOrganismName: str # noqa: N815 - suborganismIdentifierField: str | None = None # noqa: N815 + referenceIdentifierField: str | None = None # noqa: N815 def loculus_organism_to_ena_organism(config: Config) -> dict[str, list[ENAOrganism]]: loculus_organism_to_ena_organism: dict[str, list[ENAOrganism]] = defaultdict(list) for ena_organism, details in config.enaOrganisms.items(): if details.loculusOrganism: - if not details.suborganismIdentifierField: + if not details.referenceIdentifierField: error_msg = ( - "Could not find suborganismIdentifierField in enaOrganism " + "Could not find referenceIdentifierField in enaOrganism " f"config for {ena_organism}" ) logger.error(error_msg) @@ -60,7 +60,7 @@ def loculus_organism_to_ena_organism(config: Config) -> dict[str, list[ENAOrgani loculus_organism_to_ena_organism[details.loculusOrganism].append( ENAOrganism( enaOrganismName=ena_organism, - suborganismIdentifierField=details.suborganismIdentifierField, + referenceIdentifierField=details.referenceIdentifierField, ) ) continue @@ -94,11 +94,11 @@ def assign_ena_organism( entry: dict[str, Any], ena_organisms: list[ENAOrganism], ) -> str: - """Assign the correct ena organism based on suborganismIdentifierField if present.""" + """Assign the correct ena organism based on referenceIdentifierField if present.""" if len(ena_organisms) == 1: return ena_organisms[0].enaOrganismName for ena_organism in ena_organisms: - suborganism_field = ena_organism.suborganismIdentifierField + suborganism_field = ena_organism.referenceIdentifierField if ( suborganism_field and entry["metadata"].get(suborganism_field) == ena_organism.enaOrganismName diff --git a/ena-submission/src/ena_deposition/config.py b/ena-submission/src/ena_deposition/config.py index ee65a3e573..1c40d6a749 100644 --- a/ena-submission/src/ena_deposition/config.py +++ b/ena-submission/src/ena_deposition/config.py @@ -51,7 +51,7 @@ class EnaOrganismDetails(BaseModel): topology: Topology = Topology.LINEAR segments: list[str] loculusOrganism: str | None = None # noqa: N815 - suborganismIdentifierField: str | None = None # noqa: N815 + referenceIdentifierField: str | None = None # noqa: N815 def is_multi_segment(self) -> bool: return len(self.segments) > 1 diff --git a/kubernetes/loculus/templates/_common-metadata.tpl b/kubernetes/loculus/templates/_common-metadata.tpl index 6bdeda43cb..989bd60c5b 100644 --- a/kubernetes/loculus/templates/_common-metadata.tpl +++ b/kubernetes/loculus/templates/_common-metadata.tpl @@ -304,8 +304,8 @@ organisms: {{- if .includeInDownloadsByDefault }} includeInDownloadsByDefault: {{ .includeInDownloadsByDefault }} {{- end }} - {{- if .onlyForSuborganism }} - onlyForSuborganism: {{ .onlyForSuborganism }} + {{- if .onlyForReference }} + onlyForReference: {{ .onlyForReference }} {{- end }} {{- if .customDisplay }} customDisplay: @@ -527,8 +527,8 @@ enaOrganisms: {{- end }} {{- with $instance.schema }} {{ $configFile.configFile | toYaml | nindent 4 }} - {{- if $configFile.suborganismIdentifierField }} - suborganismIdentifierField: {{ quote $configFile.suborganismIdentifierField }} + {{- if $configFile.referenceIdentifierField }} + referenceIdentifierField: {{ quote $configFile.referenceIdentifierField }} {{- end }} organismName: {{ quote .organismName }} {{- $rawUniqueSegments := (include "loculus.getNucleotideSegmentNames" $instance.referenceGenomes | fromYaml).segments }} diff --git a/kubernetes/loculus/values.schema.json b/kubernetes/loculus/values.schema.json index e2637e9f30..f31bb1a157 100644 --- a/kubernetes/loculus/values.schema.json +++ b/kubernetes/loculus/values.schema.json @@ -179,7 +179,7 @@ } } }, - "onlyForSuborganism": { + "onlyForReference": { "groups": ["metadata"], "type": "string", "description": "Optional. Affects the search page of the website: specify which suborganism the metadata field applies to, this metadata field will then only be visible if that suborganism is selected (in the search bar, as a table column, and in the metadata download). Must be a valid suborganism as defined in the reference genomes." @@ -514,7 +514,7 @@ "enum": ["ascending", "descending"], "description": "Default order direction." }, - "suborganismIdentifierField": { + "referenceIdentifierField": { "groups": ["schema"], "type": "string", "description": "Must be set to use the sub-organism feature. This metadata field is used to determine which suborganism a sequence entry belongs to. N.B. the suborganism feature is incomplete and not ready for production use." diff --git a/kubernetes/loculus/values.yaml b/kubernetes/loculus/values.yaml index 563d7b0452..bce7a54781 100644 --- a/kubernetes/loculus/values.yaml +++ b/kubernetes/loculus/values.yaml @@ -1858,7 +1858,7 @@ defaultOrganisms: - &evMetadataAdd name: clade_cv_a16 displayName: Clade CV-A16 - onlyForSuborganism: CV-A16 + onlyForReference: CV-A16 header: "Clade" noInput: true generateIndex: true @@ -1872,7 +1872,7 @@ defaultOrganisms: - <<: *evMetadataAdd name: clade_cv_a10 displayName: Clade CV-A10 - onlyForSuborganism: CV-A10 + onlyForReference: CV-A10 preprocessing: args: segment: CV-A10 @@ -1880,7 +1880,7 @@ defaultOrganisms: - <<: *evMetadataAdd name: clade_ev_a71 displayName: Clade EV-A71 - onlyForSuborganism: EV-A71 + onlyForReference: EV-A71 preprocessing: args: segment: EV-A71 @@ -1888,7 +1888,7 @@ defaultOrganisms: - <<: *evMetadataAdd name: clade_ev_d68 displayName: Clade EV-D68 - onlyForSuborganism: EV-D68 + onlyForReference: EV-D68 preprocessing: args: segment: EV-D68 @@ -1915,7 +1915,7 @@ defaultOrganisms: - segment defaultOrderBy: sampleCollectionDate defaultOrder: descending - suborganismIdentifierField: genotype + referenceIdentifierField: genotype preprocessing: - <<: *preprocessing configFile: @@ -1956,25 +1956,25 @@ defaultOrganisms: taxon_id: 31704 scientific_name: "Coxsackievirus A16" molecule_type: "genomic RNA" - suborganismIdentifierField: genotype + referenceIdentifierField: genotype CV-A10: configFile: taxon_id: 42769 scientific_name: "Coxsackievirus A10" molecule_type: "genomic RNA" - suborganismIdentifierField: genotype + referenceIdentifierField: genotype EV-A71: configFile: taxon_id: 39054 scientific_name: "Enterovirus A71" molecule_type: "genomic RNA" - suborganismIdentifierField: genotype + referenceIdentifierField: genotype EV-D68: configFile: taxon_id: 42789 scientific_name: "Enterovirus D68" molecule_type: "genomic RNA" - suborganismIdentifierField: genotype + referenceIdentifierField: genotype referenceGenomes: - name: main references: diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx index eff32e7079..9745e94b4f 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx @@ -74,7 +74,7 @@ async function renderDialog({ richFastaHeaderFields, metadata = mockMetadata, selectedSuborganism = null, - suborganismIdentifierField, + referenceIdentifierField, ReferenceGenomesMap = defaultReferenceGenomesMap, }: { downloadParams?: SequenceFilter; @@ -83,7 +83,7 @@ async function renderDialog({ richFastaHeaderFields?: string[]; metadata?: Metadata[]; selectedSuborganism?: string | null; - suborganismIdentifierField?: string; + referenceIdentifierField?: string; ReferenceGenomesMap?: ReferenceGenomesMap; } = {}) { const schema: Schema = { @@ -111,7 +111,7 @@ async function renderDialog({ schema={schema} richFastaHeaderFields={richFastaHeaderFields} selectedReferenceName={selectedSuborganism} - suborganismIdentifierField={suborganismIdentifierField} + referenceIdentifierField={referenceIdentifierField} />, ); @@ -389,7 +389,7 @@ describe('DownloadDialog', () => { await renderDialog({ ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: null, - suborganismIdentifierField: 'genotype', + referenceIdentifierField: 'genotype', }); expect(screen.getByText('select a reference', { exact: false })).toBeVisible(); @@ -401,7 +401,7 @@ describe('DownloadDialog', () => { await renderDialog({ ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: null, - suborganismIdentifierField: 'genotype', + referenceIdentifierField: 'genotype', }); await checkAgreement(); @@ -416,7 +416,7 @@ describe('DownloadDialog', () => { await renderDialog({ ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: 'suborganism1', - suborganismIdentifierField: 'genotype', + referenceIdentifierField: 'genotype', }); expect(screen.getByLabelText(alignedNucleotideSequencesLabel)).toBeEnabled(); @@ -427,7 +427,7 @@ describe('DownloadDialog', () => { await renderDialog({ ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: 'suborganism1', - suborganismIdentifierField: 'genotype', + referenceIdentifierField: 'genotype', }); await checkAgreement(); @@ -441,7 +441,7 @@ describe('DownloadDialog', () => { await renderDialog({ ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: 'suborganism1', - suborganismIdentifierField: 'genotype', + referenceIdentifierField: 'genotype', }); await checkAgreement(); @@ -455,7 +455,7 @@ describe('DownloadDialog', () => { await renderDialog({ ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: 'suborganism1', - suborganismIdentifierField: 'genotype', + referenceIdentifierField: 'genotype', }); await checkAgreement(); @@ -494,7 +494,7 @@ describe('DownloadDialog', () => { await renderDialog({ ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: null, - suborganismIdentifierField: 'genotype', + referenceIdentifierField: 'genotype', metadata: metadataWithOnlyForReferenceName, }); @@ -510,7 +510,7 @@ describe('DownloadDialog', () => { await renderDialog({ ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: 'suborganism2', - suborganismIdentifierField: 'genotype', + referenceIdentifierField: 'genotype', metadata: metadataWithOnlyForReferenceName, }); diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx index 5da18219ca..7717c4585f 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx @@ -25,7 +25,7 @@ type DownloadDialogProps = { schema: Schema; richFastaHeaderFields: Schema['richFastaHeaderFields']; selectedReferenceName: string | null; - suborganismIdentifierField: string | undefined; + referenceIdentifierField: string | undefined; }; export const DownloadDialog: FC = ({ @@ -37,7 +37,7 @@ export const DownloadDialog: FC = ({ schema, richFastaHeaderFields, selectedReferenceName, - suborganismIdentifierField, + referenceIdentifierField, }) => { const [isOpen, setIsOpen] = useState(false); @@ -104,7 +104,7 @@ export const DownloadDialog: FC = ({ onSelectedFieldsChange={setSelectedFields} richFastaHeaderFields={richFastaHeaderFields} selectedReferenceName={selectedReferenceName} - suborganismIdentifierField={suborganismIdentifierField} + referenceIdentifierField={referenceIdentifierField} /> {dataUseTermsEnabled && (
diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx b/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx index d2dded2b7b..ffadbe6b94 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx @@ -12,13 +12,12 @@ import type { ReferenceGenomesMap } from '../../../types/referencesGenomes.ts'; import type { MetadataVisibility } from '../../../utils/search.ts'; import { type GeneInfo, - getMultiPathogenNucleotideSequenceNames, getMultiPathogenSequenceName, getSinglePathogenSequenceName, isMultiSegmented, type SegmentInfo, + stillRequiresReferenceNameSelection, } from '../../../utils/sequenceTypeHelpers.ts'; -import { stillRequiresReferenceNameSelection } from '../stillRequiresReferenceNameSelection.tsx'; export type DownloadFormState = { includeRestricted: boolean; @@ -40,8 +39,8 @@ type DownloadFormProps = { downloadFieldVisibilities: Map; onSelectedFieldsChange: Dispatch>>; richFastaHeaderFields: Schema['richFastaHeaderFields']; - selectedReferenceName: string | null; - suborganismIdentifierField: string | undefined; + selectedReferenceNames: Map; + referenceIdentifierField: string | undefined; }; export const DownloadForm: FC = ({ @@ -54,18 +53,17 @@ export const DownloadForm: FC = ({ downloadFieldVisibilities, onSelectedFieldsChange, richFastaHeaderFields, - selectedReferenceName, - suborganismIdentifierField, + selectedReferenceNames, + referenceIdentifierField, }) => { const [isFieldSelectorOpen, setIsFieldSelectorOpen] = useState(false); const { nucleotideSequences, genes } = useMemo( - () => getSequenceNames(ReferenceGenomesMap, selectedReferenceName), - [ReferenceGenomesMap, selectedReferenceName], + () => getSequenceNames(ReferenceGenomesMap, selectedReferenceNames), + [ReferenceGenomesMap, selectedReferenceNames], ); const disableAlignedSequences = stillRequiresReferenceNameSelection( - ReferenceGenomesMap, - selectedReferenceName, + selectedReferenceNames, ); function getDataTypeOptions(): OptionBlockOption[] { @@ -77,7 +75,7 @@ export const DownloadForm: FC = ({ onClick={() => setIsFieldSelectorOpen(true)} selectedFieldsCount={ Array.from(downloadFieldVisibilities.values()).filter((it) => - it.isVisible(selectedReferenceName), + it.isVisible(selectedReferenceNames), ).length } disabled={downloadFormState.dataType !== 'metadata'} @@ -231,7 +229,7 @@ export const DownloadForm: FC = ({ })) } /> - {disableAlignedSequences && suborganismIdentifierField !== undefined && ( + {disableAlignedSequences && referenceIdentifierField !== undefined && (
Or select a reference with the search UI to enable download of aligned sequences.
@@ -257,7 +255,7 @@ export const DownloadForm: FC = ({ schema={schema} downloadFieldVisibilities={downloadFieldVisibilities} onSelectedFieldsChange={onSelectedFieldsChange} - selectedReferenceName={selectedReferenceName} + selectedReferenceNames={selectedReferenceNames} />
); @@ -265,62 +263,54 @@ export const DownloadForm: FC = ({ export function getSequenceNames( referenceGenomesMap: ReferenceGenomesMap, - selectedReferenceName: string | null, + selectedReferenceNames: Map, ): { nucleotideSequences: SegmentInfo[]; genes: GeneInfo[]; useMultiSegmentEndpoint: boolean; defaultFastaHeaderTemplate?: string; } { - const segments = Object.keys(referenceGenomesMap.segments); - - // Check if single reference mode - const firstSegment = segments[0]; - const firstSegmentRefs = firstSegment ? referenceGenomesMap.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 = referenceGenomesMap.segments[segmentName]; - const genes = segmentData.genesByReference[referenceName] ?? []; - allGenes.push(...genes); - } - - return { - nucleotideSequences: segmentNames.map(getSinglePathogenSequenceName), - genes: allGenes.map(getSinglePathogenSequenceName), - useMultiSegmentEndpoint: isMultiSegmented(segmentNames), - }; - } - - if (selectedReferenceName === null) { - return { - nucleotideSequences: [], - genes: [], - useMultiSegmentEndpoint: false, - defaultFastaHeaderTemplate: `{${ACCESSION_VERSION_FIELD}}`, - }; - } - - // Multi-reference mode - const segmentNames = segments; - const allGenes: string[] = []; + const segments = Object.keys(referenceGenomesMap); + let lapisHasMultiSegments = segments.length > 1; + const segmentNames: SegmentInfo[] = []; + const geneNames: GeneInfo[] = []; for (const segmentName of segments) { - const segmentData = referenceGenomesMap.segments[segmentName]; - const genes = segmentData.genesByReference[selectedReferenceName] ?? []; - allGenes.push(...genes); + const segmentData = referenceGenomesMap[segmentName]; + const selectedReferenceName = selectedReferenceNames.get(segmentName); + if (selectedReferenceName) { + return { + nucleotideSequences: [], + genes: [], + useMultiSegmentEndpoint: false, + defaultFastaHeaderTemplate: `{${ACCESSION_VERSION_FIELD}}`, + }; + } + const isMultiReference = Object.keys(segmentData).length > 1; + if (!isMultiReference) { + segmentNames.push(getSinglePathogenSequenceName(segmentName)); + const genes = Object.keys(segmentData.genes).map(getSinglePathogenSequenceName) ?? []; + geneNames.push(...genes); + } else { + lapisHasMultiSegments = true; + for (const referenceName of Object.keys(segmentData)) { + if (segments.length == 1){ + segmentNames.push(getSinglePathogenSequenceName(referenceName)); + } else{ + segmentNames.push(getMultiPathogenSequenceName(segmentName, referenceName)); + } + const genes = Object.keys(segmentData.genes).map((geneName) => + getMultiPathogenSequenceName(geneName, referenceName), + ) ?? []; + geneNames.push(...genes); + } + } } - return { - nucleotideSequences: getMultiPathogenNucleotideSequenceNames(segmentNames, selectedReferenceName), - genes: allGenes.map((name: string) => getMultiPathogenSequenceName(name, selectedReferenceName)), - useMultiSegmentEndpoint: true, - }; + nucleotideSequences: segmentNames, + genes: geneNames, + useMultiSegmentEndpoint: lapisHasMultiSegments, + } } const optionToDataTypeMap: DownloadDataType['type'][] = [ diff --git a/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.tsx b/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.tsx index 479bba4223..b2a7bf5ee7 100644 --- a/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.tsx +++ b/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.tsx @@ -73,7 +73,7 @@ function getDisplayState( if (!isActiveForSelectedReferenceName(selectedReferenceName, field)) { return { type: fieldItemDisplayStateType.disabled, - tooltip: `This is only available when the ${schema.suborganismIdentifierField} ${field.onlyForReferenceName} is selected.`, + tooltip: `This is only available when the ${schema.referenceIdentifierField} ${field.onlyForReferenceName} is selected.`, }; } diff --git a/website/src/components/SearchPage/SearchForm.spec.tsx b/website/src/components/SearchPage/SearchForm.spec.tsx index ce043bc781..681bd76303 100644 --- a/website/src/components/SearchPage/SearchForm.spec.tsx +++ b/website/src/components/SearchPage/SearchForm.spec.tsx @@ -78,7 +78,7 @@ const renderSearchForm = ({ fieldValues = {}, referenceGenomesMap = defaultReferenceGenomesMap, lapisSearchParameters = {}, - suborganismIdentifierField = undefined, + referenceIdentifierField = undefined, selectedSuborganism = null, searchVisibilities = defaultSearchVisibilities, }: { @@ -86,7 +86,7 @@ const renderSearchForm = ({ fieldValues?: Record; referenceGenomesMap?: ReferenceGenomesMap; lapisSearchParameters?: Record; - suborganismIdentifierField?: string; + referenceIdentifierField?: string; selectedSuborganism?: string | null; searchVisibilities?: Map; } = {}) => { @@ -102,7 +102,7 @@ const renderSearchForm = ({ referenceGenomesMap, lapisSearchParameters, showMutationSearch: true, - suborganismIdentifierField, + referenceIdentifierField, selectedSuborganism, setSelectedSuborganism: vi.fn(), selectedReferences: {}, @@ -162,7 +162,7 @@ describe('SearchForm', () => { referenceGenomesMap={multiPathogenReferenceGenomesMap} lapisSearchParameters={{}} showMutationSearch={true} - suborganismIdentifierField='My genotype' + referenceIdentifierField='My genotype' selectedSuborganism={null} setSelectedSuborganism={setSelectedSuborganism} selectedReferences={{}} @@ -222,7 +222,7 @@ describe('SearchForm', () => { renderSearchForm({ filterSchema, searchVisibilities, - suborganismIdentifierField: 'My genotype', + referenceIdentifierField: 'My genotype', selectedSuborganism: 'suborganism1', }); @@ -234,7 +234,7 @@ describe('SearchForm', () => { renderSearchForm({ filterSchema, searchVisibilities, - suborganismIdentifierField: 'My genotype', + referenceIdentifierField: 'My genotype', selectedSuborganism: null, }); diff --git a/website/src/components/SearchPage/SearchForm.tsx b/website/src/components/SearchPage/SearchForm.tsx index 50f7905706..5fcd1ed232 100644 --- a/website/src/components/SearchPage/SearchForm.tsx +++ b/website/src/components/SearchPage/SearchForm.tsx @@ -44,7 +44,7 @@ interface SearchFormProps { referenceGenomesMap: ReferenceGenomesMap; lapisSearchParameters: LapisSearchParameters; showMutationSearch: boolean; - suborganismIdentifierField: string | undefined; + referenceIdentifierField: string | undefined; selectedSuborganism: string | null; setSelectedSuborganism: (newValue: string | null) => void; selectedReferences: Record; @@ -60,7 +60,7 @@ export const SearchForm = ({ referenceGenomesMap, lapisSearchParameters, showMutationSearch, - suborganismIdentifierField, + referenceIdentifierField, selectedSuborganism, setSelectedSuborganism, selectedReferences, @@ -98,13 +98,13 @@ export const SearchForm = ({ const fieldItems: FieldItem[] = filterSchema.filters .filter((filter) => filter.name !== ACCESSION_FIELD) // Exclude accession field - .filter((filter) => filter.name !== suborganismIdentifierField) + .filter((filter) => filter.name !== referenceIdentifierField) .filter((filter) => !filter.notSearchable) .map((filter) => ({ name: filter.name, displayName: filter.displayName ?? sentenceCase(filter.name), header: filter.header, - displayState: getDisplayState(filter, selectedSuborganism, suborganismIdentifierField), + displayState: getDisplayState(filter, selectedSuborganism, referenceIdentifierField), isChecked: searchVisibilities.get(filter.name)?.isChecked ?? false, })); @@ -171,11 +171,11 @@ export const SearchForm = ({ lapisSearchParameters={lapisSearchParameters} />
- {suborganismIdentifierField !== undefined && ( + {referenceIdentifierField !== undefined && ( diff --git a/website/src/components/SearchPage/SearchFullUI.spec.tsx b/website/src/components/SearchPage/SearchFullUI.spec.tsx index e3d847f271..f9e0828ea4 100644 --- a/website/src/components/SearchPage/SearchFullUI.spec.tsx +++ b/website/src/components/SearchPage/SearchFullUI.spec.tsx @@ -83,13 +83,13 @@ function renderSearchFullUI({ clientConfig = testConfig.public, referenceGenomesMap = defaultReferenceGenomesMap, hiddenFieldValues = {}, - suborganismIdentifierField, + referenceIdentifierField, }: { searchFormFilters?: MetadataFilter[]; clientConfig?: ClientConfig; referenceGenomesMap?: ReferenceGenomesMap; hiddenFieldValues?: FieldValues; - suborganismIdentifierField?: string | undefined; + referenceIdentifierField?: string | undefined; } = {}) { const metadataSchema: MetadataFilter[] = searchFormFilters.map((filter) => ({ ...filter, @@ -109,7 +109,7 @@ function renderSearchFullUI({ submissionDataTypes: { consensusSequences: true, }, - suborganismIdentifierField, + referenceIdentifierField, } as Schema, initialData: [], initialCount: 0, @@ -373,7 +373,7 @@ describe('SearchFullUI', () => { it('should reset suborganism specific search fields when changing the selected suborganism', async () => { renderSearchFullUI({ - suborganismIdentifierField: 'suborganism', + referenceIdentifierField: 'suborganism', searchFormFilters: [ { name: 'field1', diff --git a/website/src/components/SearchPage/SearchFullUI.tsx b/website/src/components/SearchPage/SearchFullUI.tsx index 613df0894b..d8bfc961b6 100644 --- a/website/src/components/SearchPage/SearchFullUI.tsx +++ b/website/src/components/SearchPage/SearchFullUI.tsx @@ -12,7 +12,6 @@ import { SearchPagination } from './SearchPagination'; import { SeqPreviewModal } from './SeqPreviewModal'; import { Table, type TableSequenceData } from './Table'; import { TableColumnSelectorModal } from './TableColumnSelectorModal.tsx'; -import { stillRequiresReferenceNameSelection } from './stillRequiresReferenceNameSelection.tsx'; import { useSearchPageState } from './useSearchPageState.ts'; import { type QueryState } from './useStateSyncedWithUrlQueryParams.ts'; import { getLapisUrl } from '../../config.ts'; @@ -34,6 +33,7 @@ import { import { EditDataUseTermsModal } from '../DataUseTerms/EditDataUseTermsModal.tsx'; import { ActiveFilters } from '../common/ActiveFilters.tsx'; import ErrorBox from '../common/ErrorBox.tsx'; +import { stillRequiresReferenceNameSelection } from '../../utils/sequenceTypeHelpers.ts'; export interface InnerSearchFullUIProps { accessToken?: string; @@ -91,9 +91,8 @@ export const InnerSearchFullUI = ({ setPreviewedSeqId, previewHalfScreen, setPreviewHalfScreen, - selectedSuborganism, - setSelectedSuborganism, selectedReferences, + setSelectedReferences, page, setPage, setSomeFieldValues, @@ -114,9 +113,9 @@ export const InnerSearchFullUI = ({ const columnsToShow = useMemo(() => { return schema.metadata - .filter((field) => columnVisibilities.get(field.name)?.isVisible(selectedSuborganism) === true) + .filter((field) => columnVisibilities.get(field.name)?.isVisible(selectedReferences) === true) .map((field) => field.name); - }, [schema.metadata, columnVisibilities]); + }, [schema.metadata, columnVisibilities, selectedReferences]); const orderByField = columnsToShow.includes(orderByFieldCandidate) ? orderByFieldCandidate : schema.primaryKey; @@ -215,7 +214,7 @@ export const InnerSearchFullUI = ({ const showMutationSearch = schema.submissionDataTypes.consensusSequences && - !stillRequiresReferenceNameSelection(referenceGenomesMap, selectedSuborganism); + !stillRequiresReferenceNameSelection(selectedReferences); return (
@@ -225,7 +224,7 @@ export const InnerSearchFullUI = ({ schema={schema} columnVisibilities={columnVisibilities} setAColumnVisibility={setAColumnVisibility} - selectedReferenceName={selectedSuborganism} + selectedReferences={selectedReferences} />
{linkOuts !== undefined && linkOuts.length > 0 && ( { , @@ -57,7 +57,7 @@ describe('SuborganismSelector', () => { , @@ -74,7 +74,7 @@ describe('SuborganismSelector', () => { , @@ -90,7 +90,7 @@ describe('SuborganismSelector', () => { , @@ -106,7 +106,7 @@ describe('SuborganismSelector', () => { , diff --git a/website/src/components/SearchPage/SuborganismSelector.tsx b/website/src/components/SearchPage/SuborganismSelector.tsx index c4bcf7d926..e638903260 100644 --- a/website/src/components/SearchPage/SuborganismSelector.tsx +++ b/website/src/components/SearchPage/SuborganismSelector.tsx @@ -8,7 +8,7 @@ import MaterialSymbolsClose from '~icons/material-symbols/close'; type SuborganismSelectorProps = { filterSchema: MetadataFilterSchema; referenceGenomesMap: ReferenceGenomesMap; - suborganismIdentifierField: string; + referenceIdentifierField: string; selectedSuborganism: string | null; setSelectedSuborganism: (newValue: string | null) => void; }; @@ -22,7 +22,7 @@ type SuborganismSelectorProps = { export const SuborganismSelector: FC = ({ filterSchema, referenceGenomesMap, - suborganismIdentifierField, + referenceIdentifierField, selectedSuborganism, setSelectedSuborganism, }) => { @@ -38,8 +38,8 @@ export const SuborganismSelector: FC = ({ return undefined; } - return filterSchema.filterNameToLabelMap()[suborganismIdentifierField]; - }, [isSinglePathogen, filterSchema, suborganismIdentifierField]); + return filterSchema.filterNameToLabelMap()[referenceIdentifierField]; + }, [isSinglePathogen, filterSchema, referenceIdentifierField]); if (isSinglePathogen) { return null; @@ -47,7 +47,7 @@ export const SuborganismSelector: FC = ({ if (label === undefined) { throw Error( - 'Cannot render suborganism selector without a label when using the suborganism feature. Does the field that you specified in "suborganismIdentifierField" exist in the metadata?', + 'Cannot render suborganism selector without a label when using the suborganism feature. Does the field that you specified in "referenceIdentifierField" exist in the metadata?', ); } diff --git a/website/src/components/SearchPage/TableColumnSelectorModal.tsx b/website/src/components/SearchPage/TableColumnSelectorModal.tsx index a68724a82e..66a5312fd0 100644 --- a/website/src/components/SearchPage/TableColumnSelectorModal.tsx +++ b/website/src/components/SearchPage/TableColumnSelectorModal.tsx @@ -36,10 +36,10 @@ export const TableColumnSelectorModal: FC = ({ name: field.name, displayName: field.displayName ?? field.name, header: field.header, - displayState: getDisplayState(field, selectedReferenceName, schema.suborganismIdentifierField), + displayState: getDisplayState(field, selectedReferenceName, schema.referenceIdentifierField), isChecked: columnVisibilities.get(field.name)?.isChecked ?? false, })), - [schema.metadata, schema.suborganismIdentifierField, columnVisibilities, selectedReferenceName], + [schema.metadata, schema.referenceIdentifierField, columnVisibilities, selectedReferenceName], ); return ( @@ -56,7 +56,7 @@ export const TableColumnSelectorModal: FC = ({ export function getDisplayState( field: Metadata, selectedReferenceName: string | null, - suborganismIdentifierField: string | undefined, + referenceIdentifierField: string | undefined, ): FieldItemDisplayState | undefined { if (field.name === ACCESSION_VERSION_FIELD) { return { type: fieldItemDisplayStateType.alwaysChecked }; @@ -65,7 +65,7 @@ export function getDisplayState( if (!isActiveForSelectedReferenceName(selectedReferenceName, field)) { return { type: fieldItemDisplayStateType.greyedOut, - tooltip: `This is only visible when the ${suborganismIdentifierField ?? 'suborganismIdentifierField'} ${field.onlyForReferenceName} is selected.`, + tooltip: `This is only visible when the ${referenceIdentifierField ?? 'referenceIdentifierField'} ${field.onlyForReferenceName} is selected.`, }; } diff --git a/website/src/components/SearchPage/stillRequiresReferenceNameSelection.tsx b/website/src/components/SearchPage/stillRequiresReferenceNameSelection.tsx deleted file mode 100644 index 0b49aacbd3..0000000000 --- a/website/src/components/SearchPage/stillRequiresReferenceNameSelection.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { ReferenceGenomesMap } from '../../types/referencesGenomes.ts'; - -export function stillRequiresReferenceNameSelection( - referenceGenomesMap: ReferenceGenomesMap, - selectedReferenceName: string | null, -) { - // Check if there are multiple references in any segment - const hasMultipleReferences = Object.values(referenceGenomesMap.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 deleted file mode 100644 index 1a9fa56c4b..0000000000 --- a/website/src/components/SearchPage/stillRequiresSuborganismSelection.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import type { ReferenceGenomesMap } from '../../types/referencesGenomes.ts'; - -export function stillRequiresReferenceNameSelection( - referenceGenomesMap: ReferenceGenomesMap, - selectedReferenceName: string | null, -) { - return Object.keys(referenceGenomesMap).length > 1 && selectedReferenceName === null; -} diff --git a/website/src/components/SearchPage/useSearchPageState.ts b/website/src/components/SearchPage/useSearchPageState.ts index 83d40de8fe..b2e25f59fc 100644 --- a/website/src/components/SearchPage/useSearchPageState.ts +++ b/website/src/components/SearchPage/useSearchPageState.ts @@ -88,7 +88,7 @@ export function useSearchPageState({ newState[key] = value; } - if (schema.suborganismIdentifierField !== undefined && key === schema.suborganismIdentifierField) { + if (schema.referenceIdentifierField !== undefined && key === schema.referenceIdentifierField) { delete newState[MUTATION_KEY]; filterSchema .ungroupedMetadataFilters() @@ -108,7 +108,7 @@ export function useSearchPageState({ setPage(1); } }, - [setState, setPage, hiddenFieldValues, schema.suborganismIdentifierField, filterSchema], + [setState, setPage, hiddenFieldValues, schema.referenceIdentifierField, filterSchema], ); const [previewedSeqId, setPreviewedSeqId] = useUrlParamState( @@ -127,8 +127,8 @@ export function useSearchPageState({ 'boolean', (value) => !value, ); - const [selectedSuborganism, setSelectedSuborganism] = useUrlParamState( - schema.suborganismIdentifierField ?? '', + const [selectedReferences, setSelectedReferences] = useUrlParamState( + schema.referenceIdentifierField ?? '', state, null, setSomeFieldValues, @@ -136,20 +136,6 @@ 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)) { @@ -245,9 +231,8 @@ export function useSearchPageState({ setPreviewedSeqId, previewHalfScreen, setPreviewHalfScreen, - selectedSuborganism, - setSelectedSuborganism, selectedReferences, + setSelectedReferences, page, setPage, setSomeFieldValues, @@ -265,9 +250,8 @@ export function useSearchPageState({ setPreviewedSeqId, previewHalfScreen, setPreviewHalfScreen, - selectedSuborganism, - setSelectedSuborganism, selectedReferences, + setSelectedReferences, page, setPage, setSomeFieldValues, diff --git a/website/src/components/SequenceDetailsPage/getTableData.spec.ts b/website/src/components/SequenceDetailsPage/getTableData.spec.ts index e8464881d0..50d6b970ee 100644 --- a/website/src/components/SequenceDetailsPage/getTableData.spec.ts +++ b/website/src/components/SequenceDetailsPage/getTableData.spec.ts @@ -25,7 +25,7 @@ const schema: Schema = { submissionDataTypes: { consensusSequences: true, }, - suborganismIdentifierField: 'genotype', + referenceIdentifierField: 'genotype', }; const singleReferenceGenomes: ReferenceGenomes = { diff --git a/website/src/components/SequenceDetailsPage/getTableData.ts b/website/src/components/SequenceDetailsPage/getTableData.ts index 2c0debc31a..2899443745 100644 --- a/website/src/components/SequenceDetailsPage/getTableData.ts +++ b/website/src/components/SequenceDetailsPage/getTableData.ts @@ -101,13 +101,13 @@ function getSegmentReferences( } // Multiple references mode - get from metadata field - const suborganismField = schema.suborganismIdentifierField; + const suborganismField = schema.referenceIdentifierField; if (suborganismField === undefined) { return err({ type: 'about:blank', title: 'Invalid configuration', status: 0, - detail: `No 'suborganismIdentifierField' has been configured in the schema for organism ${schema.organismName}`, + detail: `No 'referenceIdentifierField' has been configured in the schema for organism ${schema.organismName}`, instance: '/seq/' + accessionVersion, }); } diff --git a/website/src/config.spec.ts b/website/src/config.spec.ts index 0045260fda..f58822f4d8 100644 --- a/website/src/config.spec.ts +++ b/website/src/config.spec.ts @@ -48,7 +48,7 @@ describe('validateWebsiteConfig', () => { ); }); - it('should fail when "suborganismIdentifierField" is not in metadata', () => { + it('should fail when "referenceIdentifierField" is not in metadata', () => { const errors = validateWebsiteConfig({ ...defaultConfig, organisms: { @@ -62,7 +62,7 @@ describe('validateWebsiteConfig', () => { defaultOrderBy: '', defaultOrder: 'ascending', submissionDataTypes: { consensusSequences: false }, - suborganismIdentifierField: 'suborganismField', + referenceIdentifierField: 'suborganismField', }, referenceGenomes: {}, }, @@ -71,7 +71,7 @@ describe('validateWebsiteConfig', () => { expect(errors).toHaveLength(1); expect(errors[0].message).contains( - `suborganismIdentifierField 'suborganismField' of organism 'dummyOrganism' is not defined in the metadata`, + `referenceIdentifierField 'suborganismField' of organism 'dummyOrganism' is not defined in the metadata`, ); }); }); diff --git a/website/src/config.ts b/website/src/config.ts index 425eafc67c..f2373ac030 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -59,12 +59,12 @@ export function validateWebsiteConfig(config: WebsiteConfig): Error[] { } }); - const suborganismIdentifierField = schema.schema.suborganismIdentifierField; - if (suborganismIdentifierField !== undefined) { - if (!schema.schema.metadata.some((metadatum) => metadatum.name === suborganismIdentifierField)) { + const referenceIdentifierField = schema.schema.referenceIdentifierField; + if (referenceIdentifierField !== undefined) { + if (!schema.schema.metadata.some((metadatum) => metadatum.name === referenceIdentifierField)) { errors.push( new Error( - `suborganismIdentifierField '${suborganismIdentifierField}' of organism '${organism}' is not defined in the metadata.`, + `referenceIdentifierField '${referenceIdentifierField}' of organism '${organism}' is not defined in the metadata.`, ), ); } diff --git a/website/src/pages/[organism]/index.astro b/website/src/pages/[organism]/index.astro index 227b5f1c5d..7ef7997404 100644 --- a/website/src/pages/[organism]/index.astro +++ b/website/src/pages/[organism]/index.astro @@ -4,3 +4,4 @@ import { cleanOrganism } from '../../components/Navigation/cleanOrganism'; const { organism: _organism } = cleanOrganism(Astro.params.organism); return Astro.redirect(`/${_organism!.key}/search`); --- +
hello world
\ No newline at end of file diff --git a/website/src/pages/[organism]/search/index.astro b/website/src/pages/[organism]/search/index.astro index 331c637ddd..eaebc01bcc 100644 --- a/website/src/pages/[organism]/search/index.astro +++ b/website/src/pages/[organism]/search/index.astro @@ -60,7 +60,7 @@ const sequenceFlaggingConfig = getWebsiteConfig().sequenceFlagging; schema={schema} myGroups={myGroups} accessToken={accessToken} - getReferenceGenomes={getReferenceGenomes} + referenceGenomesMap={referenceGenomes} hiddenFieldValues={hiddenFieldValues} initialData={data} initialCount={totalCount} diff --git a/website/src/types/config.ts b/website/src/types/config.ts index 51e1ebda1e..cb532ad7a2 100644 --- a/website/src/types/config.ts +++ b/website/src/types/config.ts @@ -164,7 +164,7 @@ export const schema = z.object({ loadSequencesAutomatically: z.boolean().optional(), richFastaHeaderFields: z.array(z.string()).optional(), linkOuts: z.array(linkOut).optional(), - suborganismIdentifierField: z.string().optional(), + referenceIdentifierField: z.string().optional(), }); export type Schema = z.infer; diff --git a/website/src/utils/search.ts b/website/src/utils/search.ts index b63d8f5418..09054d7d7a 100644 --- a/website/src/utils/search.ts +++ b/website/src/utils/search.ts @@ -96,7 +96,7 @@ const getFieldOrColumnVisibilitiesFromQuery = ( export const getFieldVisibilitiesFromQuery = (schema: Schema, state: QueryState): Map => { const initiallyVisibleAccessor: InitialVisibilityAccessor = (field) => field.initiallyVisible === true; const isFieldSelectable: VisiblitySelectableAccessor = (field) => - field.notSearchable !== true && field.name !== schema.suborganismIdentifierField; + field.notSearchable !== true && field.name !== schema.referenceIdentifierField; return getFieldOrColumnVisibilitiesFromQuery( schema, state, diff --git a/website/src/utils/sequenceTypeHelpers.ts b/website/src/utils/sequenceTypeHelpers.ts index 7a3616fa81..16dcb8ce07 100644 --- a/website/src/utils/sequenceTypeHelpers.ts +++ b/website/src/utils/sequenceTypeHelpers.ts @@ -30,10 +30,10 @@ export function getSinglePathogenSequenceName(name: string): SegmentInfo | GeneI }; } -export function getMultiPathogenSequenceName(name: string, suborganism: string): SegmentInfo | GeneInfo { +export function getMultiPathogenSequenceName(segment: string, reference: string): SegmentInfo | GeneInfo { return { - lapisName: `${suborganism}-${name}`, - label: name, + lapisName: `${segment}-${reference}`, + label: segment, }; } @@ -107,3 +107,9 @@ export function getGeneInfoWithReference(geneName: string, referenceName: string label: geneName, }; } + +export function stillRequiresReferenceNameSelection( + selectedReferenceNames: Map, +) { + return [...selectedReferenceNames.values()].some((value) => value === undefined); +} diff --git a/website/src/utils/serversideSearch.ts b/website/src/utils/serversideSearch.ts index b55e565c52..0c79878b4b 100644 --- a/website/src/utils/serversideSearch.ts +++ b/website/src/utils/serversideSearch.ts @@ -87,11 +87,11 @@ export const performLapisSearchQueries = async ( }; function extractReferenceName(schema: Schema, state: QueryState): string | null { - if (schema.suborganismIdentifierField === undefined) { + if (schema.referenceIdentifierField === undefined) { return null; } - const suborganism = state[schema.suborganismIdentifierField]; + const suborganism = state[schema.referenceIdentifierField]; if (typeof suborganism !== 'string') { return null; } From 981556f9982059bd8e1c732d46e683c8ed727b28 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 12 Jan 2026 17:47:19 +0100 Subject: [PATCH 25/71] improve --- website/src/utils/serversideSearch.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/website/src/utils/serversideSearch.ts b/website/src/utils/serversideSearch.ts index 0c79878b4b..0b07bff1ed 100644 --- a/website/src/utils/serversideSearch.ts +++ b/website/src/utils/serversideSearch.ts @@ -25,18 +25,11 @@ export const performLapisSearchQueries = async ( ): Promise => { const suborganism = extractReferenceName(schema, state); - // Build segment references - all segments use the same reference - const segmentReferences: Record = {}; - if (suborganism !== null) { - for (const segmentName of Object.keys(referenceGenomesMap.segments)) { - segmentReferences[segmentName] = suborganism; - } - } + const suborganismSegmentAndGeneInfo = + suborganism == null + ? null + : getSegmentAndGeneInfo(referenceGenomesMap, { main: suborganism }); - const suborganismSegmentAndGeneInfo = getSegmentAndGeneInfo( - referenceGenomesMap, - Object.keys(segmentReferences).length > 0 ? segmentReferences : {}, - ); const filterSchema = new MetadataFilterSchema(schema.metadata); const fieldValues = filterSchema.getFieldValuesFromQuery(state, hiddenFieldValues); @@ -87,6 +80,7 @@ export const performLapisSearchQueries = async ( }; function extractReferenceName(schema: Schema, state: QueryState): string | null { + //TODO: make this perSegment if (schema.referenceIdentifierField === undefined) { return null; } From 650f4c6ab0d0f8f710580679740ce06de36f6406 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 12 Jan 2026 18:14:59 +0100 Subject: [PATCH 26/71] small improvements --- .../SearchPage/useSearchPageState.ts | 12 +++-------- website/src/pages/[organism]/index.astro | 3 +-- website/src/utils/getSegmentAndGeneInfo.tsx | 20 +++++++------------ website/src/utils/sequenceTypeHelpers.ts | 4 ++-- 4 files changed, 13 insertions(+), 26 deletions(-) diff --git a/website/src/components/SearchPage/useSearchPageState.ts b/website/src/components/SearchPage/useSearchPageState.ts index b2e25f59fc..2bc8f22499 100644 --- a/website/src/components/SearchPage/useSearchPageState.ts +++ b/website/src/components/SearchPage/useSearchPageState.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import useStateSyncedWithUrlQueryParams, { type QueryState } from './useStateSyncedWithUrlQueryParams.ts'; import useUrlParamState from '../../hooks/useUrlParamState.ts'; @@ -127,14 +127,8 @@ export function useSearchPageState({ 'boolean', (value) => !value, ); - const [selectedReferences, setSelectedReferences] = useUrlParamState( - schema.referenceIdentifierField ?? '', - state, - null, - setSomeFieldValues, - 'nullable-string', - (value) => value === null, - ); + const [selectedReferences, setSelectedReferences] = useState>({}); + // Set values from URL on initial load const removeFilter = useCallback( (metadataFilterName: string) => { diff --git a/website/src/pages/[organism]/index.astro b/website/src/pages/[organism]/index.astro index 7ef7997404..5d921b2147 100644 --- a/website/src/pages/[organism]/index.astro +++ b/website/src/pages/[organism]/index.astro @@ -3,5 +3,4 @@ import { cleanOrganism } from '../../components/Navigation/cleanOrganism'; const { organism: _organism } = cleanOrganism(Astro.params.organism); return Astro.redirect(`/${_organism!.key}/search`); ---- -
hello world
\ No newline at end of file +--- \ No newline at end of file diff --git a/website/src/utils/getSegmentAndGeneInfo.tsx b/website/src/utils/getSegmentAndGeneInfo.tsx index 7b7df802fd..ce6e0eb6f2 100644 --- a/website/src/utils/getSegmentAndGeneInfo.tsx +++ b/website/src/utils/getSegmentAndGeneInfo.tsx @@ -20,29 +20,23 @@ export type SegmentAndGeneInfo = { * @returns SegmentAndGeneInfo with all segments and their genes */ export function getSegmentAndGeneInfo( - schema: ReferenceGenomesMap, + referenceGenomes: ReferenceGenomesMap, selectedReferences: SegmentReferenceSelections, ): SegmentAndGeneInfo { const nucleotideSegmentInfos: SegmentInfo[] = []; const geneInfos: GeneInfo[] = []; + const isMultiSegmented = Object.keys(referenceGenomes).length > 1; - // 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)) { + for (const [segmentName, segmentData] of Object.entries(referenceGenomes)) { + const isSingleReference = Object.keys(segmentData).length === 1; 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]; + if (selectedRef && segmentData.genes) { + const geneNames = segmentData.genes.map((gene) => gene.name); for (const geneName of geneNames) { geneInfos.push(getGeneInfoWithReference(geneName, refForNaming)); } @@ -52,6 +46,6 @@ export function getSegmentAndGeneInfo( return { nucleotideSegmentInfos, geneInfos, - isMultiSegmented: Object.keys(schema.segments).length > 1, + isMultiSegmented, }; } diff --git a/website/src/utils/sequenceTypeHelpers.ts b/website/src/utils/sequenceTypeHelpers.ts index 16dcb8ce07..18badea1dc 100644 --- a/website/src/utils/sequenceTypeHelpers.ts +++ b/website/src/utils/sequenceTypeHelpers.ts @@ -109,7 +109,7 @@ export function getGeneInfoWithReference(geneName: string, referenceName: string } export function stillRequiresReferenceNameSelection( - selectedReferenceNames: Map, + selectedReferenceNames: Record, ) { - return [...selectedReferenceNames.values()].some((value) => value === undefined); + return Object.values(selectedReferenceNames).some((value) => value === undefined); } From a5429ad3e4f7e6c88b1ca2548d8a2935ed0c513f Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:38:51 +0100 Subject: [PATCH 27/71] fix config --- .../templates/_merged-reference-genomes.tpl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/kubernetes/loculus/templates/_merged-reference-genomes.tpl b/kubernetes/loculus/templates/_merged-reference-genomes.tpl index 6cb13a45e6..f85fa987c8 100644 --- a/kubernetes/loculus/templates/_merged-reference-genomes.tpl +++ b/kubernetes/loculus/templates/_merged-reference-genomes.tpl @@ -23,10 +23,10 @@ {{/* Add genes if present */}} {{- if $reference.genes -}} - {{- range $geneName, $geneData := $reference.genes -}} + {{- range $gene := $reference.genes -}} {{- $lapisGenes = append $lapisGenes (dict - "name" $geneName - "sequence" $geneData.sequence + "name" $gene.name + "sequence" $gene.sequence ) -}} {{- end -}} {{- end -}} @@ -40,10 +40,10 @@ {{/* Add genes if present */}} {{- if $reference.genes -}} {{- $referenceSuffix := printf "_%s" $reference.reference_name -}} - {{- range $geneName, $geneData := $reference.genes -}} + {{- range $gene := $reference.genes -}} {{- $lapisGenes = append $lapisGenes (dict - "name" (printf "%s%s" $geneName $referenceSuffix) - "sequence" $geneData.sequence + "name" (printf "%s%s" $gene.name $referenceSuffix) + "sequence" $gene.sequence ) -}} {{- end -}} {{- end -}} @@ -57,10 +57,10 @@ {{/* Add genes if present */}} {{- if $reference.genes -}} - {{- range $geneName, $geneData := $reference.genes -}} + {{- range $gene := $reference.genes -}} {{- $lapisGenes = append $lapisGenes (dict - "name" (printf "%s%s" $geneName $referenceSuffix) - "sequence" $geneData.sequence + "name" (printf "%s%s" $gene.name $referenceSuffix) + "sequence" $gene.sequence ) -}} {{- end -}} {{- end -}} From 12c11a977adadb64aa49f2207b0f283e90f6794d Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:59:26 +0100 Subject: [PATCH 28/71] fix more code --- .../DownloadDialog/DownloadDialog.spec.tsx | 6 +++--- .../DownloadDialog/DownloadDialog.tsx | 2 +- .../FieldSelector/FieldSelectorModal.spec.tsx | 6 +++--- .../FieldSelector/FieldSelectorModal.tsx | 12 +++++------ .../components/SearchPage/SearchForm.spec.tsx | 4 ++-- .../src/components/SearchPage/SearchForm.tsx | 3 +++ .../components/SearchPage/SearchFullUI.tsx | 12 ++++++----- .../SearchPage/TableColumnSelectorModal.tsx | 14 ++++++------- .../isActiveForSelectedReferenceName.tsx | 15 ++++---------- .../isActiveForSelectedSuborganism.tsx | 17 ---------------- website/src/config.spec.ts | 6 +++--- website/src/config.ts | 6 +++--- .../src/pages/[organism]/search/index.astro | 2 ++ website/src/types/config.ts | 3 +-- website/src/utils/search.spec.ts | 2 +- website/src/utils/search.ts | 20 +++++++++---------- website/src/utils/sequenceTypeHelpers.ts | 4 ++-- 17 files changed, 58 insertions(+), 76 deletions(-) delete mode 100644 website/src/components/SearchPage/isActiveForSelectedSuborganism.tsx diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx index 9745e94b4f..9ae6487b29 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.spec.tsx @@ -473,7 +473,7 @@ describe('DownloadDialog', () => { type: 'string', header: 'Group 1', includeInDownloadsByDefault: true, - onlyForReferenceName: 'suborganism1', + onlyForReference: 'suborganism1', }, { name: 'field2', @@ -481,7 +481,7 @@ describe('DownloadDialog', () => { type: 'string', header: 'Group 1', includeInDownloadsByDefault: true, - onlyForReferenceName: 'suborganism2', + onlyForReference: 'suborganism2', }, { name: ACCESSION_VERSION_FIELD, @@ -490,7 +490,7 @@ describe('DownloadDialog', () => { }, ]; - test('should include "onlyForReferenceName" selected fields in download if no suborganism is selected', async () => { + test('should include "onlyForReference" selected fields in download if no suborganism is selected', async () => { await renderDialog({ ReferenceGenomesMap: multiPathogenreferenceGenomesMap, selectedSuborganism: null, diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx index 7717c4585f..28f56bb2cc 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx @@ -63,7 +63,7 @@ export const DownloadDialog: FC = ({ return new Map( schema.metadata.map((field) => [ field.name, - new MetadataVisibility(selectedFields.has(field.name), field.onlyForReferenceName), + new MetadataVisibility(selectedFields.has(field.name), field.onlyForReference), ]), ); }, [selectedFields, schema]); diff --git a/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.spec.tsx b/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.spec.tsx index bc010bc04a..a5345a2fe6 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, - onlyForReferenceName: 'suborganism1', + onlyForReference: 'suborganism1', }, { name: 'field3', @@ -182,7 +182,7 @@ describe('FieldSelectorModal', () => { type: 'string', header: 'Group 2', includeInDownloadsByDefault: true, - onlyForReferenceName: 'suborganism2', + onlyForReference: 'suborganism2', }, accessionVersionField, ]); @@ -217,7 +217,7 @@ describe('FieldSelectorModal', () => { new Map( metadata.map((field) => [ field.name, - new MetadataVisibility(result.current[0].has(field.name), field.onlyForReferenceName), + new MetadataVisibility(result.current[0].has(field.name), field.onlyForReference), ]), ) } diff --git a/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.tsx b/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.tsx index b2a7bf5ee7..84e4b84c9c 100644 --- a/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.tsx +++ b/website/src/components/SearchPage/DownloadDialog/FieldSelector/FieldSelectorModal.tsx @@ -17,7 +17,7 @@ type FieldSelectorProps = { schema: Schema; downloadFieldVisibilities: Map; onSelectedFieldsChange: Dispatch>>; - selectedReferenceName: string | null; + selectedReferenceNames: Record; }; export const FieldSelectorModal: FC = ({ @@ -26,7 +26,7 @@ export const FieldSelectorModal: FC = ({ schema, downloadFieldVisibilities, onSelectedFieldsChange, - selectedReferenceName, + selectedReferenceNames, }) => { 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, selectedReferenceName, schema), + displayState: getDisplayState(field, selectedReferenceNames, schema), isChecked: downloadFieldVisibilities.get(field.name)?.isChecked ?? false, })); @@ -63,17 +63,17 @@ export const FieldSelectorModal: FC = ({ function getDisplayState( field: Metadata, - selectedReferenceName: string | null, + selectedReferenceNames: Record, schema: Schema, ): FieldItemDisplayState | undefined { if (field.name === ACCESSION_VERSION_FIELD) { return { type: fieldItemDisplayStateType.alwaysChecked }; } - if (!isActiveForSelectedReferenceName(selectedReferenceName, field)) { + if (!isActiveForSelectedReferenceName(selectedReferenceNames, field)) { return { type: fieldItemDisplayStateType.disabled, - tooltip: `This is only available when the ${schema.referenceIdentifierField} ${field.onlyForReferenceName} is selected.`, + tooltip: `This is only available when the ${schema.referenceIdentifierField} ${field.onlyForReference} is selected.`, }; } diff --git a/website/src/components/SearchPage/SearchForm.spec.tsx b/website/src/components/SearchPage/SearchForm.spec.tsx index 681bd76303..988feb7544 100644 --- a/website/src/components/SearchPage/SearchForm.spec.tsx +++ b/website/src/components/SearchPage/SearchForm.spec.tsx @@ -200,14 +200,14 @@ describe('SearchForm', () => { type: 'string', displayName: 'Field 1', initiallyVisible: true, - onlyForReferenceName: 'suborganism1', + onlyForReference: 'suborganism1', }, { name: 'field2', type: 'string', displayName: 'Field 2', initiallyVisible: true, - onlyForReferenceName: 'suborganism2', + onlyForReference: 'suborganism2', }, ]); const searchVisibilities = new Map([ diff --git a/website/src/components/SearchPage/SearchForm.tsx b/website/src/components/SearchPage/SearchForm.tsx index 5fcd1ed232..7db8117758 100644 --- a/website/src/components/SearchPage/SearchForm.tsx +++ b/website/src/components/SearchPage/SearchForm.tsx @@ -108,10 +108,13 @@ export const SearchForm = ({ isChecked: searchVisibilities.get(filter.name)?.isChecked ?? false, })); + console.log("selectedReferences:", selectedReferences); + const suborganismSegmentAndGeneInfo = useMemo( () => getSegmentAndGeneInfo(referenceGenomesMap, selectedReferences), [referenceGenomesMap, selectedReferences], ); + console.log("suborganismSegmentAndGeneInfo:", suborganismSegmentAndGeneInfo); return ( diff --git a/website/src/components/SearchPage/SearchFullUI.tsx b/website/src/components/SearchPage/SearchFullUI.tsx index d8bfc961b6..1ec695858f 100644 --- a/website/src/components/SearchPage/SearchFullUI.tsx +++ b/website/src/components/SearchPage/SearchFullUI.tsx @@ -105,6 +105,8 @@ export const InnerSearchFullUI = ({ setAColumnVisibility, } = useSearchPageState({ initialQueryDict, schema, hiddenFieldValues, filterSchema }); + console.log('selectedReferences', selectedReferences); + const searchVisibilities = useMemo(() => { return getFieldVisibilitiesFromQuery(schema, state); }, [schema, state]); @@ -224,7 +226,7 @@ export const InnerSearchFullUI = ({ schema={schema} columnVisibilities={columnVisibilities} setAColumnVisibility={setAColumnVisibility} - selectedReferences={selectedReferences} + selectedReferenceNames={selectedReferences} /> setPreviewedSeqId(seqId)} sequenceFlaggingConfig={sequenceFlaggingConfig} /> -
+ {/*
-
+
*/}
) : null} - + /> */} {linkOuts !== undefined && linkOuts.length > 0 && ( ; setAColumnVisibility: (fieldName: string, selected: boolean) => void; - selectedReferenceName: string | null; + selectedReferenceNames: Record; }; export const TableColumnSelectorModal: FC = ({ @@ -26,7 +26,7 @@ export const TableColumnSelectorModal: FC = ({ schema, columnVisibilities, setAColumnVisibility, - selectedReferenceName, + selectedReferenceNames, }) => { 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, selectedReferenceName, schema.referenceIdentifierField), + displayState: getDisplayState(field, selectedReferenceNames, schema.referenceIdentifierField), isChecked: columnVisibilities.get(field.name)?.isChecked ?? false, })), - [schema.metadata, schema.referenceIdentifierField, columnVisibilities, selectedReferenceName], + [schema.metadata, schema.referenceIdentifierField, columnVisibilities, selectedReferenceNames], ); return ( @@ -55,17 +55,17 @@ export const TableColumnSelectorModal: FC = ({ export function getDisplayState( field: Metadata, - selectedReferenceName: string | null, + selectedReferenceNames: Record, referenceIdentifierField: string | undefined, ): FieldItemDisplayState | undefined { if (field.name === ACCESSION_VERSION_FIELD) { return { type: fieldItemDisplayStateType.alwaysChecked }; } - if (!isActiveForSelectedReferenceName(selectedReferenceName, field)) { + if (!isActiveForSelectedReferenceName(selectedReferenceNames, field)) { return { type: fieldItemDisplayStateType.greyedOut, - tooltip: `This is only visible when the ${referenceIdentifierField ?? 'referenceIdentifierField'} ${field.onlyForReferenceName} is selected.`, + tooltip: `This is only visible when the ${referenceIdentifierField ?? 'referenceIdentifierField'} ${field.onlyForReference} is selected.`, }; } diff --git a/website/src/components/SearchPage/isActiveForSelectedReferenceName.tsx b/website/src/components/SearchPage/isActiveForSelectedReferenceName.tsx index 258386508e..a177c5402a 100644 --- a/website/src/components/SearchPage/isActiveForSelectedReferenceName.tsx +++ b/website/src/components/SearchPage/isActiveForSelectedReferenceName.tsx @@ -1,17 +1,10 @@ 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) +export function isActiveForSelectedReferenceName(selectedReferenceNames: Record, field: Metadata) { const matchesReference = - selectedReferenceName === null || + Object.values(selectedReferenceNames).every((value) => value === null) || field.onlyForReference === undefined || - field.onlyForReference === selectedReferenceName; + Object.values(selectedReferenceNames).some((value) => value === field.onlyForReference); - return matchesReferenceName && matchesReference; + return matchesReference; } diff --git a/website/src/components/SearchPage/isActiveForSelectedSuborganism.tsx b/website/src/components/SearchPage/isActiveForSelectedSuborganism.tsx deleted file mode 100644 index 258386508e..0000000000 --- a/website/src/components/SearchPage/isActiveForSelectedSuborganism.tsx +++ /dev/null @@ -1,17 +0,0 @@ -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/config.spec.ts b/website/src/config.spec.ts index f58822f4d8..0a5b12a23c 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 "onlyForReferenceName" is not a valid organism', () => { + it('should fail when "onlyForReference" is not a valid organism', () => { const errors = validateWebsiteConfig({ ...defaultConfig, organisms: { @@ -27,7 +27,7 @@ describe('validateWebsiteConfig', () => { { type: 'string', name: 'test field', - onlyForReferenceName: 'nonExistentReferenceName', + onlyForReference: '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 'nonExistentReferenceName' in 'onlyForReferenceName'.`, + `Metadata field 'test field' in organism 'dummyOrganism' references unknown suborganism 'nonExistentReferenceName' in 'onlyForReference'.`, ); }); diff --git a/website/src/config.ts b/website/src/config.ts index f2373ac030..213c045ffe 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -49,11 +49,11 @@ export function validateWebsiteConfig(config: WebsiteConfig): Error[] { const knownReferenceNames = Object.keys(toReferenceGenomesMap(schema.referenceGenomes)); schema.schema.metadata.forEach((metadatum) => { - const onlyForReferenceName = metadatum.onlyForReferenceName; - if (onlyForReferenceName !== undefined && !knownReferenceNames.includes(onlyForReferenceName)) { + const onlyForReference = metadatum.onlyForReference; + if (onlyForReference !== undefined && !knownReferenceNames.includes(onlyForReference)) { errors.push( new Error( - `Metadata field '${metadatum.name}' in organism '${organism}' references unknown suborganism '${onlyForReferenceName}' in 'onlyForReferenceName'.`, + `Metadata field '${metadatum.name}' in organism '${organism}' references unknown suborganism '${onlyForReference}' in 'onlyForReference'.`, ), ); } diff --git a/website/src/pages/[organism]/search/index.astro b/website/src/pages/[organism]/search/index.astro index eaebc01bcc..5dc439b49e 100644 --- a/website/src/pages/[organism]/search/index.astro +++ b/website/src/pages/[organism]/search/index.astro @@ -34,6 +34,8 @@ if (!cleanedOrganism) { const clientConfig = getRuntimeConfig().public; const schema = getSchema(cleanedOrganism.key); +console.log('cleanedOrganism.key:', cleanedOrganism.key); + const accessToken = getAccessToken(Astro.locals.session); const myGroups = accessToken !== undefined ? await getMyGroups(accessToken) : []; diff --git a/website/src/types/config.ts b/website/src/types/config.ts index cb532ad7a2..b1103b6922 100644 --- a/website/src/types/config.ts +++ b/website/src/types/config.ts @@ -74,8 +74,7 @@ export const metadata = z.object({ order: z.number().optional(), orderOnDetailsPage: z.number().optional(), includeInDownloadsByDefault: z.boolean().optional(), - onlyForReferenceName: z.string().optional(), // DEPRECATED: Use onlyForReference instead - onlyForReference: z.string().optional(), // NEW: Scopes field to a specific reference (replaces onlyForReferenceName) + onlyForReference: z.string().optional(), }); export const inputFieldOption = z.object({ diff --git a/website/src/utils/search.spec.ts b/website/src/utils/search.spec.ts index 86e2109dc4..cccecede07 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 onlyForReferenceName is undefined', () => { + it('should return true when isChecked is true and onlyForReference 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 09054d7d7a..0ffb01aa0d 100644 --- a/website/src/utils/search.ts +++ b/website/src/utils/search.ts @@ -37,23 +37,23 @@ type VisiblitySelectableAccessor = (field: MetadataFilter) => boolean; export class MetadataVisibility { public readonly isChecked: boolean; - private readonly onlyForReferenceName: string | undefined; + private readonly onlyForReference: string | undefined; - constructor(isChecked: boolean, onlyForReferenceName: string | undefined) { + constructor(isChecked: boolean, onlyForReference: string | undefined) { this.isChecked = isChecked; - this.onlyForReferenceName = onlyForReferenceName; + this.onlyForReference = onlyForReference; } - public isVisible(selectedReferenceName: string | null) { + public isVisible(selectedReferenceNames: Record): boolean { if (!this.isChecked) { return false; } - - if (this.onlyForReferenceName === undefined || selectedReferenceName === null) { - return true; + for (const value of Object.values(selectedReferenceNames)) { + if (this.onlyForReference === value) { + return true; + } } - - return this.onlyForReferenceName === selectedReferenceName; + return false; } } @@ -84,7 +84,7 @@ const getFieldOrColumnVisibilitiesFromQuery = ( const visibility = new MetadataVisibility( explicitVisibilitiesInUrlByFieldName.get(fieldName) ?? initiallyVisibleAccessor(field), - field.onlyForReferenceName, + field.onlyForReference, ); visibilities.set(fieldName, visibility); diff --git a/website/src/utils/sequenceTypeHelpers.ts b/website/src/utils/sequenceTypeHelpers.ts index 18badea1dc..51633de91d 100644 --- a/website/src/utils/sequenceTypeHelpers.ts +++ b/website/src/utils/sequenceTypeHelpers.ts @@ -109,7 +109,7 @@ export function getGeneInfoWithReference(geneName: string, referenceName: string } export function stillRequiresReferenceNameSelection( - selectedReferenceNames: Record, + selectedReferenceNames: Record, ) { - return Object.values(selectedReferenceNames).some((value) => value === undefined); + return Object.values(selectedReferenceNames).some((value) => value === null); } From 4bde3ddf7bbf8d8d4094782061614a234a2856e6 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:08:59 +0100 Subject: [PATCH 29/71] fix up --- website/src/config.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/website/src/config.ts b/website/src/config.ts index 213c045ffe..a66319d47c 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -46,18 +46,18 @@ export function validateWebsiteConfig(config: WebsiteConfig): Error[] { }); } - const knownReferenceNames = Object.keys(toReferenceGenomesMap(schema.referenceGenomes)); - - schema.schema.metadata.forEach((metadatum) => { - const onlyForReference = metadatum.onlyForReference; - if (onlyForReference !== undefined && !knownReferenceNames.includes(onlyForReference)) { - errors.push( - new Error( - `Metadata field '${metadatum.name}' in organism '${organism}' references unknown suborganism '${onlyForReference}' in 'onlyForReference'.`, - ), - ); - } - }); + //const knownReferenceNames = Object.keys(toReferenceGenomesMap(schema.referenceGenomes)); + + // schema.schema.metadata.forEach((metadatum) => { + // const onlyForReference = metadatum.onlyForReference; + // if (onlyForReference !== undefined && !knownReferenceNames.includes(onlyForReference)) { + // errors.push( + // new Error( + // `Metadata field '${metadatum.name}' in organism '${organism}' references unknown suborganism '${onlyForReference}' in 'onlyForReference'.`, + // ), + // ); + // } + // }); const referenceIdentifierField = schema.schema.referenceIdentifierField; if (referenceIdentifierField !== undefined) { @@ -78,6 +78,7 @@ export function getWebsiteConfig(): WebsiteConfig { const config = readTypedConfigFile('website_config.json', websiteConfig); const validationErrors = validateWebsiteConfig(config); if (validationErrors.length > 0) { + console.error('Website configuration validation errors:', validationErrors); throw new AggregateError(validationErrors, 'There were validation errors in the website_config.json'); } _config = config; From 8fcc48ebed7c0305c7d768e50426933a070ccf00 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:17:56 +0100 Subject: [PATCH 30/71] fixup --- website/src/utils/serversideSearch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/utils/serversideSearch.ts b/website/src/utils/serversideSearch.ts index 0b07bff1ed..0bfa5d92a2 100644 --- a/website/src/utils/serversideSearch.ts +++ b/website/src/utils/serversideSearch.ts @@ -48,7 +48,7 @@ export const performLapisSearchQueries = async ( const columnVisibilities = getColumnVisibilitiesFromQuery(schema, state); const columnsToShow = schema.metadata - .filter((field) => columnVisibilities.get(field.name)?.isVisible(suborganism) === true) + .filter((field) => columnVisibilities.get(field.name)?.isVisible({ main: suborganism }) === true) .map((field) => field.name); const client = LapisClient.createForOrganism(organism); From a527b3fe4e01d3fe1d93589b81c19b9cc26a4a9c Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:26:42 +0100 Subject: [PATCH 31/71] testing --- .../src/components/SearchPage/SearchForm.tsx | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/website/src/components/SearchPage/SearchForm.tsx b/website/src/components/SearchPage/SearchForm.tsx index 7db8117758..2264f23efe 100644 --- a/website/src/components/SearchPage/SearchForm.tsx +++ b/website/src/components/SearchPage/SearchForm.tsx @@ -5,7 +5,7 @@ import { useMemo, useState } from 'react'; import { OffCanvasOverlay } from '../OffCanvasOverlay.tsx'; import { Button } from '../common/Button'; import type { LapisSearchParameters } from './DownloadDialog/SequenceFilters.tsx'; -import { SuborganismSelector } from './SuborganismSelector.tsx'; +//import { SuborganismSelector } from './SuborganismSelector.tsx'; import { getDisplayState } from './TableColumnSelectorModal.tsx'; import { AccessionField } from './fields/AccessionField.tsx'; import { DateField, TimestampField } from './fields/DateField.tsx'; @@ -18,7 +18,7 @@ import { searchFormHelpDocsUrl } from './searchFormHelpDocsUrl.ts'; import { useOffCanvas } from '../../hooks/useOffCanvas.ts'; import { ACCESSION_FIELD, IS_REVOCATION_FIELD, VERSION_STATUS_FIELD } from '../../settings.ts'; import type { FieldValues, GroupedMetadataFilter, MetadataFilter, SetSomeFieldValues } from '../../types/config.ts'; -import { type ReferenceGenomesMap } from '../../types/referencesGenomes.ts'; +import { ReferenceGenomesMap } from '../../types/referencesGenomes.ts'; import type { ClientConfig } from '../../types/runtimeConfig.ts'; import { extractArrayValue, validateSingleValue } from '../../utils/extractFieldValue.ts'; import { getSegmentAndGeneInfo } from '../../utils/getSegmentAndGeneInfo.tsx'; @@ -45,8 +45,7 @@ interface SearchFormProps { lapisSearchParameters: LapisSearchParameters; showMutationSearch: boolean; referenceIdentifierField: string | undefined; - selectedSuborganism: string | null; - setSelectedSuborganism: (newValue: string | null) => void; + setSelectedReferences: (newValues: Record) => void; selectedReferences: Record; } @@ -61,12 +60,10 @@ export const SearchForm = ({ lapisSearchParameters, showMutationSearch, referenceIdentifierField, - selectedSuborganism, - setSelectedSuborganism, selectedReferences, }: SearchFormProps) => { const visibleFields = filterSchema.filters.filter( - (field) => searchVisibilities.get(field.name)?.isVisible(selectedSuborganism) ?? false, + (field) => searchVisibilities.get(field.name)?.isVisible(selectedReferences) ?? false, ); const [isFieldSelectorOpen, setIsFieldSelectorOpen] = useState(false); @@ -104,7 +101,7 @@ export const SearchForm = ({ name: filter.name, displayName: filter.displayName ?? sentenceCase(filter.name), header: filter.header, - displayState: getDisplayState(filter, selectedSuborganism, referenceIdentifierField), + displayState: getDisplayState(filter, selectedReferences, referenceIdentifierField), isChecked: searchVisibilities.get(filter.name)?.isChecked ?? false, })); @@ -174,15 +171,15 @@ export const SearchForm = ({ lapisSearchParameters={lapisSearchParameters} />
- {referenceIdentifierField !== undefined && ( - - )} + )} */}
Date: Mon, 12 Jan 2026 21:27:05 +0100 Subject: [PATCH 32/71] testing --- website/src/components/SearchPage/SearchFullUI.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/components/SearchPage/SearchFullUI.tsx b/website/src/components/SearchPage/SearchFullUI.tsx index 1ec695858f..8db83a4a43 100644 --- a/website/src/components/SearchPage/SearchFullUI.tsx +++ b/website/src/components/SearchPage/SearchFullUI.tsx @@ -241,7 +241,7 @@ export const InnerSearchFullUI = ({ setPreviewedSeqId={(seqId: string | null) => setPreviewedSeqId(seqId)} sequenceFlaggingConfig={sequenceFlaggingConfig} /> - {/*
+
-
*/} +
Date: Mon, 12 Jan 2026 21:32:42 +0100 Subject: [PATCH 33/71] more --- .../DownloadDialog/DownloadDialog.tsx | 18 +++++++++--------- .../SearchPage/DownloadDialog/DownloadForm.tsx | 14 +++++++------- .../src/components/SearchPage/SearchFullUI.tsx | 8 ++++---- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx index 28f56bb2cc..929547fb33 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx @@ -19,24 +19,24 @@ import { BaseDialog } from '../../common/BaseDialog.tsx'; type DownloadDialogProps = { downloadUrlGenerator: DownloadUrlGenerator; sequenceFilter: SequenceFilter; - ReferenceGenomesMap: ReferenceGenomesMap; + referenceGenomesMap: ReferenceGenomesMap; allowSubmissionOfConsensusSequences: boolean; dataUseTermsEnabled: boolean; schema: Schema; richFastaHeaderFields: Schema['richFastaHeaderFields']; - selectedReferenceName: string | null; + selectedReferenceNames: Record; referenceIdentifierField: string | undefined; }; export const DownloadDialog: FC = ({ downloadUrlGenerator, sequenceFilter, - ReferenceGenomesMap, + referenceGenomesMap, allowSubmissionOfConsensusSequences, dataUseTermsEnabled, schema, richFastaHeaderFields, - selectedReferenceName, + selectedReferenceNames, referenceIdentifierField, }) => { const [isOpen, setIsOpen] = useState(false); @@ -45,8 +45,8 @@ export const DownloadDialog: FC = ({ const closeDialog = () => setIsOpen(false); const { nucleotideSequences, genes, useMultiSegmentEndpoint, defaultFastaHeaderTemplate } = useMemo( - () => getSequenceNames(ReferenceGenomesMap, selectedReferenceName), - [ReferenceGenomesMap, selectedReferenceName], + () => getSequenceNames(referenceGenomesMap, selectedReferenceNames), + [referenceGenomesMap, selectedReferenceNames], ); const [downloadFormState, setDownloadFormState] = useState( @@ -76,7 +76,7 @@ export const DownloadDialog: FC = ({ defaultFastaHeaderTemplate, getVisibleFields: () => [ ...Array.from(downloadFieldVisibilities.entries()) - .filter(([_, visibility]) => visibility.isVisible(selectedReferenceName)) + .filter(([_, visibility]) => visibility.isVisible(selectedReferenceNames)) .map(([name]) => name), ], metadata: schema.metadata, @@ -94,7 +94,7 @@ export const DownloadDialog: FC = ({
)} = ({ downloadFieldVisibilities={downloadFieldVisibilities} onSelectedFieldsChange={setSelectedFields} richFastaHeaderFields={richFastaHeaderFields} - selectedReferenceName={selectedReferenceName} + selectedReferenceNames={selectedReferenceNames} referenceIdentifierField={referenceIdentifierField} /> {dataUseTermsEnabled && ( diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx b/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx index ffadbe6b94..bbf7a55d98 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx @@ -30,7 +30,7 @@ export type DownloadFormState = { }; type DownloadFormProps = { - ReferenceGenomesMap: ReferenceGenomesMap; + referenceGenomesMap: ReferenceGenomesMap; downloadFormState: DownloadFormState; setDownloadFormState: Dispatch>; allowSubmissionOfConsensusSequences: boolean; @@ -39,12 +39,12 @@ type DownloadFormProps = { downloadFieldVisibilities: Map; onSelectedFieldsChange: Dispatch>>; richFastaHeaderFields: Schema['richFastaHeaderFields']; - selectedReferenceNames: Map; + selectedReferenceNames: Record; referenceIdentifierField: string | undefined; }; export const DownloadForm: FC = ({ - ReferenceGenomesMap, + referenceGenomesMap, downloadFormState, setDownloadFormState, allowSubmissionOfConsensusSequences, @@ -58,8 +58,8 @@ export const DownloadForm: FC = ({ }) => { const [isFieldSelectorOpen, setIsFieldSelectorOpen] = useState(false); const { nucleotideSequences, genes } = useMemo( - () => getSequenceNames(ReferenceGenomesMap, selectedReferenceNames), - [ReferenceGenomesMap, selectedReferenceNames], + () => getSequenceNames(referenceGenomesMap, selectedReferenceNames), + [referenceGenomesMap, selectedReferenceNames], ); const disableAlignedSequences = stillRequiresReferenceNameSelection( @@ -263,7 +263,7 @@ export const DownloadForm: FC = ({ export function getSequenceNames( referenceGenomesMap: ReferenceGenomesMap, - selectedReferenceNames: Map, + selectedReferenceNames: Record, ): { nucleotideSequences: SegmentInfo[]; genes: GeneInfo[]; @@ -277,7 +277,7 @@ export function getSequenceNames( for (const segmentName of segments) { const segmentData = referenceGenomesMap[segmentName]; - const selectedReferenceName = selectedReferenceNames.get(segmentName); + const selectedReferenceName = selectedReferenceNames[segmentName]; if (selectedReferenceName) { return { nucleotideSequences: [], diff --git a/website/src/components/SearchPage/SearchFullUI.tsx b/website/src/components/SearchPage/SearchFullUI.tsx index 8db83a4a43..e53d553e34 100644 --- a/website/src/components/SearchPage/SearchFullUI.tsx +++ b/website/src/components/SearchPage/SearchFullUI.tsx @@ -345,17 +345,17 @@ export const InnerSearchFullUI = ({ ) : null} - {/* */} + /> {linkOuts !== undefined && linkOuts.length > 0 && ( Date: Mon, 12 Jan 2026 21:39:00 +0100 Subject: [PATCH 34/71] fixup --- .../src/components/SearchPage/DownloadDialog/DownloadForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx b/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx index bbf7a55d98..ecbd110f6f 100644 --- a/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx +++ b/website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx @@ -278,7 +278,7 @@ export function getSequenceNames( for (const segmentName of segments) { const segmentData = referenceGenomesMap[segmentName]; const selectedReferenceName = selectedReferenceNames[segmentName]; - if (selectedReferenceName) { + if (!selectedReferenceName) { return { nucleotideSequences: [], genes: [], From d277d170460804cf7de47c834608306e74c9ea34 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:07:45 +0100 Subject: [PATCH 35/71] fix referenceSelector --- ...or.spec.tsx => ReferenceSelector.spec.tsx} | 62 ++++++++++--------- ...nismSelector.tsx => ReferenceSelector.tsx} | 50 +++++++-------- .../src/components/SearchPage/SearchForm.tsx | 7 ++- .../SequenceDetailsPage/DataTable.tsx | 6 +- website/src/types/referencesGenomes.ts | 41 ++++++------ 5 files changed, 82 insertions(+), 84 deletions(-) rename website/src/components/SearchPage/{SuborganismSelector.spec.tsx => ReferenceSelector.spec.tsx} (70%) rename website/src/components/SearchPage/{SuborganismSelector.tsx => ReferenceSelector.tsx} (66%) diff --git a/website/src/components/SearchPage/SuborganismSelector.spec.tsx b/website/src/components/SearchPage/ReferenceSelector.spec.tsx similarity index 70% rename from website/src/components/SearchPage/SuborganismSelector.spec.tsx rename to website/src/components/SearchPage/ReferenceSelector.spec.tsx index 30eb325071..db84bbc42e 100644 --- a/website/src/components/SearchPage/SuborganismSelector.spec.tsx +++ b/website/src/components/SearchPage/ReferenceSelector.spec.tsx @@ -2,8 +2,8 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it, vi } from 'vitest'; -import { SuborganismSelector } from './SuborganismSelector'; -import { type ReferenceGenomesMap } from '../../types/referencesGenomes'; +import { ReferenceSelector } from './ReferenceSelector.tsx'; +import { type ReferenceGenomesMap } from '../../types/referencesGenomes.ts'; import { MetadataFilterSchema } from '../../utils/search.ts'; const referenceIdentifierField = 'genotype'; @@ -17,34 +17,36 @@ const filterSchema = new MetadataFilterSchema([ ]); const mockreferenceGenomesMap: ReferenceGenomesMap = { - segments: { - main: { - references: ['suborganism1', 'suborganism2'], - insdcAccessions: {}, - genesByReference: {}, + main: { + suborganism1: { + sequence: 'ATCG', + insdcAccessionFull: 'ABC123', + }, + suborganism2: { + sequence: 'ATCG', + insdcAccessionFull: 'ABC123', }, }, }; const singleReferenceSchema: ReferenceGenomesMap = { - segments: { - main: { - references: ['single'], - insdcAccessions: {}, - genesByReference: {}, - }, + main: { + suborganism1: { + sequence: 'ATCG', + insdcAccessionFull: 'ABC123', + } }, }; -describe('SuborganismSelector', () => { +describe('ReferenceSelector', () => { it('renders nothing in single pathogen case', () => { const { container } = render( - , ); @@ -54,12 +56,12 @@ describe('SuborganismSelector', () => { it('renders selector UI in multi-pathogen case', () => { const setSelected = vi.fn(); render( - , ); @@ -71,28 +73,28 @@ describe('SuborganismSelector', () => { it('updates selection when changed', async () => { const setSelected = vi.fn(); render( - , ); await userEvent.selectOptions(screen.getByRole('combobox'), 'suborganism1'); - expect(setSelected).toHaveBeenCalledWith('suborganism1'); + expect(setSelected).toHaveBeenCalledWith({"main": "suborganism1"}); }); it('shows clear button and clears selection', async () => { const setSelected = vi.fn(); render( - , ); @@ -103,12 +105,12 @@ describe('SuborganismSelector', () => { it('throws error when suborganism field is not in config', () => { expect(() => render( - , ), ).toThrow('Cannot render suborganism selector'); diff --git a/website/src/components/SearchPage/SuborganismSelector.tsx b/website/src/components/SearchPage/ReferenceSelector.tsx similarity index 66% rename from website/src/components/SearchPage/SuborganismSelector.tsx rename to website/src/components/SearchPage/ReferenceSelector.tsx index e638903260..22a722db4a 100644 --- a/website/src/components/SearchPage/SuborganismSelector.tsx +++ b/website/src/components/SearchPage/ReferenceSelector.tsx @@ -1,49 +1,43 @@ import { type FC, useId, useMemo } from 'react'; -import type { ReferenceGenomesMap } from '../../types/referencesGenomes.ts'; +import { hasSegmentWithMultipleReferences, type ReferenceGenomesMap } from '../../types/referencesGenomes.ts'; import type { MetadataFilterSchema } from '../../utils/search.ts'; -import { Button } from '../common/Button'; +import { Button } from '../common/Button.tsx'; import MaterialSymbolsClose from '~icons/material-symbols/close'; -type SuborganismSelectorProps = { +type ReferenceSelectorProps = { filterSchema: MetadataFilterSchema; referenceGenomesMap: ReferenceGenomesMap; referenceIdentifierField: string; - selectedSuborganism: string | null; - setSelectedSuborganism: (newValue: string | null) => void; + setSelectedReferences: (newValues: Record) => void; + selectedReferences: Record; }; /** - * In the multi pathogen case, this is a prominent selector at the top to choose the suborganism. + * In the multi pathogen case, this is a prominent selector at the top to choose the reference. * Choosing a value here is required e.g. to enable mutation search and download of aligned sequences. * * Does nothing in the single pathogen case. */ -export const SuborganismSelector: FC = ({ +export const ReferenceSelector: FC = ({ filterSchema, referenceGenomesMap, referenceIdentifierField, - selectedSuborganism, - setSelectedSuborganism, + selectedReferences, + setSelectedReferences, }) => { const selectId = useId(); - // Extract reference names from the segments - const segments = Object.values(referenceGenomesMap.segments); - const suborganismNames = segments.length > 0 ? segments[0].references : []; - const isSinglePathogen = suborganismNames.length < 2; + if (!hasSegmentWithMultipleReferences(referenceGenomesMap)) { + return null; + } + + const segments = Object.keys(referenceGenomesMap); const label = useMemo(() => { - if (isSinglePathogen) { - return undefined; - } return filterSchema.filterNameToLabelMap()[referenceIdentifierField]; - }, [isSinglePathogen, filterSchema, referenceIdentifierField]); - - if (isSinglePathogen) { - return null; - } + }, [filterSchema, referenceIdentifierField]); if (label === undefined) { throw Error( @@ -59,23 +53,23 @@ export const SuborganismSelector: FC = ({
- {selectedSuborganism !== '' && selectedSuborganism !== null && ( + {selectedReferences[segments[0]] !== '' && selectedReferences[segments[0]] !== null && ( - )} -
-

- Select a {formatLabel(label)} to enable mutation search and download of aligned sequences -

-
+ + +
+ + + {selectedReferences[segment] && ( + + )} +
+ +

+ Select a {formatLabel(label)} to enable mutation search and download of aligned sequences +

+
+ ); + })} + ); }; + export const formatLabel = (label: string) => { if (label === label.toUpperCase()) { return label; // all caps, keep as is diff --git a/website/src/types/referencesGenomes.ts b/website/src/types/referencesGenomes.ts index 63e4c7f10e..89c71856ed 100644 --- a/website/src/types/referencesGenomes.ts +++ b/website/src/types/referencesGenomes.ts @@ -32,10 +32,6 @@ export const ReferenceGenomesMap = z.record( ); export type ReferenceGenomesMap = z.infer; -export function hasSegmentWithMultipleReferences(referenceGenomesMap: ReferenceGenomesMap): boolean { - return Object.values(referenceGenomesMap).some((references) => Object.keys(references).length > 1); -} - export const referenceGenomesSchema = z .array( z.object({ From 4d12b344879ec14777b6b6d90a30d8647e0e793c Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:27:52 +0100 Subject: [PATCH 37/71] fix more --- .../getSegmentAndGeneDisplayNameMap.tsx | 26 +-- .../SearchPage/SegmentReferenceSelector.tsx | 155 ------------------ 2 files changed, 13 insertions(+), 168 deletions(-) delete mode 100644 website/src/components/SearchPage/SegmentReferenceSelector.tsx diff --git a/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx b/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx index 685d9e88fb..f31a7c8ffd 100644 --- a/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx +++ b/website/src/components/ReviewPage/getSegmentAndGeneDisplayNameMap.tsx @@ -4,30 +4,30 @@ export function getSegmentAndGeneDisplayNameMap( ReferenceGenomesMap: ReferenceGenomesMap, ): Map { const mappingEntries: [string, string][] = []; + const multiSegmented = Object.keys(ReferenceGenomesMap).length > 1; // Iterate through all segments and references - for (const [segmentName, segmentData] of Object.entries(ReferenceGenomesMap.segments)) { + for (const [segmentName, references] of Object.entries(ReferenceGenomesMap)) { // If only one reference, no prefix needed - if (segmentData.references.length === 1) { - // LAPIS name is just the segment name + if (Object.keys(references).length === 1) { mappingEntries.push([segmentName, segmentName]); - // Add genes for this segment/reference - const singleRef = segmentData.references[0]; - const genes = segmentData.genesByReference[singleRef] ?? []; - for (const geneName of genes) { + for (const referenceName of Object.keys(references)) { + const genes = references[referenceName].genes ?? {}; + for (const geneName of Object.keys(genes)) { mappingEntries.push([geneName, geneName]); } + } } else { - // Multiple references: use {reference}-{segment} format - for (const referenceName of segmentData.references) { - const lapisSegmentName = `${referenceName}-${segmentName}`; + // Multiple references: use {segment}-{reference} format + for (const referenceName of Object.keys(references)) { + const lapisSegmentName = multiSegmented ? `${segmentName}-${referenceName}` : referenceName; mappingEntries.push([lapisSegmentName, segmentName]); // Add genes for this segment/reference - const genes = segmentData.genesByReference[referenceName] ?? []; - for (const geneName of genes) { - const lapisGeneName = `${referenceName}-${geneName}`; + const genes = references[referenceName].genes ?? {}; + for (const geneName of Object.keys(genes)) { + const lapisGeneName = `${geneName}-${referenceName}`; mappingEntries.push([lapisGeneName, geneName]); } } diff --git a/website/src/components/SearchPage/SegmentReferenceSelector.tsx b/website/src/components/SearchPage/SegmentReferenceSelector.tsx deleted file mode 100644 index 8d96243327..0000000000 --- a/website/src/components/SearchPage/SegmentReferenceSelector.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'; -import { type FC, useId } from 'react'; - -import type { ReferenceGenomesMap } 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: ReferenceGenomesMap; - 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 && ( - - )} -
-
- ); -}; From cae8b3b449b30460313f1def79664786a6042067 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:30:47 +0100 Subject: [PATCH 38/71] finally --- website/src/components/SequenceDetailsPage/getTableData.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/components/SequenceDetailsPage/getTableData.ts b/website/src/components/SequenceDetailsPage/getTableData.ts index 2899443745..1c576753e9 100644 --- a/website/src/components/SequenceDetailsPage/getTableData.ts +++ b/website/src/components/SequenceDetailsPage/getTableData.ts @@ -12,7 +12,7 @@ import { type InsertionCount, type MutationProportionCount, } from '../../types/lapis.ts'; -import { type ReferenceGenomes } from '../../types/referencesGenomes.ts'; +import { ReferenceGenomesMap } from '../../types/referencesGenomes.ts'; import { parseUnixTimestamp } from '../../utils/parseUnixTimestamp.ts'; export type GetTableDataResult = { @@ -24,7 +24,7 @@ export type GetTableDataResult = { export async function getTableData( accessionVersion: string, schema: Schema, - referenceGenomes: ReferenceGenomes, + referenceGenomes: ReferenceGenomesMap, lapisClient: LapisClient, ): Promise> { return Promise.all([ @@ -78,7 +78,7 @@ export async function getTableData( function getSegmentReferences( details: Details, schema: Schema, - referenceGenomes: ReferenceGenomes, + referenceGenomes: ReferenceGenomesMap, accessionVersion: string, ): Result | null, ProblemDetail> { const segments = Object.keys(referenceGenomes); From a7d7e5fa63c066bab0aa21b20863c5f3a83e5098 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Tue, 13 Jan 2026 08:55:06 +0100 Subject: [PATCH 39/71] fixup --- website/src/utils/getSegmentAndGeneInfo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/utils/getSegmentAndGeneInfo.tsx b/website/src/utils/getSegmentAndGeneInfo.tsx index ce6e0eb6f2..c327b95ec9 100644 --- a/website/src/utils/getSegmentAndGeneInfo.tsx +++ b/website/src/utils/getSegmentAndGeneInfo.tsx @@ -36,7 +36,7 @@ export function getSegmentAndGeneInfo( nucleotideSegmentInfos.push(getSegmentInfoWithReference(segmentName, refForNaming)); if (selectedRef && segmentData.genes) { - const geneNames = segmentData.genes.map((gene) => gene.name); + const geneNames = Object.keys(segmentData.genes); for (const geneName of geneNames) { geneInfos.push(getGeneInfoWithReference(geneName, refForNaming)); } From febf14e591a13869fca19157a75c532a38c1e53d Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Tue, 13 Jan 2026 08:56:33 +0100 Subject: [PATCH 40/71] fix EVs --- .../nextclade/src/loculus_preprocessing/config.py | 7 +++---- .../nextclade/src/loculus_preprocessing/nextclade.py | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/preprocessing/nextclade/src/loculus_preprocessing/config.py b/preprocessing/nextclade/src/loculus_preprocessing/config.py index faca4f991d..c79a0379f6 100644 --- a/preprocessing/nextclade/src/loculus_preprocessing/config.py +++ b/preprocessing/nextclade/src/loculus_preprocessing/config.py @@ -77,8 +77,8 @@ class NextcladeSequenceAndDataset(BaseModel): nextclade_dataset_tag: str | None = None nextclade_dataset_server: str | None = None accepted_sort_matches: list[str] = Field(default_factory=list) - gene_prefix: str | None = None - # Names of genes in the Nextclade dataset; when concatenated with gene_prefix + gene_suffix: str | None = None + # Names of genes in the Nextclade dataset; when concatenated with gene_suffix # this must match the gene names expected by the backend and LAPIS genes: list[str] = Field(default_factory=list) @@ -148,8 +148,7 @@ def build_ds( if ds.nextclade_dataset_server is None: ds.nextclade_dataset_server = self.nextclade_dataset_server ds.name = set_sequence_name(multi_reference, self.multi_segment, ds) - # TODO: this should be a suffix in future - ds.gene_prefix = ds.reference_name if multi_reference else None + ds.gene_suffix = ds.reference_name if multi_reference else None return ds datasets: list[NextcladeSequenceAndDataset] = [] diff --git a/preprocessing/nextclade/src/loculus_preprocessing/nextclade.py b/preprocessing/nextclade/src/loculus_preprocessing/nextclade.py index d0554d6964..aab20a49dd 100644 --- a/preprocessing/nextclade/src/loculus_preprocessing/nextclade.py +++ b/preprocessing/nextclade/src/loculus_preprocessing/nextclade.py @@ -87,8 +87,8 @@ def mask_terminal_gaps( ) -def create_gene_name(gene: str, gene_prefix: str | None) -> str: - return gene_prefix + "-" + gene if gene_prefix else gene +def create_gene_name(gene: str, gene_suffix: str | None) -> str: + return gene + "-" + gene_suffix if gene_suffix else gene def parse_nextclade_tsv( @@ -119,7 +119,7 @@ def parse_nextclade_tsv( continue gene, val = ins.split(":", maxsplit=1) if gene in sequence_and_dataset.genes: - gene_name = create_gene_name(gene, sequence_and_dataset.gene_prefix) + gene_name = create_gene_name(gene, sequence_and_dataset.gene_suffix) amino_acid_insertions[id][gene_name].append(val) else: logger.debug( @@ -629,7 +629,7 @@ def load_aligned_aa_sequences( for aligned_sequence in aligned_translation: sequence_id = aligned_sequence.id masked_sequence = mask_terminal_gaps(str(aligned_sequence.seq), mask_char="X") - gene_name = create_gene_name(gene, sequence_and_dataset.gene_prefix) + gene_name = create_gene_name(gene, sequence_and_dataset.gene_suffix) aligned_aminoacid_sequences[sequence_id][gene_name] = masked_sequence except FileNotFoundError: # This can happen if the sequence does not cover this gene From 88acb2739ef53f1b6f817c257f4438f85f93ae0f Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:37:49 +0100 Subject: [PATCH 41/71] fixup --- website/src/components/SearchPage/SearchFullUI.tsx | 1 - website/src/pages/[organism]/search/index.astro | 2 -- website/src/utils/getSegmentAndGeneInfo.tsx | 4 ++-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/website/src/components/SearchPage/SearchFullUI.tsx b/website/src/components/SearchPage/SearchFullUI.tsx index e53d553e34..f663e12121 100644 --- a/website/src/components/SearchPage/SearchFullUI.tsx +++ b/website/src/components/SearchPage/SearchFullUI.tsx @@ -105,7 +105,6 @@ export const InnerSearchFullUI = ({ setAColumnVisibility, } = useSearchPageState({ initialQueryDict, schema, hiddenFieldValues, filterSchema }); - console.log('selectedReferences', selectedReferences); const searchVisibilities = useMemo(() => { return getFieldVisibilitiesFromQuery(schema, state); diff --git a/website/src/pages/[organism]/search/index.astro b/website/src/pages/[organism]/search/index.astro index 5dc439b49e..eaebc01bcc 100644 --- a/website/src/pages/[organism]/search/index.astro +++ b/website/src/pages/[organism]/search/index.astro @@ -34,8 +34,6 @@ if (!cleanedOrganism) { const clientConfig = getRuntimeConfig().public; const schema = getSchema(cleanedOrganism.key); -console.log('cleanedOrganism.key:', cleanedOrganism.key); - const accessToken = getAccessToken(Astro.locals.session); const myGroups = accessToken !== undefined ? await getMyGroups(accessToken) : []; diff --git a/website/src/utils/getSegmentAndGeneInfo.tsx b/website/src/utils/getSegmentAndGeneInfo.tsx index c327b95ec9..1efd20e2b1 100644 --- a/website/src/utils/getSegmentAndGeneInfo.tsx +++ b/website/src/utils/getSegmentAndGeneInfo.tsx @@ -35,8 +35,8 @@ export function getSegmentAndGeneInfo( nucleotideSegmentInfos.push(getSegmentInfoWithReference(segmentName, refForNaming)); - if (selectedRef && segmentData.genes) { - const geneNames = Object.keys(segmentData.genes); + if (selectedRef && segmentData[selectedRef].genes) { + const geneNames = Object.keys(segmentData[selectedRef].genes); for (const geneName of geneNames) { geneInfos.push(getGeneInfoWithReference(geneName, refForNaming)); } From aaf4871c84e06b38a1eeef55ec0aed61740544e6 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:41:39 +0100 Subject: [PATCH 42/71] fix tests --- .../nextclade/tests/test_nextclade_preprocessing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/preprocessing/nextclade/tests/test_nextclade_preprocessing.py b/preprocessing/nextclade/tests/test_nextclade_preprocessing.py index f7a961d9df..3622209336 100644 --- a/preprocessing/nextclade/tests/test_nextclade_preprocessing.py +++ b/preprocessing/nextclade/tests/test_nextclade_preprocessing.py @@ -1312,10 +1312,10 @@ def test_create_flatfile(): }, nucleotideInsertions={}, alignedAminoAcidSequences={ - "ebola-zaire-VP24EbolaZaire": ebola_zaire_aa( + "VP24EbolaZaire-ebola-zaire": ebola_zaire_aa( sequence_with_mutation("ebola-zaire"), "VP24" ), - "ebola-zaire-LEbolaZaire": ebola_zaire_aa( + "LEbolaZaire-ebola-zaire": ebola_zaire_aa( sequence_with_mutation("ebola-zaire"), "L" ), }, @@ -1406,7 +1406,7 @@ def test_preprocessing_multi_reference(test_case_def: Case): }, nucleotideInsertions={}, alignedAminoAcidSequences={ - "1and6-NP": cchf_s_aa(consensus_sequence("cchf-S-1and6"), "1and6"), + "NP-1and6": cchf_s_aa(consensus_sequence("cchf-S-1and6"), "1and6"), }, aminoAcidInsertions={}, sequenceNameToFastaId={"S-1and6": "seg1"}, @@ -1442,7 +1442,7 @@ def test_preprocessing_multi_reference(test_case_def: Case): }, nucleotideInsertions={}, alignedAminoAcidSequences={ - "1and6-NP": cchf_s_aa(consensus_sequence("cchf-S-1and6"), "1and6"), + "NP-1and6": cchf_s_aa(consensus_sequence("cchf-S-1and6"), "1and6"), "RdRp": cchf_l_aa(consensus_sequence("cchf-L")), }, aminoAcidInsertions={}, From 1c7fc11c6e7683b5dc0f0377a0af9f2761f64ce2 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:01:42 +0100 Subject: [PATCH 43/71] cut down code complexity --- backend/docs/organismWithSuborganisms.md | 46 ++++++++------- .../templates/_merged-reference-genomes.tpl | 56 ++++++------------- 2 files changed, 40 insertions(+), 62 deletions(-) diff --git a/backend/docs/organismWithSuborganisms.md b/backend/docs/organismWithSuborganisms.md index a07f7892a9..7dbc748857 100644 --- a/backend/docs/organismWithSuborganisms.md +++ b/backend/docs/organismWithSuborganisms.md @@ -88,22 +88,20 @@ defaultOrganisms: # `referenceGenomes` is now an object { suborganismName: referenceGenomeOfThatSuborganism } # The special suborganism name `singleReference` must be used when there is only a single suborganism referenceGenomes: - CV-A10: - nucleotideSequences: - - name: main + - name: main + references: + - reference_name: CV-A10 sequence: "..." insdcAccessionFull: ... - genes: - - name: VP4 - sequence: "..." - EV-A71: - nucleotideSequences: - - name: main + genes: + - name: VP4 + sequence: "..." + - reference_name: EV-A71 sequence: "..." insdcAccessionFull: ... - genes: - - name: VP2 - sequence: "..." + genes: + - name: VP2 + sequence: "..." ``` The website will then receive the `referenceGenomes` as configured above. @@ -322,10 +320,10 @@ The reference genome will be a product "suborganism x segment": {"name": "suborganism2", "sequence": "..."} ], "genes": [ - {"name": "suborganism1_gene1", "sequence": "..."}, - {"name": "suborganism1_gene2", "sequence": "..."}, - {"name": "suborganism2_gene1", "sequence": "..."}, - {"name": "suborganism2_gene2", "sequence": "..."} + {"name": "gene1_suborganism1", "sequence": "..."}, + {"name": "gene2_suborganism1", "sequence": "..."}, + {"name": "gene1_suborganism2", "sequence": "..."}, + {"name": "gene2_suborganism2", "sequence": "..."} ] } ``` @@ -335,16 +333,16 @@ for multi-segment: ```json { "nucleotideSequences": [ - {"name": "suborganism1_segment1", "sequence": "..."}, - {"name": "suborganism1_segment2", "sequence": "..."}, - {"name": "suborganism2_segment1", "sequence": "..."}, - {"name": "suborganism2_segment2", "sequence": "..."} + {"name": "segment1_suborganism1", "sequence": "..."}, + {"name": "segment2_suborganism1", "sequence": "..."}, + {"name": "segment1_suborganism2", "sequence": "..."}, + {"name": "segment2_suborganism2", "sequence": "..."} ], "genes": [ - {"name": "suborganism1_gene1", "sequence": "..."}, - {"name": "suborganism1_gene2", "sequence": "..."}, - {"name": "suborganism2_gene1", "sequence": "..."}, - {"name": "suborganism2_gene2", "sequence": "..."} + {"name": "gene1_suborganism1", "sequence": "..."}, + {"name": "gene2_suborganism1", "sequence": "..."}, + {"name": "gene1_suborganism2", "sequence": "..."}, + {"name": "gene2_suborganism2", "sequence": "..."} ] } ``` diff --git a/kubernetes/loculus/templates/_merged-reference-genomes.tpl b/kubernetes/loculus/templates/_merged-reference-genomes.tpl index f85fa987c8..746e82279a 100644 --- a/kubernetes/loculus/templates/_merged-reference-genomes.tpl +++ b/kubernetes/loculus/templates/_merged-reference-genomes.tpl @@ -14,55 +14,35 @@ {{- $segmentName := $segment.name -}} {{- $singleReference := eq (len $segment.references) 1 -}} {{- range $reference := $segment.references -}} + {{- $referenceName := $reference.reference_name -}} {{- if $singleReference -}} {{/* Single reference mode - no suffix */}} {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict "name" $segmentName "sequence" $reference.sequence ) -}} + {{- else -}} + {{- $name := printf "%s%s" (ternary "" (print $segmentName "_") $singleSegment) $referenceName -}} + {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict + "name" $name + "sequence" $reference.sequence + ) -}} + {{- end -}} - {{/* Add genes if present */}} - {{- if $reference.genes -}} - {{- range $gene := $reference.genes -}} + {{/* Add genes if present */}} + {{- if $reference.genes -}} + {{- range $gene := $reference.genes -}} + {{- if $singleReference -}} {{- $lapisGenes = append $lapisGenes (dict "name" $gene.name "sequence" $gene.sequence ) -}} - {{- end -}} - {{- end -}} - {{- else -}} - {{- if $singleSegment -}} - {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict - "name" $reference.reference_name - "sequence" $reference.sequence - ) -}} - - {{/* Add genes if present */}} - {{- if $reference.genes -}} - {{- $referenceSuffix := printf "_%s" $reference.reference_name -}} - {{- range $gene := $reference.genes -}} - {{- $lapisGenes = append $lapisGenes (dict - "name" (printf "%s%s" $gene.name $referenceSuffix) - "sequence" $gene.sequence - ) -}} - {{- end -}} - {{- end -}} - {{- else -}} - {{/* Multiple references mode - add suffix to names */}} - {{- $referenceSuffix := printf "_%s" $reference.reference_name -}} - {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict - "name" (printf "%s%s" $segmentName $referenceSuffix) - "sequence" $reference.sequence - ) -}} - - {{/* Add genes if present */}} - {{- if $reference.genes -}} - {{- range $gene := $reference.genes -}} - {{- $lapisGenes = append $lapisGenes (dict - "name" (printf "%s%s" $gene.name $referenceSuffix) - "sequence" $gene.sequence - ) -}} - {{- end -}} + {{- else -}} + {{- $geneName := printf "%s_%s" $gene.name $referenceName -}} + {{- $lapisGenes = append $lapisGenes (dict + "name" $geneName + "sequence" $gene.sequence + ) -}} {{- end -}} {{- end -}} {{- end -}} From ac4e5ffcf77afa062cce1b21169cd8cba6f1fa7c Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:04:29 +0100 Subject: [PATCH 44/71] revert formatting changes --- website/.prettierrc | 32 ++++++++++++++++---------------- website/README.md | 26 +++++++++++++------------- website/tests/config/README.md | 2 +- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/website/.prettierrc b/website/.prettierrc index a1e3b6cf86..b4af6d7013 100644 --- a/website/.prettierrc +++ b/website/.prettierrc @@ -1,18 +1,18 @@ { - "printWidth": 120, - "tabWidth": 4, - "trailingComma": "all", - "semi": true, - "jsxSingleQuote": true, - "singleQuote": true, - "quoteProps": "consistent", - "plugins": ["prettier-plugin-astro"], - "overrides": [ - { - "files": "*.astro", - "options": { - "parser": "astro" - } - } - ] + "printWidth": 120, + "tabWidth": 4, + "trailingComma": "all", + "semi": true, + "jsxSingleQuote": true, + "singleQuote": true, + "quoteProps": "consistent", + "plugins": ["prettier-plugin-astro"], + "overrides": [ + { + "files": "*.astro", + "options": { + "parser": "astro" + } + } + ] } diff --git a/website/README.md b/website/README.md index 75114a3333..4da8d28dc4 100644 --- a/website/README.md +++ b/website/README.md @@ -9,11 +9,11 @@ In order to run the website locally you will need to install [nodejs](https://no ### Local Development -- Set up your `.env` file, e.g. by copying `.env.example` with `cp .env.example .env` -- Install packages: `npm ci` (`ci` as opposed to `install` makes sure to install the exact versions specified in `package-lock.json`) -- Generate config files for local testing (requires Helm installed): `../generate_local_test_config.sh`. If you are not running the backend locally, run `../generate_local_test_config.sh --from-live` to point to the backend from the live server (preview of the `main` branch) or `../generate_local_test_config.sh --from-live --live-host main.loculus.org` to specify a particular host which can also be a preview. -- Run `npm run start` to start a local development server with hot reloading. -- Run `npm run format-fast` to format the code. +- Set up your `.env` file, e.g. by copying `.env.example` with `cp .env.example .env` +- Install packages: `npm ci` (`ci` as opposed to `install` makes sure to install the exact versions specified in `package-lock.json`) +- Generate config files for local testing (requires Helm installed): `../generate_local_test_config.sh`. If you are not running the backend locally, run `../generate_local_test_config.sh --from-live` to point to the backend from the live server (preview of the `main` branch) or `../generate_local_test_config.sh --from-live --live-host main.loculus.org` to specify a particular host which can also be a preview. +- Run `npm run start` to start a local development server with hot reloading. +- Run `npm run format-fast` to format the code. ### Unit Tests @@ -32,9 +32,9 @@ See `.env.docker` for the required variables. Furthermore, the website requires config files that need to be present at runtime in the directory specified in the `CONFIG_DIR` environment variable: -- `website_config.json`: Contains configuration on the underlying organism. It's similar to the database config file that LAPIS uses. -- `reference_genomes.json`: Defines names for segments of the genome and amino acids. It's equal to the file that LAPIS uses. -- `runtime_config.json`: Contains configuration that specific for a deployed instance of the website. +- `website_config.json`: Contains configuration on the underlying organism. It's similar to the database config file that LAPIS uses. +- `reference_genomes.json`: Defines names for segments of the genome and amino acids. It's equal to the file that LAPIS uses. +- `runtime_config.json`: Contains configuration that specific for a deployed instance of the website. Check our tests and examples for working config files. @@ -47,17 +47,17 @@ If the environment variable LOG_DIR is set, it will also store them in `LOG_DIR/ ### Editor -- [Astro](https://docs.astro.build/en/editor-setup/) +- [Astro](https://docs.astro.build/en/editor-setup/) ### Setup -- Install node version from `.nvmrc` with `nvm install` +- Install node version from `.nvmrc` with `nvm install` ### General tips -- Available scripts can be browsed in [`package.json`](./package.json) or by running `npm run` -- For VS code, use the ESlint extension which must be configured with `"eslint.workingDirectories": ["./website"],` in the settings.json -- Tips & Tricks for using icons from MUI https://mui.com/material-ui/guides/minimizing-bundle-size/ +- Available scripts can be browsed in [`package.json`](./package.json) or by running `npm run` +- For VS code, use the ESlint extension which must be configured with `"eslint.workingDirectories": ["./website"],` in the settings.json +- Tips & Tricks for using icons from MUI https://mui.com/material-ui/guides/minimizing-bundle-size/ ### Codemods diff --git a/website/tests/config/README.md b/website/tests/config/README.md index b72e28f35c..5aadd310e7 100644 --- a/website/tests/config/README.md +++ b/website/tests/config/README.md @@ -1,3 +1,3 @@ Config files are written to this directory by the deploy script. -[TODO (https://github.com/loculus-project/loculus/issues/5541): use a different location] +[TODO (https://github.com/loculus-project/loculus/issues/5541): use a different location] \ No newline at end of file From 69cc8c4a5a9beef9078a629ec2dfacb9b3f9c438 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:12:26 +0100 Subject: [PATCH 45/71] more fixes --- .../loculus/templates/_merged-reference-genomes.tpl | 2 +- website/src/utils/sequenceTypeHelpers.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/kubernetes/loculus/templates/_merged-reference-genomes.tpl b/kubernetes/loculus/templates/_merged-reference-genomes.tpl index 746e82279a..c730c3f447 100644 --- a/kubernetes/loculus/templates/_merged-reference-genomes.tpl +++ b/kubernetes/loculus/templates/_merged-reference-genomes.tpl @@ -22,7 +22,7 @@ "sequence" $reference.sequence ) -}} {{- else -}} - {{- $name := printf "%s%s" (ternary "" (print $segmentName "_") $singleSegment) $referenceName -}} + {{- $name := printf "%s%s" (ternary "" (printf "%s_" $segmentName) $singleSegment) $referenceName -}} {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict "name" $name "sequence" $reference.sequence diff --git a/website/src/utils/sequenceTypeHelpers.ts b/website/src/utils/sequenceTypeHelpers.ts index 51633de91d..fbda32b747 100644 --- a/website/src/utils/sequenceTypeHelpers.ts +++ b/website/src/utils/sequenceTypeHelpers.ts @@ -80,9 +80,9 @@ export function getSegmentInfoWithReference(segmentName: string, referenceName: label: segmentName, }; } - // Reference selected - prefix with reference name for LAPIS + // Reference selected - suffix with reference name for LAPIS return { - lapisName: `${referenceName}-${segmentName}`, + lapisName: `${segmentName}-${referenceName}`, label: segmentName, }; } @@ -101,9 +101,9 @@ export function getGeneInfoWithReference(geneName: string, referenceName: string label: geneName, }; } - // Reference selected - prefix with reference name for LAPIS + // Reference selected - suffix with reference name for LAPIS return { - lapisName: `${referenceName}-${geneName}`, + lapisName: `${geneName}-${referenceName}`, label: geneName, }; } From 5e26002fb73f5c56b813b1d27547cada2f170b06 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:22:26 +0100 Subject: [PATCH 46/71] wupps --- kubernetes/loculus/templates/_merged-reference-genomes.tpl | 4 ++-- website/src/components/SearchPage/SearchForm.tsx | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/kubernetes/loculus/templates/_merged-reference-genomes.tpl b/kubernetes/loculus/templates/_merged-reference-genomes.tpl index c730c3f447..03ea284b23 100644 --- a/kubernetes/loculus/templates/_merged-reference-genomes.tpl +++ b/kubernetes/loculus/templates/_merged-reference-genomes.tpl @@ -22,7 +22,7 @@ "sequence" $reference.sequence ) -}} {{- else -}} - {{- $name := printf "%s%s" (ternary "" (printf "%s_" $segmentName) $singleSegment) $referenceName -}} + {{- $name := printf "%s%s" (ternary "" (printf "%s-" $segmentName) $singleSegment) $referenceName -}} {{- $lapisNucleotideSequences = append $lapisNucleotideSequences (dict "name" $name "sequence" $reference.sequence @@ -38,7 +38,7 @@ "sequence" $gene.sequence ) -}} {{- else -}} - {{- $geneName := printf "%s_%s" $gene.name $referenceName -}} + {{- $geneName := printf "%s-%s" $gene.name $referenceName -}} {{- $lapisGenes = append $lapisGenes (dict "name" $geneName "sequence" $gene.sequence diff --git a/website/src/components/SearchPage/SearchForm.tsx b/website/src/components/SearchPage/SearchForm.tsx index f434b7e015..0e2ce33b67 100644 --- a/website/src/components/SearchPage/SearchForm.tsx +++ b/website/src/components/SearchPage/SearchForm.tsx @@ -106,13 +106,11 @@ export const SearchForm = ({ isChecked: searchVisibilities.get(filter.name)?.isChecked ?? false, })); - console.log("selectedReferences:", selectedReferences); const suborganismSegmentAndGeneInfo = useMemo( () => getSegmentAndGeneInfo(referenceGenomesMap, selectedReferences), [referenceGenomesMap, selectedReferences], ); - console.log("suborganismSegmentAndGeneInfo:", suborganismSegmentAndGeneInfo); return ( From 9fc950d804549925045a7d9f7f3214310e45768e Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:52:19 +0100 Subject: [PATCH 47/71] ugly fix --- .../SequenceDetailsPage/getTableData.ts | 57 ++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/website/src/components/SequenceDetailsPage/getTableData.ts b/website/src/components/SequenceDetailsPage/getTableData.ts index 1c576753e9..204bc7d674 100644 --- a/website/src/components/SequenceDetailsPage/getTableData.ts +++ b/website/src/components/SequenceDetailsPage/getTableData.ts @@ -17,7 +17,7 @@ import { parseUnixTimestamp } from '../../utils/parseUnixTimestamp.ts'; export type GetTableDataResult = { data: TableDataEntry[]; - segmentReferences: Record | null; + segmentReferences: Record; isRevocation: boolean; }; @@ -80,8 +80,10 @@ function getSegmentReferences( schema: Schema, referenceGenomes: ReferenceGenomesMap, accessionVersion: string, -): Result | null, ProblemDetail> { +): Result, ProblemDetail> { + //TODO: this is duplicated - refactor to share code const segments = Object.keys(referenceGenomes); + const segmentReferences: Record = {}; // Check if single reference mode (only one reference per segment) const firstSegment = segments[0]; @@ -90,7 +92,6 @@ function getSegmentReferences( 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) { @@ -100,9 +101,10 @@ function getSegmentReferences( return ok(segmentReferences); } + // TODO: extend to multi segment, multi reference mode // Multiple references mode - get from metadata field - const suborganismField = schema.referenceIdentifierField; - if (suborganismField === undefined) { + const referenceField = schema.referenceIdentifierField; + if (referenceField === undefined) { return err({ type: 'about:blank', title: 'Invalid configuration', @@ -112,21 +114,21 @@ function getSegmentReferences( }); } - const value = details[suborganismField]; - const suborganismResult = z.string().nullable().safeParse(value); - if (!suborganismResult.success) { + const value = details[referenceField]; + const referenceResult = z.string().nullable().safeParse(value); + if (!referenceResult.success) { return err({ type: 'about:blank', - title: 'Invalid suborganism field', + title: 'Invalid reference field', status: 0, - detail: `Value '${value}' of field '${suborganismField}' is not a valid string or null.`, + detail: `Value '${value}' of field '${referenceField}' is not a valid string or null.`, instance: '/seq/' + accessionVersion, }); } - const referenceName = suborganismResult.data; + const referenceName = referenceResult.data; if (referenceName === null) { - return ok(null); + return ok(segmentReferences); } // Validate that the reference exists in at least one segment @@ -141,15 +143,14 @@ function getSegmentReferences( if (!foundInAnySegment) { return err({ type: 'about:blank', - title: 'Invalid suborganism', + title: 'Invalid reference', status: 0, - detail: `ReferenceName '${referenceName}' (value of field '${suborganismField}') not found in reference genomes.`, + detail: `ReferenceName '${referenceName}' (value of field '${referenceField}') not found in reference genomes.`, instance: '/seq/' + accessionVersion, }); } // Build segment references - all segments use the same reference - const segmentReferences: Record = {}; for (const segmentName of segments) { segmentReferences[segmentName] = referenceName; } @@ -184,7 +185,7 @@ function mutationDetails( aminoAcidMutations: MutationProportionCount[], nucleotideInsertions: InsertionCount[], aminoAcidInsertions: InsertionCount[], - segmentReferences: Record | null, + segmentReferences: Record, ): TableDataEntry[] { const data: TableDataEntry[] = [ { @@ -243,7 +244,7 @@ function mutationDetails( function toTableData( config: Schema, - segmentReferences: Record | null, + segmentReferences: Record, { details, nucleotideMutations, @@ -299,7 +300,7 @@ function mapValueToDisplayedValue(value: undefined | null | string | number | bo export function substitutionsMap( mutationData: MutationProportionCount[], - segmentReferences: Record | null, + segmentReferences: Record, ): SegmentedMutations[] { const result: SegmentedMutations[] = []; const substitutionData = mutationData.filter((m) => m.mutationTo !== '-'); @@ -326,23 +327,27 @@ export function substitutionsMap( function computeSequenceDisplayName( originalSequenceName: string | null, - segmentReferences: Record | null, + segmentReferences: Record, ): string | null { + //TODO: this is bad design, instead define the label in the config and then just apply a map here if (originalSequenceName === null || segmentReferences === null) { return originalSequenceName; } - // Try to strip any reference prefix from the sequence name + // Try to strip any reference suffix 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); + // Try to strip the reference suffix + const suffixToTrim = `-${referenceName}`; + if (originalSequenceName.endsWith(suffixToTrim)) { + return originalSequenceName.substring( + 0, + originalSequenceName.length - suffixToTrim.length + ); } } @@ -351,7 +356,7 @@ function computeSequenceDisplayName( function deletionsToCommaSeparatedString( mutationData: MutationProportionCount[], - segmentReferences: Record | null, + segmentReferences: Record, ) { const segmentPositions = new Map(); mutationData @@ -403,7 +408,7 @@ function deletionsToCommaSeparatedString( function insertionsToCommaSeparatedString( insertionData: InsertionCount[], - segmentReferences: Record | null, + segmentReferences: Record, ) { return insertionData .map((insertion) => { From dbe896c2026e40d95cc1f6ffb4cd8f0deb6e5b8f Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:54:16 +0100 Subject: [PATCH 48/71] fixes --- website/src/utils/search.ts | 3 +++ website/src/utils/sequenceTypeHelpers.ts | 6 ------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/website/src/utils/search.ts b/website/src/utils/search.ts index 0ffb01aa0d..153b9d3335 100644 --- a/website/src/utils/search.ts +++ b/website/src/utils/search.ts @@ -48,6 +48,9 @@ export class MetadataVisibility { if (!this.isChecked) { return false; } + if (this.onlyForReference == undefined){ + return true; + } for (const value of Object.values(selectedReferenceNames)) { if (this.onlyForReference === value) { return true; diff --git a/website/src/utils/sequenceTypeHelpers.ts b/website/src/utils/sequenceTypeHelpers.ts index fbda32b747..5a7208b04a 100644 --- a/website/src/utils/sequenceTypeHelpers.ts +++ b/website/src/utils/sequenceTypeHelpers.ts @@ -17,12 +17,6 @@ export type GeneInfo = { label: string; }; -export function getMultiPathogenNucleotideSequenceNames(nucleotideSequences: string[], suborganism: string) { - return nucleotideSequences.length === 1 - ? [{ lapisName: suborganism, label: 'main' }] - : nucleotideSequences.map((name) => getMultiPathogenSequenceName(name, suborganism)); -} - export function getSinglePathogenSequenceName(name: string): SegmentInfo | GeneInfo { return { lapisName: name, From e44b9ee56a172a8882e6d8f2a80bb744a46fc1f8 Mon Sep 17 00:00:00 2001 From: anna-parker <50943381+anna-parker@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:43:02 +0100 Subject: [PATCH 49/71] set state correctly --- .../SearchPage/ReferenceSelector.tsx | 2 +- .../components/SearchPage/SearchFullUI.tsx | 2 +- .../SearchPage/useSearchPageState.ts | 47 +++++++++++++++++-- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/website/src/components/SearchPage/ReferenceSelector.tsx b/website/src/components/SearchPage/ReferenceSelector.tsx index 8d31d24e23..6d29b23ecd 100644 --- a/website/src/components/SearchPage/ReferenceSelector.tsx +++ b/website/src/components/SearchPage/ReferenceSelector.tsx @@ -89,7 +89,7 @@ export const ReferenceSelector: FC = ({ ))} - {selectedReferences[segment] && ( + {selectedReferences[segment] != null && (