From 1ac612debe70b7e4d279e258ed82753613b2b0b2 Mon Sep 17 00:00:00 2001 From: Mailine Nguyen <64129348+MailineN@users.noreply.github.com> Date: Fri, 3 Jul 2026 10:16:30 +0200 Subject: [PATCH 1/5] feat: add platine gestion redirection --- .env | 2 + CHANGELOG.md | 4 + src/components/Button.tsx | 5 +- src/components/Dialog.tsx | 38 +++++-- src/components/Header.test.tsx | 118 +++++++++++++++++++++ src/components/Header.tsx | 74 +++++++++++-- src/components/icons/ExitIcon.tsx | 20 ++++ src/features/orchestrator/Orchestrator.tsx | 7 ++ src/hooks/useEvent.ts | 11 ++ src/hooks/usePreLogoutAction.ts | 28 +++++ src/libs/i18n/locales/en.json | 9 +- src/libs/i18n/locales/fr.json | 9 +- src/vite-env.d.ts | 1 + 13 files changed, 306 insertions(+), 20 deletions(-) create mode 100644 src/components/icons/ExitIcon.tsx create mode 100644 src/hooks/useEvent.ts create mode 100644 src/hooks/usePreLogoutAction.ts diff --git a/.env b/.env index c21eff4..c570c00 100644 --- a/.env +++ b/.env @@ -1,6 +1,8 @@ VITE_API_URL= VITE_IDENTITY_PROVIDER= +VITE_PLATINE_GESTION_URL= + # OIDC Configurations VITE_OIDC_ISSUER= VITE_OIDC_ENABLED=false diff --git a/CHANGELOG.md b/CHANGELOG.md index e28b915..c3302ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added Platine Gestion redirection + ### Changed - Authentication: diff --git a/src/components/Button.tsx b/src/components/Button.tsx index c059a13..8b9b725 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,6 +1,7 @@ export enum ButtonStyle { Primary, Secondary, + Transparent, } export enum ButtonSize { @@ -39,7 +40,9 @@ export default function Button({ ${ buttonStyle === ButtonStyle.Primary ? 'bg-primary text-negative disabled:bg-primary-disabled hover:enabled:bg-primary-accent active:enabled:bg-primary-active' - : 'bg-white text-primary fill-action-primary disabled:bg-disabled disabled:text-disabled hover:enabled:bg-accent active:enabled:bg-active disabled:border-default border-primary' + : buttonStyle === ButtonStyle.Transparent + ? 'bg-transparent text-negative fill-current border-transparent hover:enabled:bg-white/10 active:enabled:bg-white/20 disabled:opacity-50' + : 'bg-white text-primary fill-action-primary disabled:bg-disabled disabled:text-disabled hover:enabled:bg-accent active:enabled:bg-active disabled:border-default border-primary' } ${buttonSize === ButtonSize.md ? 'px-4 py-3 min-w-40' : 'px-2 py-1'}`} {...props} > diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx index abe8624..e9446ad 100644 --- a/src/components/Dialog.tsx +++ b/src/components/Dialog.tsx @@ -15,11 +15,12 @@ interface DialogProps { */ onCancel?: () => void /** - * Function to execute if the user click on "validate". + * Async function to execute if the user click on "validate". * * The validate button is only present if this function is provided. + * While the promise is pending, buttons are disabled and the validate label shows `loadingLabel`. */ - onValidate?: () => void + onValidate?: () => void | Promise /** Children to render inside the dialog trigger button. */ children?: React.ReactElement /** Title of the dialog. */ @@ -34,6 +35,8 @@ interface DialogProps { cancelLabel?: string /** Custom label for the validate button. */ validateLabel?: string + /** Custom label for the validate button while onValidate is pending. */ + loadingLabel?: string } /** Display a button that opens a confirmation dialog. */ @@ -47,13 +50,26 @@ export default function Dialog({ setControlledOpen, cancelLabel, validateLabel, + loadingLabel, }: Readonly) { const { t } = useTranslation() const [uncontrolledOpen, setUncontrolledOpen] = useState(false) + const [isLoading, setIsLoading] = useState(false) const open = controlledOpen ?? uncontrolledOpen const setOpen = setControlledOpen ?? setUncontrolledOpen + const handleValidate = async () => { + if (!onValidate) return + setIsLoading(true) + try { + await onValidate() + } finally { + setIsLoading(false) + setOpen(false) + } + } + return ( {children && } @@ -68,23 +84,27 @@ export default function Dialog({
{onCancel ? ( - ) : ( {cancelLabel || t('common.cancel')}} + render={ + + } /> )} {onValidate ? ( ) : null}
diff --git a/src/components/Header.test.tsx b/src/components/Header.test.tsx index 24584a0..c268c43 100644 --- a/src/components/Header.test.tsx +++ b/src/components/Header.test.tsx @@ -1,11 +1,39 @@ +import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' + +import { executePreLogoutActions } from '@/hooks/usePreLogoutAction' import { renderWithI18n } from '@/testing/render' import Header from './Header' +interface InterrogationMatch { + params: { + interrogationId: string + } +} + +const mockUseMatch = vi.hoisted(() => + vi.fn<(...args: object[]) => InterrogationMatch | undefined>(), +) + +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router') + return { + ...actual, + useMatch: mockUseMatch, + } +}) + +vi.mock('@/hooks/usePreLogoutAction', () => ({ + executePreLogoutActions: vi.fn(), +})) + describe('Header', () => { beforeEach(() => { vi.stubEnv('APP_VERSION', '1.0.0') vi.stubEnv('LUNATIC_VERSION', '^3.6.9') + vi.stubEnv('VITE_PLATINE_GESTION_URL', '') + mockUseMatch.mockReturnValue(undefined) }) it('display app name', async () => { @@ -17,4 +45,94 @@ describe('Header', () => { const { getByText } = renderWithI18n(
) expect(getByText('Version 1.0.0 | Lunatic 3.6.9')).toBeInTheDocument() }) + + it('does not show button when platine URL is not configured', () => { + mockUseMatch.mockReturnValue({ + params: { interrogationId: 'test-123' }, + }) + + renderWithI18n(
) + + expect( + screen.queryByText('Retourner dans Platine Gestion'), + ).not.toBeInTheDocument() + }) + + it('does not show button when interrogation match is not found', () => { + vi.stubEnv('VITE_PLATINE_GESTION_URL', 'https://platine-gestion.url/') + renderWithI18n(
) + + expect(screen.queryByText('Go to Platine Gestion')).not.toBeInTheDocument() + }) + + it('shows button when interrogation match is found', () => { + vi.stubEnv('VITE_PLATINE_GESTION_URL', 'https://platine-gestion.url/') + mockUseMatch.mockReturnValue({ + params: { interrogationId: 'test-123' }, + }) + + renderWithI18n(
) + + expect(screen.getByText('Go to Platine Gestion')).toBeInTheDocument() + }) + + it('opens dialog when button is clicked', async () => { + vi.stubEnv('VITE_PLATINE_GESTION_URL', 'https://platine-gestion.url/') + const user = userEvent.setup() + mockUseMatch.mockReturnValue({ + params: { interrogationId: 'test-123' }, + }) + + renderWithI18n(
) + + await user.click(screen.getByText('Go to Platine Gestion')) + + expect( + screen.getByRole('button', { name: 'Save and leave' }), + ).toBeInTheDocument() + expect( + screen.getByText( + 'Do you want to go back to Platine Gestion. Your data will be saved before leaving.', + ), + ).toBeInTheDocument() + }) + + it('closes dialog when cancel is clicked', async () => { + vi.stubEnv('VITE_PLATINE_GESTION_URL', 'https://platine-gestion.url/') + const user = userEvent.setup() + mockUseMatch.mockReturnValue({ + params: { interrogationId: 'test-123' }, + }) + + renderWithI18n(
) + + await user.click(screen.getByText('Go to Platine Gestion')) + await user.click(screen.getByText('Cancel')) + + expect( + screen.queryByRole('button', { name: 'Save and leave' }), + ).not.toBeInTheDocument() + }) + + it('redirects to platine on save and leave', async () => { + const PLATINE_URL = 'https://platine-gestion.url/' + vi.stubEnv('VITE_PLATINE_GESTION_URL', PLATINE_URL) + const user = userEvent.setup() + mockUseMatch.mockReturnValue({ + params: { interrogationId: 'test-123' }, + }) + + Object.defineProperty(window, 'location', { + writable: true, + value: { href: '' }, + }) + + renderWithI18n(
) + + await user.click(screen.getByText('Go to Platine Gestion')) + await user.click(screen.getByRole('button', { name: 'Save and leave' })) + + expect(executePreLogoutActions).toHaveBeenCalledOnce() + expect(window.location.href).toBe(`${PLATINE_URL}interrogation/test-123`) + }) }) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 8dd8a07..e6228d4 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,17 +1,75 @@ +import { useState } from 'react' + +import { useMatch } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' +import { executePreLogoutActions } from '@/hooks/usePreLogoutAction' + +import Button, { ButtonStyle } from './Button' +import Dialog from './Dialog' +import ExitIcon from './icons/ExitIcon' + export default function Header() { const { t } = useTranslation() + const interrogationMatch = useMatch({ + from: '/interrogations/$interrogationId', + shouldThrow: false, + }) + + const platineGestionUrl = import.meta.env.VITE_PLATINE_GESTION_URL + + const [isDialogOpen, setIsDialogOpen] = useState(false) + + const handleGoToInterrogation = () => { + if (interrogationMatch?.params?.interrogationId) { + setIsDialogOpen(true) + } + } + + const handleSaveAndLeave = async () => { + if (!interrogationMatch?.params?.interrogationId) return + + await executePreLogoutActions() + window.location.href = `${platineGestionUrl}interrogation/${interrogationMatch.params.interrogationId}` + } + return ( -
- - {t('common.appName')} - - - Version {import.meta.env.APP_VERSION} | Lunatic{' '} - {import.meta.env.LUNATIC_VERSION.replace('^', '')} - +
+
+ + {t('common.appName')} + + + {t('common.version', { + appVersion: import.meta.env.APP_VERSION, + lunaticVersion: import.meta.env.LUNATIC_VERSION.replace('^', ''), + })} + +
+
+ {interrogationMatch && platineGestionUrl && ( + <> + + setIsDialogOpen(false)} + onValidate={handleSaveAndLeave} + controlledOpen={isDialogOpen} + setControlledOpen={setIsDialogOpen} + validateLabel={t('common.leaveAndSave')} + loadingLabel={t('common.saving')} + /> + + )} +
) } diff --git a/src/components/icons/ExitIcon.tsx b/src/components/icons/ExitIcon.tsx new file mode 100644 index 0000000..9d3acce --- /dev/null +++ b/src/components/icons/ExitIcon.tsx @@ -0,0 +1,20 @@ +/** + * Icon of exit symbol which should be used when one wants to go back to platine gestion. + */ +export default function ExitIcon({ + height = '24px', + width = '24px', + ...props +}: Readonly>) { + return ( + + + + ) +} diff --git a/src/features/orchestrator/Orchestrator.tsx b/src/features/orchestrator/Orchestrator.tsx index 8ee7e92..01fb8e1 100644 --- a/src/features/orchestrator/Orchestrator.tsx +++ b/src/features/orchestrator/Orchestrator.tsx @@ -10,6 +10,7 @@ import '@inseefr/lunatic/main.css' import { assert } from 'tsafe/assert' import WelcomeModal from '@/components/WelcomeModal' +import { useAddPreLogoutAction } from '@/hooks/usePreLogoutAction' import type { Interrogation } from '@/models/interrogation' import type { InterrogationData } from '@/models/interrogationData' import type { LunaticGetReferentiel } from '@/models/lunaticType' @@ -177,6 +178,12 @@ export default function Orchestrator(props: OrchestratorProps) { } } + useAddPreLogoutAction(async () => { + if (mode === MODE_TYPE.COLLECT && !hasBeenSent(initialState)) { + await triggerDataAndStateUpdate(true) + } + }) + return (
diff --git a/src/hooks/useEvent.ts b/src/hooks/useEvent.ts new file mode 100644 index 0000000..ec7f63a --- /dev/null +++ b/src/hooks/useEvent.ts @@ -0,0 +1,11 @@ +import { useCallback, useInsertionEffect, useRef } from 'react' + +export function useEvent( + callback: (...args: Args) => Return, +): (...args: Args) => Return { + const ref = useRef(callback) + useInsertionEffect(() => { + ref.current = callback + }) + return useCallback((...args: Args) => ref.current(...args), []) +} diff --git a/src/hooks/usePreLogoutAction.ts b/src/hooks/usePreLogoutAction.ts new file mode 100644 index 0000000..ab23929 --- /dev/null +++ b/src/hooks/usePreLogoutAction.ts @@ -0,0 +1,28 @@ +import { useEffect } from 'react' + +import { useEvent } from './useEvent' + +const actions = new Set<() => Promise | void>() + +/* + Execute pre-logout actions, i.e only save data for now (from Stromae-DSFR) + We need to create this because the exit button is not in an orchestrator component + Another solution would be using a context, but I am not sure if it's a good idea anyway +*/ +export async function executePreLogoutActions() { + for (const action of actions) { + await action() + } +} + +export function useAddPreLogoutAction( + action: () => Promise | void, +): void { + const stableAction = useEvent(action) + useEffect(() => { + actions.add(stableAction) + return () => { + actions.delete(stableAction) + } + }, [stableAction]) +} diff --git a/src/libs/i18n/locales/en.json b/src/libs/i18n/locales/en.json index c3d52fb..d630928 100644 --- a/src/libs/i18n/locales/en.json +++ b/src/libs/i18n/locales/en.json @@ -6,7 +6,14 @@ "validateData": "Validate data", "previous": "Previous", "cancel": "Cancel", - "validate": "Validate" + "validate": "Validate", + "goToPlatineGestion": "Go to Platine Gestion", + "version": "Version {{appVersion}} | Lunatic {{lunaticVersion}}", + "quit": "Quit", + "leaveConfirmation": "Save and leave", + "leaveBody": "Do you want to go back to Platine Gestion. Your data will be saved before leaving.", + "leaveAndSave": "Save and leave", + "saving": "Saving..." }, "welcomeModal": { "title": "Welcome to Walking Papers", diff --git a/src/libs/i18n/locales/fr.json b/src/libs/i18n/locales/fr.json index faca0ea..540c505 100644 --- a/src/libs/i18n/locales/fr.json +++ b/src/libs/i18n/locales/fr.json @@ -6,7 +6,14 @@ "validateData": "Valider les données saisies", "previous": "Précédent", "cancel": "Annuler", - "validate": "Valider" + "validate": "Valider", + "goToPlatineGestion": "Retourner dans Platine Gestion", + "version": "Version {{appVersion}} | Lunatic {{lunaticVersion}}", + "quit": "Quitter", + "leaveConfirmation": "Sauvegarder et quitter", + "leaveBody": "Voulez vous retourner dans Platine Gestion ? Vos données seront sauvegardées.", + "leaveAndSave": "Sauvegarder et quitter", + "saving": "Sauvegarde en cours..." }, "welcomeModal": { "title": "Bienvenue dans Walking Papers", diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index de7a129..d390602 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -5,6 +5,7 @@ type ImportMetaEnv = { // You probably want to add `/src/vite-env.d.ts` to your .prettierignore VITE_API_URL: string VITE_IDENTITY_PROVIDER: string + VITE_PLATINE_GESTION_URL: string VITE_OIDC_ISSUER: string VITE_OIDC_ENABLED: string VITE_OIDC_CLIENT_ID: string From fac4d52487b50e1025fbfb6a92f21dd8bcd37a15 Mon Sep 17 00:00:00 2001 From: Mailine Nguyen <64129348+MailineN@users.noreply.github.com> Date: Fri, 3 Jul 2026 10:20:11 +0200 Subject: [PATCH 2/5] fix: add '/' where it's missing --- src/components/Header.test.tsx | 12 ++++++------ src/components/Header.tsx | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/Header.test.tsx b/src/components/Header.test.tsx index c268c43..4bcdd5f 100644 --- a/src/components/Header.test.tsx +++ b/src/components/Header.test.tsx @@ -59,14 +59,14 @@ describe('Header', () => { }) it('does not show button when interrogation match is not found', () => { - vi.stubEnv('VITE_PLATINE_GESTION_URL', 'https://platine-gestion.url/') + vi.stubEnv('VITE_PLATINE_GESTION_URL', 'https://platine-gestion.url') renderWithI18n(
) expect(screen.queryByText('Go to Platine Gestion')).not.toBeInTheDocument() }) it('shows button when interrogation match is found', () => { - vi.stubEnv('VITE_PLATINE_GESTION_URL', 'https://platine-gestion.url/') + vi.stubEnv('VITE_PLATINE_GESTION_URL', 'https://platine-gestion.url') mockUseMatch.mockReturnValue({ params: { interrogationId: 'test-123' }, }) @@ -77,7 +77,7 @@ describe('Header', () => { }) it('opens dialog when button is clicked', async () => { - vi.stubEnv('VITE_PLATINE_GESTION_URL', 'https://platine-gestion.url/') + vi.stubEnv('VITE_PLATINE_GESTION_URL', 'https://platine-gestion.url') const user = userEvent.setup() mockUseMatch.mockReturnValue({ params: { interrogationId: 'test-123' }, @@ -98,7 +98,7 @@ describe('Header', () => { }) it('closes dialog when cancel is clicked', async () => { - vi.stubEnv('VITE_PLATINE_GESTION_URL', 'https://platine-gestion.url/') + vi.stubEnv('VITE_PLATINE_GESTION_URL', 'https://platine-gestion.url') const user = userEvent.setup() mockUseMatch.mockReturnValue({ params: { interrogationId: 'test-123' }, @@ -115,7 +115,7 @@ describe('Header', () => { }) it('redirects to platine on save and leave', async () => { - const PLATINE_URL = 'https://platine-gestion.url/' + const PLATINE_URL = 'https://platine-gestion.url' vi.stubEnv('VITE_PLATINE_GESTION_URL', PLATINE_URL) const user = userEvent.setup() mockUseMatch.mockReturnValue({ @@ -133,6 +133,6 @@ describe('Header', () => { await user.click(screen.getByRole('button', { name: 'Save and leave' })) expect(executePreLogoutActions).toHaveBeenCalledOnce() - expect(window.location.href).toBe(`${PLATINE_URL}interrogation/test-123`) + expect(window.location.href).toBe(`${PLATINE_URL}/interrogation/test-123`) }) }) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index e6228d4..efa9a42 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -31,7 +31,7 @@ export default function Header() { if (!interrogationMatch?.params?.interrogationId) return await executePreLogoutActions() - window.location.href = `${platineGestionUrl}interrogation/${interrogationMatch.params.interrogationId}` + window.location.href = `${platineGestionUrl}/interrogation/${interrogationMatch.params.interrogationId}` } return ( From c37088af822d5c98c5369db278c7f044bf254308 Mon Sep 17 00:00:00 2001 From: Mailine Nguyen <64129348+MailineN@users.noreply.github.com> Date: Mon, 6 Jul 2026 08:32:49 +0200 Subject: [PATCH 3/5] fix: feedbacks --- .env | 2 +- CHANGELOG.md | 2 +- src/components/Header.test.tsx | 40 ++++++------ src/components/Header.tsx | 53 +--------------- src/components/SaveAndExitButton.tsx | 61 +++++++++++++++++++ src/components/icons/ExitIcon.tsx | 2 +- src/features/orchestrator/Orchestrator.tsx | 2 +- .../orchestrator}/hooks/usePreLogoutAction.ts | 4 +- src/libs/i18n/locales/en.json | 4 +- src/libs/i18n/locales/fr.json | 4 +- src/vite-env.d.ts | 2 +- 11 files changed, 94 insertions(+), 82 deletions(-) create mode 100644 src/components/SaveAndExitButton.tsx rename src/{ => features/orchestrator}/hooks/usePreLogoutAction.ts (71%) diff --git a/.env b/.env index c570c00..e85fb19 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ VITE_API_URL= VITE_IDENTITY_PROVIDER= -VITE_PLATINE_GESTION_URL= +VITE_BASE_EXIT_URL= # OIDC Configurations VITE_OIDC_ISSUER= diff --git a/CHANGELOG.md b/CHANGELOG.md index c3302ed..c74bc64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added Platine Gestion redirection +- Add a button to leave the questionnaire and go back to the base app ### Changed diff --git a/src/components/Header.test.tsx b/src/components/Header.test.tsx index 4bcdd5f..fc8af0e 100644 --- a/src/components/Header.test.tsx +++ b/src/components/Header.test.tsx @@ -1,7 +1,7 @@ import { screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { executePreLogoutActions } from '@/hooks/usePreLogoutAction' +import { executePreLogoutActions } from '@/features/orchestrator/hooks/usePreLogoutAction' import { renderWithI18n } from '@/testing/render' import Header from './Header' @@ -24,7 +24,7 @@ vi.mock('@tanstack/react-router', async () => { } }) -vi.mock('@/hooks/usePreLogoutAction', () => ({ +vi.mock('@/features/orchestrator/hooks/usePreLogoutAction', () => ({ executePreLogoutActions: vi.fn(), })) @@ -32,7 +32,7 @@ describe('Header', () => { beforeEach(() => { vi.stubEnv('APP_VERSION', '1.0.0') vi.stubEnv('LUNATIC_VERSION', '^3.6.9') - vi.stubEnv('VITE_PLATINE_GESTION_URL', '') + vi.stubEnv('VITE_BASE_EXIT_URL', '') mockUseMatch.mockReturnValue(undefined) }) @@ -46,7 +46,7 @@ describe('Header', () => { expect(getByText('Version 1.0.0 | Lunatic 3.6.9')).toBeInTheDocument() }) - it('does not show button when platine URL is not configured', () => { + it('does not show button when redirect URL is not configured', () => { mockUseMatch.mockReturnValue({ params: { interrogationId: 'test-123' }, }) @@ -54,30 +54,32 @@ describe('Header', () => { renderWithI18n(
) expect( - screen.queryByText('Retourner dans Platine Gestion'), + screen.queryByText('Leave the questionnaire'), ).not.toBeInTheDocument() }) it('does not show button when interrogation match is not found', () => { - vi.stubEnv('VITE_PLATINE_GESTION_URL', 'https://platine-gestion.url') + vi.stubEnv('VITE_BASE_EXIT_URL', 'https://base-app.url') renderWithI18n(
) - expect(screen.queryByText('Go to Platine Gestion')).not.toBeInTheDocument() + expect( + screen.queryByText('Leave the questionnaire'), + ).not.toBeInTheDocument() }) it('shows button when interrogation match is found', () => { - vi.stubEnv('VITE_PLATINE_GESTION_URL', 'https://platine-gestion.url') + vi.stubEnv('VITE_BASE_EXIT_URL', 'https://base-app.url') mockUseMatch.mockReturnValue({ params: { interrogationId: 'test-123' }, }) renderWithI18n(
) - expect(screen.getByText('Go to Platine Gestion')).toBeInTheDocument() + expect(screen.getByText('Leave the questionnaire')).toBeInTheDocument() }) it('opens dialog when button is clicked', async () => { - vi.stubEnv('VITE_PLATINE_GESTION_URL', 'https://platine-gestion.url') + vi.stubEnv('VITE_BASE_EXIT_URL', 'https://base-app.url') const user = userEvent.setup() mockUseMatch.mockReturnValue({ params: { interrogationId: 'test-123' }, @@ -85,20 +87,20 @@ describe('Header', () => { renderWithI18n(
) - await user.click(screen.getByText('Go to Platine Gestion')) + await user.click(screen.getByText('Leave the questionnaire')) expect( screen.getByRole('button', { name: 'Save and leave' }), ).toBeInTheDocument() expect( screen.getByText( - 'Do you want to go back to Platine Gestion. Your data will be saved before leaving.', + 'Do you want to leave the questionnaire? Your data will be saved before leaving.', ), ).toBeInTheDocument() }) it('closes dialog when cancel is clicked', async () => { - vi.stubEnv('VITE_PLATINE_GESTION_URL', 'https://platine-gestion.url') + vi.stubEnv('VITE_BASE_EXIT_URL', 'https://base-app.url') const user = userEvent.setup() mockUseMatch.mockReturnValue({ params: { interrogationId: 'test-123' }, @@ -106,7 +108,7 @@ describe('Header', () => { renderWithI18n(
) - await user.click(screen.getByText('Go to Platine Gestion')) + await user.click(screen.getByText('Leave the questionnaire')) await user.click(screen.getByText('Cancel')) expect( @@ -114,9 +116,9 @@ describe('Header', () => { ).not.toBeInTheDocument() }) - it('redirects to platine on save and leave', async () => { - const PLATINE_URL = 'https://platine-gestion.url' - vi.stubEnv('VITE_PLATINE_GESTION_URL', PLATINE_URL) + it('redirects to base app on save and leave', async () => { + const BASE_URL = 'https://base-app.url' + vi.stubEnv('VITE_BASE_EXIT_URL', BASE_URL) const user = userEvent.setup() mockUseMatch.mockReturnValue({ params: { interrogationId: 'test-123' }, @@ -129,10 +131,10 @@ describe('Header', () => { renderWithI18n(
) - await user.click(screen.getByText('Go to Platine Gestion')) + await user.click(screen.getByText('Leave the questionnaire')) await user.click(screen.getByRole('button', { name: 'Save and leave' })) expect(executePreLogoutActions).toHaveBeenCalledOnce() - expect(window.location.href).toBe(`${PLATINE_URL}/interrogation/test-123`) + expect(window.location.href).toBe(`${BASE_URL}/interrogation/test-123`) }) }) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index efa9a42..a978b5f 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,39 +1,10 @@ -import { useState } from 'react' - -import { useMatch } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' -import { executePreLogoutActions } from '@/hooks/usePreLogoutAction' - -import Button, { ButtonStyle } from './Button' -import Dialog from './Dialog' -import ExitIcon from './icons/ExitIcon' +import SaveAndExitButton from './SaveAndExitButton' export default function Header() { const { t } = useTranslation() - const interrogationMatch = useMatch({ - from: '/interrogations/$interrogationId', - shouldThrow: false, - }) - - const platineGestionUrl = import.meta.env.VITE_PLATINE_GESTION_URL - - const [isDialogOpen, setIsDialogOpen] = useState(false) - - const handleGoToInterrogation = () => { - if (interrogationMatch?.params?.interrogationId) { - setIsDialogOpen(true) - } - } - - const handleSaveAndLeave = async () => { - if (!interrogationMatch?.params?.interrogationId) return - - await executePreLogoutActions() - window.location.href = `${platineGestionUrl}/interrogation/${interrogationMatch.params.interrogationId}` - } - return (
@@ -48,27 +19,7 @@ export default function Header() {
- {interrogationMatch && platineGestionUrl && ( - <> - - setIsDialogOpen(false)} - onValidate={handleSaveAndLeave} - controlledOpen={isDialogOpen} - setControlledOpen={setIsDialogOpen} - validateLabel={t('common.leaveAndSave')} - loadingLabel={t('common.saving')} - /> - - )} +
) diff --git a/src/components/SaveAndExitButton.tsx b/src/components/SaveAndExitButton.tsx new file mode 100644 index 0000000..78dd854 --- /dev/null +++ b/src/components/SaveAndExitButton.tsx @@ -0,0 +1,61 @@ +import { useState } from 'react' + +import { useMatch } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' + +import { executePreLogoutActions } from '@/features/orchestrator/hooks/usePreLogoutAction' + +import Button, { ButtonStyle } from './Button' +import Dialog from './Dialog' +import ExitIcon from './icons/ExitIcon' + +export default function SaveAndExitButton() { + const { t } = useTranslation() + const [isDialogOpen, setIsDialogOpen] = useState(false) + + const interrogationMatch = useMatch({ + from: '/interrogations/$interrogationId', + shouldThrow: false, + }) + + const exitUrl = import.meta.env.VITE_BASE_EXIT_URL + + const handleGoToInterrogation = () => { + if (interrogationMatch?.params?.interrogationId) { + setIsDialogOpen(true) + } + } + + const handleSaveAndLeave = async () => { + if (!interrogationMatch?.params?.interrogationId) return + + await executePreLogoutActions() + window.location.href = `${exitUrl}/interrogation/${interrogationMatch.params.interrogationId}` + } + + if (!interrogationMatch || !exitUrl) { + return null + } + + return ( + <> + + setIsDialogOpen(false)} + onValidate={handleSaveAndLeave} + controlledOpen={isDialogOpen} + setControlledOpen={setIsDialogOpen} + validateLabel={t('common.leaveAndSave')} + loadingLabel={t('common.saving')} + /> + + ) +} diff --git a/src/components/icons/ExitIcon.tsx b/src/components/icons/ExitIcon.tsx index 9d3acce..09ce4d6 100644 --- a/src/components/icons/ExitIcon.tsx +++ b/src/components/icons/ExitIcon.tsx @@ -1,5 +1,5 @@ /** - * Icon of exit symbol which should be used when one wants to go back to platine gestion. + * Icon of exit symbol which should be used when one wants to go back to the base app. */ export default function ExitIcon({ height = '24px', diff --git a/src/features/orchestrator/Orchestrator.tsx b/src/features/orchestrator/Orchestrator.tsx index 01fb8e1..29b260a 100644 --- a/src/features/orchestrator/Orchestrator.tsx +++ b/src/features/orchestrator/Orchestrator.tsx @@ -10,7 +10,6 @@ import '@inseefr/lunatic/main.css' import { assert } from 'tsafe/assert' import WelcomeModal from '@/components/WelcomeModal' -import { useAddPreLogoutAction } from '@/hooks/usePreLogoutAction' import type { Interrogation } from '@/models/interrogation' import type { InterrogationData } from '@/models/interrogationData' import type { LunaticGetReferentiel } from '@/models/lunaticType' @@ -23,6 +22,7 @@ import Navigation from './Navigation' import { EndPage } from './customPages/EndPage' import { useInterrogation } from './hooks/useInterrogation' import { useNavigation } from './hooks/useNavigation' +import { useAddPreLogoutAction } from './hooks/usePreLogoutAction' import { useUpdateEffect } from './hooks/useUpdateEffect' import { computeInterrogation, diff --git a/src/hooks/usePreLogoutAction.ts b/src/features/orchestrator/hooks/usePreLogoutAction.ts similarity index 71% rename from src/hooks/usePreLogoutAction.ts rename to src/features/orchestrator/hooks/usePreLogoutAction.ts index ab23929..0b302aa 100644 --- a/src/hooks/usePreLogoutAction.ts +++ b/src/features/orchestrator/hooks/usePreLogoutAction.ts @@ -1,13 +1,11 @@ import { useEffect } from 'react' -import { useEvent } from './useEvent' +import { useEvent } from '@/hooks/useEvent' const actions = new Set<() => Promise | void>() /* Execute pre-logout actions, i.e only save data for now (from Stromae-DSFR) - We need to create this because the exit button is not in an orchestrator component - Another solution would be using a context, but I am not sure if it's a good idea anyway */ export async function executePreLogoutActions() { for (const action of actions) { diff --git a/src/libs/i18n/locales/en.json b/src/libs/i18n/locales/en.json index d630928..ac8e670 100644 --- a/src/libs/i18n/locales/en.json +++ b/src/libs/i18n/locales/en.json @@ -7,11 +7,11 @@ "previous": "Previous", "cancel": "Cancel", "validate": "Validate", - "goToPlatineGestion": "Go to Platine Gestion", + "leaveQuestionnaire": "Leave the questionnaire", "version": "Version {{appVersion}} | Lunatic {{lunaticVersion}}", "quit": "Quit", "leaveConfirmation": "Save and leave", - "leaveBody": "Do you want to go back to Platine Gestion. Your data will be saved before leaving.", + "leaveBody": "Do you want to leave the questionnaire? Your data will be saved before leaving.", "leaveAndSave": "Save and leave", "saving": "Saving..." }, diff --git a/src/libs/i18n/locales/fr.json b/src/libs/i18n/locales/fr.json index 540c505..ba11fc3 100644 --- a/src/libs/i18n/locales/fr.json +++ b/src/libs/i18n/locales/fr.json @@ -7,11 +7,11 @@ "previous": "Précédent", "cancel": "Annuler", "validate": "Valider", - "goToPlatineGestion": "Retourner dans Platine Gestion", + "leaveQuestionnaire": "Quitter le questionnaire", "version": "Version {{appVersion}} | Lunatic {{lunaticVersion}}", "quit": "Quitter", "leaveConfirmation": "Sauvegarder et quitter", - "leaveBody": "Voulez vous retourner dans Platine Gestion ? Vos données seront sauvegardées.", + "leaveBody": "Voulez vous quitter le questionnaire ? Vos données seront sauvegardées.", "leaveAndSave": "Sauvegarder et quitter", "saving": "Sauvegarde en cours..." }, diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index d390602..e7f87a0 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -5,7 +5,7 @@ type ImportMetaEnv = { // You probably want to add `/src/vite-env.d.ts` to your .prettierignore VITE_API_URL: string VITE_IDENTITY_PROVIDER: string - VITE_PLATINE_GESTION_URL: string + VITE_BASE_EXIT_URL: string VITE_OIDC_ISSUER: string VITE_OIDC_ENABLED: string VITE_OIDC_CLIENT_ID: string From ebc70b5ffcc06281e8608f58036231052873791c Mon Sep 17 00:00:00 2001 From: Mailine Nguyen <64129348+MailineN@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:27:55 +0200 Subject: [PATCH 4/5] fix: add missing s in redirect path --- src/components/Header.test.tsx | 2 +- src/components/SaveAndExitButton.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Header.test.tsx b/src/components/Header.test.tsx index fc8af0e..7a4628a 100644 --- a/src/components/Header.test.tsx +++ b/src/components/Header.test.tsx @@ -135,6 +135,6 @@ describe('Header', () => { await user.click(screen.getByRole('button', { name: 'Save and leave' })) expect(executePreLogoutActions).toHaveBeenCalledOnce() - expect(window.location.href).toBe(`${BASE_URL}/interrogation/test-123`) + expect(window.location.href).toBe(`${BASE_URL}/interrogations/test-123`) }) }) diff --git a/src/components/SaveAndExitButton.tsx b/src/components/SaveAndExitButton.tsx index 78dd854..df2a178 100644 --- a/src/components/SaveAndExitButton.tsx +++ b/src/components/SaveAndExitButton.tsx @@ -30,7 +30,7 @@ export default function SaveAndExitButton() { if (!interrogationMatch?.params?.interrogationId) return await executePreLogoutActions() - window.location.href = `${exitUrl}/interrogation/${interrogationMatch.params.interrogationId}` + window.location.href = `${exitUrl}/interrogations/${interrogationMatch.params.interrogationId}` } if (!interrogationMatch || !exitUrl) { From 576cec02f6bd5847e64037d8fbe1fbd2e4bf9b18 Mon Sep 17 00:00:00 2001 From: Mailine Nguyen <64129348+MailineN@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:30:20 +0200 Subject: [PATCH 5/5] bump:1.0.4-rc-exit-redirection.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eed4dfa..14ed632 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "orchestrateur-saisie-papier", "private": true, - "version": "1.0.3", + "version": "1.0.4-rc-exit-redirection.0", "type": "module", "packageManager": "pnpm@11.8.0", "scripts": {