From 0d8dd34af7239842c2868687204aba1fc40cc874 Mon Sep 17 00:00:00 2001 From: Manuel Holtgrewe Date: Thu, 30 Nov 2023 17:14:52 +0100 Subject: [PATCH] feat: refurbishing SV details page (#236) (#239) --- .editorconfig | 5 + backend/.editorconfig | 1 + backend/app/assets/favicon.ico | Bin 15406 -> 9662 bytes backend/app/assets/favicon.svg | 134 +++++++ frontend/Makefile | 1 + frontend/src/api/annonars.ts | 22 ++ .../components/GeneDetails/ClinvarCard.vue | 1 + .../ClinvarCard/VariationLandscape.vue | 10 +- .../components/GeneDetails/ConditionsCard.vue | 2 + .../GeneDetails/PathogenicityCard.c.ts | 22 ++ .../GenomeBrowser.c.ts} | 0 frontend/src/components/GenomeBrowser.vue | 30 +- .../StrucvarDetails/AcmgRatingCard.c.ts} | 10 +- .../StrucvarDetails/AcmgRatingCard.vue | 164 +++++++++ .../StrucvarDetails/AcmgRatingCard/CnGain.vue | 90 +++++ .../StrucvarDetails/AcmgRatingCard/CnLoss.vue | 90 +++++ .../AcmgRatingCard/SummarySheet.vue | 63 ++++ .../StrucvarDetails/ClinvarCard.vue | 179 ++++++++++ .../StrucvarDetails/GeneListCard.vue | 262 ++++++++++++++ .../GeneListCard/GeneDosage.vue | 43 +++ .../GeneListCard/GeneListEntry.vue | 292 ++++++++++++++++ .../GeneListCard/ScoreChip.vue | 90 +++++ .../VariantToolsCard.vue} | 132 ++++--- .../src/components/SvDetails/AcmgRating.vue | 261 -------------- .../components/SvDetails/SvDetailsClinvar.vue | 38 -- frontend/src/components/SvDetails/SvGenes.vue | 252 ------------- .../components/VariantDetails/ClinVar.c.ts | 37 ++ .../src/components/VariantDetails/ClinVar.vue | 54 +-- .../src/components/VariantDetails/TxCsq.vue | 2 +- .../__tests__/GenomeBrowser.c.spec.ts} | 2 +- .../__tests__/GenomeBrowser.spec.ts | 4 +- .../AcmgRatingCard.spec.ts} | 8 +- .../StrucvarDetails/ClinvarCard.spec.ts | 14 + .../GeneListCard.spec.ts} | 26 +- .../VariantToolsCard.spec.ts} | 11 +- .../SvDetails/SvDetailsClinvar.spec.ts | 14 - .../__tests__/VariantDetails/ClinVar.spec.ts | 4 +- .../__tests__/VariantDetails/TxCsq.spec.ts | 2 +- frontend/src/router/index.ts | 8 +- .../src/stores/__tests__/genesList.spec.ts | 2 +- .../src/stores/__tests__/svAcmgRating.spec.ts | 2 +- frontend/src/stores/__tests__/terms.spec.ts | 10 +- frontend/src/stores/__tests__/user.spec.ts | 9 +- frontend/src/stores/svAcmgRating.ts | 2 +- frontend/src/stores/svInfo.ts | 17 + frontend/src/views/GeneDetailView.vue | 20 +- frontend/src/views/StrucvarDetailsView.vue | 330 ++++++++++++++++++ frontend/src/views/SvDetailView.vue | 167 --------- .../src/views/__tests__/ProfileView.spec.ts | 3 +- ...ew.spec.ts => StrucvarDetailsView.spec.ts} | 12 +- 50 files changed, 2052 insertions(+), 902 deletions(-) create mode 120000 backend/.editorconfig create mode 100644 backend/app/assets/favicon.svg rename frontend/src/{lib/genomeBrowserTracks.ts => components/GenomeBrowser.c.ts} (100%) rename frontend/src/{lib/acmgCNV.ts => components/StrucvarDetails/AcmgRatingCard.c.ts} (99%) create mode 100644 frontend/src/components/StrucvarDetails/AcmgRatingCard.vue create mode 100644 frontend/src/components/StrucvarDetails/AcmgRatingCard/CnGain.vue create mode 100644 frontend/src/components/StrucvarDetails/AcmgRatingCard/CnLoss.vue create mode 100644 frontend/src/components/StrucvarDetails/AcmgRatingCard/SummarySheet.vue create mode 100644 frontend/src/components/StrucvarDetails/ClinvarCard.vue create mode 100644 frontend/src/components/StrucvarDetails/GeneListCard.vue create mode 100644 frontend/src/components/StrucvarDetails/GeneListCard/GeneDosage.vue create mode 100644 frontend/src/components/StrucvarDetails/GeneListCard/GeneListEntry.vue create mode 100644 frontend/src/components/StrucvarDetails/GeneListCard/ScoreChip.vue rename frontend/src/components/{SvDetails/SvTools.vue => StrucvarDetails/VariantToolsCard.vue} (51%) delete mode 100644 frontend/src/components/SvDetails/AcmgRating.vue delete mode 100644 frontend/src/components/SvDetails/SvDetailsClinvar.vue delete mode 100644 frontend/src/components/SvDetails/SvGenes.vue create mode 100644 frontend/src/components/VariantDetails/ClinVar.c.ts rename frontend/src/{lib/__tests__/genomeBrowserTracks.spec.ts => components/__tests__/GenomeBrowser.c.spec.ts} (95%) rename frontend/src/components/__tests__/{SvDetails/AcmgRating.spec.ts => StrucvarDetails/AcmgRatingCard.spec.ts} (91%) create mode 100644 frontend/src/components/__tests__/StrucvarDetails/ClinvarCard.spec.ts rename frontend/src/components/__tests__/{SvDetails/SvGenes.spec.ts => StrucvarDetails/GeneListCard.spec.ts} (52%) rename frontend/src/components/__tests__/{SvDetails/SvTools.spec.ts => StrucvarDetails/VariantToolsCard.spec.ts} (79%) delete mode 100644 frontend/src/components/__tests__/SvDetails/SvDetailsClinvar.spec.ts create mode 100644 frontend/src/views/StrucvarDetailsView.vue delete mode 100644 frontend/src/views/SvDetailView.vue rename frontend/src/views/__tests__/{SvDetailView.spec.ts => StrucvarDetailsView.spec.ts} (86%) diff --git a/.editorconfig b/.editorconfig index 7ca29f06..b9caf3d1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,6 +12,11 @@ trim_trailing_whitespace = true indent_style = space indent_size = 4 +[*.{ts,vue}] +line_length=100 +indent_style = space +indent_size = 2 + [*.py] line_length=120 known_first_party=varfish diff --git a/backend/.editorconfig b/backend/.editorconfig new file mode 120000 index 00000000..38d9a0ce --- /dev/null +++ b/backend/.editorconfig @@ -0,0 +1 @@ +../.editorconfig \ No newline at end of file diff --git a/backend/app/assets/favicon.ico b/backend/app/assets/favicon.ico index 57778b8719e4b9a70e5636cec01aac6b90c0e589..74ec8c8005948e1c9959cc03c27d425b209d4bb1 100644 GIT binary patch literal 9662 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 literal 15406 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 diff --git a/backend/app/assets/favicon.svg b/backend/app/assets/favicon.svg new file mode 100644 index 00000000..78444423 --- /dev/null +++ b/backend/app/assets/favicon.svg @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/Makefile b/frontend/Makefile index b0720b1a..b1cfac00 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -25,6 +25,7 @@ format: .PHONY: lint lint: npm run lint + npm run type-check npm run format:check .PHONY: test diff --git a/frontend/src/api/annonars.ts b/frontend/src/api/annonars.ts index aca29546..ffd10146 100644 --- a/frontend/src/api/annonars.ts +++ b/frontend/src/api/annonars.ts @@ -100,4 +100,26 @@ export class AnnonarsClient { }) return result } + + /** + * Fetch overlapping ClinVar strucvars via annonars REST API. + */ + async fetchClinvarStrucvars( + genomeRelease: string, + chromosome: string, + start: number, + end: number, + pageSize: number = 1000, + minOverlap: number = 0.1 + ): Promise { + const url = + `${this.apiBaseUrl}clinvar-sv/query?genomeRelease=${genomeRelease}&` + + `chromosome=${chromosome}&start=${start}&stop=${end}&pageSize=${pageSize}&` + + `minOverlap=${minOverlap}` + + const response = await fetch(url, { + method: 'GET' + }) + return await response.json() + } } diff --git a/frontend/src/components/GeneDetails/ClinvarCard.vue b/frontend/src/components/GeneDetails/ClinvarCard.vue index 2d1b71cb..7474c9ff 100644 --- a/frontend/src/components/GeneDetails/ClinvarCard.vue +++ b/frontend/src/components/GeneDetails/ClinvarCard.vue @@ -48,6 +48,7 @@ const props = withDefaults(defineProps(), { :clinvar="geneClinvar" :transcripts="transcripts" :genome-release="genomeRelease" + :gene-symbol="geneInfo?.hgnc?.symbol" /> diff --git a/frontend/src/components/GeneDetails/ClinvarCard/VariationLandscape.vue b/frontend/src/components/GeneDetails/ClinvarCard/VariationLandscape.vue index b4b67209..6eba192f 100644 --- a/frontend/src/components/GeneDetails/ClinvarCard/VariationLandscape.vue +++ b/frontend/src/components/GeneDetails/ClinvarCard/VariationLandscape.vue @@ -10,12 +10,15 @@ export interface Props { transcripts: any /** The genome release. */ genomeRelease: 'grch37' | 'grch38' + /** Gene symbol */ + geneSymbol: string } const props = withDefaults(defineProps(), { clinvar: null, transcripts: null, - genomeRelease: 'grch37' + genomeRelease: 'grch37', + geneSymbol: '???' }) const clinvarSignificanceMapping: { [key: string]: number } = { @@ -361,9 +364,14 @@ const vegaLayer = [ :data-values="vegaData" :encoding="vegaEncoding" :layer="vegaLayer" + :width="1200" :height="300" renderer="canvas" /> +
+ The plot above shows the sequence variants in the ClinVar database that are that are located + in the gene {{ geneSymbol }} . +
diff --git a/frontend/src/components/GeneDetails/ConditionsCard.vue b/frontend/src/components/GeneDetails/ConditionsCard.vue index be2d53bb..c6213463 100644 --- a/frontend/src/components/GeneDetails/ConditionsCard.vue +++ b/frontend/src/components/GeneDetails/ConditionsCard.vue @@ -222,6 +222,7 @@ const hpoTermsToShow = computed(() => { label="numeric terms" class="ml-3 d-inline-flex flex-grow-0" density="compact" + inset /> (() => { label="show links" class="ml-3 d-inline-flex flex-grow-0" density="compact" + inset /> () +const props = withDefaults(defineProps(), { locus: '' }) /** The
to show the browser in. */ const genomeBrowserDivRef = ref(null) @@ -29,9 +29,9 @@ const igvBrowser = ref(null) * @returns The translated genome build name. (hg19/hg38) */ const translateGenome = (value: any) => { - if (value === 'GRCh37') { + if (value === 'grch37') { return 'hg19' - } else if (value === 'GRCh38') { + } else if (value === 'grch38') { return 'hg38' } else { return value @@ -51,10 +51,10 @@ const addTracks = (browser: any) => { // Watch changes to the genome (requires full reload). watch( - () => props.genome, + () => props.genomeRelease, () => { ;(igvBrowser.value! as GenomeBrowser) - .loadGenome(translateGenome(props.genome)) + .loadGenome(translateGenome(props.genomeRelease)) .then((browser: GenomeBrowser) => { browser.search(props.locus) }) @@ -78,7 +78,7 @@ watch( onMounted(() => { igv .createBrowser(genomeBrowserDivRef.value, { - genome: translateGenome(props.genome), + genome: translateGenome(props.genomeRelease), locus: props.locus }) .then((browser: GenomeBrowser) => { @@ -92,9 +92,11 @@ onMounted(() => { +@/lib/GenomeBrowser.c diff --git a/frontend/src/lib/acmgCNV.ts b/frontend/src/components/StrucvarDetails/AcmgRatingCard.c.ts similarity index 99% rename from frontend/src/lib/acmgCNV.ts rename to frontend/src/components/StrucvarDetails/AcmgRatingCard.c.ts index 43e1f6ee..65f5e4e6 100644 --- a/frontend/src/lib/acmgCNV.ts +++ b/frontend/src/components/StrucvarDetails/AcmgRatingCard.c.ts @@ -552,7 +552,7 @@ const ACMG_CRITERIA_CNV_DEFS: Map< description: `Reported proband (from literature, public databases, or internal lab data) has either: • A complete deletion of or a LOF variant within gene encompassed by the observed copy-number loss OR - • An overlapping copy-number loss similar in genomic content to the observed copynumber loss AND + • An overlapping copy-number loss similar in genomic content to the observed copynumber loss AND the reported phenotype is highly specific and relatively unique to the gene or genomic region,`, conflictingEvidence: [AcmgCriteriaCNVLoss.Loss4B, AcmgCriteriaCNVLoss.Loss4C], @@ -568,7 +568,7 @@ const ACMG_CRITERIA_CNV_DEFS: Map< description: `Reported proband (from literature, public databases, or internal lab data) has either: • A complete deletion of or a LOF variant within gene encompassed by the observed copy-number loss OR - • An overlapping copy-number loss similar in genomic content to the observed copynumber loss AND + • An overlapping copy-number loss similar in genomic content to the observed copynumber loss AND the reported phenotype is consistent with the gene/genomic region, is highly specific, but not necessarily unique to the gene/genomic region.`, conflictingEvidence: [AcmgCriteriaCNVLoss.Loss4A, AcmgCriteriaCNVLoss.Loss4C], @@ -584,7 +584,7 @@ const ACMG_CRITERIA_CNV_DEFS: Map< description: `Reported proband (from literature, public databases, or internal lab data) has either: • A complete deletion of or a LOF variant within gene encompassed by the observed copy-number loss OR - • An overlapping copy-number loss similar in genomic content to the observed copynumber loss AND + • An overlapping copy-number loss similar in genomic content to the observed copynumber loss AND the reported phenotype is consistent with the gene/genomic region, but not highly specific and/or with high genetic heterogeneity.`, conflictingEvidence: [AcmgCriteriaCNVLoss.Loss4A, AcmgCriteriaCNVLoss.Loss4B], @@ -600,7 +600,7 @@ const ACMG_CRITERIA_CNV_DEFS: Map< description: `Reported proband (from literature, public databases, or internal lab data) has either: • A complete deletion of or a LOF variant within gene encompassed by the observed copy-number loss OR - • An overlapping copy-number loss similar in genomic content to the observed copynumber loss AND + • An overlapping copy-number loss similar in genomic content to the observed copynumber loss AND the reported phenotype is NOT consistent with what is expected for the gene/ genomic region or not consistent in general.`, conflictingEvidence: [], @@ -740,7 +740,7 @@ const ACMG_CRITERIA_CNV_DEFS: Map< minScore: 0, maxScore: 0.45, label: '5A', - hint: `Observed copy-number loss is de novo. Use de novo scoring categories from section 4 + hint: `Observed copy-number loss is de novo. Use de novo scoring categories from section 4 (4A-4D) to determine score`, description: `Use appropriate category from de novo scoring section in section 4.`, conflictingEvidence: [ diff --git a/frontend/src/components/StrucvarDetails/AcmgRatingCard.vue b/frontend/src/components/StrucvarDetails/AcmgRatingCard.vue new file mode 100644 index 00000000..b8c345b3 --- /dev/null +++ b/frontend/src/components/StrucvarDetails/AcmgRatingCard.vue @@ -0,0 +1,164 @@ + + + + + diff --git a/frontend/src/components/StrucvarDetails/AcmgRatingCard/CnGain.vue b/frontend/src/components/StrucvarDetails/AcmgRatingCard/CnGain.vue new file mode 100644 index 00000000..c5b4a9d6 --- /dev/null +++ b/frontend/src/components/StrucvarDetails/AcmgRatingCard/CnGain.vue @@ -0,0 +1,90 @@ + + + diff --git a/frontend/src/components/StrucvarDetails/AcmgRatingCard/CnLoss.vue b/frontend/src/components/StrucvarDetails/AcmgRatingCard/CnLoss.vue new file mode 100644 index 00000000..7e4f0ad5 --- /dev/null +++ b/frontend/src/components/StrucvarDetails/AcmgRatingCard/CnLoss.vue @@ -0,0 +1,90 @@ + + + diff --git a/frontend/src/components/StrucvarDetails/AcmgRatingCard/SummarySheet.vue b/frontend/src/components/StrucvarDetails/AcmgRatingCard/SummarySheet.vue new file mode 100644 index 00000000..33c57573 --- /dev/null +++ b/frontend/src/components/StrucvarDetails/AcmgRatingCard/SummarySheet.vue @@ -0,0 +1,63 @@ + + + diff --git a/frontend/src/components/StrucvarDetails/ClinvarCard.vue b/frontend/src/components/StrucvarDetails/ClinvarCard.vue new file mode 100644 index 00000000..1def18a4 --- /dev/null +++ b/frontend/src/components/StrucvarDetails/ClinvarCard.vue @@ -0,0 +1,179 @@ + + + diff --git a/frontend/src/components/StrucvarDetails/GeneListCard.vue b/frontend/src/components/StrucvarDetails/GeneListCard.vue new file mode 100644 index 00000000..9951599c --- /dev/null +++ b/frontend/src/components/StrucvarDetails/GeneListCard.vue @@ -0,0 +1,262 @@ + + + diff --git a/frontend/src/components/StrucvarDetails/GeneListCard/GeneDosage.vue b/frontend/src/components/StrucvarDetails/GeneListCard/GeneDosage.vue new file mode 100644 index 00000000..dad26727 --- /dev/null +++ b/frontend/src/components/StrucvarDetails/GeneListCard/GeneDosage.vue @@ -0,0 +1,43 @@ + + + diff --git a/frontend/src/components/StrucvarDetails/GeneListCard/GeneListEntry.vue b/frontend/src/components/StrucvarDetails/GeneListCard/GeneListEntry.vue new file mode 100644 index 00000000..329b6d66 --- /dev/null +++ b/frontend/src/components/StrucvarDetails/GeneListCard/GeneListEntry.vue @@ -0,0 +1,292 @@ + + + diff --git a/frontend/src/components/StrucvarDetails/GeneListCard/ScoreChip.vue b/frontend/src/components/StrucvarDetails/GeneListCard/ScoreChip.vue new file mode 100644 index 00000000..4287ce41 --- /dev/null +++ b/frontend/src/components/StrucvarDetails/GeneListCard/ScoreChip.vue @@ -0,0 +1,90 @@ + + + diff --git a/frontend/src/components/SvDetails/SvTools.vue b/frontend/src/components/StrucvarDetails/VariantToolsCard.vue similarity index 51% rename from frontend/src/components/SvDetails/SvTools.vue rename to frontend/src/components/StrucvarDetails/VariantToolsCard.vue index 3c7a03e6..0b0e3cac 100644 --- a/frontend/src/components/SvDetails/SvTools.vue +++ b/frontend/src/components/StrucvarDetails/VariantToolsCard.vue @@ -4,6 +4,7 @@ import { computed } from 'vue' import { type SvRecord } from '@/stores/svInfo' export interface Props { + genomeRelease: 'grch37' | 'grch38' svRecord: SvRecord | null } @@ -15,7 +16,7 @@ const ucscLinkout = computed((): string => { if (!props.svRecord) { return '#' } - const db = props.svRecord.release === 'grch37' ? 'hg19' : 'hg38' + const db = props.genomeRelease === 'grch37' ? 'hg19' : 'hg38' return ( `https://genome-euro.ucsc.edu/cgi-bin/hgTracks?db=${db}&position=` + `${props.svRecord.chromosome}:${props.svRecord.start}-` + @@ -28,9 +29,9 @@ const ensemblLinkout = computed((): string => { return '#' } const loc = `${props.svRecord.chromosome}:${props.svRecord.start}-${props.svRecord.end}` - if (props.svRecord.release === 'grch37') { + if (props.genomeRelease === 'grch37') { return `https://grch37.ensembl.org/Homo_sapiens/Location/View?r=${loc}` - } else if (props.svRecord.release === 'grch38') { + } else if (props.genomeRelease === 'grch38') { return `https://ensembl.org/Homo_sapiens/Location/View?r=${loc}` } return '#' @@ -40,7 +41,7 @@ const dgvLinkout = computed((): string => { if (!props.svRecord) { return '#' } - const db = props.svRecord.release === 'grch37' ? 'hg19' : 'hg38' + const db = props.genomeRelease === 'grch37' ? 'hg19' : 'hg38' return ( `http://dgv.tcag.ca/gb2/gbrowse/dgv2_${db}/?name=${props.svRecord.chromosome}:` + `${props.svRecord.start}-${props.svRecord.end};search=Search` @@ -51,7 +52,7 @@ const gnomadLinkout = computed((): string => { if (!props.svRecord) { return '#' } - const dataset = props.svRecord.release === 'grch37' ? 'gnomad_r2_1' : 'gnomad_r3' + const dataset = props.genomeRelease === 'grch37' ? 'gnomad_r2_1' : 'gnomad_r3' return ( `https://gnomad.broadinstitute.org/region/${props.svRecord.chromosome}:` + `${props.svRecord.start}-${props.svRecord.end}?dataset=${dataset}` @@ -62,13 +63,25 @@ const varsomeLinkout = computed((): string => { if (!props.svRecord) { return '#' } - const urlRelease = props.svRecord.release === 'grch37' ? 'hg19' : 'hg38' + const urlRelease = props.genomeRelease === 'grch37' ? 'hg19' : 'hg38' const chrom = props.svRecord.chromosome.startsWith('chr') ? props.svRecord.chromosome : `chr${props.svRecord.chromosome}` return `https://varsome.com/cnv/${urlRelease}/${chrom}:${props.svRecord.start}:${props.svRecord.end}:${props.svRecord.svType}` }) +const franklinLinkout = computed((): string => { + if (!props.svRecord) { + return '#' + } + const { chromosome, start, end, svType } = props.svRecord + const urlRelease = props.genomeRelease === 'grch37' ? 'hg19' : 'hg38' + return `https://franklin.genoox.com/clinical-db/variant/sv/chr${chromosome.replace( + 'chr', + '' + )}-${start}-${end}-${svType}-${urlRelease}` +}) + const jumpToLocus = async () => { const chrPrefixed = props.svRecord?.chromosome.startsWith('chr') ? props.svRecord?.chromosome @@ -81,61 +94,68 @@ const jumpToLocus = async () => { console.error(msg, e) }) } + +interface Linkout { + href: string + label: string +} + +const genomeBrowsers = computed(() => { + return [ + { label: 'ENSEMBL', href: ensemblLinkout.value }, + { label: 'UCSC', href: ucscLinkout.value } + ] +}) + +const resources = computed(() => { + return [ + { label: 'DGV', href: dgvLinkout.value }, + { label: 'gnomAD', href: gnomadLinkout.value }, + { label: 'varsome', href: varsomeLinkout.value }, + { label: 'genoox Franklin', href: franklinLinkout.value } + ] +}) diff --git a/frontend/src/components/SvDetails/AcmgRating.vue b/frontend/src/components/SvDetails/AcmgRating.vue deleted file mode 100644 index 07f93837..00000000 --- a/frontend/src/components/SvDetails/AcmgRating.vue +++ /dev/null @@ -1,261 +0,0 @@ - - - - - diff --git a/frontend/src/components/SvDetails/SvDetailsClinvar.vue b/frontend/src/components/SvDetails/SvDetailsClinvar.vue deleted file mode 100644 index 02f59726..00000000 --- a/frontend/src/components/SvDetails/SvDetailsClinvar.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/frontend/src/components/SvDetails/SvGenes.vue b/frontend/src/components/SvDetails/SvGenes.vue deleted file mode 100644 index 1a63c8d8..00000000 --- a/frontend/src/components/SvDetails/SvGenes.vue +++ /dev/null @@ -1,252 +0,0 @@ - - - diff --git a/frontend/src/components/VariantDetails/ClinVar.c.ts b/frontend/src/components/VariantDetails/ClinVar.c.ts new file mode 100644 index 00000000..4a276062 --- /dev/null +++ b/frontend/src/components/VariantDetails/ClinVar.c.ts @@ -0,0 +1,37 @@ +export const CLINICAL_SIGNIFICANCE_LABEL: { [key: string]: string } = { + CLINICAL_SIGNIFICANCE_PATHOGENIC: 'pathogenic', + CLINICAL_SIGNIFICANCE_LIKELY_PATHOGENIC: 'likely pathogenic', + CLINICAL_SIGNIFICANCE_UNCERTAIN_SIGNIFICANCE: 'uncertain significance', + CLINICAL_SIGNIFICANCE_LIKELY_BENIGN: 'likely benign', + CLINICAL_SIGNIFICANCE_BENIGN: 'benign' +} + +export const CLINICAL_SIGNIFICANCE_COLOR: { [key: string]: string } = { + CLINICAL_SIGNIFICANCE_PATHOGENIC: 'red-darken-3', + CLINICAL_SIGNIFICANCE_LIKELY_PATHOGENIC: 'orange-draken-2', + CLINICAL_SIGNIFICANCE_UNCERTAIN_SIGNIFICANCE: 'gray-lighten-2', + CLINICAL_SIGNIFICANCE_LIKELY_BENIGN: 'green-lighen-3', + CLINICAL_SIGNIFICANCE_BENIGN: 'green-darken-2' +} + +export const REVIEW_STATUS_LABEL: { [key: string]: string } = { + REVIEW_STATUS_PRACTICE_GUIDELINE: 'practice guideline', + REVIEW_STATUS_REVIEWED_BY_EXPERT_PANEL: 'reviewed by expert panel', + REVIEW_STATUS_CRITERIA_PROVIDED_MULTIPLE_SUBMITTERS_NO_CONFLICTS: + 'criteria provided, multiple submitters, no conflicts', + REVIEW_STATUS_CRITERIA_PROVIDED_SINGLE_SUBMITTER: 'criteria provided, single submitter', + REVIEW_STATUS_CRITERIA_PROVIDED_CONFLICTING_INTERPRETATIONS: + 'criteria provided, conflicting interpretations', + REVIEW_STATUS_NO_ASSERTION_CRITERIA_PROVIDED: 'no assertion criteria provided', + REVIEW_STATUS_NO_ASSERTION_PROVIDED: 'no assertion provided' +} + +export const REVIEW_STATUS_STARS: { [key: string]: number } = { + REVIEW_STATUS_PRACTICE_GUIDELINE: 4, + REVIEW_STATUS_REVIEWED_BY_EXPERT_PANEL: 3, + REVIEW_STATUS_CRITERIA_PROVIDED_MULTIPLE_SUBMITTERS_NO_CONFLICTS: 2, + REVIEW_STATUS_CRITERIA_PROVIDED_SINGLE_SUBMITTER: 1, + REVIEW_STATUS_CRITERIA_PROVIDED_CONFLICTING_INTERPRETATIONS: 0, + REVIEW_STATUS_NO_ASSERTION_CRITERIA_PROVIDED: 0, + REVIEW_STATUS_NO_ASSERTION_PROVIDED: 0 +} diff --git a/frontend/src/components/VariantDetails/ClinVar.vue b/frontend/src/components/VariantDetails/ClinVar.vue index 1be95e59..0b3b1e94 100644 --- a/frontend/src/components/VariantDetails/ClinVar.vue +++ b/frontend/src/components/VariantDetails/ClinVar.vue @@ -1,42 +1,18 @@ + + + + diff --git a/frontend/src/views/SvDetailView.vue b/frontend/src/views/SvDetailView.vue deleted file mode 100644 index 959cb56d..00000000 --- a/frontend/src/views/SvDetailView.vue +++ /dev/null @@ -1,167 +0,0 @@ - - - - - diff --git a/frontend/src/views/__tests__/ProfileView.spec.ts b/frontend/src/views/__tests__/ProfileView.spec.ts index c03eb695..cda378c0 100644 --- a/frontend/src/views/__tests__/ProfileView.spec.ts +++ b/frontend/src/views/__tests__/ProfileView.spec.ts @@ -12,7 +12,8 @@ const adminUser: UserData = { email: 'admin@example.com', is_active: true, is_superuser: true, - is_verified: true + is_verified: true, + oauth_accounts: [] } const exampleBookmark: BookmarkData = { diff --git a/frontend/src/views/__tests__/SvDetailView.spec.ts b/frontend/src/views/__tests__/StrucvarDetailsView.spec.ts similarity index 86% rename from frontend/src/views/__tests__/SvDetailView.spec.ts rename to frontend/src/views/__tests__/StrucvarDetailsView.spec.ts index 099aa9ad..fde4830c 100644 --- a/frontend/src/views/__tests__/SvDetailView.spec.ts +++ b/frontend/src/views/__tests__/StrucvarDetailsView.spec.ts @@ -6,10 +6,10 @@ import * as BRCA1GeneInfo from '@/assets/__tests__/BRCA1GeneInfo.json' import * as CurrentSV from '@/assets/__tests__/ExampleSV.json' import GenomeBrowser from '@/components/GenomeBrowser.vue' import HeaderDetailPage from '@/components/HeaderDetailPage.vue' -import AcmgRating from '@/components/SvDetails/AcmgRating.vue' -import SvDetailsClinvar from '@/components/SvDetails/SvDetailsClinvar.vue' -import SvGenes from '@/components/SvDetails/SvGenes.vue' -import SvTools from '@/components/SvDetails/SvTools.vue' +import AcmgRatingCard from '@/components/StrucvarDetails/AcmgRatingCard.vue' +import SvDetailsClinvar from '@/components/StrucvarDetails/ClinvarCard.vue' +import SvGenes from '@/components/StrucvarDetails/GeneListCard.vue' +import VariantToolsCard from '@/components/StrucvarDetails/VariantToolsCard.vue' import { setupMountedComponents } from '@/lib/test-utils' import { StoreState } from '@/stores/misc' import { useSvInfoStore } from '@/stores/svInfo' @@ -67,9 +67,9 @@ describe.concurrent('VariantDetailView', async () => { const { wrapper } = await makeWrapper() const svGenes = wrapper.findComponent(SvGenes) - const svTools = wrapper.findComponent(SvTools) + const svTools = wrapper.findComponent(VariantToolsCard) const svDetailsClinvar = wrapper.findComponent(SvDetailsClinvar) - const acmgRating = wrapper.findComponent(AcmgRating) + const acmgRating = wrapper.findComponent(AcmgRatingCard) const genomeBrowser = wrapper.findComponent(GenomeBrowser) expect(svGenes.exists()).toBe(true) expect(svTools.exists()).toBe(true)