diff --git a/src/components/Search/OcResourceSearchFacetList/OcResourceSearchFacetList.stories.ts b/src/components/Search/OcResourceSearchFacetList/OcResourceSearchFacetList.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..717aa4a90f20ecd0f785f7d7ed5600471c673041 --- /dev/null +++ b/src/components/Search/OcResourceSearchFacetList/OcResourceSearchFacetList.stories.ts @@ -0,0 +1,23 @@ +import type { Meta, StoryObj } from '@storybook/vue3' + +import OcResourceSearchFacetList from './OcResourceSearchFacetList.vue' + +const meta: Meta<typeof OcResourceSearchFacetList> = { + component: OcResourceSearchFacetList +} + +export default meta +type Story = StoryObj<typeof OcResourceSearchFacetList> + +export const Default: Story = { + render: (args) => ({ + components: { OcResourceSearchFacetList }, + setup() { + return { args } + }, + template: '<OcResourceSearchFacetList v-bind="args" />' + }), + args: { + modelValue: {} + } +} diff --git a/src/components/Search/OcResourceSearchFacetList/OcResourceSearchFacetList.vue b/src/components/Search/OcResourceSearchFacetList/OcResourceSearchFacetList.vue new file mode 100644 index 0000000000000000000000000000000000000000..e58907f3b840e376f16ab810084ee23fc9562290 --- /dev/null +++ b/src/components/Search/OcResourceSearchFacetList/OcResourceSearchFacetList.vue @@ -0,0 +1,47 @@ +<template> + <Panel :header="t('search.resourceType')" toggleable> + <template #toggleicon="toggleIconProps"> + <i v-if="toggleIconProps.collapsed" class="fa-solid fa-chevron-down text-gray-700" /> + <i v-else class="fa-solid fa-chevron-up text-gray-700" /> + </template> + <div class="flex items-center gap-2" v-for="(dcatType, uri) in type2ResourceType" v-bind:key="uri"> + <Checkbox + v-model="model.resourceType" + :value="uri" + :disabled="props.loading" + /> + <label for="resourceType" class="capitalize"> + {{ t('resourceType.' + dcatType) }} + </label> + </div> + </Panel> +</template> + +<script setup lang="ts"> +import type { OcSearchQuery } from '@/declarations'; +import { useI18n } from 'vue-i18n'; +import Panel from 'primevue/panel' +import Checkbox from 'primevue/checkbox' +import { type2ResourceType } from '@/helpers/resourceType' + +const { t } = useI18n() + +const model = defineModel<Partial<OcSearchQuery>>({ required: true }) + +const props = defineProps({ + loading: { + type: Boolean, + default: false + } +}) + +</script> +<style scoped> +:deep(.p-panel-header) { + @apply bg-primary rounded-t text-white mb-2; +} + +:deep(.p-panel) { + @apply bg-gray-100; +} +</style> \ No newline at end of file diff --git a/src/components/OcSearchResultCard/OcSearchResultCard.stories.ts b/src/components/Search/OcSearchResultCard/OcSearchResultCard.stories.ts similarity index 81% rename from src/components/OcSearchResultCard/OcSearchResultCard.stories.ts rename to src/components/Search/OcSearchResultCard/OcSearchResultCard.stories.ts index d65470ee7f5ebf8459ff61b50c21407ad80a2c54..29705f5087d812f2fe3a2d896cc02d1371f526ff 100644 --- a/src/components/OcSearchResultCard/OcSearchResultCard.stories.ts +++ b/src/components/Search/OcSearchResultCard/OcSearchResultCard.stories.ts @@ -148,6 +148,21 @@ export const Default: Story = { 'title': {"fr":"LabEx DRIIHM 2","en":"DRIIHM LabEx 2"}, 'graph': ["ex:sandbox2"] } + }, + community: { + description: { + fr: "Lauréat de la deuxième vague de l'appel à projet Laboratoire d'Excellence (LabEx) dans le cadre du programme « Investissements d'avenir », le LabEx DRIIHM, Dispositif de Recherche Interdisciplinaire sur les Interactions Hommes-Milieux, regroupe à ce jour 13 Observatoires Hommes-Milieux, outils d'observation de socio-écosystèmes impactés par un événement d'origine anthropique. Créés par le CNRS-INEE en 2007, ils sont répartis en France métropolitaine, en outre-mer et à l’étranger.", + en: "Laureate of the Laboratory for Excellence project (LabEx) in the program « Investment in the future », the DRIIHM LabEx, Device for Interdisciplinary Research on human-environments Interactions, aggregate 13 human-environments observatories (OHM in french), tools for observing socio-ecosystems impacted by anthropic events. Created by CNRS-INEE in 2007, they are located in metropolitan France, overseas France and abroad." + }, + identifier: "189088ec-baa9-4397-8c6f-eefde9a3790c", + title: { + fr: "Communauté du LabEx DRIIHM", + en: "DRIIHM Community" + }, + logo: "https://www.driihm.fr/images/images/logos_png/logo_DRIIHM_r%C3%A9duit.png", + name: "driihm", + isSpaceOf: "https://www.irit.fr/opencommon/agents/organization/9a20f121-c64e-4049-93a7-4bedbe819fd6", + color: 'linen' } } } diff --git a/src/components/OcSearchResultCard/OcSearchResultCard.vue b/src/components/Search/OcSearchResultCard/OcSearchResultCard.vue similarity index 96% rename from src/components/OcSearchResultCard/OcSearchResultCard.vue rename to src/components/Search/OcSearchResultCard/OcSearchResultCard.vue index a66d38f490f6031fe92588a9eadf0bc56ce991bf..12fa32f43daba495675d75f0e111cefcbfb81277 100644 --- a/src/components/OcSearchResultCard/OcSearchResultCard.vue +++ b/src/components/Search/OcSearchResultCard/OcSearchResultCard.vue @@ -51,9 +51,9 @@ import { iconsDict } from '@/helpers/icons'; import { getResourceTypeFromAtType, ResourceType } from '@/helpers/resourceType'; import { computed, type PropType } from 'vue' import { useI18n } from 'vue-i18n'; -import OcLink from '../OcLink.vue'; +import OcLink from '@/components/OcLink.vue'; import { getResourceVisibility } from '@/helpers/resourceVisibility'; -import OcVisibilityIcon from '../OcVisibilityIcon/OcVisibilityIcon.vue'; +import OcVisibilityIcon from '@/components/OcVisibilityIcon/OcVisibilityIcon.vue'; const { t } = useI18n() const { translateValue } = useTranslateValue() diff --git a/src/components/OcSearchResultCardSkeleton/OcSearchResultCardSkeleton.stories.ts b/src/components/Search/OcSearchResultCardSkeleton/OcSearchResultCardSkeleton.stories.ts similarity index 100% rename from src/components/OcSearchResultCardSkeleton/OcSearchResultCardSkeleton.stories.ts rename to src/components/Search/OcSearchResultCardSkeleton/OcSearchResultCardSkeleton.stories.ts diff --git a/src/components/OcSearchResultCardSkeleton/OcSearchResultCardSkeleton.vue b/src/components/Search/OcSearchResultCardSkeleton/OcSearchResultCardSkeleton.vue similarity index 100% rename from src/components/OcSearchResultCardSkeleton/OcSearchResultCardSkeleton.vue rename to src/components/Search/OcSearchResultCardSkeleton/OcSearchResultCardSkeleton.vue diff --git a/src/declarations.ts b/src/declarations.ts index 6c2b39a1b09f9d56adf3531aa6ae40ade1d2577e..10a0d66145764c80bddff0e5a8e71a7f6ad7506f 100644 --- a/src/declarations.ts +++ b/src/declarations.ts @@ -146,8 +146,14 @@ export type OcSearchResult = OcResource & { /** A representation of a search query */ export type OcSearchQuery = { - queryString?: string - resourceType?: Array<keyof typeof type2ResourceType> + q?: string + params?: Partial<Record<keyof typeof SearchQueryParams, string|string[]>> +} + +export enum SearchQueryParams { + title = "http://purl.org/dc/terms/title", + description = "http://purl.org/dc/terms/description", + creator = "http://purl.org/dc/terms/creator" } export type OcTreeNode = OcResource & { diff --git a/src/pages/community/[community].search.vue b/src/pages/community/[community].search.vue index e87616567ecea6abc1f6b035380013d5271cc944..c537edc6da08590cb21aa2ed8cf5d58e798827de 100644 --- a/src/pages/community/[community].search.vue +++ b/src/pages/community/[community].search.vue @@ -1,27 +1,6 @@ <template> <OcLayoutSimple :breadcrumb-items="breadcrumbItems" :is-authenticated="accountStore.isAuthenticated"> <div class="flex flex-row"> - <div class="p-4 pl-8 w-2/12 ml-auto mr-auto"> - <Panel :header="t('search.resourceType')" toggleable> - <template #toggleicon="toggleIconProps"> - <i v-if="toggleIconProps.collapsed" class="fa-solid fa-chevron-down text-gray-700" /> - <i v-else class="fa-solid fa-chevron-up text-gray-700" /> - </template> - <div class="flex items-center gap-2" v-for="(dcatType, uri) in type2ResourceType" v-bind:key="uri"> - <Checkbox - v-model="resourceType" - :inputId="uri" - name="resourceType" - :value="uri" - @change="submit" - :disabled="searching" - /> - <label for="resourceType" class="capitalize"> - {{ t('resourceType.' + dcatType) }} - </label> - </div> - </Panel> - </div> <div class="p-4 pl-8 w-9/12 ml-auto mr-auto"> <h1 class="font-title text-4xl uppercase font-bold mb-4 text-primary"> {{ t('search.title') }} @@ -29,7 +8,7 @@ <IconField class="w-full mt-8 mb-4"> <InputText id="search" - v-model="queryString" + v-model="searchQuery.q" fluid size="large" :placeholder="t('community.homepage.searchBarPlaceholder')" @@ -75,7 +54,7 @@ import { useTranslateValue } from '@/composables/useTranslateValue' import { useAccountData } from '@/dataLoaders/account' import { useCommunityData } from '@/dataLoaders/community' -import type { OcBreadcrumbItem, OcSearchResult } from '@/declarations' +import { SearchQueryParams, type OcBreadcrumbItem, type OcSearchQuery, type OcSearchResult } from '@/declarations' import OcLayoutSimple from '@/layout/OcLayoutSimple/OcLayoutSimple.vue' import { useAccountStore } from '@/stores/account' import { computed, onBeforeMount, ref, watch } from 'vue' @@ -83,13 +62,10 @@ import { useI18n } from 'vue-i18n' import IconField from 'primevue/iconfield' import InputText from 'primevue/inputtext' import InputIcon from 'primevue/inputicon' -import OcSearchResultCard from '@/components/OcSearchResultCard/OcSearchResultCard.vue' -import OcSearchResultCardSkeleton from '@/components/OcSearchResultCardSkeleton/OcSearchResultCardSkeleton.vue' +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 Panel from 'primevue/panel' -import Checkbox from 'primevue/checkbox' -import { type2ResourceType } from '@/helpers/resourceType' import { useRoute, useRouter } from 'vue-router' definePage({ @@ -113,8 +89,7 @@ const router = useRouter() const itemsPerPage = 5 const start = ref(0) -const queryString = ref('') -const resourceType = ref<string[]>([]) +const searchQuery = ref<OcSearchQuery>({}) const searchResultList = ref<string[]>([]) const searchResultObjects = ref<OcSearchResult[]>([]) @@ -131,8 +106,7 @@ const submit = () => { community: community.value.name }, query: { - q: queryString.value, - type: resourceType.value, + q: searchQuery.value.q, start: start.value } }) @@ -146,13 +120,7 @@ const search = async () => { searching.value = true searchResultObjects.value = [] try { - searchResultList.value = await searchResources( - { - queryString: queryString.value, - resourceType: resourceType.value - }, - accountStore.auth - ) + searchResultList.value = await searchResources(searchQuery.value, accountStore.auth) } catch (e) { console.error(e) errorMessage.value = (e as Error).message ?? 'Error' @@ -198,20 +166,24 @@ const breadcrumbItems = computed<OcBreadcrumbItem[]>(() => [ ]) const loadRouteParams = () => { - queryString.value = route.query.q as string ?? "" - if (Array.isArray(route.query.type)){ - resourceType.value = route.query.type as string[] - } else if (typeof route.query.type === 'string'){ - resourceType.value = [route.query.type as string] + 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[] + } else { + searchQuery.value.params[param] = route.query[param] as string + } } - start.value = parseInt(route.query.start as string) ?? 0 + start.value = route.query.start ? parseInt(route.query.start as string) : 0 + console.log(searchQuery.value) } -watch(() => [route.query.q, route.query.type], search) -watch(() => route.query.page, () => { loadRouteParams(), getResultsForStart(start.value)}) +watch(() => route.query.q, search) +watch(() => route.query.page, () => { getResultsForStart(start.value)}) onBeforeMount(() => { - if (route.query.q || route.query.type) { + if (Object.keys(route.query).length > 0) { loadRouteParams() search() } @@ -219,11 +191,4 @@ onBeforeMount(() => { </script> <style scoped> -:deep(.p-panel-header) { - @apply bg-primary rounded-t text-white mb-2; -} - -:deep(.p-panel) { - @apply bg-gray-100; -} </style> diff --git a/src/sparql/search.ts b/src/sparql/search.ts index 917841447bf782781cc3db6c1000fbbab2c951f6..1a9e8df43babd043588f840d12b3e9275860ace9 100644 --- a/src/sparql/search.ts +++ b/src/sparql/search.ts @@ -7,16 +7,58 @@ import { resourceContext } from './resource' * Get results URI of a search query */ export const searchResources = async (query: OcSearchQuery, auth?: Credentials) => { - let typeFilter = '' - if (query.resourceType?.length) { - typeFilter = 'FILTER (?type = <' - typeFilter += query.resourceType.join('> || ?type = <') - typeFilter += '>)' + let queryFilter = '' + if (query.q) { + queryFilter = `FILTER regex(?o, "${query.q}", "i")` } - let queryFilter = '' - if (query.queryString) { - queryFilter = `FILTER regex(?o, "${query.queryString}", "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") + ` + }) + } 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` + } + } + + 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` + } } const res = await executeSparqlSelect( @@ -25,12 +67,11 @@ export const searchResources = async (query: OcSearchQuery, auth?: Credentials) WHERE { ?resource rdf:type ?type. ?resource dct:identifier ?identifier. - ${typeFilter} - FILTER EXISTS { - VALUES ?p { dct:title dct:description } - ?resource ?p ?o - ${queryFilter} - } + + VALUES ?p { dct:title dct:description } + ?resource ?p ?o. + ${queryFilter} + } ORDER BY ASC(?resource) `, {