Skip to content
Snippets Groups Projects
Commit 8c29001c authored by Mathieu Massaviol's avatar Mathieu Massaviol
Browse files

Create search page in community #29

parent f3584799
No related branches found
No related tags found
1 merge request!75Resolve "Permettre la recherche d'un jeu de données"
<template>
<div
class="rounded-md shadow-oc"
class="rounded-md shadow-md border bg-gray-100"
>
<div class="p-4">
<div id="title" class="font-semibold text-2xl">
<i :class="icon" :title="t('resourceType.'+dcatType)" class="mr-1"></i>
{{ title }}
<i :class="icon" :title="t('resourceType.'+dcatType)" class="mr-2"></i>
<OcLink
:to="{
name: 'community.resource',
params: {
identifier: identifier,
community: props.community.name,
resource: dcatType
}
}"
>
{{ title }}
</OcLink>
</div>
<div id="subtitle" class="text-sm text-gray-600">
{{ t('search.searchResult.id') }}: {{ identifier }} <span v-if="version">/ {{ t('search.searchResult.version') }} {{ version }}</span>
</div>
<div id="subtitle" class="text-sm text-gray-600">{{ subtitle }}</div>
<div id="creators" class="mt-1 mb-1">{{ creators }}</div>
<div id="description">
<p class="font-medium">{{ t('search.searchResult.description') }}</p>
<p class="text-justify text-sm">{{ description }}</p>
</div>
</div>
<div id="footer" class="pl-4 pr-4 pt-2 pb-2 bg-gray-100 rounded-b-md text-sm flex flex-row gap-2 justify-between">
<div id="footer" class="pl-4 pr-4 pt-2 pb-2 bg-gray-200 rounded-b-md text-sm flex flex-row gap-2 justify-between">
<span v-if="dcatType===ResourceType.CATALOG">
{{ catalogues }} {{ t('search.searchResult.catalogues') }} {{ t('and') }} {{ datasets }} {{ t('search.searchResult.datasets') }}
</span>
<span v-if="dcatType===ResourceType.DATASET">{{ distributions +' '+ t('search.searchResult.distributions')}}</span>
<span v-if="parentCatalogueTitle">{{ t('search.searchResult.parentCatalogue') + ' : ' + parentCatalogueTitle }}</span>
</div>
......@@ -22,11 +38,12 @@
</template>
<script setup lang="ts">
import { useTranslateValue } from '@/composables/useTranslateValue'
import type { OcSearchResult } from '@/declarations';
import type { OcCommunity, OcSearchResult } from '@/declarations';
import { iconsDict } from '@/helpers/icons';
import { ResourceType, type2ResourceType } from '@/helpers/resourceType';
import { computed, type PropType } from 'vue'
import { useI18n } from 'vue-i18n';
import OcLink from '../OcLink.vue';
const { t } = useI18n()
const { translateValue } = useTranslateValue()
......@@ -35,24 +52,34 @@ const props = defineProps({
searchResult: {
type: Object as PropType<OcSearchResult>,
required: true
},
community: {
type: Object as PropType<OcCommunity>,
required: true
}
})
const title = computed(() => translateValue(props.searchResult.title))
const dcatType = computed(() => {
const compatibleTypes = props.searchResult['@type'].filter((type) => type in type2ResourceType)
return type2ResourceType[compatibleTypes[0]] ?? ResourceType.UNKNOWN
if (Array.isArray(props.searchResult['@type'])){
const compatibleTypes = props.searchResult['@type'].filter((type) => type in type2ResourceType)
return type2ResourceType[compatibleTypes[0]] ?? ResourceType.UNKNOWN
} else {
return type2ResourceType[props.searchResult['@type']] ?? ResourceType.UNKNOWN
}
})
const icon = computed(() => {
return iconsDict[dcatType.value as string] ?? ''
})
const subtitle = computed(() => {
return props.searchResult.identifier + " / Version " + "1.0.0"
})
const identifier = computed(() => props.searchResult.identifier)
const version = computed(() => props.searchResult.version)
const creators = computed(() => props.searchResult.creator?.map((creator) => creator.name).join('; '))
const description = computed(() => translateValue(props.searchResult.description))
const parentCatalogueTitle = computed(() => translateValue(props.searchResult.parentCatalog?.title))
const parentCatalogueIdentifier = computed(() => props.searchResult.parentCatalog?.identifier)
const catalogues = computed(() => props.searchResult.catalog?.length ?? 0)
const datasets = computed(() => props.searchResult.dataset?.length ?? 0)
const distributions = computed(() => props.searchResult.distribution?.length ?? 0)
</script>
<style scoped>
......
import type { Meta, StoryObj } from '@storybook/vue3'
import OcSearchResultCardSkeleton from './OcSearchResultCardSkeleton.vue'
const meta: Meta<typeof OcSearchResultCardSkeleton> = {
component: OcSearchResultCardSkeleton
}
export default meta
type Story = StoryObj<typeof OcSearchResultCardSkeleton>
export const Default: Story = {
render: (args) => ({
components: { OcSearchResultCardSkeleton },
setup() {
return { args }
},
template: '<OcSearchResultCardSkeleton v-bind="args" />'
}),
}
<template>
<div class="bg-gray-100 w-full h-44 rounded-md ">
<div class="p-4 h-36">
<div id="title" class="font-semibold text-2xl flex flex-row gap-2 mb-2">
<p class="w-6 h-6 bg-slate-200 rounded animate-pulse"></p>
<p class="rounded bg-slate-200 w-4/5 h-6 text-white font-semibold text-2xl animate-pulse"></p>
</div>
<div id="subtitle" class="text-sm w-1/5 h-2 bg-slate-200 mb-2"></div>
<div id="creators" class="rounded bg-slate-200 w-4/5 h-4 text-white font-semibold text-2xl animate-pulse"></div>
<div id="description">
<p class="font-medium text-gray-500 animate-pulse">{{ t('search.searchResult.description') }}</p>
<p class="rounded text-sm bg-slate-200 h-8 w-full"></p>
</div>
</div>
<div id="footer" class="bg-gray-200 rounded-b-md h-8">
</div>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t } = useI18n()
</script>
<style scoped>
</style>
\ No newline at end of file
......@@ -7,6 +7,7 @@ import {
rolePlatformManager,
roleCatalogManager
} from '@/ability'
import type { type2ResourceType } from './helpers/resourceType'
export type Credentials = {
email: string
......@@ -135,12 +136,20 @@ export type OcDistributionSummary = OcResource & {
* for search page results cards
*/
export type OcSearchResult = OcResource & {
version?: string,
version?: string
creator?: Array<OcPerson | OcOrganization>
catalog?: string[]
dataset?: string[]
distribution?: string[]
parentCatalog?: OcCatalog
}
/** A representation of a search query */
export type OcSearchQuery = {
queryString?: string
resourceType?: Array<keyof typeof type2ResourceType>
}
export type OcTreeNode = OcResource & {
children: Array<OcTreeNode>
catalog?: Array<OcTreeNode | string>
......
......@@ -12,5 +12,6 @@ export const iconsDict:Record<string, string> = {
catalog: 'fa-solid fa-book-open',
dataset: 'fa-solid fa-table',
distribution: 'fa-solid fa-file',
service: 'fa-solid fa-cube'
service: 'fa-solid fa-cube',
search: 'fa-solid fa-magnifying-glass'
}
......@@ -126,7 +126,8 @@ export default {
signIn: 'Sign in',
signOut: 'Sign out',
register: 'Request an account',
joinCommunity: 'Join a community'
joinCommunity: 'Join a community',
search: 'Search'
},
back: 'Back',
next: 'Next',
......@@ -344,10 +345,14 @@ Greetings,
}
},
search: {
title: "Search",
resourceType: "Resource type",
searchResult: {
description: "Description",
datasets: "Datasets",
distributions: "Distributions",
parentCatalogue: "Parent catalogue"
parentCatalogue: "Parent catalogue",
id: "Id"
}
},
resourceType: {
......@@ -356,5 +361,6 @@ Greetings,
'distribution': 'distribution',
'service': 'service',
'unknown': 'unknown',
}
},
and: 'and'
}
......@@ -127,7 +127,8 @@ export default {
signIn: 'Se connecter',
signOut: 'Déconnexion',
register: "Demander la création d'un compte",
joinCommunity: 'Rejoindre une communauté'
joinCommunity: 'Rejoindre une communauté',
search: 'Rechercher'
},
back: 'Précédent',
next: 'Suivant',
......@@ -359,10 +360,15 @@ Cordialement,
},
},
search: {
title: "Rechercher",
resourceType: "Type de ressource",
searchResult: {
description: "Description",
catalogues: "Sous-catalogues",
datasets: "Jeux de données",
distributions: "Distributions",
parentCatalogue: "Catalogue parent"
parentCatalogue: "Catalogue parent",
id: "Id"
}
},
resourceType: {
......@@ -371,5 +377,6 @@ Cordialement,
'distribution': 'distribution',
'service': 'service',
'unknown': 'inconnu',
}
},
and: 'et'
}
<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" />
<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') }}
</h1>
<IconField class="w-full mt-8 mb-4">
<InputText
id="search"
v-model="queryString"
fluid
size="large"
:placeholder="t('community.homepage.searchBarPlaceholder')"
/>
<InputIcon class="fa-solid fa-magnifying-glass" :onclick="search" />
</IconField>
<div v-if="!searching">
<template v-for="result in searchResultObjects" v-bind:key="result">
<OcSearchResultCard :search-result="result" :community="community" class="mb-4" />
</template>
</div>
<div v-else>
<OcSearchResultCardSkeleton
v-for="n in itemsPerPage"
v-bind:key="n"
class="mb-4"
></OcSearchResultCardSkeleton>
</div>
<Paginator
class="mt-8"
always-show="false"
:rows="itemsPerPage"
:total-records="searchResultList.length"
v-bind:currentPage="currentPage"
v-on:page="getResultsForPage($event.page)"
/>
</div>
</div>
</OcLayoutSimple>
</template>
<script setup lang="ts">
import { useTranslateValue } from '@/composables/useTranslateValue'
import { useAccountData } from '@/dataLoaders/account'
import { useCommunityData } from '@/dataLoaders/community'
import type { OcBreadcrumbItem, OcSearchResult } from '@/declarations'
import OcLayoutSimple from '@/layout/OcLayoutSimple/OcLayoutSimple.vue'
import { useAccountStore } from '@/stores/account'
import { computed, ref } 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/OcSearchResultCard/OcSearchResultCard.vue'
import OcSearchResultCardSkeleton from '@/components/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'
definePage({
name: 'community.search',
meta: {
needsAuth: false,
loaders: [useCommunityData, useAccountData]
}
})
const accountStore = useAccountStore()
const { t } = useI18n()
const { translateValue } = useTranslateValue()
const { data: community } = useCommunityData()
const itemsPerPage = 5
const currentPage = ref(0)
const queryString = ref('')
const resourceType = ref([])
const searchResultList = ref<string[]>([])
const searchResultObjects = ref<OcSearchResult[]>([])
const searching = ref(false)
const errorMessage = ref<string | null>(null)
const search = async () => {
if (searching.value) {
return
}
searching.value = true
currentPage.value = 0
try {
searchResultList.value = await searchResources(
{
queryString: queryString.value,
resourceType: resourceType.value
},
accountStore.auth
)
} catch (e) {
console.error(e)
errorMessage.value = (e as Error).message ?? 'Error'
}
try {
searchResultObjects.value = await getSearchResults(
searchResultList.value.slice(0, itemsPerPage),
accountStore.auth
)
} catch (e) {
console.error(e)
errorMessage.value = (e as Error).message ?? 'Error'
}
searching.value = false
}
const getResultsForPage = async (pagenumber: number) => {
searching.value = true
try {
searchResultObjects.value = await getSearchResults(
searchResultList.value.slice(pagenumber * itemsPerPage, (pagenumber + 1) * itemsPerPage),
accountStore.auth
)
} catch (e) {
console.error(e)
errorMessage.value = (e as Error).message ?? 'Error'
}
searching.value = false
}
const breadcrumbItems = computed<OcBreadcrumbItem[]>(() => [
{
label: translateValue(community.value.title),
key: 'community',
type: 'community',
to: { name: 'community', params: { community: community.value.name } }
},
{
label: t('breadcrumb.search'),
key: 'search',
type: 'search'
}
])
</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
import type { Credentials, OcSearchQuery, OcSearchResult } from '@/declarations'
import { executeSparqlConstruct, executeSparqlSelect } from './sparql'
import type { ContextDefinition } from 'jsonld'
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.queryString){
queryFilter = `FILTER regex(?o, "${query.queryString}", "i")`
}
const res = await executeSparqlSelect(
`
SELECT DISTINCT ?resource
WHERE {
?resource rdf:type ?type.
${typeFilter}
FILTER EXISTS {
VALUES ?p { dct:title dct:description }
?resource ?p ?o
${queryFilter}
}
}
`,
{
auth: auth,
})
return res.map((result) => result.resource.value)
}
/** Get searchResults objects from URI list */
export const getSearchResults = async (resourceUriList: string[], auth?: Credentials) => {
const searchResultContext: ContextDefinition = {
...resourceContext,
catalog: {
'@id': 'http://www.w3.org/ns/dcat#catalog',
'@type': '@id',
'@container': '@set'
},
dataset: {
'@id': 'http://www.w3.org/ns/dcat#dataset',
'@type': '@id',
'@container': '@set'
},
distribution: {
'@id': 'http://www.w3.org/ns/dcat#distribution',
'@type': '@id',
'@container': '@set'
},
}
const formattedUris = '<' + resourceUriList.join('> <') + '>'
return await executeSparqlConstruct<OcSearchResult>(
`
CONSTRUCT {
?s ?p ?o.
?s oct:graph ?g.
?s dcat:dataset ?dataset
}
WHERE {
VALUES ?s { ${formattedUris} }
VALUES ?p {
rdf:type
dct:identifier
dct:title
dct:description
dcat:version
dct:creator
dcat:catalog
dcat:dataset
dcat:distribution
}
?s ?p ?o.
GRAPH ?g {
?s dct:identifier ?identifier.
}
}
`,
{
context: searchResultContext,
auth: auth,
})
}
\ No newline at end of file
......@@ -30,6 +30,7 @@ declare module 'vue-router/auto-routes' {
'community.dashboard.dataset': RouteRecordInfo<'community.dashboard.dataset', '/:lang/community/:community/dashboard/dataset', { community: ParamValue<true> }, { community: ParamValue<false> }>,
'community.datasets.new': RouteRecordInfo<'community.datasets.new', '/:lang/community/:community/dataset/new', { community: ParamValue<true> }, { community: ParamValue<false> }>,
'community.join': RouteRecordInfo<'community.join', '/:lang/community/:community/join', { community: ParamValue<true> }, { community: ParamValue<false> }>,
'/:lang/community/[community].search': RouteRecordInfo<'/:lang/community/[community].search', '/:lang/community/:community/search', { community: ParamValue<true> }, { community: ParamValue<false> }>,
'connection': RouteRecordInfo<'connection', '/:lang/connection', Record<never, never>, Record<never, never>>,
'logout': RouteRecordInfo<'logout', '/:lang/logout', Record<never, never>, Record<never, never>>,
'profile': RouteRecordInfo<'profile', '/:lang/profile', Record<never, never>, Record<never, never>>,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment