From dc1c61b9bdfca56f48ca88e03b36bc83c4a8e53a Mon Sep 17 00:00:00 2001 From: Remi PLANEL <rplanel@pasteur.fr> Date: Tue, 28 Nov 2023 20:46:54 +0100 Subject: [PATCH] WIP: start handle free text search --- components/ServerDbTable.vue | 139 ++++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 36 deletions(-) diff --git a/components/ServerDbTable.vue b/components/ServerDbTable.vue index ef87a964..dade7e3e 100644 --- a/components/ServerDbTable.vue +++ b/components/ServerDbTable.vue @@ -3,8 +3,6 @@ import { useDisplay } from "vuetify"; import { useFacetsStore, type Facets } from '~~/stores/facets' import { useMeiliSearch } from "#imports" - -import type { SearchResponse, SearchParams } from 'meilisearch' interface SortItem { key: string, order: boolean | 'asc' | 'desc' @@ -20,10 +18,16 @@ export interface Props { } export interface FilterItem { - type: 'facet' | 'operator' | 'value' + type: 'facet' | 'operator' | 'value' | 'text' value: string title: string count?: number + props: { + [key: string]: any + // title: string + // value: any + } + // raw?: any } const props = withDefaults(defineProps<Props>(), { @@ -39,11 +43,11 @@ 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[] | string | null> = ref(null) +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(undefined) +const msFilter: Ref<string | undefined> = ref(undefined) const page = ref(1) let loading = ref(false) @@ -54,6 +58,32 @@ const computedTableHeight = computed(() => { return computedHeight > minTableHeight.value ? computedHeight : minTableHeight.value }) + +const filterInputValues = computed(() => { + if (filterOrSearch.value != null) { + return filterOrSearch.value.filter(({ props }) => props.type !== 'text') + } else { + return null + } +}) + +const queryInputValue = computed(() => { + if (filterOrSearch.value !== null) { + const phrase = filterOrSearch.value + .filter((f) => { + console.log(f) + return f.props.type === 'text' + }) + .map((f) => { + return f.value + }) + .join(" ") + return `'${phrase}'` + } else { + return null + } +}) + const isFilter = computed(() => { return Array.isArray(filterOrSearch.value) }) @@ -70,23 +100,6 @@ const msSortBy = computed(() => { }) - -// const msFilter = computed(() => { -// if (isFilter.value) { -// return filterOrSearch.value.map((it, index) => { -// if (index >= 1 && (index + 1) % 3 === 1) { -// return ` AND ${it.value}` -// } else if ((index + 1) % 3 === 0) { -// return `"${it.value}"` -// } else { -// return `${it.value}` -// } - -// }).join("") -// } -// // else { return undefined } -// }) - const reactiveParams = reactive({ hitsPerPage: 25, page: 1, @@ -94,7 +107,7 @@ const reactiveParams = reactive({ facets: ["*"], filter: [], sort: ["type:asc"], - attributesToHighlight: ["*"] + // attributesToHighlight: ["*"] }) @@ -118,7 +131,8 @@ const msError = computed(() => { async function searchOrFilter() { try { loading.value = true - await msSearch(toValue(search), { ...reactiveParams, filter: msFilter.value, sort: msSortBy.value }) + const q = queryInputValue.value === null ? "" : queryInputValue.value + await msSearch(q, { ...reactiveParams, filter: msFilter.value, sort: msSortBy.value }) } catch (error: any) { filterError.value = error console.log(error) @@ -150,9 +164,11 @@ watch(msResult, (newRes) => { facetStore.setFacets({ facetDistribution: newRes.facetDistribution, facetStat: newRes.facetStat }) }, { deep: true }) -watch(filterOrSearch, (newSoF) => { - if (isFilter.value && newSoF.length % 3 === 0) { - msFilter.value = newSoF.map((it, index) => { + + +watch(filterInputValues, (newSoF) => { + if (isFilter.value && filterInputValues.value !== null && filterInputValues.value?.length % 3 === 0) { + msFilter.value = filterInputValues.value.map((it, index) => { if (index >= 1 && (index + 1) % 3 === 1) { return ` AND ${it.value}` } else if ((index + 1) % 3 === 0) { @@ -165,10 +181,18 @@ watch(filterOrSearch, (newSoF) => { } }) const filterStep = computed(() => { - return (Array.isArray(filterOrSearch.value) && filterOrSearch.value.length > 0) ? filterOrSearch.value?.length % 3 : null + return filterInputValues.value !== null && filterInputValues.value.length > 0 ? filterInputValues.value?.length % 3 : null }) const operatorItems = ref([ - { type: "operator", value: '=', title: "is" }, { type: "operator", value: '!=', title: "is not" } + { + type: "operator", value: '=', title: "is", props: { + type: "operator" + } + }, { + type: "operator", value: '!=', title: "is not", props: { + type: "operator" + } + } ]) const autocompleteItems = computed(() => { @@ -177,7 +201,10 @@ const autocompleteItems = computed(() => { return { type: "facet", value, - title: value + title: value, + props: { + type: "facet" + } } }) } @@ -192,18 +219,31 @@ const autocompleteItems = computed(() => { const facetDistri = facetStore.facets?.facetDistribution console.log(facetDistri) return facetDistri?.[value] ? Object.entries(facetDistri[value]).map(([key, val]) => { - return { type: "value", value: key, title: key, count: val } + return { + type: "value", value: key, title: key, count: val, props: { + type: 'value', count: val + } + } }) : [] } } }) +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) { +function deleteOneFilter(index: number) { console.log("deleteOnefilter") + console.log(index) console.log(isFilter.value) console.log(filterOrSearch) if (isFilter.value) { @@ -211,17 +251,41 @@ function deleteOneFilter(index) { } } + + function clearSearch() { search.value = "" } + + + +function runTextSearch() { + if (canAddTextSearch) { + const item: FilterItem = { + type: 'text', title: search.value, value: search.value, props: { type: "text" } + } + if (Array.isArray(filterOrSearch.value)) { + filterOrSearch.value = [ + ...filterOrSearch.value, item + + ] + } else { + filterOrSearch.value = [item] + } + search.value = "" + searchOrFilter() + } +} + </script> <template> <v-card flat> <v-toolbar> - <v-autocomplete v-model:search="search" v-model:model-value="filterOrSearch" auto-select-first chips clearable - label="Search or filter results..." :items="autocompleteItems" item-value="value" item-title="title" - multiple return-object append-inner-icon="md:search" @click:appendInner="searchOrFilter" - @click:clear="clearFilterOrSearch" @update:modelValue="() => clearSearch()"> + <v-autocomplete ref="autocompleteInput" v-model:search="search" v-model:model-value="filterOrSearch" + auto-select-first chips clearable label="Search or filter results..." :items="autocompleteItems" + item-value="value" item-title="title" multiple return-object append-inner-icon="md:search" + @click:appendInner="searchOrFilter" @click:clear="clearFilterOrSearch" + @update:modelValue="() => clearSearch()"> <template #chip="{ props, item, index }"> <v-chip v-if="(index + 1) % 3 === 0" v-bind="props" :text="item.raw.title" closable @click:close="deleteOneFilter(index)"></v-chip> @@ -233,6 +297,9 @@ function clearSearch() { </v-list-item> </template> + <template #prepend-item> + <v-list-item v-if="canAddTextSearch" title="Text search" @click="runTextSearch"> </v-list-item> + </template> </v-autocomplete> </v-toolbar> <v-data-table-server v-if="!msError" v-model:page="reactiveParams.page" -- GitLab