Skip to content
Snippets Groups Projects

Resolve "Wizzard to create db filters"

Merged Remi PLANEL requested to merge wizzard-db-filters into dev
2 files
+ 48
31
Compare changes
  • Side-by-side
  • Inline
Files
2
+ 396
0
<script setup lang="ts">
// import type { FacetDistribution } from "meilisearch";
import { useDisplay } from "vuetify";
import { useFacetsStore, type Facets } from '~~/stores/facets'
import { useMeiliSearch } from "#imports"
interface SortItem {
key: string,
order: boolean | 'asc' | 'desc'
}
export interface Props {
title?: string
db?: string
sortBy?: SortItem[]
facets: string[]
headers: { title: string, key: string }[]
itemValue: string
}
export interface FilterItem {
type: 'facet' | 'operator' | 'value' | 'text'
value: string
title: string
count?: number
deletable: boolean
props: {
[key: string]: any
// title: string
// value: any
}
// raw?: any
}
const props = withDefaults(defineProps<Props>(), {
title: '',
db: 'refseq',
sortBy: () => [{ key: "type", order: "asc" }],
});
const sortByRef = ref(toValue(props.sortBy))
const facetsRef = toRef(() => props.facets)
const { search: msSearch, result: msResult } = useMeiliSearch(props.db)
const facetStore = useFacetsStore()
const search: Ref<string> = ref("");
const filterOrSearch: Ref<FilterItem[] | null> = ref(null)
const hitsPerPage: Ref<number> = ref(25)
const limit = ref(1000)
const filterError: Ref<string | null> = ref(null)
const msFilter: Ref<string | undefined> = ref(undefined)
const page = ref(1)
let loading = ref(false)
const { height } = useDisplay();
const minTableHeight = ref(400)
const computedTableHeight = computed(() => {
const computedHeight = height.value - 350
return computedHeight > minTableHeight.value ? computedHeight : minTableHeight.value
})
const filterInputValues = computed(() => {
console.log("recompouted FILTER value")
if (filterOrSearch.value != null) {
return filterOrSearch.value.filter(({ props }) => props.type !== 'text')
} else {
return null
}
})
const queryInputValue = computed(() => {
console.log("recompouted TEXT value")
if (filterOrSearch.value !== null) {
const phrase = filterOrSearch.value
.filter((f) => {
return f.props.type === 'text'
})
.map((f) => {
return f.value
})
if (phrase.length > 1) {
return `${phrase.join(" ")}`
}
else { return phrase[0] }
} else {
return null
}
})
const isFilter = computed(() => {
return Array.isArray(filterOrSearch.value)
})
const msSortBy = computed(() => {
if (sortByRef.value.length > 0) {
return sortByRef.value.map((curr) => {
if (curr?.key && curr?.order) {
return `${curr.key}:${curr.order}`
}
else { return "" }
})
} else { return undefined }
})
const reactiveParams = reactive({
hitsPerPage: 25,
page: 1,
limit: 1000,
facets: ["*"],
filter: [],
sort: ["type:asc"],
// prefix_length: 3,
// attributesToHighlight: ["*"]
})
watch([reactiveParams, msSortBy, page], ([newParams, newSort, newPage]) => {
searchOrFilter()
})
onMounted(async () => {
searchOrFilter()
})
// Fetch results
const msError = computed(() => {
if (filterError.value?.type && filterError.value?.message) {
return filterError.value?.message
} else { return false }
})
async function searchOrFilter() {
try {
loading.value = true
// const q = queryInputValue.value === null ? "" : queryInputValue.value
const q = search.value
await msSearch(q, { ...reactiveParams, filter: msFilter.value, sort: msSortBy.value })
} catch (error: any) {
filterError.value = error
console.log(error)
}
finally {
loading.value = false
}
}
function clearFilterOrSearch() {
filterOrSearch.value = null
searchOrFilter()
}
watch(msFilter, async (fos) => {
console.log("the filter change")
console.log(msFilter)
console.log(fos)
searchOrFilter()
search.value = ''
})
watch(msResult, (newRes) => {
console.log(msResult)
console.log(newRes)
facetStore.setFacets({ facetDistribution: newRes.facetDistribution, facetStat: newRes.facetStat })
}, { deep: true })
watch(filterInputValues, (newSoF) => {
if (isFilter.value && filterInputValues.value !== null && filterInputValues.value?.length % 3 === 0) {
msFilter.value = filterInputValues.value.map((it, index) => {
const sanitizedValue = it.value.split("-")[0]
if (index >= 1 && (index + 1) % 3 === 1) {
return ` AND ${sanitizedValue}`
} else if ((index + 1) % 3 === 0) {
return `"${sanitizedValue}"`
} else {
return `${sanitizedValue}`
}
}).join("")
}
})
watch(search, () => { searchOrFilter() })
// watch(queryInputValue, (newQuery) => {
// searchOrFilter()
// })
const filterStep = computed(() => {
return filterInputValues.value !== null && filterInputValues.value.length > 0 ? filterInputValues.value?.length % 3 : null
})
const operatorItems = ref([
{
type: "operator", value: '=', title: "is", deletable: false, props: {
type: "operator", deletable: false
}
}, {
type: "operator", value: '!=', title: "is not", deletable: false, props: {
type: "operator",
deletable: false
}
},
])
const autocompleteItems = computed(() => {
const index = filterOrSearch.value?.length ?? 0
console.log(index)
if (filterStep.value === null || filterStep.value === 0) {
return props.facets.map(value => {
return {
type: "facet",
value: `${value}-${index}`,
title: value,
deletable: false,
props: {
deletable: false,
type: "facet"
}
}
})
}
if (filterStep.value === 1) {
return operatorItems.value.map(it => { return { ...it, value: `${it.value}-${index}`, } })
}
if (filterStep.value === 2) {
// get the facet value
if (Array.isArray(filterOrSearch.value)) {
const { type, value } = filterOrSearch.value?.slice(-2, -1)[0]
const sanitizedValue = value.split("-")[0]
console.log("compute new facets")
const facetDistri = facetStore.facets?.facetDistribution
console.log(facetDistri)
return facetDistri?.[sanitizedValue] ? Object.entries(facetDistri[sanitizedValue]).map(([key, val]) => {
return {
type: "value", value: `${key}-${index}`, title: key, count: val, deletable: true, props: {
type: "value", count: val, deletable: true
}
}
}) : []
}
}
})
const canAddTextSearch = computed(() => {
if (filterOrSearch.value !== null && filterOrSearch.value.length > 0) {
const lastItem = filterOrSearch.value.slice(-1)[0]
return lastItem?.props.type === 'value' || lastItem?.props.type === "text"
}
return true
})
function selectItem(item) {
filterOrSearch.value = Array.isArray(filterOrSearch.value) ? [...filterOrSearch.value, item] : [item]
}
function deleteOneFilter(index: number) {
console.log("deleteOnefilter")
console.log(index)
console.log(isFilter.value)
console.log(filterOrSearch)
if (isFilter.value) {
filterOrSearch.value?.splice(index - 2, 2)
console.log(filterOrSearch.value)
}
}
function deleteTextFilter(index: number) {
console.log("delete text filter")
console.log(index)
console.log(isFilter.value)
console.log(filterOrSearch)
console.log(filterOrSearch.value?.length)
if (isFilter.value) {
if (index === 0) {
filterOrSearch.value?.shift()
} else {
filterOrSearch.value?.splice(index, 1)
}
console.log(filterOrSearch.value?.length)
console.log(filterOrSearch.value)
}
}
function clearSearch() {
search.value = ""
}
function runTextSearch() {
if (canAddTextSearch) {
const item: FilterItem = reactive({
type: 'text', title: search.value, value: search.value, deletable: true, props: { type: "text", deletable: true, }
})
if (Array.isArray(filterOrSearch.value)) {
filterOrSearch.value = [
...filterOrSearch.value, item
]
} else {
filterOrSearch.value = [item]
}
search.value = ""
searchOrFilter()
}
}
function namesToCollapsibleChips(names: string[]) {
return names.filter((it) => it !== "").map(it => ({ title: it }))
}
function namesToAccessionChips(names: string[]) {
return namesToCollapsibleChips(names).map(it => {
return { ...it, href: new URL(it.title, "http://toto.pasteur.cloud").toString() }
})
}
</script>
<template>
<v-card flat>
<v-card-text>
<v-row>
<v-col cols="5">
<v-text-field v-model="search" label="Search..." hide-details prepend-inner-icon="mdi-magnify"
single-line clearable></v-text-field>
</v-col>
<v-col>
<v-autocomplete ref="autocompleteInput" hide-details v-model:model-value="filterOrSearch"
auto-select-first chips clearable label="Filter results..." :items="autocompleteItems" single-line
item-value="value" item-title="title" multiple return-object prepend-inner-icon="md:search"
@click:appendInner="searchOrFilter" @click:clear="clearFilterOrSearch"
@update:modelValue="() => clearSearch()">
<template #chip="{ props, item, index }">
<v-chip v-bind="props" :text="item.raw.title" :closable="item.props.deletable"
@click:close="item.props.type === 'text' ? deleteTextFilter(index) : deleteOneFilter(index)"></v-chip>
<!-- <v-chip v-if="(index + 1) % 3 === 0" v-bind="props" :text="item.raw.title" closable
@click:close="deleteOneFilter(index)"></v-chip>
<v-chip v-else v-bind="props" :text="item.raw.title"></v-chip> -->
</template>
<template #item="{ props, item }">
<v-list-item v-bind="{ ...props, active: false, onClick: () => selectItem(item) }"
:title="item.title" :subtitle="item.raw?.count ? item.raw.count : ''" :value="props.value">
</v-list-item>
</template>
<!-- <template #no-data></template>
<template #prepend-item>
<v-list-item v-if="canAddTextSearch" :title="`Text search: ${search}`" @click="runTextSearch"> </v-list-item>
</template> -->
</v-autocomplete>
</v-col>
</v-row>
</v-card-text>
<v-data-table-server v-if="!msError" v-model:page="reactiveParams.page"
v-model:items-per-page="reactiveParams.hitsPerPage" v-model:sortBy="sortByRef" fixed-header :loading="loading"
:headers="headers" :items="msResult?.hits ?? []" :items-length="msResult?.totalHits ?? 0"
:item-value="itemValue" multi-sort density="compact" :height="computedTableHeight" class="elevation-1 mt-2">
<template #[`item.accession_in_sys`]="{ item }">
<CollapsibleChips :items="namesToAccessionChips(item.accession_in_sys)"></CollapsibleChips>
</template>
<template #[`item.proteins_in_the_prediction`]="{ item }">
<CollapsibleChips :items="namesToCollapsibleChips(item.proteins_in_the_prediction)"></CollapsibleChips>
</template>
<template #[`item.system_genes`]="{ item }">
<CollapsibleChips :items="namesToCollapsibleChips(item.system_genes)"></CollapsibleChips>
</template>
<template #[`item.completed`]="{ item }">
<v-icon v-if="item.completed" color="success" icon="md:check"></v-icon>
<v-icon v-else color="warning" icon="md:dangerous"></v-icon>
</template>
</v-data-table-server>
<v-alert v-else type="error">
{{ msError }}
</v-alert>
</v-card>
</template>
\ No newline at end of file
Loading