Skip to content

Commit 14c5519

Browse files
authored
Merge pull request #413 from VariantEffect/release-2025.2.1
Release 2025.2.1
2 parents 68f5bb8 + f212829 commit 14c5519

File tree

6 files changed

+194
-15
lines changed

6 files changed

+194
-15
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mavedb-ui",
3-
"version": "2025.2.0",
3+
"version": "2025.2.1",
44
"private": true,
55
"scripts": {
66
"build": "vite build --mode=${MODE=live}",

src/components/screens/SearchVariantsScreen.vue

Lines changed: 115 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<div class="flex flex-wrap justify-content-center gap-3">
99
<IconField iconPosition="left">
1010
<InputIcon class="pi pi-search"></InputIcon>
11-
<InputText v-model="searchText" ref="searchTextInput" type="search" class="p-inputtext-sm" placeholder="HGVS string" style="width: 500px;" />
11+
<InputText v-model="searchText" @keyup.enter="hgvsSearch" ref="searchTextInput" type="search" class="p-inputtext-sm" placeholder="HGVS string" style="width: 500px;" />
1212
</IconField>
1313
<Button class="p-button-plain" @click="hgvsSearch">Search</Button>
1414
</div>
@@ -41,7 +41,7 @@
4141
<div class="variant-search-result">
4242
<div v-if="allele.variants.length > 0" class="variant-search-result-button">
4343
<router-link :to="`/variants/${allele.clingenAlleleId}`">
44-
<Button label="View in MaveMD" icon="pi pi-eye" />
44+
<Button label="View in MaveDB Clinical View" icon="pi pi-eye" />
4545
</router-link>
4646
</div>
4747
<div class="variant-search-result-content">
@@ -113,6 +113,8 @@ import InputText from 'primevue/inputtext'
113113
import Button from 'primevue/button'
114114
import Message from 'primevue/message'
115115
import {defineComponent} from 'vue'
116+
import { useRoute, useRouter } from 'vue-router'
117+
import { useToast } from 'primevue/usetoast'
116118
// import {debounce} from 'vue-debounce'
117119
118120
import config from '@/config'
@@ -124,6 +126,12 @@ const SCORE_SETS_TO_SHOW = 5
124126
export default defineComponent({
125127
name: 'SearchVariantsScreen',
126128
components: {Card, Column, DataTable, DefaultLayout, Dropdown, EntityLink, IconField, InputIcon, InputText, Button, Message},
129+
setup() {
130+
const route = useRoute()
131+
const router = useRouter()
132+
const toast = useToast()
133+
return { route, router, toast }
134+
},
127135
128136
data: function() {
129137
return {
@@ -175,6 +183,46 @@ export default defineComponent({
175183
}
176184
}
177185
},
186+
'$route.query.search': {
187+
immediate: true,
188+
handler(newVal) {
189+
if (typeof newVal === 'string') {
190+
this.searchText = newVal
191+
} else if (!newVal) {
192+
this.searchText = ''
193+
}
194+
}
195+
},
196+
'$route.query.gene': {
197+
immediate: true,
198+
handler(newVal) {
199+
this.inputGene = typeof newVal === 'string' ? newVal : ''
200+
}
201+
},
202+
'$route.query.variantType': {
203+
immediate: true,
204+
handler(newVal) {
205+
this.inputVariantType = typeof newVal === 'string' ? newVal : ''
206+
}
207+
},
208+
'$route.query.variantPosition': {
209+
immediate: true,
210+
handler(newVal) {
211+
this.inputVariantPosition = typeof newVal === 'string' ? newVal : ''
212+
}
213+
},
214+
'$route.query.refAllele': {
215+
immediate: true,
216+
handler(newVal) {
217+
this.inputReferenceAllele = typeof newVal === 'string' ? newVal : ''
218+
}
219+
},
220+
'$route.query.altAllele': {
221+
immediate: true,
222+
handler(newVal) {
223+
this.inputAlternateAllele = typeof newVal === 'string' ? newVal : ''
224+
}
225+
},
178226
},
179227
180228
computed: {
@@ -196,6 +244,11 @@ export default defineComponent({
196244
197245
methods: {
198246
hgvsSearch: async function() {
247+
// Remove fuzzy search params from the URL
248+
const { gene, variantType, variantPosition, refAllele, altAllele, ...rest } = this.route.query;
249+
this.router.replace({
250+
query: { ...rest, search: this.searchText || undefined }
251+
})
199252
this.alleles = []
200253
this.loading = true;
201254
if (this.searchText !== null && this.searchText !== '') {
@@ -249,10 +302,16 @@ export default defineComponent({
249302
break
250303
}
251304
this.alleles.push(newAllele)
252-
} catch (error) {
305+
} catch (error: any) {
253306
// NOTE: not resetting alleles here, because any error will have occurred before pushing to alleles.
254307
// don't want to reset alleles because this function may be called in a loop to process several hgvs strings.
255308
console.log("Error while loading search results", error)
309+
this.toast.add({
310+
severity: 'error',
311+
summary: error.response.data?.errorType && error.response.data?.description ? `${error.response.data?.errorType}: ${error.response.data?.description}` : 'Error fetching results',
312+
detail: error.response.data?.message || 'Invalid HGVS string provided.',
313+
life: 10000,
314+
})
256315
}
257316
},
258317
searchVariants: async function() {
@@ -265,21 +324,38 @@ export default defineComponent({
265324
clingenAlleleIds: [allele.clingenAlleleId]
266325
}
267326
)
268-
if (response.data !== null && response.data.length > 0) {
269-
allele.variants = response.data[0]
327+
328+
allele.variants = response.data[0]
329+
allele.variantsStatus = 'Loaded'
330+
} catch (error: any) {
331+
allele.variants = []
332+
console.log("Error while loading MaveDB search results for variant", error)
333+
if (error.response?.status === 404) {
270334
allele.variantsStatus = 'Loaded'
335+
this.toast.add({ severity: 'info', summary: 'No results found', detail: 'No variants match the provided search criteria.', life: 10000 })
336+
} else if (error.response?.status >= 500) {
337+
allele.variantsStatus = 'Error'
338+
this.toast.add({ severity: 'error', summary: 'Server Error', detail: 'The server encountered an unexpected error. Please try again later.', life: 10000 })
271339
} else {
272-
allele.variants = []
273-
allele.variantsStatus = 'Loaded'
340+
allele.variantsStatus = 'Error'
341+
this.toast.add({ severity: 'error', summary: 'Error fetching results', detail: 'An error occurred while fetching MaveDB variants.', life: 10000 })
274342
}
275-
} catch (error) {
276-
allele.variants = []
277-
allele.variantsStatus = 'Error'
278-
console.log("Error while loading MaveDB search results for variant", error)
279343
}
280344
}
281345
},
282346
fuzzySearch: async function() {
347+
// Remove HGVS search param from the URL
348+
const { search, ...rest } = this.route.query;
349+
this.router.replace({
350+
query: {
351+
...rest,
352+
gene: this.inputGene || undefined,
353+
variantType: this.inputVariantType || undefined,
354+
variantPosition: this.inputVariantPosition || undefined,
355+
refAllele: this.inputReferenceAllele || undefined,
356+
altAllele: this.inputAlternateAllele || undefined
357+
}
358+
})
283359
this.alleles = []
284360
this.loading = true;
285361
await this.fetchFuzzySearchResults()
@@ -351,13 +427,40 @@ export default defineComponent({
351427
for (const hgvsString of hgvsStrings) {
352428
await this.fetchHgvsSearchResults(hgvsString.hgvsString, hgvsString.maneStatus)
353429
}
354-
} catch (error) {
430+
} catch (error: any) {
355431
this.alleles = []
356432
console.log("Error while loading search results", error)
433+
this.toast.add({
434+
severity: 'error',
435+
summary: error.response.data?.errorType && error.response.data?.description ? `${error.response.data?.errorType}: ${error.response.data?.description}` : 'Error fetching results',
436+
detail: error.response.data?.message || 'Invalid HGVS string provided.',
437+
life: 10000,
438+
})
357439
}
358440
}
359441
}
442+
},
443+
444+
mounted() {
445+
// If HGVS search param is present, run HGVS search
446+
if (this.route.query.search && String(this.route.query.search).trim() !== '') {
447+
this.hgvsSearchVisible = true;
448+
this.fuzzySearchVisible = false;
449+
this.hgvsSearch();
450+
} else if (
451+
this.route.query.gene ||
452+
this.route.query.variantType ||
453+
this.route.query.variantPosition ||
454+
this.route.query.refAllele ||
455+
this.route.query.altAllele
456+
) {
457+
// If any fuzzy search param is present, run fuzzy search
458+
this.hgvsSearchVisible = false;
459+
this.fuzzySearchVisible = true;
460+
this.fuzzySearch();
461+
}
360462
}
463+
361464
})
362465
363466
</script>

src/components/screens/SearchView.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import { paths, components } from '@/schema/openapi'
6060
6161
import type {LocationQueryValue} from "vue-router";
6262
import { textForTargetGeneCategory } from '@/lib/target-genes'
63+
import { routeToVariantSearchIfVariantIsSearchable } from '@/lib/search'
6364
6465
type ShortScoreSet = components['schemas']['ShortScoreSet']
6566
type ShortTargetGene = components['schemas']['ShortTargetGene']
@@ -316,6 +317,12 @@ export default defineComponent({
316317
this.debouncedSearchFunction()
317318
},
318319
search: async function() {
320+
// TODO#410 Because of the debounced search, this is super aggressive and will send the user to the variant search page as they are
321+
// typing. I'm not sure that is the best user experience, but it seems unlikely people will actually be typing an HGVS string.
322+
if (routeToVariantSearchIfVariantIsSearchable(this.searchText)) {
323+
return
324+
}
325+
319326
this.$router.push({query: {
320327
...(this.searchText && this.searchText.length > 0) ? {search: this.searchText} : {},
321328
...(this.filterTargetNames.length > 0) ? {'target-name': this.filterTargetNames} : {},

src/lib/mave-hgvs.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,13 @@ export function preferredVariantLabel(variant: SimpleMaveVariant): VariantLabel
122122
return {mavedb_label: variant.accession}
123123
}
124124
}
125+
126+
127+
/**
128+
* Regular expression for parsing a generic HGVS style variant.
129+
*
130+
* This should be used only for deciding whether a string is a valid HGVS variant, not for parsing it.
131+
* It matches a string that starts with an identifier (e.g., "NM_001301717.2") followed by a colon and a description.
132+
* The description can be anything (which isn't technically correct), including spaces and special characters.
133+
*/
134+
export const genericVariant = /^(?<identifier>[A-z_0-9.]+):(?<description>.*)$/gm

src/lib/search.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import router from '@/router'
2+
import { genericVariant } from "./mave-hgvs"
3+
4+
export function routeToVariantSearchIfVariantIsSearchable(searchText: string | null | undefined): boolean {
5+
if (!searchText || searchText.trim() === '') {
6+
return false
7+
}
8+
9+
searchText = searchText.trim()
10+
const hgvsMatches = genericVariant.exec(searchText)
11+
if (hgvsMatches && hgvsMatches.groups) {
12+
const identifier = hgvsMatches.groups.identifier
13+
const description = hgvsMatches.groups.description
14+
15+
// Regex for RefSeq/Ensembl transcript IDs
16+
const transcriptRegex = /^(N[CMPR]_|X[MR]_|ENST|ENSMUST|ENSMUST|XM_|XR_)[0-9]+(\.[0-9]+)?$/gm
17+
if (transcriptRegex.test(identifier)) {
18+
// Transcript: treat as normal HGVS
19+
console.log(`Routing to search-variants with HGVS: ${hgvsMatches[0]}`)
20+
router.push({ name: 'search-variants', query: { search: hgvsMatches[0] } })
21+
} else {
22+
// Assume identifier is an HGNC gene symbol, parse description for fuzzy search
23+
// Example: BRCA1:c.123A>G or BRCA1:p.Arg123Gly
24+
let gene = identifier
25+
let variantType = ''
26+
let variantPosition = ''
27+
let refAllele = ''
28+
let altAllele = ''
29+
30+
// Try to parse c. or p. notation
31+
const fuzzyMatch = /^(c\.|p\.)?([A-Za-z]+)?([0-9]+)([A-Za-z*-]+)?(?:>([A-Za-z*-]+))?$/gm.exec(description)
32+
if (fuzzyMatch) {
33+
variantType = fuzzyMatch[1] || ''
34+
if (variantType === 'c.') {
35+
variantPosition = fuzzyMatch[3] || ''
36+
refAllele = fuzzyMatch[4] || ''
37+
altAllele = fuzzyMatch[5] || ''
38+
} else if (variantType === 'p.') {
39+
refAllele = fuzzyMatch[2] || ''
40+
variantPosition = fuzzyMatch[3] || ''
41+
altAllele = fuzzyMatch[4] || ''
42+
}
43+
}
44+
45+
router.push({
46+
name: 'search-variants',
47+
query: {
48+
gene,
49+
variantType,
50+
variantPosition,
51+
refAllele,
52+
altAllele
53+
}
54+
})
55+
}
56+
return true
57+
}
58+
return false
59+
}

0 commit comments

Comments
 (0)