From 2622b50ce90e379f75446c8cf3dad5210671eaaf Mon Sep 17 00:00:00 2001 From: hegeaal Date: Fri, 22 Nov 2024 14:17:36 +0100 Subject: [PATCH] feat: add distribution to dataset form --- apps/dataset-catalog/app/actions/actions.ts | 8 + .../datasets/[datasetId]/edit/page.tsx | 8 +- .../[catalogId]/datasets/new/page.tsx | 4 + .../dataset-form-access-rights-section.tsx | 52 +-- .../dataset-form-concept-section.tsx | 2 +- .../dataset-form-content-section.tsx | 36 +- .../dataset-form-provenance-section.tsx | 19 +- .../dataset-form-distribution-section.tsx | 170 ++++++++++ .../distribution-details.tsx | 134 ++++++++ .../distribution-modal.tsx | 312 ++++++++++++++++++ .../distributions.module.css | 60 ++++ .../components/dataset-form/index.tsx | 14 +- .../utils/dataset-initial-values.tsx | 27 +- .../dataset-form/utils/validation-schema.tsx | 28 ++ .../dataset-catalog/hooks/useSearchService.ts | 37 ++- libs/types/src/lib/data-service.ts | 1 + libs/types/src/lib/dataset.ts | 15 + libs/types/src/lib/reference-data.ts | 1 + .../utils/src/lib/language/dataset.form.nb.ts | 11 + libs/utils/src/lib/language/nb.ts | 3 + 20 files changed, 872 insertions(+), 70 deletions(-) create mode 100644 apps/dataset-catalog/components/dataset-form/components/dataset-from-distribution/dataset-form-distribution-section.tsx create mode 100644 apps/dataset-catalog/components/dataset-form/components/dataset-from-distribution/distribution-details.tsx create mode 100644 apps/dataset-catalog/components/dataset-form/components/dataset-from-distribution/distribution-modal.tsx create mode 100644 apps/dataset-catalog/components/dataset-form/components/dataset-from-distribution/distributions.module.css diff --git a/apps/dataset-catalog/app/actions/actions.ts b/apps/dataset-catalog/app/actions/actions.ts index 5b41418b1..eb7584244 100644 --- a/apps/dataset-catalog/app/actions/actions.ts +++ b/apps/dataset-catalog/app/actions/actions.ts @@ -53,6 +53,10 @@ export async function createDataset(values: DatasetToBeCreated, catalogId: strin concepts: values?.conceptList ? convertListToListOfObjects(values.conceptList, 'uri') : [], spatial: values?.spatialList ? convertListToListOfObjects(values.spatialList, 'uri') : [], language: values.languageList ? convertListToListOfObjects(values.languageList, 'uri') : [], + distribution: values.distribution?.map((dist) => ({ + ...dist, + accessService: dist.accessServiceList ? convertListToListOfObjects(dist.accessServiceList, 'uri') : [], + })), }; const datasetNoEmptyValues = removeEmptyValues(newDataset); @@ -107,6 +111,10 @@ export async function updateDataset(catalogId: string, initialDataset: Dataset, concepts: values?.conceptList ? convertListToListOfObjects(values.conceptList, 'uri') : [], spatial: values?.spatialList ? convertListToListOfObjects(values.spatialList, 'uri') : [], language: values.languageList ? convertListToListOfObjects(values.languageList, 'uri') : [], + distribution: values.distribution?.map((dist) => ({ + ...dist, + accessService: dist.accessServiceList ? convertListToListOfObjects(dist.accessServiceList, 'uri') : [], + })), }); const diff = compare(initialDataset, updatedDataset); 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 a9b766dd8..33d257b2b 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 @@ -12,6 +12,7 @@ import { getFrequencies, getLanguages, getLosThemes, + getOpenLicenses, getOrganization, getProvenanceStatements, } from '@catalog-frontend/data-access'; @@ -32,7 +33,8 @@ export default async function EditDatasetPage({ params }: Params) { datasetTypesResponse, provenanceStatementsResponse, frequenciesResponse, - languagesResponse, + languageResponse, + licenceResponse, ] = await Promise.all([ getLosThemes().then((res) => res.json()), getDataThemes().then((res) => res.json()), @@ -40,6 +42,7 @@ export default async function EditDatasetPage({ params }: Params) { getProvenanceStatements().then((res) => res.json()), getFrequencies().then((res) => res.json()), getLanguages().then((res) => res.json()), + getOpenLicenses().then((res) => res.json()), ]); const referenceData = { @@ -48,7 +51,8 @@ export default async function EditDatasetPage({ params }: Params) { datasetTypes: datasetTypesResponse.datasetTypes, provenanceStatements: provenanceStatementsResponse.provenanceStatements, frequencies: frequenciesResponse.frequencies, - languages: languagesResponse.linguisticSystems, + languages: languageResponse.linguisticSystems, + openLicenses: licenceResponse.openLicenses, }; const breadcrumbList = [ 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 2f9b243fc..7a5c306e7 100644 --- a/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/new/page.tsx +++ b/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/new/page.tsx @@ -11,6 +11,7 @@ import { getFrequencies, getLanguages, getLosThemes, + getOpenLicenses, getOrganization, getProvenanceStatements, } from '@catalog-frontend/data-access'; @@ -32,6 +33,7 @@ export default async function NewDatasetPage({ params }: Params) { provenanceStatementsResponse, frequenciesResponse, languageResponse, + licenceResponse, ] = await Promise.all([ getLosThemes().then((res) => res.json()), getDataThemes().then((res) => res.json()), @@ -39,6 +41,7 @@ export default async function NewDatasetPage({ params }: Params) { getProvenanceStatements().then((res) => res.json()), getFrequencies().then((res) => res.json()), getLanguages().then((res) => res.json()), + getOpenLicenses().then((res) => res.json()), ]); const referenceData = { @@ -48,6 +51,7 @@ export default async function NewDatasetPage({ params }: Params) { provenanceStatements: provenanceStatementsResponse.provenanceStatements, frequencies: frequenciesResponse.frequencies, languages: languageResponse.linguisticSystems, + openLicenses: licenceResponse.openLicenses, }; const breadcrumbList = [ diff --git a/apps/dataset-catalog/components/dataset-form/components/dataset-form-access-rights-section.tsx b/apps/dataset-catalog/components/dataset-form/components/dataset-form-access-rights-section.tsx index 1942d5163..fa3770fcb 100644 --- a/apps/dataset-catalog/components/dataset-form/components/dataset-form-access-rights-section.tsx +++ b/apps/dataset-catalog/components/dataset-form/components/dataset-form-access-rights-section.tsx @@ -1,9 +1,10 @@ import { AccessRights, Dataset } from '@catalog-frontend/types'; -import { FormContainer, TitleWithTag } from '@catalog-frontend/ui'; +import { AddButton, FormContainer, FormikLanguageFieldset, TitleWithTag } from '@catalog-frontend/ui'; import { localization } from '@catalog-frontend/utils'; import { Button, Heading, NativeSelect, Textfield } from '@digdir/designsystemet-react'; import { MinusCircleIcon, PlusCircleIcon } from '@navikt/aksel-icons'; import { Field, FieldArray, FormikErrors, FormikHelpers } from 'formik'; +import FieldsetWithDelete from '../../fieldset-with-delete'; type AccessRightsSectionProps = { errors: FormikErrors; @@ -12,11 +13,7 @@ type AccessRightsSectionProps = { export const AccessRightsSection = ({ errors, values }: AccessRightsSectionProps) => { return ( -
- {/* */} + <> {({ field, form }: { field: any; form: FormikHelpers }) => ( {values.legalBasisForRestriction?.map((_, index) => (
- - - + remove(index)}> + +
))} -
- -
+ push({ prefLabel: { nb: '' }, uri: '' })} /> )} @@ -181,6 +161,6 @@ export const AccessRightsSection = ({ errors, values }: AccessRightsSectionProps )} -
+ ); }; diff --git a/apps/dataset-catalog/components/dataset-form/components/dataset-form-concept-section.tsx b/apps/dataset-catalog/components/dataset-form/components/dataset-form-concept-section.tsx index a9a256ca5..382ee04ea 100644 --- a/apps/dataset-catalog/components/dataset-form/components/dataset-form-concept-section.tsx +++ b/apps/dataset-catalog/components/dataset-form/components/dataset-form-concept-section.tsx @@ -97,7 +97,7 @@ export const ConceptSection = ({ searchEnv }: Props) => { diff --git a/apps/dataset-catalog/components/dataset-form/components/dataset-form-content-section.tsx b/apps/dataset-catalog/components/dataset-form/components/dataset-form-content-section.tsx index d9d0c1d4b..7b1a0e3b4 100644 --- a/apps/dataset-catalog/components/dataset-form/components/dataset-form-content-section.tsx +++ b/apps/dataset-catalog/components/dataset-form/components/dataset-form-content-section.tsx @@ -1,6 +1,6 @@ 'use client'; import { Dataset } from '@catalog-frontend/types'; -import { AddButton, FormContainer } from '@catalog-frontend/ui'; +import { AddButton, FormContainer, FormikLanguageFieldset, TextareaWithPrefix } from '@catalog-frontend/ui'; import { localization } from '@catalog-frontend/utils'; import { Heading, Textfield, Textarea, Button, Label, HelpText, Fieldset } from '@digdir/designsystemet-react'; import { MinusCircleIcon, PlusCircleIcon } from '@navikt/aksel-icons'; @@ -47,28 +47,32 @@ export const ContentSection = () => { )} - - - - ); diff --git a/apps/dataset-catalog/components/dataset-form/components/dataset-form-provenance-section.tsx b/apps/dataset-catalog/components/dataset-form/components/dataset-form-provenance-section.tsx index 2e3ac6303..0c37593ac 100644 --- a/apps/dataset-catalog/components/dataset-form/components/dataset-form-provenance-section.tsx +++ b/apps/dataset-catalog/components/dataset-form/components/dataset-form-provenance-section.tsx @@ -1,8 +1,8 @@ 'use client'; import { Dataset, ReferenceDataCode } from '@catalog-frontend/types'; -import { FormContainer } from '@catalog-frontend/ui'; +import { FormContainer, FormikLanguageFieldset, TextareaWithPrefix } from '@catalog-frontend/ui'; import { capitalizeFirstLetter, getTranslateText, localization } from '@catalog-frontend/utils'; -import { Heading, Combobox, Textfield, Textarea } from '@digdir/designsystemet-react'; +import { Heading, Combobox, Textfield, Textarea, Label } from '@digdir/designsystemet-react'; import { Field, useFormikContext } from 'formik'; interface Props { @@ -14,7 +14,7 @@ export const ProvenanceSection = ({ data }: Props) => { const { provenanceStatements, frequencies } = data; return ( -
+ <> { type='date' label={localization.datasetForm.heading.lastUpdated} /> - - -
+ + ); }; diff --git a/apps/dataset-catalog/components/dataset-form/components/dataset-from-distribution/dataset-form-distribution-section.tsx b/apps/dataset-catalog/components/dataset-form/components/dataset-from-distribution/dataset-form-distribution-section.tsx new file mode 100644 index 000000000..f3c5efc6d --- /dev/null +++ b/apps/dataset-catalog/components/dataset-form/components/dataset-from-distribution/dataset-form-distribution-section.tsx @@ -0,0 +1,170 @@ +'use client'; + +import { useState } from 'react'; +import { FieldArray, useFormikContext } from 'formik'; +import { Button, Card, Heading, Label, Link, Tag } from '@digdir/designsystemet-react'; +import { ChevronDownIcon, ChevronUpIcon, PencilWritingIcon } from '@navikt/aksel-icons'; +import { Dataset, Distribution, ReferenceDataCode } from '@catalog-frontend/types'; +import { AddButton, DeleteButton } from '@catalog-frontend/ui'; +import { getTranslateText, localization } from '@catalog-frontend/utils'; +import { useSearchFileTypeByUri } from '../../../../hooks/useReferenceDataSearch'; +import { DistributionModal } from './distribution-modal'; +import { DistributionDetails } from './distribution-details'; +import styles from './distributions.module.css'; + +interface Props { + referenceDataEnv: string; + searchEnv: string; + openLicenses: ReferenceDataCode[]; +} + +export const DistributionSection = ({ referenceDataEnv, searchEnv, openLicenses }: Props) => { + const { values, setFieldValue } = useFormikContext(); + const [expandedIndex, setExpandedIndex] = useState(null); + const { data: selectedFileTypes } = useSearchFileTypeByUri(values?.distribution?.[0]?.format ?? [], referenceDataEnv); + + function showSeeMoreButton(distribution: Distribution | undefined | null): boolean { + if (!distribution) { + return false; + } + + if ( + distribution.downloadURL?.[0] || + distribution.mediaType?.[0] || + distribution.accessServiceList?.[0] || + distribution.accessService?.[0] || + distribution.license?.uri || + distribution.description?.nb || + distribution.page?.[0]?.uri || + distribution?.conformsTo?.[0]?.prefLabel?.nb + ) { + return true; + } + return false; + } + + return ( + <> + + {() => ( +
+ {values.distribution?.map((_, index) => ( + +
+ + {getTranslateText(values?.distribution?.[index]?.title) ?? ''} + +
+ setFieldValue(`distribution[${index}]`, updatedDist)} + trigger={ + + } + /> + + setFieldValue(`distribution[${index}]`, undefined)} /> +
+
+ {values?.distribution?.[index].accessURL && ( + + )} + + {values?.distribution?.[index].accessURL} + +
+ {values?.distribution?.[index].format?.map((uri) => ( + + {(selectedFileTypes?.find((format) => format.uri === uri) ?? {}).code ?? uri} + + ))} +
+ {showSeeMoreButton(values?.distribution?.[index]) && ( + + )} + + {expandedIndex === index && ( + + )} +
+ ))} +
+
+ {localization.datasetForm.button.addDistribution}} + onSuccess={(def) => + setFieldValue( + values?.distribution ? `distribution[${values?.distribution?.length}]` : 'distribution[0]', + def, + ) + } + referenceDataEnv={referenceDataEnv} + searchEnv={searchEnv} + openLicenses={openLicenses} + distribution={{ + title: { nb: '' }, + description: { nb: '' }, + downloadURL: [], + accessURL: [], + format: [], + mediaType: [], + conformsTo: [{ uri: '', prefLabel: { nb: '' } }], + accessServiceList: [], + }} + /> +
+ {(!values?.distribution || values.distribution?.length === 0) && ( +
+ {localization.tag.recommended} +
+ )} +
+
+ )} +
+ + ); +}; diff --git a/apps/dataset-catalog/components/dataset-form/components/dataset-from-distribution/distribution-details.tsx b/apps/dataset-catalog/components/dataset-form/components/dataset-from-distribution/distribution-details.tsx new file mode 100644 index 000000000..54e474ac8 --- /dev/null +++ b/apps/dataset-catalog/components/dataset-form/components/dataset-from-distribution/distribution-details.tsx @@ -0,0 +1,134 @@ +'use client'; + +import { Dataset, ReferenceDataCode } from '@catalog-frontend/types'; +import { getTranslateText, localization } from '@catalog-frontend/utils'; +import { Paragraph, Label, Tag, Divider, Table, TableBody } from '@digdir/designsystemet-react'; +import { useFormikContext } from 'formik'; +import styles from './distributions.module.css'; +import { useSearchDataServiceByUri } from '../../../../hooks/useSearchService'; +import { useSearchMediaTypeByUri } from '../../../../hooks/useReferenceDataSearch'; +import _ from 'lodash'; + +interface Props { + index: number; + searchEnv: string; + referenceDataEnv: string; + openLicenses: ReferenceDataCode[]; +} + +export const DistributionDetails = ({ index, searchEnv, referenceDataEnv, openLicenses }: Props) => { + const { values } = useFormikContext(); + const distribution = values?.distribution?.[index]; + + const { data: selectedDataServices, isLoading: isLoadingSelectedDataServices } = useSearchDataServiceByUri( + searchEnv, + distribution?.accessServiceList ?? [], + ); + + const { data: selectedMediaTypes, isLoading: loadingSelectedMediaTypes } = useSearchMediaTypeByUri( + distribution?.mediaType ?? [], + referenceDataEnv, + ); + + const hasConformsToValues = _.some(distribution?.conformsTo, (item) => { + return _.trim(item.uri) || _.trim(_.get(item, 'prefLabel.nb')); + }); + + return ( +
+ {distribution && ( +
+ + {distribution.description?.nb && ( +
+ + {distribution.description.nb} +
+ )} + + {distribution.downloadURL && distribution.downloadURL.length > 0 && ( +
+ + {distribution?.downloadURL?.[0] ?? ''} +
+ )} + + {distribution.mediaType && distribution.mediaType.length > 0 && ( +
+ +
    + {distribution?.mediaType?.map((uri) => ( +
  • + {(selectedMediaTypes?.find((type) => type.uri === uri) ?? {}).code ?? uri} +
  • + ))} +
+
+ )} + + {distribution.accessServiceList && distribution.accessServiceList.length > 0 && ( +
+ + +
    + {distribution.accessServiceList?.map((uri) => ( +
  • + + {getTranslateText(selectedDataServices?.find((type) => type.uri === uri)?.title) ?? uri} + +
  • + ))} +
+
+ )} + + {distribution.license?.uri && ( + <> + +
+ + {getTranslateText(openLicenses.find((license) => license.uri === distribution.license?.uri)?.label)} + +
+ + )} + + {distribution.conformsTo && hasConformsToValues && ( +
+ + + + + + {localization.title} + {localization.link} + + + + {distribution.conformsTo.map((conform, index) => ( + + {getTranslateText(conform.prefLabel)} + {conform.uri} + + ))} + +
+
+ )} + + {distribution.page && distribution.page?.[0].uri && ( +
+ + {distribution?.page?.[0].uri} +
+ )} +
+ )} +
+ ); +}; diff --git a/apps/dataset-catalog/components/dataset-form/components/dataset-from-distribution/distribution-modal.tsx b/apps/dataset-catalog/components/dataset-form/components/dataset-from-distribution/distribution-modal.tsx new file mode 100644 index 000000000..5aa0fffa7 --- /dev/null +++ b/apps/dataset-catalog/components/dataset-form/components/dataset-from-distribution/distribution-modal.tsx @@ -0,0 +1,312 @@ +'use client'; + +import { DataService, Distribution, ReferenceDataCode } from '@catalog-frontend/types'; +import { + AddButton, + DeleteButton, + FormikLanguageFieldset, + FormikSearchCombobox, + TextareaWithPrefix, + TitleWithTag, +} from '@catalog-frontend/ui'; +import { getTranslateText, localization, trimObjectWhitespace } from '@catalog-frontend/utils'; +import { Button, Combobox, Divider, Label, Modal, Textfield } from '@digdir/designsystemet-react'; +import { + useSearchFileTypeByUri, + useSearchFileTypes, + useSearchMediaTypeByUri, + useSearchMediaTypes, +} from '../../../../hooks/useReferenceDataSearch'; +import { useSearchDataServiceByUri, useSearchDataServiceSuggestions } from '../../../../hooks/useSearchService'; +import { Field, FieldArray, Formik } from 'formik'; +import { ReactNode, useRef, useState } from 'react'; +import styles from './distributions.module.css'; +import { distributionTemplate } from '../../utils/dataset-initial-values'; +import { distributionSectionSchema } from '../../utils/validation-schema'; + +interface Props { + trigger: ReactNode; + referenceDataEnv: string; + searchEnv: string; + openLicenses: ReferenceDataCode[]; + onSuccess: (def: Distribution) => void; + distribution: Distribution | undefined; + type: 'new' | 'edit'; +} + +export const DistributionModal = ({ + referenceDataEnv, + searchEnv, + openLicenses, + onSuccess, + trigger, + distribution, + type, +}: Props) => { + const template = distributionTemplate(distribution); + const [submitted, setSubmitted] = useState(false); + const modalRef = useRef(null); + + const [searchQueryMediaTypes, setSearchQueryMediaTypes] = useState(''); + const [searchQueryFileTypes, setSearchQueryFileTypes] = useState(''); + const [searchDataServicesQuery, setSearchDataServicesQuery] = useState(''); + + const { data: mediaTypes, isLoading: searchingMediaTypes } = useSearchMediaTypes( + searchQueryMediaTypes, + referenceDataEnv, + ); + + const { data: selectedMediaTypes, isLoading: loadingSelectedMediaTypes } = useSearchMediaTypeByUri( + distribution?.mediaType ?? [], + referenceDataEnv, + ); + + const { data: fileTypes, isLoading: searchingFileTypes } = useSearchFileTypes(searchQueryFileTypes, referenceDataEnv); + + const { data: selectedFileTypes, isLoading: loadingSelectedFileTypes } = useSearchFileTypeByUri( + distribution?.format ?? [], + referenceDataEnv, + ); + + const { data: selectedDataServices, isLoading: isLoadingSelectedDataServices } = useSearchDataServiceByUri( + searchEnv, + distribution?.accessServiceList ?? [], + ); + + const { data: dataServices, isLoading: isLoadingDataServices } = useSearchDataServiceSuggestions( + searchEnv, + searchDataServicesQuery, + ); + + const comboboxOptions = [ + // Combine selectedDataServices and dataServices, adding missing URIs + ...new Map( + [ + ...(selectedDataServices ?? []), + ...(dataServices ?? []), + ...(distribution?.accessServiceList ?? []).map((uri) => { + const foundItem = + selectedDataServices?.find((item) => item.uri === uri) || + dataServices?.find((item: DataService) => item.uri === uri); + + return { + uri, + title: foundItem?.title ?? null, + }; + }), + ].map((option) => [option.uri, option]), + ).values(), + ]; + + return ( + + {trigger} + + { + const trimmedValues = trimObjectWhitespace(values); + onSuccess(trimmedValues); + setSubmitting(false); + setSubmitted(true); + modalRef.current?.close(); + }} + > + {({ errors, isSubmitting, submitForm, setFieldValue, values }) => ( + <> + {distribution && ( + <> + + {type === 'new' ? localization.datasetForm.button.addDistribution : localization.edit} + + + + + } + /> +
+ +
+ + + } + error={errors?.accessURL?.[0]} + /> + + + + +
+ + setSearchQueryFileTypes(event.target.value)} + onValueChange={(selectedValues) => setFieldValue('format', selectedValues)} + value={values?.format || []} + selectedValuesSearchHits={selectedFileTypes ?? []} + querySearchHits={fileTypes ?? []} + formikValues={distribution?.format ?? []} + loading={loadingSelectedFileTypes || searchingFileTypes} + portal={false} + /> +
+ + setSearchQueryMediaTypes(event.target.value)} + onValueChange={(selectedValues) => setFieldValue('mediaType', selectedValues)} + value={values?.mediaType || []} + selectedValuesSearchHits={selectedMediaTypes ?? []} + querySearchHits={mediaTypes ?? []} + formikValues={distribution?.mediaType ?? []} + loading={loadingSelectedMediaTypes || searchingMediaTypes} + portal={false} + label={localization.datasetForm.fieldLabel.mediaTypes} + /> + + + {!isLoadingSelectedDataServices && ( + setSearchDataServicesQuery(event.target.value)} + value={[...(values.accessServiceList || []), ...(distribution?.accessServiceList || [])]} + onValueChange={(selectedValues) => setFieldValue('accessServiceList', selectedValues)} + label={localization.datasetForm.fieldLabel.accessService} + placeholder={`${localization.search.search}...`} + > + {comboboxOptions.map((option) => ( + + {option.title ? getTranslateText(option.title) : option.uri} + + ))} + + )} + + + setFieldValue('license.uri', selectedValues.toString())} + > + {openLicenses.map((license) => ( + + {getTranslateText(license.label)} + + ))} + + + + + + + + + + + + {({ push, remove }) => ( + <> + {distribution.conformsTo?.map((_, i) => ( +
+ + + remove(i)} /> +
+ ))} + push({ uri: '', prefLabel: { nb: '' } })}> + {localization.datasetForm.button.addStandard} + + + )} +
+
+ + + + + + + )} + + )} +
+
+
+ ); +}; diff --git a/apps/dataset-catalog/components/dataset-form/components/dataset-from-distribution/distributions.module.css b/apps/dataset-catalog/components/dataset-form/components/dataset-from-distribution/distributions.module.css new file mode 100644 index 000000000..098a70caf --- /dev/null +++ b/apps/dataset-catalog/components/dataset-form/components/dataset-from-distribution/distributions.module.css @@ -0,0 +1,60 @@ +.field { + padding-bottom: 1.5rem; +} + +.modalContent { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.list { + display: flex; + gap: 0.5rem; +} + +.standard { + display: grid; + grid-template-columns: 43% 43% 10%; + gap: 2%; + padding-bottom: 1.5rem; +} + +.button { + width: fit-content; +} + +.add { + display: flex; + height: fit-content; + align-items: center; +} + +.format { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.buttons { + display: flex; +} + +.heading { + display: flex; + justify-content: space-between; +} + +.tags { + display: flex; + gap: 1rem; + padding-top: 2rem; + padding-bottom: 2rem; + max-width: 963px; + flex-wrap: wrap; +} + +.dialog { + max-width: 1000px; + overflow: hidden; +} diff --git a/apps/dataset-catalog/components/dataset-form/index.tsx b/apps/dataset-catalog/components/dataset-form/index.tsx index 2cdf8a979..71b9c9eb0 100644 --- a/apps/dataset-catalog/components/dataset-form/index.tsx +++ b/apps/dataset-catalog/components/dataset-form/index.tsx @@ -21,6 +21,7 @@ import { InformationModelSection } from './components/dataset-form-information-m 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'; +import { DistributionSection } from './components/dataset-from-distribution/dataset-form-distribution-section'; type Props = { initialValues: DatasetToBeCreated | Dataset; @@ -41,7 +42,8 @@ export const DatasetForm = ({ }: Props) => { const { catalogId, datasetId } = useParams(); const [isDirty, setIsDirty] = useState(false); - const { losThemes, dataThemes, provenanceStatements, datasetTypes, frequencies, languages } = referenceData; + const { losThemes, dataThemes, provenanceStatements, datasetTypes, frequencies, languages, openLicenses } = + referenceData; useWarnIfUnsavedChanges({ unsavedChanges: isDirty }); @@ -214,6 +216,16 @@ export const DatasetForm = ({ datasetSeries={datasetSeries} /> + + + ); 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 8bf2aa6c5..7e3a03acf 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 @@ -1,4 +1,4 @@ -import { AccessRights, Dataset, DatasetToBeCreated, PublicationStatus } from '@catalog-frontend/types'; +import { AccessRights, Dataset, DatasetToBeCreated, Distribution, PublicationStatus } from '@catalog-frontend/types'; import { groupByKeys } from '@catalog-frontend/utils'; export const datasetTemplate = (dataset: Dataset): Dataset => { @@ -58,6 +58,10 @@ export const datasetTemplate = (dataset: Dataset): Dataset => { references: dataset.references ?? [{ source: { uri: '' }, referenceType: { code: '' } }], relations: dataset.relations ?? [{ uri: '', prefLabel: { nb: '' } }], inSeries: dataset.inSeries ?? '', + distribution: dataset.distribution?.map((dist) => ({ + ...dist, + accessServiceList: dist.accessService?.map((service) => service.uri) || [], + })), }; }; @@ -114,5 +118,26 @@ export const datasetToBeCreatedTemplate = (): DatasetToBeCreated => { references: [{ source: { uri: '' }, referenceType: { code: '' } }], relations: [{ uri: '', prefLabel: { nb: '' } }], inSeries: '', + distribution: [], }; }; + +export const distributionTemplate = (dist: Distribution | undefined) => { + return dist + ? { + ...dist, + accessServiceList: dist.accessService?.map((service) => service.uri) || [], + } + : { + title: { nb: '' }, + description: { nb: '' }, + downloadURL: [], + accessURL: [], + format: [], + mediaType: [], + licenseList: [], + conformsTo: [{ uri: '', prefLabel: { nb: '' } }], + pageList: [], + accessServiceList: [], + }; +}; 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 8844dd824..bf07b754b 100644 --- a/apps/dataset-catalog/components/dataset-form/utils/validation-schema.tsx +++ b/apps/dataset-catalog/components/dataset-form/utils/validation-schema.tsx @@ -65,3 +65,31 @@ export const datasetValidationSchema = Yup.object().shape({ }), ), }); + +export const distributionSectionSchema = Yup.object().shape({ + title: Yup.object().shape({ + nb: Yup.string().required(localization.validation.titleRequired), + }), + accessURL: Yup.array() + .of( + Yup.string().matches(httpsRegex, localization.validation.invalidProtocol).url(localization.validation.invalidUrl), + ) + .required(localization.datasetForm.validation.titleRequired), + downloadURL: Yup.array().of( + Yup.string().matches(httpsRegex, localization.validation.invalidProtocol).url(localization.validation.accessURL), + ), + conformsTo: Yup.array().of( + Yup.object().shape({ + uri: Yup.string() + .matches(httpsRegex, localization.validation.invalidProtocol) + .url(localization.validation.invalidUrl), + }), + ), + page: 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 d72befae8..1360731c6 100644 --- a/apps/dataset-catalog/hooks/useSearchService.ts +++ b/apps/dataset-catalog/hooks/useSearchService.ts @@ -14,6 +14,17 @@ export const useSearchInformationModelsSuggestions = (searchEnv: string, searchQ }); }; +export const useSearchConceptSuggestions = (searchEnv: string, searchQuery?: string) => { + return useQuery({ + queryKey: ['searchConceptSuggestions', searchQuery], + queryFn: async () => { + const data = await searchSuggestions(searchEnv, searchQuery, 'concepts'); + return data.json(); + }, + enabled: !!searchQuery && !!searchEnv, + }); +}; + export const useSearchInformationModelsByUri = (searchEnv: string, uriList: string[]) => { const searchOperation: Search.SearchOperation = { filters: { uri: { value: uriList } }, @@ -32,11 +43,13 @@ export const useSearchInformationModelsByUri = (searchEnv: string, uriList: stri }); }; -export const useSearchConceptSuggestions = (searchEnv: string, searchQuery?: string) => { +// Dataservices + +export const useSearchDataServiceSuggestions = (searchEnv: string, searchQuery?: string) => { return useQuery({ - queryKey: ['searchConceptSuggestions', 'searchQuery', searchQuery], + queryKey: ['searchDataServiceSuggestions', searchQuery], queryFn: async () => { - const res = await searchSuggestions(searchEnv, searchQuery, 'concepts'); + const res = await searchSuggestions(searchEnv, searchQuery, 'dataservices'); const data = await res.json(); return data.suggestions; }, @@ -44,6 +57,24 @@ export const useSearchConceptSuggestions = (searchEnv: string, searchQuery?: str }); }; +export const useSearchDataServiceByUri = (searchEnv: string, uriList: string[]) => { + const searchOperation: Search.SearchOperation = { + filters: { uri: { value: uriList } }, + }; + return useQuery({ + queryKey: ['searchDataServicesByUri', uriList], + queryFn: async () => { + if (uriList.length === 0) { + return []; + } + const res = await searchResourcesWithFilter(searchEnv, 'dataservices', searchOperation); + const data = await res.json(); + return data.hits as Search.SearchObject[]; + }, + enabled: !!uriList && !!searchEnv, + }); +}; + export const useSearchConceptsByUri = (searchEnv: string, uriList: string[]) => { const searchOperation: Search.SearchOperation = { filters: { uri: { value: uriList } }, diff --git a/libs/types/src/lib/data-service.ts b/libs/types/src/lib/data-service.ts index b4a71fea3..e4377ff0a 100644 --- a/libs/types/src/lib/data-service.ts +++ b/libs/types/src/lib/data-service.ts @@ -7,4 +7,5 @@ export interface DataService { modified: string; status: string; organizationId: string; + uri: string; } diff --git a/libs/types/src/lib/dataset.ts b/libs/types/src/lib/dataset.ts index 60dc830c2..55fa9cbde 100644 --- a/libs/types/src/lib/dataset.ts +++ b/libs/types/src/lib/dataset.ts @@ -42,6 +42,7 @@ export interface DatasetToBeCreated { references?: Reference[]; relations?: UriWithLabel[]; inSeries?: string; + distribution?: Distribution[]; // Arrays of uris used as helper values for Formik. These properties is not part of the db object. losThemeList?: string[]; euThemeList?: string[]; @@ -77,3 +78,17 @@ export interface DatasetSeries { uri: string; id: string; } +export interface Distribution { + title?: LocalizedStrings; + description?: LocalizedStrings; + downloadURL?: string[]; + accessURL?: string[]; + format?: string[]; + mediaType?: string[]; + license?: { uri: string; code: string }; + conformsTo?: UriWithLabel[]; + page?: [{ uri: string }]; + accessService?: [{ uri: string }]; + // Arrays of uris used as helper values for Formik. These properties is not part of the db object. + accessServiceList?: string[]; +} diff --git a/libs/types/src/lib/reference-data.ts b/libs/types/src/lib/reference-data.ts index 274d079ec..9e9c597fa 100644 --- a/libs/types/src/lib/reference-data.ts +++ b/libs/types/src/lib/reference-data.ts @@ -40,4 +40,5 @@ export interface ReferenceData { datasetTypes: ReferenceDataCode[]; frequencies: ReferenceDataCode[]; languages: ReferenceDataCode[]; + openLicenses: ReferenceDataCode[]; } diff --git a/libs/utils/src/lib/language/dataset.form.nb.ts b/libs/utils/src/lib/language/dataset.form.nb.ts index 9c0bc6942..5d00890f6 100644 --- a/libs/utils/src/lib/language/dataset.form.nb.ts +++ b/libs/utils/src/lib/language/dataset.form.nb.ts @@ -74,6 +74,7 @@ export const datasetFormNb = { relationsDataset: 'Relasjoner til datasett', relationDatasetSeries: 'Relasjoner til datasettserier', relatedResources: 'Relaterte ressurser', + distribution: 'Distribusjon', }, accessRight: { public: 'Allmenn tilgang', @@ -96,6 +97,12 @@ export const datasetFormNb = { relationType: 'Relasjonstype', datasetSeries: 'Datasettserie', choseRelation: 'Velg relasjon', + standard: 'Standard', + distributionLink: 'Lenke til dokumentasjon av distribusjonen', + license: 'Lisens', + accessService: 'Tilgangstjeneste', + downloadUrl: 'Nedlastingslenke', + accessUrl: 'Tilgangslenke', }, alert: { confirmDelete: 'Er du sikker på at du vil slette datasettbeskrivelsen?', @@ -109,10 +116,14 @@ export const datasetFormNb = { url: `Ugyldig lenke. Vennligst sørg for at lenken starter med ‘https://’ og inneholder et gyldig toppdomene (f.eks. ‘.no’).`, euTheme: 'Minst ett EU-tema må være valgt.', searchString: 'Ingen treff. Søkestrengen må inneholde minst to bokstaver.', + accessURL: 'Tilgangslenke er påkrevd.', }, button: { addDate: 'Legg til tidsperiode', addInformationModel: 'Legg til informasjonsmodell', + addStandard: 'Legg til standard', + addDistribution: 'Legg til distribusjon', + updateDistribution: 'Oppdater distribusjon', }, errors: { qualifiedAttributions: 'Kunne ikke hente enheter.', diff --git a/libs/utils/src/lib/language/nb.ts b/libs/utils/src/lib/language/nb.ts index 6c46a3c49..9f2f3b221 100644 --- a/libs/utils/src/lib/language/nb.ts +++ b/libs/utils/src/lib/language/nb.ts @@ -49,6 +49,8 @@ export const nb = { services: 'Tjenester', showLess: 'Vis færre', showMore: 'Vis flere', + seeMore: 'Se mer', + seeLess: 'Se mindre', somethingWentWrong: 'Beklager, noe gikk galt. Prøv på nytt litt senere.', sorting: 'Sortering', status: 'Status', @@ -426,6 +428,7 @@ export const nb = { invalidProtocol: `Ugyldig lenke. Vennligst sørg for at lenken starter med ‘https://’.`, invalidTlf: 'Ugyldig telefonnummer', nameRequired: 'Må ha navn', + accessURLRequired: 'Tilgangslenke må fylles ut.', }, serviceCatalog: {