Skip to content

Commit

Permalink
feat: more integrated search component
Browse files Browse the repository at this point in the history
  • Loading branch information
holtgrewe committed Dec 6, 2023
1 parent 8f67c86 commit 7f49274
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 88 deletions.
8 changes: 6 additions & 2 deletions frontend/src/components/HeaderDetailPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ const CaseInformationCard = defineAsyncComponent(
() => import('@/components/CaseInformationCard.vue')
)
/** Genome release string values. */
type GenomeRelease = 'grch37' | 'grch38'
export interface Props {
searchTerm?: string
genomeRelease?: string
genomeRelease?: GenomeRelease
}
const props = withDefaults(defineProps<Props>(), {
Expand Down Expand Up @@ -65,9 +68,10 @@ watch(() => props.searchTerm, updateTerms)
v-model:search-term="searchTermRef"
v-model:genome-release="genomeReleaseRef"
class="top-search-bar"
density="compact"
@click-search="performSearch"
/>
<v-spacer />
<v-spacer></v-spacer>
<v-toolbar-items class="topbar-links">
<v-dialog scrollable width="auto" location="top">
<template #activator="{ props: vProps }">
Expand Down
136 changes: 78 additions & 58 deletions frontend/src/components/SearchBar.vue
Original file line number Diff line number Diff line change
@@ -1,78 +1,98 @@
<script setup lang="ts">
export interface GenomeReleaseChoice {
value: string
/** Genome release string values. */
type GenomeRelease = 'grch37' | 'grch38'
/** Type for genome releases. */
interface GenomeReleaseChoice {
value: GenomeRelease
label: string
}
export interface Props {
/** The choices of genomes available. */
const GENOME_RELEASES: GenomeReleaseChoice[] = [
{ value: 'grch37', label: 'GRCh37' },
{ value: 'grch38', label: 'GRCh38' }
]
/** Mapping from gennome release name to `GenomeReleaseChoice`. */
const GENOME_RELEASES_MAP: { [key: string]: GenomeReleaseChoice } = Object.fromEntries(
GENOME_RELEASES.map((choice) => [choice.value, choice])
)
/** Type definition for component's props. */
interface Props {
searchTerm?: string
genomeRelease?: string
genomeReleaseChoices?: GenomeReleaseChoice[]
genomeRelease?: GenomeRelease
density?: 'default' | 'comfortable' | 'compact'
}
/** Define the component's props. */
const props = withDefaults(defineProps<Props>(), {
searchTerm: '',
genomeRelease: 'grch37',
density: 'default'
})
/** Launch search if any term has been entered. */
const runSearch = async () => {
if (props.searchTerm) {
emit('clickSearch', props.searchTerm, props.genomeRelease)
}
}
/** Define the emits. */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const emit = defineEmits<{
(event: 'update:searchTerm' | 'update:genomeRelease', value: string): void
(event: 'clickSearch', searchTerm: string, genomeRelease: string): void
}>()
const props = withDefaults(defineProps<Props>(), {
searchTerm: '',
genomeRelease: 'grch37',
genomeReleaseChoices: () => [
{ value: 'grch37', label: 'GRCh37' },
{ value: 'grch38', label: 'GRCh38' }
]
})
</script>

<template>
<v-toolbar id="search-bar" floating>
<div class="d-flex d-flex-row">
<v-text-field
id="search-term"
class="my-3 search-term"
:label="props.density != 'compact' ? 'Search for variant or gene' : undefined"
prepend-inner-icon="mdi-magnify"
rounded="xl"
variant="outlined"
hide-details
single-line
:model-value="props.searchTerm"
label="Enter search term"
:density="props.density"
clearable
:hide-details="true"
:model-value="searchTerm"
@input="$emit('update:searchTerm', $event.target.value)"
@keydown.enter="$emit('clickSearch', props.searchTerm, props.genomeRelease)"
/>
<div>
<v-select
id="genome-release"
variant="outlined"
hide-details
single-line
:model-value="props.genomeRelease"
:items="props.genomeReleaseChoices"
item-title="label"
item-value="value"
label="Genome Release"
@update:model-value="$emit('update:genomeRelease', $event)"
/>
</div>

<v-btn
id="search"
color="primary"
@click="$emit('clickSearch', props.searchTerm, props.genomeRelease)"
@keydown.enter="() => runSearch()"
>
<v-icon>mdi-magnify</v-icon>
search
</v-btn>
</v-toolbar>
<template #append-inner>
<v-menu :transition="false">
<template #activator="{ props: innerProps }">
<v-btn
color="black"
v-bind="innerProps"
rounded="xs"
spacing="compact"
append-icon="mdi-chevron-down"
variant="text"
class="genome-release-menu"
>
{{ GENOME_RELEASES_MAP[genomeRelease]?.label }}
</v-btn>
</template>
<v-list>
<v-list-item
v-for="{ value, label } in GENOME_RELEASES"
:key="value"
:value="value"
@click.prevent="() => runSearch()"
>
<v-list-item-title>{{ label }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-btn variant="text" rounded="xl" class="start-search" @click.prevent="() => runSearch()">
<v-icon>mdi-rocket-launch</v-icon>
</v-btn>
</template>
</v-text-field>
</div>
</template>

<style scoped>
#search-bar {
background-color: white;
border: 1px solid #455a64;
border-radius: 10px;
padding: 0 10px;
}
#search {
margin-left: 10px;
}
</style>
29 changes: 11 additions & 18 deletions frontend/src/components/__tests__/SearchBar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,13 @@ describe.concurrent('SearchBar.vue', () => {
)

const textField = wrapper.find('.v-text-field')
const select = wrapper.find('.v-select')
const searchButton = wrapper.find('#search')
const genomeReleaseMenu = wrapper.find('.genome-release-menu')
const searchButton = wrapper.find('.start-search')
expect(textField.exists()).toBe(true)
expect(select.exists()).toBe(true)
expect(genomeReleaseMenu.exists()).toBe(true)
expect(searchButton.exists()).toBe(true)
expect(textField.html()).toMatch('Enter search term')
expect(select.html()).toMatch('Genome Release')
expect(select.html()).toMatch('label')
expect(select.html()).toMatch('value')
expect(select.html()).toMatch('GRCh37')
expect(textField.html()).toMatch('Search for variant or gene')
expect(genomeReleaseMenu.html()).toMatch('GRCh37')
expect(searchButton.html()).toMatch('search')
})

Expand All @@ -47,22 +44,18 @@ describe.concurrent('SearchBar.vue', () => {
}
)

const textField = wrapper.find('#search-term') as any
const textField = wrapper.find('.search-term input') as any
expect(textField.exists()).toBe(true)
await textField.setValue('test')
expect(textField.element.value).toBe('test')

const select = wrapper.find('#genome-release') as any
const select = wrapper.find('.genome-release-menu') as any
expect(select.exists()).toBe(true)

const searchButton = wrapper.findComponent('#search') as any
expect(searchButton.exists()).toBe(true)
await searchButton.trigger('click')
await nextTick()
await searchButton.trigger('click')
const genomeReleaseMenu = wrapper.findComponent('.genome-release-menu') as any
expect(genomeReleaseMenu.exists()).toBe(true)
await genomeReleaseMenu.trigger('click')
await nextTick()
expect(wrapper.emitted()).toHaveProperty('click')
expect(wrapper.emitted('click')).toHaveLength(2)
})

