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}
+ >
+ );
+};