diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e8bdb282..5c7bcd77b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to - ✨(y-provider) preserve callouts, PDFs, page breaks, interlinking links and commented text on HTML/markdown export #2296 +- ✨(frontend) add a user menu #2463 ### Changed diff --git a/src/frontend/apps/e2e/__tests__/app-impress/auth.setup.ts b/src/frontend/apps/e2e/__tests__/app-impress/auth.setup.ts index 5ccee2192..a28440f44 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/auth.setup.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/auth.setup.ts @@ -24,10 +24,11 @@ const saveStorageState = async ( await SignIn(page, browserName); + /** + * If the grid is displayed, it means the user is logged in and the storage state can be saved. + */ await expect( - page.locator('header').first().getByRole('button', { - name: 'Logout', - }), + page.getByRole('heading', { name: 'All docs', level: 2 }), ).toBeVisible({ timeout: 10000 }); await page.context().storageState({ diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts index 2b4f746a2..0b7ad3a9d 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts @@ -17,6 +17,7 @@ import { updateRoleUser, updateShareLink, } from './utils-share'; +import { logOut } from './utils-signin'; test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -312,7 +313,7 @@ test.describe('Doc Comments', () => { await updateShareLink(page, 'Public', 'Editing'); // Anonymous user can see and add comments - await otherPage.getByRole('button', { name: 'Logout' }).click(); + await logOut(otherPage); await expect( otherPage diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts index c6c804e8c..37817bc12 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts @@ -9,7 +9,7 @@ import { } from './utils-common'; import { writeInEditor } from './utils-editor'; import { connectOtherUserToDoc, updateRoleUser } from './utils-share'; -import { SignIn } from './utils-signin'; +import { SignIn, logOut } from './utils-signin'; import { createRootSubPage } from './utils-sub-pages'; test.describe('Document create member', () => { @@ -371,11 +371,7 @@ test.describe('Document create member: Multiple login', () => { const urlDoc = page.url(); - await page - .getByRole('button', { - name: 'Logout', - }) - .click(); + await logOut(page); const otherBrowser = BROWSERS.find((b) => b !== browserName); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts index 060eb4a33..d6d4cfaaf 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts @@ -407,21 +407,23 @@ test.describe('Doc Tree', () => { await expect(selectedSubDoc).toBeFocused(); await page.keyboard.press('Tab'); + await expect(page.getByLabel('User menu')).toBeFocused(); + await page.keyboard.press('Tab'); await expect(page.getByLabel('Open help menu')).toBeFocused(); await page.keyboard.press('Tab'); - await expect( page.locator('[data-panel-resize-handle-id]').first(), ).toBeFocused(); await page.keyboard.press('Shift+Tab'); - await expect(page.getByLabel('Open help menu')).toBeFocused(); await page.keyboard.press('Shift+Tab'); + await expect(page.getByLabel('User menu')).toBeFocused(); + await page.keyboard.press('Shift+Tab'); await expect(selectedSubDoc).toBeFocused(); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts index 6dca8b40b..548880600 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts @@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test'; import { BROWSERS, createDoc, verifyDocName } from './utils-common'; import { getEditor, writeInEditor } from './utils-editor'; import { addNewMember, connectOtherUserToDoc } from './utils-share'; -import { SignIn, expectLoginPage } from './utils-signin'; +import { SignIn, expectLoginPage, logOut } from './utils-signin'; import { createRootSubPage } from './utils-sub-pages'; test.describe('Doc Visibility', () => { @@ -81,13 +81,7 @@ test.describe('Doc Visibility: Restricted', () => { await verifyDocName(page, docTitle); const urlDoc = page.url(); - - await page - .getByRole('button', { - name: 'Logout', - }) - .click(); - + await logOut(page); await expectLoginPage(page); await page.goto(urlDoc); @@ -112,11 +106,7 @@ test.describe('Doc Visibility: Restricted', () => { const urlDoc = page.url(); - await page - .getByRole('button', { - name: 'Logout', - }) - .click(); + await logOut(page); const otherBrowser = BROWSERS.find((b) => b !== browserName); if (!otherBrowser) { diff --git a/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts index 2036733d8..cdeacf0df 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts @@ -1,7 +1,6 @@ import { expect, test } from '@playwright/test'; import { overrideConfig } from './utils-common'; -import { SignIn, expectLoginPage } from './utils-signin'; test.describe('Header', () => { test('checks all the elements are visible', async ({ page }) => { @@ -14,14 +13,6 @@ test.describe('Header', () => { 'font-family', /Roboto/i, ); - - await expect( - header.getByRole('button', { - name: 'Logout', - }), - ).toBeVisible(); - - await expect(header.getByText('English')).toBeVisible(); }); test('checks all the elements are visible with DSFR theme', async ({ @@ -106,21 +97,3 @@ test.describe('Header', () => { await expect(logoImage).toHaveAttribute('alt', ''); }); }); - -test.describe('Header: Log out', () => { - test.use({ storageState: { cookies: [], origins: [] } }); - - // eslint-disable-next-line playwright/expect-expect - test('checks logout button', async ({ page, browserName }) => { - await page.goto('/'); - await SignIn(page, browserName); - - await page - .getByRole('button', { - name: 'Logout', - }) - .click(); - - await expectLoginPage(page); - }); -}); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts index 56e20515e..51082d147 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts @@ -40,75 +40,71 @@ test.describe('Language', () => { }); test('checks language switching', async ({ page }) => { - const header = page.locator('header').first(); - const languagePicker = header.locator('.--docs--language-picker-text'); - - await expect(page.locator('html')).toHaveAttribute('lang', 'en-us'); - - // initial language should be english - await expect( - page.getByRole('link', { - name: 'New', - exact: true, - }), - ).toBeVisible(); - // switch to french await waitForLanguageSwitch(page, TestLanguage.French); await expect(page.locator('html')).toHaveAttribute('lang', 'fr'); + await page.getByLabel('Menu utilisateur').click(); await expect( - header.getByRole('button').getByText('Français'), + page.getByRole('button', { name: 'Language: Français' }), ).toBeVisible(); - await expect(page.getByLabel('Se déconnecter')).toBeVisible(); + await expect(page.getByText('Déconnexion')).toBeVisible(); + + await page.keyboard.press('Escape'); // Switch to German using the utility function for consistency - await waitForLanguageSwitch(page, TestLanguage.German); - await expect(header.getByRole('button').getByText('Deutsch')).toBeVisible(); + await waitForLanguageSwitch(page, TestLanguage.German, 'Menu utilisateur'); - await expect(page.getByLabel('Abmelden')).toBeVisible(); + await page.getByLabel('User menu').click(); + await expect( + page.getByRole('button', { name: 'Language: Deutsch' }), + ).toBeVisible(); + + await expect(page.getByText('Logout', { exact: true })).toBeVisible(); await expect(page.locator('html')).toHaveAttribute('lang', 'de'); - - await languagePicker.click(); - - await expect(page.locator('[role="menu"]')).toBeVisible(); - - const menuItems = page.locator('[role="menuitemradio"]'); - await expect(menuItems.first()).toBeVisible(); - - await menuItems.first().click(); - - await expect(page.locator('html')).toHaveAttribute('lang', 'en'); - await expect(languagePicker).toContainText('English'); }); test('can switch language using only keyboard', async ({ page }) => { - await page.goto('/'); - await waitForLanguageSwitch(page, TestLanguage.English); + await waitForLanguageSwitch(page, TestLanguage.English, 'User menu', false); - const languagePicker = page.getByRole('button', { - name: /select language/i, - }); - - await expect(languagePicker).toBeVisible(); + await page.getByLabel('User menu').click(); await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Enter'); - const menu = page.getByRole('menu'); - await expect(menu).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'English' })).toBeVisible(); + await expect( + page.getByRole('menuitem', { name: 'Français' }), + ).toBeVisible(); + + await page.waitForTimeout(300); await page.keyboard.press('ArrowDown'); await page.keyboard.press('Enter'); - await expect(page.locator('html')).not.toHaveAttribute('lang', 'en-us'); + await expect(page.locator('html')).toHaveAttribute('lang', 'fr'); + await expect( + page.getByRole('button', { name: 'Déconnexion' }), + ).toBeVisible(); + + await page.keyboard.press('Escape'); + + await page.getByLabel('Menu utilisateur').click(); + + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Enter'); + await page.waitForTimeout(300); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowUp'); + await page.keyboard.press('Enter'); + + await expect(page.locator('html')).toHaveAttribute('lang', 'en'); + await expect(page.getByRole('button', { name: 'Logout' })).toBeVisible(); }); test('checks that backend uses the same language as the frontend', async ({ @@ -171,7 +167,7 @@ test.describe('Language', () => { /** * Swedish is not yet supported in the BlockNote locales, so it should fallback to English */ - await waitForLanguageSwitch(page, TestLanguage.Swedish); + await waitForLanguageSwitch(page, TestLanguage.Swedish, 'Menu utilisateur'); await openSuggestionMenu({ page }); await expect( suggestionMenu.getByText('Headings', { exact: true }), diff --git a/src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts index e3104b4bd..5a3eeddd2 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts @@ -7,6 +7,7 @@ import { verifyDocName, } from './utils-common'; import { tryFocusEditorContent } from './utils-editor'; +import { SignIn, expectLoginPage, logOut } from './utils-signin'; import { createRootSubPage } from './utils-sub-pages'; test.describe('Left panel desktop', () => { @@ -19,6 +20,8 @@ test.describe('Left panel desktop', () => { await expect(page.getByTestId('left-panel-mobile')).toBeHidden(); await expect(page.getByTestId('home-button')).toBeHidden(); await expect(page.getByTestId('new-doc-button')).toBeVisible(); + await expect(page.getByLabel('User menu')).toBeVisible(); + await expect(page.getByLabel('Open help menu')).toBeVisible(); await goToGridDoc(page); @@ -178,6 +181,7 @@ test.describe('Left panel desktop', () => { test.describe('Left panel responsive', () => { test('checks elements visibility on different screen sizes', async ({ page, + browserName, }) => { await page.setViewportSize({ width: 500, height: 1200 }); await page.goto('/'); @@ -188,26 +192,22 @@ test.describe('Left panel responsive', () => { const header = page.locator('header').first(); const homeButton = page.getByTestId('home-button'); const newDocButton = page.getByTestId('new-doc-button'); - const languageButton = page.getByRole('button', { - name: 'Select language', + const userMenu = page.getByRole('button', { + name: 'User menu', }); - const logoutButton = page.getByRole('button', { name: 'Logout' }); await expect(homeButton).not.toBeInViewport(); await expect(newDocButton).not.toBeInViewport(); - await expect(languageButton).not.toBeInViewport(); - await expect(logoutButton).not.toBeInViewport(); + await expect(userMenu).not.toBeInViewport(); - const title = await goToGridDoc(page); - await verifyDocName(page, title); + await createDoc(page, 'mobile-doc-test', browserName, 1, true); await header.getByLabel('Open the header menu').click(); await expect(page.getByTestId('left-panel-mobile')).toBeInViewport(); await expect(homeButton).toBeInViewport(); await expect(newDocButton).toBeInViewport(); - await expect(languageButton).toBeInViewport(); - await expect(logoutButton).toBeInViewport(); + await expect(userMenu).toBeInViewport(); await header.getByLabel('Close the header menu').click(); @@ -217,8 +217,7 @@ test.describe('Left panel responsive', () => { await expect(page.getByRole('link', { name: 'All docs' })).toBeInViewport(); await expect(newDocButton).toBeInViewport(); - await expect(languageButton).toBeInViewport(); - await expect(logoutButton).toBeInViewport(); + await expect(userMenu).toBeInViewport(); await expect(header.getByLabel('Open the header menu')).toBeHidden(); }); @@ -309,3 +308,16 @@ test.describe('Left panel responsive', () => { await expect(leftPanel).toBeHidden(); }); }); + +test.describe('Left Panel: Log out', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + // eslint-disable-next-line playwright/expect-expect + test('checks logout button', async ({ page, browserName }) => { + await page.goto('/'); + await SignIn(page, browserName); + await logOut(page); + + await expectLoginPage(page); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts index 24b2c0b88..c880c3aa9 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -399,9 +399,11 @@ type TestLanguageValue = (typeof TestLanguage)[TestLanguageKey]; export async function waitForLanguageSwitch( page: Page, lang: TestLanguageValue, + labelUserMenu = 'User menu', + withMocking = true, ) { await page.route(/\**\/api\/v1.0\/users\/\**/, async (route, request) => { - if (request.method().includes('PATCH')) { + if (request.method().includes('PATCH') && withMocking) { await route.fulfill({ json: { language: lang.expectedLocale[0], @@ -412,19 +414,21 @@ export async function waitForLanguageSwitch( } }); - const header = page.locator('header').first(); - const languagePicker = header.locator('.--docs--language-picker-text'); + await page.getByLabel(labelUserMenu).click(); + const languagePicker = page.getByRole('button', { name: /Language/ }); const isAlreadyTargetLanguage = await languagePicker .innerText() .then((text) => text.toLowerCase().includes(lang.label.toLowerCase())); if (isAlreadyTargetLanguage) { + await page.keyboard.press('Escape'); return; } await languagePicker.click(); - await page.getByRole('menuitemradio', { name: lang.label }).click(); + await page.getByRole('menuitem', { name: lang.label }).click(); + await page.keyboard.press('Escape'); } export const clickInEditorShareButton = async (page: Page) => { diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-signin.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-signin.ts index a3331011f..1c7efeadf 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-signin.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-signin.ts @@ -13,6 +13,11 @@ export const SignIn = async ( await keycloakSignIn(page, browserName, fromHome); }; +export const logOut = async (page: Page) => { + await page.getByLabel('User menu').click(); + await page.getByText('Logout', { exact: true }).click(); +}; + export const customSignIn = async ( page: Page, browserName: string, diff --git a/src/frontend/apps/impress/package.json b/src/frontend/apps/impress/package.json index fd0ee4682..8572c1412 100644 --- a/src/frontend/apps/impress/package.json +++ b/src/frontend/apps/impress/package.json @@ -42,7 +42,7 @@ "@fontsource/material-icons": "5.2.7", "@gouvfr-lasuite/cunningham-react": "4.3.0", "@gouvfr-lasuite/integration": "1.0.3", - "@gouvfr-lasuite/ui-kit": "0.23.2", + "@gouvfr-lasuite/ui-kit": "0.25.0", "@hocuspocus/provider": "3.4.4", "@mantine/core": "9.3.1", "@mantine/hooks": "9.3.1", diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-tokens.css b/src/frontend/apps/impress/src/cunningham/cunningham-tokens.css index 7a9b67987..f995bee96 100644 --- a/src/frontend/apps/impress/src/cunningham/cunningham-tokens.css +++ b/src/frontend/apps/impress/src/cunningham/cunningham-tokens.css @@ -838,6 +838,9 @@ --c--components--button--border-radius: 4px; --c--components--button--border-radius--active: 4px; --c--components--button--border-radius--focus: 4px; + --c--components--resize-handle--hover--color: var( + --c--contextuals--border--surface--primary + ); --c--components--datagrid--header--color: var( --c--contextuals--content--semantic--neutral--primary ); @@ -2539,6 +2542,9 @@ --c--components--button--border-radius: 4px; --c--components--button--border-radius--active: 4px; --c--components--button--border-radius--focus: 4px; + --c--components--resize-handle--hover--color: var( + --c--contextuals--border--surface--primary + ); --c--components--datagrid--header--color: var( --c--contextuals--content--semantic--neutral--primary ); diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts b/src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts index b6c84c50b..1e5fb8bdf 100644 --- a/src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts +++ b/src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts @@ -657,6 +657,7 @@ export const tokens = { 'border-radius--active': '4px', 'border-radius--focus': '4px', }, + 'resize-handle': { 'hover--color': '#E2E2EA' }, datagrid: { 'header--color': '#25252F', 'header--size': '12px', @@ -1961,6 +1962,7 @@ export const tokens = { 'border-radius--active': '4px', 'border-radius--focus': '4px', }, + 'resize-handle': { 'hover--color': '#E2E2EA' }, datagrid: { 'header--color': '#25252F', 'header--size': '12px', diff --git a/src/frontend/apps/impress/src/features/auth/components/ButtonLogin.tsx b/src/frontend/apps/impress/src/features/auth/components/ButtonLogin.tsx index b4666f47a..2d5e5da18 100644 --- a/src/frontend/apps/impress/src/features/auth/components/ButtonLogin.tsx +++ b/src/frontend/apps/impress/src/features/auth/components/ButtonLogin.tsx @@ -2,51 +2,30 @@ import { Button } from '@gouvfr-lasuite/cunningham-react'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; -import { Box, BoxButton } from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; +import { BoxButton } from '@/components'; import ProConnectImg from '../assets/button-proconnect.svg'; import { useAuth } from '../hooks'; -import { gotoLogin, gotoLogout } from '../utils'; +import { gotoLogin } from '../utils'; export const ButtonLogin = () => { const { t } = useTranslation(); const { authenticated } = useAuth(); - const { colorsTokens } = useCunninghamTheme(); - if (!authenticated) { - return ( - - ); + if (authenticated) { + return null; } return ( - gotoLogin()} + color="brand" + size="small" + aria-label={t('Sign in')} + className="--docs--button-login" > - - + {t('Sign in')} + ); }; diff --git a/src/frontend/apps/impress/src/features/auth/hooks/useAuth.tsx b/src/frontend/apps/impress/src/features/auth/hooks/useAuth.tsx index 0663a99d9..a7a6e11d5 100644 --- a/src/frontend/apps/impress/src/features/auth/hooks/useAuth.tsx +++ b/src/frontend/apps/impress/src/features/auth/hooks/useAuth.tsx @@ -8,14 +8,14 @@ import { useAuthQuery } from '../api'; const regexpUrlsAuth = [/\/docs\/$/g, /\/docs$/g, /^\/$/g]; export const useAuth = () => { - const { data: user, ...authStates } = useAuthQuery(); + const { data: user, isFetched, isLoading, isSuccess } = useAuthQuery(); const { pathname } = useRouter(); const { trackEvent } = useAnalytics(); - const [hasTracked, setHasTracked] = useState(authStates.isFetched); - const isAuthLoading = - authStates.fetchStatus !== 'idle' || authStates.isLoading; + const [hasTracked, setHasTracked] = useState(isFetched); + const isAuthLoading = isLoading; + const hasInitiallyLoaded = useRef(false); - if (authStates.isFetched) { + if (isFetched) { hasInitiallyLoaded.current = true; } const [pathAllowed, setPathAllowed] = useState( @@ -27,7 +27,7 @@ export const useAuth = () => { }, [pathname]); useEffect(() => { - if (!hasTracked && user && authStates.isSuccess) { + if (!hasTracked && user && isSuccess) { trackEvent({ eventName: 'user', id: user?.id || '', @@ -35,14 +35,13 @@ export const useAuth = () => { }); setHasTracked(true); } - }, [hasTracked, authStates.isSuccess, user, trackEvent]); + }, [hasTracked, isSuccess, user, trackEvent]); return { user, - authenticated: !!user && authStates.isSuccess, + authenticated: !!user && isSuccess, pathAllowed, hasInitiallyLoaded: hasInitiallyLoaded.current, isAuthLoading, - ...authStates, }; }; diff --git a/src/frontend/apps/impress/src/features/header/components/Header.tsx b/src/frontend/apps/impress/src/features/header/components/Header.tsx index 4ce20a093..ae97f09cc 100644 --- a/src/frontend/apps/impress/src/features/header/components/Header.tsx +++ b/src/frontend/apps/impress/src/features/header/components/Header.tsx @@ -6,7 +6,6 @@ import { Box, SkipToContent, StyledLink } from '@/components/'; import { useConfig } from '@/core/config'; import { useCunninghamTheme } from '@/cunningham'; import { ButtonLogin } from '@/features/auth'; -import { LanguagePicker } from '@/features/language'; import { LeftPanelToggleMobile } from '@/features/left-panel'; import { useResponsiveStore } from '@/stores'; @@ -89,7 +88,6 @@ export const Header = () => { $direction="row" > - )} diff --git a/src/frontend/apps/impress/src/features/home/components/HomeHeader.tsx b/src/frontend/apps/impress/src/features/home/components/HomeHeader.tsx index 0c854c1bf..e33746354 100644 --- a/src/frontend/apps/impress/src/features/home/components/HomeHeader.tsx +++ b/src/frontend/apps/impress/src/features/home/components/HomeHeader.tsx @@ -5,7 +5,7 @@ import { Waffle } from '@/components/Waffle'; import { useConfig } from '@/core'; import { useCunninghamTheme } from '@/cunningham'; import { Title } from '@/features/header'; -import { LanguagePicker } from '@/features/language'; +import { LanguagePickerLegacy } from '@/features/language'; import { LeftPanelToggleMobile } from '@/features/left-panel'; import { useResponsiveStore } from '@/stores'; @@ -80,7 +80,7 @@ export const HomeHeader = () => { {!isSmallMobile && ( - + )} diff --git a/src/frontend/apps/impress/src/features/language/components/LanguagePicker.tsx b/src/frontend/apps/impress/src/features/language/components/LanguagePicker.tsx index 59655ca59..75b219fff 100644 --- a/src/frontend/apps/impress/src/features/language/components/LanguagePicker.tsx +++ b/src/frontend/apps/impress/src/features/language/components/LanguagePicker.tsx @@ -1,3 +1,7 @@ +import { + LanguagePicker as LanguagePickerUIKit, + LanguagesOption, +} from '@gouvfr-lasuite/ui-kit'; import { announce } from '@react-aria/live-announcer'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -11,7 +15,13 @@ import { useSynchronizedLanguage, } from '@/features/language'; -export const LanguagePicker = () => { +/** + * LanguagePickerLegacy component for selecting language. + * We still have some legacy code that uses this component, so we keep it for now. + * @deprecated Use LanguagePicker instead. + * @returns JSX.Element + */ +export const LanguagePickerLegacy = () => { const { t, i18n } = useTranslation(); const { data: conf } = useConfig(); const { data: user } = useAuthQuery(); @@ -85,3 +95,43 @@ export const LanguagePicker = () => { ); }; + +export const LanguagePicker = () => { + const { t, i18n } = useTranslation(); + const { data: conf } = useConfig(); + const { data: user } = useAuthQuery(); + const { changeLanguageSynchronized } = useSynchronizedLanguage(); + const language = i18n.language; + + const languages: LanguagesOption[] = useMemo(() => { + const backendOptions = conf?.LANGUAGES ?? [[language, language]]; + return backendOptions.map(([backendLocale, backendLabel]) => ({ + label: backendLabel, + value: backendLocale, + isChecked: getMatchingLocales([backendLocale], [language]).length > 0, + })); + }, [conf?.LANGUAGES, language]); + + const onChange = (value: string) => { + const lang = conf?.LANGUAGES?.find(([code]) => code === value); + const backendLabel = lang?.[1] ?? value; + void changeLanguageSynchronized(value, user).then(() => { + announce( + t('Language changed to {{language}}', { + language: backendLabel, + defaultValue: `Language changed to ${backendLabel}`, + }), + 'polite', + ); + }); + }; + + return ( + + ); +}; diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFooter.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFooter.tsx index 5c6e762db..54082afac 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFooter.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFooter.tsx @@ -1,12 +1,24 @@ +import { UserMenu } from '@gouvfr-lasuite/ui-kit'; +import { useTranslation } from 'react-i18next'; + import { Box, SeparatedSection } from '@/components'; import { Waffle } from '@/components/Waffle'; import { ButtonLogin } from '@/features/auth'; +import { useAuth } from '@/features/auth/hooks/useAuth'; +import { gotoLogout } from '@/features/auth/utils'; import { HelpMenu } from '@/features/help'; -import { LanguagePicker } from '@/features/language'; +import { LanguagePicker } from '@/features/language/components/LanguagePicker'; import { useResponsiveStore } from '@/stores'; export const LeftPanelFooter = () => { const { isLargeScreen } = useResponsiveStore(); + const { t } = useTranslation(); + const { user } = useAuth(); + + const userMenu = user || { + full_name: t('Guest'), + email: '', + }; return ( @@ -15,14 +27,15 @@ export const LeftPanelFooter = () => { $justify="space-between" $direction="row" > - + + } + withMobileView={false} + /> - {!isLargeScreen && ( - <> - - - - )} + {!isLargeScreen && } diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index fb673957b..49e02c248 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -2128,10 +2128,10 @@ resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/integration/-/integration-1.0.3.tgz#7aca824ba61d343a7905dc90c8a8bbdbce8f9a09" integrity sha512-OgP28CqlPi35wQPul1Dr52SngACXAk8buLGqHYXDp23fbTOJThqarrZE/pgJHoc9Ndwiu7ngwBSO4rZ7OPyMpA== -"@gouvfr-lasuite/ui-kit@0.23.2": - version "0.23.2" - resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/ui-kit/-/ui-kit-0.23.2.tgz#9a7531fb35821b75b786bfc3167ad53aab116c4c" - integrity sha512-o6bQjqJadBbWMbFiwDYwVeNKYAD/EGeU2W0/omqNgTEYWjKQCGYwgcxgeYR+SZN9GnWKCXEARs18aYgBl+oRbQ== +"@gouvfr-lasuite/ui-kit@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/ui-kit/-/ui-kit-0.25.0.tgz#9feb7da860ea610cb5f171b90ee074fe39271940" + integrity sha512-yvP1/rpckaDPFGcuNiZqwUn43asnSq8ffS660Oikxrq/m+/5oMtRsApHjkt2Wtn1HfUWGvqazN8wSrr1yEwG0Q== dependencies: "@dnd-kit/core" "6.3.1" "@dnd-kit/modifiers" "9.0.0" @@ -8323,11 +8323,6 @@ crelt@^1.0.0: resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== -crisp-sdk-web@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/crisp-sdk-web/-/crisp-sdk-web-1.1.2.tgz#e5b31ee784cef678a711170f54eca68e28968a22" - integrity sha512-qxSpiT4EGHnVEO6J/t1UURY6fg1ojgquLU0IIr7RNjZxXZs1R2e6AhAiFqxiGZ13Vb4o5VoUpATFU2TzX/ppOQ== - cross-env@10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-10.1.0.tgz#cfd2a6200df9ed75bfb9cb3d7ce609c13ea21783"