it('correctly emits search', async () => {
Expand All @@ -86,7 +79,7 @@ describe.concurrent('SearchBar.vue', () => {
await searchBar.setValue('grch37', 'genomeRelease')
expect(searchBar.emitted()).toHaveProperty('update:searchTerm')
expect(searchBar.emitted()).toHaveProperty('update:genomeRelease')
const searchButton = searchBar.findComponent('#search') as any
const searchButton = searchBar.findComponent('button.start-search') as any
expect(searchButton.exists()).toBe(true)
await searchButton.trigger('click')
await nextTick()
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/views/GenesListView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import { StoreState } from '@/stores/misc'
// Components
const HeaderDetailPage = defineAsyncComponent(() => import('@/components/HeaderDetailPage.vue'))
/** Genome release string values. */
type GenomeRelease = 'grch37' | 'grch38'
export interface Props {
genomeRelease?: string
genomeRelease?: GenomeRelease
}
const props = withDefaults(defineProps<Props>(), {
Expand All @@ -21,7 +24,7 @@ const router = useRouter()
const genesListStore = useGenesListStore()
const searchTermRef = ref(String(router.currentRoute.value.query.q))
const genomeReleaseRef = ref(props.genomeRelease)
const genomeReleaseRef = ref<GenomeRelease>(props.genomeRelease)
const loadDataToStore = async () => {
await genesListStore.loadData(router.currentRoute.value.query)
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/views/HomeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ const SearchBar = defineAsyncComponent(() => import('@/components/SearchBar.vue'
const router = useRouter()
const searchTerm = ref('')
const genomeRelease = ref('grch37')
/** Genome release string values. */
type GenomeRelease = 'grch37' | 'grch38'
const searchTerm = ref<string>('')
const genomeRelease = ref<GenomeRelease>('grch37')
const showCaseInformation = ref(false)
interface Example {
query: string
label?: string
genomeRelease: string
genomeRelease: GenomeRelease
}
const examples: Example[] = [
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/views/__tests__/HomeView.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ describe.concurrent('HomeView with mocked router', async () => {
)

const textField = wrapper.find('.v-text-field')
const select = wrapper.find('.v-select')
const searchButton = wrapper.find('#search')
const genomeReleaseButton = wrapper.find('.genome-release-menu')
const searchButton = wrapper.find('.start-search')
expect(textField.exists()).toBe(true)
expect(select.exists()).toBe(true)
expect(genomeReleaseButton.exists()).toBe(true)
expect(searchButton.exists()).toBe(true)
})

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/views/__tests__/StrucvarDetailsView.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import { VMenu } from 'vuetify/components'

import * as BRCA1GeneInfo from '@/assets/__tests__/BRCA1GeneInfo.json'
import * as CurrentSV from '@/assets/__tests__/ExampleSV.json'
import ClinvarCard from '@/components/GeneDetails/ClinvarCard.vue'
// import ClinvarCard from '@/components/GeneDetails/ClinvarCard.vue'
import ConditionsCard from '@/components/GeneDetails/ConditionsCard.vue'
import ExpressionCard from '@/components/GeneDetails/ExpressionCard.vue'
import OverviewCard from '@/components/GeneDetails/OverviewCard.vue'
import PathogenicityCard from '@/components/GeneDetails/PathogenicityCard.vue'
import GenomeBrowser from '@/components/GenomeBrowser.vue'
import HeaderDetailPage from '@/components/HeaderDetailPage.vue'
import SearchBar from '@/components/SearchBar.vue'
import ClinsigCard from '@/components/StrucvarDetails/ClinsigCard.vue'
// import ClinsigCard from '@/components/StrucvarDetails/ClinsigCard.vue'
import StrucvarClinvarCard from '@/components/StrucvarDetails/ClinvarCard.vue'
import GeneListCard from '@/components/StrucvarDetails/GeneListCard.vue'
import VariantToolsCard from '@/components/StrucvarDetails/VariantToolsCard.vue'
Expand Down

0 comments on commit 7f49274

Please sign in to comment.