diff --git a/package-lock.json b/package-lock.json index 91e460cc1..00a93a23f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bauhaus", - "version": "4.1.6", + "version": "4.3.1-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bauhaus", - "version": "4.1.6", + "version": "4.3.1-beta.1", "license": "MIT", "dependencies": { "@tanstack/react-query": "^5.59.20", diff --git a/src/packages/modules-datasets/datasets/edit/edit.jsx b/src/packages/modules-datasets/datasets/edit/edit.jsx index b4fabc151..5d3b6580b 100644 --- a/src/packages/modules-datasets/datasets/edit/edit.jsx +++ b/src/packages/modules-datasets/datasets/edit/edit.jsx @@ -176,6 +176,7 @@ export const Component = () => { ), }, diff --git a/src/packages/modules-datasets/datasets/edit/tabs/statistical-information.jsx b/src/packages/modules-datasets/datasets/edit/tabs/statistical-information.jsx index b69000789..72da286d1 100644 --- a/src/packages/modules-datasets/datasets/edit/tabs/statistical-information.jsx +++ b/src/packages/modules-datasets/datasets/edit/tabs/statistical-information.jsx @@ -4,7 +4,6 @@ import { NumberInput } from '@components/form/input'; import { Row } from '@components/layout'; import { withCodesLists } from '@utils/hoc/withCodesLists'; -import { useStructures } from '@utils/hooks/structures'; import { D1 } from '../../../../deprecated-locales'; import { @@ -16,20 +15,16 @@ import { } from '../../../../redux/actions/constants/codeList'; import { convertCodesListsToSelectOption } from '../../../utils/codelist-to-select-options'; import { TemporalField } from '../../components/temporalField'; +import { DataStructure } from './statistical-information/data-structure'; const StatisticalInformationTab = ({ editingDataset, setEditingDataset, + clientSideErrors, ...props }) => { const clDataTypes = convertCodesListsToSelectOption(props[CL_DATA_TYPES]); - const { data: structures } = useStructures(); - - const structuresOptions = - structures?.map(({ iri, labelLg1 }) => ({ value: iri, label: labelLg1 })) ?? - []; - const clStatUnit = convertCodesListsToSelectOption(props[CL_STAT_UNIT]); const clFreqOptions = convertCodesListsToSelectOption(props[CL_FREQ]); @@ -58,21 +53,16 @@ const StatisticalInformationTab = ({ -
- -
+ { + setEditingDataset({ + ...editingDataset, + dataStructure: value, + }); + }} + />
diff --git a/src/packages/modules-datasets/datasets/edit/tabs/statistical-information/data-structure.css b/src/packages/modules-datasets/datasets/edit/tabs/statistical-information/data-structure.css new file mode 100644 index 000000000..5efdfa5ee --- /dev/null +++ b/src/packages/modules-datasets/datasets/edit/tabs/statistical-information/data-structure.css @@ -0,0 +1,10 @@ +.data-structure-input { + display: flex; + gap: 1em; +} + +.data-structure-input button { + height: fit-content; + align-self: end; + margin-bottom: 5px; +} diff --git a/src/packages/modules-datasets/datasets/edit/tabs/statistical-information/data-structure.spec.tsx b/src/packages/modules-datasets/datasets/edit/tabs/statistical-information/data-structure.spec.tsx new file mode 100644 index 000000000..236ee9681 --- /dev/null +++ b/src/packages/modules-datasets/datasets/edit/tabs/statistical-information/data-structure.spec.tsx @@ -0,0 +1,102 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, Mock, vi } from 'vitest'; + +import { useStructures } from '@utils/hooks/structures'; + +import { DataStructure } from './data-structure'; + +// Mock useStructures hook +vi.mock('@utils/hooks/structures', () => ({ + useStructures: vi.fn(), +})); + +describe('DataStructure Component', () => { + it('should display a text input in URN mode by default if value does not match a structure', () => { + (useStructures as Mock).mockReturnValue({ data: [] }); + + render(); + + expect(screen.getByRole('textbox')).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /Choisir une structure/i }), + ).toBeInTheDocument(); + }); + + it('should display a select dropdown in URL mode if the value matches a structure', () => { + const structures = [ + { iri: 'http://example.com/structure1', labelLg1: 'Structure 1' }, + ]; + (useStructures as Mock).mockReturnValue({ data: structures }); + + render( + , + ); + + expect(screen.getByText('Structure 1')).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /Saisir une URN/i }), + ).toBeInTheDocument(); + }); + + it('should switch from URN mode to URL mode when clicking the button', async () => { + (useStructures as Mock).mockReturnValue({ data: [] }); + const onChangeMock = vi.fn(); + + render(); + + const button = screen.getByRole('button', { + name: /Choisir une structure/i, + }); + fireEvent.click(button); + + expect(screen.getByRole('combobox')).toBeInTheDocument(); + }); + + it('should switch from URL mode to URN mode when clicking the button', async () => { + const structures = [ + { iri: 'http://example.com/structure1', labelLg1: 'Structure 1' }, + ]; + (useStructures as Mock).mockReturnValue({ data: structures }); + + render( + , + ); + + const button = screen.getByRole('button', { name: /Saisir une URN/i }); + fireEvent.click(button); + + expect(screen.getByRole('textbox')).toBeInTheDocument(); + }); + + it('should call onChange when the user enters a value in URN mode', async () => { + (useStructures as Mock).mockReturnValue({ data: [] }); + const onChangeMock = vi.fn(); + + render(); + + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: 'urn:5678' } }); + + expect(onChangeMock).toHaveBeenCalledWith('urn:5678'); + }); + + it('should display an error message if an error is provided', async () => { + (useStructures as Mock).mockReturnValue({ data: [] }); + + render( + , + ); + + expect(screen.getByText('Required field')).toBeInTheDocument(); + }); +}); diff --git a/src/packages/modules-datasets/datasets/edit/tabs/statistical-information/data-structure.tsx b/src/packages/modules-datasets/datasets/edit/tabs/statistical-information/data-structure.tsx new file mode 100644 index 000000000..d227212ff --- /dev/null +++ b/src/packages/modules-datasets/datasets/edit/tabs/statistical-information/data-structure.tsx @@ -0,0 +1,96 @@ +import { Option } from '@model/SelectOption'; +import { useState } from 'react'; +import ReactSelect from 'react-select'; + +import { ClientSideError } from '@components/errors-bloc'; +import { TextInput } from '@components/form/input'; + +import { createDictionary, firstLang } from '@utils/dictionnary'; +import { useStructures } from '@utils/hooks/structures'; + +import { D1 } from '../../../../../deprecated-locales'; +import './data-structure.css'; + +const URN_MODE = 'URN_MODE'; +const URL_MODE = 'URL_MODE'; + +const D = createDictionary(firstLang, { + chooseUrn: { + fr: 'Saisir une URN', + en: 'Type a URN', + }, + chooseUrl: { + fr: 'Choisir une structure', + en: 'Choose a structure', + }, +}); +export const DataStructure = ({ + value, + onChange, + error, +}: Readonly<{ + value: string; + onChange: (value: string) => void; + error?: string; +}>) => { + const { data: structures } = useStructures(); + + const options: Option[] = + structures?.map(({ iri, labelLg1 }) => ({ value: iri, label: labelLg1 })) ?? + []; + + const [mode, setMode] = useState( + structures?.find((s) => s.iri === value) ? URL_MODE : URN_MODE, + ); + + if (mode === URN_MODE) { + return ( +
+
+ + +
+ +
+ ); + } + return ( +
+ + +
+ ); +}; diff --git a/src/packages/modules-datasets/datasets/edit/validation.spec.tsx b/src/packages/modules-datasets/datasets/edit/validation.spec.tsx index 14a516ef2..0fc9d44b7 100644 --- a/src/packages/modules-datasets/datasets/edit/validation.spec.tsx +++ b/src/packages/modules-datasets/datasets/edit/validation.spec.tsx @@ -23,6 +23,7 @@ describe('validation', function () { altIdentifier: '', creator: '', contributor: '', + dataStructure: '', disseminationStatus: '', wasGeneratedIRIs: '', }, @@ -44,6 +45,7 @@ describe('validation', function () { altIdentifier: '', creator: '', contributor: '', + dataStructure: '', disseminationStatus: '', wasGeneratedIRIs: '', }, @@ -70,6 +72,7 @@ describe('validation', function () { altIdentifier: '', creator: 'The property Owner is required.', contributor: 'The property Contributors is required.', + dataStructure: '', disseminationStatus: 'The property Dissemination status is required.', wasGeneratedIRIs: @@ -96,13 +99,63 @@ describe('validation', function () { altIdentifier: '', creator: '', contributor: '', + dataStructure: '', disseminationStatus: '', wasGeneratedIRIs: 'The property Produced from is required.', }, }); }); + + it('should return an error if datastructure is not a URI', function () { + expect( + validate({ + labelLg1: 'labelLg2', + labelLg2: 'labelLg2', + catalogRecord, + disseminationStatus: 'status', + wasGeneratedIRIs: ['id'], + dataStructure: 'dataset', + }), + ).toEqual({ + errorMessage: ['Invalid url'], + fields: { + labelLg1: '', + labelLg2: '', + altIdentifier: '', + creator: '', + contributor: '', + dataStructure: 'Invalid url', + disseminationStatus: '', + wasGeneratedIRIs: '', + }, + }); + }); it('should return no error', function () { + expect( + validate({ + labelLg1: 'labelLg2', + labelLg2: 'labelLg2', + catalogRecord, + disseminationStatus: 'status', + wasGeneratedIRIs: ['id'], + dataStructure: 'http://dataset', + }), + ).toEqual({ + errorMessage: [], + fields: { + labelLg1: '', + labelLg2: '', + altIdentifier: '', + creator: '', + contributor: '', + dataStructure: '', + disseminationStatus: '', + wasGeneratedIRIs: '', + }, + }); + }); + it('should return no error if datastructure is undefined', function () { expect( validate({ labelLg1: 'labelLg2', @@ -119,6 +172,7 @@ describe('validation', function () { altIdentifier: '', creator: '', contributor: '', + dataStructure: '', disseminationStatus: '', wasGeneratedIRIs: '', }, diff --git a/src/packages/modules-datasets/datasets/edit/validation.tsx b/src/packages/modules-datasets/datasets/edit/validation.tsx index e87bae0ae..6f2e9df3a 100644 --- a/src/packages/modules-datasets/datasets/edit/validation.tsx +++ b/src/packages/modules-datasets/datasets/edit/validation.tsx @@ -23,6 +23,7 @@ const ZodDataset = z.object({ disseminationStatus: mandatoryAndNotEmptySelectField( D.disseminationStatusTitle, ), + dataStructure: z.string().url().optional(), wasGeneratedIRIs: mandatoryAndNotEmptyMultiSelectField(D.generatedBy), }); diff --git a/src/packages/modules-datasets/datasets/view/StatisticalInformations.spec.tsx b/src/packages/modules-datasets/datasets/view/StatisticalInformations.spec.tsx index 6f25f5cb3..9c51018ae 100644 --- a/src/packages/modules-datasets/datasets/view/StatisticalInformations.spec.tsx +++ b/src/packages/modules-datasets/datasets/view/StatisticalInformations.spec.tsx @@ -43,6 +43,21 @@ describe('StatisticalInformations Component', () => { getByText('Number of time-series : 10'); }); + it('renders datastructure URL if the structure do not exist', () => { + (hooks.useCodesList as Mock).mockReturnValue([]); + (structureHooks.useStructures as Mock).mockReturnValue({ + data: [], + }); + + const { getByText } = render( + , + ); + + getByText('Data structure : structure1'); + getByText('Number of observation : 100'); + getByText('Number of time-series : 10'); + }); + it('renders conditional fields correctly', () => { (hooks.useCodesList as Mock).mockReturnValue([]); (structureHooks.useStructures as Mock).mockReturnValue({ diff --git a/src/packages/modules-datasets/datasets/view/StatisticalInformations.tsx b/src/packages/modules-datasets/datasets/view/StatisticalInformations.tsx index c56bd58e5..da8996168 100644 --- a/src/packages/modules-datasets/datasets/view/StatisticalInformations.tsx +++ b/src/packages/modules-datasets/datasets/view/StatisticalInformations.tsx @@ -5,7 +5,6 @@ import { Note } from '@components/note'; import { stringToDate } from '@utils/date-utils'; import { useCodesList } from '@utils/hooks/codeslist'; -import { useStructures } from '@utils/hooks/structures'; import D, { D1 } from '../../../deprecated-locales/build-dictionary'; import { Dataset } from '../../../model/Dataset'; @@ -16,6 +15,7 @@ import { CL_STAT_UNIT, CL_TYPE_GEO, } from '../../../redux/actions/constants/codeList'; +import { DataStructure } from './statistical-informations/data-structure'; interface StatisticalInformationsTypes { dataset: Dataset; @@ -30,8 +30,6 @@ export const StatisticalInformations = ({ const clGeo = useCodesList(CL_GEO); const clFreq = useCodesList(CL_FREQ); - const { data: structures } = useStructures(); - return ( {dataset.dataStructure && (
  • - {D.datasetsDataStructure} :{' '} - { - structures?.find((t) => dataset.dataStructure === t.iri) - ?.labelLg1 - } +
  • )} {dataset.temporalCoverageDataType && ( diff --git a/src/packages/modules-datasets/datasets/view/statistical-informations/data-structure.tsx b/src/packages/modules-datasets/datasets/view/statistical-informations/data-structure.tsx new file mode 100644 index 000000000..2681f9585 --- /dev/null +++ b/src/packages/modules-datasets/datasets/view/statistical-informations/data-structure.tsx @@ -0,0 +1,17 @@ +import { useStructures } from '@utils/hooks/structures'; + +import D from '../../../../deprecated-locales/build-dictionary'; + +export const DataStructure = ({ + dataStructure, +}: Readonly<{ dataStructure: string }>) => { + const { data: structures } = useStructures(); + + return ( + <> + {D.datasetsDataStructure} :{' '} + {structures?.find((t) => dataStructure === t.iri)?.labelLg1 ?? + dataStructure} + + ); +};