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