Skip to content
Merged
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
"jsonpath-plus": "^10.3.0",
"jsonwebtoken": "^9.0.2",
"jszip": "^3.10.1",
"linkify-html": "^4.3.2",
"linkifyjs": "^4.3.2",
"lodash": "^4.17.21",
"marked": "^11.1.0",
"moment": "^2.29.4",
Expand Down
10 changes: 5 additions & 5 deletions src/components/CollectionBadge.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<template>
<div class="mavedb-collection-badge">
<router-link :to="{name: 'collection', params: {urn: collection.urn}}">
<img v-if="badgeImage" :alt="collection.name" :class="badgeClassName" :src="badgeImage" />
<img v-else-if="badgeNameIsLink" :alt="collection.name" :class="badgeClassName" :src="collection.badgeName" />
<img v-if="badgeImage" :alt="collection.name" :class="`inline ${badgeClassName}`" :src="badgeImage" />
<img v-else-if="badgeNameIsLink" :alt="collection.name" :class="`inline ${badgeClassName}`" :src="collection.badgeName" />
<Tag v-else :class="badgeClassName" rounded :value="collection.badgeName" />
</router-link>
</div>
Expand All @@ -11,13 +11,14 @@
<script lang="ts">
import Tag from 'primevue/tag'
import {defineComponent} from 'vue'
import igvfTagImage from '@/assets/igvf-tag.png'

const BUILT_IN_BADGE_CLASSES: {[key: string]: string} = {
IGVF: 'mavedb-collection-badge-igvf'
}

const BUILT_IN_BADGE_IMAGE_ASSETS: {[key: string]: string} = {
IGVF: '../assets/igvf-tag.png'
IGVF: igvfTagImage
}

