diff --git a/e2e/tests/resource-page.spec.ts b/e2e/tests/resource-page.spec.ts index 5ddc01b82..39342d016 100644 --- a/e2e/tests/resource-page.spec.ts +++ b/e2e/tests/resource-page.spec.ts @@ -1,8 +1,9 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { Page, expect, test } from '@playwright/test'; -import { getResource, getResourceUrl } from '../utils'; -import { Resource } from '../../src/common/lib/types'; +import { getResourceUrl, getResource } from '../utils'; + import { testData } from '../constants'; +import { Resource } from '../../src/common/lib/types'; test.describe('Resource page', async () => { let page: Page; @@ -10,8 +11,8 @@ test.describe('Resource page', async () => { let resource: Resource; test.beforeAll(async ({ browser }) => { - resourceUrl = await getResourceUrl(); resource = await getResource(); + resourceUrl = await getResourceUrl(); page = await browser.newPage(); await page.goto(resourceUrl); @@ -40,6 +41,7 @@ test.describe('Resource page', async () => { test('Location name', async () => { const resourceTitle = resource.name.fi; + await expect(page.getByText(resourceTitle)).toBeVisible(); }); @@ -174,10 +176,7 @@ test.describe('Resource page', async () => { }); test('Dates - day selection', async () => { - const createNewPeriodUrl = `/resource/${resource.id}/period/new`; - await page.getByRole('button', { name: 'Lisää aukioloaika +' }).click(); - await page.goto(createNewPeriodUrl); await page .getByLabel('Aukiolomääritys 1') diff --git a/e2e/utils.ts b/e2e/utils.ts index de67b02d0..9ceab3b26 100644 --- a/e2e/utils.ts +++ b/e2e/utils.ts @@ -1,8 +1,11 @@ import { promisify } from 'util'; import { exec } from 'child_process'; +import dns from 'node:dns'; import { DatePeriod, Resource } from '../src/common/lib/types'; import { apiUrl, testData } from './constants'; +dns.setDefaultResultOrder('ipv4first'); + const getAuthParams = async () => { const execAsync = promisify(exec); diff --git a/package.json b/package.json index 3ec75404a..f357454ee 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@testing-library/user-event": "^12.2.2", "@types/jest": "^26.0.24", "@types/lodash": "4.14.192", - "@types/node": "^12.20.37", + "@types/node": "^22.10.2", "@types/react": "^17.0.1", "@types/react-dom": "^17.0.1", "@types/react-router-dom": "^5.3.2", @@ -43,7 +43,7 @@ "react-dom": "^17.0.1", "react-hook-form": "7.34.2", "react-i18next": "^13.5.0", - "react-router-dom": "^5.2.0", + "react-router-dom": "^6.28.0", "react-test-renderer": "^16.13.1", "sass": "^1.77.2", "typescript": "4.9.5", diff --git a/playwright.config.ts b/playwright.config.ts index 8fbe1e64c..8f35d33e7 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line import/no-extraneous-dependencies -import { defineConfig } from '@playwright/test'; +import { defineConfig, devices } from '@playwright/test'; import { e2eTestUrl } from './e2e/constants'; export default defineConfig({ @@ -11,6 +11,9 @@ export default defineConfig({ /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, + /* The maximum number of retry attempts given to failed tests */ + retries: process.env.CI ? 1 : undefined, + // Timeout for each test in milliseconds timeout: 60 * 1000, @@ -49,8 +52,12 @@ export default defineConfig({ projects: [ { - name: 'Tests', - testMatch: [/tests/], + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, }, ], globalTeardown: require.resolve('./e2e/tests/global.teardown.ts'), diff --git a/src/App.tsx b/src/App.tsx index e40b25520..b1c6c718e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,6 @@ /* eslint-disable no-underscore-dangle */ import React, { useCallback, useMemo, useState } from 'react'; -import { - BrowserRouter as Router, - Route, - Switch, - RouteComponentProps, -} from 'react-router-dom'; +import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; import 'hds-core'; import { AppContext } from './App-context'; import urlUtils, { SearchParameters } from './common/utils/url/url'; @@ -22,7 +17,7 @@ import NavigationAndFooterWrapper from './components/navigation-and-footer-wrapp import HaukiHeader from './components/header/HaukiHeader'; import './App.scss'; import { Language } from './common/lib/types'; -import PrivateResourceRoute from './components/router/PrivateResourceRoute'; +import PermissionResolver from './components/router/PermissionResolver'; import ResourcePage from './pages/ResourcePage'; import ResourceBatchUpdatePage from './pages/ResourceBatchUpdatePage'; import AddNormalOpeningHoursPage from './pages/AddNormalOpeningHoursPage'; @@ -121,240 +116,234 @@ const App = (): JSX.Element => { - - - -
-

Etusivu

-
-
-
- - -
-

Kohdetta ei löydy

-

- Kohdetta ei löytynyt. Yritä myöhemmin uudestaan. - Ongelman toistuessa ota yhteys sivuston ylläpitoon. - Teidät on automaattisesti kirjattu ulos. -

-
-
-
- - -
-

Puutteelliset tunnukset

-
-
-
- - -
-

Puuttuvat tunnukset

-
-
-
- - -
- -
-
-
- + +
+

Etusivu

+
+ + } + /> + +
+

Kohdetta ei löydy

+

+ Kohdetta ei löytynyt. Yritä myöhemmin uudestaan. + Ongelman toistuessa ota yhteys sivuston ylläpitoon. + Teidät on automaattisesti kirjattu ulos. +

+
+ + } + /> + +
+

Puutteelliset tunnukset

+
+ + } + /> + +
+

Puuttuvat tunnukset

+
+ + } + /> + +
+ +
+ + } + /> + ) => { - const { id, childId } = match.params; - - return ( - id && ( - -
- - - -
-
- ) - ); - }} + path="/resource/:id" + element={ + + +
+ + + +
+
+
+ } + /> + + +
+ + + +
+
+ + } /> - ) => { - const { id } = match.params; - - return ( - id && ( - -
- - - -
-
- ) - ); - }} + path="/resource/:id/batch" + element={ + + +
+ + + +
+
+
+ } + /> + + +
+ +
+ + } /> - ) => { - const { id } = match.params; - - return ( - id && ( - <> - -
- -
- - ) - ); - }} + path="/resource/:id/period/new" + element={ + + +
+ +
+
+ } + /> + + +
+ +
+ + } /> - ) => { - const { id, datePeriodId } = match.params; - - return ( - id && - datePeriodId && ( - <> - -
- -
- - ) - ); - }} + path="/resource/:id/period/:datePeriodId" + element={ + + +
+ +
+
+ } + /> + + +
+ +
+ + } /> - ) => { - const { id } = match.params; - - return ( - id && ( - <> - -
- -
- - ) - ); - }} + path="/resource/:id/holidays" + element={ + + +
+ +
+
+ } /> - ) => { - const { id } = match.params; - - return ( - id && ( - <> - -
- -
- - ) - ); - }} + + +
+ +
+ + } /> - ) => { - const { id, datePeriodId } = match.params; - - return ( - id && - datePeriodId && ( - <> - -
- -
- - ) - ); - }} + path="/resource/:id/exception/new/" + element={ + + +
+ +
+
+ } + /> + + +
+ +
+ + } + /> + + +
+ +
+ + } /> -
+
diff --git a/src/common/utils/url/url.ts b/src/common/utils/url/url.ts index f971d0ea7..5e56eae18 100644 --- a/src/common/utils/url/url.ts +++ b/src/common/utils/url/url.ts @@ -3,7 +3,11 @@ import querystring from 'querystring'; export type SearchParameters = { [key: string]: string | string[] }; const parseSearchParameters = (queryStr: string): SearchParameters => { - return querystring.parse(queryStr.replace('?', '')); + const parsed = querystring.parse(queryStr.replace('?', '')); + + return Object.fromEntries( + Object.entries(parsed).filter(([, value]) => value !== undefined) + ) as SearchParameters; }; export default { parseSearchParameters }; diff --git a/src/components/header/HaukiHeader.test.tsx b/src/components/header/HaukiHeader.test.tsx index a2068b644..521500be5 100644 --- a/src/components/header/HaukiHeader.test.tsx +++ b/src/components/header/HaukiHeader.test.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { render, waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { BrowserRouter as Router } from 'react-router-dom'; -import { Mock } from 'vitest'; import api from '../../common/utils/api/api'; import { AppContext } from '../../App-context'; import { AuthContext } from '../../auth/auth-context'; @@ -33,9 +32,7 @@ vi.mock('react-router-dom', async (importOriginal) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment /* @ts-ignore */ ...mod, - useHistory: (): { push: Mock } => ({ - push: vi.fn(), - }), + useNavigate: vi.fn(), }; }); diff --git a/src/components/header/HaukiHeader.tsx b/src/components/header/HaukiHeader.tsx index def00ea4e..47b380be0 100644 --- a/src/components/header/HaukiHeader.tsx +++ b/src/components/header/HaukiHeader.tsx @@ -1,6 +1,6 @@ /* eslint-disable jsx-a11y/anchor-is-valid */ import React from 'react'; -import { useHistory, useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { IconCrossCircleFill, IconUser, @@ -25,7 +25,7 @@ const HaukiHeader = (): JSX.Element => { const authProps: Partial = useAuth(); const { i18n, t } = useTranslation(); const { authTokens, clearAuth } = authProps; - const history = useHistory(); + const navigate = useNavigate(); const isAuthenticated = !!authTokens; const { id } = useParams<{ id: string }>(); @@ -41,7 +41,7 @@ const HaukiHeader = (): JSX.Element => { if (clearAuth) { clearAuth(); } - history.push('/'); + navigate('/'); } else { showSignOutErrorNotification(t('Header.SignOutRejected')); } @@ -84,7 +84,7 @@ const HaukiHeader = (): JSX.Element => { const urlSearchParams = new URLSearchParams(window.location.search); urlSearchParams.set('lang', newLanguage); - history.push({ + navigate({ pathname: window.location.pathname, search: urlSearchParams.toString(), }); diff --git a/src/components/opening-periods-section/OpeningPeriodsSection.tsx b/src/components/opening-periods-section/OpeningPeriodsSection.tsx index af9266ab0..f96a1df9f 100644 --- a/src/components/opening-periods-section/OpeningPeriodsSection.tsx +++ b/src/components/opening-periods-section/OpeningPeriodsSection.tsx @@ -1,7 +1,7 @@ import { Checkbox, LoadingSpinner } from 'hds-react'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { DatePeriod } from '../../common/lib/types'; import { PrimaryButton, SecondaryButton } from '../button/Button'; import './OpeningPeriodsSection.scss'; @@ -35,7 +35,7 @@ const OpeningPeriodsSection = ({ title, theme, }: Props): JSX.Element => { - const history = useHistory(); + const navigate = useNavigate(); const { selectedDatePeriods, removeDatePeriods, @@ -117,7 +117,7 @@ const OpeningPeriodsSection = ({ dataTest={addNewOpeningPeriodButtonDataTest} light onClick={(): void => { - history.push(newUrl); + navigate(newUrl); }} size="small"> {addDatePeriodButtonText} diff --git a/src/components/router/PrivateResourceRoute.test.tsx b/src/components/router/PermissionResolver.test.tsx similarity index 68% rename from src/components/router/PrivateResourceRoute.test.tsx rename to src/components/router/PermissionResolver.test.tsx index 2fad5be43..ee3c4bbdb 100644 --- a/src/components/router/PrivateResourceRoute.test.tsx +++ b/src/components/router/PermissionResolver.test.tsx @@ -1,13 +1,9 @@ import React from 'react'; -import { - BrowserRouter as Router, - Route, - RouteComponentProps, -} from 'react-router-dom'; +import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; import { render, screen, waitFor } from '@testing-library/react'; import api from '../../common/utils/api/api'; import * as AuthContext from '../../auth/auth-context'; -import PrivateResourceRoute from './PrivateResourceRoute'; +import PermissionResolver from './PermissionResolver'; import { AuthTokens } from '../../auth/auth-context'; const testTokens: AuthTokens = { @@ -21,7 +17,18 @@ const testTokens: AuthTokens = { hsa_has_organization_rights: 'true', }; -const renderRoutesWithPrivateRoute = () => { +vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + + return { + ...mod, + useParams: () => ({ + id: 'tprek:8100', + }), + }; +}); + +const renderRoutesWithPermissionResolver = () => { window.history.pushState( {}, 'Test page', @@ -30,23 +37,24 @@ const renderRoutesWithPrivateRoute = () => { return render( - -

Test not found

-
- -

Test unauthenticated

-
- -

Test unauthorized

-
- ) => ( -

{match.params.id}

- )} - /> + + Test not found} /> + Test unauthenticated} + /> + + Test unauthorized} /> + +

{testTokens.hsa_resource}

+ + } + /> +
); }; @@ -71,7 +79,7 @@ const mockPermissionsApi = (hasPermission: boolean): void => { ); }; -describe(``, () => { +describe(``, () => { afterEach(() => { vi.clearAllMocks(); }); @@ -80,7 +88,7 @@ describe(``, () => { mockContext(); mockPermissionsApi(true); - const { getByText } = renderRoutesWithPrivateRoute(); + const { getByText } = renderRoutesWithPermissionResolver(); expect(getByText('Sivua alustetaan..')).toBeInTheDocument(); }); @@ -89,7 +97,7 @@ describe(``, () => { mockContext(); mockPermissionsApi(true); - renderRoutesWithPrivateRoute(); + renderRoutesWithPermissionResolver(); await waitFor(async () => expect( @@ -102,7 +110,7 @@ describe(``, () => { mockContext(); mockPermissionsApi(false); - renderRoutesWithPrivateRoute(); + renderRoutesWithPermissionResolver(); await waitFor(async () => expect(await screen.findByText('Test unauthorized')).toBeInTheDocument() @@ -113,7 +121,7 @@ describe(``, () => { mockContext({ tokens: undefined }); mockPermissionsApi(false); - renderRoutesWithPrivateRoute(); + renderRoutesWithPermissionResolver(); await waitFor(async () => expect( @@ -135,7 +143,7 @@ describe(``, () => { Promise.reject(resourceNotFoundError) ); - renderRoutesWithPrivateRoute(); + renderRoutesWithPermissionResolver(); await waitFor(async () => expect(await screen.findByText('Test not found')).toBeInTheDocument() diff --git a/src/components/router/PermissionResolver.tsx b/src/components/router/PermissionResolver.tsx new file mode 100644 index 000000000..5afe012ae --- /dev/null +++ b/src/components/router/PermissionResolver.tsx @@ -0,0 +1,71 @@ +import React, { ReactNode, useEffect, useState } from 'react'; +import { + Navigate, + useLocation, + useNavigate, + useParams, +} from 'react-router-dom'; +import { AuthContextProps, useAuth } from '../../auth/auth-context'; +import api from '../../common/utils/api/api'; +import Main from '../main/Main'; + +const PermissionResolver = ({ + children, +}: { + children?: ReactNode; +}): JSX.Element => { + const { authTokens, clearAuth }: Partial = useAuth(); + const { pathname, search } = useLocation(); + const navigate = useNavigate(); + const { id } = useParams<{ + id?: string; + childId?: string; + datePeriodId?: string; + }>(); + + const isAuthenticated = !!authTokens; + const [isLoading, setIsLoading] = useState(true); + const [isAuthorized, setIsAuthorized] = useState(false); + + useEffect(() => { + if (!id || !isAuthenticated) { + setIsLoading(false); + } else { + api + .testResourcePostPermission(id) + .then((hasPermission: boolean) => { + setIsAuthorized(hasPermission); + setIsLoading(false); + }) + .catch((e) => { + if (clearAuth) { + clearAuth(); + } + if (e.response?.status === 404) { + return navigate(`/not_found?original_request=${pathname}${search}`); + } + return setIsLoading(false); + }); + } + }, [clearAuth, navigate, id, isAuthenticated, pathname, search]); + + if (isLoading) { + return ( +
+

Sivua alustetaan..

+
+ ); + } + + if (!isAuthenticated) { + return ; + } + + if (!isAuthorized) { + return ; + } + + return <>{children}; +}; + +export default PermissionResolver; diff --git a/src/components/router/PrivateResourceRoute.tsx b/src/components/router/PrivateResourceRoute.tsx deleted file mode 100644 index 6d37b1893..000000000 --- a/src/components/router/PrivateResourceRoute.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { ReactNode, useEffect, useState } from 'react'; -import { - Route, - Redirect, - RouteProps, - RouteComponentProps, - useLocation, - useHistory, -} from 'react-router-dom'; -import { AuthContextProps, useAuth } from '../../auth/auth-context'; -import api from '../../common/utils/api/api'; -import Main from '../main/Main'; - -const PermissionResolver = ({ - id, - children, -}: { - id?: string; - children?: ReactNode; -}): JSX.Element => { - const { authTokens, clearAuth }: Partial = useAuth(); - const { pathname, search } = useLocation(); - const history = useHistory(); - const isAuthenticated = !!authTokens; - const [isLoading, setLoading] = useState(true); - const [isAuthorized, setIsAuthorized] = useState(false); - - useEffect(() => { - if (!id || !isAuthenticated) { - setLoading(false); - } else { - api - .testResourcePostPermission(id) - .then((hasPermission: boolean) => { - setIsAuthorized(hasPermission); - setLoading(false); - }) - .catch((e) => { - if (clearAuth) { - clearAuth(); - } - if (e.response?.status === 404) { - return history.push( - `/not_found?original_request=${pathname}${search}` - ); - } - return setLoading(false); - }); - } - }, [clearAuth, history, id, isAuthenticated, pathname, search]); - - if (isLoading) { - return ( -
-

Sivua alustetaan..

-
- ); - } - - if (!isAuthenticated) { - return ; - } - - if (!isAuthorized) { - return ; - } - - return <>{children}; -}; - -type ComputedMatch = { - params: { - id?: string; - }; -}; - -interface PrivateRouteProps extends RouteProps { - computedMatch?: ComputedMatch; - id: string; -} - -const PrivateResourceRoute = (routeProps: PrivateRouteProps): JSX.Element => { - const { render, ...rest } = routeProps; - - return ( - ) => { - if (!render) { - // eslint-disable-next-line no-console - console.error( - 'Missing required render prop from PrivateResourceRoute-attributes' - ); - return ; - } - - return ( - - {render(props)} - - ); - }} - /> - ); -}; - -export default PrivateResourceRoute; diff --git a/src/hooks/useGoToResourceBatchUpdatePage.test.tsx b/src/hooks/useGoToResourceBatchUpdatePage.test.tsx new file mode 100644 index 000000000..88c9a1a9f --- /dev/null +++ b/src/hooks/useGoToResourceBatchUpdatePage.test.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import * as routerMock from 'react-router-dom'; +import useGoToResourceBatchUpdatePage from './useGoToResourceBatchUpdatePage'; + +const mockNavigate = vi.fn(); + +vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + + return { + ...mod, + useParams: vi.fn(), + useNavigate: () => mockNavigate, + }; +}); + +describe('useGoToResourceBatchUpdatePage', () => { + it('should navigate to the correct batch update page when parentId and resourceId are provided', () => { + vi.spyOn(routerMock, 'useParams').mockReturnValueOnce({ + parentId: 'parent1', + id: 'resource1', + }); + + const { result } = renderHook(() => useGoToResourceBatchUpdatePage(), { + wrapper: ({ children }) => ( + + + + + + ), + }); + + result.current(); + + expect(mockNavigate).toHaveBeenCalledWith( + '/resource/parent1/child/resource1/batch' + ); + }); + + it('should navigate to the correct batch update page when only resourceId is provided', () => { + vi.spyOn(routerMock, 'useParams').mockReturnValueOnce({ id: 'resource1' }); + + const { result } = renderHook(() => useGoToResourceBatchUpdatePage(), { + wrapper: ({ children }) => ( + + + + + + ), + }); + + result.current(); + + expect(mockNavigate).toHaveBeenCalledWith('/resource/resource1/batch'); + }); + + it('should throw an error when neither parentId nor resourceId is provided', () => { + vi.spyOn(routerMock, 'useParams').mockReturnValueOnce({}); + + const { result } = renderHook(() => useGoToResourceBatchUpdatePage(), { + wrapper: ({ children }) => ( + + + + + + ), + }); + + expect(result.current).toThrowError( + 'Invalid route. No resource id found from the path.' + ); + }); +}); diff --git a/src/hooks/useGoToResourceBatchUpdatePage.ts b/src/hooks/useGoToResourceBatchUpdatePage.ts index 3b25a4808..9bffa20ad 100644 --- a/src/hooks/useGoToResourceBatchUpdatePage.ts +++ b/src/hooks/useGoToResourceBatchUpdatePage.ts @@ -1,11 +1,9 @@ import { useCallback } from 'react'; -import { useHistory, useRouteMatch } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; const useGotToResourceBatchUpdataPage = (): (() => void) => { - const history = useHistory(); - const { - params: { parentId, id: resourceId }, - } = useRouteMatch<{ + const navigate = useNavigate(); + const { parentId, id: resourceId } = useParams<{ parentId?: string; id?: string; }>(); @@ -19,8 +17,8 @@ const useGotToResourceBatchUpdataPage = (): (() => void) => { ? `${parentId}/child/${resourceId}` : resourceId; - history.push(`/resource/${resourcePath}/batch`); - }, [history, parentId, resourceId]); + navigate(`/resource/${resourcePath}/batch`); + }, [navigate, parentId, resourceId]); }; export default useGotToResourceBatchUpdataPage; diff --git a/src/hooks/useReturnToResourcePage.ts b/src/hooks/useReturnToResourcePage.ts index 8403219d2..b1d6caaae 100644 --- a/src/hooks/useReturnToResourcePage.ts +++ b/src/hooks/useReturnToResourcePage.ts @@ -1,11 +1,9 @@ import { useCallback } from 'react'; -import { useHistory, useRouteMatch } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; const useReturnToResourcePage = (): (() => void) => { - const history = useHistory(); - const { - params: { parentId, id: resourceId }, - } = useRouteMatch<{ + const navigate = useNavigate(); + const { parentId, id: resourceId } = useParams<{ parentId?: string; id?: string; }>(); @@ -19,8 +17,8 @@ const useReturnToResourcePage = (): (() => void) => { ? `${parentId}/child/${resourceId}` : resourceId; - history.push(`/resource/${resourcePath}`); - }, [history, parentId, resourceId]); + navigate(`/resource/${resourcePath}`); + }, [navigate, parentId, resourceId]); }; export default useReturnToResourcePage; diff --git a/src/pages/AddExceptionOpeningHoursPage.tsx b/src/pages/AddExceptionOpeningHoursPage.tsx index 102f0dd82..e5dd9e736 100644 --- a/src/pages/AddExceptionOpeningHoursPage.tsx +++ b/src/pages/AddExceptionOpeningHoursPage.tsx @@ -1,15 +1,16 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; import api from '../common/utils/api/api'; import ExceptionOpeningHoursForm from '../components/exception-opening-hours-form/ExceptionOpeningHoursForm'; import useDatePeriodConfig from '../services/useDatePeriodConfig'; import useResource from '../services/useResource'; -const AddExceptionOpeningHoursPage = ({ - resourceId, -}: { - resourceId: string; -}): JSX.Element => { +const AddExceptionOpeningHoursPage = (): JSX.Element => { + const { id: resourceId } = useParams<{ + id?: string; + }>(); + const { t } = useTranslation(); const resource = useResource(resourceId); const datePeriodConfig = useDatePeriodConfig(); diff --git a/src/pages/AddNormalOpeningHoursPage.tsx b/src/pages/AddNormalOpeningHoursPage.tsx index e4a538c04..289d30dbd 100644 --- a/src/pages/AddNormalOpeningHoursPage.tsx +++ b/src/pages/AddNormalOpeningHoursPage.tsx @@ -1,17 +1,19 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; import { ApiDatePeriod } from '../common/lib/types'; import api from '../common/utils/api/api'; import NormalOpeningHoursForm from '../components/normal-opening-hours-form/NormalOpeningHoursForm'; import useDatePeriodConfig from '../services/useDatePeriodConfig'; import useResource from '../services/useResource'; -const AddNormalOpeningHoursPage = ({ - resourceId, -}: { - resourceId: string; -}): JSX.Element => { +const AddNormalOpeningHoursPage = (): JSX.Element => { const { t } = useTranslation(); + + const { id: resourceId } = useParams<{ + id: string; + }>(); + const resource = useResource(resourceId); const datePeriodConfig = useDatePeriodConfig(); diff --git a/src/pages/EditExceptionOpeningHoursPage.tsx b/src/pages/EditExceptionOpeningHoursPage.tsx index 930e3b318..be2547c48 100644 --- a/src/pages/EditExceptionOpeningHoursPage.tsx +++ b/src/pages/EditExceptionOpeningHoursPage.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; import api from '../common/utils/api/api'; import { ApiDatePeriod } from '../common/lib/types'; import ExceptionOpeningHoursForm from '../components/exception-opening-hours-form/ExceptionOpeningHoursForm'; @@ -7,13 +8,12 @@ import useResource from '../services/useResource'; import useDatePeriodConfig from '../services/useDatePeriodConfig'; import useDatePeriod from '../services/useDatePeriod'; -const EditExceptionOpeningHoursPage = ({ - resourceId, - datePeriodId, -}: { - resourceId: string; - datePeriodId: string; -}): JSX.Element => { +const EditExceptionOpeningHoursPage = (): JSX.Element => { + const { id: resourceId, datePeriodId } = useParams<{ + id?: string; + datePeriodId?: string; + }>(); + const { t } = useTranslation(); const resource = useResource(resourceId); const datePeriodConfig = useDatePeriodConfig(); diff --git a/src/pages/EditHolidaysPage.tsx b/src/pages/EditHolidaysPage.tsx index 8f451dca6..f4f17451f 100644 --- a/src/pages/EditHolidaysPage.tsx +++ b/src/pages/EditHolidaysPage.tsx @@ -2,6 +2,7 @@ import React, { CSSProperties, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Checkbox, IconPenLine, LoadingSpinner } from 'hds-react'; import { FormProvider, useForm } from 'react-hook-form'; +import { useParams } from 'react-router-dom'; import { ApiDatePeriod, Language, @@ -299,11 +300,7 @@ const HolidayListItem = ({ ); }; -const EditHolidaysPage = ({ - resourceId, -}: { - resourceId: string; -}): JSX.Element => { +const EditHolidaysPage = (): JSX.Element => { const { t } = useTranslation(); const [resource, setResource] = useState(); const [holidayValues, setHolidayValues] = useState< @@ -311,19 +308,26 @@ const EditHolidaysPage = ({ >(); const [datePeriodConfig, setDatePeriodConfig] = useState(); + + const { id: resourceId } = useParams<{ + id?: string; + }>(); + const { language = Language.FI } = useAppContext(); const [holidays, setHolidays] = useState([]); useEffect((): void => { const fetchData = async (): Promise => { try { - const [apiResource, uiDatePeriodOptions] = await Promise.all([ - api.getResource(resourceId), - getDatePeriodFormConfig(), - ]); - setHolidays(getHolidays()); - setResource(apiResource); - setDatePeriodConfig(uiDatePeriodOptions); + if (resourceId) { + const [apiResource, uiDatePeriodOptions] = await Promise.all([ + api.getResource(resourceId), + getDatePeriodFormConfig(), + ]); + setHolidays(getHolidays()); + setResource(apiResource); + setDatePeriodConfig(uiDatePeriodOptions); + } } catch (e) { // eslint-disable-next-line no-console console.error('Fetching data failed in holidays page:', e); diff --git a/src/pages/EditNormalOpeningHoursPage.tsx b/src/pages/EditNormalOpeningHoursPage.tsx index 8de40b4f9..b62e5ae8f 100644 --- a/src/pages/EditNormalOpeningHoursPage.tsx +++ b/src/pages/EditNormalOpeningHoursPage.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; import api from '../common/utils/api/api'; import { ApiDatePeriod } from '../common/lib/types'; import NormalOpeningHoursForm from '../components/normal-opening-hours-form/NormalOpeningHoursForm'; @@ -7,14 +8,14 @@ import useResource from '../services/useResource'; import useDatePeriod from '../services/useDatePeriod'; import useDatePeriodConfig from '../services/useDatePeriodConfig'; -const EditNormalOpeningHoursPage = ({ - resourceId, - datePeriodId, -}: { - resourceId: string; - datePeriodId: string; -}): JSX.Element => { +const EditNormalOpeningHoursPage = (): JSX.Element => { const { t } = useTranslation(); + + const { id: resourceId, datePeriodId } = useParams<{ + id?: string; + datePeriodId?: string; + }>(); + const resource = useResource(resourceId); const datePeriodConfig = useDatePeriodConfig(); const datePeriod = useDatePeriod(datePeriodId); diff --git a/src/pages/ResourceBatchUpdatePage.test.tsx b/src/pages/ResourceBatchUpdatePage.test.tsx index 7de3bdcfd..16e6257c3 100644 --- a/src/pages/ResourceBatchUpdatePage.test.tsx +++ b/src/pages/ResourceBatchUpdatePage.test.tsx @@ -18,7 +18,6 @@ import { TargetResourcesProps } from '../components/resource-opening-hours/Resou import { SelectedDatePeriodsProvider } from '../common/selectedDatePeriodsContext/SelectedDatePeriodsContext'; const testResourceBatchUpdatePageProps: ResourceBatchUpdatePageProps = { - mainResourceId: 'tprek:10', targetResourcesString: 'tprek:11,tprek:12,tprek:13,tprek:14', }; @@ -185,6 +184,17 @@ vi.mock('../services/useDatePeriodConfig', () => ({ })), })); +vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + + return { + ...mod, + useParams: () => ({ + id: 'tprek:10', + }), + }; +}); + const renderPage = () => { return render( @@ -263,9 +273,7 @@ describe(``, () => { renderPage(); await waitFor(async () => { - expect(apiGetResourceSpy).toHaveBeenCalledWith( - testResourceBatchUpdatePageProps.mainResourceId - ); + expect(apiGetResourceSpy).toHaveBeenCalledWith('tprek:10'); }); }); diff --git a/src/pages/ResourceBatchUpdatePage.tsx b/src/pages/ResourceBatchUpdatePage.tsx index a394576d0..b91fef98b 100644 --- a/src/pages/ResourceBatchUpdatePage.tsx +++ b/src/pages/ResourceBatchUpdatePage.tsx @@ -1,6 +1,6 @@ /* eslint-disable no-underscore-dangle */ import React, { useEffect, useState } from 'react'; -import { useHistory } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { Notification, Table, @@ -37,7 +37,6 @@ import OpeningPeriod from '../components/opening-period/OpeningPeriod'; import OpeningPeriodsSection from '../components/opening-periods-section/OpeningPeriodsSection'; export type ResourceBatchUpdatePageProps = { - mainResourceId: string; targetResourcesString?: string; }; @@ -57,7 +56,6 @@ export type ResourceWithOrigins = Resource & { }; const ResourceBatchUpdatePage = ({ - mainResourceId, targetResourcesString, }: ResourceBatchUpdatePageProps): JSX.Element => { const { @@ -67,7 +65,7 @@ const ResourceBatchUpdatePage = ({ } = useAppContext(); const authProps: Partial = useAuth(); const { authTokens, clearAuth } = authProps; - const history = useHistory(); + const navigate = useNavigate(); const isAuthenticated = !!authTokens; const { isModalOpen, openModal } = useModal(); const [resource, setResource] = useState(undefined); @@ -78,6 +76,11 @@ const ResourceBatchUpdatePage = ({ const [targetResourceData, setTargetResourceData] = useState< TargetResourcesProps | undefined >(undefined); + + const { id: mainResourceId } = useParams<{ + id?: string; + }>(); + const ReturnToResourcePage = useReturnToResourcePage(); const { selectedDatePeriods, @@ -120,7 +123,7 @@ const ResourceBatchUpdatePage = ({ if (clearAuth) { clearAuth(); } - history.push('/'); + navigate('/'); } else { showErrorNotification(t('ResourcePage.Notifications.SignOutFailed')); } @@ -334,22 +337,25 @@ const ResourceBatchUpdatePage = ({ let isMounted = true; setLoading(true); - api - .getResource(mainResourceId) - .then(async (r: Resource) => { - if (!isMounted) return; - setResource(r); - setLoading(false); - }) - .catch((e: Error) => { - setLoading(false); - setError(e); - showErrorNotification( - t('ResourcePage.Notifications.ErrorLoadingResource'), - t('ResourcePage.Notifications.Error2') + e - ); - }); + if (mainResourceId) { + api + .getResource(mainResourceId) + .then(async (r: Resource) => { + if (!isMounted) return; + + setResource(r); + setLoading(false); + }) + .catch((e: Error) => { + setLoading(false); + setError(e); + showErrorNotification( + t('ResourcePage.Notifications.ErrorLoadingResource'), + t('ResourcePage.Notifications.Error2') + e + ); + }); + } return () => { isMounted = false; diff --git a/src/pages/ResourcePage.test.tsx b/src/pages/ResourcePage.test.tsx index 5ad52ac44..70fe69a32 100644 --- a/src/pages/ResourcePage.test.tsx +++ b/src/pages/ResourcePage.test.tsx @@ -251,6 +251,17 @@ vi.mock('react-i18next', () => ({ }, })); +vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + + return { + ...mod, + useParams: () => ({ + id: 'tprek:8100', + }), + }; +}); + describe(``, () => { beforeEach(() => { vi.spyOn(api, 'getResource').mockImplementation(() => @@ -302,7 +313,7 @@ describe(``, () => { render( - + ); @@ -323,7 +334,7 @@ describe(``, () => { render( - + ); @@ -347,7 +358,7 @@ describe(``, () => { render( - + ); @@ -366,7 +377,7 @@ describe(``, () => { container = render( - + ).container; @@ -395,7 +406,7 @@ describe(``, () => { container = render( - + ).container; @@ -413,7 +424,7 @@ describe(``, () => { render( - + ); @@ -439,10 +450,7 @@ describe(``, () => { render( - + ); @@ -460,10 +468,7 @@ describe(``, () => { render( - + ); @@ -491,10 +496,7 @@ describe(``, () => { render( - + ); diff --git a/src/pages/ResourcePage.tsx b/src/pages/ResourcePage.tsx index fa7f3f348..d7dd7ce11 100644 --- a/src/pages/ResourcePage.tsx +++ b/src/pages/ResourcePage.tsx @@ -1,6 +1,7 @@ import React, { ReactNode, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Accordion, Notification, IconArrowRight, Card } from 'hds-react'; +import { useParams } from 'react-router-dom'; import { useAppContext } from '../App-context'; import api from '../common/utils/api/api'; import { Language, Resource } from '../common/lib/types'; @@ -48,12 +49,8 @@ const ResourceDetailsSection = ({ ); const ResourcePage = ({ - childId, - mainResourceId, targetResourcesString, }: { - childId?: string; - mainResourceId: string; targetResourcesString?: string; }): JSX.Element => { const { language: contextLanguage } = useAppContext(); @@ -68,6 +65,12 @@ const ResourcePage = ({ >(undefined); const [errorWhenNothingSelected, setErrorWhenNothingSelected] = useState(false); + + const { id: mainResourceId, childId } = useParams<{ + id: string; + childId?: string; + }>(); + const targetResourcesStorageKey = 'targetResources'; const goToResourceBatchUpdatePage = useGoToResourceBatchUpdatePage(); const { @@ -128,31 +131,33 @@ const ResourcePage = ({ // UseEffect's callbacks are synchronous to prevent a race condition. // We can not use an async function as an useEffect's callback because it would return Promise - api - .getResource(mainResourceId) - .then(async (r: Resource) => { - if (!isMounted) return; + if (mainResourceId) { + api + .getResource(mainResourceId) + .then(async (r: Resource) => { + if (!isMounted) return; - setResource(r); - const resourceHasChildren = r.children.length > 0; - const resourceHasParents = r.parents.length > 0; + setResource(r); + const resourceHasChildren = r.children.length > 0; + const resourceHasParents = r.parents.length > 0; - if (resourceHasChildren || resourceHasParents) { - // fetch children and parents - const [childR, parentR] = await Promise.all([ - resourceHasChildren ? api.getChildResourcesByParentId(r.id) : [], - resourceHasParents ? api.getParentResourcesByChildId(r.id) : [], - ]); + if (resourceHasChildren || resourceHasParents) { + // fetch children and parents + const [childR, parentR] = await Promise.all([ + resourceHasChildren ? api.getChildResourcesByParentId(r.id) : [], + resourceHasParents ? api.getParentResourcesByChildId(r.id) : [], + ]); - setChildResources(childR); - setParentResources(parentR); - } - setLoading(false); - }) - .catch((e: Error) => { - setError(e); - setLoading(false); - }); + setChildResources(childR); + setParentResources(parentR); + } + setLoading(false); + }) + .catch((e: Error) => { + setError(e); + setLoading(false); + }); + } return () => { isMounted = false; diff --git a/src/services/useDatePeriod.ts b/src/services/useDatePeriod.ts index c8e97ea22..7486c7bf2 100644 --- a/src/services/useDatePeriod.ts +++ b/src/services/useDatePeriod.ts @@ -2,13 +2,19 @@ import { useEffect, useState } from 'react'; import { ApiDatePeriod } from '../common/lib/types'; import api from '../common/utils/api/api'; -const useDatePeriod = (datePeriodId: string): ApiDatePeriod | undefined => { +const useDatePeriod = ( + datePeriodId: string | undefined +): ApiDatePeriod | undefined => { const [datePeriod, setDatePeriod] = useState(); useEffect((): void => { const fetchData = async (): Promise => { - const apiDatePeriod = await api.getDatePeriod(parseInt(datePeriodId, 10)); - setDatePeriod(apiDatePeriod); + if (datePeriodId) { + const apiDatePeriod = await api.getDatePeriod( + parseInt(datePeriodId, 10) + ); + setDatePeriod(apiDatePeriod); + } }; fetchData(); diff --git a/src/services/useResource.ts b/src/services/useResource.ts index 86d85098c..9927fb097 100644 --- a/src/services/useResource.ts +++ b/src/services/useResource.ts @@ -2,13 +2,15 @@ import { useEffect, useState } from 'react'; import { Resource } from '../common/lib/types'; import api from '../common/utils/api/api'; -const useResource = (resourceId: string): Resource | undefined => { +const useResource = (resourceId: string | undefined): Resource | undefined => { const [resource, setResource] = useState(); useEffect((): void => { const fetchData = async (): Promise => { - const apiResource = await api.getResource(resourceId); - setResource(apiResource); + if (resourceId) { + const apiResource = await api.getResource(resourceId); + setResource(apiResource); + } }; fetchData(); diff --git a/yarn.lock b/yarn.lock index 0b6af4963..34d04cf34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -257,7 +257,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.9.2": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e" integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw== @@ -990,6 +990,11 @@ resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.22.1.tgz#4e5de032fcb0b7bca50f6a9f8e133fd882821930" integrity sha512-PCpa+Vo6BKnRMuOEzy5zAZ3/H5tnQg1e80khMhK2xys0j6ZqzkgQC+fHMNZ7VDFNLqqNMj/o0eVeSBDh2POjkw== +"@remix-run/router@1.21.0": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.21.0.tgz#c65ae4262bdcfe415dbd4f64ec87676e4a56e2b5" + integrity sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA== + "@rollup/pluginutils@^4.2.1": version "4.2.1" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" @@ -1531,10 +1536,12 @@ dependencies: undici-types "~5.26.4" -"@types/node@^12.20.37": - version "12.20.55" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" - integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== +"@types/node@^22.10.2": + version "22.10.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9" + integrity sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ== + dependencies: + undici-types "~6.20.0" "@types/normalize-package-data@^2.4.0": version "2.4.4" @@ -3725,19 +3732,7 @@ hds-react@^3.8.0: uuid "^9.0.0" yup "^1.0.2" -history@^4.9.0: - version "4.10.1" - resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" - integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== - dependencies: - "@babel/runtime" "^7.1.2" - loose-envify "^1.2.0" - resolve-pathname "^3.0.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - value-equal "^1.0.1" - -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -4110,11 +4105,6 @@ is-weakset@^2.0.1: call-bind "^1.0.2" get-intrinsic "^1.1.1" -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== - isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" @@ -4448,7 +4438,7 @@ lodash@^4.17.15, lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -4949,13 +4939,6 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== - dependencies: - isarray "0.0.1" - path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -5184,7 +5167,7 @@ react-i18next@^13.5.0: "@babel/runtime" "^7.22.5" html-parse-stringify "^3.0.1" -react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.6: +react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.6: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -5217,33 +5200,20 @@ react-refresh@^0.14.2: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== -react-router-dom@^5.2.0: - version "5.3.4" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.4.tgz#2ed62ffd88cae6db134445f4a0c0ae8b91d2e5e6" - integrity sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ== +react-router-dom@^6.28.0: + version "6.28.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.28.0.tgz#f73ebb3490e59ac9f299377062ad1d10a9f579e6" + integrity sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg== dependencies: - "@babel/runtime" "^7.12.13" - history "^4.9.0" - loose-envify "^1.3.1" - prop-types "^15.6.2" - react-router "5.3.4" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - -react-router@5.3.4: - version "5.3.4" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.4.tgz#8ca252d70fcc37841e31473c7a151cf777887bb5" - integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== - dependencies: - "@babel/runtime" "^7.12.13" - history "^4.9.0" - hoist-non-react-statics "^3.1.0" - loose-envify "^1.3.1" - path-to-regexp "^1.7.0" - prop-types "^15.6.2" - react-is "^16.6.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" + "@remix-run/router" "1.21.0" + react-router "6.28.0" + +react-router@6.28.0: + version "6.28.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.28.0.tgz#29247c86d7ba901d7e5a13aa79a96723c3e59d0d" + integrity sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg== + dependencies: + "@remix-run/router" "1.21.0" react-test-renderer@^16.13.1: version "16.14.0" @@ -5390,11 +5360,6 @@ resolve-global@1.0.0, resolve-global@^1.0.0: dependencies: global-dirs "^0.1.1" -resolve-pathname@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" - integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== - resolve@^1.10.0, resolve@^1.12.0, resolve@^1.22.4, resolve@^1.22.8: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" @@ -5960,16 +5925,6 @@ tiny-case@^1.0.3: resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== -tiny-invariant@^1.0.2: - version "1.3.3" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" - integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== - -tiny-warning@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - tinybench@^2.5.1: version "2.8.0" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.8.0.tgz#30e19ae3a27508ee18273ffed9ac7018949acd7b" @@ -6171,6 +6126,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + update-browserslist-db@^1.0.13: version "1.0.13" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" @@ -6209,11 +6169,6 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -value-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" - integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== - vite-node@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.6.0.tgz#2c7e61129bfecc759478fa592754fd9704aaba7f"