From 0c27fca1a6ac2b2c7d9de10267b9fc5c15a69e07 Mon Sep 17 00:00:00 2001 From: hegeaal Date: Wed, 2 Oct 2024 11:31:20 +0200 Subject: [PATCH] feat: add tema section to dataset form --- apps/dataset-catalog/app/actions/actions.ts | 50 ++++- .../datasets/datasets-page-client.tsx | 4 +- .../catalogs/[catalogId]/datasets/page.tsx | 2 +- .../app/context/themes/index.tsx | 49 +++++ apps/dataset-catalog/app/layout.tsx | 25 +-- .../dataset-form-access-rights.section.tsx | 2 +- .../dataset-form-tema-section.tsx | 184 ++++++++++++++++++ .../dataset-form/dataset-form.module.css | 16 ++ .../dataset-form/dataset-initial-values.tsx | 4 + .../components/dataset-form/index.tsx | 5 + .../dataset-form/validation-schema.tsx | 3 + libs/data-access/src/index.ts | 1 + libs/data-access/src/lib/themes/api/index.tsx | 21 ++ libs/types/src/index.ts | 1 + libs/types/src/lib/dataset.ts | 3 + libs/types/src/lib/theme.ts | 24 +++ .../utils/src/lib/language/dataset.form.nb.ts | 7 + 17 files changed, 383 insertions(+), 18 deletions(-) create mode 100644 apps/dataset-catalog/app/context/themes/index.tsx create mode 100644 apps/dataset-catalog/components/dataset-form/dataset-form-tema-section.tsx create mode 100644 libs/data-access/src/lib/themes/api/index.tsx create mode 100644 libs/types/src/lib/theme.ts diff --git a/apps/dataset-catalog/app/actions/actions.ts b/apps/dataset-catalog/app/actions/actions.ts index 564d456b1..96abe0a59 100644 --- a/apps/dataset-catalog/app/actions/actions.ts +++ b/apps/dataset-catalog/app/actions/actions.ts @@ -6,6 +6,8 @@ import { getById, postDataset, updateDataset as update, + getLosThemes as losThemes, + getDataThemes as dataThemes, } from '@catalog-frontend/data-access'; import { Dataset, DatasetToBeCreated } from '@catalog-frontend/types'; import { getValidSession, localization, removeEmptyValues } from '@catalog-frontend/utils'; @@ -36,13 +38,25 @@ export async function getDatasetById(catalogId: string, datasetId: string): Prom return jsonResponse; } +const convertListToObjectStructure = (uriList: string[]) => { + return uriList.map((uri) => ({ uri })); +}; + export async function createDataset(values: DatasetToBeCreated, catalogId: string) { - const newDataset = removeEmptyValues(values); + const newDataset = { + ...values, + theme: [ + ...(values.losThemeList ? convertListToObjectStructure(values.losThemeList) : []), + ...(values.euThemeList ? convertListToObjectStructure(values.euThemeList) : []), + ], + }; + const datasetNoEmptyValues = removeEmptyValues(newDataset); + const session = await getValidSession(); let success = false; let datasetId = undefined; try { - const response = await postDataset(newDataset, catalogId, `${session?.accessToken}`); + const response = await postDataset(datasetNoEmptyValues, catalogId, `${session?.accessToken}`); if (response.status !== 201) { throw new Error(); } @@ -79,7 +93,15 @@ export async function deleteDataset(catalogId: string, datasetId: string) { } export async function updateDataset(catalogId: string, initialDataset: Dataset, values: Dataset) { - const diff = compare(initialDataset, removeEmptyValues(values)); + const updatedDataset = removeEmptyValues({ + ...values, + theme: [ + ...(values.losThemeList ? convertListToObjectStructure(values.losThemeList) : []), + ...(values.euThemeList ? convertListToObjectStructure(values.euThemeList) : []), + ], + }); + + const diff = compare(initialDataset, updatedDataset); if (diff.length === 0) { throw new Error(localization.alert.noChanges); @@ -105,3 +127,25 @@ export async function updateDataset(catalogId: string, initialDataset: Dataset, redirect(`/catalogs/${catalogId}/datasets/${initialDataset.id}`); } } + +export async function getLosThemes() { + const session = await getValidSession(); + + const response = await losThemes(`${session?.accessToken}`); + if (response.status !== 200) { + throw new Error('getLosThemes failed with response code ' + response.status); + } + const jsonResponse = await response.json(); + return jsonResponse.losNodes; +} + +export async function getDataThemes() { + const session = await getValidSession(); + + const response = await dataThemes(`${session?.accessToken}`); + if (response.status !== 200) { + throw new Error('getDataThemes failed with response code ' + response.status); + } + const jsonResponse = await response.json(); + return jsonResponse.dataThemes; +} diff --git a/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/datasets-page-client.tsx b/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/datasets-page-client.tsx index be8dd4333..e99f6126c 100644 --- a/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/datasets-page-client.tsx +++ b/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/datasets-page-client.tsx @@ -90,9 +90,9 @@ const DatasetsPageClient = ({ datasets, catalogId }: Props) => {
- Legg til... + {`${localization.add}...`} diff --git a/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/page.tsx b/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/page.tsx index 2ebd91e8e..da316cac9 100644 --- a/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/page.tsx +++ b/apps/dataset-catalog/app/catalogs/[catalogId]/datasets/page.tsx @@ -18,7 +18,7 @@ export default async function DatasetSearchHitsPage({ params }: Params) { const datasets: Dataset[] = await getDatasets(catalogId); const organization: Organization = await getOrganization(catalogId).then((res) => res.json()); - const hasWritePermission = await hasOrganizationWritePermission(session.accessToken, catalogId); + const hasWritePermission = hasOrganizationWritePermission(session.accessToken, catalogId); const breadcrumbList = [ { diff --git a/apps/dataset-catalog/app/context/themes/index.tsx b/apps/dataset-catalog/app/context/themes/index.tsx new file mode 100644 index 000000000..99df3417b --- /dev/null +++ b/apps/dataset-catalog/app/context/themes/index.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { createContext, useContext, useEffect, useState } from 'react'; +import { getDataThemes, getLosThemes } from '../../actions/actions'; +import { DataTheme, LosTheme } from '@catalog-frontend/types'; + +interface ThemesContextProps { + losThemes: LosTheme[] | undefined; + dataThemes: DataTheme[] | undefined; + loading: boolean; +} + +const ThemesContext = createContext(undefined); +ThemesContext.displayName = 'ThemesContext'; + +const ThemesProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [losThemes, setLosThemes] = useState(undefined); + const [dataThemes, setDataThemes] = useState(undefined); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchThemes = async () => { + try { + const los = await getLosThemes(); + const data: DataTheme[] = await getDataThemes(); + setLosThemes(los.flat()); + setDataThemes(data); + } catch (error) { + console.error('Failed to fetch themes:', error); + } finally { + setLoading(false); + } + }; + + fetchThemes(); + }, []); + + return {children}; +}; + +const useThemes = () => { + const context = useContext(ThemesContext); + if (!context) { + throw new Error('useThemes must be used within a ThemesProvider'); + } + return context; +}; + +export { ThemesProvider, useThemes }; diff --git a/apps/dataset-catalog/app/layout.tsx b/apps/dataset-catalog/app/layout.tsx index c3992bee7..7954a35dc 100644 --- a/apps/dataset-catalog/app/layout.tsx +++ b/apps/dataset-catalog/app/layout.tsx @@ -2,6 +2,7 @@ import { Layout, NextAuthProvider } from '@catalog-frontend/ui'; import { localization } from '@catalog-frontend/utils'; import { Metadata } from 'next'; import { Inter } from 'next/font/google'; +import { ThemesProvider } from './context/themes'; export const metadata: Metadata = { title: localization.catalogType.dataset, @@ -15,17 +16,19 @@ const RootLayout = ({ children }: { children: React.ReactNode }) => { - - {children} - + + + {children} + + diff --git a/apps/dataset-catalog/components/dataset-form/dataset-form-access-rights.section.tsx b/apps/dataset-catalog/components/dataset-form/dataset-form-access-rights.section.tsx index 637365e5e..cdedc33c1 100644 --- a/apps/dataset-catalog/components/dataset-form/dataset-form-access-rights.section.tsx +++ b/apps/dataset-catalog/components/dataset-form/dataset-form-access-rights.section.tsx @@ -31,7 +31,7 @@ export const AccessRightsSection = ({ errors, values }: AccessRightsSectionProps label={ } diff --git a/apps/dataset-catalog/components/dataset-form/dataset-form-tema-section.tsx b/apps/dataset-catalog/components/dataset-form/dataset-form-tema-section.tsx new file mode 100644 index 000000000..ecb87b9ac --- /dev/null +++ b/apps/dataset-catalog/components/dataset-form/dataset-form-tema-section.tsx @@ -0,0 +1,184 @@ +import { Dataset, Option } from '@catalog-frontend/types'; +import { FormContainer, TitleWithTag } from '@catalog-frontend/ui'; +import { useThemes } from '../../app/context/themes/index'; +import { Chip, Combobox, Spinner } from '@digdir/designsystemet-react'; +import { getTranslateText, localization } from '@catalog-frontend/utils'; +import { FormikErrors, useFormikContext } from 'formik'; +import styles from './dataset-form.module.css'; + +type Props = { + errors: FormikErrors; + values: Dataset; +}; + +export const TemaSection = ({ errors, values }: Props) => { + const { losThemes, dataThemes, loading } = useThemes(); + const { setFieldValue, values: formikValues } = useFormikContext(); + + const getNameFromLosPath = (path: string): string | string[] => { + const obj = losThemes?.find((obj) => obj.losPaths.includes(path)); + return obj ? getTranslateText(obj.name) : []; + }; + + const getParentNames = (inputPaths: string[]): string => { + const results: string[] = []; + + inputPaths.forEach((path) => { + const parts = path.split('/').slice(0, -1); + const parentPath = parts.slice(0, -1).join('/'); + const childPath = parts.join('/'); + + const parentName = getNameFromLosPath(parentPath); + const childName = getNameFromLosPath(childPath); + + const formattedResult = `${parentName} - ${childName}`; + results.push(formattedResult); + }); + + return `${localization.datasetForm.helptext.parentTheme}: ${results.join('; ')}`; + }; + + const containsFilter = (inputValue: string, option: Option): boolean => { + return option.label.toLowerCase().includes(inputValue.toLowerCase()); + }; + + const handleLosValueChange = (newValues: string[]) => { + setFieldValue('losThemeList', newValues); + }; + + const handleEuValueChange = (newValues: string[]) => { + setFieldValue('euThemeList', newValues); + }; + + const handleRemoveLosChip = (valueToRemove: string) => { + const newValues = formikValues.losThemeList && formikValues.losThemeList.filter((value) => value !== valueToRemove); + setFieldValue('losThemeList', newValues); + }; + + const handleRemoveEuChip = (valueToRemove: string) => { + const newValues = formikValues.euThemeList && formikValues.euThemeList.filter((value) => value !== valueToRemove); + setFieldValue('euThemeList', newValues); + }; + + return ( + + + <> +
+ + + {localization.search.noHits} + {losThemes?.map((theme) => ( + + {getTranslateText(theme.name)} + + ))} + +
+
+ {loading ? ( +
+ +
+ ) : ( + + {formikValues?.losThemeList && + formikValues.losThemeList.map((value) => { + const theme = losThemes?.find((theme) => theme.uri === value); + return ( + handleRemoveLosChip(value)} + > + {theme ? getTranslateText(theme.name) : ''} + + ); + })} + + )} +
+ + + <> +
+ + + + {localization.search.noHits} + {dataThemes?.map((eutheme) => ( + + {getTranslateText(eutheme.label)} + + ))} + +
+
+ {loading ? ( +
+ +
+ ) : ( + + {formikValues?.euThemeList && + formikValues.euThemeList.map((value) => { + const theme = dataThemes?.find((theme) => theme.uri === value); + return ( + handleRemoveEuChip(value)} + > + {theme ? getTranslateText(theme.label) : ''} + + ); + })} + + )} +
+ +
+ ); +}; + +export default TemaSection; diff --git a/apps/dataset-catalog/components/dataset-form/dataset-form.module.css b/apps/dataset-catalog/components/dataset-form/dataset-form.module.css index e69de29bb..42224264b 100644 --- a/apps/dataset-catalog/components/dataset-form/dataset-form.module.css +++ b/apps/dataset-catalog/components/dataset-form/dataset-form.module.css @@ -0,0 +1,16 @@ +.combobox { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.chip > ul { + display: flex; + flex-wrap: wrap; +} + +.spinner { + display: flex; + justify-content: center; + align-items: center; +} diff --git a/apps/dataset-catalog/components/dataset-form/dataset-initial-values.tsx b/apps/dataset-catalog/components/dataset-form/dataset-initial-values.tsx index 8b16b07da..b0c1581da 100644 --- a/apps/dataset-catalog/components/dataset-form/dataset-initial-values.tsx +++ b/apps/dataset-catalog/components/dataset-form/dataset-initial-values.tsx @@ -24,6 +24,8 @@ export const datasetTemplate = (dataset: Dataset): Dataset => { dataset.landingPage && dataset?.landingPage?.length > 0 && dataset.landingPage.every((page) => page !== null) ? dataset.landingPage : [''], + losThemeList: dataset.theme ? dataset.theme.filter((t) => t.uri.includes('/los/')).map((t) => t.uri) : [], + euThemeList: dataset.theme ? dataset.theme.filter((t) => t.uri.includes('/data-theme/')).map((t) => t.uri) : [], }; }; @@ -45,5 +47,7 @@ export const datasetToBeCreatedTemplate = (): DatasetToBeCreated => { legalBasisForAccess: [{ uri: '', prefLabel: { nb: '' } }], legalBasisForProcessing: [{ uri: '', prefLabel: { nb: '' } }], legalBasisForRestriction: [{ uri: '', prefLabel: { nb: '' } }], + losThemeList: [], + euThemeList: [], }; }; diff --git a/apps/dataset-catalog/components/dataset-form/index.tsx b/apps/dataset-catalog/components/dataset-form/index.tsx index 9e176764f..be6b4f4ea 100644 --- a/apps/dataset-catalog/components/dataset-form/index.tsx +++ b/apps/dataset-catalog/components/dataset-form/index.tsx @@ -11,6 +11,7 @@ import { useState } from 'react'; import { datasetValidationSchema } from './validation-schema'; import { TitleSection } from './dataset-from-title-section'; import { AccessRightsSection } from './dataset-form-access-rights.section'; +import TemaSection from './dataset-form-tema-section'; type Props = { initialValues: DatasetToBeCreated | Dataset; @@ -92,6 +93,10 @@ export const DatasetForm = ({ initialValues, submitType }: Props) => { values={values} errors={errors} /> +
diff --git a/apps/dataset-catalog/components/dataset-form/validation-schema.tsx b/apps/dataset-catalog/components/dataset-form/validation-schema.tsx index 8b8fb398b..9a41e1744 100644 --- a/apps/dataset-catalog/components/dataset-form/validation-schema.tsx +++ b/apps/dataset-catalog/components/dataset-form/validation-schema.tsx @@ -39,4 +39,7 @@ export const datasetValidationSchema = Yup.object().shape({ .url(localization.validation.invalidUrl), }), ), + euThemeList: Yup.array() + .min(1, localization.datasetForm.validation.euTheme) + .required(localization.datasetForm.validation.euTheme), }); diff --git a/libs/data-access/src/index.ts b/libs/data-access/src/index.ts index 771484c43..2a1f520ec 100644 --- a/libs/data-access/src/index.ts +++ b/libs/data-access/src/index.ts @@ -18,3 +18,4 @@ export * from './lib/data-service/api'; export * from './lib/records-of-processing-activities/api'; export * from './lib/strapi/generated/graphql'; export * from './lib/strapi/service-messages'; +export * from './lib/themes/api'; diff --git a/libs/data-access/src/lib/themes/api/index.tsx b/libs/data-access/src/lib/themes/api/index.tsx new file mode 100644 index 000000000..f01bba08f --- /dev/null +++ b/libs/data-access/src/lib/themes/api/index.tsx @@ -0,0 +1,21 @@ +export const getLosThemes = async (accessToken: string) => { + const resource = `${process.env.FDK_BASE_URI}/reference-data/los/themes-and-words`; + const options = { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }; + return await fetch(resource, options); +}; + +export const getDataThemes = async (accessToken: string) => { + const resource = `${process.env.FDK_BASE_URI}/reference-data/eu/data-themes`; + const options = { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }; + return await fetch(resource, options); +}; diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index 1b8de2154..4355bb410 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -19,3 +19,4 @@ export * from './lib/filters'; export * from './lib/dataset'; export * from './lib/enums'; export * from './lib/catalogs'; +export * from './lib/theme'; diff --git a/libs/types/src/lib/dataset.ts b/libs/types/src/lib/dataset.ts index a13cf757f..80f431d57 100644 --- a/libs/types/src/lib/dataset.ts +++ b/libs/types/src/lib/dataset.ts @@ -16,6 +16,9 @@ export interface DatasetToBeCreated { legalBasisForAccess?: UriWIthLabel[]; legalBasisForRestriction?: UriWIthLabel[]; landingPage?: string[]; + theme?: { uri: string }[]; + losThemeList?: string[]; // An array of los theme uris used as helper values for Formik. This property is not part of the db object. + euThemeList?: string[]; // An array of eu theme uris used as helper values for Formik. This property is not part of the db object. } export interface UriWIthLabel { diff --git a/libs/types/src/lib/theme.ts b/libs/types/src/lib/theme.ts new file mode 100644 index 000000000..1077842a6 --- /dev/null +++ b/libs/types/src/lib/theme.ts @@ -0,0 +1,24 @@ +import { UriWIthLabel } from './dataset'; +import { LocalizedStrings } from './localization'; + +export interface LosTheme { + children?: null; + parents?: string[]; + isTheme?: boolean; + losPaths: string[]; + name?: LocalizedStrings; + definition?: null; + uri: string; + synonyms?: string[]; + relatedTerms?: null; + theme?: boolean; + internalId: null; +} + +export interface DataTheme { + uri: string; + code?: string; + label: LocalizedStrings; + startUse?: string; + conceptSchema?: UriWIthLabel; +} diff --git a/libs/utils/src/lib/language/dataset.form.nb.ts b/libs/utils/src/lib/language/dataset.form.nb.ts index 7c019d227..e01e66925 100644 --- a/libs/utils/src/lib/language/dataset.form.nb.ts +++ b/libs/utils/src/lib/language/dataset.form.nb.ts @@ -9,6 +9,8 @@ export const datasetFormNb = { legalBasisForRestriction: 'Angi referanse til relevant lov eller forskrift. Helst til lovdata på paragrafnivå.', legalBasisForProcessing: 'Angi referanse til relevant lov eller forskrift, samtykke eller nødvendighetsvurdering.', legalBasisForAccess: 'Angi referanse til relevant lov eller forskrift. Helst til lovdata på paragrafnivå.', + theme: 'Velg tema(er) som beskriver inneholdet i datasettet.', + parentTheme: 'Overordnet tema', }, heading: { description: 'Beskrivelse av datasettet', @@ -18,6 +20,8 @@ export const datasetFormNb = { legalBasisForRestriction: 'Skjermingshjemmel', legalBasisForProcessing: 'Behandlingsgrunnlag', legalBasisForAccess: 'Utleveringshjemmel', + losTheme: 'LOS-tema og emner', + euTheme: 'EU-tema', }, accessRight: { public: 'Allmenn tilgang', @@ -27,6 +31,8 @@ export const datasetFormNb = { fieldLabel: { description: 'Beskrivelse av datasettet (Norsk bokmål)', title: 'Tittel (Norsk bokmål)', + losTheme: 'Velg tema, kategorier og emner', + euTheme: 'Velg EU-tema(er)', }, alert: { confirmDelete: 'Er du sikker på at du vil slette datasettbeskrivelsen?', @@ -38,5 +44,6 @@ export const datasetFormNb = { descriptionRequired: 'Beskrivelse er påkrevd.', description: 'Beskrivelsen må være minst 5 karakterer lang.', url: `Ugyldig lenke. Vennligst sørg for at lenken starter med ‘https://’ og inneholder et gyldig toppdomene (f.eks. ‘.no’).`, + euTheme: 'Minst et EU-tema må være valgt.', }, };