diff --git a/apps/catalog-portal/app/actions/actions.ts b/apps/catalog-portal/app/actions/actions.ts index 206f6adb2..0fc266d88 100644 --- a/apps/catalog-portal/app/actions/actions.ts +++ b/apps/catalog-portal/app/actions/actions.ts @@ -7,7 +7,7 @@ import { getAllServiceCatalogs, getAllProcessingActivities, getAllServiceMessages, - ServiceMessage, + Strapi, } from '@catalog-frontend/data-access'; import { ConceptCatalog, @@ -103,7 +103,7 @@ export async function getAllProcessingActivitiesCatalogs(): Promise { +export async function getServiceMessages(): Promise { const response = await getAllServiceMessages(); if (response.status !== 200) { console.error('getServiceMessages failed with response code ' + response.status); diff --git a/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/[datasetId]/dataset-details-page-client.tsx b/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/[datasetId]/dataset-details-page-client.tsx new file mode 100644 index 000000000..906cf34ea --- /dev/null +++ b/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/[datasetId]/dataset-details-page-client.tsx @@ -0,0 +1,106 @@ +'use client'; + +import { Dataset, DatasetSeries, ReferenceData } from '@catalog-frontend/types'; +import { DeleteButton, DetailsPageLayout, LinkButton } from '@catalog-frontend/ui'; +import { getTranslateText, localization } from '@catalog-frontend/utils'; +import { useState } from 'react'; +import styles from './dataset-details-page.module.css'; +import { RightColumn } from '../../../../../components/details-page-columns/details-page-right-column'; +import { LeftColumn } from '../../../../../components/details-page-columns/details-page-left-column'; +import { deleteDataset } from '../../../../actions/actions'; +import { Tag } from '@digdir/designsystemet-react'; + +interface datasetDetailsPageProps { + dataset: Dataset; + catalogId: string; + datasetId: string; + hasWritePermission: boolean; + searchEnv: string; + referenceDataEnv: string; + referenceData: ReferenceData; + datasetSeries: DatasetSeries[]; +} + +const DatasetDetailsPageClient = ({ + dataset, + catalogId, + datasetId, + hasWritePermission, + searchEnv, + referenceDataEnv, + referenceData, + datasetSeries, +}: datasetDetailsPageProps) => { + const [language, setLanguage] = useState('nb'); + + const handleLanguageChange = (value: string) => { + setLanguage(value); + }; + + const handleDeleteDataset = async () => { + if (dataset) { + if (window.confirm(localization.serviceCatalog.form.confirmDelete)) { + try { + await deleteDataset(catalogId, dataset.id); + } catch (error) { + window.alert(error); + } + } + } + }; + + enum StatusColors { + DRAFT = 'second', + PUBLISH = 'success', + APPROVE = 'info', + } + + return ( + + {localization.datasetForm.status[dataset.registrationStatus]} + + } + loading={false} + > + + + + + + + + {hasWritePermission && ( +
+ + {localization.button.edit} + + + handleDeleteDataset()} + /> +
+ )} +
+
+ ); +}; + +export default DatasetDetailsPageClient; diff --git a/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/[datasetId]/dataset-details-page.module.css b/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/[datasetId]/dataset-details-page.module.css new file mode 100644 index 000000000..4b092f596 --- /dev/null +++ b/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/[datasetId]/dataset-details-page.module.css @@ -0,0 +1,5 @@ +.set { + display: flex; + gap: 0.5rem; + padding-bottom: 1rem; +} diff --git a/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/[datasetId]/page.tsx b/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/[datasetId]/page.tsx index 007869618..9728be840 100644 --- a/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/[datasetId]/page.tsx +++ b/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/[datasetId]/page.tsx @@ -1,15 +1,39 @@ import { Breadcrumbs, BreadcrumbType, PageBanner } from '@catalog-frontend/ui'; import { getDatasetById } from '../../../../actions/actions'; import { Params } from 'next/dist/shared/lib/router/utils/route-matcher'; -import { getTranslateText, localization } from '@catalog-frontend/utils'; +import { + getTranslateText, + getValidSession, + hasOrganizationWritePermission, + localization, +} from '@catalog-frontend/utils'; import { Organization } from '@catalog-frontend/types'; -import { getOrganization } from '@catalog-frontend/data-access'; +import { + getAllDatasetSeries, + getDatasetTypes, + getDataThemes, + getFrequencies, + getLanguages, + getLosThemes, + getOpenLicenses, + getOrganization, + getProvenanceStatements, +} from '@catalog-frontend/data-access'; +import DatasetDetailsPageClient from './dataset-details-page-client'; export default async function EditDatasetPage({ params }: Params) { const { catalogId, datasetId } = params; const dataset = await getDatasetById(catalogId, datasetId); 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({ + callbackUrl: `/catalogs/${catalogId}/services/${datasetId}`, + }); + const hasWritePermission = session && hasOrganizationWritePermission(session?.accessToken, catalogId); + const breadcrumbList = [ { href: `/catalogs/${catalogId}/datasets`, @@ -21,14 +45,59 @@ export default async function EditDatasetPage({ params }: Params) { }, ] as BreadcrumbType[]; + const [ + losThemesResponse, + dataThemesResponse, + datasetTypesResponse, + provenanceStatementsResponse, + frequenciesResponse, + languageResponse, + licenceResponse, + ] = await Promise.all([ + getLosThemes().then((res) => res.json()), + getDataThemes().then((res) => res.json()), + getDatasetTypes().then((res) => res.json()), + getProvenanceStatements().then((res) => res.json()), + getFrequencies().then((res) => res.json()), + getLanguages().then((res) => res.json()), + getOpenLicenses().then((res) => res.json()), + ]); + + const referenceData = { + losThemes: losThemesResponse.losNodes, + dataThemes: dataThemesResponse.dataThemes, + datasetTypes: datasetTypesResponse.datasetTypes, + provenanceStatements: provenanceStatementsResponse.provenanceStatements, + frequencies: frequenciesResponse.frequencies, + languages: languageResponse.linguisticSystems, + openLicenses: licenceResponse.openLicenses, + }; + + const accessToken = session?.accessToken; + const datasetSeries = await getAllDatasetSeries(catalogId, accessToken).then((res) => res.json()); + return ( <> - + -
Her kommer details-page
+
+ +
); } diff --git a/apps/dataset-catalog/components/dataset-form/components/about-section.tsx b/apps/dataset-catalog/components/dataset-form/components/about-section.tsx index e37a37571..9c8d4f32e 100644 --- a/apps/dataset-catalog/components/dataset-form/components/about-section.tsx +++ b/apps/dataset-catalog/components/dataset-form/components/about-section.tsx @@ -1,13 +1,11 @@ -import { Dataset } from '@catalog-frontend/types'; import { FormikLanguageFieldset, LabelWithHelpTextAndTag, TextareaWithPrefix } from '@catalog-frontend/ui'; import { localization } from '@catalog-frontend/utils'; import { Textfield } from '@digdir/designsystemet-react'; -import { FastField, useFormikContext } from 'formik'; +import { FastField } from 'formik'; import { FieldsetDivider } from '@catalog-frontend/ui'; import { AccessRightFields } from './access-rights.tsx/dataset-form-access-rights-section'; export const AboutSection = () => { - const errors = useFormikContext()?.errors; return ( <> { label={ - {localization.datasetForm.fieldLabel.releaseDate} + {localization.datasetForm.fieldLabel.issued} } /> diff --git a/apps/dataset-catalog/components/dataset-form/components/access-rights.tsx/access-rights-uri-table.tsx b/apps/dataset-catalog/components/dataset-form/components/access-rights.tsx/access-rights-uri-table.tsx index 7a628c7fb..f97597e4b 100644 --- a/apps/dataset-catalog/components/dataset-form/components/access-rights.tsx/access-rights-uri-table.tsx +++ b/apps/dataset-catalog/components/dataset-form/components/access-rights.tsx/access-rights-uri-table.tsx @@ -8,7 +8,7 @@ import { LabelWithHelpTextAndTag, } from '@catalog-frontend/ui'; import { getTranslateText, localization, trimObjectWhitespace } from '@catalog-frontend/utils'; -import { Button, Label, Modal, Radio, Table, Textfield } from '@digdir/designsystemet-react'; +import { Button, Modal, Radio, Table, Textfield } from '@digdir/designsystemet-react'; import { FastField, Formik, useFormikContext } from 'formik'; import styles from '../../dataset-form.module.css'; import { useRef, useState } from 'react'; diff --git a/apps/dataset-catalog/components/dataset-form/components/details-section/hidden-detail-fields.tsx b/apps/dataset-catalog/components/dataset-form/components/details-section/hidden-detail-fields.tsx index dc6ebdd4b..da6ef376a 100644 --- a/apps/dataset-catalog/components/dataset-form/components/details-section/hidden-detail-fields.tsx +++ b/apps/dataset-catalog/components/dataset-form/components/details-section/hidden-detail-fields.tsx @@ -62,7 +62,6 @@ export const HiddenDetailFields = ({ datasetTypes, provenanceStatements, frequen [frequencies], ); - console.log(_.every(values.conformsTo, _.isUndefined), values.conformsTo); return (
diff --git a/apps/dataset-catalog/components/dataset-form/components/details-section/recommended-detail-fields.tsx b/apps/dataset-catalog/components/dataset-form/components/details-section/recommended-detail-fields.tsx index d4c7ff5c5..f64bc9d49 100644 --- a/apps/dataset-catalog/components/dataset-form/components/details-section/recommended-detail-fields.tsx +++ b/apps/dataset-catalog/components/dataset-form/components/details-section/recommended-detail-fields.tsx @@ -1,5 +1,5 @@ 'use client'; -import { FieldsetDivider, FormikSearchCombobox, LabelWithHelpTextAndTag, TitleWithTag } from '@catalog-frontend/ui'; +import { FieldsetDivider, FormikSearchCombobox, LabelWithHelpTextAndTag } from '@catalog-frontend/ui'; import { getTranslateText, localization } from '@catalog-frontend/utils'; import { Checkbox } from '@digdir/designsystemet-react'; import { useCallback, useState } from 'react'; diff --git a/apps/dataset-catalog/components/dataset-form/components/details-section/temporal-modal.tsx b/apps/dataset-catalog/components/dataset-form/components/details-section/temporal-modal.tsx index 5e6b8eeb9..113120b4c 100644 --- a/apps/dataset-catalog/components/dataset-form/components/details-section/temporal-modal.tsx +++ b/apps/dataset-catalog/components/dataset-form/components/details-section/temporal-modal.tsx @@ -1,4 +1,4 @@ -import { DateRange, UriWithLabel } from '@catalog-frontend/types'; +import { DateRange } from '@catalog-frontend/types'; import { AddButton, DeleteButton, EditButton } from '@catalog-frontend/ui'; import { localization, trimObjectWhitespace } from '@catalog-frontend/utils'; import { Button, Label, Modal, Table, Textfield } from '@digdir/designsystemet-react'; diff --git a/apps/dataset-catalog/components/dataset-form/components/distribution-section/distribution-details.tsx b/apps/dataset-catalog/components/dataset-form/components/distribution-section/distribution-details.tsx index c6c6524df..728b4a3c4 100644 --- a/apps/dataset-catalog/components/dataset-form/components/distribution-section/distribution-details.tsx +++ b/apps/dataset-catalog/components/dataset-form/components/distribution-section/distribution-details.tsx @@ -44,20 +44,20 @@ export const DistributionDetails = ({ distribution, searchEnv, referenceDataEnv, {distribution.downloadURL && distribution.downloadURL.length > 0 && (
- + {distribution?.downloadURL?.[0] ?? ''}
)} {distribution.mediaType && distribution.mediaType.length > 0 && (
- +
    {distribution?.mediaType?.map((uri, index) => (
  • {(selectedMediaTypes?.find((type) => type.uri === uri) ?? {}).code ?? uri} diff --git a/apps/dataset-catalog/components/dataset-form/components/information-model-section.tsx b/apps/dataset-catalog/components/dataset-form/components/information-model-section.tsx index 5d4213e65..fe98ac86c 100644 --- a/apps/dataset-catalog/components/dataset-form/components/information-model-section.tsx +++ b/apps/dataset-catalog/components/dataset-form/components/information-model-section.tsx @@ -18,7 +18,7 @@ interface Props { } export const InformationModelSection = ({ searchEnv }: Props) => { - const { setFieldValue, values, errors } = useFormikContext(); + const { setFieldValue, values } = useFormikContext(); const [searchTerm, setSearchTerm] = useState(''); const { data: informationModelSuggestions, isLoading: searching } = useSearchInformationModelsSuggestions( diff --git a/apps/dataset-catalog/components/dataset-form/components/qualified-attributions-section.tsx b/apps/dataset-catalog/components/dataset-form/components/qualified-attributions-section.tsx index d5615a13f..eb9d79ea6 100644 --- a/apps/dataset-catalog/components/dataset-form/components/qualified-attributions-section.tsx +++ b/apps/dataset-catalog/components/dataset-form/components/qualified-attributions-section.tsx @@ -11,7 +11,7 @@ export const QualifiedAttributionsSection = () => { const { setFieldValue, values } = useFormikContext(); const [searchTerm, setSearchTerm] = useState(''); - const { data: selectedEnheter, isLoading } = useSearchEnheterByOrgNmbs(values.qualifiedAttributions); + const { data: selectedEnheter } = useSearchEnheterByOrgNmbs(values.qualifiedAttributions); const { data: enheter, isLoading: searching } = useSearchEnheter(searchTerm); const debouncedSearch = useCallback( diff --git a/apps/dataset-catalog/components/details-page-columns/components/access-rights-details.tsx b/apps/dataset-catalog/components/details-page-columns/components/access-rights-details.tsx new file mode 100644 index 000000000..1265683e7 --- /dev/null +++ b/apps/dataset-catalog/components/details-page-columns/components/access-rights-details.tsx @@ -0,0 +1,92 @@ +import { AccessRights, Dataset, UriWithLabel } from '@catalog-frontend/types'; +import { getTranslateText, localization } from '@catalog-frontend/utils'; +import { Card, Label, Table, Tag } from '@digdir/designsystemet-react'; +import { useMemo } from 'react'; +import styles from '../details-columns.module.css'; +import _ from 'lodash'; + +type Props = { + dataset: Dataset; +}; + +export const AccessRightsDetails = ({ dataset }: Props) => { + const allLegalBases = useMemo( + () => [ + ...(dataset.legalBasisForRestriction ?? []).map((item, index) => ({ + uriWithLabel: item, + type: 'legalBasisForRestriction', + index: index, + })), + ...(dataset.legalBasisForProcessing ?? []).map((item, index) => ({ + uriWithLabel: item, + type: 'legalBasisForProcessing', + index: index, + })), + ...(dataset.legalBasisForAccess ?? []).map((item, index) => ({ + uriWithLabel: item, + type: 'legalBasisForAccess', + index: index, + })), + ], + [dataset.legalBasisForRestriction, dataset.legalBasisForProcessing, dataset.legalBasisForAccess], + ); + + const accessRightsOptions = useMemo( + () => [ + { value: AccessRights.PUBLIC, label: localization.datasetForm.accessRight.public }, + { value: AccessRights.RESTRICTED, label: localization.datasetForm.accessRight.restricted }, + { value: AccessRights.NON_PUBLIC, label: localization.datasetForm.accessRight.nonPublic }, + ], + [], + ); + + const hasNoFieldValues = (values: UriWithLabel) => { + if (!values) return true; + return _.isEmpty(_.trim(values.uri)) && _.isEmpty(_.pickBy(values.prefLabel, _.identity)); + }; + + return ( + <> + {dataset?.accessRights && ( +
    + + {dataset.accessRights?.uri && + accessRightsOptions.find((option) => option.value === dataset.accessRights?.uri)?.label} + + {dataset?.accessRights.uri !== AccessRights.PUBLIC && allLegalBases.length > 0 && ( + + + + {allLegalBases && allLegalBases?.length > 0 && !hasNoFieldValues(allLegalBases[0].uriWithLabel) && ( + + + + {localization.title} + {localization.link} + {localization.type} + + + + {allLegalBases.map( + (item, i) => + item?.uriWithLabel && ( + + {getTranslateText(item?.uriWithLabel.prefLabel)} + {item?.uriWithLabel.uri} + {localization.datasetForm.fieldLabel[item?.type]} + + ), + )} + +
    + )} +
    + )} +
    + )} + + ); +}; diff --git a/apps/dataset-catalog/components/details-page-columns/components/distribution-details.tsx b/apps/dataset-catalog/components/details-page-columns/components/distribution-details.tsx new file mode 100644 index 000000000..1307fae7e --- /dev/null +++ b/apps/dataset-catalog/components/details-page-columns/components/distribution-details.tsx @@ -0,0 +1,63 @@ +import { Distribution, ReferenceDataCode } from '@catalog-frontend/types'; +import { localization, getTranslateText } from '@catalog-frontend/utils'; +import { Card, Label, Tag } from '@digdir/designsystemet-react'; +import { DistributionDetails } from '../../dataset-form/components/distribution-section/distribution-details'; +import styles from '../details-columns.module.css'; +import { useSearchFileTypeByUri } from '../../../hooks/useReferenceDataSearch'; + +type Props = { + distribution: Partial; + searchEnv: string; + referenceDataEnv: string; + openLicenses: ReferenceDataCode[]; +}; + +export const DistributionDetailsCard = ({ distribution, searchEnv, referenceDataEnv, openLicenses }: Props) => { + const { data: formats } = useSearchFileTypeByUri(distribution.format, referenceDataEnv); + + return ( + +
    + {distribution?.title && ( +
    + +

    {getTranslateText(distribution?.title)}

    +
    + )} + + {distribution?.accessURL && ( +
    + +

    {distribution?.accessURL}

    +
    + )} + + {distribution?.format && ( +
    + +
  • + {distribution.format?.map((item: string) => { + const match = formats && formats.find((format: any) => format.uri === item); + return match ? ( + + {getTranslateText(match?.label) ?? item} + + ) : null; + })} +
  • +
+ )} +
+ + + ); +}; diff --git a/apps/dataset-catalog/components/details-page-columns/components/temporal-details.tsx b/apps/dataset-catalog/components/details-page-columns/components/temporal-details.tsx new file mode 100644 index 000000000..aefb85f5e --- /dev/null +++ b/apps/dataset-catalog/components/details-page-columns/components/temporal-details.tsx @@ -0,0 +1,37 @@ +import { DateRange } from '@catalog-frontend/types'; +import { localization } from '@catalog-frontend/utils'; +import { Table } from '@digdir/designsystemet-react'; + +type Props = { + temporal: DateRange[] | undefined; +}; + +export const TemporalDetails = ({ temporal }: Props) => { + return ( + <> + {temporal && ( + + + + {localization.from} + {localization.to} + + + + {temporal.map((item, index) => { + const startDate = item?.startDate ? new Date(item.startDate) : null; + const endDate = item?.endDate ? new Date(item.endDate) : null; + + return ( + + {startDate ? startDate.toLocaleDateString('no-NO') : ''} + {endDate ? endDate.toLocaleDateString('no-NO') : ''} + + ); + })} + +
+ )} + + ); +}; diff --git a/apps/dataset-catalog/components/details-page-columns/details-columns.module.css b/apps/dataset-catalog/components/details-page-columns/details-columns.module.css new file mode 100644 index 000000000..39229b0ef --- /dev/null +++ b/apps/dataset-catalog/components/details-page-columns/details-columns.module.css @@ -0,0 +1,28 @@ +.contactPoints { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.contactPoints > span { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.contactPoints > span > div { + display: flex; + align-items: center; +} + +.infoCardItems { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.list { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} diff --git a/apps/dataset-catalog/components/details-page-columns/details-page-left-column.tsx b/apps/dataset-catalog/components/details-page-columns/details-page-left-column.tsx new file mode 100644 index 000000000..3aa530862 --- /dev/null +++ b/apps/dataset-catalog/components/details-page-columns/details-page-left-column.tsx @@ -0,0 +1,381 @@ +import { InfoCard } from '@catalog-frontend/ui'; +import { localization, getTranslateText, capitalizeFirstLetter } from '@catalog-frontend/utils'; +import { Label, Paragraph, Table, Tag } from '@digdir/designsystemet-react'; +import _ from 'lodash'; +import { Dataset, DatasetSeries, ReferenceData } from '@catalog-frontend/types'; +import styles from './details-columns.module.css'; +import { useSearchAdministrativeUnitsByUri } from '../../hooks/useReferenceDataSearch'; +import { useSearchEnheterByOrgNmbs } from '../../hooks/useEnhetsregister'; +import { + useSearchConceptsByUri, + useSearchDatasetsByUri, + useSearchInformationModelsByUri, +} from '../../hooks/useSearchService'; +import relations from '../dataset-form/utils/relations.json'; +import { ReferenceDataTags } from '../reference-data-tags'; +import { UriWithLabelTable } from '../uri-with-label-table'; +import { DistributionDetailsCard } from './components/distribution-details'; +import { AccessRightsDetails } from './components/access-rights-details'; +import { TemporalDetails } from './components/temporal-details'; + +type Props = { + dataset: Dataset; + referenceDataEnv: string; + searchEnv: string; + referenceData: ReferenceData; + datasetSeries: DatasetSeries[]; +}; + +export const LeftColumn = ({ dataset, referenceDataEnv, searchEnv, referenceData, datasetSeries }: Props) => { + const getAllSpatialUris = dataset?.spatial?.reduce((acc, item) => { + return acc.concat(item?.uri ?? []); + }, [] as string[]); + + const { data: spatial } = useSearchAdministrativeUnitsByUri(getAllSpatialUris, referenceDataEnv); + + const { data: qualifiedAttributions } = useSearchEnheterByOrgNmbs(dataset?.qualifiedAttributions); + + const allReferenceUris = + dataset.references?.reduce((acc, ref) => { + if (ref?.source?.uri) acc.push(ref.source.uri); + return acc; + }, [] as string[]) ?? []; + + const { data: references } = useSearchDatasetsByUri(searchEnv, allReferenceUris); + + const allConceptUris = dataset.concepts?.map((item) => item.uri) ?? []; + const { data: concepts } = useSearchConceptsByUri(searchEnv, allConceptUris); + + const { data: informationModels } = useSearchInformationModelsByUri( + searchEnv, + dataset?.informationModelsFromFDK ?? [], + ); + + return ( + + {!_.isEmpty(dataset?.description) && ( + + {getTranslateText(dataset?.description)} + + )} + {dataset?.accessRights && ( + + + + )} + + {dataset.theme && ( + <> + +
  • + {dataset.theme?.map((item) => { + const matchedTheme = referenceData.dataThemes.find((los) => los.uri === item.uri); + return matchedTheme ? ( + + {getTranslateText(matchedTheme?.label)} + + ) : null; + })} +
  • +
    + +
  • + {dataset.theme?.map((item) => { + const matchedTheme = referenceData.losThemes.find((los) => los.uri === item.uri); + return matchedTheme ? ( + + {getTranslateText(matchedTheme?.name)} + + ) : null; + })} +
  • +
    + + )} + + {dataset?.distribution && ( + + {dataset.distribution.map((dist, index) => ( + + ))} + + )} + + {dataset?.sample && ( + + {dataset.sample.map((dist, index) => ( + + ))} + + )} + + {dataset?.language && ( + + {dataset.language + .map((lang) => { + const matchedLang = referenceData?.languages?.find((item) => item.uri === lang.uri); + return matchedLang ? getTranslateText(matchedLang.label) : null; + }) + .filter(Boolean) + .join(', ')} + + )} + {dataset?.spatial && ( + + {getTranslateText(dataset?.spatial?.map((area) => spatial?.find((item) => item.uri === area.uri)?.label))} + { + + } + + )} + + {dataset?.landingPage && ( + +
    + {dataset?.landingPage.map((item) => {item})} +
    +
    + )} + + {Array.isArray(dataset.temporal) && dataset.temporal.length > 0 && ( + + + + )} + + {dataset?.type && ( + + + + )} + + {dataset?.provenance && ( + + + + )} + + {(!_.isEmpty(dataset?.accrualPeriodicity) || !_.isEmpty(dataset?.hasCurrentnessAnnotation)) && ( + +
    + + + {!_.isEmpty(dataset?.hasAccuracyAnnotation) && ( +
    + + {getTranslateText(dataset?.hasCurrentnessAnnotation?.hasBody)} +
    + )} +
    +
    + )} + + {dataset?.conformsTo && ( + + + + )} + + {!_.isEmpty(dataset?.hasRelevanceAnnotation) && ( + + {getTranslateText(dataset?.hasRelevanceAnnotation?.hasBody)} + + )} + + {!_.isEmpty(dataset?.hasCompletenessAnnotation) && ( + + {getTranslateText(dataset?.hasCompletenessAnnotation?.hasBody)} + + )} + + {!_.isEmpty(dataset?.hasAccuracyAnnotation) && ( + + {getTranslateText(dataset?.hasAccuracyAnnotation?.hasBody)} + + )} + + {!_.isEmpty(dataset?.hasAvailabilityAnnotation) && ( + + {getTranslateText(dataset?.hasAvailabilityAnnotation?.hasBody)} + + )} + {dataset?.qualifiedAttributions && + dataset?.qualifiedAttributions.length > 0 && + !_.isEmpty(dataset?.qualifiedAttributions) && ( + +
  • + {dataset?.qualifiedAttributions?.map((org) => { + const match = + qualifiedAttributions && + qualifiedAttributions.find((attribution) => attribution.organisasjonsnummer === org); + return match ? ( + + {getTranslateText(match?.navn) ?? org} + + ) : null; + })} +
  • +
    + )} + {(!_.isEmpty(dataset.references) || !_.isEmpty(dataset.relations)) && ( + +
    + {dataset?.references && _.compact(dataset?.references).length > 0 && ( + + + + {localization.datasetForm.fieldLabel.relationType} + {localization.datasetForm.fieldLabel.dataset} + + + + {dataset?.references && + dataset?.references.map((ref, index) => ( + + + {getTranslateText(relations.find((rel) => rel.code === ref?.referenceType?.code)?.label) ?? + ref?.referenceType?.code} + + + {getTranslateText(references?.find((item) => item.uri === ref?.source?.uri)?.title) ?? + ref?.source?.uri} + + + ))} + +
    + )} + + {dataset?.inSeries && ( +
    + + {(() => { + const matchedType = datasetSeries.find((item) => item.id === dataset.inSeries); + return matchedType ? ( + <> + + {capitalizeFirstLetter(getTranslateText(matchedType.title).toString())} + + + ) : ( + {dataset.inSeries} + ); + })()} +
    + )} + + {dataset?.relations && ( +
    + + + +
    + )} +
    +
    + )} + {!_.isEmpty(dataset.concepts) && ( + +
  • + {dataset.concepts?.map((item) => { + const match = concepts && concepts.find((concept) => concept.uri === item.uri); + return match ? ( + + {capitalizeFirstLetter(getTranslateText(match?.title).toString()) ?? item.uri} + + ) : null; + })} +
  • +
    + )} + {!_.isEmpty(dataset?.keyword) && ( + +
  • + {dataset.keyword?.map((item, index) => { + return item ? ( + + {capitalizeFirstLetter(getTranslateText(item).toString()) ?? item.uri} + + ) : null; + })} +
  • +
    + )} + + {!_.isEmpty(dataset?.informationModelsFromFDK) && ( + +
  • + {dataset.informationModelsFromFDK?.map((item) => { + const match = informationModels && informationModels.find((model) => model.uri === item); + return match ? ( + + {capitalizeFirstLetter(getTranslateText(match?.title).toString()) ?? item} + + ) : null; + })} +
  • +
    + )} + + {!_.isEmpty(dataset?.informationModel) && ( + + + + )} +
    + ); +}; diff --git a/apps/dataset-catalog/components/details-page-columns/details-page-right-column.tsx b/apps/dataset-catalog/components/details-page-columns/details-page-right-column.tsx new file mode 100644 index 000000000..1952d0dd8 --- /dev/null +++ b/apps/dataset-catalog/components/details-page-columns/details-page-right-column.tsx @@ -0,0 +1,106 @@ +import { InfoCard } from '@catalog-frontend/ui'; +import { localization } from '@catalog-frontend/utils'; +import { EnvelopeClosedIcon, PhoneIcon, LinkIcon } from '@navikt/aksel-icons'; +import _ from 'lodash'; +import PublishSwitch from '../publish-switch'; +import { Dataset, PublicationStatus } from '@catalog-frontend/types'; +import styles from './details-columns.module.css'; + +type Props = { + dataset: Dataset; + hasWritePermission: boolean; +}; + +export const RightColumn = ({ dataset, hasWritePermission }: Props) => { + const published = dataset.registrationStatus === PublicationStatus.PUBLISH; + + return ( + + + {dataset?.id} + + + + + + {published ? localization.publicationState.publishedInFDK : localization.publicationState.unpublished} + + + {dataset?.issued && ( + + {new Date(dataset.issued).toLocaleDateString('no-NO')} + + )} + + {dataset?.contactPoint && !_.isEmpty(dataset?.contactPoint[0]) && ( + +
    + {dataset?.contactPoint[0].email && ( + +
    + +
    + + {dataset?.contactPoint[0].email} +
    + )} + {dataset?.contactPoint[0].hasTelephone && ( + +
    + +
    + {dataset?.contactPoint[0].hasTelephone} +
    + )} + + {dataset?.contactPoint[0].hasURL && ( + +
    + +
    + + {dataset?.contactPoint[0].hasURL} +
    + )} + {dataset?.contactPoint[0].organizationUnit && ( + +
    + +
    +
    {dataset?.contactPoint[0].organizationUnit}
    +
    + )} +
    +
    + )} + + {dataset?.modified && ( + + {new Date(dataset.modified).toLocaleDateString('no-NO')} + + )} +
    + ); +}; diff --git a/apps/dataset-catalog/components/publish-switch/index.tsx b/apps/dataset-catalog/components/publish-switch/index.tsx new file mode 100644 index 000000000..d35fcd562 --- /dev/null +++ b/apps/dataset-catalog/components/publish-switch/index.tsx @@ -0,0 +1,61 @@ +'use client'; +import { localization } from '@catalog-frontend/utils'; +import { Switch } from '@digdir/designsystemet-react'; +import styles from './publish-switch.module.css'; +import { updateDataset } from '../../app/actions/actions'; +import { Dataset, PublicationStatus } from '@catalog-frontend/types'; + +type Props = { + catalogId: string; + dataset: Dataset; + disabled: boolean; +}; + +export const PublishSwitch = ({ catalogId, dataset, disabled }: Props) => { + const handlePublishDataset = async () => { + if (dataset.registrationStatus !== PublicationStatus.PUBLISH) { + if (window.confirm(localization.datasetForm.alert.confirmPublish)) { + const publishedDataset = { + ...dataset, + registrationStatus: PublicationStatus.PUBLISH, + }; + try { + await updateDataset(catalogId, dataset, publishedDataset); + } catch (error) { + window.alert(error); + } + } + } + + if (dataset.registrationStatus === PublicationStatus.PUBLISH) { + if (window.confirm(localization.datasetForm.alert.confirmUnpublish)) { + const approvedDataset = { + ...dataset, + registrationStatus: PublicationStatus.APPROVE, + }; + try { + await updateDataset(catalogId, dataset, approvedDataset); + } catch (error) { + window.alert(error); + } + } + } + }; + + return ( + <> + handlePublishDataset()} + checked={dataset?.registrationStatus === PublicationStatus.PUBLISH} + disabled={disabled || dataset.registrationStatus === PublicationStatus.DRAFT} + > + {localization.publicationState.published} + + + ); +}; + +export default PublishSwitch; diff --git a/apps/dataset-catalog/components/publish-switch/publish-switch.module.css b/apps/dataset-catalog/components/publish-switch/publish-switch.module.css new file mode 100644 index 000000000..1ed520d34 --- /dev/null +++ b/apps/dataset-catalog/components/publish-switch/publish-switch.module.css @@ -0,0 +1,4 @@ +.center > label { + display: flex; + align-items: center; +} diff --git a/apps/dataset-catalog/components/reference-data-tags/index.tsx b/apps/dataset-catalog/components/reference-data-tags/index.tsx new file mode 100644 index 000000000..603a1f888 --- /dev/null +++ b/apps/dataset-catalog/components/reference-data-tags/index.tsx @@ -0,0 +1,37 @@ +import { UriWithLabel, ReferenceDataCode } from '@catalog-frontend/types'; +import { capitalizeFirstLetter, getTranslateText } from '@catalog-frontend/utils'; +import { Tag } from '@digdir/designsystemet-react'; +import styles from './referenceDataTags.module.css'; + +type Props = { + values: UriWithLabel[] | string | undefined; + data: ReferenceDataCode[] | undefined; +}; + +export const ReferenceDataTags = ({ values, data }: Props) => { + if (!values) { + return null; + } + + const dataMap = new Map(data?.map((item) => [item.uri, item.label])); + + const renderTag = (uri: string) => { + const label = dataMap.get(uri); + const displayText = capitalizeFirstLetter(getTranslateText(label)?.toString() ?? uri); + return ( + + {displayText} + + ); + }; + + if (typeof values === 'string') { + return <>{renderTag(values)}; + } + + return
      {values.filter((item) => item.uri).map((item) => renderTag(item.uri!))}
    ; +}; diff --git a/apps/dataset-catalog/components/reference-data-tags/referenceDataTags.module.css b/apps/dataset-catalog/components/reference-data-tags/referenceDataTags.module.css new file mode 100644 index 000000000..f163d6faa --- /dev/null +++ b/apps/dataset-catalog/components/reference-data-tags/referenceDataTags.module.css @@ -0,0 +1,5 @@ +.list { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} diff --git a/apps/dataset-catalog/components/uri-with-label-table/index.tsx b/apps/dataset-catalog/components/uri-with-label-table/index.tsx new file mode 100644 index 000000000..40b89d459 --- /dev/null +++ b/apps/dataset-catalog/components/uri-with-label-table/index.tsx @@ -0,0 +1,30 @@ +import { UriWithLabel } from '@catalog-frontend/types'; +import { localization, getTranslateText } from '@catalog-frontend/utils'; +import { Table } from '@digdir/designsystemet-react'; + +type Props = { + values?: UriWithLabel[]; +}; + +export const UriWithLabelTable = ({ values = [] }: Props) => { + if (values.length === 0) return null; + + return ( + + + + {localization.title} + {localization.link} + + + + {values.map((item) => ( + + {getTranslateText(item.prefLabel)} + {item.uri} + + ))} + +
    + ); +}; diff --git a/apps/dataset-catalog/hooks/useReferenceDataSearch.ts b/apps/dataset-catalog/hooks/useReferenceDataSearch.ts index 7987a4ae8..8fd56d888 100644 --- a/apps/dataset-catalog/hooks/useReferenceDataSearch.ts +++ b/apps/dataset-catalog/hooks/useReferenceDataSearch.ts @@ -86,7 +86,7 @@ export const useSearchFileTypeByUri = (uriList: string[] | undefined, envVariabl const data: ReferenceDataCode[] = await searchReferenceDataByUri(uriList, envVariable, [ SearchAlternative.EuFileTypes, ]); - return data; + return data as ReferenceDataCode[]; }, enabled: Array.isArray(uriList) && uriList.length > 0, }); diff --git a/libs/data-access/src/index.ts b/libs/data-access/src/index.ts index 69d5b73a7..b60223f9b 100644 --- a/libs/data-access/src/index.ts +++ b/libs/data-access/src/index.ts @@ -17,6 +17,6 @@ export * from './lib/statuses/api'; export * from './lib/datasets/api'; export * from './lib/data-service/api'; export * from './lib/records-of-processing-activities/api'; -export * from './lib/strapi/generated/graphql'; +export * as Strapi from './lib/strapi/generated/graphql'; export * from './lib/strapi/service-messages'; export * from './lib/enhetsregisteret'; diff --git a/libs/types/src/lib/dataset.ts b/libs/types/src/lib/dataset.ts index 5cb091767..b96d70bee 100644 --- a/libs/types/src/lib/dataset.ts +++ b/libs/types/src/lib/dataset.ts @@ -18,10 +18,9 @@ export type DatasetToBeCreated = { legalBasisForAccess?: UriWithLabel[]; legalBasisForRestriction?: UriWithLabel[]; landingPage?: string[]; - theme?: { uri: string }[]; + theme?: { uri: string }[]; // Both euTheme and losTheme type?: string; keyword?: { [key: string]: string }[]; - keywordList?: { nb?: string[]; nn?: string[]; en?: string[] }; concepts?: [{ uri: string }]; provenance?: ReferenceDataCode; accrualPeriodicity?: ReferenceDataCode; @@ -39,18 +38,19 @@ export type DatasetToBeCreated = { informationModel?: UriWithLabel[]; informationModelsFromFDK?: string[]; qualifiedAttributions?: string[]; - sample?: Sample[]; + sample?: Partial[]; references?: Reference[]; relations?: UriWithLabel[]; inSeries?: string; distribution?: Distribution[]; contactPoint: DatasetContactPoint[]; - // Arrays of uris used as helper values for Formik. These properties is not part of the db object. + // Arrays of URIs used as helper values for Formik. These properties are not part of the database object. losThemeList?: string[]; euThemeList?: string[]; conceptList?: string[]; spatialList?: string[]; languageList?: string[]; + keywordList?: { nb?: string[]; nn?: string[]; en?: string[] }; }; export type UriWithLabel = { @@ -63,13 +63,6 @@ export type DateRange = { endDate?: string; }; -export type Sample = { - downloadURL?: string[]; - accessURL?: string[]; - format?: string[]; - mediaType?: string[]; - description?: LocalizedStrings; -}; export type Reference = { referenceType: { code: string }; source: { uri: string }; diff --git a/libs/ui/src/lib/info-card/info-card-item.tsx b/libs/ui/src/lib/info-card/info-card-item.tsx index f7767ce35..4a11838b7 100644 --- a/libs/ui/src/lib/info-card/info-card-item.tsx +++ b/libs/ui/src/lib/info-card/info-card-item.tsx @@ -1,6 +1,8 @@ import cn from 'classnames'; import React, { forwardRef } from 'react'; import classes from './info-card.module.css'; +import { HelpMarkdown } from '../help-markdown'; +import { Label } from '@digdir/designsystemet-react'; export const labelColor = ['neutral', 'light'] as const; type LabelColor = (typeof labelColor)[number]; @@ -23,12 +25,18 @@ export interface InfoCardItemProps extends React.HTMLAttributes * Should include one Accordion.Header and one Accordion.Content */ children: React.ReactNode; + + /** + * Helptext + **/ + + helpText?: string; } export type InfoCardItemType = React.ForwardRefExoticComponent>; const InfoCardItem: InfoCardItemType = forwardRef( - ({ label, labelColor = 'neutral', labelLevel = 3, children, className, ...rest }, ref) => { + ({ label, labelColor = 'neutral', labelLevel = 3, children, className, helpText, ...rest }, ref) => { const HeadingTag = `h${labelLevel}` as React.ElementType; return ( @@ -38,8 +46,25 @@ const InfoCardItem: InfoCardItemType = forwardRef( {...rest} >
    - {label && {label}} - {children} + + {label && ( + + )} + {helpText && ( + + {helpText} + + )} + +
    {children}
    ); diff --git a/libs/ui/src/lib/info-card/info-card.module.css b/libs/ui/src/lib/info-card/info-card.module.css index 1f1686933..b398d20e7 100644 --- a/libs/ui/src/lib/info-card/info-card.module.css +++ b/libs/ui/src/lib/info-card/info-card.module.css @@ -33,14 +33,20 @@ padding-top: 37px; } -.label { - font-size: 14px; - font-weight: 500; - padding-bottom: 8px; -} - .light, .light a { font-weight: 400; color: #6c737a; } + +.set { + display: flex; + gap: 0.5rem; + align-items: center; + padding-bottom: 0.5rem; +} + +.wrap { + display: block; + word-break: break-word; +} diff --git a/libs/ui/src/lib/info-card/info-card.spec.tsx b/libs/ui/src/lib/info-card/info-card.spec.tsx index d042bb9fe..0cc0d46bb 100644 --- a/libs/ui/src/lib/info-card/info-card.spec.tsx +++ b/libs/ui/src/lib/info-card/info-card.spec.tsx @@ -1,8 +1,9 @@ -import React from 'react'; import { render } from '@testing-library/react'; - +import React from 'react'; import InfoCard from './info-card'; +jest.mock('react-markdown', () => (props) =>
    ); + describe('CardList', () => { const cardList = ( diff --git a/libs/ui/src/lib/service-messages/index.tsx b/libs/ui/src/lib/service-messages/index.tsx index 70db923c7..bfc017149 100644 --- a/libs/ui/src/lib/service-messages/index.tsx +++ b/libs/ui/src/lib/service-messages/index.tsx @@ -1,4 +1,4 @@ -import { ServiceMessage } from '@catalog-frontend/data-access'; +import { Strapi } from '@catalog-frontend/data-access'; import { Alert, Heading, Link, Paragraph } from '@digdir/designsystemet-react'; import styles from './service-messages.module.css'; import { localization } from '@catalog-frontend/utils'; @@ -6,7 +6,7 @@ import { localization } from '@catalog-frontend/utils'; type Severity = 'success' | 'danger' | 'info' | 'warning'; interface ServiceMessagesProps { - serviceMessages: ServiceMessage[]; + serviceMessages: Strapi.ServiceMessage[]; } const isValidDateRange = (validFrom: string, validTo: string): boolean => { diff --git a/libs/utils/src/lib/language/dataset.form.nb.ts b/libs/utils/src/lib/language/dataset.form.nb.ts index 23d9783b1..3ed57fc80 100644 --- a/libs/utils/src/lib/language/dataset.form.nb.ts +++ b/libs/utils/src/lib/language/dataset.form.nb.ts @@ -14,7 +14,7 @@ export const datasetFormNb = { - **Skjermingshjemmel**: Referanse til lov eller forskrift som begrenser deling av datasettet (f.eks. offentlighetsloven, sikkerhetsloven). - **Behandlingsgrunnlag**: Lov, forskrift, samtykke eller nødvendighetsvurdering som grunnlag for behandling av personopplysninger. - **Utleveringshjemmel**: Henvisning til lov eller forskrift som gir offentlig virksomhet rett eller plikt til å utlevere opplysninger til private eller juridiske personer.`, - releaseDate: 'Dato for når innholdet i datasettet ble eller blir tilgjengeliggjort.', + issued: 'Dato for når innholdet i datasettet ble eller blir tilgjengeliggjort.', euTheme: 'Velg ett eller flere hovedtema som beskriver innholdet i datasettet. Listen er fra EUs kontrollerte vokabular.', theme: @@ -67,6 +67,8 @@ export const datasetFormNb = { 'Emneord eller tagger beskriver sentralt innhold i datasettet, spesielt når begrepsdefinisjoner mangler eller når det brukes ord som folk ofte søker etter, men som ikke formelt er knyttet til datasettet.', concept: 'Søk etter begrep som er publisert i Data.norge.no og velg fra nedtrekkslisten. Her legger du inn de begrepene som brukes i datasettet. Begrepene brukes til å si noe om hva informasjonen i datasettet betyr. Ved å henvise til gjennomarbeidede definisjoner som virksomheten selv er ansvarlig for å vedlikeholde, sikrer vi at det er tydelig hvordan et begrep brukt i datasettet skal forstås og at denne forståelsen til en hver tid er riktig og oppdatert.', + publish: + 'Publiser datasettbeskrivelsen til Data.Norge.no. Den må være godkjent for å kunne publiseres. En beskrivelse kan ikke slettes så lenge den er publisert.', }, heading: { about: 'Om datasettet', @@ -84,9 +86,9 @@ export const datasetFormNb = { nonPublic: 'Ikke-allmenn tilgang', }, fieldLabel: { - theme: 'Velg LOS-tema(er)', - euTheme: 'Velg hovedtema(er)', - concept: 'Legg inn begreper', + theme: 'LOS-tema(er)', + euTheme: 'Hovedtema(er)', + concept: 'Begreper', mediaType: 'Mediatyper', format: 'Format', accessURL: 'Tilgangslenke', @@ -107,7 +109,7 @@ export const datasetFormNb = { keyword: 'Emneord', distribution: 'Distribusjon', distributions: 'Distribusjoner', - releaseDate: 'Utgivelsesdato', + issued: 'Utgivelsesdato', language: 'Språk', spatial: 'Dekningsområde', temporal: 'Tidsrom', @@ -129,10 +131,13 @@ export const datasetFormNb = { informationModelsFromFDK: 'Informasjonsmodell fra Data.norge.no', informationModel: 'Informasjonsmodell fra andre kilder', sample: 'Eksempeldata', + datasetID: 'Datasett-ID', }, alert: { confirmDelete: 'Er du sikker på at du vil slette datasettbeskrivelsen?', formError: 'Du har feil i skjemaet. Rett opp i disse før du kan lagre.', + confirmUnpublish: 'Er du sikker på at du vil avpublisere datasettbeskrivelsen?', + confirmPublish: 'Er du sikkert på at du vil publisere datasettbeskrivelsen?', }, validation: { title: 'Tittelen må være minst 3 karakterer lang.', @@ -156,4 +161,9 @@ export const datasetFormNb = { errors: { qualifiedAttributions: 'Kunne ikke hente enheter.', }, + status: { + DRAFT: 'Utkast', + PUBLISH: 'Publisert', + APPROVE: 'Godkjent', + }, }; diff --git a/libs/utils/src/lib/language/nb.ts b/libs/utils/src/lib/language/nb.ts index 237f6b276..33c2a1e4f 100644 --- a/libs/utils/src/lib/language/nb.ts +++ b/libs/utils/src/lib/language/nb.ts @@ -68,6 +68,7 @@ export const nb = { noName: 'Uten navn', choose: 'Velg', relation: 'Relasjon', + relations: 'Relasjoner', serviceMessageError: 'Kunne ikke laste inn tjenestemeldinger. Vennligst prøv igjen senere.', serviceMessageSeeMore: 'Se detaljert driftsmelding for mer informasjon.', @@ -422,12 +423,12 @@ export const nb = { publicationState: { confirmPublish: 'Er du sikker på at du vil publisere begrepet?', - description: 'Publiseringstilstand forteller om et begrep er publisert i Felles Datakatalog eller ikke.', + description: 'Publiseringstilstand forteller om et begrep er publisert på Data.Norge.no eller ikke.', published: 'Publisert', - publishedInFDK: 'Publisert i Felles Datakatalog', + publishedInFDK: 'Publisert på Data.Norge.no', state: 'Publiseringstilstand', unpublished: 'Ikke publisert', - unpublishedInFDK: 'Ikke publisert i Felles Datakatalog', + unpublishedInFDK: 'Ikke publisert på Data.Norge.no', }, validation: {