From 9cddf386f9c97a5d50693e5fa7ea12860f31f8d6 Mon Sep 17 00:00:00 2001 From: Mailine Nguyen <64129348+MailineN@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:05:06 +0200 Subject: [PATCH 1/6] feat: add release overview and form pages --- .../layout/QuestionnaireSidebar.tsx | 7 + .../release/ReleaseOverview.test.tsx | 47 +++ .../components/release/ReleaseOverview.tsx | 171 ++++++++++ .../release/ReleasesOverviewLayout.test.tsx | 50 +++ .../release/ReleasesOverviewLayout.tsx | 34 ++ .../release/create/CreateRelease.tsx | 24 ++ .../release/create/CreateReleaseForm.tsx | 78 +++++ .../release/create/CreateReleaseLayout.tsx | 17 + .../components/release/form/ReleaseForm.tsx | 305 ++++++++++++++++++ next/src/components/release/form/consts.ts | 24 ++ next/src/components/release/form/schema.ts | 23 ++ .../release/overview/CopyButton.test.tsx | 43 +++ .../release/overview/CopyButton.tsx | 37 +++ .../overview/RegistryReleaseTile.test.tsx | 134 ++++++++ .../release/overview/RegistryReleaseTile.tsx | 119 +++++++ .../ReleaseOptionalParametersDisplay.test.tsx | 77 +++++ .../ReleaseOptionalParametersDisplay.tsx | 44 +++ .../overview/ReleaseRequestTile.test.tsx | 174 ++++++++++ .../release/overview/ReleaseRequestTile.tsx | 177 ++++++++++ next/src/components/ui/ExpandButton.tsx | 5 +- .../src/components/ui/icons/CopyIcon.test.tsx | 8 + next/src/components/ui/icons/CopyIcon.tsx | 20 ++ next/src/components/ui/icons/InfoIcon.tsx | 17 + .../components/ui/icons/ReleaseIcon.test.tsx | 8 + next/src/components/ui/icons/ReleaseIcon.tsx | 17 + .../ui/icons/VisualizeIcon.test.tsx | 8 + .../src/components/ui/icons/VisualizeIcon.tsx | 18 ++ .../__snapshots__/CopyIcon.test.tsx.snap | 16 + .../__snapshots__/ReleaseIcon.test.tsx.snap | 16 + .../__snapshots__/VisualizeIcon.test.tsx.snap | 16 + next/src/lib/i18n/locales/en/crumb.json | 1 + next/src/lib/i18n/locales/en/release.json | 82 +++++ next/src/lib/i18n/locales/en/translation.ts | 2 + next/src/lib/i18n/locales/fr/crumb.json | 1 + next/src/lib/i18n/locales/fr/history.json | 2 +- next/src/lib/i18n/locales/fr/release.json | 82 +++++ next/src/lib/i18n/locales/fr/translation.ts | 2 + next/src/models/releases.ts | 54 ++++ next/src/routeTree.gen.ts | 81 +++++ .../_layout-q/releases/index.tsx | 36 +++ .../_layout-q/releases/new.tsx | 39 +++ .../_layout-q/releases/route.tsx | 9 + 42 files changed, 2123 insertions(+), 2 deletions(-) create mode 100644 next/src/components/release/ReleaseOverview.test.tsx create mode 100644 next/src/components/release/ReleaseOverview.tsx create mode 100644 next/src/components/release/ReleasesOverviewLayout.test.tsx create mode 100644 next/src/components/release/ReleasesOverviewLayout.tsx create mode 100644 next/src/components/release/create/CreateRelease.tsx create mode 100644 next/src/components/release/create/CreateReleaseForm.tsx create mode 100644 next/src/components/release/create/CreateReleaseLayout.tsx create mode 100644 next/src/components/release/form/ReleaseForm.tsx create mode 100644 next/src/components/release/form/consts.ts create mode 100644 next/src/components/release/form/schema.ts create mode 100644 next/src/components/release/overview/CopyButton.test.tsx create mode 100644 next/src/components/release/overview/CopyButton.tsx create mode 100644 next/src/components/release/overview/RegistryReleaseTile.test.tsx create mode 100644 next/src/components/release/overview/RegistryReleaseTile.tsx create mode 100644 next/src/components/release/overview/ReleaseOptionalParametersDisplay.test.tsx create mode 100644 next/src/components/release/overview/ReleaseOptionalParametersDisplay.tsx create mode 100644 next/src/components/release/overview/ReleaseRequestTile.test.tsx create mode 100644 next/src/components/release/overview/ReleaseRequestTile.tsx create mode 100644 next/src/components/ui/icons/CopyIcon.test.tsx create mode 100644 next/src/components/ui/icons/CopyIcon.tsx create mode 100644 next/src/components/ui/icons/InfoIcon.tsx create mode 100644 next/src/components/ui/icons/ReleaseIcon.test.tsx create mode 100644 next/src/components/ui/icons/ReleaseIcon.tsx create mode 100644 next/src/components/ui/icons/VisualizeIcon.test.tsx create mode 100644 next/src/components/ui/icons/VisualizeIcon.tsx create mode 100644 next/src/components/ui/icons/__snapshots__/CopyIcon.test.tsx.snap create mode 100644 next/src/components/ui/icons/__snapshots__/ReleaseIcon.test.tsx.snap create mode 100644 next/src/components/ui/icons/__snapshots__/VisualizeIcon.test.tsx.snap create mode 100644 next/src/lib/i18n/locales/en/release.json create mode 100644 next/src/lib/i18n/locales/fr/release.json create mode 100644 next/src/models/releases.ts create mode 100644 next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/index.tsx create mode 100644 next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/new.tsx create mode 100644 next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/route.tsx diff --git a/next/src/components/layout/QuestionnaireSidebar.tsx b/next/src/components/layout/QuestionnaireSidebar.tsx index 84e2809b2..4dab05c2f 100644 --- a/next/src/components/layout/QuestionnaireSidebar.tsx +++ b/next/src/components/layout/QuestionnaireSidebar.tsx @@ -14,6 +14,7 @@ import VariableIcon from '@/components/ui/icons/VariableIcon' import { useAltIcon } from '@/hooks/useAltIcon' import ProtocolesIcon from '../ui/icons/ProtocolesIcon' +import ReleaseIcon from '../ui/icons/ReleaseIcon' import Sidebar from './sidebar/Sidebar' import SidebarItem from './sidebar/SidebarItem' import SidebarItemGroup from './sidebar/SidebarItemGroup' @@ -156,6 +157,12 @@ export default function QuestionnaireSidebar() { path="/questionnaire/$questionnaireId/versions" questionnaireId={questionnaireId} /> + { + beforeEach(() => { + vi.stubEnv('VITE_TROMBI_URL', 'https://trombi.example.com') + }) + + it('renders both sections with correct headers', () => { + const { getByText } = renderWithI18n() + + expect(getByText('My publication requests')).toBeInTheDocument() + expect(getByText('My publications')).toBeInTheDocument() + }) + + it('renders all request tiles', () => { + const { getByText, getAllByText } = renderWithI18n() + + expect( + getAllByText('Recette intégrée oct 2025 pour SRCV_REINTERRO 2026').length, + ).toBe(2) + expect( + getByText('Autre demande de publication pour SRCV_REINTERRO 2026'), + ).toBeInTheDocument() + }) + + it('renders all publication tiles', () => { + const { getByText } = renderWithI18n() + + expect(getByText('ESA 2026 PROD')).toBeInTheDocument() + expect(getByText('ESA 2026 TEST TERRAIN')).toBeInTheDocument() + expect(getByText("Publication la plus ancienne d'ESA")).toBeInTheDocument() + }) + + it('renders requests sorted by date descending', () => { + const { getAllByText } = renderWithI18n() + + const descriptions = getAllByText( + (content) => + content.includes('SRCV_REINTERRO 2026') && + !content.includes('Failed') && + !content.includes('In progress'), + ) + expect(descriptions.length).toBeGreaterThanOrEqual(2) + }) +}) diff --git a/next/src/components/release/ReleaseOverview.tsx b/next/src/components/release/ReleaseOverview.tsx new file mode 100644 index 000000000..292b92f68 --- /dev/null +++ b/next/src/components/release/ReleaseOverview.tsx @@ -0,0 +1,171 @@ +import { useTranslation } from 'react-i18next' + +import { useState } from 'react' + +import type { RegistryRelease, ReleaseRequest } from '../../models/releases' +import { RegistryReleaseTile } from './overview/RegistryReleaseTile' +import { ReleaseRequestTile } from './overview/ReleaseRequestTile' + +const MOCK_REQUESTS: ReleaseRequest[] = [ + { + trackerId: 1, + author: 'xbeltv', + requestDate: new Date('2026-07-06T10:30:00').getTime(), + currentStep: 'BUILD_PARAMETERS', + status: 'RUNNING', + statusDescription: '', + poguesVersionId: '550e8400-e29b-41d4-a716-446655440000', + poguesId: 'SRCV_REINTERRO', + releaseDescription: 'Recette intégrée oct 2025 pour SRCV_REINTERRO 2026', + mode: 'CAPI', + context: 'HOUSEHOLD', + overrideGenerationParameters: { + questionNumberingMode: 'SEQUENCE', + responseTimeQuestion: true, + }, + }, + { + trackerId: 2, + author: 'nazdsn', + requestDate: new Date('2026-07-05T14:15:00').getTime(), + currentStep: 'GENERATE_DDI', + status: 'FAILED', + statusDescription: 'Erreur lors de la génération Eno', + poguesVersionId: '550e8400-e29b-41d4-a716-446655440001', + poguesId: 'SRCV_REINTERRO', + releaseDescription: 'Autre demande de publication pour SRCV_REINTERRO 2026', + mode: 'CAPI', + context: 'BUSINESS', + overrideGenerationParameters: { + questionNumberingMode: 'NONE', + responseTimeQuestion: false, + }, + }, + { + trackerId: 3, + author: 'bcbab8', + requestDate: new Date('2026-07-04T09:00:00').getTime(), + currentStep: 'PUBLISH_DDI', + status: 'FAILED', + statusDescription: 'Erreur 409 - Conflit de version', + poguesVersionId: '550e8400-e29b-41d4-a716-446655440002', + poguesId: 'SRCV_REINTERRO', + releaseDescription: 'Recette intégrée oct 2025 pour SRCV_REINTERRO 2026', + mode: 'CAPI', + context: 'HOUSEHOLD', + overrideGenerationParameters: { + questionNumberingMode: 'ALL', + responseTimeQuestion: true, + }, + }, +] + +const MOCK_PUBLICATIONS: RegistryRelease[] = [ + { + collectionInstrumentId: '550e8400-e29b-41d4-a716-446655440001', + version: 3, + author: 'xbeltv', + releaseDate: new Date('2026-07-04T08:00:00').getTime(), + poguesVersionId: '93d1e85c-327d-4153-a5fa-e04f54ca0e3e', + releaseDescription: 'ESA 2026 PROD', + mode: 'CAWI', + context: 'HOUSEHOLD', + overrideGenerationParameters: { + questionNumberingMode: 'NONE', + responseTimeQuestion: false, + }, + visualizeUrl: 'https://visu.example.com/esa-2026-prod', + }, + { + collectionInstrumentId: '550e8400-e29b-41d4-a716-446655440002', + version: 2, + author: 'nazdsn', + releaseDate: new Date('2026-06-28T10:00:00').getTime(), + poguesVersionId: '5ddf61df-00c8-4018-a763-fa7bc91b0162', + releaseDescription: 'ESA 2026 TEST TERRAIN', + mode: 'CAPI', + context: 'BUSINESS', + overrideGenerationParameters: { + questionNumberingMode: 'NONE', + responseTimeQuestion: false, + }, + visualizeUrl: 'https://visu.example.com/esa-2026-test', + }, + { + collectionInstrumentId: '550e8400-e29b-41d4-a716-446655440003', + version: 1, + author: 'bcbab8', + releaseDate: new Date('2026-06-15T11:30:00').getTime(), + poguesVersionId: 'b77e7cad-475d-4d83-b036-fa7a98a84a8a', + releaseDescription: "Publication la plus ancienne d'ESA", + mode: 'CAWI', + context: 'HOUSEHOLD', + overrideGenerationParameters: null, + visualizeUrl: 'https://visu.example.com/esa-2026-old', + }, +] + +function sortByRequestDateDesc(items: ReleaseRequest[]): ReleaseRequest[] { + return [...items].sort((a, b) => b.requestDate - a.requestDate) +} + +function sortByReleaseDateDesc(items: RegistryRelease[]): RegistryRelease[] { + return [...items].sort((a, b) => b.releaseDate - a.releaseDate) +} + +export default function ReleaseOverview() { + const { t } = useTranslation() + + const [requests, setRequests] = useState(MOCK_REQUESTS) + const publications = MOCK_PUBLICATIONS + + const hasAnyContent = requests.length > 0 || publications.length > 0 + + function handleDeleteRequest(trackerId: number) { + setRequests((prev) => prev.filter((d) => d.trackerId !== trackerId)) + } + + const sortedRequests = sortByRequestDateDesc(requests) + const sortedPublications = sortByReleaseDateDesc(publications) + + if (!hasAnyContent) { + return ( + + {t('release.noPublication')} + + ) + } + + return ( + + {sortedRequests.length > 0 && ( + + + {t('release.publicationRequests')} + + {sortedRequests.map((request) => ( + + ))} + + )} + + {sortedPublications.length > 0 && ( + + + {t('release.myPublications')} + + {sortedPublications.map((release) => ( + + ))} + + )} + + ) +} diff --git a/next/src/components/release/ReleasesOverviewLayout.test.tsx b/next/src/components/release/ReleasesOverviewLayout.test.tsx new file mode 100644 index 000000000..7622ae266 --- /dev/null +++ b/next/src/components/release/ReleasesOverviewLayout.test.tsx @@ -0,0 +1,50 @@ +import { renderWithI18n } from '@/testing/render' + +import ReleaseOverviewLayout from './ReleasesOverviewLayout' + +vi.mock('@/components/ui/ButtonLink', () => ({ + default: ({ children, to }: { children: React.ReactNode; to: string }) => ( + {children} + ), +})) + +vi.mock('@/components/ui/Breadcrumb', () => ({ + default: () => null, +})) + +describe('ReleaseOverviewLayout', () => { + it('renders title and create button', () => { + const { getByText } = renderWithI18n( + + child content + , + ) + + expect(getByText('Releases')).toBeInTheDocument() + expect(getByText('Create a release')).toBeInTheDocument() + }) + + it('renders children', () => { + const { getByText } = renderWithI18n( + + child content + , + ) + + expect(getByText('child content')).toBeInTheDocument() + }) + + it('renders create button with correct link', () => { + const { container } = renderWithI18n( + + child content + , + ) + + const link = container.querySelector('a') + expect(link).toHaveAttribute( + 'href', + '/questionnaire/$questionnaireId/releases/new', + ) + }) +}) diff --git a/next/src/components/release/ReleasesOverviewLayout.tsx b/next/src/components/release/ReleasesOverviewLayout.tsx new file mode 100644 index 000000000..cb9b54c60 --- /dev/null +++ b/next/src/components/release/ReleasesOverviewLayout.tsx @@ -0,0 +1,34 @@ +import { useTranslation } from 'react-i18next' + +import ContentWrapper from '@/components/layout/ContentWrapper' + +import ButtonLink from '../ui/ButtonLink' + +type Props = { + children: React.ReactNode + questionnaireId?: string +} + +/** Display "release" title and use default content style. */ +export default function ReleaseOverviewLayout({ + children, + questionnaireId = '', +}: Readonly) { + const { t } = useTranslation() + + return ( + + {t('release.create')} + + } + title={t('release.title')} + > + {children} + + ) +} diff --git a/next/src/components/release/create/CreateRelease.tsx b/next/src/components/release/create/CreateRelease.tsx new file mode 100644 index 000000000..9ed1be842 --- /dev/null +++ b/next/src/components/release/create/CreateRelease.tsx @@ -0,0 +1,24 @@ +import type { TargetModes } from '@/models/questionnaires' + +import CreateReleaseForm from './CreateReleaseForm' + +type Props = { + questionnaireId: string + targetModes: Set +} + +export default function CreateRelease({ + questionnaireId, + targetModes, +}: Readonly) { + return ( + + + + ) +} diff --git a/next/src/components/release/create/CreateReleaseForm.tsx b/next/src/components/release/create/CreateReleaseForm.tsx new file mode 100644 index 000000000..13d6638f7 --- /dev/null +++ b/next/src/components/release/create/CreateReleaseForm.tsx @@ -0,0 +1,78 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useNavigate } from '@tanstack/react-router' +import toast from 'react-hot-toast' +import { useTranslation } from 'react-i18next' + +import { questionnairesKeys } from '@/api/questionnaires' +import { TargetModes } from '@/models/questionnaires' + +import ReleaseForm from '../form/ReleaseForm' +import { type FormValues } from '../form/schema' + +type Props = { + questionnaireId: string + seriesId?: string + seriesLabel?: string + targetModes: Set +} + +export default function CreateReleaseForm({ + questionnaireId, + seriesId, + seriesLabel, + targetModes, +}: Readonly) { + const { t } = useTranslation() + const queryClient = useQueryClient() + const navigate = useNavigate() + + const mutation = useMutation({ + mutationFn: async (formValues: FormValues) => { + // TODO: Replace with actual API call when release endpoint is available + const response = await fetch('/api/releases', { + method: 'POST', + body: JSON.stringify({ + questionnaireId, + ...formValues, + }), + headers: { 'Content-Type': 'application/json' }, + }) + if (!response.ok) { + throw new Error('Failed to create release') + } + return response.json() + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: questionnairesKeys.detail(questionnaireId), + }) + }, + }) + + const onSubmit = async (formValues: FormValues) => { + const promise = mutation.mutateAsync(formValues, { + onSuccess: () => { + navigate({ + to: '/questionnaire/$questionnaireId/releases', + params: { questionnaireId }, + }) + }, + }) + toast.promise(promise, { + loading: t('common.loading'), + success: t('release.create.success'), + error: (err: Error) => err.toString(), + }) + } + + return ( + + ) +} diff --git a/next/src/components/release/create/CreateReleaseLayout.tsx b/next/src/components/release/create/CreateReleaseLayout.tsx new file mode 100644 index 000000000..7791e75d3 --- /dev/null +++ b/next/src/components/release/create/CreateReleaseLayout.tsx @@ -0,0 +1,17 @@ +import { useTranslation } from 'react-i18next' + +import ContentWrapper from '@/components/layout/ContentWrapper' + +type Props = { + children: React.ReactNode +} + +export default function CreateReleaseLayout({ children }: Readonly) { + const { t } = useTranslation() + + return ( + + {children} + + ) +} diff --git a/next/src/components/release/form/ReleaseForm.tsx b/next/src/components/release/form/ReleaseForm.tsx new file mode 100644 index 000000000..3cc5bc05b --- /dev/null +++ b/next/src/components/release/form/ReleaseForm.tsx @@ -0,0 +1,305 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { useNavigate } from '@tanstack/react-router' +import { Controller, type SubmitHandler, useForm } from 'react-hook-form' +import { useTranslation } from 'react-i18next' + +import Tooltip from '@/components/ui/Tooltip' +import Checkbox from '@/components/ui/form/Checkbox' +import Field from '@/components/ui/form/Field' +import Form from '@/components/ui/form/Form' +import Input from '@/components/ui/form/Input' +import RadioGroup from '@/components/ui/form/RadioGroup' +import Select from '@/components/ui/form/Select' +import InfoIcon from '@/components/ui/icons/InfoIcon' +import { TargetModes } from '@/models/questionnaires' + +import { CONTEXTE_OPTIONS, NUMEROTATION_OPTIONS } from './consts' +import { type FormValues, schema } from './schema' + +type Props = { + questionnaireId: string + seriesId?: string + seriesLabel?: string + targetModes: Set + onSubmit: SubmitHandler + submitLabel: string +} + +export default function ReleaseForm({ + questionnaireId, + seriesId, + seriesLabel, + onSubmit, + submitLabel, +}: Readonly) { + const { t } = useTranslation() + const navigate = useNavigate() + + const { + control, + handleSubmit, + formState: { isDirty, isSubmitted, isValid }, + watch, + } = useForm({ + mode: 'onChange', + defaultValues: { + description: '', + collectMode: TargetModes.CAWI, + contexte: 'menages', + pageTempsReponse: false, + numerotationQuestions: 'sequence', + }, + resolver: zodResolver(schema), + }) + + const contexteValue = watch('contexte') + + const isSeriesMissing = !seriesId || !seriesLabel + const isFormValid = isValid && !isSeriesMissing + + const handleCancel = () => { + navigate({ + to: '/questionnaire/$questionnaireId/releases', + params: { questionnaireId }, + ignoreBlocker: true, + }) + } + + return ( + + + {t('release.form.introduction')} + + + + {t('release.form.seriesInfo')} + + + + + {t('release.form.series.id')} :{' '} + + {seriesId ? ( + {seriesId} + ) : ( + <> + + {t('release.form.series.missingId')} + + + + + > + )} + + + + {t('release.form.series.label')} :{' '} + + {seriesLabel || ( + <> + + {t('release.form.series.missingLabel')} + + + + + > + )} + + + + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + {contexteValue === 'entreprise' ? ( + + + {t('release.form.optionalParameters')} + + + + ( + <> + + {!value ? ( + + {t('release.form.pageTempsReponse.warning')} + + ) : null} + > + )} + /> + + + + + ( + + {t('release.form.numerotationQuestions.label')} + + + + + + } + name={name} + touched={isTouched} + > + + options={NUMEROTATION_OPTIONS} + value={value} + onChange={onChange} + /> + + )} + /> + + + + ) : null} + + + ) +} diff --git a/next/src/components/release/form/consts.ts b/next/src/components/release/form/consts.ts new file mode 100644 index 000000000..2bbe0c710 --- /dev/null +++ b/next/src/components/release/form/consts.ts @@ -0,0 +1,24 @@ +import { t } from 'i18next' + +export const CONTEXTE_OPTIONS = [ + { label: t('release.form.contexte.menages'), value: 'menages' as const }, + { + label: t('release.form.contexte.entreprise'), + value: 'entreprise' as const, + }, +] + +export const NUMEROTATION_OPTIONS = [ + { + label: t('release.form.numerotationQuestions.none'), + value: 'none' as const, + }, + { + label: t('release.form.numerotationQuestions.sequence'), + value: 'sequence' as const, + }, + { + label: t('release.form.numerotationQuestions.questionnaire'), + value: 'questionnaire' as const, + }, +] diff --git a/next/src/components/release/form/schema.ts b/next/src/components/release/form/schema.ts new file mode 100644 index 000000000..077b3dfef --- /dev/null +++ b/next/src/components/release/form/schema.ts @@ -0,0 +1,23 @@ +import z from 'zod' + +import i18next from '@/lib/i18n' +import { TargetModes } from '@/models/questionnaires' + +export const schema = z.object({ + description: z + .string() + .min(1, { message: i18next.t('release.form.description.required') }) + .max(249, { message: i18next.t('release.form.description.maxLength') }), + collectMode: z.enum(TargetModes, { + error: i18next.t('release.form.collectMode.required'), + }), + contexte: z.enum(['menages', 'entreprise'], { + error: i18next.t('release.form.contexte.required'), + }), + pageTempsReponse: z.boolean().default(true), + numerotationQuestions: z + .enum(['none', 'sequence', 'questionnaire']) + .default('sequence'), +}) + +export type FormValues = z.input diff --git a/next/src/components/release/overview/CopyButton.test.tsx b/next/src/components/release/overview/CopyButton.test.tsx new file mode 100644 index 000000000..517f92a54 --- /dev/null +++ b/next/src/components/release/overview/CopyButton.test.tsx @@ -0,0 +1,43 @@ +import userEvent from '@testing-library/user-event' + +import { renderWithI18n } from '@/testing/render' + +import { CopyButton } from './CopyButton' + +describe('CopyButton', () => { + beforeEach(() => { + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: vi.fn().mockResolvedValue(undefined), + }, + configurable: true, + writable: true, + }) + }) + + it('renders with copy icon initially', () => { + const { container } = renderWithI18n() + + const svg = container.querySelector('svg') + expect(svg).toBeInTheDocument() + }) + + it('has correct accessible name before copy', () => { + const { getByRole } = renderWithI18n() + + expect( + getByRole('button', { name: 'Copy to clipboard' }), + ).toBeInTheDocument() + }) + + it('copies text and shows copied label after click', async () => { + const user = userEvent.setup() + const { findByText, container } = renderWithI18n( + , + ) + + await user.click(container.querySelector('button')!) + + expect(await findByText('Copied')).toBeInTheDocument() + }) +}) diff --git a/next/src/components/release/overview/CopyButton.tsx b/next/src/components/release/overview/CopyButton.tsx new file mode 100644 index 000000000..39c4c8c14 --- /dev/null +++ b/next/src/components/release/overview/CopyButton.tsx @@ -0,0 +1,37 @@ +import { useTranslation } from 'react-i18next' + +import { useState } from 'react' + +import CopyIcon from '@/components/ui/icons/CopyIcon' + +export function CopyButton({ text }: Readonly<{ text: string }>) { + const { t } = useTranslation() + const [copied, setCopied] = useState(false) + + async function handleCopy() { + try { + await navigator.clipboard.writeText(text) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch { + //idl what to put here + } + } + + return ( + + {copied ? ( + {t('release.copy.copiedLabel')} + ) : ( + + )} + + ) +} diff --git a/next/src/components/release/overview/RegistryReleaseTile.test.tsx b/next/src/components/release/overview/RegistryReleaseTile.test.tsx new file mode 100644 index 000000000..eb9014e64 --- /dev/null +++ b/next/src/components/release/overview/RegistryReleaseTile.test.tsx @@ -0,0 +1,134 @@ +import { TargetModes } from '@/models/questionnaires' +import { renderWithI18n } from '@/testing/render' +import { computeDayFromDate } from '@/utils/date' + +import type { RegistryRelease } from '../../../models/releases' +import { RegistryReleaseTile } from './RegistryReleaseTile' + +describe('RegistryReleaseTile', () => { + const mockRelease: RegistryRelease = { + collectionInstrumentId: 'COL-123', + version: 2, + author: 'maelle', + releaseDate: new Date('2025-06-15T10:00:00Z').getTime(), + poguesVersionId: '550e8400-e29b-41d4-a716-446655440001', + releaseDescription: 'ESA 2026', + mode: TargetModes.CAPI, + context: 'BUSINESS', + overrideGenerationParameters: { + questionNumberingMode: 'SEQUENCE', + responseTimeQuestion: true, + }, + visualizeUrl: 'https://visu.example.com/esa-2026', + } + + beforeEach(() => { + vi.stubEnv('VITE_TROMBI_URL', 'https://trombi.example.com') + }) + + it('displays all publication information', () => { + const { getByText } = renderWithI18n( + , + ) + + expect(getByText('ESA 2026')).toBeInTheDocument() + expect(getByText('COL-123')).toBeInTheDocument() + expect(getByText('2')).toBeInTheDocument() + expect(getByText('BUSINESS')).toBeInTheDocument() + expect( + getByText('550e8400-e29b-41d4-a716-446655440001'), + ).toBeInTheDocument() + expect(getByText('maelle')).toBeInTheDocument() + expect( + getByText(computeDayFromDate(new Date(mockRelease.releaseDate))), + ).toBeInTheDocument() + }) + + it('shows optional parameters when context is BUSINESS', () => { + const { getByText } = renderWithI18n( + , + ) + + expect( + getByText((content) => content.startsWith('Optional parameters')), + ).toBeInTheDocument() + expect(getByText('SEQUENCE')).toBeInTheDocument() + }) + + it('does not show optional parameters when context is not BUSINESS', () => { + const release: RegistryRelease = { + ...mockRelease, + context: 'HOUSEHOLD', + } + const { queryByText } = renderWithI18n( + , + ) + + expect( + queryByText((content) => content.startsWith('Optional parameters')), + ).toBeNull() + }) + + it('does not show optional parameters when overrideGenerationParameters is null', () => { + const release: RegistryRelease = { + ...mockRelease, + overrideGenerationParameters: null, + } + const { queryByText } = renderWithI18n( + , + ) + + expect( + queryByText((content) => content.startsWith('Optional parameters')), + ).toBeNull() + }) + + it('renders visualize link with visualizeUrl', () => { + const { getAllByRole } = renderWithI18n( + , + ) + + const links = getAllByRole('link') + expect(links[0]).toHaveAttribute( + 'href', + 'https://visu.example.com/esa-2026', + ) + expect(links[0]).toHaveAttribute('target', '_blank') + }) + + it('renders poguesVersionId link with visualizeUrl', () => { + const { getAllByRole } = renderWithI18n( + , + ) + + const links = getAllByRole('link') + expect(links[1]).toHaveAttribute( + 'href', + 'https://visu.example.com/esa-2026', + ) + expect(links[1]).toHaveAttribute('target', '_blank') + }) + + it('renders author link with trombi URL', () => { + const { getByText } = renderWithI18n( + , + ) + + const authorLink = getByText('maelle').closest('a') + expect(authorLink).toHaveAttribute( + 'href', + 'https://trombi.example.com/maelle', + ) + expect(authorLink).toHaveAttribute('target', '_blank') + }) + + it('renders a copy button for the collection instrument ID', () => { + const { getByRole } = renderWithI18n( + , + ) + + expect( + getByRole('button', { name: 'Copy to clipboard' }), + ).toBeInTheDocument() + }) +}) diff --git a/next/src/components/release/overview/RegistryReleaseTile.tsx b/next/src/components/release/overview/RegistryReleaseTile.tsx new file mode 100644 index 000000000..a98931bc0 --- /dev/null +++ b/next/src/components/release/overview/RegistryReleaseTile.tsx @@ -0,0 +1,119 @@ +import { useTranslation } from 'react-i18next' + +import { computeDayFromDate, computeFullDateFromDate } from '@/utils/date' + +import type { RegistryRelease } from '../../../models/releases' +import OpenInNewIcon from '../../ui/icons/OpenInNewIcon' +import VisualizeIcon from '../../ui/icons/VisualizeIcon' +import { CopyButton } from './CopyButton' +import { ReleaseOptionalParametersDisplay } from './ReleaseOptionalParametersDisplay' + +export function RegistryReleaseTile({ + release, +}: Readonly<{ + release: RegistryRelease +}>) { + const { t } = useTranslation() + const trombiUrl = import.meta.env.VITE_TROMBI_URL + const releaseDate = new Date(release.releaseDate) + + return ( + + + + {release.releaseDescription} + + + + + + + + + {t('release.instrumentCollecte')} :{' '} + + {release.collectionInstrumentId}{' '} + + + + + {t('release.version')} :{' '} + + {release.version} + + + + + {release.context === 'BUSINESS' && + release.overrideGenerationParameters && ( + + + + )} + + + + + {t('release.collectMode')} :{' '} + + {release.mode} + + + + {t('release.contexte')} :{' '} + + {release.context} + + + + + + {t('release.sauvegardePogues')} :{' '} + + + {release.poguesVersionId} + + + + + + {t('release.author')} :{' '} + + + {release.author} + + + + + + {t('release.publicationDate')} :{' '} + + + {computeDayFromDate(releaseDate)}{' '} + + + + + + ) +} diff --git a/next/src/components/release/overview/ReleaseOptionalParametersDisplay.test.tsx b/next/src/components/release/overview/ReleaseOptionalParametersDisplay.test.tsx new file mode 100644 index 000000000..b14a4ee16 --- /dev/null +++ b/next/src/components/release/overview/ReleaseOptionalParametersDisplay.test.tsx @@ -0,0 +1,77 @@ +import { renderWithI18n } from '@/testing/render' + +import { ReleaseOptionalParametersDisplay } from './ReleaseOptionalParametersDisplay' + +describe('ReleaseOptionalParametersDisplay', () => { + it('renders optional parameters header', () => { + const { getByText } = renderWithI18n( + , + ) + + expect( + getByText((content) => content.startsWith('Optional parameters')), + ).toBeInTheDocument() + }) + + it('renders checkbox checked when responseTimeQuestion is true', () => { + const { getByRole } = renderWithI18n( + , + ) + + const checkbox = getByRole('checkbox') as HTMLInputElement + expect(checkbox.checked).toBe(true) + expect(checkbox).toBeDisabled() + }) + + it('renders checkbox unchecked when responseTimeQuestion is false', () => { + const { getByRole } = renderWithI18n( + , + ) + + const checkbox = getByRole('checkbox') as HTMLInputElement + expect(checkbox.checked).toBe(false) + }) + + it('displays questionNumberingMode value', () => { + const { getByText } = renderWithI18n( + , + ) + + expect(getByText('SEQUENCE')).toBeInTheDocument() + }) + + it('applies custom innerClassName', () => { + const { container } = renderWithI18n( + , + ) + + const inner = container.querySelector('.custom-class') + expect(inner).toBeInTheDocument() + }) +}) diff --git a/next/src/components/release/overview/ReleaseOptionalParametersDisplay.tsx b/next/src/components/release/overview/ReleaseOptionalParametersDisplay.tsx new file mode 100644 index 000000000..4b7897fa2 --- /dev/null +++ b/next/src/components/release/overview/ReleaseOptionalParametersDisplay.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from 'react-i18next' + +import type { ReleaseRequest } from '../../../models/releases' + +type OverrideGenerationParameters = + ReleaseRequest['overrideGenerationParameters'] + +export function ReleaseOptionalParametersDisplay({ + overrideGenerationParameters, + innerClassName = 'space-y-1', +}: Readonly<{ + overrideGenerationParameters: OverrideGenerationParameters + innerClassName?: string +}>) { + const { t } = useTranslation() + + return ( + <> + {t('release.optionalParameters')} : + + + + {t('release.form.pageTempsReponse.label')} + {': '} + + + + + + {t('release.form.numerotationQuestions.label')} :{' '} + + + {overrideGenerationParameters.questionNumberingMode} + + + + > + ) +} diff --git a/next/src/components/release/overview/ReleaseRequestTile.test.tsx b/next/src/components/release/overview/ReleaseRequestTile.test.tsx new file mode 100644 index 000000000..f7e0762c2 --- /dev/null +++ b/next/src/components/release/overview/ReleaseRequestTile.test.tsx @@ -0,0 +1,174 @@ +import userEvent from '@testing-library/user-event' + +import { TargetModes } from '@/models/questionnaires' +import { renderWithI18n } from '@/testing/render' +import { computeDayFromDate } from '@/utils/date' + +import type { ReleaseRequest } from '../../../models/releases' +import { ReleaseRequestTile } from './ReleaseRequestTile' + +describe('ReleaseRequestTile', () => { + const baseRequest: ReleaseRequest = { + trackerId: 1, + author: 'maelle', + requestDate: new Date('2025-06-15T10:00:00Z').getTime(), + currentStep: 'BUILD_PARAMETERS', + status: 'RUNNING', + statusDescription: '', + poguesVersionId: '550e8400-e29b-41d4-a716-446655440000', + poguesId: 'ESA2026', + releaseDescription: 'Publication ESA 2026', + mode: TargetModes.CAWI, + context: 'BUSINESS', + overrideGenerationParameters: { + questionNumberingMode: 'NONE', + responseTimeQuestion: false, + }, + } + + beforeEach(() => { + vi.stubEnv('VITE_TROMBI_URL', 'https://trombi.example.com') + }) + + it('displays request description', () => { + const { getByText } = renderWithI18n( + , + ) + + expect(getByText('Publication ESA 2026')).toBeInTheDocument() + }) + + it('shows in progress status badge', () => { + const { getByText } = renderWithI18n( + , + ) + + expect( + getByText((content) => content.startsWith('In progress')), + ).toBeInTheDocument() + }) + + it('shows failed status badge', () => { + const request: ReleaseRequest = { ...baseRequest, status: 'FAILED' } + const { getByText } = renderWithI18n( + , + ) + + expect( + getByText((content) => content.startsWith('Failed')), + ).toBeInTheDocument() + }) + + it('shows error message when failed with statusDescription', async () => { + const user = userEvent.setup() + const request: ReleaseRequest = { + ...baseRequest, + status: 'FAILED', + statusDescription: 'Error during publication', + } + const { getByText, getByRole } = renderWithI18n( + , + ) + + await user.click(getByRole('button', { name: /Publication ESA 2026/ })) + + expect(getByText('Error during publication')).toBeInTheDocument() + }) + + it('does not show error message when in progress', () => { + const request: ReleaseRequest = { + ...baseRequest, + status: 'RUNNING', + statusDescription: 'Should not appear', + } + const { queryByText } = renderWithI18n( + , + ) + + expect(queryByText('Should not appear')).toBeNull() + }) + + it('displays expanded content on click', async () => { + const user = userEvent.setup() + const { getByText, getByRole } = renderWithI18n( + , + ) + + await user.click(getByRole('button', { name: /Publication ESA 2026/ })) + + expect(getByText('BUSINESS')).toBeInTheDocument() + expect(getByText('maelle')).toBeInTheDocument() + expect( + getByText(computeDayFromDate(new Date(baseRequest.requestDate))), + ).toBeInTheDocument() + expect( + getByText((content) => content.startsWith('Optional parameters')), + ).toBeInTheDocument() + expect(getByText('NONE')).toBeInTheDocument() + }) + + it('collapses content on click', async () => { + const user = userEvent.setup() + const { getByRole, queryByText } = renderWithI18n( + , + ) + + const header = getByRole('button', { name: /Publication ESA 2026/ }) + await user.click(header) + await user.click(header) + + expect( + queryByText((content) => content.startsWith('Optional parameters')), + ).toBeNull() + }) + + it('shows delete button when request has failed', async () => { + const user = userEvent.setup() + const request: ReleaseRequest = { ...baseRequest, status: 'FAILED' } + const { getByRole } = renderWithI18n( + , + ) + + await user.click(getByRole('button', { name: /Publication ESA 2026/ })) + + expect( + getByRole('button', { name: 'Delete this request' }), + ).toBeInTheDocument() + }) + + it('shows delete button when request is completed', async () => { + const user = userEvent.setup() + const request: ReleaseRequest = { ...baseRequest, status: 'COMPLETED' } + const { getByRole } = renderWithI18n( + , + ) + + await user.click(getByRole('button', { name: /Publication ESA 2026/ })) + + expect( + getByRole('button', { name: 'Delete this request' }), + ).toBeInTheDocument() + }) + + it('does not show delete button when request is in progress', () => { + const { queryByRole } = renderWithI18n( + , + ) + + expect(queryByRole('button', { name: 'Delete this request' })).toBeNull() + }) + + it('calls onDelete with trackerId when delete button is clicked', async () => { + const user = userEvent.setup() + const onDelete = vi.fn() + const request: ReleaseRequest = { ...baseRequest, status: 'FAILED' } + const { getByRole } = renderWithI18n( + , + ) + + await user.click(getByRole('button', { name: /Publication ESA 2026/ })) + await user.click(getByRole('button', { name: 'Delete this request' })) + + expect(onDelete).toHaveBeenCalledWith(1) + }) +}) diff --git a/next/src/components/release/overview/ReleaseRequestTile.tsx b/next/src/components/release/overview/ReleaseRequestTile.tsx new file mode 100644 index 000000000..b73f3e787 --- /dev/null +++ b/next/src/components/release/overview/ReleaseRequestTile.tsx @@ -0,0 +1,177 @@ +import { useTranslation } from 'react-i18next' + +import { useState } from 'react' + +import ExpandButton from '@/components/ui/ExpandButton' +import Tooltip from '@/components/ui/Tooltip' +import DeleteIcon from '@/components/ui/icons/DeleteIcon' +import OpenInNewIcon from '@/components/ui/icons/OpenInNewIcon' +import { computeDayFromDate, computeFullDateFromDate } from '@/utils/date' + +import type { ReleaseRequest } from '../../../models/releases' +import { ReleaseOptionalParametersDisplay } from './ReleaseOptionalParametersDisplay' + +function getStatusLabel( + status: ReleaseRequest['status'], + t: (key: string) => string, +): string { + switch (status) { + case 'COMPLETED': + return t('release.status.completed') + case 'FAILED': + return t('release.status.failed') + default: + return t('release.status.inProgress') + } +} + +function getStatusIcon(status: ReleaseRequest['status']): string { + switch (status) { + case 'COMPLETED': + return '\u2705' + case 'FAILED': + return '\u274C' + default: + return '\u2699\uFE0F' + } +} + +function isFailedOrCompleted(status: ReleaseRequest['status']): boolean { + return status === 'FAILED' || status === 'COMPLETED' +} + +export function ReleaseRequestTile({ + request, + onDelete, +}: Readonly<{ + request: ReleaseRequest + onDelete: (trackerId: number) => void +}>) { + const { t } = useTranslation() + const [isExpanded, setIsExpanded] = useState(false) + const contentId = `request-content-${request.trackerId}` + const trombiUrl = import.meta.env.VITE_TROMBI_URL + const requestDate = new Date(request.requestDate) + + const statusBadge = ( + + {getStatusLabel(request.status, t)} + {getStatusIcon(request.status)} + + ) + + return ( + + setIsExpanded((v) => !v)} + aria-expanded={isExpanded} + aria-controls={contentId} + > + {request.releaseDescription} + + {request.status === 'FAILED' && request.statusDescription ? ( + {statusBadge} + ) : ( + statusBadge + )} + + + + + {isExpanded && ( + + + + + + {t('release.collectMode')} :{' '} + + {request.mode} + + + + + + + + + + + {t('release.contexte')} :{' '} + + {request.context} + + + + + + + {t('release.author')} :{' '} + + + {request.author} + + + + + + {t('release.demandDate')} :{' '} + + + {computeDayFromDate(requestDate)} + + + + + {request.status === 'FAILED' && request.statusDescription && ( + + {request.statusDescription} + + )} + + + {isFailedOrCompleted(request.status) && ( + { + e.stopPropagation() + onDelete(request.trackerId) + }} + className="text-gray-400 hover:text-red-600 transition cursor-pointer" + title={t('release.deleteRequest')} + > + + + )} + + + )} + + ) +} diff --git a/next/src/components/ui/ExpandButton.tsx b/next/src/components/ui/ExpandButton.tsx index fe7490ca2..05c76b968 100644 --- a/next/src/components/ui/ExpandButton.tsx +++ b/next/src/components/ui/ExpandButton.tsx @@ -18,7 +18,10 @@ export default function ExpandButton({ return ( setIsExpanded((v) => !v)} + onClick={(e) => { + e.stopPropagation() + setIsExpanded((v) => !v) + }} aria-expanded={isExpanded} aria-controls={ariaControls} aria-label={isExpanded ? t('common.collapse') : t('common.expand')} diff --git a/next/src/components/ui/icons/CopyIcon.test.tsx b/next/src/components/ui/icons/CopyIcon.test.tsx new file mode 100644 index 000000000..239cd6906 --- /dev/null +++ b/next/src/components/ui/icons/CopyIcon.test.tsx @@ -0,0 +1,8 @@ +import { render } from '@testing-library/react' + +import CopyIcon from './CopyIcon' + +it('CopyIcon renders correctly', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() +}) diff --git a/next/src/components/ui/icons/CopyIcon.tsx b/next/src/components/ui/icons/CopyIcon.tsx new file mode 100644 index 000000000..4e402959d --- /dev/null +++ b/next/src/components/ui/icons/CopyIcon.tsx @@ -0,0 +1,20 @@ +/** + * Icon of a plus symbol which should be used when something can be copied + */ +export default function CopyIcon({ + height = '24px', + width = '24px', + ...props +}: Readonly>) { + return ( + + + + ) +} diff --git a/next/src/components/ui/icons/InfoIcon.tsx b/next/src/components/ui/icons/InfoIcon.tsx new file mode 100644 index 000000000..e856ef8c9 --- /dev/null +++ b/next/src/components/ui/icons/InfoIcon.tsx @@ -0,0 +1,17 @@ +export default function InfoIcon({ + height = '24px', + width = '24px', + ...props +}: Readonly>) { + return ( + + + + ) +} diff --git a/next/src/components/ui/icons/ReleaseIcon.test.tsx b/next/src/components/ui/icons/ReleaseIcon.test.tsx new file mode 100644 index 000000000..d81899d2c --- /dev/null +++ b/next/src/components/ui/icons/ReleaseIcon.test.tsx @@ -0,0 +1,8 @@ +import { render } from '@testing-library/react' + +import ReleaseIcon from './ReleaseIcon' + +it('ReleaseIcon renders correctly', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() +}) diff --git a/next/src/components/ui/icons/ReleaseIcon.tsx b/next/src/components/ui/icons/ReleaseIcon.tsx new file mode 100644 index 000000000..0b6dce1d4 --- /dev/null +++ b/next/src/components/ui/icons/ReleaseIcon.tsx @@ -0,0 +1,17 @@ +export default function ReleaseIcon({ + height = '24px', + width = '24px', + ...props +}: Readonly>) { + return ( + + + + ) +} diff --git a/next/src/components/ui/icons/VisualizeIcon.test.tsx b/next/src/components/ui/icons/VisualizeIcon.test.tsx new file mode 100644 index 000000000..99b04ff40 --- /dev/null +++ b/next/src/components/ui/icons/VisualizeIcon.test.tsx @@ -0,0 +1,8 @@ +import { render } from '@testing-library/react' + +import VisualizeIcon from './VisualizeIcon' + +it('VisualizeIcon renders correctly', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() +}) diff --git a/next/src/components/ui/icons/VisualizeIcon.tsx b/next/src/components/ui/icons/VisualizeIcon.tsx new file mode 100644 index 000000000..9ef4225b6 --- /dev/null +++ b/next/src/components/ui/icons/VisualizeIcon.tsx @@ -0,0 +1,18 @@ +/** Icon of an eye which should be used for redirection to orchestator visualization mode. */ +export default function VisualizeIcon({ + height = '24px', + width = '24px', + ...props +}: Readonly>) { + return ( + + + + ) +} diff --git a/next/src/components/ui/icons/__snapshots__/CopyIcon.test.tsx.snap b/next/src/components/ui/icons/__snapshots__/CopyIcon.test.tsx.snap new file mode 100644 index 000000000..42c86915b --- /dev/null +++ b/next/src/components/ui/icons/__snapshots__/CopyIcon.test.tsx.snap @@ -0,0 +1,16 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`CopyIcon renders correctly 1`] = ` + + + + + +`; diff --git a/next/src/components/ui/icons/__snapshots__/ReleaseIcon.test.tsx.snap b/next/src/components/ui/icons/__snapshots__/ReleaseIcon.test.tsx.snap new file mode 100644 index 000000000..65a87b9bc --- /dev/null +++ b/next/src/components/ui/icons/__snapshots__/ReleaseIcon.test.tsx.snap @@ -0,0 +1,16 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ReleaseIcon renders correctly 1`] = ` + + + + + +`; diff --git a/next/src/components/ui/icons/__snapshots__/VisualizeIcon.test.tsx.snap b/next/src/components/ui/icons/__snapshots__/VisualizeIcon.test.tsx.snap new file mode 100644 index 000000000..113e15570 --- /dev/null +++ b/next/src/components/ui/icons/__snapshots__/VisualizeIcon.test.tsx.snap @@ -0,0 +1,16 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`VisualizeIcon renders correctly 1`] = ` + + + + + +`; diff --git a/next/src/lib/i18n/locales/en/crumb.json b/next/src/lib/i18n/locales/en/crumb.json index 6b95f8043..3392bda86 100644 --- a/next/src/lib/i18n/locales/en/crumb.json +++ b/next/src/lib/i18n/locales/en/crumb.json @@ -8,6 +8,7 @@ "nomenclatures": "Nomenclatures", "questionnaire": "Questionnaire {{id}}", "questionnaires": "Questionnaires", + "release": "Release {{id}}", "variable": "Variable {{id}}", "variables": "Variables", "version": "Version {{id}}", diff --git a/next/src/lib/i18n/locales/en/release.json b/next/src/lib/i18n/locales/en/release.json new file mode 100644 index 000000000..6f086e1fd --- /dev/null +++ b/next/src/lib/i18n/locales/en/release.json @@ -0,0 +1,82 @@ +{ + "author": "Author", + "id": "ID", + "noReleases": "No release available", + "oldReleases": "{{count}} older releases", + "questionnaireIsReadonly": "This release of the survey is on readonly.", + "title": "Releases", + "create": "Create a release", + "create.title": "New publication", + "create.success": "Publication created successfully", + "view": "View", + "noPublication": "The questionnaire has not yet been published", + "publicationRequests": "My publication requests", + "myPublications": "My publications", + "collectMode": "Collection mode", + "contexte": "Context", + "optionalParameters": "Optional parameters", + "demandDate": "Demand Date", + "status": { + "inProgress": "In progress ", + "failed": "Failed " + }, + "deleteRequest": "Delete this request", + "copy": { + "tooltip": "Copy to clipboard", + "copiedTooltip": "Copied!", + "copiedLabel": "Copied" + }, + "instrumentCollecte": "Collection instrument", + "version": "Version", + "publicationDate": "Publication date", + "sauvegardePogues": "Pogues save", + "form": { + "introduction": "You are about to publish in the survey questionnaire register a version of your questionnaire based on the latest save. Make sure you have completed your review beforehand.", + "seriesInfo": "Series information", + "publish": "Publish", + "series": { + "id": "Series, RMéS identifier", + "label": "Short label of the series", + "missingId": "The series is missing, please choose one in the detail of the questionnaire.", + "missingIdTooltip": "If the series you look for is not in the list, please contact the quality team.", + "missingLabel": "The short label is missing, please choose one in the detail of the questionnaire.", + "missingLabelTooltip": "If the short label you look for is not in the list, please contact the quality team." + }, + "description": { + "label": "Description", + "placeholder": "Enter a description", + "example": "For example ESA 2026 PROD, integrated review Oct 2025 for SRCV_REINTERRO 2026, publication for integration on 01/01/2030, VPP PROD web", + "required": "Description is required", + "maxLength": "Description cannot exceed 249 characters" + }, + "collectMode": { + "label": "Collection mode", + "required": "Collection mode is required", + "noMatch": "No collection mode compatible with those declared in the questionnaire", + "options": { + "CAWI": "CAWI", + "CAPI": "CAPI", + "PAPI": "PAPI", + "CATI": "CATI" + } + }, + "contexte": { + "label": "Context", + "required": "Context is required", + "menages": "Household", + "entreprise": "Business" + }, + "optionalParameters": "Optional parameters", + "pageTempsReponse": { + "label": "Response time page", + "warning": "Risk of anomaly if ending on a loop and legal obligation" + }, + "numerotationQuestions": { + "label": "Question numbering", + "none": "No numbering", + "sequence": "By sequence", + "questionnaire": "Entire questionnaire", + "tooltip": "No numbering: no automatic numbering. By sequence: numbering reset at each sequence. Entire questionnaire: continuous numbering throughout the questionnaire." + } + } +} diff --git a/next/src/lib/i18n/locales/en/translation.ts b/next/src/lib/i18n/locales/en/translation.ts index bcce73270..81d3c7499 100644 --- a/next/src/lib/i18n/locales/en/translation.ts +++ b/next/src/lib/i18n/locales/en/translation.ts @@ -12,6 +12,7 @@ import personalization from './personalization.json' import protocoles from './protocoles.json' import questionnaire from './questionnaire.json' import questionnaires from './questionnaires.json' +import release from './release.json' import variable from './variable.json' import variables from './variables.json' @@ -30,6 +31,7 @@ export const translation = { protocoles, questionnaire, questionnaires, + release, variable, variables, } diff --git a/next/src/lib/i18n/locales/fr/crumb.json b/next/src/lib/i18n/locales/fr/crumb.json index dca3aedeb..2c1337bc5 100644 --- a/next/src/lib/i18n/locales/fr/crumb.json +++ b/next/src/lib/i18n/locales/fr/crumb.json @@ -8,6 +8,7 @@ "nomenclatures": "Nomenclatures", "questionnaire": "Questionnaire {{id}}", "questionnaires": "Questionnaires", + "release": "Publications", "variable": "Variable {{id}}", "variables": "Variables", "version": "Version {{id}}", diff --git a/next/src/lib/i18n/locales/fr/history.json b/next/src/lib/i18n/locales/fr/history.json index ba74f2b98..1c195562d 100644 --- a/next/src/lib/i18n/locales/fr/history.json +++ b/next/src/lib/i18n/locales/fr/history.json @@ -1,5 +1,5 @@ { - "author": "Auteur", + "author": "author", "backToLatest": "Retour à la dernière sauvegarde", "id": "ID", "noVersions": "Aucune sauvegarde disponible", diff --git a/next/src/lib/i18n/locales/fr/release.json b/next/src/lib/i18n/locales/fr/release.json new file mode 100644 index 000000000..49ef0a69f --- /dev/null +++ b/next/src/lib/i18n/locales/fr/release.json @@ -0,0 +1,82 @@ +{ + "author": "Auteur", + "id": "ID", + "noReleases": "Aucune publication disponible", + "oldReleases": "{{count}} publications plus anciennes", + "questionnaireIsReadonly": "Cette publication du questionnaire est en lecture seule.", + "title": "Publications", + "view": "Consulter", + "create": "Publier un instrument de collecte", + "create.title": "Nouvelle publication", + "create.success": "Publication créée avec succès", + "noPublication": "Le questionnaire n'a pas encore été publié", + "publicationRequests": "Mes demandes de publication", + "myPublications": "Mes publications", + "collectMode": "Mode de collecte", + "contexte": "Contexte", + "optionalParameters": "Paramètres optionnels", + "demandDate": "Date de demande", + "status": { + "inProgress": "En cours ", + "failed": "Échec " + }, + "deleteRequest": "Supprimer cette demande", + "copy": { + "tooltip": "Copier dans le presse-papier", + "copiedTooltip": "Copié !", + "copiedLabel": "Copié" + }, + "instrumentCollecte": "Instrument de collecte", + "version": "Version", + "publicationDate": "Date de publication", + "sauvegardePogues": "Sauvegarde Pogues", + "form": { + "introduction": "Vous allez publier dans le registre de questionnaires de la filière d'enquêtes une version de votre questionnaire basée sur la dernière sauvegarde. Assurez-vous au préalable d'avoir terminé votre recette.", + "seriesInfo": "Informations sur la série", + "publish": "Publier", + "series": { + "id": "Série", + "label": "Libellé court de la série", + "missingId": "La série est absente, veuillez en sélectionner une dans le détail de votre questionnaire", + "missingIdTooltip": "Si la série qui concerne votre enquête n'est pas présente dans la liste, rapprochez-vous de l'unité qualité de l'Insee.", + "missingLabel": "Le libellé court est absent, veuillez en sélectionner un dans le détail de votre questionnaire", + "missingLabelTooltip": "Si le libellé court qui concerne votre enquête n'est pas présent dans la liste, rapprochez-vous de l'unité qualité de l'Insee." + }, + "description": { + "label": "Description", + "placeholder": "Saisissez une description", + "example": "Par exemple ESA 2026 PROD, recette intégrée oct 2025 pour SRCV_REINTERRO 2026, publication pour intégration du 01/01/2030, VPP PROD web", + "required": "La description est obligatoire", + "maxLength": "La description ne peut pas dépasser 249 caractères" + }, + "collectMode": { + "label": "Mode de collecte", + "required": "Le mode de collecte est obligatoire", + "noMatch": "Aucun mode de collecte compatible avec les modes déclarés dans le questionnaire", + "options": { + "CAWI": "CAWI", + "CAPI": "CAPI", + "PAPI": "PAPI", + "CATI": "CATI" + } + }, + "contexte": { + "label": "Contexte", + "required": "Le contexte est obligatoire", + "menages": "Ménages", + "entreprise": "Entreprise" + }, + "optionalParameters": "Paramètres optionnels", + "pageTempsReponse": { + "label": "Page sur les temps de réponse", + "warning": "Risque d'anomalie si termine sur boucle et obligation légale" + }, + "numerotationQuestions": { + "label": "Numérotation des questions", + "none": "Pas de numérotation", + "sequence": "Par séquence", + "questionnaire": "Questionnaire entier", + "tooltip": "Pas de numérotation : aucune numérotation automatique. Par séquence : numérotation réinitialisée à chaque séquence. Questionnaire entier : numérotation continue sur l'ensemble du questionnaire." + } + } +} diff --git a/next/src/lib/i18n/locales/fr/translation.ts b/next/src/lib/i18n/locales/fr/translation.ts index bcce73270..81d3c7499 100644 --- a/next/src/lib/i18n/locales/fr/translation.ts +++ b/next/src/lib/i18n/locales/fr/translation.ts @@ -12,6 +12,7 @@ import personalization from './personalization.json' import protocoles from './protocoles.json' import questionnaire from './questionnaire.json' import questionnaires from './questionnaires.json' +import release from './release.json' import variable from './variable.json' import variables from './variables.json' @@ -30,6 +31,7 @@ export const translation = { protocoles, questionnaire, questionnaires, + release, variable, variables, } diff --git a/next/src/models/releases.ts b/next/src/models/releases.ts new file mode 100644 index 000000000..8d0a049f1 --- /dev/null +++ b/next/src/models/releases.ts @@ -0,0 +1,54 @@ +import { TargetModes } from '@/models/questionnaires' + +export type ReleaseRequestStatus = + | 'CREATED' + | 'RUNNING' + | 'COMPLETED' + | 'FAILED' + +export type CurrentStep = + | 'BUILD_PARAMETERS' + | 'PUBLISH_PRERELEASE' + | 'GENERATE_DDI' + | 'GENERATE_LUNATIC' + | 'PUBLISH_DDI' + | 'PUBLISH_LUNATIC' + | 'FINISHED' + +export type QuestionNumberingMode = 'NONE' | 'SEQUENCE' | 'ALL' + +export type ReleaseContext = 'HOUSEHOLD' | 'BUSINESS' + +export type ReleaseRequest = { + trackerId: number + author: string + requestDate: number + currentStep: CurrentStep + status: ReleaseRequestStatus + statusDescription: string + poguesVersionId: string + poguesId: string + releaseDescription: string + mode: TargetModes + context: ReleaseContext + overrideGenerationParameters: { + questionNumberingMode: QuestionNumberingMode + responseTimeQuestion: boolean + } +} + +export type RegistryRelease = { + collectionInstrumentId: string + version: number + author: string + releaseDate: number + poguesVersionId: string + releaseDescription: string + mode: TargetModes + context: ReleaseContext + overrideGenerationParameters: { + questionNumberingMode: QuestionNumberingMode + responseTimeQuestion: boolean + } | null + visualizeUrl: string +} diff --git a/next/src/routeTree.gen.ts b/next/src/routeTree.gen.ts index 0d949d771..935bd9fd4 100644 --- a/next/src/routeTree.gen.ts +++ b/next/src/routeTree.gen.ts @@ -29,6 +29,9 @@ import { Route as LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationPublic import { Route as LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationIndexRouteImport } from './routes/_layout/questionnaire.$questionnaireId/_layout-q/personalization/index' import { Route as LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationNewRouteImport } from './routes/_layout/questionnaire.$questionnaireId/_layout-q/personalization/new' import { Route as LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationRouteRouteImport } from './routes/_layout/questionnaire.$questionnaireId/_layout-q/personalization/route' +import { Route as LayoutQuestionnaireQuestionnaireIdLayoutQReleasesIndexRouteImport } from './routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/index' +import { Route as LayoutQuestionnaireQuestionnaireIdLayoutQReleasesNewRouteImport } from './routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/new' +import { Route as LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRouteImport } from './routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/route' import { Route as LayoutQuestionnaireQuestionnaireIdLayoutQTcmCompositionRouteImport } from './routes/_layout/questionnaire.$questionnaireId/_layout-q/tcm-composition' import { Route as LayoutQuestionnaireQuestionnaireIdLayoutQVariablesIndexRouteImport } from './routes/_layout/questionnaire.$questionnaireId/_layout-q/variables/index' import { Route as LayoutQuestionnaireQuestionnaireIdLayoutQVariablesNewRouteImport } from './routes/_layout/questionnaire.$questionnaireId/_layout-q/variables/new' @@ -134,6 +137,12 @@ const LayoutQuestionnaireQuestionnaireIdLayoutQVariablesRouteRoute = path: '/variables', getParentRoute: () => LayoutQuestionnaireQuestionnaireIdLayoutQRoute, } as any) +const LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRoute = + LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRouteImport.update({ + id: '/releases', + path: '/releases', + getParentRoute: () => LayoutQuestionnaireQuestionnaireIdLayoutQRoute, + } as any) const LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationRouteRoute = LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationRouteRouteImport.update( { @@ -175,6 +184,13 @@ const LayoutQuestionnaireQuestionnaireIdLayoutQVariablesIndexRoute = getParentRoute: () => LayoutQuestionnaireQuestionnaireIdLayoutQVariablesRouteRoute, } as any) +const LayoutQuestionnaireQuestionnaireIdLayoutQReleasesIndexRoute = + LayoutQuestionnaireQuestionnaireIdLayoutQReleasesIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => + LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRoute, + } as any) const LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationIndexRoute = LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationIndexRouteImport.update( { @@ -221,6 +237,13 @@ const LayoutQuestionnaireQuestionnaireIdLayoutQVariablesNewRoute = getParentRoute: () => LayoutQuestionnaireQuestionnaireIdLayoutQVariablesRouteRoute, } as any) +const LayoutQuestionnaireQuestionnaireIdLayoutQReleasesNewRoute = + LayoutQuestionnaireQuestionnaireIdLayoutQReleasesNewRouteImport.update({ + id: '/new', + path: '/new', + getParentRoute: () => + LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRoute, + } as any) const LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationNewRoute = LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationNewRouteImport.update( { @@ -359,6 +382,7 @@ export interface FileRoutesByFullPath { '/questionnaire/$questionnaireId/multimode': typeof LayoutQuestionnaireQuestionnaireIdLayoutQMultimodeRouteRouteWithChildren '/questionnaire/$questionnaireId/nomenclatures': typeof LayoutQuestionnaireQuestionnaireIdLayoutQNomenclaturesRouteRouteWithChildren '/questionnaire/$questionnaireId/personalization': typeof LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationRouteRouteWithChildren + '/questionnaire/$questionnaireId/releases': typeof LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRouteWithChildren '/questionnaire/$questionnaireId/variables': typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesRouteRouteWithChildren '/questionnaire/$questionnaireId/composition': typeof LayoutQuestionnaireQuestionnaireIdLayoutQCompositionRoute '/questionnaire/$questionnaireId/duplicate-variables': typeof LayoutQuestionnaireQuestionnaireIdLayoutQDuplicateVariablesRoute @@ -374,12 +398,14 @@ export interface FileRoutesByFullPath { '/questionnaire/$questionnaireId/multimode/edit': typeof LayoutQuestionnaireQuestionnaireIdLayoutQMultimodeEditRoute '/questionnaire/$questionnaireId/personalization/$publicEnemyId': typeof LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationPublicEnemyIdRoute '/questionnaire/$questionnaireId/personalization/new': typeof LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationNewRoute + '/questionnaire/$questionnaireId/releases/new': typeof LayoutQuestionnaireQuestionnaireIdLayoutQReleasesNewRoute '/questionnaire/$questionnaireId/variables/new': typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesNewRoute '/questionnaire/$questionnaireId/articulation/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQArticulationIndexRoute '/questionnaire/$questionnaireId/codes-lists/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQCodesListsIndexRoute '/questionnaire/$questionnaireId/multimode/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQMultimodeIndexRoute '/questionnaire/$questionnaireId/nomenclatures/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQNomenclaturesIndexRoute '/questionnaire/$questionnaireId/personalization/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationIndexRoute + '/questionnaire/$questionnaireId/releases/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQReleasesIndexRoute '/questionnaire/$questionnaireId/variables/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesIndexRoute '/questionnaire/$questionnaireId/variables/variable/$variableId': typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesVariableVariableIdRoute '/questionnaire/$questionnaireId/version/$versionId/articulation': typeof LayoutQuestionnaireQuestionnaireIdLayoutQVersionVersionIdArticulationRoute @@ -407,12 +433,14 @@ export interface FileRoutesByTo { '/questionnaire/$questionnaireId/multimode/edit': typeof LayoutQuestionnaireQuestionnaireIdLayoutQMultimodeEditRoute '/questionnaire/$questionnaireId/personalization/$publicEnemyId': typeof LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationPublicEnemyIdRoute '/questionnaire/$questionnaireId/personalization/new': typeof LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationNewRoute + '/questionnaire/$questionnaireId/releases/new': typeof LayoutQuestionnaireQuestionnaireIdLayoutQReleasesNewRoute '/questionnaire/$questionnaireId/variables/new': typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesNewRoute '/questionnaire/$questionnaireId/articulation': typeof LayoutQuestionnaireQuestionnaireIdLayoutQArticulationIndexRoute '/questionnaire/$questionnaireId/codes-lists': typeof LayoutQuestionnaireQuestionnaireIdLayoutQCodesListsIndexRoute '/questionnaire/$questionnaireId/multimode': typeof LayoutQuestionnaireQuestionnaireIdLayoutQMultimodeIndexRoute '/questionnaire/$questionnaireId/nomenclatures': typeof LayoutQuestionnaireQuestionnaireIdLayoutQNomenclaturesIndexRoute '/questionnaire/$questionnaireId/personalization': typeof LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationIndexRoute + '/questionnaire/$questionnaireId/releases': typeof LayoutQuestionnaireQuestionnaireIdLayoutQReleasesIndexRoute '/questionnaire/$questionnaireId/variables': typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesIndexRoute '/questionnaire/$questionnaireId/variables/variable/$variableId': typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesVariableVariableIdRoute '/questionnaire/$questionnaireId/version/$versionId/articulation': typeof LayoutQuestionnaireQuestionnaireIdLayoutQVersionVersionIdArticulationRoute @@ -437,6 +465,7 @@ export interface FileRoutesById { '/_layout/questionnaire/$questionnaireId/_layout-q/multimode': typeof LayoutQuestionnaireQuestionnaireIdLayoutQMultimodeRouteRouteWithChildren '/_layout/questionnaire/$questionnaireId/_layout-q/nomenclatures': typeof LayoutQuestionnaireQuestionnaireIdLayoutQNomenclaturesRouteRouteWithChildren '/_layout/questionnaire/$questionnaireId/_layout-q/personalization': typeof LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationRouteRouteWithChildren + '/_layout/questionnaire/$questionnaireId/_layout-q/releases': typeof LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRouteWithChildren '/_layout/questionnaire/$questionnaireId/_layout-q/variables': typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesRouteRouteWithChildren '/_layout/questionnaire/$questionnaireId/_layout-q/composition': typeof LayoutQuestionnaireQuestionnaireIdLayoutQCompositionRoute '/_layout/questionnaire/$questionnaireId/_layout-q/duplicate-variables': typeof LayoutQuestionnaireQuestionnaireIdLayoutQDuplicateVariablesRoute @@ -452,12 +481,14 @@ export interface FileRoutesById { '/_layout/questionnaire/$questionnaireId/_layout-q/multimode/edit': typeof LayoutQuestionnaireQuestionnaireIdLayoutQMultimodeEditRoute '/_layout/questionnaire/$questionnaireId/_layout-q/personalization/$publicEnemyId': typeof LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationPublicEnemyIdRoute '/_layout/questionnaire/$questionnaireId/_layout-q/personalization/new': typeof LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationNewRoute + '/_layout/questionnaire/$questionnaireId/_layout-q/releases/new': typeof LayoutQuestionnaireQuestionnaireIdLayoutQReleasesNewRoute '/_layout/questionnaire/$questionnaireId/_layout-q/variables/new': typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesNewRoute '/_layout/questionnaire/$questionnaireId/_layout-q/articulation/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQArticulationIndexRoute '/_layout/questionnaire/$questionnaireId/_layout-q/codes-lists/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQCodesListsIndexRoute '/_layout/questionnaire/$questionnaireId/_layout-q/multimode/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQMultimodeIndexRoute '/_layout/questionnaire/$questionnaireId/_layout-q/nomenclatures/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQNomenclaturesIndexRoute '/_layout/questionnaire/$questionnaireId/_layout-q/personalization/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationIndexRoute + '/_layout/questionnaire/$questionnaireId/_layout-q/releases/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQReleasesIndexRoute '/_layout/questionnaire/$questionnaireId/_layout-q/variables/': typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesIndexRoute '/_layout/questionnaire/$questionnaireId/_layout-q/variables/variable/$variableId': typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesVariableVariableIdRoute '/_layout/questionnaire/$questionnaireId/_layout-q/version/$versionId/articulation': typeof LayoutQuestionnaireQuestionnaireIdLayoutQVersionVersionIdArticulationRoute @@ -481,6 +512,7 @@ export interface FileRouteTypes { | '/questionnaire/$questionnaireId/multimode' | '/questionnaire/$questionnaireId/nomenclatures' | '/questionnaire/$questionnaireId/personalization' + | '/questionnaire/$questionnaireId/releases' | '/questionnaire/$questionnaireId/variables' | '/questionnaire/$questionnaireId/composition' | '/questionnaire/$questionnaireId/duplicate-variables' @@ -496,12 +528,14 @@ export interface FileRouteTypes { | '/questionnaire/$questionnaireId/multimode/edit' | '/questionnaire/$questionnaireId/personalization/$publicEnemyId' | '/questionnaire/$questionnaireId/personalization/new' + | '/questionnaire/$questionnaireId/releases/new' | '/questionnaire/$questionnaireId/variables/new' | '/questionnaire/$questionnaireId/articulation/' | '/questionnaire/$questionnaireId/codes-lists/' | '/questionnaire/$questionnaireId/multimode/' | '/questionnaire/$questionnaireId/nomenclatures/' | '/questionnaire/$questionnaireId/personalization/' + | '/questionnaire/$questionnaireId/releases/' | '/questionnaire/$questionnaireId/variables/' | '/questionnaire/$questionnaireId/variables/variable/$variableId' | '/questionnaire/$questionnaireId/version/$versionId/articulation' @@ -529,12 +563,14 @@ export interface FileRouteTypes { | '/questionnaire/$questionnaireId/multimode/edit' | '/questionnaire/$questionnaireId/personalization/$publicEnemyId' | '/questionnaire/$questionnaireId/personalization/new' + | '/questionnaire/$questionnaireId/releases/new' | '/questionnaire/$questionnaireId/variables/new' | '/questionnaire/$questionnaireId/articulation' | '/questionnaire/$questionnaireId/codes-lists' | '/questionnaire/$questionnaireId/multimode' | '/questionnaire/$questionnaireId/nomenclatures' | '/questionnaire/$questionnaireId/personalization' + | '/questionnaire/$questionnaireId/releases' | '/questionnaire/$questionnaireId/variables' | '/questionnaire/$questionnaireId/variables/variable/$variableId' | '/questionnaire/$questionnaireId/version/$versionId/articulation' @@ -558,6 +594,7 @@ export interface FileRouteTypes { | '/_layout/questionnaire/$questionnaireId/_layout-q/multimode' | '/_layout/questionnaire/$questionnaireId/_layout-q/nomenclatures' | '/_layout/questionnaire/$questionnaireId/_layout-q/personalization' + | '/_layout/questionnaire/$questionnaireId/_layout-q/releases' | '/_layout/questionnaire/$questionnaireId/_layout-q/variables' | '/_layout/questionnaire/$questionnaireId/_layout-q/composition' | '/_layout/questionnaire/$questionnaireId/_layout-q/duplicate-variables' @@ -573,12 +610,14 @@ export interface FileRouteTypes { | '/_layout/questionnaire/$questionnaireId/_layout-q/multimode/edit' | '/_layout/questionnaire/$questionnaireId/_layout-q/personalization/$publicEnemyId' | '/_layout/questionnaire/$questionnaireId/_layout-q/personalization/new' + | '/_layout/questionnaire/$questionnaireId/_layout-q/releases/new' | '/_layout/questionnaire/$questionnaireId/_layout-q/variables/new' | '/_layout/questionnaire/$questionnaireId/_layout-q/articulation/' | '/_layout/questionnaire/$questionnaireId/_layout-q/codes-lists/' | '/_layout/questionnaire/$questionnaireId/_layout-q/multimode/' | '/_layout/questionnaire/$questionnaireId/_layout-q/nomenclatures/' | '/_layout/questionnaire/$questionnaireId/_layout-q/personalization/' + | '/_layout/questionnaire/$questionnaireId/_layout-q/releases/' | '/_layout/questionnaire/$questionnaireId/_layout-q/variables/' | '/_layout/questionnaire/$questionnaireId/_layout-q/variables/variable/$variableId' | '/_layout/questionnaire/$questionnaireId/_layout-q/version/$versionId/articulation' @@ -701,6 +740,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesRouteRouteImport parentRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQRoute } + '/_layout/questionnaire/$questionnaireId/_layout-q/releases': { + id: '/_layout/questionnaire/$questionnaireId/_layout-q/releases' + path: '/releases' + fullPath: '/questionnaire/$questionnaireId/releases' + preLoaderRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRouteImport + parentRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQRoute + } '/_layout/questionnaire/$questionnaireId/_layout-q/personalization': { id: '/_layout/questionnaire/$questionnaireId/_layout-q/personalization' path: '/personalization' @@ -743,6 +789,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesIndexRouteImport parentRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesRouteRoute } + '/_layout/questionnaire/$questionnaireId/_layout-q/releases/': { + id: '/_layout/questionnaire/$questionnaireId/_layout-q/releases/' + path: '/' + fullPath: '/questionnaire/$questionnaireId/releases/' + preLoaderRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQReleasesIndexRouteImport + parentRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRoute + } '/_layout/questionnaire/$questionnaireId/_layout-q/personalization/': { id: '/_layout/questionnaire/$questionnaireId/_layout-q/personalization/' path: '/' @@ -785,6 +838,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesNewRouteImport parentRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesRouteRoute } + '/_layout/questionnaire/$questionnaireId/_layout-q/releases/new': { + id: '/_layout/questionnaire/$questionnaireId/_layout-q/releases/new' + path: '/new' + fullPath: '/questionnaire/$questionnaireId/releases/new' + preLoaderRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQReleasesNewRouteImport + parentRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRoute + } '/_layout/questionnaire/$questionnaireId/_layout-q/personalization/new': { id: '/_layout/questionnaire/$questionnaireId/_layout-q/personalization/new' path: '/new' @@ -1002,6 +1062,24 @@ const LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationRouteRouteWithChil LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationRouteRouteChildren, ) +interface LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRouteChildren { + LayoutQuestionnaireQuestionnaireIdLayoutQReleasesNewRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQReleasesNewRoute + LayoutQuestionnaireQuestionnaireIdLayoutQReleasesIndexRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQReleasesIndexRoute +} + +const LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRouteChildren: LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRouteChildren = + { + LayoutQuestionnaireQuestionnaireIdLayoutQReleasesNewRoute: + LayoutQuestionnaireQuestionnaireIdLayoutQReleasesNewRoute, + LayoutQuestionnaireQuestionnaireIdLayoutQReleasesIndexRoute: + LayoutQuestionnaireQuestionnaireIdLayoutQReleasesIndexRoute, + } + +const LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRouteWithChildren = + LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRoute._addFileChildren( + LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRouteChildren, + ) + interface LayoutQuestionnaireQuestionnaireIdLayoutQVariablesRouteRouteChildren { LayoutQuestionnaireQuestionnaireIdLayoutQVariablesNewRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesNewRoute LayoutQuestionnaireQuestionnaireIdLayoutQVariablesIndexRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesIndexRoute @@ -1059,6 +1137,7 @@ interface LayoutQuestionnaireQuestionnaireIdLayoutQRouteChildren { LayoutQuestionnaireQuestionnaireIdLayoutQMultimodeRouteRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQMultimodeRouteRouteWithChildren LayoutQuestionnaireQuestionnaireIdLayoutQNomenclaturesRouteRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQNomenclaturesRouteRouteWithChildren LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationRouteRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationRouteRouteWithChildren + LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRouteWithChildren LayoutQuestionnaireQuestionnaireIdLayoutQVariablesRouteRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQVariablesRouteRouteWithChildren LayoutQuestionnaireQuestionnaireIdLayoutQCompositionRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQCompositionRoute LayoutQuestionnaireQuestionnaireIdLayoutQDuplicateVariablesRoute: typeof LayoutQuestionnaireQuestionnaireIdLayoutQDuplicateVariablesRoute @@ -1082,6 +1161,8 @@ const LayoutQuestionnaireQuestionnaireIdLayoutQRouteChildren: LayoutQuestionnair LayoutQuestionnaireQuestionnaireIdLayoutQNomenclaturesRouteRouteWithChildren, LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationRouteRoute: LayoutQuestionnaireQuestionnaireIdLayoutQPersonalizationRouteRouteWithChildren, + LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRoute: + LayoutQuestionnaireQuestionnaireIdLayoutQReleasesRouteRouteWithChildren, LayoutQuestionnaireQuestionnaireIdLayoutQVariablesRouteRoute: LayoutQuestionnaireQuestionnaireIdLayoutQVariablesRouteRouteWithChildren, LayoutQuestionnaireQuestionnaireIdLayoutQCompositionRoute: diff --git a/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/index.tsx b/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/index.tsx new file mode 100644 index 000000000..aad9d69c4 --- /dev/null +++ b/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/index.tsx @@ -0,0 +1,36 @@ +import { createFileRoute } from '@tanstack/react-router' + +import ErrorComponent from '@/components/layout/ErrorComponent' +import ReleaseOverview from '@/components/release/ReleaseOverview' +import ReleaseOverviewLayout from '@/components/release/ReleasesOverviewLayout' + +export const Route = createFileRoute( + '/_layout/questionnaire/$questionnaireId/_layout-q/releases/', +)({ + component: RouteComponent, + errorComponent: ({ error }) => ( + + + + ), +}) + +function RouteComponent() { + const questionnaireId = Route.useParams().questionnaireId + + return ( + + + + ) +} + +function CustomLayout({ children }: Readonly<{ children: React.ReactNode }>) { + const { questionnaireId } = Route.useParams() + + return ( + + {children} + + ) +} diff --git a/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/new.tsx b/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/new.tsx new file mode 100644 index 000000000..646d77aa7 --- /dev/null +++ b/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/new.tsx @@ -0,0 +1,39 @@ +import { useSuspenseQuery } from '@tanstack/react-query' +import { createFileRoute } from '@tanstack/react-router' + +import { questionnaireQueryOptions } from '@/api/questionnaires' +import ErrorComponent from '@/components/layout/ErrorComponent' +import CreateRelease from '@/components/release/create/CreateRelease' +import CreateReleaseLayout from '@/components/release/create/CreateReleaseLayout' +import type { Questionnaire } from '@/models/questionnaires' + +export const Route = createFileRoute( + '/_layout/questionnaire/$questionnaireId/_layout-q/releases/new', +)({ + component: RouteComponent, + errorComponent: ({ error }) => ( + + + + ), + loader: async ({ context: { t } }) => { + return { crumb: t('crumb.new') } + }, +}) + +function RouteComponent() { + const questionnaireId = Route.useParams().questionnaireId + + const { data: questionnaire }: { data: Questionnaire } = useSuspenseQuery( + questionnaireQueryOptions(questionnaireId), + ) + + return ( + + + + ) +} diff --git a/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/route.tsx b/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/route.tsx new file mode 100644 index 000000000..db6b101bb --- /dev/null +++ b/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/route.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute( + '/_layout/questionnaire/$questionnaireId/_layout-q/releases', +)({ + loader: async ({ context: { t } }) => { + return { crumb: t('crumb.release') } + }, +}) From 4c56e126502387057796264ecf621715af09817d Mon Sep 17 00:00:00 2001 From: Mailine Nguyen <64129348+MailineN@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:24:48 +0200 Subject: [PATCH 2/6] feat: add remote related releases functions --- CHANGELOG.md | 5 + next/src/api/models/releaseDTO.ts | 63 ++++++ next/src/api/releases.test.ts | 113 +++++++++++ next/src/api/releases.ts | 87 ++++++++ next/src/api/utils/releases.test.ts | 114 +++++++++++ next/src/api/utils/releases.ts | 90 +++++++++ .../release/ReleaseOverview.test.tsx | 2 +- .../release/create/CreateReleaseForm.tsx | 30 +-- .../create/CreateReleaseLayout.test.tsx | 14 ++ .../release/form/ReleaseForm.test.tsx | 185 ++++++++++++++++++ .../components/release/form/ReleaseForm.tsx | 34 ++-- next/src/components/release/form/consts.ts | 10 +- next/src/components/release/form/schema.ts | 17 +- .../release/overview/ReleaseRequestTile.tsx | 3 +- .../components/ui/icons/ErrorIcon.test.tsx | 8 + .../src/components/ui/icons/InfoIcon.test.tsx | 8 + .../__snapshots__/ErrorIcon.test.tsx.snap | 18 ++ .../__snapshots__/InfoIcon.test.tsx.snap | 16 ++ next/src/lib/i18n/locales/en/release.json | 5 +- next/src/lib/i18n/locales/fr/release.json | 1 + 20 files changed, 777 insertions(+), 46 deletions(-) create mode 100644 next/src/api/models/releaseDTO.ts create mode 100644 next/src/api/releases.test.ts create mode 100644 next/src/api/releases.ts create mode 100644 next/src/api/utils/releases.test.ts create mode 100644 next/src/api/utils/releases.ts create mode 100644 next/src/components/release/create/CreateReleaseLayout.test.tsx create mode 100644 next/src/components/release/form/ReleaseForm.test.tsx create mode 100644 next/src/components/ui/icons/ErrorIcon.test.tsx create mode 100644 next/src/components/ui/icons/InfoIcon.test.tsx create mode 100644 next/src/components/ui/icons/__snapshots__/ErrorIcon.test.tsx.snap create mode 100644 next/src/components/ui/icons/__snapshots__/InfoIcon.test.tsx.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index e76b10605..b36004d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Page to overview released and pending releases of a questionnaire +- Form to create a release of a questionnaire + ## [3.5.0](https://github.com/InseeFr/Pogues/releases/tag/3.5.0) - 2026-07-06 ### Added diff --git a/next/src/api/models/releaseDTO.ts b/next/src/api/models/releaseDTO.ts new file mode 100644 index 000000000..3421fe21e --- /dev/null +++ b/next/src/api/models/releaseDTO.ts @@ -0,0 +1,63 @@ +/** Model of the registry release returned by the Pogues API's releases endpoint. */ +export type RegistryReleaseDTO = { + collectionInstrumentId: string + version: number + author: string + releaseDate: number + poguesVersionId: string + releaseDescription: string + mode: TargetModesDTO + context: ReleaseContextDTO + overrideGenerationParameters: OverrideGenerationParametersDTO | null + visualizeUrl: string +} + +/** Model of the release request returned by the Pogues API's release-requests endpoint. */ +export type ReleaseRequestDTO = { + trackerId: number + author: string + requestDate: number + currentStep: CurrentStepDTO + status: ReleaseRequestStatusDTO + statusDescription: string + poguesVersionId: string + poguesId: string + releaseDescription: string + mode: TargetModesDTO + context: ReleaseContextDTO + overrideGenerationParameters: OverrideGenerationParametersDTO +} + +export type CreateReleaseDTO = { + poguesId: string + releaseDescription: string + mode: TargetModesDTO + context: ReleaseContextDTO + overrideGenerationParameters: OverrideGenerationParametersDTO +} + +export type OverrideGenerationParametersDTO = { + questionNumberingMode: QuestionNumberingModeDTO + responseTimeQuestion: boolean +} + +export type TargetModesDTO = 'CAWI' | 'CAPI' | 'PAPI' | 'CATI' + +export type ReleaseContextDTO = 'HOUSEHOLD' | 'BUSINESS' + +export type CurrentStepDTO = + | 'BUILD_PARAMETERS' + | 'PUBLISH_PRERELEASE' + | 'GENERATE_DDI' + | 'GENERATE_LUNATIC' + | 'PUBLISH_DDI' + | 'PUBLISH_LUNATIC' + | 'FINISHED' + +export type ReleaseRequestStatusDTO = + | 'CREATED' + | 'RUNNING' + | 'COMPLETED' + | 'FAILED' + +export type QuestionNumberingModeDTO = 'NONE' | 'SEQUENCE' | 'ALL' diff --git a/next/src/api/releases.test.ts b/next/src/api/releases.test.ts new file mode 100644 index 000000000..e2dae51ee --- /dev/null +++ b/next/src/api/releases.test.ts @@ -0,0 +1,113 @@ +import nock from 'nock' + +import { TargetModes } from '@/models/questionnaires' +import type { RegistryRelease, ReleaseRequest } from '@/models/releases' + +import type { + CreateReleaseDTO, + RegistryReleaseDTO, + ReleaseRequestDTO, +} from './models/releaseDTO' +import { + deleteReleaseRequest, + getPendingReleases, + getReleases, + postRelease, +} from './releases' + +vi.mock('@/lib/auth/oidc') + +const registryReleaseDTO: RegistryReleaseDTO = { + collectionInstrumentId: '550e8400-e29b-41d4-a716-446655440001', + version: 3, + author: 'xbeltv', + releaseDate: 1780560000000, + poguesVersionId: '93d1e85c-327d-4153-a5fa-e04f54ca0e3e', + releaseDescription: 'ESA 2026 PROD', + mode: 'CAWI', + context: 'HOUSEHOLD', + overrideGenerationParameters: { + questionNumberingMode: 'NONE', + responseTimeQuestion: false, + }, + visualizeUrl: 'https://visu.example.com/esa-2026-prod', +} + +const registryRelease: RegistryRelease = { + ...registryReleaseDTO, + mode: TargetModes.CAWI, +} + +const releaseRequestDTO: ReleaseRequestDTO = { + trackerId: 1, + author: 'xbeltv', + requestDate: 1780560000000, + currentStep: 'BUILD_PARAMETERS', + status: 'RUNNING', + statusDescription: '', + poguesVersionId: '550e8400-e29b-41d4-a716-446655440000', + poguesId: 'SRCV_REINTERRO', + releaseDescription: 'Release description', + mode: 'CAPI', + context: 'HOUSEHOLD', + overrideGenerationParameters: { + questionNumberingMode: 'SEQUENCE', + responseTimeQuestion: true, + }, +} + +const releaseRequest: ReleaseRequest = { + ...releaseRequestDTO, + mode: TargetModes.CAPI, +} + +it('Get releases works', async () => { + const releases: RegistryReleaseDTO[] = [registryReleaseDTO] + + nock('https://mock-api') + .get('/persistence/questionnaire/my-questionnaire/releases') + .reply(200, releases) + + const res = await getReleases('my-questionnaire') + expect(res).toEqual([registryRelease]) +}) + +it('Get pending releases works', async () => { + const requests: ReleaseRequestDTO[] = [releaseRequestDTO] + + nock('https://mock-api') + .get('/persistence/questionnaire/my-questionnaire/release-requests') + .reply(200, requests) + + const res = await getPendingReleases('my-questionnaire') + expect(res).toEqual([releaseRequest]) +}) + +it('Post release works', async () => { + const createRelease: CreateReleaseDTO = { + poguesId: 'my-questionnaire', + releaseDescription: 'New release', + mode: 'CAWI', + context: 'HOUSEHOLD', + overrideGenerationParameters: { + questionNumberingMode: 'SEQUENCE', + responseTimeQuestion: true, + }, + } + + nock('https://mock-api') + .post('/persistence/questionnaire/my-questionnaire/releases') + .reply(201) + + const res = await postRelease('my-questionnaire', createRelease) + expect(res.status).toEqual(201) +}) + +it('Delete release request works', async () => { + nock('https://mock-api') + .delete('/persistence/questionnaire/my-questionnaire/release-requests/42') + .reply(204) + + const res = await deleteReleaseRequest('my-questionnaire', 42) + expect(res.status).toEqual(204) +}) diff --git a/next/src/api/releases.ts b/next/src/api/releases.ts new file mode 100644 index 000000000..4435a09b5 --- /dev/null +++ b/next/src/api/releases.ts @@ -0,0 +1,87 @@ +import { queryOptions } from '@tanstack/react-query' + +import type { RegistryRelease, ReleaseRequest } from '@/models/releases' + +import { instance } from './instance' +import type { + CreateReleaseDTO, + RegistryReleaseDTO, + ReleaseRequestDTO, +} from './models/releaseDTO' +import { + computeRegistryReleases, + computeReleaseRequests, +} from './utils/releases' + +export const releasesKeys = { + released: (questionnaireId: string) => ['released', questionnaireId] as const, + pending: (questionnaireId: string) => ['pending', questionnaireId] as const, +} + +/** + * Used to retrieve questionnaire registry releases associated to its id. + */ +export const releasesQueryOptions = (questionnaireId: string) => + queryOptions({ + queryKey: releasesKeys.released(questionnaireId), + queryFn: () => getReleases(questionnaireId), + }) + +/** + * Used to retrieve pending questionnaire registry releases associated to its id. + */ +export const pendingReleasesQueryOptions = (questionnaireId: string) => + queryOptions({ + queryKey: releasesKeys.pending(questionnaireId), + queryFn: () => getPendingReleases(questionnaireId), + }) + +/** Retrieve questionnaire releases by the questionnaire id. */ +export async function getReleases( + questionnaireId: string, +): Promise { + return instance + .get(`/persistence/questionnaire/${questionnaireId}/releases`, { + headers: { Accept: 'application/json' }, + }) + .then(({ data }: { data: RegistryReleaseDTO[] }) => { + return computeRegistryReleases(data) + }) +} + +/** Retrieve questionnaire pending releases by the questionnaire id. */ +export async function getPendingReleases( + questionnaireId: string, +): Promise { + return instance + .get(`/persistence/questionnaire/${questionnaireId}/release-requests`, { + headers: { Accept: 'application/json' }, + }) + .then(({ data }: { data: ReleaseRequestDTO[] }) => { + return computeReleaseRequests(data) + }) +} + +/** Create a new release. */ +export async function postRelease( + questionnaireId: string, + release: CreateReleaseDTO, +): Promise { + return instance.post( + `/persistence/questionnaire/${questionnaireId}/releases`, + release, + { + headers: { 'Content-Type': 'application/json' }, + }, + ) +} + +/** Delete a release request by tracker id. */ +export async function deleteReleaseRequest( + questionnaireId: string, + trackerId: number, +): Promise { + return instance.delete( + `/persistence/questionnaire/${questionnaireId}/release-requests/${trackerId}`, + ) +} diff --git a/next/src/api/utils/releases.test.ts b/next/src/api/utils/releases.test.ts new file mode 100644 index 000000000..08ab60977 --- /dev/null +++ b/next/src/api/utils/releases.test.ts @@ -0,0 +1,114 @@ +import { describe, expect, it } from 'vitest' + +import { TargetModes } from '@/models/questionnaires' + +import type { + RegistryReleaseDTO, + ReleaseRequestDTO, +} from '../models/releaseDTO' +import { + computeRegistryRelease, + computeRegistryReleaseDTO, + computeReleaseRequest, + computeReleaseRequestDTO, +} from './releases' + +describe('computeRegistryRelease', () => { + it.each([ + ['CAWI', TargetModes.CAWI], + ['CAPI', TargetModes.CAPI], + ['PAPI', TargetModes.PAPI], + ['CATI', TargetModes.CATI], + ] as const)( + 'should compute a registry release with %s mode correctly', + (modeDTO, modeModel) => { + const dto: RegistryReleaseDTO = { + collectionInstrumentId: '550e8400-e29b-41d4-a716-446655440001', + version: 3, + author: 'xbeltv', + releaseDate: 1780560000000, + poguesVersionId: '93d1e85c-327d-4153-a5fa-e04f54ca0e3e', + releaseDescription: 'ESA 2026 PROD', + mode: modeDTO, + context: 'HOUSEHOLD', + overrideGenerationParameters: { + questionNumberingMode: 'NONE', + responseTimeQuestion: false, + }, + visualizeUrl: 'https://visu.example.com/esa-2026-prod', + } + + const result = computeRegistryRelease(dto) + expect(result).toEqual({ + ...dto, + mode: modeModel, + }) + + const resultDTO = computeRegistryReleaseDTO(result) + expect(resultDTO).toEqual(dto) + }, + ) + + it('should compute a registry release with null overrideGenerationParameters', () => { + const dto: RegistryReleaseDTO = { + collectionInstrumentId: '550e8400-e29b-41d4-a716-446655440001', + version: 1, + author: 'bcbab8', + releaseDate: 1780560000000, + poguesVersionId: 'b77e7cad-475d-4d83-b036-fa7a98a84a8a', + releaseDescription: 'Old publication', + mode: 'CAWI', + context: 'HOUSEHOLD', + overrideGenerationParameters: null, + visualizeUrl: 'https://visu.example.com/old', + } + + const result = computeRegistryRelease(dto) + expect(result).toEqual({ + ...dto, + mode: TargetModes.CAWI, + }) + + const resultDTO = computeRegistryReleaseDTO(result) + expect(resultDTO).toEqual(dto) + }) +}) + +describe('computeReleaseRequest', () => { + it.each([ + ['CAWI', TargetModes.CAWI], + ['CAPI', TargetModes.CAPI], + ['PAPI', TargetModes.PAPI], + ['CATI', TargetModes.CATI], + ] as const)( + 'should compute a release request with %s mode correctly', + (modeDTO, modeModel) => { + const dto: ReleaseRequestDTO = { + trackerId: 1, + author: 'xbeltv', + requestDate: 1780560000000, + currentStep: 'BUILD_PARAMETERS', + status: 'RUNNING', + statusDescription: '', + poguesVersionId: '550e8400-e29b-41d4-a716-446655440000', + poguesId: 'SRCV_REINTERRO', + releaseDescription: 'Release description', + mode: modeDTO, + context: 'HOUSEHOLD', + overrideGenerationParameters: { + questionNumberingMode: 'SEQUENCE', + responseTimeQuestion: true, + }, + } + + const result = computeReleaseRequest(dto) + expect(result).toEqual({ + ...dto, + mode: modeModel, + }) + + const resultDTO = computeReleaseRequestDTO(result) + expect(resultDTO).toEqual(dto) + }, + ) +}) diff --git a/next/src/api/utils/releases.ts b/next/src/api/utils/releases.ts new file mode 100644 index 000000000..e7300d960 --- /dev/null +++ b/next/src/api/utils/releases.ts @@ -0,0 +1,90 @@ +import { TargetModes } from '@/models/questionnaires' +import type { RegistryRelease, ReleaseRequest } from '@/models/releases' + +import type { + RegistryReleaseDTO, + ReleaseRequestDTO, + TargetModesDTO, +} from '../models/releaseDTO' + +/** Compute registry releases from API data. */ +export function computeRegistryReleases( + dtos: RegistryReleaseDTO[], +): RegistryRelease[] { + return dtos.map((dto) => computeRegistryRelease(dto)) +} + +/** Compute a registry release from API data. */ +export function computeRegistryRelease( + dto: RegistryReleaseDTO, +): RegistryRelease { + return { + ...dto, + mode: computeTargetModes(dto.mode), + } +} + +/** Compute a registry release DTO from model data. */ +export function computeRegistryReleaseDTO( + release: RegistryRelease, +): RegistryReleaseDTO { + return { + ...release, + mode: computeTargetModesDTO(release.mode), + } +} + +/** Compute release requests from API data. */ +export function computeReleaseRequests( + dtos: ReleaseRequestDTO[], +): ReleaseRequest[] { + return dtos.map((dto) => computeReleaseRequest(dto)) +} + +/** Compute a release request from API data. */ +export function computeReleaseRequest(dto: ReleaseRequestDTO): ReleaseRequest { + return { + ...dto, + mode: computeTargetModes(dto.mode), + } +} + +/** Compute a release request DTO from model data. */ +export function computeReleaseRequestDTO( + request: ReleaseRequest, +): ReleaseRequestDTO { + return { + ...request, + mode: computeTargetModesDTO(request.mode), + } +} + +function computeTargetModes(mode: TargetModesDTO): TargetModes { + switch (mode) { + case 'CAWI': + return TargetModes.CAWI + case 'CAPI': + return TargetModes.CAPI + case 'PAPI': + return TargetModes.PAPI + case 'CATI': + return TargetModes.CATI + default: + throw new Error('Unknown target mode') + } +} + +function computeTargetModesDTO(mode: TargetModes): TargetModesDTO { + switch (mode) { + case TargetModes.CAWI: + return 'CAWI' + case TargetModes.CAPI: + return 'CAPI' + case TargetModes.PAPI: + return 'PAPI' + case TargetModes.CATI: + return 'CATI' + default: + throw new Error('Unknown target mode') + } +} diff --git a/next/src/components/release/ReleaseOverview.test.tsx b/next/src/components/release/ReleaseOverview.test.tsx index 9f26496c8..91420945f 100644 --- a/next/src/components/release/ReleaseOverview.test.tsx +++ b/next/src/components/release/ReleaseOverview.test.tsx @@ -10,7 +10,7 @@ describe('ReleaseOverview', () => { it('renders both sections with correct headers', () => { const { getByText } = renderWithI18n() - expect(getByText('My publication requests')).toBeInTheDocument() + expect(getByText('My release requests')).toBeInTheDocument() expect(getByText('My publications')).toBeInTheDocument() }) diff --git a/next/src/components/release/create/CreateReleaseForm.tsx b/next/src/components/release/create/CreateReleaseForm.tsx index 13d6638f7..310e3be5d 100644 --- a/next/src/components/release/create/CreateReleaseForm.tsx +++ b/next/src/components/release/create/CreateReleaseForm.tsx @@ -1,10 +1,12 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { useNavigate } from '@tanstack/react-router' +import { isAxiosError } from 'axios' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' +import { instance } from '@/api/instance' import { questionnairesKeys } from '@/api/questionnaires' -import { TargetModes } from '@/models/questionnaires' +import type { TargetModes } from '@/models/questionnaires' import ReleaseForm from '../form/ReleaseForm' import { type FormValues } from '../form/schema' @@ -28,19 +30,21 @@ export default function CreateReleaseForm({ const mutation = useMutation({ mutationFn: async (formValues: FormValues) => { - // TODO: Replace with actual API call when release endpoint is available - const response = await fetch('/api/releases', { - method: 'POST', - body: JSON.stringify({ - questionnaireId, - ...formValues, - }), - headers: { 'Content-Type': 'application/json' }, - }) - if (!response.ok) { - throw new Error('Failed to create release') + try { + const response = await instance.post( + `/api/questionnaire/${questionnaireId}/releases`, + { + poguesId: questionnaireId, + ...formValues, + }, + ) + return response.data + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + throw new Error(t('release.create.notFound')) + } + throw error } - return response.json() }, onSuccess: () => { queryClient.invalidateQueries({ diff --git a/next/src/components/release/create/CreateReleaseLayout.test.tsx b/next/src/components/release/create/CreateReleaseLayout.test.tsx new file mode 100644 index 000000000..cd67170ce --- /dev/null +++ b/next/src/components/release/create/CreateReleaseLayout.test.tsx @@ -0,0 +1,14 @@ +import { renderWithRouter } from '@/testing/render' + +import CreateReleaseLayout from './CreateReleaseLayout' + +describe('CreateCodesListLayout', () => { + it('displays title and children', async () => { + const { getByText } = await renderWithRouter( + Hello world, + ) + + expect(getByText('New release')).toBeInTheDocument() + expect(getByText('Hello world')).toBeInTheDocument() + }) +}) diff --git a/next/src/components/release/form/ReleaseForm.test.tsx b/next/src/components/release/form/ReleaseForm.test.tsx new file mode 100644 index 000000000..e1c563184 --- /dev/null +++ b/next/src/components/release/form/ReleaseForm.test.tsx @@ -0,0 +1,185 @@ +import { screen, waitFor, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' + +import { TargetModes } from '@/models/questionnaires' +import { renderWithRouter } from '@/testing/render' + +import ReleaseForm from './ReleaseForm' + +const TARGET_MODES = new Set([ + TargetModes.CAWI, + TargetModes.CAPI, + TargetModes.CATI, +]) + +describe('ReleaseForm', () => { + it('should disable submit button when description is empty', async () => { + await renderWithRouter( + , + ) + + await waitFor(() => { + expect(screen.getByTestId('form-submit-button')).toBeDisabled() + }) + }) + + it('should enable submit button when description is filled', async () => { + const user = userEvent.setup() + + const { getByRole } = await renderWithRouter( + , + ) + + await waitFor(() => { + expect(screen.getByTestId('form-submit-button')).toBeDisabled() + }) + + await user.type( + getByRole('textbox', { name: /Description/i }), + 'My release', + ) + + await waitFor(() => { + expect(screen.getByTestId('form-submit-button')).toBeEnabled() + }) + }) + + it('should call onSubmit with form values when submitted', async () => { + const user = userEvent.setup() + const onSubmit = vi.fn() + + const { getByRole } = await renderWithRouter( + , + ) + + await user.type( + getByRole('textbox', { name: /Description/i }), + 'My release', + ) + + await user.click(screen.getByTestId('form-submit-button')) + + await waitFor(() => { + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + releaseDescription: 'My release', + mode: 'CAWI', + context: 'HOUSEHOLD', + overrideGenerationParameters: expect.objectContaining({ + responseTimeQuestion: false, + questionNumberingMode: 'SEQUENCE', + }), + }), + expect.anything(), + ) + }) + }) + + it('should show optional parameters section for BUSINESS context', async () => { + const user = userEvent.setup() + + await renderWithRouter( + , + ) + + const contextGroup = screen.getByRole('radiogroup', { name: /Context/ }) + const contextRadios = within(contextGroup).getAllByRole('radio') + await user.click(contextRadios[1]) + + await waitFor(() => { + expect(screen.getByText('Optional parameters')).toBeInTheDocument() + }) + }) + + it('should hide optional parameters section for HOUSEHOLD context', async () => { + await renderWithRouter( + , + ) + + expect(screen.queryByText('Optional parameters')).not.toBeInTheDocument() + }) + + it('should toggle optional parameters section when switching context', async () => { + const user = userEvent.setup() + + await renderWithRouter( + , + ) + + const contextGroup = screen.getByRole('radiogroup', { name: /Context/ }) + const contextRadios = within(contextGroup).getAllByRole('radio') + + expect(screen.queryByText('Optional parameters')).not.toBeInTheDocument() + + await user.click(contextRadios[1]) + + await waitFor(() => { + expect(screen.getByText('Optional parameters')).toBeInTheDocument() + }) + + await user.click(contextRadios[0]) + + await waitFor(() => { + expect(screen.queryByText('Optional parameters')).not.toBeInTheDocument() + }) + }) + + it('should disable submit button when series is missing', async () => { + await renderWithRouter( + , + ) + + await waitFor(() => { + expect(screen.getByTestId('form-submit-button')).toBeDisabled() + }) + }) +}) diff --git a/next/src/components/release/form/ReleaseForm.tsx b/next/src/components/release/form/ReleaseForm.tsx index 3cc5bc05b..cd0b3cb2d 100644 --- a/next/src/components/release/form/ReleaseForm.tsx +++ b/next/src/components/release/form/ReleaseForm.tsx @@ -11,7 +11,7 @@ import Input from '@/components/ui/form/Input' import RadioGroup from '@/components/ui/form/RadioGroup' import Select from '@/components/ui/form/Select' import InfoIcon from '@/components/ui/icons/InfoIcon' -import { TargetModes } from '@/models/questionnaires' +import type { TargetModes } from '@/models/questionnaires' import { CONTEXTE_OPTIONS, NUMEROTATION_OPTIONS } from './consts' import { type FormValues, schema } from './schema' @@ -43,16 +43,18 @@ export default function ReleaseForm({ } = useForm({ mode: 'onChange', defaultValues: { - description: '', - collectMode: TargetModes.CAWI, - contexte: 'menages', - pageTempsReponse: false, - numerotationQuestions: 'sequence', + releaseDescription: '', + mode: 'CAWI', + context: 'HOUSEHOLD', + overrideGenerationParameters: { + responseTimeQuestion: false, + questionNumberingMode: 'SEQUENCE', + }, }, resolver: zodResolver(schema), }) - const contexteValue = watch('contexte') + const contextValue = watch('context') const isSeriesMissing = !seriesId || !seriesLabel const isFormValid = isValid && !isSeriesMissing @@ -130,7 +132,7 @@ export default function ReleaseForm({ - {contexteValue === 'entreprise' ? ( + {contextValue === 'BUSINESS' ? ( {t('release.form.optionalParameters')} @@ -231,7 +233,7 @@ export default function ReleaseForm({ ( <> @@ -253,7 +255,7 @@ export default function ReleaseForm({ diff --git a/next/src/components/release/overview/ReleaseRequestTile.tsx b/next/src/components/release/overview/ReleaseRequestTile.tsx index b73f3e787..da5a998e7 100644 --- a/next/src/components/release/overview/ReleaseRequestTile.tsx +++ b/next/src/components/release/overview/ReleaseRequestTile.tsx @@ -1,3 +1,4 @@ +import type { TFunction } from 'i18next' import { useTranslation } from 'react-i18next' import { useState } from 'react' @@ -13,7 +14,7 @@ import { ReleaseOptionalParametersDisplay } from './ReleaseOptionalParametersDis function getStatusLabel( status: ReleaseRequest['status'], - t: (key: string) => string, + t: TFunction, ): string { switch (status) { case 'COMPLETED': diff --git a/next/src/components/ui/icons/ErrorIcon.test.tsx b/next/src/components/ui/icons/ErrorIcon.test.tsx new file mode 100644 index 000000000..2b8d2edd6 --- /dev/null +++ b/next/src/components/ui/icons/ErrorIcon.test.tsx @@ -0,0 +1,8 @@ +import { render } from '@testing-library/react' + +import ErrorIcon from './ErrorIcon' + +it('ErrorIcon renders correctly', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() +}) diff --git a/next/src/components/ui/icons/InfoIcon.test.tsx b/next/src/components/ui/icons/InfoIcon.test.tsx new file mode 100644 index 000000000..e12c9e860 --- /dev/null +++ b/next/src/components/ui/icons/InfoIcon.test.tsx @@ -0,0 +1,8 @@ +import { render } from '@testing-library/react' + +import InfoIcon from './InfoIcon' + +it('InfoIcon renders correctly', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() +}) diff --git a/next/src/components/ui/icons/__snapshots__/ErrorIcon.test.tsx.snap b/next/src/components/ui/icons/__snapshots__/ErrorIcon.test.tsx.snap new file mode 100644 index 000000000..f47fa4dc4 --- /dev/null +++ b/next/src/components/ui/icons/__snapshots__/ErrorIcon.test.tsx.snap @@ -0,0 +1,18 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ErrorIcon renders correctly 1`] = ` + + + + + + +`; diff --git a/next/src/components/ui/icons/__snapshots__/InfoIcon.test.tsx.snap b/next/src/components/ui/icons/__snapshots__/InfoIcon.test.tsx.snap new file mode 100644 index 000000000..1e73784e0 --- /dev/null +++ b/next/src/components/ui/icons/__snapshots__/InfoIcon.test.tsx.snap @@ -0,0 +1,16 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`InfoIcon renders correctly 1`] = ` + + + + + +`; diff --git a/next/src/lib/i18n/locales/en/release.json b/next/src/lib/i18n/locales/en/release.json index 6f086e1fd..b13e44d59 100644 --- a/next/src/lib/i18n/locales/en/release.json +++ b/next/src/lib/i18n/locales/en/release.json @@ -6,11 +6,12 @@ "questionnaireIsReadonly": "This release of the survey is on readonly.", "title": "Releases", "create": "Create a release", - "create.title": "New publication", + "create.title": "New release", "create.success": "Publication created successfully", + "create.notFound": "Questionnaire not found", "view": "View", "noPublication": "The questionnaire has not yet been published", - "publicationRequests": "My publication requests", + "publicationRequests": "My release requests", "myPublications": "My publications", "collectMode": "Collection mode", "contexte": "Context", diff --git a/next/src/lib/i18n/locales/fr/release.json b/next/src/lib/i18n/locales/fr/release.json index 49ef0a69f..c608aebfa 100644 --- a/next/src/lib/i18n/locales/fr/release.json +++ b/next/src/lib/i18n/locales/fr/release.json @@ -9,6 +9,7 @@ "create": "Publier un instrument de collecte", "create.title": "Nouvelle publication", "create.success": "Publication créée avec succès", + "create.notFound": "Questionnaire introuvable", "noPublication": "Le questionnaire n'a pas encore été publié", "publicationRequests": "Mes demandes de publication", "myPublications": "Mes publications", From aeabe6b867b7ff23b55c83121e6e6644d4298b12 Mon Sep 17 00:00:00 2001 From: Mailine Nguyen <64129348+MailineN@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:25:46 +0200 Subject: [PATCH 3/6] fix: labelling --- .../components/release/form/ReleaseForm.tsx | 8 ++--- next/src/components/release/form/consts.ts | 6 ++-- .../overview/RegistryReleaseTile.test.tsx | 2 +- .../release/overview/RegistryReleaseTile.tsx | 4 +-- .../ReleaseOptionalParametersDisplay.test.tsx | 2 +- .../ReleaseOptionalParametersDisplay.tsx | 20 +++++++++++-- .../overview/ReleaseRequestTile.test.tsx | 2 +- .../release/overview/ReleaseRequestTile.tsx | 29 +++++-------------- next/src/lib/i18n/locales/en/release.json | 7 +++-- next/src/lib/i18n/locales/fr/release.json | 7 +++-- 10 files changed, 43 insertions(+), 44 deletions(-) diff --git a/next/src/components/release/form/ReleaseForm.tsx b/next/src/components/release/form/ReleaseForm.tsx index cd0b3cb2d..34b111ee2 100644 --- a/next/src/components/release/form/ReleaseForm.tsx +++ b/next/src/components/release/form/ReleaseForm.tsx @@ -267,12 +267,10 @@ export default function ReleaseForm({ invalid={invalid} label={ - {t('release.form.numerotationQuestions.label')} + {t('release.form.questionNumbering.label')} diff --git a/next/src/components/release/form/consts.ts b/next/src/components/release/form/consts.ts index 32dcee4db..c3b7099a7 100644 --- a/next/src/components/release/form/consts.ts +++ b/next/src/components/release/form/consts.ts @@ -10,15 +10,15 @@ export const CONTEXTE_OPTIONS = [ export const NUMEROTATION_OPTIONS = [ { - label: t('release.form.numerotationQuestions.none'), + label: t('release.form.questionNumbering.none'), value: 'NONE' as const, }, { - label: t('release.form.numerotationQuestions.sequence'), + label: t('release.form.questionNumbering.sequence'), value: 'SEQUENCE' as const, }, { - label: t('release.form.numerotationQuestions.questionnaire'), + label: t('release.form.questionNumbering.questionnaire'), value: 'ALL' as const, }, ] diff --git a/next/src/components/release/overview/RegistryReleaseTile.test.tsx b/next/src/components/release/overview/RegistryReleaseTile.test.tsx index eb9014e64..9a0bb350b 100644 --- a/next/src/components/release/overview/RegistryReleaseTile.test.tsx +++ b/next/src/components/release/overview/RegistryReleaseTile.test.tsx @@ -52,7 +52,7 @@ describe('RegistryReleaseTile', () => { expect( getByText((content) => content.startsWith('Optional parameters')), ).toBeInTheDocument() - expect(getByText('SEQUENCE')).toBeInTheDocument() + expect(getByText('By sequence')).toBeInTheDocument() }) it('does not show optional parameters when context is not BUSINESS', () => { diff --git a/next/src/components/release/overview/RegistryReleaseTile.tsx b/next/src/components/release/overview/RegistryReleaseTile.tsx index a98931bc0..ba32dac6d 100644 --- a/next/src/components/release/overview/RegistryReleaseTile.tsx +++ b/next/src/components/release/overview/RegistryReleaseTile.tsx @@ -34,7 +34,7 @@ export function RegistryReleaseTile({ - {t('release.instrumentCollecte')} :{' '} + {t('release.collectInstrument')} :{' '} {release.collectionInstrumentId}{' '} @@ -77,7 +77,7 @@ export function RegistryReleaseTile({ - {t('release.sauvegardePogues')} :{' '} + {t('release.poguesSave')} :{' '} { />, ) - expect(getByText('SEQUENCE')).toBeInTheDocument() + expect(getByText('By sequence')).toBeInTheDocument() }) it('applies custom innerClassName', () => { diff --git a/next/src/components/release/overview/ReleaseOptionalParametersDisplay.tsx b/next/src/components/release/overview/ReleaseOptionalParametersDisplay.tsx index 4b7897fa2..0159e9f81 100644 --- a/next/src/components/release/overview/ReleaseOptionalParametersDisplay.tsx +++ b/next/src/components/release/overview/ReleaseOptionalParametersDisplay.tsx @@ -1,10 +1,22 @@ import { useTranslation } from 'react-i18next' -import type { ReleaseRequest } from '../../../models/releases' +import type { + QuestionNumberingMode, + ReleaseRequest, +} from '../../../models/releases' type OverrideGenerationParameters = ReleaseRequest['overrideGenerationParameters'] +const QUESTION_NUMBERING_LABEL_KEY: Record< + QuestionNumberingMode, + 'none' | 'sequence' | 'questionnaire' +> = { + NONE: 'none', + SEQUENCE: 'sequence', + ALL: 'questionnaire', +} + export function ReleaseOptionalParametersDisplay({ overrideGenerationParameters, innerClassName = 'space-y-1', @@ -32,10 +44,12 @@ export function ReleaseOptionalParametersDisplay({ - {t('release.form.numerotationQuestions.label')} :{' '} + {t('release.form.questionNumbering.label')} :{' '} - {overrideGenerationParameters.questionNumberingMode} + {t( + `release.form.questionNumbering.${QUESTION_NUMBERING_LABEL_KEY[overrideGenerationParameters.questionNumberingMode]}`, + )} diff --git a/next/src/components/release/overview/ReleaseRequestTile.test.tsx b/next/src/components/release/overview/ReleaseRequestTile.test.tsx index f7e0762c2..d0cc3e013 100644 --- a/next/src/components/release/overview/ReleaseRequestTile.test.tsx +++ b/next/src/components/release/overview/ReleaseRequestTile.test.tsx @@ -104,7 +104,7 @@ describe('ReleaseRequestTile', () => { expect( getByText((content) => content.startsWith('Optional parameters')), ).toBeInTheDocument() - expect(getByText('NONE')).toBeInTheDocument() + expect(getByText('No numbering')).toBeInTheDocument() }) it('collapses content on click', async () => { diff --git a/next/src/components/release/overview/ReleaseRequestTile.tsx b/next/src/components/release/overview/ReleaseRequestTile.tsx index da5a998e7..857d3a44c 100644 --- a/next/src/components/release/overview/ReleaseRequestTile.tsx +++ b/next/src/components/release/overview/ReleaseRequestTile.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next' import { useState } from 'react' +import ButtonIcon, { ButtonIconStyle } from '@/components/ui/ButtonIcon' import ExpandButton from '@/components/ui/ExpandButton' import Tooltip from '@/components/ui/Tooltip' import DeleteIcon from '@/components/ui/icons/DeleteIcon' @@ -26,17 +27,6 @@ function getStatusLabel( } } -function getStatusIcon(status: ReleaseRequest['status']): string { - switch (status) { - case 'COMPLETED': - return '\u2705' - case 'FAILED': - return '\u274C' - default: - return '\u2699\uFE0F' - } -} - function isFailedOrCompleted(status: ReleaseRequest['status']): boolean { return status === 'FAILED' || status === 'COMPLETED' } @@ -65,7 +55,6 @@ export function ReleaseRequestTile({ }`} > {getStatusLabel(request.status, t)} - {getStatusIcon(request.status)} ) @@ -158,17 +147,13 @@ export function ReleaseRequestTile({ {isFailedOrCompleted(request.status) && ( - { - e.stopPropagation() - onDelete(request.trackerId) - }} - className="text-gray-400 hover:text-red-600 transition cursor-pointer" + - - + onClick={() => onDelete(request.trackerId)} + buttonStyle={ButtonIconStyle.Delete} + /> )} diff --git a/next/src/lib/i18n/locales/en/release.json b/next/src/lib/i18n/locales/en/release.json index b13e44d59..75a2de6c0 100644 --- a/next/src/lib/i18n/locales/en/release.json +++ b/next/src/lib/i18n/locales/en/release.json @@ -18,6 +18,7 @@ "optionalParameters": "Optional parameters", "demandDate": "Demand Date", "status": { + "completed": "Validated", "inProgress": "In progress ", "failed": "Failed " }, @@ -27,10 +28,10 @@ "copiedTooltip": "Copied!", "copiedLabel": "Copied" }, - "instrumentCollecte": "Collection instrument", + "collectInstrument": "Collection instrument", "version": "Version", "publicationDate": "Publication date", - "sauvegardePogues": "Pogues save", + "poguesSave": "Pogues save", "form": { "introduction": "You are about to publish in the survey questionnaire register a version of your questionnaire based on the latest save. Make sure you have completed your review beforehand.", "seriesInfo": "Series information", @@ -72,7 +73,7 @@ "label": "Response time page", "warning": "Risk of anomaly if ending on a loop and legal obligation" }, - "numerotationQuestions": { + "questionNumbering": { "label": "Question numbering", "none": "No numbering", "sequence": "By sequence", diff --git a/next/src/lib/i18n/locales/fr/release.json b/next/src/lib/i18n/locales/fr/release.json index c608aebfa..cdb019f51 100644 --- a/next/src/lib/i18n/locales/fr/release.json +++ b/next/src/lib/i18n/locales/fr/release.json @@ -18,6 +18,7 @@ "optionalParameters": "Paramètres optionnels", "demandDate": "Date de demande", "status": { + "completed": "Validé", "inProgress": "En cours ", "failed": "Échec " }, @@ -27,10 +28,10 @@ "copiedTooltip": "Copié !", "copiedLabel": "Copié" }, - "instrumentCollecte": "Instrument de collecte", + "collectInstrument": "Instrument de collecte", "version": "Version", "publicationDate": "Date de publication", - "sauvegardePogues": "Sauvegarde Pogues", + "poguesSave": "Sauvegarde Pogues", "form": { "introduction": "Vous allez publier dans le registre de questionnaires de la filière d'enquêtes une version de votre questionnaire basée sur la dernière sauvegarde. Assurez-vous au préalable d'avoir terminé votre recette.", "seriesInfo": "Informations sur la série", @@ -72,7 +73,7 @@ "label": "Page sur les temps de réponse", "warning": "Risque d'anomalie si termine sur boucle et obligation légale" }, - "numerotationQuestions": { + "questionNumbering": { "label": "Numérotation des questions", "none": "Pas de numérotation", "sequence": "Par séquence", From 215cdf1490c8cc3354d125a974eb26d24dc1d7af Mon Sep 17 00:00:00 2001 From: Mailine Nguyen <64129348+MailineN@users.noreply.github.com> Date: Thu, 16 Jul 2026 08:00:01 +0200 Subject: [PATCH 4/6] fix(style): dropdown styling in release form --- next/src/components/release/form/consts.ts | 8 ++++---- .../components/release/overview/RegistryReleaseTile.tsx | 2 +- next/src/components/ui/form/Select.tsx | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/next/src/components/release/form/consts.ts b/next/src/components/release/form/consts.ts index c3b7099a7..e0daa7b48 100644 --- a/next/src/components/release/form/consts.ts +++ b/next/src/components/release/form/consts.ts @@ -9,14 +9,14 @@ export const CONTEXTE_OPTIONS = [ ] export const NUMEROTATION_OPTIONS = [ - { - label: t('release.form.questionNumbering.none'), - value: 'NONE' as const, - }, { label: t('release.form.questionNumbering.sequence'), value: 'SEQUENCE' as const, }, + { + label: t('release.form.questionNumbering.none'), + value: 'NONE' as const, + }, { label: t('release.form.questionNumbering.questionnaire'), value: 'ALL' as const, diff --git a/next/src/components/release/overview/RegistryReleaseTile.tsx b/next/src/components/release/overview/RegistryReleaseTile.tsx index ba32dac6d..9e644b8f6 100644 --- a/next/src/components/release/overview/RegistryReleaseTile.tsx +++ b/next/src/components/release/overview/RegistryReleaseTile.tsx @@ -31,7 +31,7 @@ export function RegistryReleaseTile({ - + {t('release.collectInstrument')} :{' '} diff --git a/next/src/components/ui/form/Select.tsx b/next/src/components/ui/form/Select.tsx index c0a62335f..e0284d702 100644 --- a/next/src/components/ui/form/Select.tsx +++ b/next/src/components/ui/form/Select.tsx @@ -73,6 +73,7 @@ export default function Select({ From ed4b67de1f040d1d5f40cb7d411358fa88fd222e Mon Sep 17 00:00:00 2001 From: Mailine Nguyen <64129348+MailineN@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:39:34 +0200 Subject: [PATCH 5/6] fix: typing & api calls --- next/src/api/models/releaseDTO.ts | 10 +- next/src/api/releases.test.ts | 80 +++------- next/src/api/releases.ts | 144 +++++++++++++++--- .../release/ReleaseOverview.test.tsx | 18 ++- .../components/release/ReleaseOverview.tsx | 111 ++------------ .../release/overview/RegistryReleaseTile.tsx | 3 +- .../release/overview/ReleaseRequestTile.tsx | 3 +- next/src/models/questionnaires.ts | 2 + .../_layout-q/releases/index.tsx | 20 ++- 9 files changed, 193 insertions(+), 198 deletions(-) diff --git a/next/src/api/models/releaseDTO.ts b/next/src/api/models/releaseDTO.ts index 3421fe21e..12752ed27 100644 --- a/next/src/api/models/releaseDTO.ts +++ b/next/src/api/models/releaseDTO.ts @@ -1,3 +1,5 @@ +import type { TargetMode } from '@/models/questionnaires' + /** Model of the registry release returned by the Pogues API's releases endpoint. */ export type RegistryReleaseDTO = { collectionInstrumentId: string @@ -6,7 +8,7 @@ export type RegistryReleaseDTO = { releaseDate: number poguesVersionId: string releaseDescription: string - mode: TargetModesDTO + mode: TargetMode context: ReleaseContextDTO overrideGenerationParameters: OverrideGenerationParametersDTO | null visualizeUrl: string @@ -23,7 +25,7 @@ export type ReleaseRequestDTO = { poguesVersionId: string poguesId: string releaseDescription: string - mode: TargetModesDTO + mode: TargetMode context: ReleaseContextDTO overrideGenerationParameters: OverrideGenerationParametersDTO } @@ -31,7 +33,7 @@ export type ReleaseRequestDTO = { export type CreateReleaseDTO = { poguesId: string releaseDescription: string - mode: TargetModesDTO + mode: TargetMode context: ReleaseContextDTO overrideGenerationParameters: OverrideGenerationParametersDTO } @@ -41,8 +43,6 @@ export type OverrideGenerationParametersDTO = { responseTimeQuestion: boolean } -export type TargetModesDTO = 'CAWI' | 'CAPI' | 'PAPI' | 'CATI' - export type ReleaseContextDTO = 'HOUSEHOLD' | 'BUSINESS' export type CurrentStepDTO = diff --git a/next/src/api/releases.test.ts b/next/src/api/releases.test.ts index e2dae51ee..c4e3f0c18 100644 --- a/next/src/api/releases.test.ts +++ b/next/src/api/releases.test.ts @@ -1,14 +1,9 @@ import nock from 'nock' -import { TargetModes } from '@/models/questionnaires' -import type { RegistryRelease, ReleaseRequest } from '@/models/releases' - -import type { - CreateReleaseDTO, - RegistryReleaseDTO, - ReleaseRequestDTO, -} from './models/releaseDTO' +import type { CreateReleaseDTO } from './models/releaseDTO' import { + MOCK_PUBLICATIONS, + MOCK_REQUESTS, deleteReleaseRequest, getPendingReleases, getReleases, @@ -17,70 +12,31 @@ import { vi.mock('@/lib/auth/oidc') -const registryReleaseDTO: RegistryReleaseDTO = { - collectionInstrumentId: '550e8400-e29b-41d4-a716-446655440001', - version: 3, - author: 'xbeltv', - releaseDate: 1780560000000, - poguesVersionId: '93d1e85c-327d-4153-a5fa-e04f54ca0e3e', - releaseDescription: 'ESA 2026 PROD', - mode: 'CAWI', - context: 'HOUSEHOLD', - overrideGenerationParameters: { - questionNumberingMode: 'NONE', - responseTimeQuestion: false, - }, - visualizeUrl: 'https://visu.example.com/esa-2026-prod', -} - -const registryRelease: RegistryRelease = { - ...registryReleaseDTO, - mode: TargetModes.CAWI, -} - -const releaseRequestDTO: ReleaseRequestDTO = { - trackerId: 1, - author: 'xbeltv', - requestDate: 1780560000000, - currentStep: 'BUILD_PARAMETERS', - status: 'RUNNING', - statusDescription: '', - poguesVersionId: '550e8400-e29b-41d4-a716-446655440000', - poguesId: 'SRCV_REINTERRO', - releaseDescription: 'Release description', - mode: 'CAPI', - context: 'HOUSEHOLD', - overrideGenerationParameters: { - questionNumberingMode: 'SEQUENCE', - responseTimeQuestion: true, - }, -} - -const releaseRequest: ReleaseRequest = { - ...releaseRequestDTO, - mode: TargetModes.CAPI, -} - it('Get releases works', async () => { - const releases: RegistryReleaseDTO[] = [registryReleaseDTO] + // const releases: RegistryReleaseDTO[] = [registryReleaseDTO] - nock('https://mock-api') - .get('/persistence/questionnaire/my-questionnaire/releases') - .reply(200, releases) + // nock('https://mock-api') + // .get('/persistence/questionnaire/my-questionnaire/releases') + // .reply(200, releases) + + // const res = await getReleases('my-questionnaire') + // expect(res).toEqual([registryRelease]) const res = await getReleases('my-questionnaire') - expect(res).toEqual([registryRelease]) + expect(res).toEqual(MOCK_PUBLICATIONS) }) it('Get pending releases works', async () => { - const requests: ReleaseRequestDTO[] = [releaseRequestDTO] + // const requests: ReleaseRequestDTO[] = [releaseRequestDTO] - nock('https://mock-api') - .get('/persistence/questionnaire/my-questionnaire/release-requests') - .reply(200, requests) + // nock('https://mock-api') + // .get('/persistence/questionnaire/my-questionnaire/release-requests') + // .reply(200, requests) + // const res = await getPendingReleases('my-questionnaire') + // expect(res).toEqual([releaseRequest]) const res = await getPendingReleases('my-questionnaire') - expect(res).toEqual([releaseRequest]) + expect(res).toEqual(MOCK_REQUESTS) }) it('Post release works', async () => { diff --git a/next/src/api/releases.ts b/next/src/api/releases.ts index 4435a09b5..19a17da2e 100644 --- a/next/src/api/releases.ts +++ b/next/src/api/releases.ts @@ -1,17 +1,109 @@ import { queryOptions } from '@tanstack/react-query' +import { TargetModes } from '@/models/questionnaires' import type { RegistryRelease, ReleaseRequest } from '@/models/releases' import { instance } from './instance' -import type { - CreateReleaseDTO, - RegistryReleaseDTO, - ReleaseRequestDTO, -} from './models/releaseDTO' -import { - computeRegistryReleases, - computeReleaseRequests, -} from './utils/releases' +import type { CreateReleaseDTO } from './models/releaseDTO' + +export const MOCK_REQUESTS: ReleaseRequest[] = [ + { + trackerId: 1, + author: 'xbeltv', + requestDate: new Date('2026-07-06T10:30:00').getTime(), + currentStep: 'BUILD_PARAMETERS', + status: 'RUNNING', + statusDescription: '', + poguesVersionId: '550e8400-e29b-41d4-a716-446655440000', + poguesId: 'SRCV_REINTERRO', + releaseDescription: 'Recette intégrée oct 2025 pour SRCV_REINTERRO 2026', + mode: TargetModes.CAPI, + context: 'HOUSEHOLD', + overrideGenerationParameters: { + questionNumberingMode: 'SEQUENCE', + responseTimeQuestion: true, + }, + }, + { + trackerId: 2, + author: 'nazdsn', + requestDate: new Date('2026-07-05T14:15:00').getTime(), + currentStep: 'GENERATE_DDI', + status: 'FAILED', + statusDescription: 'Erreur lors de la génération Eno', + poguesVersionId: '550e8400-e29b-41d4-a716-446655440001', + poguesId: 'SRCV_REINTERRO', + releaseDescription: 'Autre demande de publication pour SRCV_REINTERRO 2026', + mode: TargetModes.CAWI, + context: 'BUSINESS', + overrideGenerationParameters: { + questionNumberingMode: 'NONE', + responseTimeQuestion: false, + }, + }, + { + trackerId: 3, + author: 'bcbab8', + requestDate: new Date('2026-07-04T09:00:00').getTime(), + currentStep: 'PUBLISH_DDI', + status: 'FAILED', + statusDescription: 'Erreur 409 - Conflit de version', + poguesVersionId: '550e8400-e29b-41d4-a716-446655440002', + poguesId: 'SRCV_REINTERRO', + releaseDescription: 'Recette intégrée oct 2025 pour SRCV_REINTERRO 2026', + mode: TargetModes.CAWI, + context: 'HOUSEHOLD', + overrideGenerationParameters: { + questionNumberingMode: 'ALL', + responseTimeQuestion: true, + }, + }, +] + +export const MOCK_PUBLICATIONS: RegistryRelease[] = [ + { + collectionInstrumentId: '550e8400-e29b-41d4-a716-446655440001', + version: 3, + author: 'xbeltv', + releaseDate: new Date('2026-07-04T08:00:00').getTime(), + poguesVersionId: '93d1e85c-327d-4153-a5fa-e04f54ca0e3e', + releaseDescription: 'ESA 2026 PROD', + mode: TargetModes.CAWI, + context: 'HOUSEHOLD', + overrideGenerationParameters: { + questionNumberingMode: 'NONE', + responseTimeQuestion: false, + }, + visualizeUrl: 'https://visu.example.com/esa-2026-prod', + }, + { + collectionInstrumentId: '550e8400-e29b-41d4-a716-446655440002', + version: 2, + author: 'nazdsn', + releaseDate: new Date('2026-06-28T10:00:00').getTime(), + poguesVersionId: '5ddf61df-00c8-4018-a763-fa7bc91b0162', + releaseDescription: 'ESA 2026 TEST TERRAIN', + mode: TargetModes.CAPI, + context: 'BUSINESS', + overrideGenerationParameters: { + questionNumberingMode: 'NONE', + responseTimeQuestion: false, + }, + visualizeUrl: 'https://visu.example.com/esa-2026-test', + }, + { + collectionInstrumentId: '550e8400-e29b-41d4-a716-446655440003', + version: 1, + author: 'bcbab8', + releaseDate: new Date('2026-06-15T11:30:00').getTime(), + poguesVersionId: 'b77e7cad-475d-4d83-b036-fa7a98a84a8a', + releaseDescription: "Publication la plus ancienne d'ESA", + mode: TargetModes.CAWI, + context: 'HOUSEHOLD', + overrideGenerationParameters: null, + visualizeUrl: 'https://visu.example.com/esa-2026-old', + }, +] export const releasesKeys = { released: (questionnaireId: string) => ['released', questionnaireId] as const, @@ -40,26 +132,32 @@ export const pendingReleasesQueryOptions = (questionnaireId: string) => export async function getReleases( questionnaireId: string, ): Promise { - return instance - .get(`/persistence/questionnaire/${questionnaireId}/releases`, { - headers: { Accept: 'application/json' }, - }) - .then(({ data }: { data: RegistryReleaseDTO[] }) => { - return computeRegistryReleases(data) - }) + console.log('questionnaireId', questionnaireId) + + // return instance + // .get(`/persistence/questionnaire/${questionnaireId}/releases`, { + // headers: { Accept: 'application/json' }, + // }) + // .then(({ data }: { data: RegistryReleaseDTO[] }) => { + // return computeRegistryReleases(data) + // }) + return Promise.resolve(MOCK_PUBLICATIONS) } /** Retrieve questionnaire pending releases by the questionnaire id. */ export async function getPendingReleases( questionnaireId: string, ): Promise { - return instance - .get(`/persistence/questionnaire/${questionnaireId}/release-requests`, { - headers: { Accept: 'application/json' }, - }) - .then(({ data }: { data: ReleaseRequestDTO[] }) => { - return computeReleaseRequests(data) - }) + console.log('questionnaireId', questionnaireId) + + // return instance + // .get(`/persistence/questionnaire/${questionnaireId}/release-requests`, { + // headers: { Accept: 'application/json' }, + // }) + // .then(({ data }: { data: ReleaseRequestDTO[] }) => { + // return computeReleaseRequests(data) + // }) + return Promise.resolve(MOCK_REQUESTS) } /** Create a new release. */ diff --git a/next/src/components/release/ReleaseOverview.test.tsx b/next/src/components/release/ReleaseOverview.test.tsx index 91420945f..40d5e00c9 100644 --- a/next/src/components/release/ReleaseOverview.test.tsx +++ b/next/src/components/release/ReleaseOverview.test.tsx @@ -1,21 +1,29 @@ +import { MOCK_PUBLICATIONS, MOCK_REQUESTS } from '@/api/releases' import { renderWithI18n } from '@/testing/render' import ReleaseOverview from './ReleaseOverview' +const defaultProps = { + pendingRequests: MOCK_REQUESTS, + releases: MOCK_PUBLICATIONS, +} + describe('ReleaseOverview', () => { beforeEach(() => { vi.stubEnv('VITE_TROMBI_URL', 'https://trombi.example.com') }) it('renders both sections with correct headers', () => { - const { getByText } = renderWithI18n() + const { getByText } = renderWithI18n() expect(getByText('My release requests')).toBeInTheDocument() expect(getByText('My publications')).toBeInTheDocument() }) it('renders all request tiles', () => { - const { getByText, getAllByText } = renderWithI18n() + const { getByText, getAllByText } = renderWithI18n( + , + ) expect( getAllByText('Recette intégrée oct 2025 pour SRCV_REINTERRO 2026').length, @@ -26,7 +34,7 @@ describe('ReleaseOverview', () => { }) it('renders all publication tiles', () => { - const { getByText } = renderWithI18n() + const { getByText } = renderWithI18n() expect(getByText('ESA 2026 PROD')).toBeInTheDocument() expect(getByText('ESA 2026 TEST TERRAIN')).toBeInTheDocument() @@ -34,7 +42,9 @@ describe('ReleaseOverview', () => { }) it('renders requests sorted by date descending', () => { - const { getAllByText } = renderWithI18n() + const { getAllByText } = renderWithI18n( + , + ) const descriptions = getAllByText( (content) => diff --git a/next/src/components/release/ReleaseOverview.tsx b/next/src/components/release/ReleaseOverview.tsx index 292b92f68..2949edab2 100644 --- a/next/src/components/release/ReleaseOverview.tsx +++ b/next/src/components/release/ReleaseOverview.tsx @@ -6,104 +6,10 @@ import type { RegistryRelease, ReleaseRequest } from '../../models/releases' import { RegistryReleaseTile } from './overview/RegistryReleaseTile' import { ReleaseRequestTile } from './overview/ReleaseRequestTile' -const MOCK_REQUESTS: ReleaseRequest[] = [ - { - trackerId: 1, - author: 'xbeltv', - requestDate: new Date('2026-07-06T10:30:00').getTime(), - currentStep: 'BUILD_PARAMETERS', - status: 'RUNNING', - statusDescription: '', - poguesVersionId: '550e8400-e29b-41d4-a716-446655440000', - poguesId: 'SRCV_REINTERRO', - releaseDescription: 'Recette intégrée oct 2025 pour SRCV_REINTERRO 2026', - mode: 'CAPI', - context: 'HOUSEHOLD', - overrideGenerationParameters: { - questionNumberingMode: 'SEQUENCE', - responseTimeQuestion: true, - }, - }, - { - trackerId: 2, - author: 'nazdsn', - requestDate: new Date('2026-07-05T14:15:00').getTime(), - currentStep: 'GENERATE_DDI', - status: 'FAILED', - statusDescription: 'Erreur lors de la génération Eno', - poguesVersionId: '550e8400-e29b-41d4-a716-446655440001', - poguesId: 'SRCV_REINTERRO', - releaseDescription: 'Autre demande de publication pour SRCV_REINTERRO 2026', - mode: 'CAPI', - context: 'BUSINESS', - overrideGenerationParameters: { - questionNumberingMode: 'NONE', - responseTimeQuestion: false, - }, - }, - { - trackerId: 3, - author: 'bcbab8', - requestDate: new Date('2026-07-04T09:00:00').getTime(), - currentStep: 'PUBLISH_DDI', - status: 'FAILED', - statusDescription: 'Erreur 409 - Conflit de version', - poguesVersionId: '550e8400-e29b-41d4-a716-446655440002', - poguesId: 'SRCV_REINTERRO', - releaseDescription: 'Recette intégrée oct 2025 pour SRCV_REINTERRO 2026', - mode: 'CAPI', - context: 'HOUSEHOLD', - overrideGenerationParameters: { - questionNumberingMode: 'ALL', - responseTimeQuestion: true, - }, - }, -] - -const MOCK_PUBLICATIONS: RegistryRelease[] = [ - { - collectionInstrumentId: '550e8400-e29b-41d4-a716-446655440001', - version: 3, - author: 'xbeltv', - releaseDate: new Date('2026-07-04T08:00:00').getTime(), - poguesVersionId: '93d1e85c-327d-4153-a5fa-e04f54ca0e3e', - releaseDescription: 'ESA 2026 PROD', - mode: 'CAWI', - context: 'HOUSEHOLD', - overrideGenerationParameters: { - questionNumberingMode: 'NONE', - responseTimeQuestion: false, - }, - visualizeUrl: 'https://visu.example.com/esa-2026-prod', - }, - { - collectionInstrumentId: '550e8400-e29b-41d4-a716-446655440002', - version: 2, - author: 'nazdsn', - releaseDate: new Date('2026-06-28T10:00:00').getTime(), - poguesVersionId: '5ddf61df-00c8-4018-a763-fa7bc91b0162', - releaseDescription: 'ESA 2026 TEST TERRAIN', - mode: 'CAPI', - context: 'BUSINESS', - overrideGenerationParameters: { - questionNumberingMode: 'NONE', - responseTimeQuestion: false, - }, - visualizeUrl: 'https://visu.example.com/esa-2026-test', - }, - { - collectionInstrumentId: '550e8400-e29b-41d4-a716-446655440003', - version: 1, - author: 'bcbab8', - releaseDate: new Date('2026-06-15T11:30:00').getTime(), - poguesVersionId: 'b77e7cad-475d-4d83-b036-fa7a98a84a8a', - releaseDescription: "Publication la plus ancienne d'ESA", - mode: 'CAWI', - context: 'HOUSEHOLD', - overrideGenerationParameters: null, - visualizeUrl: 'https://visu.example.com/esa-2026-old', - }, -] +interface ReleaseOveriewProps { + pendingRequests: ReleaseRequest[] + releases: RegistryRelease[] +} function sortByRequestDateDesc(items: ReleaseRequest[]): ReleaseRequest[] { return [...items].sort((a, b) => b.requestDate - a.requestDate) @@ -113,11 +19,14 @@ function sortByReleaseDateDesc(items: RegistryRelease[]): RegistryRelease[] { return [...items].sort((a, b) => b.releaseDate - a.releaseDate) } -export default function ReleaseOverview() { +export default function ReleaseOverview({ + pendingRequests, + releases, +}: Readonly) { const { t } = useTranslation() - const [requests, setRequests] = useState(MOCK_REQUESTS) - const publications = MOCK_PUBLICATIONS + const [requests, setRequests] = useState(pendingRequests) + const publications = releases const hasAnyContent = requests.length > 0 || publications.length > 0 diff --git a/next/src/components/release/overview/RegistryReleaseTile.tsx b/next/src/components/release/overview/RegistryReleaseTile.tsx index 9e644b8f6..51d84ec71 100644 --- a/next/src/components/release/overview/RegistryReleaseTile.tsx +++ b/next/src/components/release/overview/RegistryReleaseTile.tsx @@ -1,5 +1,6 @@ import { useTranslation } from 'react-i18next' +import { TargetModes } from '@/models/questionnaires' import { computeDayFromDate, computeFullDateFromDate } from '@/utils/date' import type { RegistryRelease } from '../../../models/releases' @@ -65,7 +66,7 @@ export function RegistryReleaseTile({ {t('release.collectMode')} :{' '} - {release.mode} + {TargetModes[release.mode]} diff --git a/next/src/components/release/overview/ReleaseRequestTile.tsx b/next/src/components/release/overview/ReleaseRequestTile.tsx index 857d3a44c..62c479f90 100644 --- a/next/src/components/release/overview/ReleaseRequestTile.tsx +++ b/next/src/components/release/overview/ReleaseRequestTile.tsx @@ -8,6 +8,7 @@ import ExpandButton from '@/components/ui/ExpandButton' import Tooltip from '@/components/ui/Tooltip' import DeleteIcon from '@/components/ui/icons/DeleteIcon' import OpenInNewIcon from '@/components/ui/icons/OpenInNewIcon' +import { TargetModes } from '@/models/questionnaires' import { computeDayFromDate, computeFullDateFromDate } from '@/utils/date' import type { ReleaseRequest } from '../../../models/releases' @@ -90,7 +91,7 @@ export function ReleaseRequestTile({ {t('release.collectMode')} :{' '} - {request.mode} + {TargetModes[request.mode]} diff --git a/next/src/models/questionnaires.ts b/next/src/models/questionnaires.ts index cd1d4ef25..ea3c674c7 100644 --- a/next/src/models/questionnaires.ts +++ b/next/src/models/questionnaires.ts @@ -22,6 +22,8 @@ export enum TargetModes { CATI, } +export type TargetMode = keyof typeof TargetModes + export enum FlowLogics { Filter = 'FILTER', Redirection = 'REDIRECTION', diff --git a/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/index.tsx b/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/index.tsx index aad9d69c4..26da21e4a 100644 --- a/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/index.tsx +++ b/next/src/routes/_layout/questionnaire.$questionnaireId/_layout-q/releases/index.tsx @@ -1,5 +1,10 @@ +import { useSuspenseQuery } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' +import { + pendingReleasesQueryOptions, + releasesQueryOptions, +} from '@/api/releases' import ErrorComponent from '@/components/layout/ErrorComponent' import ReleaseOverview from '@/components/release/ReleaseOverview' import ReleaseOverviewLayout from '@/components/release/ReleasesOverviewLayout' @@ -13,14 +18,27 @@ export const Route = createFileRoute( ), + loader: async ({ context: { queryClient }, params: { questionnaireId } }) => { + await Promise.all([ + queryClient.ensureQueryData(releasesQueryOptions(questionnaireId)), + queryClient.ensureQueryData(pendingReleasesQueryOptions(questionnaireId)), + ]) + }, }) function RouteComponent() { const questionnaireId = Route.useParams().questionnaireId + const { data: requests } = useSuspenseQuery( + pendingReleasesQueryOptions(questionnaireId), + ) + const { data: releases } = useSuspenseQuery( + releasesQueryOptions(questionnaireId), + ) + return ( - + ) } From 4d7d7ea0034eb0b7b856beb223df80103f66909d Mon Sep 17 00:00:00 2001 From: Mailine Nguyen <64129348+MailineN@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:45:51 +0200 Subject: [PATCH 6/6] bump: 3.6.2-rc-releases-page.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6199771db..45fe9f8dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pogues", - "version": "3.6.1", + "version": "3.6.2-rc-releases-page.0", "description": "Outil de conception et de test de questionnaires.", "scripts": { "test:next": "cd next && pnpm test",
{t('release.noPublication')}
child content
+ {t('release.form.introduction')} +
+ + {t('release.form.series.id')} :{' '} + + {seriesId ? ( + {seriesId} + ) : ( + <> + + {t('release.form.series.missingId')} + + + + + > + )} +
{seriesId}
+ + {t('release.form.series.label')} :{' '} + + {seriesLabel || ( + <> + + {t('release.form.series.missingLabel')} + + + + + > + )} +
+ {t('release.form.pageTempsReponse.warning')} +
{t('release.copy.copiedLabel')}