MAJ React Router (#2170)

* Configuration react-router-dom-v5-compat

À supprimer une fois la migration vers React Router 6 terminée
confer https://github.com/remix-run/react-router/discussions/8753

* Migration d'une API dépréciée

* Refacto useSearchParams React Router v6

* Fix types
pull/2178/head
Maxime Quandalle 2022-06-22 12:27:23 +02:00 committed by GitHub
parent f57a0aae6d
commit 4f3004932a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 133 additions and 182 deletions

View File

@ -97,6 +97,7 @@
"react-redux": "^7.2.8",
"react-router": "^5.2.1",
"react-router-dom": "^5.3.0",
"react-router-dom-v5-compat": "^6.3.0",
"react-signature-pad-wrapper": "^1.2.11",
"react-spring": "^9.3.1",
"react-use-measure": "^2.0.4",
@ -121,6 +122,7 @@
"@storybook/builder-vite": "^0.1.23",
"@storybook/react": "^6.5.0-alpha.49",
"@storybook/testing-library": "^0.0.9",
"@types/history": "^5.0.0",
"@types/react": "^17.0.0",
"@types/react-color": "^3.0.1",
"@types/react-dom": "^17.0.9",

View File

@ -21,6 +21,7 @@ import { Helmet } from 'react-helmet-async'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { Redirect, Route, Switch } from 'react-router-dom'
import { CompatRoute } from 'react-router-dom-v5-compat'
import styled, { css } from 'styled-components'
import Accessibilité from './pages/Accessibilité'
import Budget from './pages/Budget/Budget'

View File

@ -1,5 +1,3 @@
import { OverlayProvider } from '@react-aria/overlays'
import { ErrorBoundary } from '@sentry/react'
import { ThemeColorsProvider } from '@/components/utils/colors'
import { DisableAnimationOnPrintProvider } from '@/components/utils/DisableAnimationContext'
import { IsEmbeddedProvider } from '@/components/utils/embeddedContext'
@ -13,18 +11,20 @@ import DesignSystemThemeProvider from '@/design-system/root'
import { H1 } from '@/design-system/typography/heading'
import { Link } from '@/design-system/typography/link'
import { Body, Intro } from '@/design-system/typography/paragraphs'
import { createBrowserHistory } from 'history'
import i18next from 'i18next'
import logo from '@/images/logo-monentreprise.svg'
import { useIframeResizer } from '@/hooks/useIframeResizer'
import { createContext, ReactNode, useMemo } from 'react'
import logo from '@/images/logo-monentreprise.svg'
import { OverlayProvider } from '@react-aria/overlays'
import { ErrorBoundary } from '@sentry/react'
import i18next from 'i18next'
import { createContext, ReactNode } from 'react'
import { HelmetProvider } from 'react-helmet-async'
import { I18nextProvider } from 'react-i18next'
import { Provider as ReduxProvider } from 'react-redux'
import { Router } from 'react-router-dom'
import { inIframe } from './utils'
import { store } from './store'
import { BrowserRouter } from 'react-router-dom'
import { CompatRouter } from 'react-router-dom-v5-compat'
import * as safeLocalStorage from './storage/safeLocalStorage'
import { store } from './store'
import { inIframe } from './utils'
// ATInternet Tracking
import { TrackingContext } from './ATInternetTracking'
@ -135,14 +135,6 @@ function BrowserRouterProvider({
if (import.meta.env.SSR) {
return <>{children}</>
}
// eslint-disable-next-line react-hooks/rules-of-hooks
const history = useMemo(
() =>
createBrowserHistory({
basename: import.meta.env.MODE === 'production' ? '' : basename,
}),
[basename]
)
const ATTracker = createTracker(
import.meta.env.VITE_AT_INTERNET_SITE_ID,
@ -159,9 +151,11 @@ function BrowserRouterProvider({
})
}
>
<Router history={history}>
<>{children}</>
</Router>
<BrowserRouter
basename={import.meta.env.MODE === 'production' ? '' : basename}
>
<CompatRouter>{children}</CompatRouter>
</BrowserRouter>
</TrackingContext.Provider>
</HelmetProvider>
)

View File

@ -2,7 +2,7 @@ import { useNextQuestionUrl } from '@/selectors/companyStatusSelectors'
import { LegalStatusRequirements } from '@/types/companyTypes'
import { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux'
import { useHistory } from 'react-router'
import { useNavigate } from 'react-router-dom-v5-compat'
import { Action } from './actions'
export type CompanyStatusAction = ReturnType<
@ -17,12 +17,12 @@ export type CompanyStatusAction = ReturnType<
// This feels hacky, we should express this "dispatch and navigate" in another way
export const useDispatchAndGoToNextQuestion = () => {
const dispatch = useDispatch()
const history = useHistory()
const navigate = useNavigate()
const nextQuestion = useNextQuestionUrl()
const [dispatched, setDispatched] = useState(false)
useEffect(() => {
if (dispatched) {
history.push(nextQuestion)
navigate(nextQuestion)
}
}, [dispatched])

View File

@ -1,7 +1,7 @@
import { ScrollToElement } from '@/components/utils/Scroll'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation } from 'react-router'
import { useLocation } from 'react-router-dom-v5-compat'
import styled from 'styled-components'
declare global {

View File

@ -3,7 +3,7 @@ import { SmallCard } from '@/design-system/card'
import { H2 } from '@/design-system/typography/heading'
import { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation } from 'react-router-dom'
import { useLocation } from 'react-router-dom-v5-compat'
import { icons } from './ui/SocialIcon'
import Emoji from './utils/Emoji'
import { SitePathsContext } from './utils/SitePathsContext'

View File

@ -1,16 +1,13 @@
import { useIsEmbedded } from '@/components/utils/embeddedContext'
import Meta from '@/components/utils/Meta'
import { SitePathsContext } from '@/components/utils/SitePathsContext'
import useSearchParamsSimulationSharing from '@/components/utils/useSearchParamsSimulationSharing'
import useSimulationConfig from '@/components/utils/useSimulationConfig'
import { Spacing } from '@/design-system/layout'
import { H1 } from '@/design-system/typography/heading'
import { Intro } from '@/design-system/typography/paragraphs'
import { situationSelector } from '@/selectors/simulationSelectors'
import { ComponentPropsWithoutRef, useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { ComponentPropsWithoutRef } from 'react'
import { useSelector } from 'react-redux'
import { useLocation } from 'react-router-dom'
import { TrackChapter } from '../ATInternetTracking'
import {
CurrentSimulatorDataProvider,

View File

@ -1,6 +1,6 @@
import { Helmet } from 'react-helmet-async'
import { useTranslation } from 'react-i18next'
import { useLocation } from 'react-router'
import { useLocation } from 'react-router-dom-v5-compat'
type PropType = {
page: string

View File

@ -5,7 +5,7 @@ import { Li, Ol, Ul } from '@/design-system/typography/list'
import { Body } from '@/design-system/typography/paragraphs'
import MarkdownToJsx, { MarkdownToJSX } from 'markdown-to-jsx'
import React, { useContext, useEffect } from 'react'
import { useLocation } from 'react-router-dom'
import { useLocation } from 'react-router-dom-v5-compat'
import { isIterable } from '../../utils'
import { SiteNameContext } from '../../Provider'
import Emoji from './Emoji'

View File

@ -1,66 +0,0 @@
// backported from react-router 6
// https://github.com/ReactTraining/react-router/blob/a97dbdb7297474ff0114411e363db2c8fb417e55/packages/react-router-dom/index.tsx#L383
import { useCallback, useMemo, useRef } from 'react'
import { useLocation, useHistory } from 'react-router-dom'
export type ParamKeyValuePair = [string, string]
export type URLSearchParamsInit =
| string
| ParamKeyValuePair[]
| Record<string, string | string[]>
| URLSearchParams
export function useSearchParams(defaultInit?: URLSearchParamsInit) {
const defaultSearchParamsRef = useRef(createSearchParams(defaultInit))
const location = useLocation()
const searchParams = useMemo(() => {
const searchParams = createSearchParams(location.search)
for (const key of defaultSearchParamsRef.current.keys()) {
if (!searchParams.has(key)) {
defaultSearchParamsRef.current.getAll(key).forEach((value) => {
searchParams.append(key, value)
})
}
}
return searchParams
}, [location.search])
const history = useHistory()
const setSearchParams = useCallback(
(
nextInit: URLSearchParamsInit,
navigateOptions?: { replace?: boolean }
) => {
if (navigateOptions?.replace) {
history.replace('?' + createSearchParams(nextInit).toString())
} else {
history.push('?' + createSearchParams(nextInit).toString())
}
},
[history]
)
return [searchParams, setSearchParams] as const
}
export function createSearchParams(
init: URLSearchParamsInit = ''
): URLSearchParams {
return new URLSearchParams(
typeof init === 'string' ||
Array.isArray(init) ||
init instanceof URLSearchParams
? init
: Object.keys(init).reduce((memo, key) => {
const value = init[key]
return memo.concat(
Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]]
)
}, [] as ParamKeyValuePair[])
)
}

View File

@ -1,8 +1,7 @@
import { useEffect, useMemo, useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { RootState, SimulationConfig, Situation } from '@/reducers/rootReducer'
import { useHistory } from 'react-router'
import { useSearchParams } from '@/components/utils/useSearchParams'
import { useLocation, useSearchParams } from 'react-router-dom-v5-compat'
import { useEngine } from '@/components/utils/EngineContext'
import { configSelector } from '@/selectors/simulationSelectors'
import Engine, { ParsedRules, serializeEvaluation } from 'publicodes'
@ -18,7 +17,7 @@ export default function useSearchParamsSimulationSharing() {
const [searchParams, setSearchParams] = useSearchParams()
const config = useSelector(configSelector)
const simulationUrl = useSelector((state: RootState) => state.simulation?.url)
const currentUrl = useHistory().location.pathname
const currentUrl = useLocation().pathname
const dispatch = useDispatch()
const engine = useEngine()

View File

@ -6,6 +6,7 @@ import { AppFr } from './entry-fr'
import { AppEn } from './entry-en'
import { ServerStyleSheet, StyleSheetManager } from 'styled-components'
import { FilledContext, HelmetProvider } from 'react-helmet-async'
import { CompatRouter } from 'react-router-dom-v5-compat'
export function render(url: string, lang: 'fr' | 'en') {
const sheet = new ServerStyleSheet()
@ -21,7 +22,9 @@ export function render(url: string, lang: 'fr' | 'en') {
<SSRProvider>
<StyleSheetManager sheet={sheet.instance}>
<StaticRouter location={url}>
<App />
<CompatRouter>
<App />
</CompatRouter>
</StaticRouter>
</StyleSheetManager>
</SSRProvider>

View File

@ -20,7 +20,7 @@ import { ComponentType, useContext, useMemo } from 'react'
import { Helmet } from 'react-helmet-async'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { Redirect, Route, useLocation } from 'react-router-dom'
import { Redirect, Route, useLocation, useParams } from 'react-router-dom'
import styled from 'styled-components'
import { TrackPage } from '../ATInternetTracking'
import RuleLink from '../components/RuleLink'
@ -33,7 +33,6 @@ export default function MonEntrepriseRulePage() {
() => getDocumentationSiteMap({ engine, documentationPath }),
[engine, documentationPath]
)
const { i18n } = useTranslation()
if (pathname === '/documentation') {
return <DocumentationLanding />
@ -57,34 +56,40 @@ export default function MonEntrepriseRulePage() {
<Grid item md={10}>
<BackToSimulation />
<Spacing xl />
<Route
path={documentationPath + '/:name+'}
render={({ match }) =>
match.params.name && (
<StyledDocumentation>
<RulePage
language={i18n.language as 'fr' | 'en'}
rulePath={match.params.name}
engine={engine}
documentationPath={documentationPath}
renderers={{
Head: Helmet,
Link: Link as ComponentType<{
to: string
}>,
Text: Markdown,
References,
}}
/>
</StyledDocumentation>
)
}
/>
<Route path={documentationPath + '/:name+'}>
<DocumentationPageBody />
</Route>
</Grid>
</FromBottom>
)
}
function DocumentationPageBody() {
const engine = useEngine()
const documentationPath = useContext(SitePathsContext).documentation.index
const { i18n } = useTranslation()
const params = useParams<{ name: string }>()
return (
<StyledDocumentation>
<RulePage
language={i18n.language as 'fr' | 'en'}
rulePath={params.name}
engine={engine}
documentationPath={documentationPath}
renderers={{
Head: Helmet,
Link: Link as ComponentType<{
to: string
}>,
Text: Markdown,
References,
}}
/>
</StyledDocumentation>
)
}
function BackToSimulation() {
const url = useSelector((state: RootState) => state.simulation?.url)
if (!url) {

View File

@ -19,7 +19,7 @@ import { Grid } from '@mui/material'
import { useCallback, useContext, useEffect } from 'react'
import { Trans } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { generatePath, useHistory } from 'react-router-dom'
import { generatePath, useNavigate } from 'react-router-dom-v5-compat'
export default function SearchOrCreate() {
const sitePaths = useContext(SitePathsContext)
@ -81,7 +81,7 @@ export default function SearchOrCreate() {
}
function useHandleCompanySubmit() {
const history = useHistory()
const navigate = useNavigate()
const sitePaths = useContext(SitePathsContext)
const setEntreprise = useSetEntreprise()
@ -93,9 +93,9 @@ function useHandleCompanySubmit() {
setEntreprise(établissement)
const entreprise = établissement.siren
const path = generatePath(sitePaths.gérer.entreprise, { entreprise })
history.push(path)
navigate(path)
},
[history, setEntreprise, sitePaths.gérer.entreprise]
[navigate, setEntreprise, sitePaths.gérer.entreprise]
)
return handleCompanySubmit

View File

@ -12,7 +12,7 @@ import { H1 } from '@/design-system/typography/heading'
import { GenericButtonOrLinkProps, Link } from '@/design-system/typography/link'
import { Body } from '@/design-system/typography/paragraphs'
import { useContext, useEffect, useMemo, useState } from 'react'
import { Redirect, useHistory, useRouteMatch } from 'react-router-dom'
import { Navigate, useMatch, useNavigate } from 'react-router-dom-v5-compat'
import styled from 'styled-components'
import { TrackPage } from '../../ATInternetTracking'
@ -37,10 +37,9 @@ export default function Nouveautés() {
console.error(err)
)
}, [])
const history = useHistory()
const navigate = useNavigate()
const sitePaths = useContext(SitePathsContext)
const slug = useRouteMatch<{ slug: string }>(`${sitePaths.nouveautés}/:slug`)
?.params?.slug
const slug = useMatch(`${sitePaths.nouveautés}/:slug`)?.params?.slug
useEffect(hideNewsBanner, [])
const releasesWithId = useMemo(
@ -58,7 +57,7 @@ export default function Nouveautés() {
`${sitePaths.nouveautés}/${slugify(data[index].name)}`
if (!slug || selectedRelease === -1) {
return <Redirect to={getPath(0)} />
return <Navigate to={getPath(0)} />
}
const releaseName = data[selectedRelease].name.toLowerCase()
@ -96,7 +95,7 @@ export default function Nouveautés() {
value={selectedRelease}
items={releasesWithId}
onSelectionChange={(id) => {
history.push(getPath(Number(id)))
navigate(getPath(Number(id)))
}}
>
{(release) => (

View File

@ -5,7 +5,7 @@ import { SitePathsContext } from '@/components/utils/SitePathsContext'
import { Link } from '@/design-system/typography/link'
import { useContext, useEffect, useMemo } from 'react'
import { Trans } from 'react-i18next'
import { Route, Switch, useLocation } from 'react-router-dom'
import { Route, Routes, useLocation } from 'react-router-dom-v5-compat'
import Home from './Home'
import useSimulatorsData from './metadata'
import SimulateurPage from '../../components/PageData'
@ -33,7 +33,7 @@ export default function Simulateurs() {
<Route
key={s.path}
path={s.path}
render={() => <SimulateurPage {...s} />}
element={<SimulateurPage {...s} />}
/>
)),
[simulatorsData, sitePaths]
@ -59,10 +59,10 @@ export default function Simulateurs() {
</Link>
)
) : null)}
<Switch>
<Route exact path={sitePaths.simulateurs.index} component={Home} />
<Routes>
<Route path={sitePaths.simulateurs.index} element={<Home />} />
{simulatorRoutes}
</Switch>
</Routes>
</>
)
}

View File

@ -10,7 +10,6 @@ import { H2, H3 } from '@/design-system/typography/heading'
import { formatValue } from 'publicodes'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Trans } from 'react-i18next'
import { useHistory, useLocation } from 'react-router-dom'
import { toAtString } from '../../ATInternetTracking'
import statsJson from '@/data/stats.json'
import { debounce, groupBy } from '../../utils'
@ -22,6 +21,7 @@ import GlobalStats, { BigIndicator } from './GlobalStats'
import SatisfactionChart from './SatisfactionChart'
import { Page, PageChapter2, PageSatisfaction, StatsStruct } from './types'
import { formatDay, formatMonth } from './utils'
import { useSearchParams } from 'react-router-dom-v5-compat'
const stats = statsJson as unknown as StatsStruct
@ -117,30 +117,23 @@ interface BrushStartEndIndex {
const StatsDetail = () => {
const defaultPeriod = 'mois'
const history = useHistory()
const location = useLocation()
const [searchParams, setSearchParams] = useSearchParams()
useScrollToHash()
const urlParams = new URLSearchParams(location.search ?? '')
const [period, setPeriod] = useState<Period>(
(urlParams.get('periode') as Period) ?? defaultPeriod
(searchParams.get('periode') as Period) ?? defaultPeriod
)
const [chapter2, setChapter2] = useState<Chapter2 | ''>(
(urlParams.get('module') as Chapter2) ?? ''
(searchParams.get('module') as Chapter2) ?? ''
)
// The logic to persist some state in query parameters in the URL could be
// abstracted in a dedicated React hook.
useEffect(() => {
const queryParams = [
period !== defaultPeriod && `periode=${period}`,
chapter2 && `module=${chapter2}`,
].filter(Boolean)
history.replace({
search: `?${queryParams.join('&')}`,
hash: location.hash,
})
}, [period, chapter2])
const paramsEntries = [
['periode', period !== defaultPeriod ? period : ''],
['module', chapter2],
] as [string, string][]
setSearchParams(paramsEntries.filter(([, val]) => val !== ''))
}, [period, chapter2, setSearchParams])
const visites = useMemo(() => {
const rawData = period === 'jours' ? stats.visitesJours : stats.visitesMois

View File

@ -15,7 +15,7 @@ import { Grid } from '@mui/material'
import { lazy, Suspense, useContext, useEffect, useRef, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Route } from 'react-router'
import { MemoryRouter, useHistory, useLocation } from 'react-router-dom'
import { MemoryRouter } from 'react-router-dom'
import styled from 'styled-components'
import { TrackingContext, TrackPage } from '../../ATInternetTracking'
import { hexToHSL } from '../../hexToHSL'
@ -29,6 +29,7 @@ import './iframe.css'
import cciLogo from './images/cci.png'
import minTraLogo from './images/min-tra.jpg'
import poleEmploiLogo from './images/pole-emploi.png'
import { useSearchParams } from 'react-router-dom-v5-compat'
const LazyColorPicker = lazy(() => import('../Dev/ColorPicker'))
@ -46,13 +47,11 @@ const getFromSimu = <S extends SimulatorData, T extends string>(
: undefined
function IntegrationCustomizer() {
const { search } = useLocation()
const simulatorsData = useSimulatorsData()
const sitePaths = useContext(SitePathsContext)
const history = useHistory()
const [searchParams, setSearchParams] = useSearchParams()
const defaultModuleFromUrl =
new URLSearchParams(search ?? '').get('module') ?? ''
const defaultModuleFromUrl = searchParams.get('module') ?? ''
const [currentModule, setCurrentModule] = useState(
getFromSimu(simulatorsData, defaultModuleFromUrl)
@ -61,8 +60,8 @@ function IntegrationCustomizer() {
)
useEffect(() => {
history.replace({ search: `?module=${currentModule}` })
}, [currentModule, history])
setSearchParams({ module: currentModule }, { replace: true })
}, [currentModule, setSearchParams])
const [color, setColor] = useState<string | undefined>()

View File

@ -6,7 +6,7 @@ import { Banner, InnerBanner } from '@/design-system/banner'
import { Link } from '@/design-system/typography/link'
import { useContext } from 'react'
import { Trans } from 'react-i18next'
import { Route, Switch, useLocation } from 'react-router-dom'
import { Routes, Route, useLocation } from 'react-router-dom-v5-compat'
import { TrackChapter } from '../../ATInternetTracking'
import API from './API'
import Iframe from './Iframe'
@ -50,16 +50,16 @@ export default function Integration() {
</InnerBanner>
</Banner>
)}
<Switch>
<Route exact path={sitePaths.développeur.index} component={Options} />
<Route path={sitePaths.développeur.iframe} component={Iframe} />
<Route path={sitePaths.développeur.library} component={Library} />
<Route path={sitePaths.développeur.api} component={API} />
<Routes>
<Route path={sitePaths.développeur.index} element={<Options />} />
<Route path={sitePaths.développeur.iframe} element={<Iframe />} />
<Route path={sitePaths.développeur.library} element={<Library />} />
<Route path={sitePaths.développeur.api} element={<API />} />
<Route
path={sitePaths.développeur.spreadsheet}
component={Spreadsheet}
element={<Spreadsheet />}
/>
</Switch>
</Routes>
</TrackChapter>
)
}

View File

@ -6361,6 +6361,15 @@ __metadata:
languageName: node
linkType: hard
"@types/history@npm:^5.0.0":
version: 5.0.0
resolution: "@types/history@npm:5.0.0"
dependencies:
history: "*"
checksum: 53d35a7b3b7425dd1bac33ea024e3446eb9b8b1a390fef9bedb9428a6cfe84d9716d69c74364068dfca082abbbc5d5d126c5472ee5507150c4f55127422b5462
languageName: node
linkType: hard
"@types/hoist-non-react-statics@npm:*, @types/hoist-non-react-statics@npm:^3.3.0":
version: 3.3.1
resolution: "@types/hoist-non-react-statics@npm:3.3.1"
@ -13803,6 +13812,15 @@ __metadata:
languageName: node
linkType: hard
"history@npm:*, history@npm:^5.2.0, history@npm:^5.3.0":
version: 5.3.0
resolution: "history@npm:5.3.0"
dependencies:
"@babel/runtime": ^7.7.6
checksum: d73c35df49d19ac172f9547d30a21a26793e83f16a78386d99583b5bf1429cc980799fcf1827eb215d31816a6600684fba9686ce78104e23bd89ec239e7c726f
languageName: node
linkType: hard
"history@npm:5.0.0":
version: 5.0.0
resolution: "history@npm:5.0.0"
@ -13826,15 +13844,6 @@ __metadata:
languageName: node
linkType: hard
"history@npm:^5.2.0":
version: 5.3.0
resolution: "history@npm:5.3.0"
dependencies:
"@babel/runtime": ^7.7.6
checksum: d73c35df49d19ac172f9547d30a21a26793e83f16a78386d99583b5bf1429cc980799fcf1827eb215d31816a6600684fba9686ce78104e23bd89ec239e7c726f
languageName: node
linkType: hard
"hmac-drbg@npm:^1.0.1":
version: 1.0.1
resolution: "hmac-drbg@npm:1.0.1"
@ -18928,6 +18937,20 @@ __metadata:
languageName: node
linkType: hard
"react-router-dom-v5-compat@npm:^6.3.0":
version: 6.3.0
resolution: "react-router-dom-v5-compat@npm:6.3.0"
dependencies:
history: ^5.3.0
react-router: 6.3.0
peerDependencies:
react: ">=16.8"
react-dom: ">=16.8"
react-router-dom: 4 || 5
checksum: ee82b48078bac91cea3b8c499954cbf13f9261daef87e5032695946652b594b2f0590d279448ac59dd26a1c0d208a7c53d3aef2e97dcd0335f4af82df228f197
languageName: node
linkType: hard
"react-router-dom@npm:^5.3.0":
version: 5.3.0
resolution: "react-router-dom@npm:5.3.0"
@ -20356,6 +20379,7 @@ __metadata:
"@storybook/builder-vite": ^0.1.23
"@storybook/react": ^6.5.0-alpha.49
"@storybook/testing-library": ^0.0.9
"@types/history": ^5.0.0
"@types/react": ^17.0.0
"@types/react-color": ^3.0.1
"@types/react-dom": ^17.0.9
@ -20393,6 +20417,7 @@ __metadata:
react-redux: ^7.2.8
react-router: ^5.2.1
react-router-dom: ^5.3.0
react-router-dom-v5-compat: ^6.3.0
react-signature-pad-wrapper: ^1.2.11
react-spring: ^9.3.1
react-use-measure: ^2.0.4