diff --git a/package-lock.json b/package-lock.json index dfb6cc6789998183cba2caf217d2bc96e73b1e38..01d7a20c1712d729258e6610fc6a25941d73b352 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1690,32 +1690,59 @@ } }, "node_modules/@primevue/core": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.0.5.tgz", - "integrity": "sha512-DUCslDA93eUOVW0A1I3yoZgRLI4zmI2++loZQXbUF5jaXCwKiAza14+iyUU+cWH27VSq+jQnCEP9QJtPZiJJ0w==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.2.1.tgz", + "integrity": "sha512-L81TZSZU8zRznIi2g6IWwlZ5wraaE8DrNUJyxieCRCTpbSF3rSlYmhDEuzal8PfE0RuvXpRsxqedTHxz5cdqPg==", "dependencies": { - "@primeuix/styled": "^0.0.5", - "@primeuix/utils": "^0.0.5" + "@primeuix/styled": "^0.3.0", + "@primeuix/utils": "^0.3.0" }, "engines": { "node": ">=12.11.0" }, "peerDependencies": { - "vue": "^3.0.0" + "vue": "^3.3.0" + } + }, + "node_modules/@primevue/core/node_modules/@primeuix/styled": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.3.0.tgz", + "integrity": "sha512-XsLbmyM1u50A0EDATIHyqm5O/zOCSyNKPk4pNN8HFvEPehbsjf4tkXcRZAyaVvntSCLpV4XGAj7v5EDCQkBRlg==", + "dependencies": { + "@primeuix/utils": "^0.3.0" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/core/node_modules/@primeuix/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-d6ymWez1n+iqwzAVhyOTmrOHl5qnSX2oGlTy97qGuA15gLai+MQaxONHFNdDia8Q7o396v7KK9IvhAx9VET/+A==", + "engines": { + "node": ">=12.11.0" } }, "node_modules/@primevue/icons": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.0.5.tgz", - "integrity": "sha512-ZxR9W1wlAE2fTtUhrHyeMx5t0jNyAgxDcHPm0cNXpX8q1XF95rSM/qb48QKXIBDBrJ/xs57BcyCNADP/VDPY4g==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.2.1.tgz", + "integrity": "sha512-TOhxgkcmgBqmlHlf2x+gs4874iHopkow0gRAC5FztZTgTZQrqy8hPIA9b4O1lW7P6GOjGuVIwSH8y2lw6Q8koA==", "dependencies": { - "@primeuix/utils": "^0.0.5", - "@primevue/core": "4.0.5" + "@primeuix/utils": "^0.3.0", + "@primevue/core": "4.2.1" }, "engines": { "node": ">=12.11.0" } }, + "node_modules/@primevue/icons/node_modules/@primeuix/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-d6ymWez1n+iqwzAVhyOTmrOHl5qnSX2oGlTy97qGuA15gLai+MQaxONHFNdDia8Q7o396v7KK9IvhAx9VET/+A==", + "engines": { + "node": ">=12.11.0" + } + }, "node_modules/@primevue/themes": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.0.5.tgz", @@ -8374,19 +8401,38 @@ } }, "node_modules/primevue": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.0.5.tgz", - "integrity": "sha512-MALszGIZ5SnEQy1XeZLBFhpMXQ1OS7D1U7H+l/JAX5U46RQ1vufo7NAiWbbV5/ADjPGw4uLplqMQxujkksNY2g==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.2.1.tgz", + "integrity": "sha512-cU9ZQVq9fitsQEIrfGeIl7xELBn61JCMxWkzcS9dkr165g29AvUrUNS9ufs1t2NoMJzE8VllwzweF/tSFAr2cw==", "dependencies": { - "@primeuix/styled": "^0.0.5", - "@primeuix/utils": "^0.0.5", - "@primevue/core": "4.0.5", - "@primevue/icons": "4.0.5" + "@primeuix/styled": "^0.3.0", + "@primeuix/utils": "^0.3.0", + "@primevue/core": "4.2.1", + "@primevue/icons": "4.2.1" }, "engines": { "node": ">=12.11.0" } }, + "node_modules/primevue/node_modules/@primeuix/styled": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.3.0.tgz", + "integrity": "sha512-XsLbmyM1u50A0EDATIHyqm5O/zOCSyNKPk4pNN8HFvEPehbsjf4tkXcRZAyaVvntSCLpV4XGAj7v5EDCQkBRlg==", + "dependencies": { + "@primeuix/utils": "^0.3.0" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/primevue/node_modules/@primeuix/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-d6ymWez1n+iqwzAVhyOTmrOHl5qnSX2oGlTy97qGuA15gLai+MQaxONHFNdDia8Q7o396v7KK9IvhAx9VET/+A==", + "engines": { + "node": ">=12.11.0" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", diff --git a/src/assets/communityExample1.ts b/src/assets/communityExample1.ts new file mode 100644 index 0000000000000000000000000000000000000000..73028a07e765b7a8fe4d7487c43aea65fa3564b3 --- /dev/null +++ b/src/assets/communityExample1.ts @@ -0,0 +1,34 @@ +import type { OcCommunity } from "@/declarations"; + +export const communityExample1: OcCommunity = { + "@id": "https://www.irit.fr/opencommon/communities/189088ec-baa9-4397-8c6f-eefde9a3790c", + "@type": [ + "http://www.w3.org/ns/dcat#Catalog", + "http://www.w3.org/2002/07/owl#NamedIndividual", + "https://www.irit.fr/opencommon/system/Space", + "https://www.irit.fr/opencommon/terms/Space", + "https://www.irit.fr/opencommon/terms/Community", + "http://www.w3.org/1999/02/22-rdf-syntax-ns#Bag" + ], + "abstract": { + "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.", + "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." + }, "description": { + "en": "InterDisciplinary Research Facility aggregating 13 human-environments observatories (OHM).", + "fr": "Dispositif de Recherche Interdisciplinaire avec ses 13 Observatoires Hommes-Milieux (OHM)." + }, + "identifier": "189088ec-baa9-4397-8c6f-eefde9a3790c", + "title": { + "fr": "Labex DRIIHM", + "en": "DRIIHM" + }, + "catalog": [ + "https://www.irit.fr/opencommon/resourceTree/MyResources", + "https://opencommon.irit.fr/catalogs/6B8E175A-8C94-11EF-A39A-D6E96336453A", + "https://opencommon.irit.fr/catalogs/AF1A5FAE-8C91-11EF-A39A-D6E96336453A" + ], + "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": "olivine", +} \ No newline at end of file diff --git a/src/components/DashboardDataset/DashboardDataset.stories.ts b/src/components/DashboardDataset/DashboardDataset.stories.ts index 0038574ae60b9339aef0486daed9d7f69ce883dd..f47adee2a2092b2bc035e068520f92d4c55b009a 100644 --- a/src/components/DashboardDataset/DashboardDataset.stories.ts +++ b/src/components/DashboardDataset/DashboardDataset.stories.ts @@ -3,6 +3,7 @@ import type { Meta, StoryObj } from '@storybook/vue3'; import DashboardDataset from './DashboardDataset.vue'; import { datasetSummaries, distributionSummaries } from './exampleSummaries' import type { OcDatasetSummary } from '@/declarations'; +import { communityExample1 } from '@/assets/communityExample1'; const meta: Meta<typeof DashboardDataset> = { component: DashboardDataset, @@ -20,19 +21,7 @@ export const Default: Story = { template: '<DashboardDataset v-bind="args" />', }), args: { - community: { - "@id": "https://www.irit.fr/distributions/7f452387-c7ef-4928-b776-b5c3f9080769", - "@type": ["oc:Space"], - name: 'driihm', - abstract: { "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.", "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." }, - identifier: "189088ec-baa9-4397-8c6f-eefde9a3790c", - title: { - fr: "Communauté du LabEx DRIIHM", - en: "DRIIHM Community" - }, - isSpaceOf: "https://www.irit.fr/opencommon/agents/organization/9a20f121-c64e-4049-93a7-4bedbe819fd6", - color: 'linen' - }, + community: communityExample1, datasets: datasetSummaries as OcDatasetSummary[], distributionsCallback: (_: string[]) => new Promise((resolve) => { setTimeout(() => { diff --git a/src/components/DatasetMultiStep/OcDatasetFormStep2/OcDatasetFormStep2.stories.ts b/src/components/DatasetMultiStep/OcDatasetFormStep2/OcDatasetFormStep2.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..b14698ff806d58620e8bad138e9cc4df9a178d7a --- /dev/null +++ b/src/components/DatasetMultiStep/OcDatasetFormStep2/OcDatasetFormStep2.stories.ts @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from '@storybook/vue3'; + +import OcDatasetFormStep2 from './OcDatasetFormStep2.vue'; +import { communityExample1 } from '@/assets/communityExample1'; + +const meta: Meta<typeof OcDatasetFormStep2> = { + component: OcDatasetFormStep2, +}; + +export default meta; +type Story = StoryObj<typeof OcDatasetFormStep2>; + +export const Default: Story = { + render: (args) => ({ + components: { OcDatasetFormStep2 }, + setup() { + return { args }; + }, + template: '<OcDatasetFormStep2 v-bind="args" />', + }), + args: { + modelValue: {}, + community: communityExample1, + userPrivateGraph: 'private-graph', + auth: { + email: import.meta.env.VITE_VISITOR_USER, + password: import.meta.env.VITE_VISITOR_PWD, + }, + }, +}; diff --git a/src/components/DatasetMultiStep/OcDatasetFormStep2/OcDatasetFormStep2.vue b/src/components/DatasetMultiStep/OcDatasetFormStep2/OcDatasetFormStep2.vue index b2577def83c9f629844012fe9abb8b4c9ccf4e2e..183f9c44e921badbb98777ea1e20f72015ef737d 100644 --- a/src/components/DatasetMultiStep/OcDatasetFormStep2/OcDatasetFormStep2.vue +++ b/src/components/DatasetMultiStep/OcDatasetFormStep2/OcDatasetFormStep2.vue @@ -2,15 +2,19 @@ <Form class="flex flex-col gap-2" :validation-schema="validationSchema" - :initial-values="model" + :initial-values="{ catalog: model.catalog, graph: model.graph?.[0] }" @submit="submit" + v-slot="{ resetField }" > <Field name="catalog" v-slot="{ value, errorMessage, handleChange }"> <OcField :metadata="datasetMetadata.catalog" for="catalog"> - <OcCatalogAutocomplete + <OcCatalogSelect inputId="catalog" :model-value="value" - @update:model-value="handleChange" + :community="community" + :user-private-graph="userPrivateGraph" + :auth="auth" + @update:model-value="updateCatalogValue($event, handleChange, resetField)" :invalid="!!errorMessage" :multiple="false" required @@ -19,6 +23,39 @@ </OcField> <Message v-if="errorMessage" severity="error">{{ errorMessage }}</Message> </Field> + <Field name="visibility" v-slot="{ value, errorMessage, handleChange }"> + <div class="flex gap-1 mb-2 items-center"> + <label for="visibility" class="required"> + {{ t('datasets.new.steps.2.access') }} + </label> + <OcFairBadge :letter="false" size="small" :badges="['f', 'r']" :popover="true" /> + </div> + <Message severity="secondary" icon="fa fa-info" class="text-sm"> + <p class="italic mb-2">{{ t('datasets.new.steps.2.accessHelp.1') }}</p> + <p class="mb-2">{{ t('datasets.new.steps.2.accessHelp.2') }}</p> + <p>{{ t('datasets.new.steps.2.accessHelp.3') }}</p> + <p class="mb-2">{{ t('datasets.new.steps.2.accessHelp.4') }}</p> + <p class="mb-2">{{ t('datasets.new.steps.2.accessHelp.5') }}</p> + <p class="mb-2">{{ t('datasets.new.steps.2.accessHelp.6') }}</p> + </Message> + <SelectButton + inputId="visibility" + :model-value="value" + :options="visibilityOptions" + optionLabel="key" + optionValue="graph" + optionDisabled="disabled" + @update:model-value="handleChange" + :invalid="!!errorMessage" + required + > + <template #option="slotProps"> + <OcVisibilityIcon :visibility="slotProps.option.key" class="text-[0.6rem] -mr-1" /> + {{ t('resourceVisibility.' + slotProps.option.key) }} + </template> + </SelectButton> + <Message v-if="errorMessage" severity="error">{{ errorMessage }}</Message> + </Field> <div class="flex justify-between pt-6"> <Button :label="t('back')" @@ -44,27 +81,82 @@ import { toTypedSchema } from '@vee-validate/yup' import * as yup from 'yup' import Button from 'primevue/button' import { useI18n } from 'vue-i18n' -import type { OcCatalog, OcDataset } from '@/declarations' -import OcCatalogAutocomplete from '@/components/FormInputs/OcCatalogAutocomplete/OcCatalogAutocomplete.vue' +import type { Credentials, OcCatalogSummary, OcCommunity, OcDataset } from '@/declarations' +import OcCatalogSelect from '@/components/FormInputs/OcCatalogSelect/OcCatalogSelect.vue' import Message from 'primevue/message' import { datasetMetadata } from '@/modelMetadata/dataset' import OcField from '@/components/FormInputs/OcField/OcField.vue' +import { computed, ref, type PropType } from 'vue' +import { useTranslateValue } from '@/composables/useTranslateValue' +import { getVisibilityOptionsFor } from '@/helpers/resourceVisibility' +import { SelectButton } from 'primevue' +import OcVisibilityIcon from '@/components/OcVisibilityIcon/OcVisibilityIcon.vue' +import OcFairBadge from '@/components/OcFairBadge/OcFairBadge.vue' const model = defineModel<Partial<OcDataset>>({ required: true }) +const props = defineProps({ + community: { + type: Object as PropType<OcCommunity>, + required: true + }, + userPrivateGraph: { + type: String, + required: true + }, + auth: { + type: Object as PropType<Credentials>, + required: true + } +}) const emit = defineEmits(['back', 'next']) const { t } = useI18n() +const { translateValue } = useTranslateValue() + +const validationSchema = computed(() => + toTypedSchema( + yup.object({ + catalog: yup + .mixed<OcCatalogSummary>() + .required() + .label(translateValue(datasetMetadata.catalog.label)), + visibility: yup + .string() + .matches( + new RegExp( + visibilityOptions.value + .filter((item) => !item.disabled) + .map((item) => item.key) + .join('|') + ), + 'Choosen option is disabled' + ) + .required() + .label(t('datasets.new.steps.2.access')) + }) + ) +) -const validationSchema = toTypedSchema( - yup.object({ - catalog: yup.object<OcCatalog>().required().label(t('datasets.new.steps.2.catalog')) - }) +const visibilityOptions = ref( + getVisibilityOptionsFor(props.community, undefined, props.userPrivateGraph) ) +const updateCatalogValue = ( + value: OcCatalogSummary | undefined, + handleChange: Function, + resetField: Function +) => { + handleChange(value) + visibilityOptions.value = getVisibilityOptionsFor(props.community, value, props.userPrivateGraph) + resetField('visibility', undefined) +} + const submit = (values: GenericObject, ctx: SubmissionContext) => { model.value.catalog = values.catalog + model.value.graph = [values.visibility] + const event = ctx.evt as SubmitEvent const action = ((event?.submitter as HTMLInputElement)?.value ?? 'next') as 'back' | 'next' emit(action) diff --git a/src/components/FormInputs/OcAgentAutocomplete/OcAgentAutocomplete.vue b/src/components/FormInputs/OcAgentAutocomplete/OcAgentAutocomplete.vue index 3678ccfc744fd2da8e0b1cea8fde211cf39406c1..8ecb6ba7947c6eb730dc36450f63613ea5872429 100644 --- a/src/components/FormInputs/OcAgentAutocomplete/OcAgentAutocomplete.vue +++ b/src/components/FormInputs/OcAgentAutocomplete/OcAgentAutocomplete.vue @@ -10,7 +10,8 @@ :minLength="3" @complete="search($event.query)" :loading="loading" - :pt:pcInput:root:class="{ 'w-full': true, 'border-amber-200': !!errorMessage }" + :pt:pcInput:root:class="{ 'border-amber-200': !!errorMessage }" + fluid > <template #option="{ option }"> <div class="flex items-center"> diff --git a/src/components/FormInputs/OcCatalogAutocomplete/OcCatalogAutocomplete.stories.ts b/src/components/FormInputs/OcCatalogAutocomplete/OcCatalogAutocomplete.stories.ts deleted file mode 100644 index 57cdb57637f3c7445654a1c8ce794ad2be972b17..0000000000000000000000000000000000000000 --- a/src/components/FormInputs/OcCatalogAutocomplete/OcCatalogAutocomplete.stories.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3'; - -import OcCatalogAutocomplete from './OcCatalogAutocomplete.vue'; - -const meta: Meta<typeof OcCatalogAutocomplete> = { - component: OcCatalogAutocomplete, -}; - -export default meta; -type Story = StoryObj<typeof OcCatalogAutocomplete>; - -export const OrganizationAndPerson: Story = { - render: (args) => ({ - components: { OcCatalogAutocomplete }, - setup() { - return { args }; - }, - template: '<OcCatalogAutocomplete v-bind="args" />', - }), - args: {}, -}; diff --git a/src/components/FormInputs/OcCatalogAutocomplete/OcCatalogAutocomplete.vue b/src/components/FormInputs/OcCatalogAutocomplete/OcCatalogAutocomplete.vue deleted file mode 100644 index a087a16d6c7b864854cbbcdd40d4a10914b8862c..0000000000000000000000000000000000000000 --- a/src/components/FormInputs/OcCatalogAutocomplete/OcCatalogAutocomplete.vue +++ /dev/null @@ -1,72 +0,0 @@ -<template> - <div class="flex flex-col w-full"> - <AutoComplete - :inputId="$attrs.inputId" - v-model="model" - :suggestions="items" - :optionLabel="displayName" - forceSelection - :minLength="2" - @complete="search($event.query)" - :loading="loading" - :virtualScrollerOptions="{ itemSize: 30 }" - :pt:pcInput:root:class="{ 'w-full': true, 'border-amber-200': !!errorMessage }" - > - <template #option="{ option }"> - <div class="flex items-center"> - <span :class="icon" class="text-xs mr-1" /> - {{ displayName(option) }} - </div> - </template> - </AutoComplete> - <Message class="mt-2" v-if="!!errorMessage" severity="warn"> - {{ t('form.error.autocomplete') }} - </Message> - </div> -</template> - -<script setup lang="ts"> -import { useTranslateValue } from '@/composables/useTranslateValue' -import type { OcCatalog } from '@/declarations' -import { iconsDict } from '@/helpers/icons' -import { queryCatalog } from '@/sparql/catalog' -import AutoComplete from 'primevue/autocomplete' -import Message from 'primevue/message' -import { ref } from 'vue' -import { useI18n } from 'vue-i18n' - -const { t } = useI18n() -const { translateValue } = useTranslateValue() - -defineProps({ - multiple: { - type: Boolean, - required: false, - default: true - } -}) - -const model = defineModel<OcCatalog | undefined>() - -const { locale } = useI18n() -const loading = ref<boolean>(false) -const errorMessage = ref<string | null>(null) -const items = ref<Array<OcCatalog>>([]) - -const icon = iconsDict.catalog -const displayName = (item: OcCatalog) => translateValue(item.title) - -async function search(query: string) { - loading.value = true - errorMessage.value = null - try { - items.value = await queryCatalog(query, locale.value) - } catch (e) { - items.value = [] - errorMessage.value = (e as Error).message - console.error(e) - } - - loading.value = false -} -</script> diff --git a/src/components/FormInputs/OcCatalogSelect/OcCatalogSelect.stories.ts b/src/components/FormInputs/OcCatalogSelect/OcCatalogSelect.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..52c0ca34cc05a79c25fb7e1fbf0fe65faa482bc2 --- /dev/null +++ b/src/components/FormInputs/OcCatalogSelect/OcCatalogSelect.stories.ts @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from '@storybook/vue3'; + +import OcCatalogSelect from './OcCatalogSelect.vue'; +import { communityExample1 } from '@/assets/communityExample1'; + +const meta: Meta<typeof OcCatalogSelect> = { + component: OcCatalogSelect, +}; + +export default meta; +type Story = StoryObj<typeof OcCatalogSelect>; + +export const Default: Story = { + render: (args) => ({ + components: { OcCatalogSelect }, + setup() { + return { args }; + }, + template: '<OcCatalogSelect v-bind="args" />', + }), + args: { + community: communityExample1, + auth: { + email: import.meta.env.VITE_VISITOR_USER, + password: import.meta.env.VITE_VISITOR_PWD, + } + }, +}; diff --git a/src/components/FormInputs/OcCatalogSelect/OcCatalogSelect.vue b/src/components/FormInputs/OcCatalogSelect/OcCatalogSelect.vue new file mode 100644 index 0000000000000000000000000000000000000000..fcb1829d2f4284c601b4157b94142902be4cc37b --- /dev/null +++ b/src/components/FormInputs/OcCatalogSelect/OcCatalogSelect.vue @@ -0,0 +1,187 @@ +<template> + <div class="flex flex-col w-full"> + <TreeSelect + v-model="internalModel" + name="node" + @node-expand="onNodeExpand" + :loading="loading" + :options="nodes" + :loading-mode="loading ? 'mask' : 'icon'" + :pt:pcInput:root:class="{ 'w-full': true, 'border-amber-200': !!errorMessage }" + > + <template #option="{ node }"> + <span class="p-tree-node-icon text-[0.8rem]"> + <i :class="iconsDict.catalog" class="fa-lg fa-fw" /> + <OcVisibilityIcon + v-if="node.visibility !== 'public'" + :visibility="node.visibility" + class="text-[0.6rem] pt-2 -ml-3 mr-0.5" + /> + </span> + {{ node.label }} + </template> + </TreeSelect> + <Message class="mt-2" v-if="!!errorMessage" severity="warn"> + {{ t('form.error.autocomplete') }} + </Message> + </div> +</template> + +<script setup lang="ts"> +import OcVisibilityIcon from '@/components/OcVisibilityIcon/OcVisibilityIcon.vue' +import { useTranslateValue } from '@/composables/useTranslateValue' +import type { Credentials, OcCatalogSummary, OcCommunity } from '@/declarations' +import { iconsDict } from '@/helpers/icons' +import { + getCatalogSummaryFromParentUri, + getCatalogSummaryFromChildUri, + getCatalogSummaryFromUri +} from '@/sparql/catalog' +import Message from 'primevue/message' +import type { TreeNode } from 'primevue/treenode' +import TreeSelect from 'primevue/treeselect' +import { getResourceVisibility } from '@/helpers/resourceVisibility' +import { onMounted, ref, watch, type PropType } from 'vue' +import { useI18n } from 'vue-i18n' +import { homeNodeUri } from '@/stores/tree' + +const { t } = useI18n() +const { translateValue } = useTranslateValue() + +const props = defineProps({ + community: { + type: Object as PropType<OcCommunity>, + required: true + }, + userPrivateGraph: { + type: String, + required: false + }, + auth: { + type: Object as PropType<Credentials | undefined>, + required: false + } +}) + +const model = defineModel<OcCatalogSummary | undefined>() +const internalModel = ref<{ [k: string]: boolean } | undefined>(undefined) + +watch(internalModel, (internalModel) => { + if (internalModel === undefined) { + model.value = undefined + } + const catalogUri = Object.keys(internalModel as { [k: string]: boolean })[0] + model.value = internalModel ? catalogList.value[catalogUri] : undefined +}) + +const errorMessage = ref<string | null>(null) +const loading = ref(true) + +const nodes = ref<TreeNode[]>([]) +const catalogList = ref<Record<string, OcCatalogSummary>>({}) + +onMounted(async () => { + loading.value = true + // Initalize tree with given value + if (model.value !== undefined) { + const givenCatalog = await getCatalogSummaryFromUri(model.value['@id'], props.auth) + + if (givenCatalog) { + // We go from the pre-selected catalog to a catalog in our list. + catalogList.value[givenCatalog['@id']] = givenCatalog + let givenCatalogBranch: TreeNode = { + key: givenCatalog['@id'], + label: translateValue(givenCatalog.title), + leaf: !givenCatalog.catalog?.length, + visibility: getResourceVisibility(props.community, givenCatalog, props.userPrivateGraph) + } + let currentCatalogUri: string | undefined = model.value['@id'] + do { + const catalogs = await getCatalogSummaryFromChildUri(currentCatalogUri, props.auth) + if (catalogs && catalogs.length) { + // We arbitrary take the first catalog + const catalog = catalogs[0] + givenCatalogBranch = { + key: catalog['@id'], + label: translateValue(catalog.title), + leaf: !catalog.catalog?.length, + visibility: getResourceVisibility(props.community, catalog, props.userPrivateGraph), + children: [{ ...givenCatalogBranch }] + } + + catalogList.value[catalog['@id']] = catalog + await onNodeExpand(givenCatalogBranch, true) + currentCatalogUri = catalog['@id'] + } else { + currentCatalogUri = undefined + } + } while (currentCatalogUri) + + nodes.value.push(givenCatalogBranch) + internalModel.value = { [model.value['@id']]: true } + } else { + errorMessage.value = "Given catalog can't be found !" + } + } else { + try { + for (const catalog of await getCatalogSummaryFromParentUri(homeNodeUri, props.auth)) { + if (!Object.keys(catalogList.value).includes(catalog['@id'])) { + const node = { + key: catalog['@id'], + label: translateValue(catalog.title), + leaf: !catalog.catalog?.length, + visibility: getResourceVisibility(props.community, catalog, props.userPrivateGraph) + } + nodes.value.push(node) + catalogList.value[catalog['@id']] = catalog + + if (catalog.catalog?.length) { + onNodeExpand(node) + } + } + } + } catch (e) { + console.error(e) + errorMessage.value = (e as Error).message + } + } + + loading.value = false +}) + +const onNodeExpand = async (node: TreeNode, force: boolean = false) => { + // No child to get + if (node.leaf) { + return + } + // Children already loaded + if (!force && node.children?.length) { + return + } + + node.loading = true + const nodeCatalog = catalogList.value[node.key] + try { + for (const catalog of await getCatalogSummaryFromParentUri(nodeCatalog['@id'], props.auth)) { + const child = { + key: catalog['@id'], + label: translateValue(catalog.title), + leaf: !catalog.catalog?.length, + visibility: getResourceVisibility(props.community, catalog, props.userPrivateGraph) + } + if (node.children) { + if (!node.children.find((child) => child.key === catalog['@id'])) { + node.children.push(child) + } + } else { + node.children = [child] + } + catalogList.value[catalog['@id']] = catalog + } + } catch (e) { + console.error(e) + errorMessage.value = (e as Error).message + } + node.loading = false +} +</script> diff --git a/src/components/FormInputs/OcConceptAutocomplete/OcConceptAutocomplete.vue b/src/components/FormInputs/OcConceptAutocomplete/OcConceptAutocomplete.vue index 82e9a6b9d811a50675cb8ff2d7735714ff7ab8ac..e3e13189cc1287581366322afb0fe3f6b2c8b971 100644 --- a/src/components/FormInputs/OcConceptAutocomplete/OcConceptAutocomplete.vue +++ b/src/components/FormInputs/OcConceptAutocomplete/OcConceptAutocomplete.vue @@ -12,7 +12,8 @@ :minLength="1" @complete="search($event.query)" :loading="loading" - :pt:pcInput:root:class="{ 'w-full': true, 'border-amber-200': !!errorMessage }" + :pt:pcInput:root:class="{ 'border-amber-200': !!errorMessage }" + fluid class="w-full" /> <Button @@ -105,7 +106,7 @@ async function search(query: string) { } watch(model, () => { - if(!props.multiple && isBrowserVisible.value){ + if (!props.multiple && isBrowserVisible.value) { isBrowserVisible.value = false } }) diff --git a/src/components/FormInputs/OcMultilingualField/OcMultilingualField.vue b/src/components/FormInputs/OcMultilingualField/OcMultilingualField.vue index 7317a87e04d2bbfcdd23b1c6bc8ff7f606ffb26f..ed9484604f8a86ed5601b7bbe5b31ebe7c372d76 100644 --- a/src/components/FormInputs/OcMultilingualField/OcMultilingualField.vue +++ b/src/components/FormInputs/OcMultilingualField/OcMultilingualField.vue @@ -14,7 +14,7 @@ <div class="flex flex-col gap-1 w-full"> <InputGroup class="w-56"> <InputGroupAddon> - <i class="fa-solid fa-globe"></i> + <i class="fa-solid fa-globe mx-1"></i> </InputGroupAddon> <Select :modelValue="values[key].locale" diff --git a/src/components/OcCardCommunity/OcCardCommunity.stories.ts b/src/components/OcCardCommunity/OcCardCommunity.stories.ts index ac76cbd1ca68eafa559c18ece15eaaf23a78be48..f0dd966cc02ffd2a43cdddb8bd3d86257e00eaae 100644 --- a/src/components/OcCardCommunity/OcCardCommunity.stories.ts +++ b/src/components/OcCardCommunity/OcCardCommunity.stories.ts @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from '@storybook/vue3'; import OcCardCommunity from './OcCardCommunity.vue'; +import { communityExample1 } from '@/assets/communityExample1'; const meta: Meta<typeof OcCardCommunity> = { component: OcCardCommunity, @@ -18,20 +19,6 @@ export const Primary: Story = { template: '<OcCardCommunity v-bind="args" />', }), args: { - 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' - } + community: communityExample1 }, }; \ No newline at end of file diff --git a/src/components/OcCatalogForm/OcCatalogForm.stories.ts b/src/components/OcCatalogForm/OcCatalogForm.stories.ts index ac76118d9e754628437f69221a0c00f26f683d6c..f6b585d371c0bfc23d5d2abcb1de67c198bb0a74 100644 --- a/src/components/OcCatalogForm/OcCatalogForm.stories.ts +++ b/src/components/OcCatalogForm/OcCatalogForm.stories.ts @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from '@storybook/vue3'; import OcCatalogForm from './OcCatalogForm.vue'; +import { communityExample1 } from '@/assets/communityExample1'; const meta: Meta<typeof OcCatalogForm> = { component: OcCatalogForm, @@ -19,6 +20,12 @@ export const Default: Story = { }), args: { modelValue: {}, + community: communityExample1, + userPrivateGraph: 'private-graph', + auth: { + email: import.meta.env.VITE_VISITOR_USER, + password: import.meta.env.VITE_VISITOR_PWD, + } }, }; @@ -32,6 +39,12 @@ export const CustomLabels: Story = { }), args: { modelValue: {}, + community: communityExample1, + userPrivateGraph: 'private-graph', + auth: { + email: import.meta.env.VITE_VISITOR_USER, + password: import.meta.env.VITE_VISITOR_PWD, + }, saveLabel: 'Next', cancelLabel: 'Exit', }, @@ -47,6 +60,12 @@ export const WithoutCancel: Story = { }), args: { modelValue: {}, + community: communityExample1, + userPrivateGraph: 'private-graph', + auth: { + email: import.meta.env.VITE_VISITOR_USER, + password: import.meta.env.VITE_VISITOR_PWD, + }, cancelButton: false, }, }; diff --git a/src/components/OcCatalogForm/OcCatalogForm.vue b/src/components/OcCatalogForm/OcCatalogForm.vue index b90021523f61fc0b1ebca5fe752f368b3290ad0d..6528b670fa93d03f5b936236204da3ea023dc3ce 100644 --- a/src/components/OcCatalogForm/OcCatalogForm.vue +++ b/src/components/OcCatalogForm/OcCatalogForm.vue @@ -7,10 +7,12 @@ > <Field name="parentCatalog" v-slot="{ value, errorMessage, handleChange }"> <OcField for="parentCatalog" :metadata="catalogMetadata.parentCatalog"> - <OcCatalogAutocomplete + <OcCatalogSelect inputId="parentCatalog" :model-value="value" @update:model-value="handleChange" + :community="community" + :auth="auth" :invalid="!!errorMessage" :multiple="false" required @@ -71,7 +73,7 @@ </OcField> <Message v-if="errorMessage" severity="error">{{ errorMessage }}</Message> </Field> - <Field name="issued" v-slot="{ value, errorMessage, handleChange }" > + <Field name="issued" v-slot="{ value, errorMessage, handleChange }"> <OcField for="issued" :metadata="catalogMetadata.issued"> <DatePicker id="issued" @@ -84,7 +86,7 @@ </OcField> <Message v-if="errorMessage" severity="error">{{ errorMessage }}</Message> </Field> - <Field name="theme" v-slot="{ value, handleChange, errorMessage }" > + <Field name="theme" v-slot="{ value, handleChange, errorMessage }"> <OcField for="theme" :metadata="catalogMetadata.theme"> <OcConceptAutocomplete inputId="theme" @@ -99,7 +101,7 @@ </OcField> <Message v-if="errorMessage" severity="error">{{ errorMessage }}</Message> </Field> - <Field name="temporal" v-slot="{ value, errorMessage, handleChange }" > + <Field name="temporal" v-slot="{ value, errorMessage, handleChange }"> <OcField for="temporal" :metadata="catalogMetadata.temporal"> <OcMultipleField id="temporal" @@ -117,7 +119,7 @@ </OcField> <Message v-if="errorMessage" severity="error">{{ errorMessage }}</Message> </Field> - <Field name="spatial" v-slot="{ value, errorMessage, handleChange }" > + <Field name="spatial" v-slot="{ value, errorMessage, handleChange }"> <OcField for="spatial" :metadata="catalogMetadata.spatial"> <OcConceptAutocomplete inputId="spatial" @@ -162,7 +164,10 @@ import type { OcPerson, OcConcept, LocalizedProperty, - OcCatalog + OcCatalog, + OcCommunity, + Credentials, + OcCatalogSummary } from '@/declarations' import Button from 'primevue/button' import { useI18n } from 'vue-i18n' @@ -173,17 +178,30 @@ import Message from 'primevue/message' import OcField from '@/components/FormInputs/OcField/OcField.vue' import OcAgentAutocomplete from '@/components/FormInputs/OcAgentAutocomplete/OcAgentAutocomplete.vue' import OcConceptAutocomplete from '@/components/FormInputs/OcConceptAutocomplete/OcConceptAutocomplete.vue' -import OcCatalogAutocomplete from '../FormInputs/OcCatalogAutocomplete/OcCatalogAutocomplete.vue' +import OcCatalogSelect from '../FormInputs/OcCatalogSelect/OcCatalogSelect.vue' import { useTranslateValue } from '@/composables/useTranslateValue' import { catalogMetadata } from '@/modelMetadata/catalog' import { toTypedSchema } from '@vee-validate/yup' import OcMultilingualField from '../FormInputs/OcMultilingualField/OcMultilingualField.vue' import OcMultipleField from '../FormInputs/OcMultipleField/OcMultipleField.vue' +import type { PropType } from 'vue' const { t } = useI18n() const { translateValue } = useTranslateValue() defineProps({ + community: { + type: Object as PropType<OcCommunity>, + required: true + }, + userPrivateGraph: { + type: String, + required: false + }, + auth: { + type: Object as PropType<Credentials | undefined>, + required: false + }, saveLabel: { type: [String, null], default: null @@ -208,7 +226,7 @@ const emit = defineEmits<{ const validationSchema = toTypedSchema( yup.object({ parentCatalog: yup - .object<OcCatalog>() + .object<OcCatalogSummary>() .required() .label(translateValue(catalogMetadata.parentCatalog.label)), title: yup diff --git a/src/declarations.ts b/src/declarations.ts index 1a54d6b0e146df711fffdb0d4dd89f0b9c413c12..ec5bd930e6fa65d239b0b4128e4b3fcd2f99e2fc 100644 --- a/src/declarations.ts +++ b/src/declarations.ts @@ -1,5 +1,12 @@ import type { ContextDefinition } from 'jsonld' import type { RouteLocationAsRelativeGeneric } from 'vue-router' +import { + roleVisitor, + roleCommunityMember, + roleCommunityManager, + rolePlatformManager, + roleCatalogManager +} from '@/helpers/roles' export type Credentials = { email: string @@ -25,6 +32,7 @@ export type OcCommunity = { logo: undefined | string color: undefined | string isSpaceOf: undefined | string + catalog: string[] } export type OcAdmsIdentifier = { @@ -51,10 +59,12 @@ export type OcCatalog = OcResource & { issued?: Date temporal?: [Date, Date][] spatial?: Array<OcConcept | OcGeometry> - parentCatalog?: OcCatalog + parentCatalog?: OcCatalog | OcCatalogSummary } -export type OcCatalogSummary = OcResource +export type OcCatalogSummary = OcResource & { + catalog?: string[] +} export type OcDataset = OcResource & { otherIdentifier?: OcAdmsIdentifier[] @@ -147,7 +157,7 @@ export type OcConceptScheme = { export type OcGeometry = { '@id'?: string, - geometry: Array<{'@type': string, '@value': string}> + geometry: Array<{ '@type': string, '@value': string }> } export type OcOrganization = { @@ -181,7 +191,7 @@ export type OcMemberInfos = { } export type OcMembership = { - role: string + role: OcRole organization: string } @@ -231,3 +241,5 @@ export type OcLocale = { prefLabel: LocalizedProperty<string> code: string } + +export type OcRole = typeof roleVisitor | typeof roleCommunityMember | typeof roleCommunityManager | typeof rolePlatformManager | typeof roleCatalogManager \ No newline at end of file diff --git a/src/helpers/resourceVisibility.ts b/src/helpers/resourceVisibility.ts index 75ba81fbce2ec421065ee363466848c338098bee..b0ff3cd980cea44a642e6c858d6b8c63607cc508 100644 --- a/src/helpers/resourceVisibility.ts +++ b/src/helpers/resourceVisibility.ts @@ -1,5 +1,13 @@ import type { OcCommunity, OcResource, Visibility } from "@/declarations"; +function getCommunityPublicGraph(community?: OcCommunity, legacy: boolean = false) { + return `https://www.irit.fr/opencommon/${legacy ? 'spaces' : 'communities'}/${community?.identifier}/publicResources` +} + +function getCommunityGraph(community?: OcCommunity, legacy: boolean = false) { + return `https://www.irit.fr/opencommon/${legacy ? 'spaces' : 'communities'}/${community?.identifier}/communityResources` +} + export function getResourceVisibility( community?: OcCommunity, resource?: OcResource, @@ -8,13 +16,13 @@ export function getResourceVisibility( const graph = resource?.graph ?? [] if ( - graph.includes(`https://www.irit.fr/opencommon/communities/${community?.identifier}/publicResources`) - || graph.includes(`https://www.irit.fr/opencommon/spaces/${community?.identifier}/publicResources`) + graph.includes(getCommunityPublicGraph(community)) + || graph.includes(getCommunityPublicGraph(community, true)) ) { return 'public' } else if ( - graph.includes(`https://www.irit.fr/opencommon/communities/${community?.identifier}/communityResources`) - || graph.includes(`https://www.irit.fr/opencommon/spaces/${community?.identifier}/communityResources`) + graph.includes(getCommunityGraph(community)) + || graph.includes(getCommunityGraph(community, true)) ) { return 'community' } else if (userPrivateGraph && graph.includes(userPrivateGraph)) { @@ -24,3 +32,35 @@ export function getResourceVisibility( return 'protected' } } + +export function getVisibilityOptionsFor(community?: OcCommunity, resource?: OcResource, userPrivateGraph?: string) { + const options = { + public: { key: 'public', graph: getCommunityPublicGraph(community), disabled: false }, + community: { key: 'community', graph: getCommunityGraph(community), disabled: false }, + private: { key: 'private', graph: userPrivateGraph, disabled: false }, + } + + if (resource === undefined) { + options.public.disabled = true + options.community.disabled = true + options.private.disabled = true + + return Object.values(options) + } + + switch (getResourceVisibility(community, resource, userPrivateGraph)) { + case 'public': + // Nothing to do, all options are available + break + case 'community': + case 'protected': + options.public.disabled = true + break + case 'private': + options.public.disabled = true + options.community.disabled = true + break + } + + return Object.values(options) +} diff --git a/src/helpers/roles.ts b/src/helpers/roles.ts new file mode 100644 index 0000000000000000000000000000000000000000..2ae5a68486f658e9914ef15aca0ef80a83ca1c45 --- /dev/null +++ b/src/helpers/roles.ts @@ -0,0 +1,13 @@ +export const roleVisitor = 'https://www.irit.fr/opencommon/terms/Visitor' + +export const roleCommunityMember = 'https://www.irit.fr/opencommon/terms/communityMember' +/** @deprecated use roleCommunityMember instead */ +export const roleSpaceMember = 'https://www.irit.fr/opencommon/terms/SpaceMember' + +export const roleCommunityManager = 'https://www.irit.fr/opencommon/terms/communityManager' +/** @deprecated use roleCommunityManager instead */ +export const roleSpaceManager = 'https://www.irit.fr/opencommon/terms/SpaceManager' + +export const rolePlatformManager = 'https://www.irit.fr/opencommon/terms/PlatformManager' + +export const roleCatalogManager = 'https://www.irit.fr/opencommon/terms/CatalogManager' \ No newline at end of file diff --git a/src/locales/en.ts b/src/locales/en.ts index 8e03bde9f15286cf2b379c37e4585e7563aeb99d..4e2e40d5bb77a5b0711bf5063fe007dd4f23539d 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -172,10 +172,18 @@ export default { 2: { label: 'Select a catalogue and manage metadata access', description: { - 1: "Select a location to store the dataset's metadata..", + 1: "Select a location to store the dataset's metadata.", 2: 'Select the level of metadata access in the chosen catalogue.' }, - catalog: 'Catalog' + access: 'Access rights to the resource', + accessHelp: { + 1: "Access rights determine who can view the resource.", + 2: "Depending on your role within the community and the catalog in which you wish to insert the resource, the available options may vary.", + 3: "If you want your resource to be public, but the option is not available, it's because you don't have sufficient rights to do so.", + 4: "In this case, create your resource as a community resource. Once created, you'll be able to ask a moderator to publish it.", + 5: "A community resource is only visible to members of the community.", + 6: "You can choose to keep your resource private, in which case it will only be visible to you. It will then be possible to modify this access from the dashboard." + } }, 3: { label: 'Add Distribution(s)', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index ff16956381d025f25e0a9449ded7293be1915840..4788c5c1adf09ccd6b6bd136cb63630c6adc42ca 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -176,7 +176,15 @@ export default { 1: 'Sélectionner un endroit où stocker les métadonnées du jeu de données.', 2: "Sélectionner le niveau d'accès aux métadonnées dans le catalogue choisi." }, - catalog: 'Catalogue' + access: 'Droit d\'accès de la ressource', + accessHelp: { + 1: "Les droits d'accès déterminent qui aura le droit de voir la ressource.", + 2: "En fonction de votre rôle au sein de la communauté et du catalogue dans lequel vous souhaitez insérer la ressource, les options disponibles peuvent variées.", + 3: "Si vous souhaitez qui votre resource soit publique mais que l'option n'est pas disponible, c'est que vous n'avez pas les droits suffisants pour le faire.", + 4: "Dans ce cas, créer votre en tant que resource communautaire, une fois votre resource créée, vous aurez la possibilité de demander sa publication à un modérateur.", + 5: "Une resource communautaire n'est visible que des membre de la communauté.", + 6: "Vous pouvez choisir de garder votre ressource privée, dans ce cas, elle ne sera visible que par vous. Il sera ensuite possible de modifier accès à partir du tableau de bord." + } }, 3: { label: 'Ajouter une ou des distributions', diff --git a/src/pages/community/[community].catalog/[[identifier]].new.vue b/src/pages/community/[community].catalog/[[identifier]].new.vue index 3705cb8b57e22bc0271fae1f17eaac3a8025113b..bcb39ae57dd1a688caae6769720a49bb812565a1 100644 --- a/src/pages/community/[community].catalog/[[identifier]].new.vue +++ b/src/pages/community/[community].catalog/[[identifier]].new.vue @@ -11,6 +11,9 @@ </h1> <OcCatalogForm :modelValue="catalog" + :community="community" + :user-private-graph="accountStore.infos?.hasPrivateGraph" + :auth="accountStore.auth" @save="send" @cancel="router.back()" /> @@ -95,7 +98,7 @@ const catalog = ref<OcCatalog>({ '@type': [''], title: {}, description: {}, - parentCatalog: parentCatalog.value, + parentCatalog: parentCatalog.value }) const sending = ref(false) @@ -133,7 +136,6 @@ const send = async () => { accountStore.auth ) - sent.value = true } catch (e) { console.error(e) @@ -157,4 +159,4 @@ const breadcrumbItems = computed<OcBreadcrumbItem[]>(() => [ type: 'new' } ]) -</script> \ No newline at end of file +</script> diff --git a/src/pages/community/[community].dataset/new.vue b/src/pages/community/[community].dataset/new.vue index 1e4374cd78f24d8c75416c76200f92ae1385dd01..aa6e83d85358ee128c66dcb13c67092d08894edc 100644 --- a/src/pages/community/[community].dataset/new.vue +++ b/src/pages/community/[community].dataset/new.vue @@ -219,7 +219,11 @@ const steps = computed(() => { label: 'datasets.new.steps.2.label', description: ['datasets.new.steps.2.description.1', 'datasets.new.steps.2.description.2'], component: OcDatasetFormStep2, - props: {}, + props: { + community: community.value, + userPrivateGraph: accountStore.infos?.hasPrivateGraph, + auth: accountStore.auth + }, back: (activateCallback: Function) => activateCallback('1'), next: (activateCallback: Function) => activateCallback('3') }, diff --git a/src/pages/community/[community]/dataset.[identifier].edit.vue b/src/pages/community/[community]/dataset.[identifier].edit.vue index 5545c4c3488d263405dcb4ecd755aac0d93ae0c6..44860552df5c532d6e89013656a141addde5ae24 100644 --- a/src/pages/community/[community]/dataset.[identifier].edit.vue +++ b/src/pages/community/[community]/dataset.[identifier].edit.vue @@ -124,13 +124,7 @@ const save = async () => { try { // Update dataset - dataset.value = await updateDataset( - dataset.value, - // @todo How to get dataset actual graph? - accountStore.infos.hasPrivateGraph, - accountStore.profile, - accountStore.auth - ) + dataset.value = await updateDataset(dataset.value, accountStore.profile, accountStore.auth) saved.value = true } catch (e) { diff --git a/src/sparql/catalog.ts b/src/sparql/catalog.ts index 2f97db5c4213f8eff51ae871cb87152eac2a98c6..0f1ae00616a6794e05b7317e9019205e84178e2b 100644 --- a/src/sparql/catalog.ts +++ b/src/sparql/catalog.ts @@ -27,8 +27,8 @@ export const queryCatalog = async (query: string, locale: string, auth?: Credent ) } -export const getCatalogSummary = async (identifier:string, auth?: Credentials): Promise<OcCatalogSummary | null> => { - const result = await executeSparqlConstruct<OcCatalog>( +export const getCatalogSummary = async (identifier: string, auth?: Credentials) => { + const result = await executeSparqlConstruct<OcCatalogSummary>( ` CONSTRUCT { ?catalog dct:title ?title; @@ -41,6 +41,7 @@ export const getCatalogSummary = async (identifier:string, auth?: Credentials): rdf:type dct:title dct:description + dcat:catalog } ?catalog a dcat:Catalog; ?p ?o. @@ -52,17 +53,120 @@ export const getCatalogSummary = async (identifier:string, auth?: Credentials): `, { auth: auth, - context: resourceContext + context: { + ...resourceContext, + catalog: { + "@id": "http://www.w3.org/ns/dcat#catalog", + "@type": "@id", + "@container": "@set" + } + } + } + ) + if (result.length) { + return result[0] + } else { + console.warn('Catalog with identifier "' + identifier + '" not found') + return null + } +} + +export const getCatalogSummaryFromUri = async (uri: string, auth?: Credentials) => { + const result = await executeSparqlConstruct<OcCatalogSummary>( + ` + CONSTRUCT { + <${uri}> ?p ?o; + oct:graph ?g. + } + WHERE { + <${uri}> a dcat:Catalog; + ?p ?o. + GRAPH ?g { + <${uri}> dct:identifier ?identifier + } + } + `, + { + auth: auth, + context: { + ...resourceContext, + catalog: { + "@id": "http://www.w3.org/ns/dcat#catalog", + "@type": "@id", + "@container": "@set" + } + } } ) - if (result.length){ + + if (result.length) { return result[0] } else { - console.warn('Catalog with identifier "'+identifier+'" not found') + console.warn('Catalog with uri "' + uri + '" not found') return null } } +export const getCatalogSummaryFromParentUri = async (uri: string, auth?: Credentials) => { + return await executeSparqlConstruct<OcCatalogSummary>( + ` + CONSTRUCT { + ?node ?p ?o; + oct:graph ?g. + } + WHERE { + <${uri}> a dcat:Catalog; + dcat:catalog ?node. + ?node ?p ?o. + GRAPH ?g { + ?node dct:identifier ?identifier + } + } + `, + { + auth: auth, + context: { + ...resourceContext, + catalog: { + "@id": "http://www.w3.org/ns/dcat#catalog", + "@type": "@id", + "@container": "@set" + } + } + } + ) +} + +export const getCatalogSummaryFromChildUri = async (uri: string, auth?: Credentials) => { + return await executeSparqlConstruct<OcCatalogSummary>( + ` + CONSTRUCT { + ?node ?p ?o; + oct:graph ?g. + } + WHERE { + ?node a dcat:Catalog; + dcat:catalog <${uri}>. + ?node ?p ?o. + GRAPH ?g { + ?node dct:identifier ?identifier + } + } + `, + { + auth: auth, + context: { + ...resourceContext, + catalog: { + "@id": "http://www.w3.org/ns/dcat#catalog", + "@type": "@id", + "@container": "@set" + } + } + } + ) +} + export async function insertCatalog( catalog: OcCatalog, graph: string, diff --git a/src/sparql/communities.ts b/src/sparql/communities.ts index f049af2c14f07a15c29dd6a6e43cc53a172a3880..a2185979b701aa762d4e9ba699d0a9281942a374 100644 --- a/src/sparql/communities.ts +++ b/src/sparql/communities.ts @@ -70,7 +70,3 @@ export function insertCommunityAccessRequest(communityUri: string, userLoginID: { auth: auth } ) } - -export function buildCommunityGraphUri(community: OcCommunity): string { - return `https://www.irit.fr/opencommon/spaces/${community.identifier}` -} diff --git a/src/sparql/datasets.ts b/src/sparql/datasets.ts index 2b2ef0af00d340a861b40838a78366993d04c81f..fecf89f3874340b47d688ffb9d10814c49c4c7c6 100644 --- a/src/sparql/datasets.ts +++ b/src/sparql/datasets.ts @@ -1,14 +1,13 @@ -import { - type OcResource, - type Credentials, - type OcCatalog, - type OcDataset, - type OcPerson, - type OcConcept, - type OcOrganization, - type OcDatasetSummary, - type OcAdmsIdentifier, - type OcGeometry +import type { + OcResource, + Credentials, + OcDataset, + OcPerson, + OcConcept, + OcOrganization, + OcDatasetSummary, + OcAdmsIdentifier, + OcGeometry } from '@/declarations' import { defaultLocalizedPropFormatter, @@ -24,17 +23,22 @@ import { resourceContext } from './resource' export async function insertDataset( dataset: OcDataset, - graph: string, - catalog: OcCatalog, profile: OcPerson, auth?: Credentials ) { dataset.identifier ||= crypto.randomUUID() dataset['@id'] ||= `${import.meta.env.VITE_OC_URI_BASE_URL}/datasets/${dataset.identifier}` + if (!dataset.graph) { + throw Error('Dataset has no graph!') + } + if (!dataset.catalog) { + throw Error('Dataset has no catalog!') + } + let insertQuery = ` - INSERT INTO <${graph}> { - <${catalog['@id']}> dcat:dataset <${dataset["@id"]}>. + INSERT INTO <${dataset.graph[0]}> { + <${dataset.catalog['@id']}> dcat:dataset <${dataset["@id"]}>. ` insertQuery += buildDatasetTriples(dataset, profile) insertQuery += `}` @@ -46,7 +50,6 @@ export async function insertDataset( export async function updateDataset( dataset: OcDataset, - graph: string, profile: OcPerson, auth?: Credentials ) { @@ -56,9 +59,14 @@ export async function updateDataset( // form, then we insert new values. // Note that we keep `prov:qualifiedAttribution` // values. + + if (!dataset.graph) { + throw Error('Dataset has no graph!') + } + await executeSparqlUpdate( ` - WITH <${graph}> + WITH <${dataset.graph[0]}> DELETE { <${dataset['@id']}> ?p ?o. ?temporal ?p2 ?o2. @@ -94,7 +102,7 @@ export async function updateDataset( <${dataset['@id']}> ?p ?o. }; - WITH <${graph}> + WITH <${dataset.graph[0]}> INSERT { ${buildDatasetTriples(dataset, profile)} }; ` , { auth } diff --git a/src/stores/account.ts b/src/stores/account.ts index 61a4371663186cd1974be6ab3b57b8766035ce72..8487808b9342d37af110ed73f28c232c2419a4b4 100644 --- a/src/stores/account.ts +++ b/src/stores/account.ts @@ -1,4 +1,4 @@ -import type { Credentials, OcMemberInfos, OcMembership, OcPerson } from '@/declarations' +import type { Credentials, OcMemberInfos, OcMembership, OcPerson, OcRole } from '@/declarations' import { defineStore } from 'pinia' import { computed, ref, watch } from 'vue' @@ -8,7 +8,8 @@ interface AccountState { profile: OcPerson | null memberships: OcMembership[] | null isAuthenticated: boolean - memberCommunitiesList: string[] + memberCommunitiesList: string[], + roleByOrganization: { [organization: string]: OcRole } } export const useAccountStore = defineStore<string, AccountState>('account', () => { @@ -48,12 +49,27 @@ export const useAccountStore = defineStore<string, AccountState>('account', () = } }) + const roleByOrganization = computed(() => { + const list: { [organization: string]: OcRole } = {} + + if (!Array.isArray(memberships.value)) { + return [] + } + + memberships.value.forEach((membership: OcMembership) => { + list[membership.organization] = membership.role + }) + + return list + }) + return { auth, infos, profile, memberships, isAuthenticated, - memberCommunitiesList + memberCommunitiesList, + roleByOrganization } }) diff --git a/src/stores/tree.ts b/src/stores/tree.ts index fde90529ac57893dc9d84d27be31f33c9a617ea7..fd82c90ddf82003f9239d9282bb3eb076e57edc9 100644 --- a/src/stores/tree.ts +++ b/src/stores/tree.ts @@ -109,7 +109,7 @@ export const useTreeStore = defineStore('tree', () => { account.auth?.value ) - if (result.length){ + if (result.length) { const ordered = reorderNodesFromHomeToResource(result, homeNodeUri, resourceId) await ordered.reduce(async (promiseChain, item) => { @@ -198,7 +198,7 @@ export const useTreeStore = defineStore('tree', () => { return [] } return path.map((node) => ocTreeNode2BreadcrumbItem(node)) - } else {// Si il n'y a pas de noeud selectionné, le breadcrum est vide + } else {// Si il n'y a pas de noeud selectionné, le breadcrumb est vide return [] } })