Skip to content
Snippets Groups Projects
Commit 0d73bd09 authored by Remi  PLANEL's avatar Remi PLANEL
Browse files

WIP: start encapsulate autocomplete based on meilisearch facet

parent 3a2a5e49
No related branches found
No related tags found
2 merge requests!203Foldseek pages,!186Refactor facet autocomplete
Pipeline #119859 passed with stages
in 6 minutes and 30 seconds
This commit is part of merge request !186. Comments created here will be created in the context of that merge request.
<script setup lang="ts">
export interface FilterItem {
type: 'facet' | 'innerOperator' | 'outerOperator' | 'value' | 'text'
value: string
title: string
count?: number
deletable: boolean
props: Record<string, any>
}
export interface Props {
db: string
modelValue: string | undefined
}
const emit = defineEmits(['update:modelValue'])
// const emit = defineEmits(["refresh:search"])
const msfilter = defineModel<string>("msfilter", { default: "" })
const props = withDefaults(defineProps<Props>(), {
modelValue: ""
});
const filterItems = ref<FilterItem[] | undefined>(undefined)
const { result: msResult } = useMeiliSearch(props.db)
const reactiveParams = reactive({
facets: ["*"],
filter: [],
sort: ["type:asc"],
})
const isAutocompleteFocused = ref<boolean>(false)
const facets = computed(() => {
const toValMsResult = toValue(msResult)
if (toValMsResult?.facetDistribution) {
return Object.keys(toValMsResult?.facetDistribution)
}
else { return undefined }
})
const filterStep = computed(() => {
const toValFilterItems = toValue(filterItems)
if (toValFilterItems !== undefined) {
return toValFilterItems.length % 4
}
})
const innerOperatorItems = ref<FilterItem[]>([
{
type: "innerOperator", value: '=', title: "is", deletable: false, props: {
type: "innerOperator", deletable: false
}
}, {
type: "innerOperator", value: '!=', title: "is not", deletable: false, props: {
type: "innerOperator",
deletable: false
}
},
])
const outerOperatorItems = ref<FilterItem[]>([
{
type: "outerOperator", value: 'AND', title: "AND", deletable: false, props: {
type: "outerOperator", deletable: false
}
}, {
type: "outerOperator", value: 'OR', title: "OR", deletable: false, props: {
type: "outerOperator",
deletable: false
}
},
])
const autocompleteItems = computed(() => {
const toValFilterItems = toValue(filterItems)
const index = toValFilterItems?.length ?? 0
if (filterStep.value === undefined || filterStep.value === 0) {
return toValue(facets)?.map(value => {
return {
type: "facet",
value: `${value}-${index}`,
title: value,
deletable: false,
props: {
deletable: false,
type: "facet"
}
}
})
}
if (filterStep.value === 1) {
return innerOperatorItems.value.map(it => { return { ...it, value: `${it.value}-${index}`, } })
}
if (filterStep.value === 2) {
// get the facet value
if (Array.isArray(toValFilterItems)) {
const { type, value } = toValFilterItems?.slice(-2, -1)[0]
const sanitizedValue = value.split("-")[0]
// console.log("compute new facets")
const facetDistri = msResult.value?.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
}
}
}) : []
}
}
if (filterStep.value === 3) {
return outerOperatorItems.value.map(it => { return { ...it, value: `${it.value}-${index}`, } })
}
})
const lastFilterItem = computed(() => {
const toValFilterItems = toValue(filterItems)
if (toValFilterItems !== undefined) {
return toValFilterItems.slice(-1)[0]
}
})
const isValidFilters = computed(() => {
const toValFilterStep = toValue(filterStep)
const toValFilterItems = toValue(filterItems)
if (toValFilterItems === undefined) {
return true
}
else {
const toValLastFilterItem = toValue(lastFilterItem)
if (toValLastFilterItem !== undefined) {
return toValLastFilterItem.type === 'value' && isAutocompleteFocused.value === false || (toValFilterStep === 0 && toValLastFilterItem.type === "outerOperator" && toValLastFilterItem.value.split("-")[0] === "AND")
}
}
return false
})
function toMeiliFilter(filterItems: FilterItem[] | undefined) {
if (filterItems !== undefined) {
const tmpFilterItems = [...filterItems]
if (tmpFilterItems.length % 4 === 0) {
tmpFilterItems.splice(-1)
}
return tmpFilterItems.map((it, index) => {
const sanitizedValue = it.value.split("-").slice(0, -1).join("-")
if ((index + 1) % 4 === 3) {
return `"${sanitizedValue}"`
} else if ((index + 1) % 4 === 0) {
return ` ${sanitizedValue} `
}
else {
return `${sanitizedValue}`
}
}).join("")
}
}
function updateAutocompleteFocused(isFocused: boolean) {
isAutocompleteFocused.value = isFocused
}
watchEffect(() => {
console.log("watch effect")
const toValFilterStep = toValue(filterStep)
const toValLastFilterItem = toValue(lastFilterItem)
console.log(toValFilterStep)
console.log(toValLastFilterItem?.type)
console.log(toValLastFilterItem?.value)
if (toValue(isValidFilters)) {
console.log("should refresh the search in compo")
const meiliFilter = toMeiliFilter(toValue(filterItems))
console.log(meiliFilter)
emit('update:modelValue', meiliFilter)
}
if (toValFilterStep === 0 && toValLastFilterItem !== undefined && toValLastFilterItem.type === "outerOperator" && toValLastFilterItem.value.split("-")[0] === "AND") {
}
})
</script>
<template>
<v-autocomplete v-model:model-value="filterItems" :items="autocompleteItems" auto-select-first chips clearable multiple
return-object label="Filter results..." prepend-inner-icon="md:search" single-line item-value="value"
item-title="title" class="mx-2" @update:focused="updateAutocompleteFocused">
<template #item="{ props, item }">
<v-list-item v-bind="{ ...props, active: false }" :title="item.title"
:subtitle="item.raw?.count ? item.raw.count : ''" :value="props.value">
</v-list-item>
</template></v-autocomplete>
</template>
\ No newline at end of file
......@@ -62,7 +62,7 @@ const filterOrSearch: Ref<FilterItem[] | null> = ref(null)
const hitsPerPage: Ref<number> = ref(25)
const itemsPerPage: Ref<number[]> = ref([25, 50, 100])
const filterError: Ref<string | null> = ref(null)
const msFilter: Ref<string | undefined> = ref(undefined)
// const msFilter: Ref<string | undefined> = ref(undefined)
const page = ref(1)
let loading = ref(false)
const expanded = ref([])
......@@ -123,17 +123,38 @@ onMounted(async () => {
emitRefreshRes()
})
const msFilterCompo = ref<string | undefined>(undefined)
const msFilter = computed(() => {
if (isFilter.value && filterInputValues.value !== null && filterInputValues.value?.length % 3 === 0) {
return filterInputValues.value.map((it, index) => {
const sanitizedValue = it.value.split("-").slice(0, -1).join("-")
if (index >= 1 && (index + 1) % 3 === 1) {
return ` AND ${sanitizedValue}`
} else if ((index + 1) % 3 === 0) {
return `"${sanitizedValue}"`
} else {
return `${sanitizedValue}`
}
}).join("")
} else { return null }
})
const computedFilter = computed(() => {
return [toValue(msFilter), props.numericalFilters].filter(f => f !== undefined).join(" AND ")
console.log(toValue(msFilter))
if (msFilter.value !== null) {
return [toValue(msFilter), props.numericalFilters].filter(f => f !== undefined && f !== null).join(" AND ")
}
})
watch(computedFilter, () => {
searchOrFilter()
emitRefreshRes()
console.log(toValue(computedFilter))
if (toValue(computedFilter) !== undefined || toValue(filterInputValues) === null) {
searchOrFilter()
emitRefreshRes()
}
})
......@@ -154,7 +175,7 @@ async function searchOrFilter() {
loading.value = true
// const q = queryInputValue.value === null ? "" : queryInputValue.value
const q = search.value
await msSearch(q, { ...paginationParams.value, filter: toValue(computedFilter), sort: msSortBy.value })
await msSearch(q, { ...paginationParams.value, filter: toValue(msFilterCompo), sort: msSortBy.value })
} catch (error: any) {
filterError.value = error
console.log(error)
......@@ -180,42 +201,37 @@ function clearFilterOrSearch() {
// searchOrFilter()
// emitRefreshRes()
}
// watch(msFilter, async (fos) => {
// searchOrFilter()
// emitRefreshRes()
// search.value = ''
// })
const totalHits = computed(() => {
return toValue(msResult)?.totalHits ?? toValue(msResult)?.estimatedTotalHits ?? 0
})
watch(filterInputValues, (newSoF) => {
if (isFilter.value && filterInputValues.value !== null && filterInputValues.value?.length % 3 === 0) {
msFilter.value = filterInputValues.value.map((it, index) => {
// 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("-").slice(0, -1).join("-")
// if (index >= 1 && (index + 1) % 3 === 1) {
// return ` AND ${sanitizedValue}`
// } else if ((index + 1) % 3 === 0) {
// return `"${sanitizedValue}"`
// } else {
// return `${sanitizedValue}`
// }
// }).join("")
// }
// })
const sanitizedValue = it.value.split("-").slice(0, -1).join("-")
if (index >= 1 && (index + 1) % 3 === 1) {
return ` AND ${sanitizedValue}`
} else if ((index + 1) % 3 === 0) {
return `"${sanitizedValue}"`
} else {
return `${sanitizedValue}`
}
watch(search, () => {
searchOrFilter()
// emitRefreshRes()
}).join("")
}
})
watch(search, () => {
watch(msFilterCompo, () => {
searchOrFilter()
emitRefreshRes()
})
const filterStep = computed(() => {
......@@ -304,6 +320,14 @@ async function downloadData() {
pendingDownloadData.value = false
}
}
function focusedOrBlur(isFocused: boolean) {
if (!isFocused) {
emitRefreshRes()
}
}
</script>
<template>
<v-card flat color="transparent">
......@@ -325,13 +349,18 @@ async function downloadData() {
props.title }}
</v-btn>
</v-badge></v-toolbar>
<v-toolbar><v-text-field v-model="search" label="Search..." hide-details="auto"
prepend-inner-icon="mdi-magnify" single-line clearable class="mx-2"></v-text-field></v-toolbar>
<v-toolbar><v-autocomplete ref="autocompleteInput" hide-details v-model:model-value="filterOrSearch"
<v-toolbar>
<v-text-field v-model="search" label="Search..." hide-details="auto"
prepend-inner-icon="mdi-magnify" single-line clearable :disabled="pendingDownloadData"
class="mx-2" @update:focused="focusedOrBlur"></v-text-field>
</v-toolbar>
<v-toolbar>
<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" class="mx-2"
@click:clear="clearFilterOrSearch" @update:modelValue="() => clearSearch()">
:disabled="pendingDownloadData" prepend-inner-icon="md:search"
@click:appendInner="searchOrFilter" class="mx-2" @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 === deleteOneFilter(index)"></v-chip>
......@@ -343,7 +372,8 @@ async function downloadData() {
</v-list-item>
</template>
</v-autocomplete></v-toolbar>
</v-autocomplete>
</v-toolbar>
</template>
<template v-else>
<v-toolbar>
......@@ -357,14 +387,16 @@ async function downloadData() {
<v-spacer></v-spacer>
<v-card variant="flat" color="transparent" :min-width="400" class="mx-2" :rounded="false">
<v-text-field v-model="search" label="Search..." hide-details="auto"
prepend-inner-icon="mdi-magnify" single-line clearable></v-text-field>
:disabled="pendingDownloadData" prepend-inner-icon="mdi-magnify" single-line clearable
@update:focused="focusedOrBlur"></v-text-field>
</v-card>
<v-card variant="flat" color="transparent" :min-width="500" class="mx-2" :rounded="false">
<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()">
:disabled="pendingDownloadData" 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 === deleteOneFilter(index)"></v-chip>
......@@ -377,6 +409,12 @@ async function downloadData() {
</v-list-item>
</template>
</v-autocomplete>
</v-card>
<v-card>
<AutocompleteMeiliFacets v-model="msFilterCompo" :db="props.db"
@refresh:search="() => console.log('refresh the search new compo')">
</AutocompleteMeiliFacets>
</v-card>
</v-toolbar>
......
......@@ -82,16 +82,7 @@ const computedWidth = computed(() => {
const allHits: Ref<Record<string, any> | undefined> = ref(undefined)
// onMounted(async () => {
// console.log("on mounted get all hits")
// const params = {
// facets: ["*"],
// filter: undefined,
// sort: ["type:asc"],
// limit: 500000
// }
// getAllHits({ index: "refseq", params, query: "" })
// })
const pendingAllHits = ref(false)
async function getAllHits(params: { index: string, params: Record<string, any>, query: string }) {
console.log(params.index)
......@@ -249,10 +240,7 @@ const binPlotDataOptions = computed(() => {
type: scaleType.value,
tickFormat: '~s',
ticks: scaleType.value === 'symlog' ? 3 : 5,
// width: 350
},
// fy: { domain: groupSortDomain.value },
marks: [
Plot.cell(toValueAllHits?.hits ?? [], Plot.group({ fill: "count" }, { x: "type", y: selectedTaxoRank.value, tip: true, inset: 0.5, sort: { y: "fill" } })),
]
......
export interface FilterItem {
type: 'facet' | 'operator' | 'value' | 'text'
value: string
title: string
count?: number
deletable: boolean
props: Record<string, any>
}
export function useFacetFilters(inputFilters: MaybeRef<FilterItem[] | undefined>) {
}
\ No newline at end of file
This diff is collapsed.
......@@ -7,13 +7,13 @@
"preview": "nuxt preview"
},
"devDependencies": {
"@nuxt/content": "^2.9.0",
"@types/node": "^20.10.4",
"@vueuse/core": "^10.6.1",
"@vueuse/nuxt": "^10.6.1",
"@nuxt/content": "^2.10.0",
"@types/node": "^20.10.6",
"@vueuse/core": "^10.7.1",
"@vueuse/nuxt": "^10.7.1",
"nuxt": "^3.9.0",
"nuxt-meilisearch": "^1.1.0",
"vuetify-nuxt-module": "^0.7.3"
"vuetify-nuxt-module": "^0.8.0"
},
"overrides": {
"vue": "latest"
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment