diff --git a/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/[datasetId]/edit/page.tsx b/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/[datasetId]/edit/page.tsx index 095c911ef..a9b766dd8 100644 --- a/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/[datasetId]/edit/page.tsx +++ b/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/[datasetId]/edit/page.tsx @@ -3,9 +3,10 @@ import { getDatasetById } from '../../../../../actions/actions'; import { DatasetForm } from '../../../../../../components/dataset-form'; import { Params } from 'next/dist/shared/lib/router/utils/route-matcher'; -import { getTranslateText, localization } from '@catalog-frontend/utils'; +import { getTranslateText, getValidSession, localization } from '@catalog-frontend/utils'; import { Organization } from '@catalog-frontend/types'; import { + getAllDatasetSeries, getDatasetTypes, getDataThemes, getFrequencies, @@ -21,6 +22,9 @@ export default async function EditDatasetPage({ params }: Params) { const referenceDataEnv = process.env.FDK_BASE_URI ?? ''; const dataset = await getDatasetById(catalogId, datasetId); const organization: Organization = await getOrganization(catalogId).then((res) => res.json()); + const session = await getValidSession(); + const accessToken = session?.accessToken; + const datasetSeries = await getAllDatasetSeries(catalogId, accessToken).then((res) => res.json()); const [ losThemesResponse, @@ -79,6 +83,7 @@ export default async function EditDatasetPage({ params }: Params) { searchEnv={searchEnv} referenceDataEnv={referenceDataEnv} referenceData={referenceData} + datasetSeries={datasetSeries._embedded.datasets} > diff --git a/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/new/page.tsx b/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/new/page.tsx index 30bcba123..2f9b243fc 100644 --- a/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/new/page.tsx +++ b/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/new/page.tsx @@ -2,9 +2,10 @@ import { Breadcrumbs, BreadcrumbType, PageBanner } from '@catalog-frontend/ui'; import { DatasetForm } from '../../../../../components/dataset-form'; import { datasetToBeCreatedTemplate } from '../../../../../components/dataset-form/utils/dataset-initial-values'; import { Params } from 'next/dist/shared/lib/router/utils/route-matcher'; -import { getTranslateText, localization } from '@catalog-frontend/utils'; +import { getTranslateText, getValidSession, localization } from '@catalog-frontend/utils'; import { Organization } from '@catalog-frontend/types'; import { + getAllDatasetSeries, getDatasetTypes, getDataThemes, getFrequencies, @@ -20,6 +21,9 @@ export default async function NewDatasetPage({ params }: Params) { const organization: Organization = await getOrganization(catalogId).then((res) => res.json()); const searchEnv = process.env.FDK_SEARCH_SERVICE_BASE_URI ?? ''; const referenceDataEnv = process.env.FDK_BASE_URI ?? ''; + const session = await getValidSession(); + const accessToken = session?.accessToken; + const datasetSeries = await getAllDatasetSeries(catalogId, accessToken).then((res) => res.json()); const [ losThemesResponse, @@ -74,6 +78,7 @@ export default async function NewDatasetPage({ params }: Params) { referenceData={referenceData} searchEnv={searchEnv} referenceDataEnv={referenceDataEnv} + datasetSeries={datasetSeries._embedded.datasets} > diff --git a/apps/dataset-catalog/components/dataset-form/components/dataset-form-relations-section.tsx b/apps/dataset-catalog/components/dataset-form/components/dataset-form-relations-section.tsx new file mode 100644 index 000000000..03b2ba1e1 --- /dev/null +++ b/apps/dataset-catalog/components/dataset-form/components/dataset-form-relations-section.tsx @@ -0,0 +1,181 @@ +import { useState } from 'react'; +import { Dataset, DatasetSeries } from '@catalog-frontend/types'; +import { AddButton, DeleteButton, FormContainer } from '@catalog-frontend/ui'; +import { getTranslateText, localization } from '@catalog-frontend/utils'; +import { Heading, Combobox, Textfield } from '@digdir/designsystemet-react'; +import { Field, FieldArray, useFormikContext } from 'formik'; +import relations from '../utils/relations.json'; +import { useSearchDatasetsByUri, useSearchDatasetSuggestions } from '../../../hooks/useSearchService'; + +type TitleSectionProps = { + searchEnv: string; + datasetSeries: DatasetSeries[]; +}; + +export const RelationsSection = ({ searchEnv, datasetSeries }: TitleSectionProps) => { + const { setFieldValue, values, errors } = useFormikContext(); + const [searchQuery, setSearchQuery] = useState(''); + + const getUriList = () => { + return values.references?.map((reference) => reference.source.uri).filter((uri) => uri) ?? []; + }; + + const { data: searchHits, isLoading: searching } = useSearchDatasetSuggestions(searchEnv, searchQuery); + const { data: selectedValues, isLoading } = useSearchDatasetsByUri(searchEnv, getUriList()); + + const comboboxOptions = [ + ...new Map( + [ + ...(searchHits ?? []), + ...(selectedValues ?? []), + ...(getUriList() ?? []).map((uri) => { + const foundItem = + searchHits?.find((item) => item.uri === uri) || selectedValues?.find((item) => item.uri === uri); + + return { + uri, + title: foundItem?.title ?? null, + }; + }), + ].map((option) => [option.uri, option]), + ).values(), + ]; + + return ( +
+ + {localization.datasetForm.heading.relations} + + + + + + {({ remove, push }) => ( +
+ {values.references?.map((_, index) => ( +
+ + setFieldValue(`references[${index}].referenceType.code`, value.toString()) + } + value={ + values.references?.[index]?.referenceType?.code + ? [values.references?.[index]?.referenceType?.code] + : [] + } + placeholder={`${localization.datasetForm.fieldLabel.choseRelation}...`} + > + {localization.search.noHits} + {relations.map((relation) => ( + + {getTranslateText(relation?.label)} + + ))} + + + {!isLoading && ( + setSearchQuery(input.target.value)} + onValueChange={(value) => { + setFieldValue(`references.${[index]}.source.uri`, value.toString()); + }} + loading={searching} + value={values.references?.[index]?.source?.uri ? [values.references?.[index]?.source?.uri] : []} + placeholder={`${localization.search.search}...`} + > + {localization.search.noHits} + {comboboxOptions?.map((dataset) => ( + + {dataset?.title ? getTranslateText(dataset?.title) : dataset.uri} + + ))} + + )} + remove(index)}> +
+ ))} + + push({ type: { code: '' }, source: { uri: '' } })}> +
+ )} +
+ + {datasetSeries && ( + setFieldValue('inSeries', value.toString())} + value={values.inSeries ? [values.inSeries] : []} + initialValue={values?.inSeries ? [values?.inSeries] : []} + placeholder={`${localization.search.search}...`} + > + {localization.search.noHits} + {datasetSeries.map((dataset) => ( + + {getTranslateText(dataset.title)} + + ))} + + )} + + + + {({ push, remove }) => ( +
+ {values.relations?.map((_, index) => ( +
+ + + + remove(index)}> +
+ ))} + + + push({ + prefLabel: { nb: '' }, + uri: '', + }) + } + > +
+ )} +
+
+
+ ); +}; diff --git a/apps/dataset-catalog/components/dataset-form/index.tsx b/apps/dataset-catalog/components/dataset-form/index.tsx index 34fd6be5f..2cdf8a979 100644 --- a/apps/dataset-catalog/components/dataset-form/index.tsx +++ b/apps/dataset-catalog/components/dataset-form/index.tsx @@ -1,7 +1,7 @@ 'use client'; import { localization, trimObjectWhitespace } from '@catalog-frontend/utils'; import { Button } from '@digdir/designsystemet-react'; -import { Dataset, DatasetToBeCreated, ReferenceData } from '@catalog-frontend/types'; +import { Dataset, DatasetSeries, DatasetToBeCreated, ReferenceData } from '@catalog-frontend/types'; import { FormLayout, useWarnIfUnsavedChanges } from '@catalog-frontend/ui'; import { Formik, Form } from 'formik'; import { useParams } from 'next/navigation'; @@ -20,6 +20,7 @@ import { GeographySection } from './components/dataset-form-geography-section'; import { InformationModelSection } from './components/dataset-form-information-model-section'; import { QualifiedAttributionsSection } from './components/dataset-form-qualified-attributions-section'; import { ExampleDataSection } from './components/dataset-form-example-data-section'; +import { RelationsSection } from './components/dataset-form-relations-section'; type Props = { initialValues: DatasetToBeCreated | Dataset; @@ -27,9 +28,17 @@ type Props = { searchEnv: string; // Environment variable to search service referenceDataEnv: string; // Environment variable to reference data referenceData: ReferenceData; + datasetSeries: DatasetSeries[]; }; -export const DatasetForm = ({ initialValues, submitType, referenceData, searchEnv, referenceDataEnv }: Props) => { +export const DatasetForm = ({ + initialValues, + submitType, + referenceData, + searchEnv, + referenceDataEnv, + datasetSeries, +}: Props) => { const { catalogId, datasetId } = useParams(); const [isDirty, setIsDirty] = useState(false); const { losThemes, dataThemes, provenanceStatements, datasetTypes, frequencies, languages } = referenceData; @@ -196,6 +205,15 @@ export const DatasetForm = ({ initialValues, submitType, referenceData, searchEn > + + + ); diff --git a/apps/dataset-catalog/components/dataset-form/utils/dataset-initial-values.tsx b/apps/dataset-catalog/components/dataset-form/utils/dataset-initial-values.tsx index 1883ae432..8bf2aa6c5 100644 --- a/apps/dataset-catalog/components/dataset-form/utils/dataset-initial-values.tsx +++ b/apps/dataset-catalog/components/dataset-form/utils/dataset-initial-values.tsx @@ -20,7 +20,7 @@ export const datasetTemplate = (dataset: Dataset): Dataset => { landingPage: dataset.landingPage && dataset?.landingPage?.length > 0 && dataset.landingPage.every((page) => page !== null) ? dataset.landingPage - : [''], + : [], losThemeList: dataset.theme ? dataset.theme.filter((t) => t.uri && t.uri.includes('/los/')).map((t) => t.uri) : [], euThemeList: dataset.theme ? dataset.theme.filter((t) => t.uri && t.uri.includes('/data-theme/')).map((t) => t.uri) @@ -55,6 +55,9 @@ export const datasetTemplate = (dataset: Dataset): Dataset => { mediaType: [], }, ], + references: dataset.references ?? [{ source: { uri: '' }, referenceType: { code: '' } }], + relations: dataset.relations ?? [{ uri: '', prefLabel: { nb: '' } }], + inSeries: dataset.inSeries ?? '', }; }; @@ -71,7 +74,7 @@ export const datasetToBeCreatedTemplate = (): DatasetToBeCreated => { en: '', }, registrationStatus: PublicationStatus.DRAFT, - landingPage: [''], + landingPage: [], accessRights: { uri: '' }, legalBasisForAccess: [{ uri: '', prefLabel: { nb: '' } }], legalBasisForProcessing: [{ uri: '', prefLabel: { nb: '' } }], @@ -108,5 +111,8 @@ export const datasetToBeCreatedTemplate = (): DatasetToBeCreated => { mediaType: [], }, ], + references: [{ source: { uri: '' }, referenceType: { code: '' } }], + relations: [{ uri: '', prefLabel: { nb: '' } }], + inSeries: '', }; }; diff --git a/apps/dataset-catalog/components/dataset-form/utils/relations.json b/apps/dataset-catalog/components/dataset-form/utils/relations.json new file mode 100644 index 000000000..9c4da7d52 --- /dev/null +++ b/apps/dataset-catalog/components/dataset-form/utils/relations.json @@ -0,0 +1,102 @@ +[ + { + "code": "hasVersion", + "uriAsPrefix": "dct:hasVersion", + "label": { + "nn": "Har versjon", + "nb": "Har versjon", + "en": "Has version" + }, + "uri": "http://purl.org/dc/terms/hasVersion" + }, + { + "code": "isVersionOf", + "uriAsPrefix": "dct:isVersionOf", + "label": { + "nn": "Er versjon av", + "nb": "Er versjon av", + "en": "Is version of" + }, + "uri": "http://purl.org/dc/terms/isVersionOf" + }, + { + "code": "isPartOf", + "uriAsPrefix": "dct:isPartOf", + "label": { + "nn": "Er del av", + "nb": "Er en del av", + "en": "Is part of" + }, + "uri": "http://purl.org/dc/terms/isPartOf" + }, + { + "code": "hasPart", + "uriAsPrefix": "dct:hasPart", + "label": { + "nn": "Har del", + "nb": "Har del", + "en": "Has part" + }, + "uri": "http://purl.org/dc/terms/hasPart" + }, + { + "code": "isReferencedBy", + "uriAsPrefix": "dct:isReferencedBy", + "label": { + "nn": "Er referert av", + "nb": "Er referert av", + "en": "Is referenced by" + }, + "uri": "http://purl.org/dc/terms/isReferencedBy" + }, + { + "code": "references", + "uriAsPrefix": "dct:references", + "label": { + "nn": "Refererar", + "nb": "Refererer", + "en": "References" + }, + "uri": "http://purl.org/dc/terms/references" + }, + { + "code": "isReplacedBy", + "uriAsPrefix": "dct:isReplacedBy", + "label": { + "nn": "Er erstatta av", + "nb": "Er erstattet av", + "en": "Is replaced by" + }, + "uri": "http://purl.org/dc/terms/isReplacedBy" + }, + { + "code": "replaces", + "uriAsPrefix": "dct:replaces", + "label": { + "nn": "Erstatter", + "nb": "Erstatter", + "en": "Replaces" + }, + "uri": "http://purl.org/dc/terms/replaces" + }, + { + "code": "relation", + "uriAsPrefix": "dct:relation", + "label": { + "nn": "Er relatert til", + "nb": "Er relatert til", + "en": "Has relation to" + }, + "uri": "http://purl.org/dc/terms/relation" + }, + { + "code": "source", + "uriAsPrefix": "dct:source", + "label": { + "nn": "Kjelde", + "nb": "Kilde", + "en": "Source" + }, + "uri": "http://purl.org/dc/terms/source" + } +] diff --git a/apps/dataset-catalog/components/dataset-form/utils/validation-schema.tsx b/apps/dataset-catalog/components/dataset-form/utils/validation-schema.tsx index cea825d7a..8844dd824 100644 --- a/apps/dataset-catalog/components/dataset-form/utils/validation-schema.tsx +++ b/apps/dataset-catalog/components/dataset-form/utils/validation-schema.tsx @@ -57,4 +57,11 @@ export const datasetValidationSchema = Yup.object().shape({ .url(localization.validation.invalidUrl), }), ), + relations: Yup.array().of( + Yup.object().shape({ + uri: Yup.string() + .matches(httpsRegex, localization.validation.invalidProtocol) + .url(localization.validation.invalidUrl), + }), + ), }); diff --git a/apps/dataset-catalog/hooks/useSearchService.ts b/apps/dataset-catalog/hooks/useSearchService.ts index 9d6db536c..d72befae8 100644 --- a/apps/dataset-catalog/hooks/useSearchService.ts +++ b/apps/dataset-catalog/hooks/useSearchService.ts @@ -61,3 +61,33 @@ export const useSearchConceptsByUri = (searchEnv: string, uriList: string[]) => enabled: !!uriList && !!searchEnv, }); }; + +export const useSearchDatasetSuggestions = (searchEnv: string, searchQuery?: string) => { + return useQuery({ + queryKey: ['searchDatasetSuggestions', 'searchQuery', searchQuery], + queryFn: async () => { + const res = await searchSuggestions(searchEnv, searchQuery, 'datasets'); + const data = await res.json(); + return data.suggestions; + }, + enabled: !!searchQuery && !!searchEnv, + }); +}; + +export const useSearchDatasetsByUri = (searchEnv: string, uriList: string[]) => { + const searchOperation: Search.SearchOperation = { + filters: { uri: { value: uriList } }, + }; + return useQuery({ + queryKey: ['searchDatasetByUri', 'uriList', uriList], + queryFn: async () => { + if (uriList.length === 0) { + return []; + } + const res = await searchResourcesWithFilter(searchEnv, 'datasets', searchOperation); + const data = await res.json(); + return data.hits as Search.SearchObject[]; + }, + enabled: !!uriList && !!searchEnv, + }); +}; diff --git a/libs/data-access/src/lib/datasets/api/index.tsx b/libs/data-access/src/lib/datasets/api/index.tsx index fc9b6b190..9c2734a5c 100644 --- a/libs/data-access/src/lib/datasets/api/index.tsx +++ b/libs/data-access/src/lib/datasets/api/index.tsx @@ -105,3 +105,15 @@ export const getAllDatasetCatalogs = async (accessToken: string) => { }; return await fetch(resource, options); }; + +export const getAllDatasetSeries = async (catalogId: string, accessToken: string) => { + const resource = `${path}/catalogs/${catalogId}/datasets?specializedType=SERIES`; + const options = { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + next: { tags: ['datasetSeries'] }, + }; + return await fetch(resource, options); +}; diff --git a/libs/types/src/lib/dataset.ts b/libs/types/src/lib/dataset.ts index 11877b6a1..60dc830c2 100644 --- a/libs/types/src/lib/dataset.ts +++ b/libs/types/src/lib/dataset.ts @@ -39,6 +39,9 @@ export interface DatasetToBeCreated { informationModelsFromFDK?: string[]; qualifiedAttributions?: string[]; sample?: Sample[]; + references?: Reference[]; + relations?: UriWithLabel[]; + inSeries?: string; // Arrays of uris used as helper values for Formik. These properties is not part of the db object. losThemeList?: string[]; euThemeList?: string[]; @@ -64,3 +67,13 @@ interface Sample { mediaType?: string[]; description?: LocalizedStrings; } +export interface Reference { + referenceType: { code: string }; + source: { uri: string }; +} + +export interface DatasetSeries { + title: string; + uri: string; + id: string; +} diff --git a/libs/utils/src/lib/language/dataset.form.nb.ts b/libs/utils/src/lib/language/dataset.form.nb.ts index 255f16315..9c0bc6942 100644 --- a/libs/utils/src/lib/language/dataset.form.nb.ts +++ b/libs/utils/src/lib/language/dataset.form.nb.ts @@ -33,6 +33,9 @@ export const datasetFormNb = { informationModelFDK: 'Søk etter informasonsmodeller fra Felles datakatalog og velg fra nedtrekksliste.', informationModelOther: 'Legg til informasjonsmodell via lenke.', qualifiedAttributions: 'Søk på organisasjoner eller oppgi organisasjonsnummer.', + relationsDataset: 'Oppgi relaterte datasett', + relationDatasetSeries: 'Oppgi relaterte datasettserier.', + relatedResources: 'Oppgi relaterte ressurser.', }, heading: { description: 'Beskrivelse av datasettet', @@ -67,6 +70,10 @@ export const datasetFormNb = { qualifiedAttributions: 'Innholdsleverandører', exampleData: 'Eksempeldata', type: 'Type', + relations: 'Relasjoner', + relationsDataset: 'Relasjoner til datasett', + relationDatasetSeries: 'Relasjoner til datasettserier', + relatedResources: 'Relaterte ressurser', }, accessRight: { public: 'Allmenn tilgang', @@ -85,6 +92,10 @@ export const datasetFormNb = { format: 'Format', accessURL: 'Tilgangslenke', downloadURL: 'Nedlastingslenke', + dataset: 'Datasett', + relationType: 'Relasjonstype', + datasetSeries: 'Datasettserie', + choseRelation: 'Velg relasjon', }, alert: { confirmDelete: 'Er du sikker på at du vil slette datasettbeskrivelsen?',