export default defineComponent({
Expand All @@ -37,8 +38,7 @@ export default defineComponent({
},

badgeImage: function () {
const asset = BUILT_IN_BADGE_IMAGE_ASSETS[this.collection.badgeName]
return asset ? new URL(asset, import.meta.url).href : undefined
return BUILT_IN_BADGE_IMAGE_ASSETS[this.collection.badgeName] || undefined
},

badgeNameIsLink: function () {
Expand Down
29 changes: 25 additions & 4 deletions src/components/ScoreSetSecondaryMetadata.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,16 @@
<EntityLink entity-type="scoreSet" :urn="urn" />
</template>
</div>
<div v-if="scoreSet.externalLinks?.ucscGenomeBrowser?.url">
<a class="flex space-x-2" :href="scoreSet.externalLinks.ucscGenomeBrowser.url" target="blank">
<img alt="UCSC Genome Browser" src="@/assets/logo-ucsc-genome-browser.png" style="height: 20px" />
<span>View in the UCSC Genome Browser</span>
<div v-if="scoreSet.externalLinks?.igvf?.url" class="external-link">
<a :href="scoreSet.externalLinks.igvf.url" target="blank">
<img alt="IGVF" src="@/assets/igvf-tag.png" />
Raw data available in the IGVF Portal
</a>
</div>
<div v-if="scoreSet.externalLinks?.ucscGenomeBrowser?.url" class="external-link">
<a :href="scoreSet.externalLinks.ucscGenomeBrowser.url" target="blank">
<img alt="UCSC Genome Browser" src="@/assets/logo-ucsc-genome-browser.png" />
View in the UCSC Genome Browser
</a>
</div>
</template>
Expand Down Expand Up @@ -109,4 +115,19 @@ const sortedMetaAnalyzedByScoreSetUrns = computed(() => _.sortBy(props.scoreSet.
.mavedb-contributor {
margin: 0 0.5em;
}
.external-link {
display: block;
}
.external-link a {
display: inline-flex;
align-items: center;
gap: 6px;
}
.external-link img {
height: 20px;
width: auto;
}
.external-link img {
display: block;
}
</style>
14 changes: 12 additions & 2 deletions src/components/screens/CollectionView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@
@open="editCollectionDescription"
>
<template #display>
{{ item.description || '(Click here to add description)' }}
<!-- eslint-disable vue/no-v-html -->
<div
v-html="item.description ? linkifyTextHtml(item.description) : '(Click here to add description)'"
></div>
<!-- eslint-enable vue/no-v-html -->
</template>
<template #content>
<div class="flex mave-collection-description-editor">
Expand All @@ -87,7 +91,13 @@
</Inplace>
</div>
<div v-else>
<div v-if="item.description" class="mave-collection-description">{{ item.description }}</div>
<!-- eslint-disable vue/no-v-html -->
<div
v-if="item.description"
class="mave-collection-description"
v-html="linkifyTextHtml(item.description)"
></div>
<!-- eslint-enable vue/no-v-html -->
</div>
</div>
<div class="mavedb-1000px-col">
Expand Down
12 changes: 9 additions & 3 deletions src/components/screens/CollectionsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@
}}</router-link></template
>
</Column>
<Column class="mave-collection-description" field="description" header="Description" :sortable="true" />
<Column class="mave-collection-description" field="description" header="Description" :sortable="true">
<template #body="{data}">
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-if="data.description" v-html="linkifyTextHtml(data.description)"></span>
<span v-else>—</span>
</template>
</Column>
<Column
body-class="mave-align-center"
:field="(c) => (c.experimentUrns || []).length"
Expand Down Expand Up @@ -86,9 +92,9 @@ export default {

setup: () => {
useHead({title: 'My saved collections'})

return {
...useFormatters(),
...useFormatters()
}
},

Expand Down
30 changes: 27 additions & 3 deletions src/components/screens/ExperimentView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,15 @@
}}</router-link>
</div>
<div v-if="item.currentVersion">Current version {{ item.currentVersion }}</div>
<CollectionAdder class="mave-save-to-collection-button" data-set-type="experiment" :data-set-urn="item.urn" />

<div v-if="item.externalLinks?.igvf?.url" class="external-link">
<a :href="item.externalLinks.igvf.url" target="blank">
<img alt="IGVF" src="@/assets/igvf-tag.png" />
View this experiment in the IGVF Portal
</a>
</div>
<div style="margin-top: 1em">
<CollectionAdder class="mave-save-to-collection-button" data-set-type="experiment" :data-set-urn="item.urn" />
</div>
<div class="mave-score-set-section-title">Score Sets</div>
<div v-if="associatedScoreSets.length != 0">
<ul class="list-disc pl-4">
Expand Down Expand Up @@ -528,6 +535,23 @@ export default {
}

.mave-save-to-collection-button {
margin: 1em 0;
margin: 1em;
}

/* External links */
.external-link {
display: block;
}
.external-link a {
display: inline-flex;
align-items: center;
gap: 6px;
}
.external-link img {
height: 20px;
width: auto;
}
.external-link img {
display: block;
}
</style>
14 changes: 14 additions & 0 deletions src/components/screens/HomeScreen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,20 @@
<template #title><h2 class="mt-0">News</h2></template>
<template #content>
<ul class="ml-2 list-disc space-y-4">
<li>
<p>
MaveDB is an official
<a href="https://catalog.igvf.org/" target="_blank">
<img class="inline h-5 mb-1" alt="IGVF" src="@/assets/igvf-tag.png" />
</a>
Data Resource! You can now find data from MaveDB datasets in the
<a href="https://catalog.igvf.org/" target="_blank">IGVF Data Catalog</a>.
</p>
<p>
In addition, many MaveDB score sets now have links to view the corresponding raw data in the
<a href="https://portal.igvf.org/" target="_blank">IGVF Data Portal</a>.
</p>
</li>
<li>
<p>Tracks for many score sets are now available on the UCSC Genome Browser.</p>
<p class="mt-2">
Expand Down
3 changes: 2 additions & 1 deletion src/composition/formatters.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import _ from 'lodash'
import pluralize from 'pluralize'

import {formatDate, formatInt} from '@/lib/formats'
import {formatDate, formatInt, linkifyTextHtml} from '@/lib/formats'

export default () => ({
formatDate,
formatInt,
linkifyTextHtml,
pluralize,
startCase: (s: string) => _.startCase(s)
})
35 changes: 35 additions & 0 deletions src/lib/formats.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import moment from 'moment'
import {Opts} from 'linkifyjs'
import linkifyHtml from 'linkify-html'

export function formatDate(x: string) {
return moment(x).format('MMM DD, YYYY')
Expand All @@ -12,3 +14,36 @@ export function formatInt(x: number | null) {
maximumFractionDigits: 0
})
}

/**
* Safely convert plain text containing URLs into HTML with clickable links.
* - Escapes HTML to prevent XSS
* - Linkifies URLs via linkify-html
*
* @param text The plain text to linkify.
* @param options Optional linkify-html options.
* @returns The linkified HTML string.
*/
export function linkifyTextHtml(text: string | null | undefined, options?: Opts): string {
if (!text) return ''

const defaultOptions = {
defaultProtocol: 'https',
target: {
url: '_blank'
},
rel: 'noopener noreferrer'
}

const escapeHtml = (s: string) =>
String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')

const escaped = escapeHtml(text)

return linkifyHtml(escaped, {...defaultOptions, ...options})
}
29 changes: 22 additions & 7 deletions src/schema/openapi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,9 +571,9 @@ export interface paths {
* The index to start from. If None, starts from the beginning.
* limit : Optional[int]
* The maximum number of variants to return. If None, returns all variants.
* namespaces: List[Literal["scores", "counts"]]
* namespaces: List[Literal["scores", "counts", "vep", "gnomad"]]
* The namespaces of all columns except for accession, hgvs_nt, hgvs_pro, and hgvs_splice.
* We may add ClinVar and gnomAD in the future.
* We may add ClinVar in the future.
* drop_na_columns : bool, optional
* Whether to drop columns that contain only NA values. Defaults to False.
* db : Session
Expand Down Expand Up @@ -2577,6 +2577,10 @@ export interface components {
keywords: components["schemas"]["ExperimentControlledKeyword"][];
/** Scoreseturns */
scoreSetUrns: string[];
/** Externallinks */
externalLinks: {
[key: string]: components["schemas"]["ExternalLink"];
};
/** Numscoresets */
numScoreSets?: number | null;
/** Processingstate */
Expand Down Expand Up @@ -2945,7 +2949,14 @@ export interface components {
/** Offset */
offset: number;
};
/** ExternalLink */
/**
* ExternalLink
* @description Represents an external hyperlink for view models.
*
* Attributes:
* url (Optional[str]): Fully qualified URL for the external resource.
* May be None if no link is available or applicable.
*/
ExternalLink: {
/** Url */
url?: string | null;
Expand Down Expand Up @@ -4573,6 +4584,10 @@ export interface components {
keywords: components["schemas"]["SavedExperimentControlledKeyword"][];
/** Scoreseturns */
scoreSetUrns: string[];
/** Externallinks */
externalLinks: {
[key: string]: components["schemas"]["ExternalLink"];
};
/** Processingstate */
processingState?: string | null;
};
Expand Down Expand Up @@ -8749,9 +8764,9 @@ export interface operations {
* The index to start from. If None, starts from the beginning.
* limit : Optional[int]
* The maximum number of variants to return. If None, returns all variants.
* namespaces: List[Literal["scores", "counts"]]
* namespaces: List[Literal["scores", "counts", "vep", "gnomad"]]
* The namespaces of all columns except for accession, hgvs_nt, hgvs_pro, and hgvs_splice.
* We may add ClinVar and gnomAD in the future.
* We may add ClinVar in the future.
* drop_na_columns : bool, optional
* Whether to drop columns that contain only NA values. Defaults to False.
* db : Session
Expand All @@ -8771,8 +8786,8 @@ export interface operations {
start?: number;
/** @description Maximum number of variants to return */
limit?: number;
/** @description One or more data types to include: scores, counts, clinVar, gnomAD */
namespaces?: ("scores" | "counts")[];
/** @description One or more data types to include: scores, counts, clinVar, gnomAD, VEP */
namespaces?: ("scores" | "counts" | "vep" | "gnomad")[];
drop_na_columns?: boolean | null;
include_custom_columns?: boolean | null;
include_post_mapped_hgvs?: boolean | null;
Expand Down