From 8be506f720186d0f7b48441874a2657e05d3f69d Mon Sep 17 00:00:00 2001
From: Mathieu Massaviol <mathieu.massaviol@univ-amu.fr>
Date: Thu, 7 Nov 2024 15:27:18 +0100
Subject: [PATCH] Refactor of simple research #29

---
 .../OcResourceSearchSimple.stories.ts         | 47 ++++++++++
 .../OcResourceSearchSimple.vue                | 93 +++++++++++++++++++
 src/declarations.ts                           |  4 +-
 src/locales/en.ts                             |  1 +
 src/locales/fr.ts                             |  1 +
 src/modelMetadata/search.ts                   | 22 +++++
 src/pages/community/[community].search.vue    | 35 +++----
 src/pages/community/[community]/index.vue     |  2 +-
 src/sparql/search.ts                          | 61 +++++-------
 9 files changed, 203 insertions(+), 63 deletions(-)
 create mode 100644 src/components/Search/OcResourceSearchSimple/OcResourceSearchSimple.stories.ts
 create mode 100644 src/components/Search/OcResourceSearchSimple/OcResourceSearchSimple.vue
 create mode 100644 src/modelMetadata/search.ts

diff --git a/src/components/Search/OcResourceSearchSimple/OcResourceSearchSimple.stories.ts b/src/components/Search/OcResourceSearchSimple/OcResourceSearchSimple.stories.ts
new file mode 100644
index 0000000..837a07d
--- /dev/null
+++ b/src/components/Search/OcResourceSearchSimple/OcResourceSearchSimple.stories.ts
@@ -0,0 +1,47 @@
+import type { Meta, StoryObj } from '@storybook/vue3'
+import { fn } from '@storybook/test'
+
+import OcResourceSearchSimple from './OcResourceSearchSimple.vue'
+
+const meta: Meta<typeof OcResourceSearchSimple> = {
+  component: OcResourceSearchSimple
+}
+
+export default meta
+type Story = StoryObj<typeof OcResourceSearchSimple>
+
+export const Default: Story = {
+  render: (args) => ({
+    components: { OcResourceSearchSimple },
+    setup() {
+      return { args }
+    },
+    template: '<OcResourceSearchSimple v-bind="args" />'
+  }),
+  args: {
+    modelValue: {
+      params: {}
+    },
+    onSubmit: fn()
+  }
+}
+
+export const InitialValues: Story = {
+  render: (args) => ({
+    components: { OcResourceSearchSimple },
+    setup() {
+      return { args }
+    },
+    template: '<OcResourceSearchSimple v-bind="args" />'
+  }),
+  args: {
+    modelValue: {
+      q: "DRIIHM",
+      params: {
+        title: ["mon titre"],
+        creator: ["jean", "michel"]
+      }
+    },
+    onSubmit: fn()
+  }
+}
\ No newline at end of file
diff --git a/src/components/Search/OcResourceSearchSimple/OcResourceSearchSimple.vue b/src/components/Search/OcResourceSearchSimple/OcResourceSearchSimple.vue
new file mode 100644
index 0000000..0a43426
--- /dev/null
+++ b/src/components/Search/OcResourceSearchSimple/OcResourceSearchSimple.vue
@@ -0,0 +1,93 @@
+<template>
+  <IconField class="w-full mt-8 mb-4">
+    <InputIcon class="fa-solid fa-magnifying-glass cursor-pointer hover:text-primary" @click="emit('submit')" />
+    <InputText
+      id="search"
+      v-model="model.q"
+      fluid
+      size="large"
+      :placeholder="t('community.homepage.searchBarPlaceholder')"
+      v-on:keyup.enter="emit('submit')"
+      :disabled="props.loading"
+    />
+    <InputIcon
+      v-if="searchPop"
+      :class="{
+        'fa-chevron-down': !searchPop.visible,
+        'fa-chevron-up': searchPop.visible
+      }"
+      class="fa-solid"
+      @click="searchPop.toggle"
+    />
+  </IconField>
+  <div id="popoverPlace" class="h-0 m-0 p-0" />
+  <Popover id="popover" class="h-fit w-full" ref="searchPop" appendTo="#popoverPlace">
+    <div v-for="key in Object.keys(SearchQueryParams)" v-bind:key="key">
+      <label :for="key" class="font-medium">{{ translateValue(searchMetadata[key as keyof typeof SearchQueryParams].label) }}</label>
+      <OcMultipleField
+        class="mt-1 mb-2"
+        :name="key"
+        :id="key"
+        :inputComponent="InputText"
+        :inputProps="{ fluid: true }"
+        :initialValue="model.params[key as keyof typeof SearchQueryParams]"
+        v-on:updateValue="handleChange($event, key)"
+      />
+    </div>
+    <div class="text-right">
+      <Button class="mt-6" @click="searchPop.toggle($event), emit('submit')">
+        {{ t('search.launchSearch') }}
+      </Button>
+    </div>
+  </Popover>
+</template>
+
+<script setup lang="ts">
+import IconField from 'primevue/iconfield'
+import InputText from 'primevue/inputtext'
+import InputIcon from 'primevue/inputicon'
+import Button from 'primevue/button'
+import Popover from 'primevue/popover'
+import { SearchQueryParams, type OcSearchQuery } from '@/declarations'
+import { useI18n } from 'vue-i18n'
+import { ref } from 'vue'
+import OcMultipleField from '@/components/FormInputs/OcMultipleField/OcMultipleField.vue'
+import { searchMetadata } from '@/modelMetadata/search'
+import { useTranslateValue } from '@/composables/useTranslateValue'
+
+const { t } = useI18n()
+const { translateValue } = useTranslateValue()
+
+const model = defineModel<OcSearchQuery>({
+  required: true,
+  default: {
+    params: {}
+  }
+})
+
+const props = defineProps({
+  loading: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emit = defineEmits<{
+  submit: []
+}>()
+
+const searchPop = ref()
+
+function handleChange(newValue: any, key: string) {
+  model.value.params[key as keyof typeof SearchQueryParams] = newValue
+}
+</script>
+<style scoped>
+:deep(#popover) {
+  position: relative !important;
+  left: 0 !important;
+  top: 0 !important;
+  z-index: 1 !important;
+  --p-popover-arrow-offset: 0.3rem
+}
+</style>
diff --git a/src/declarations.ts b/src/declarations.ts
index 10a0d66..93cd00b 100644
--- a/src/declarations.ts
+++ b/src/declarations.ts
@@ -147,9 +147,11 @@ export type OcSearchResult = OcResource & {
 /** A representation of a search query */
 export type OcSearchQuery = {
   q?: string
-  params?: Partial<Record<keyof typeof SearchQueryParams, string|string[]>>
+  params: OcSearchParameters
 }
 
+export type OcSearchParameters = Partial<Record<keyof typeof SearchQueryParams, string[]>>
+
 export enum SearchQueryParams {
   title = "http://purl.org/dc/terms/title",
   description = "http://purl.org/dc/terms/description",
diff --git a/src/locales/en.ts b/src/locales/en.ts
index e3c9657..27f1f3a 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -348,6 +348,7 @@ Greetings,
     title: "Search",
     resourceType: "Resource type",
     results: "Results",
+    launchSearch: "Launch the search",
     searchResult: {
       description: "Description",
       datasets: "Datasets",
diff --git a/src/locales/fr.ts b/src/locales/fr.ts
index 180ebb7..7868483 100644
--- a/src/locales/fr.ts
+++ b/src/locales/fr.ts
@@ -363,6 +363,7 @@ Cordialement,
     title: "Rechercher",
     resourceType: "Type de ressource",
     results: "Résultats",
+    launchSearch: "Lancer la recherche",
     searchResult: {
       description: "Description",
       catalogues: "Sous-catalogues",
diff --git a/src/modelMetadata/search.ts b/src/modelMetadata/search.ts
new file mode 100644
index 0000000..fe7799a
--- /dev/null
+++ b/src/modelMetadata/search.ts
@@ -0,0 +1,22 @@
+import type { OcModelMetadata, OcSearchParameters } from "@/declarations";
+
+export const searchMetadata: OcModelMetadata<OcSearchParameters> = {
+  title: {
+    label: {
+        fr: "Titre",
+        en: "Title"
+    }
+  },
+  description: {
+    label: {
+        fr: "Description",
+        en: "Description"
+    }
+  },
+  creator: {
+    label: {
+        fr: "Auteur",
+        en: "Author"
+    }
+  }
+}
diff --git a/src/pages/community/[community].search.vue b/src/pages/community/[community].search.vue
index c537edc..8423e6f 100644
--- a/src/pages/community/[community].search.vue
+++ b/src/pages/community/[community].search.vue
@@ -5,18 +5,11 @@
         <h1 class="font-title text-4xl uppercase font-bold mb-4 text-primary">
           {{ t('search.title') }}
         </h1>
-        <IconField class="w-full mt-8 mb-4">
-          <InputText
-            id="search"
-            v-model="searchQuery.q"
-            fluid
-            size="large"
-            :placeholder="t('community.homepage.searchBarPlaceholder')"
-            v-on:keyup.enter="submit"
-            :disabled="searching"
-          />
-          <InputIcon class="fa-solid fa-magnifying-glass" :onclick="submit" />
-        </IconField>
+        <OcResourceSearchSimple
+          v-model="searchQuery"
+          :loading="searching"
+          v-on:submit="submit"
+        />
         <div v-if="!searching">
           <h4 v-if="searched" class="font-bold text-xl mb-4">
             {{ searchResultList.length }} {{ t('search.results') }}
@@ -59,14 +52,12 @@ import OcLayoutSimple from '@/layout/OcLayoutSimple/OcLayoutSimple.vue'
 import { useAccountStore } from '@/stores/account'
 import { computed, onBeforeMount, ref, watch } from 'vue'
 import { useI18n } from 'vue-i18n'
-import IconField from 'primevue/iconfield'
-import InputText from 'primevue/inputtext'
-import InputIcon from 'primevue/inputicon'
 import OcSearchResultCard from '@/components/Search/OcSearchResultCard/OcSearchResultCard.vue'
 import OcSearchResultCardSkeleton from '@/components/Search/OcSearchResultCardSkeleton/OcSearchResultCardSkeleton.vue'
 import { getSearchResults, searchResources } from '@/sparql/search'
 import Paginator from 'primevue/paginator'
 import { useRoute, useRouter } from 'vue-router'
+import OcResourceSearchSimple from '@/components/Search/OcResourceSearchSimple/OcResourceSearchSimple.vue'
 
 definePage({
   name: 'community.search',
@@ -89,7 +80,7 @@ const router = useRouter()
 const itemsPerPage = 5
 const start = ref(0)
 
-const searchQuery = ref<OcSearchQuery>({})
+const searchQuery = ref<OcSearchQuery>({params: {}})
 
 const searchResultList = ref<string[]>([])
 const searchResultObjects = ref<OcSearchResult[]>([])
@@ -107,6 +98,7 @@ const submit = () => {
     },
     query: {
       q: searchQuery.value.q,
+      ...searchQuery.value.params,
       start: start.value
     }
   })
@@ -166,21 +158,22 @@ const breadcrumbItems = computed<OcBreadcrumbItem[]>(() => [
 ])
 
 const loadRouteParams = () => {
-  searchQuery.value.q = route.query.q as string ?? ""
+  if (route.query.q){ searchQuery.value.q = route.query.q as string }  
   searchQuery.value.params = {}
   for (const param in SearchQueryParams){
     if (Array.isArray(route.query[param])){
-      searchQuery.value.params[param] = route.query[param] as string[]
+      searchQuery.value.params[param as keyof typeof SearchQueryParams] = route.query[param] as string[]
     } else {
-      searchQuery.value.params[param] = route.query[param] as string
+      if (route.query[param]){
+        searchQuery.value.params[param as keyof typeof SearchQueryParams] = [route.query[param] as string]
+      }
     }
   }
   start.value = route.query.start ? parseInt(route.query.start as string) : 0
-  console.log(searchQuery.value)
 }
 
-watch(() => route.query.q, search)
 watch(() => route.query.page, () => { getResultsForStart(start.value)})
+watch(() => route.query, search)
 
 onBeforeMount(() => {
   if (Object.keys(route.query).length > 0) {
diff --git a/src/pages/community/[community]/index.vue b/src/pages/community/[community]/index.vue
index 7d538ea..4d1d596 100644
--- a/src/pages/community/[community]/index.vue
+++ b/src/pages/community/[community]/index.vue
@@ -31,7 +31,7 @@
         :placeholder="t('community.homepage.searchBarPlaceholder')"
         v-on:keyup.enter="search"
       />
-      <InputIcon class="fa-solid fa-magnifying-glass" @click="search"/>
+      <InputIcon class="fa-solid fa-magnifying-glass cursor-pointer hover:text-primary" @click="search"/>
     </IconField>
 
     </section>
diff --git a/src/sparql/search.ts b/src/sparql/search.ts
index 1a9e8df..fea4c5b 100644
--- a/src/sparql/search.ts
+++ b/src/sparql/search.ts
@@ -7,58 +7,39 @@ import { resourceContext } from './resource'
  * Get results URI of a search query
  */
 export const searchResources = async (query: OcSearchQuery, auth?: Credentials) => {
+  console.log(query)
   let queryFilter = ''
   if (query.q) {
     queryFilter = `FILTER regex(?o, "${query.q}", "i")`
   }
 
   if (query.params?.title){
-    queryFilter += '?resource dct:title ?title.\n'
-    if (Array.isArray(query.params.title)){
-      query.params.title.forEach((title, index) => {
-        queryFilter += `
-          ?resource dct:title ?title${index}.
-          FILTER regex(?title${index}, "${title}", "i")
+    query.params.title.forEach((title, index) => {
+      queryFilter += `
+        ?resource dct:title ?title${index}.
+        FILTER regex(?title${index}, "${title}", "i")
         `
-      })
-    } else {
-      queryFilter += `FILTER regex(?title, "${query.params.title}", "i")\n`
-    }
-    
+    })    
   }
 
   if (query.params?.description){
-    queryFilter += '?resource dct:description ?description.\n'
-    if (Array.isArray(query.params.description)){
-      query.params.description.forEach((description, index) => {
-        queryFilter += `
-          ?resource dct:description ?description${index}.
-          FILTER regex(?description${index}, "${description}", "i")
-        `
-      })
-    } else {
-      queryFilter += `FILTER regex(?description, "${query.params.description}", "i")\n`
-    }
+    query.params.description.forEach((description, index) => {
+      queryFilter += `
+        ?resource dct:description ?description${index}.
+        FILTER regex(?description${index}, "${description}", "i")
+      `
+    })
   }
 
   if (query.params?.creator){
-    queryFilter += `
-      ?resource dct:creator ?creator.
-      VALUES ?pCreator { foaf:name foaf:givenName foaf:familyName foaf:firstName }
-      ?creator ?pCreator ?oCreator.
-    `
-    if (Array.isArray(query.params.creator)){
-      query.params.creator.forEach((creator, index) => {
-        queryFilter += `
-          ?resource dct:creator ?creator${index}.
-          VALUES ?pCreator${index} { foaf:name foaf:givenName foaf:familyName foaf:firstName }
-          ?creator${index} ?pCreator${index} ?oCreator${index}.
-          FILTER regex(?oCreator${index}, "${creator}", "i")
-        `
-      })
-    } else {
-      queryFilter += `FILTER regex(?oCreator, "${query.params.creator}", "i")\n`
-    }
+    query.params.creator.forEach((creator, index) => {
+      queryFilter += `
+        ?resource dct:creator ?creator${index}.
+        VALUES ?pCreator${index} { foaf:name foaf:givenName foaf:familyName foaf:firstName }
+        ?creator${index} ?pCreator${index} ?oCreator${index}.
+        FILTER regex(?oCreator${index}, "${creator}", "i")
+      `
+    })
   }
 
   const res = await executeSparqlSelect(
@@ -133,7 +114,7 @@ export const getSearchResults = async (resourceUriList: string[], auth?: Credent
     }
   }
 
-  const formattedUris = '<' + resourceUriList.join('> <') + '>'
+  const formattedUris = '<' + resourceUriList.map((uri) => uri.trim()).join('> <') + '>'
 
   const res = await executeSparqlConstruct<OcSearchResult>(
     `
-- 
GitLab