diff --git a/package-lock.json b/package-lock.json index eca7b5d10..3a4709e78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@mui/material": "^5.16.14", "@mui/system": "^5.14.9", "@mui/x-date-pickers": "^6.13.0", - "@pagopa/interop-fe-commons": "^1.4.6", + "@pagopa/interop-fe-commons": "^1.5.2", "@pagopa/mui-italia": "^1.8.0", "@tanstack/react-query": "^5.51.4", "@tanstack/react-query-devtools": "^5.51.4", @@ -33,7 +33,7 @@ "react-error-boundary": "^4.0.11", "react-hook-form": "^7.46.1", "react-i18next": "^13.2.2", - "react-router-dom": "latest", + "react-router-dom": "^6.16.0", "ts-pattern": "^5.2.0", "zod": "^4.0.0", "zustand": "^4.4.1" @@ -1752,9 +1752,10 @@ "dev": true }, "node_modules/@pagopa/interop-fe-commons": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@pagopa/interop-fe-commons/-/interop-fe-commons-1.4.6.tgz", - "integrity": "sha512-GRQrO0ks7woeWgKjEekL1rlW/y3MfhlRBXycqstFRGbJGYdq+np+kxLC/k5VyujCVG+CxbB+qOlcP2aNQJoCag==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagopa/interop-fe-commons/-/interop-fe-commons-1.5.2.tgz", + "integrity": "sha512-Enox13h4exxfXKRzGxipNbDO/VGbE2fF7biZRexXw4jZpflRXiBF+T1bpiWDJwpmjOB3LZkcgnzDGiGNj7oXSA==", + "license": "MIT", "peerDependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", @@ -1764,7 +1765,7 @@ "@mui/material": "^5.14.9", "@mui/system": "^5.14.9", "@mui/x-date-pickers": "^6.13.0", - "@pagopa/mui-italia": "^1.0.1", + "@pagopa/mui-italia": "^1.9.0", "date-fns": "^2.30.0", "i18next": "^23.5.1", "mixpanel-browser": "^2.50.0", @@ -1772,7 +1773,8 @@ "react-dom": "^18.2.0", "react-hook-form": "^7.46.1", "react-i18next": "^13.2.2", - "react-router-dom": "^6.16.0" + "react-router-dom": "^6.16.0", + "zod": "^4.0.0" } }, "node_modules/@pagopa/mui-italia": { @@ -1855,11 +1857,12 @@ } }, "node_modules/@remix-run/router": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.3.tgz", - "integrity": "sha512-ceuyTSs7PZ/tQqi19YZNBc5X7kj1f8p+4DIyrcIYFY9h+hd1OKm4RqtiWldR9eGEvIiJfsqwM4BsuCtRIuEw6Q==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", "engines": { - "node": ">=14" + "node": ">=14.0.0" } }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -8108,29 +8111,31 @@ } }, "node_modules/react-router": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.4.3.tgz", - "integrity": "sha512-BT6DoGn6aV1FVP5yfODMOiieakp3z46P1Fk0RNzJMACzE7C339sFuHebfvWtnB4pzBvXXkHP2vscJzWRuUjTtA==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.0.3" + "@remix-run/router": "1.23.2" }, "engines": { - "node": ">=14" + "node": ">=14.0.0" }, "peerDependencies": { "react": ">=16.8" } }, "node_modules/react-router-dom": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.3.tgz", - "integrity": "sha512-MiaYQU8CwVCaOfJdYvt84KQNjT78VF0TJrA17SIQgNHRvLnXDJO6qsFqq8F/zzB1BWZjCFIrQpu4QxcshitziQ==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.0.3", - "react-router": "6.4.3" + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" }, "engines": { - "node": ">=14" + "node": ">=14.0.0" }, "peerDependencies": { "react": ">=16.8", diff --git a/package.json b/package.json index e6415a9ad..42f1154a5 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@mui/material": "^5.16.14", "@mui/system": "^5.14.9", "@mui/x-date-pickers": "^6.13.0", - "@pagopa/interop-fe-commons": "^1.4.6", + "@pagopa/interop-fe-commons": "^1.5.2", "@pagopa/mui-italia": "^1.8.0", "@tanstack/react-query": "^5.51.4", "@tanstack/react-query-devtools": "^5.51.4", @@ -38,7 +38,7 @@ "react-error-boundary": "^4.0.11", "react-hook-form": "^7.46.1", "react-i18next": "^13.2.2", - "react-router-dom": "latest", + "react-router-dom": "^6.16.0", "ts-pattern": "^5.2.0", "zod": "^4.0.0", "zustand": "^4.4.1" diff --git a/src/App.tsx b/src/App.tsx index 831d2600f..efe18f471 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,7 +15,6 @@ import { MaintenanceBanner } from './components/shared/banners/MaintenanceBanner import { FirstLoadingSpinner } from './components/shared/FirstLoadingSpinner' import { queryClient } from './config/query-client' import type { EnvironmentBannerProps } from '@pagopa/mui-italia' - import { AuthQueries } from './api/auth' import i18n from './config/react-i18next' @@ -31,7 +30,6 @@ if (redirectUrl) { } else { queryClient.prefetchQuery(AuthQueries.getSessionToken()) } - // end init --- function App() { diff --git a/src/components/dialogs/DialogDeleteOperator.tsx b/src/components/dialogs/DialogDeleteOperator.tsx index 437e9cb17..dc7b6a5be 100644 --- a/src/components/dialogs/DialogDeleteOperator.tsx +++ b/src/components/dialogs/DialogDeleteOperator.tsx @@ -1,4 +1,5 @@ import { SELFCARE_BASE_URL } from '@/config/env' +import useCurrentLanguage from '@/hooks/useCurrentLanguage' import { useDialog } from '@/stores' import type { DialogDeleteOperatorProps } from '@/types/dialog.types' import { @@ -19,10 +20,11 @@ export const DialogDeleteOperator: React.FC = ({ const ariaLabelId = React.useId() const ariaDescriptionId = React.useId() const { closeDialog } = useDialog() + const lang = useCurrentLanguage() const { t: tCommon } = useTranslation('common', { keyPrefix: 'actions' }) const { t } = useTranslation('shared-components', { keyPrefix: 'dialogDeleteOperator' }) - const selfcareUserPageUrl = `${SELFCARE_BASE_URL}/dashboard/${selfcareId}/users/${userId}` + const selfcareUserPageUrl = `${SELFCARE_BASE_URL}/dashboard/${selfcareId}/users/${userId}?lang=${lang}` const handleCancel = () => { closeDialog() diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index cca63577f..00b7c001b 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -145,7 +145,7 @@ export const Header: React.FC = ({ jwt, isSupport }) => { window.location.assign( `${SELFCARE_BASE_URL}/token-exchange?institutionId=${ party.id - }&productId=${getCurrentSelfCareProductId()}` + }&productId=${getCurrentSelfCareProductId()}&lang=${lang}` ) } @@ -155,12 +155,12 @@ export const Header: React.FC = ({ jwt, isSupport }) => { if (!selfcareId) return if (product.id === 'selfcare') { - window.location.assign(`${SELFCARE_BASE_URL}/dashboard/${selfcareId}`) + window.location.assign(`${SELFCARE_BASE_URL}/dashboard/${selfcareId}?lang=${lang}`) return } window.location.assign( - `${SELFCARE_BASE_URL}/token-exchange?institutionId=${selfcareId}&productId=${product.id}` + `${SELFCARE_BASE_URL}/token-exchange?institutionId=${selfcareId}&productId=${product.id}&lang=${lang}` ) } diff --git a/src/components/layout/__test__/Header.test.tsx b/src/components/layout/__test__/Header.test.tsx index 5f3272f1a..1931bcb82 100644 --- a/src/components/layout/__test__/Header.test.tsx +++ b/src/components/layout/__test__/Header.test.tsx @@ -379,7 +379,7 @@ describe('Header', () => { await user.click(selectPartyButton) expect(mockWindowAssign).toBeCalledWith( - `${SELFCARE_BASE_URL}/token-exchange?institutionId=${'test-party-id'}&productId=prod-interop` + `${SELFCARE_BASE_URL}/token-exchange?institutionId=${'test-party-id'}&productId=prod-interop&lang=it` ) }) @@ -451,7 +451,7 @@ describe('Header', () => { await user.click(selectProductButton) - expect(mockWindowAssign).toBeCalledWith(`${SELFCARE_BASE_URL}/dashboard/${selfcareId}`) + expect(mockWindowAssign).toBeCalledWith(`${SELFCARE_BASE_URL}/dashboard/${selfcareId}?lang=it`) }) it('Header handleSelectProcuct action should return the correct url if product.id is not "selfcare" and there is a jwt.selcareId', async () => { @@ -493,7 +493,7 @@ describe('Header', () => { await user.click(selectProductButton) expect(mockWindowAssign).toBeCalledWith( - `${SELFCARE_BASE_URL}/token-exchange?institutionId=${selfcareId}&productId=${productId}` + `${SELFCARE_BASE_URL}/token-exchange?institutionId=${selfcareId}&productId=${productId}&lang=it` ) }) }) diff --git a/src/components/sidebar/InteropSidebarItems.tsx b/src/components/sidebar/InteropSidebarItems.tsx index 873958680..a2d6384b4 100644 --- a/src/components/sidebar/InteropSidebarItems.tsx +++ b/src/components/sidebar/InteropSidebarItems.tsx @@ -19,6 +19,7 @@ import { NotificationQueries } from '@/api/notification' import { match } from 'ts-pattern' import { routes as routesDefinitions } from '@/router/routes' import { get } from 'lodash' +import useCurrentLanguage from '@/hooks/useCurrentLanguage' type InteropSidebarItems = { routes: SidebarRoutes @@ -28,13 +29,18 @@ export const InteropSidebarItems: React.FC = ({ routes }) = const generatePath = useGeneratePath() const isRouteInCurrentSubtree = useIsRouteInCurrentSubtree() const { t } = useTranslation('sidebar') + const lang = useCurrentLanguage() const pathname = useCurrentRoute().routeKey const { jwt, isAdmin, isSupport } = AuthHooks.useJwt() const selfcareUsersPageUrl = - jwt && `${SELFCARE_BASE_URL}/dashboard/${jwt.selfcareId}/users#${getCurrentSelfCareProductId()}` - const selfcareGroupsPageUrl = jwt && `${SELFCARE_BASE_URL}/dashboard/${jwt.selfcareId}/groups` + jwt && + `${SELFCARE_BASE_URL}/dashboard/${ + jwt.selfcareId + }/users#${getCurrentSelfCareProductId()}?lang=${lang}` + const selfcareGroupsPageUrl = + jwt && `${SELFCARE_BASE_URL}/dashboard/${jwt.selfcareId}/groups?lang=${lang}` const [parentExpandedItem, setParentExpandedItem] = useState( routes.find( diff --git a/src/router/components/RoutesWrapper/RoutesWrapper.tsx b/src/router/components/RoutesWrapper/RoutesWrapper.tsx index 90c9e0e22..9d8fe0183 100644 --- a/src/router/components/RoutesWrapper/RoutesWrapper.tsx +++ b/src/router/components/RoutesWrapper/RoutesWrapper.tsx @@ -1,8 +1,8 @@ -import React from 'react' +import React, { useEffect } from 'react' import { Footer, Header } from '@/components/layout' import { AppLayout } from '@/components/layout/AppLayout' import { PageContainerSkeleton } from '@/components/layout/containers' -import { Outlet } from 'react-router-dom' +import { Outlet, useSearchParams } from 'react-router-dom' import useScrollTopOnLocationChange from '../../hooks/useScrollTopOnLocationChange' import { Box } from '@mui/material' import { AuthGuard } from './AuthGuard' @@ -11,15 +11,28 @@ import TOSAgreement from './TOSAgreement' import { useTOSAgreement } from '../../hooks/useTOSAgreement' import { ErrorPage } from '@/pages' import { Dialog } from '@/components/dialogs' -import { routes, useCurrentRoute } from '@/router' +import { routes, useCurrentRoute, useSwitchPathLang } from '@/router' import { AuthHooks } from '@/api/auth' import { Stack } from '@mui/system' +import { AllowedLanguage } from '@/router/routes' function EmptyWrapper({ children }: { children: React.ReactNode }) { return <>{children} } const _RoutesWrapper: React.FC = () => { + const switchLang = useSwitchPathLang() + const [searchParams] = useSearchParams() + + useEffect(() => { + const language = AllowedLanguage.safeParse(searchParams.get('lang')) + if (language.success) { + switchLang(language.data) + } else { + console.warn('Language URL params is not valid') + } + }, [searchParams, switchLang]) + const { isPublic, routeKey } = useCurrentRoute() const { jwt, diff --git a/src/router/routes.tsx b/src/router/routes.tsx index b6eb9aaa8..4e19b0c2f 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -67,13 +67,17 @@ import { ConsumerPurposeTemplateDetailsPage } from '@/pages/ConsumerPurposeTempl import ConsumerPurposeTemplateCatalogDetailsPage from '@/pages/ConsumerPurposeTemplateCatalogDetailsPage/ConsumerPurposeTemplateCatalogDetailsPage' import { ConsumerPurposeTemplateSummaryPage } from '@/pages/ConsumerPurposeTemplateSummaryPage' import { ConsumerPurposeTemplateEditPage } from '@/pages/ConsumerPurposeTemplateEditPage' +import z from 'zod' + +const languages = ['it', 'en'] as const +export const AllowedLanguage = z.enum(languages) export const { routes, reactRouterDOMRoutes, hooks, components, utils } = new InteropRouterBuilder< LangCode, UserProductRole, { hideSideNav: boolean } >({ - languages: ['it', 'en'], + languages, }) .addRoute({ key: 'LOGOUT',