From 87469fbb9afb52839aa7e7b794e7e82501ae0fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20G=C3=B3mez?= Date: Mon, 27 Jul 2026 10:33:27 +0200 Subject: [PATCH 1/2] chore: upgrade to react-router 7 Replace react-router-dom with react-router; v7 folds the DOM exports into the main package. Also drops the stale @types/react-router-dom v5 typings. Stops at 7.18.1 rather than v8: v8 requires react/react-dom 19.2.7+ and this app is on React 18. Node 22.22 already meets v8's floor, so React is the only blocker - deferred until there is enough test coverage to make a React major safe. No future flags were needed: the data-router flags don't apply to , all links under the /admin-dashboard/* splat are absolute, and every React.lazy call is at module scope. Breaking for consumers passing additionalRoutes - they must move to react-router 7 too. Assisted-By: anthropic:claude-opus-4-8[1m] --- webui/CHANGELOG.md | 3 + webui/package.json | 4 +- webui/src/components/extension-card.tsx | 2 +- .../extension/extension-detail-view.tsx | 2 +- webui/src/components/sanitized-markdown.tsx | 2 +- webui/src/context/search/search-context.tsx | 2 +- webui/src/default/default-app.tsx | 2 +- webui/src/default/menu-content.tsx | 2 +- webui/src/default/page-settings.tsx | 2 +- webui/src/header-menu.tsx | 2 +- webui/src/hooks/use-search.ts | 2 +- webui/src/layout/app-footer.tsx | 2 +- webui/src/layout/app-layout.tsx | 2 +- webui/src/layout/app-navbar.tsx | 2 +- webui/src/layout/nav-search-field.tsx | 2 +- webui/src/layout/scroll-to-top.tsx | 2 +- webui/src/main.tsx | 2 +- .../pages/admin-dashboard/admin-dashboard.tsx | 2 +- .../pages/admin-dashboard/admin-header.tsx | 2 +- .../pages/admin-dashboard/admin-sidepanel.tsx | 2 +- .../customers/customer-details.tsx | 2 +- .../customers/customer-member-list.tsx | 2 +- .../admin-dashboard/customers/customers.tsx | 2 +- .../pages/admin-dashboard/extension-admin.tsx | 2 +- .../pages/admin-dashboard/namespace-admin.tsx | 2 +- .../pages/admin-dashboard/publisher-admin.tsx | 2 +- .../admin-dashboard/publisher-details.tsx | 2 +- .../usage-stats/usage-stats.tsx | 2 +- webui/src/pages/admin-dashboard/welcome.tsx | 2 +- .../extension-detail-overview.tsx | 2 +- .../extension-detail/extension-detail.tsx | 2 +- webui/src/pages/home/get-involved.tsx | 2 +- webui/src/pages/home/hero-search.tsx | 2 +- webui/src/pages/home/home-page.tsx | 2 +- .../namespace-detail/namespace-detail.tsx | 2 +- webui/src/pages/user/avatar.tsx | 2 +- .../user-namespace-extension-list-item.tsx | 2 +- webui/src/pages/user/user-setting-tabs.tsx | 2 +- .../pages/user/user-settings-extension.tsx | 2 +- .../user/user-settings-namespace-detail.tsx | 2 +- webui/src/pages/user/user-settings-tokens.tsx | 2 +- webui/src/pages/user/user-settings.tsx | 2 +- webui/yarn.lock | 83 ++++++------------- 43 files changed, 70 insertions(+), 100 deletions(-) diff --git a/webui/CHANGELOG.md b/webui/CHANGELOG.md index d8f67674e..76397919f 100644 --- a/webui/CHANGELOG.md +++ b/webui/CHANGELOG.md @@ -24,6 +24,7 @@ This change log covers only the frontend library (webui) of Open VSX. - Migrate admin dashboard to use `@tanstack/react-query` ([#1917](https://github.com/eclipse-openvsx/openvsx/pull/1917) - Replace formatting from `stylistic` with `prettier` ([#1916](https://github.com/eclipse-openvsx/openvsx/pull/1916)) - Upgrade to vite 8+ and disable manual chunks for bundling ([#1989](https://github.com/eclipse-openvsx/openvsx/pull/1989)) +- Upgrade to react-router 7: import everything from `react-router` instead of `react-router-dom`. Consumers passing `additionalRoutes` must upgrade to react-router 7 as well ### Fixed @@ -37,6 +38,8 @@ This change log covers only the frontend library (webui) of Open VSX. - Bump tar from `7.5.16` to `7.5.22` ([#1994](https://github.com/eclipse-openvsx/openvsx/pull/1994)) - Bump js-yaml from `4.2.0` to `4.3.0` ([#1976](https://github.com/eclipse-openvsx/openvsx/pull/1976)) - Bump dompurify from `3.4.11` to `3.4.12` ([#1984](https://github.com/eclipse-openvsx/openvsx/pull/1984)) +- Bump react-router from `6.30.4` to `7.18.1` +- Remove react-router-dom and its stale `@types/react-router-dom` v5 typings ## [v1.0.2] (23/06/2026) diff --git a/webui/package.json b/webui/package.json index 930327baa..d64316f10 100644 --- a/webui/package.json +++ b/webui/package.json @@ -70,8 +70,7 @@ "react-dropzone": "^14.2.3", "react-helmet-async": "^2.0.5", "react-infinite-scroller": "^1.2.6", - "react-router": "^6.30.4", - "react-router-dom": "^6.30.4" + "react-router": "^7.18.1" }, "resolutions": { "@types/react": "^18.2.14", @@ -94,7 +93,6 @@ "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@types/react-infinite-scroller": "^1.2.0", - "@types/react-router-dom": "^5.3.0", "@types/react-transition-group": "^4.4.0", "@typescript-eslint/eslint-plugin": "^8.56.0", "@typescript-eslint/parser": "^8.56.0", diff --git a/webui/src/components/extension-card.tsx b/webui/src/components/extension-card.tsx index 79dab0b76..f0503456e 100644 --- a/webui/src/components/extension-card.tsx +++ b/webui/src/components/extension-card.tsx @@ -12,7 +12,7 @@ ********************************************************************************/ import { forwardRef, FunctionComponent, memo } from 'react'; -import { Link as RouteLink } from 'react-router-dom'; +import { Link as RouteLink } from 'react-router'; import { Paper, Typography, Box, Fade, Skeleton } from '@mui/material'; import { CSSObject, styled, Theme } from '@mui/material/styles'; import SaveAltIcon from '@mui/icons-material/SaveAlt'; diff --git a/webui/src/components/extension/extension-detail-view.tsx b/webui/src/components/extension/extension-detail-view.tsx index 7bf2cf7b8..1ca748d0a 100644 --- a/webui/src/components/extension/extension-detail-view.tsx +++ b/webui/src/components/extension/extension-detail-view.tsx @@ -13,7 +13,7 @@ import { FunctionComponent, ReactNode, useEffect, useState } from 'react'; import { Box, Button, Divider, Stack, Typography } from '@mui/material'; -import { Link as RouteLink } from 'react-router-dom'; +import { Link as RouteLink } from 'react-router'; import { Extension, VERSION_ALIASES, VersionTargetPlatforms } from '../../extension-registry-types'; import { ExtensionHeader } from './extension-header'; import { ExtensionStatusChips } from './extension-status-chips'; diff --git a/webui/src/components/sanitized-markdown.tsx b/webui/src/components/sanitized-markdown.tsx index 98a49513d..eff4aab1e 100644 --- a/webui/src/components/sanitized-markdown.tsx +++ b/webui/src/components/sanitized-markdown.tsx @@ -15,7 +15,7 @@ import { alert } from '@mdit/plugin-alert'; import DOMPurify from 'dompurify'; import { Theme, styled } from '@mui/material/styles'; import linkIcon from './link-icon'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; const Markdown = styled('div')(({ theme }: { theme: Theme }) => ({ // The container owns the leading gap; the first block shouldn't add its own diff --git a/webui/src/context/search/search-context.tsx b/webui/src/context/search/search-context.tsx index 3f969a342..5cde43715 100644 --- a/webui/src/context/search/search-context.tsx +++ b/webui/src/context/search/search-context.tsx @@ -12,7 +12,7 @@ ********************************************************************************/ import { createContext, FunctionComponent, ReactNode, useContext, useEffect, useMemo, useState } from 'react'; -import { useSearchParams } from 'react-router-dom'; +import { useSearchParams } from 'react-router'; import { SearchFocusProvider } from './search-focus-context'; import { PageSearchBarProvider } from './page-search-bar-context'; diff --git a/webui/src/default/default-app.tsx b/webui/src/default/default-app.tsx index 6db67b827..5c477b73f 100644 --- a/webui/src/default/default-app.tsx +++ b/webui/src/default/default-app.tsx @@ -18,7 +18,7 @@ import '../main.css'; import { createRoot } from 'react-dom/client'; import { useMemo } from 'react'; import { HelmetProvider } from 'react-helmet-async'; -import { BrowserRouter } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import { ThemeProvider } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import { ExtensionRegistryService } from '../extension-registry-service'; diff --git a/webui/src/default/menu-content.tsx b/webui/src/default/menu-content.tsx index 9033b9710..75d6d6bfe 100644 --- a/webui/src/default/menu-content.tsx +++ b/webui/src/default/menu-content.tsx @@ -10,7 +10,7 @@ import { FunctionComponent, PropsWithChildren, useContext, useRef, useState } from 'react'; import { Avatar, Button, IconButton, Link, Menu, MenuItem, Typography } from '@mui/material'; -import { useLocation, useNavigate, Link as RouteLink } from 'react-router-dom'; +import { useLocation, useNavigate, Link as RouteLink } from 'react-router'; import { UserAvatar } from '../pages/user/avatar'; import { UserSettingsRoutes } from '../pages/user/user-settings-routes'; import { alpha, styled, Theme } from '@mui/material/styles'; diff --git a/webui/src/default/page-settings.tsx b/webui/src/default/page-settings.tsx index 4e9f2f84d..1078fda8b 100644 --- a/webui/src/default/page-settings.tsx +++ b/webui/src/default/page-settings.tsx @@ -11,7 +11,7 @@ import { FunctionComponent, ReactNode } from 'react'; import { Helmet } from 'react-helmet-async'; import { Typography, Box, Link } from '@mui/material'; -import { Link as RouteLink, Route, useParams } from 'react-router-dom'; +import { Link as RouteLink, Route, useParams } from 'react-router'; import GitHubIcon from '@mui/icons-material/GitHub'; import CallSplitIcon from '@mui/icons-material/CallSplit'; import BugReportIcon from '@mui/icons-material/BugReport'; diff --git a/webui/src/header-menu.tsx b/webui/src/header-menu.tsx index 6cc81c5a2..2af2dd779 100644 --- a/webui/src/header-menu.tsx +++ b/webui/src/header-menu.tsx @@ -13,7 +13,7 @@ import { Menu, IconButton, useTheme } from '@mui/material'; import useMediaQuery from '@mui/material/useMediaQuery'; import MenuIcon from '@mui/icons-material/Menu'; import { MainContext } from './context'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; export const HeaderMenu: FunctionComponent = () => { const theme = useTheme(); diff --git a/webui/src/hooks/use-search.ts b/webui/src/hooks/use-search.ts index 2880360d8..acc61d166 100644 --- a/webui/src/hooks/use-search.ts +++ b/webui/src/hooks/use-search.ts @@ -12,7 +12,7 @@ ********************************************************************************/ import { useCallback, useMemo } from 'react'; -import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; +import { useLocation, useNavigate, useSearchParams } from 'react-router'; import { ExtensionListRoutes } from '../pages/extension-list/extension-list-routes'; import { ExtensionCategory, SortBy, SortOrder } from '../extension-registry-types'; diff --git a/webui/src/layout/app-footer.tsx b/webui/src/layout/app-footer.tsx index 9630142fe..743022956 100644 --- a/webui/src/layout/app-footer.tsx +++ b/webui/src/layout/app-footer.tsx @@ -15,7 +15,7 @@ import { FunctionComponent, useContext, useState } from 'react'; import { Box, Container, Typography } from '@mui/material'; import { styled } from '@mui/material/styles'; import type { Theme } from '@mui/material/styles'; -import { Link as RouteLink } from 'react-router-dom'; +import { Link as RouteLink } from 'react-router'; import { KbdKey } from '../components/kbd-key'; import { useShortcut } from '../hooks/use-shortcut'; import { Eyebrow, accentHover, focusOutline } from '../components/page-primitives'; diff --git a/webui/src/layout/app-layout.tsx b/webui/src/layout/app-layout.tsx index 10bba3734..3596a35fe 100644 --- a/webui/src/layout/app-layout.tsx +++ b/webui/src/layout/app-layout.tsx @@ -12,7 +12,7 @@ ********************************************************************************/ import { FunctionComponent, lazy, Suspense, useContext, useEffect, useState } from 'react'; -import { Routes, Route, useNavigate } from 'react-router-dom'; +import { Routes, Route, useNavigate } from 'react-router'; import { Box } from '@mui/material'; import { styled } from '@mui/material/styles'; import { Banner } from '../components/banner'; diff --git a/webui/src/layout/app-navbar.tsx b/webui/src/layout/app-navbar.tsx index 7a774453b..430405dae 100644 --- a/webui/src/layout/app-navbar.tsx +++ b/webui/src/layout/app-navbar.tsx @@ -14,7 +14,7 @@ import { FunctionComponent, useContext, useEffect, useRef, useState } from 'react'; import { AppBar, Box, Toolbar } from '@mui/material'; import { alpha, styled, useTheme } from '@mui/material/styles'; -import { Link as RouteLink } from 'react-router-dom'; +import { Link as RouteLink } from 'react-router'; import { HeaderMenu } from '../header-menu'; import { MainContext } from '../context'; import { usePageSearchBar } from '../context/search/page-search-bar-context'; diff --git a/webui/src/layout/nav-search-field.tsx b/webui/src/layout/nav-search-field.tsx index 7071529d5..a943750f0 100644 --- a/webui/src/layout/nav-search-field.tsx +++ b/webui/src/layout/nav-search-field.tsx @@ -22,7 +22,7 @@ import { useState } from 'react'; import { Box } from '@mui/material'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import { ExtensionSearchfield } from '../components/extension-searchfield'; import { ExtensionListRoutes } from '../pages/extension-list/extension-list-routes'; import { useSearch, SEARCH_DEBOUNCE_MS } from '../hooks/use-search'; diff --git a/webui/src/layout/scroll-to-top.tsx b/webui/src/layout/scroll-to-top.tsx index 35f955714..9b2cdc82b 100644 --- a/webui/src/layout/scroll-to-top.tsx +++ b/webui/src/layout/scroll-to-top.tsx @@ -12,7 +12,7 @@ ********************************************************************************/ import { FunctionComponent, useLayoutEffect } from 'react'; -import { useLocation, useNavigationType } from 'react-router-dom'; +import { useLocation, useNavigationType } from 'react-router'; // BrowserRouter leaves window scroll untouched on navigation, so a page opened // from deep in a long list would start at that old offset. Reset to the top on diff --git a/webui/src/main.tsx b/webui/src/main.tsx index e26094ae7..049052bfc 100644 --- a/webui/src/main.tsx +++ b/webui/src/main.tsx @@ -10,7 +10,7 @@ import { FunctionComponent, ReactNode, useEffect, useState, useRef, lazy, Suspense } from 'react'; import { CssBaseline } from '@mui/material'; -import { Route, Routes } from 'react-router-dom'; +import { Route, Routes } from 'react-router'; import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { queryClient } from './query-client'; diff --git a/webui/src/pages/admin-dashboard/admin-dashboard.tsx b/webui/src/pages/admin-dashboard/admin-dashboard.tsx index 59e305f6e..cb78f7e45 100644 --- a/webui/src/pages/admin-dashboard/admin-dashboard.tsx +++ b/webui/src/pages/admin-dashboard/admin-dashboard.tsx @@ -11,7 +11,7 @@ import { FunctionComponent, ReactNode, useContext, lazy, Suspense } from 'react'; import { Box, Container, CssBaseline, Typography, IconButton } from '@mui/material'; import { styled } from '@mui/material/styles'; -import { Route, Routes, useNavigate } from 'react-router-dom'; +import { Route, Routes, useNavigate } from 'react-router'; import AccountBoxIcon from '@mui/icons-material/AccountBox'; import AssignmentIndIcon from '@mui/icons-material/AssignmentInd'; import BarChartIcon from '@mui/icons-material/BarChart'; diff --git a/webui/src/pages/admin-dashboard/admin-header.tsx b/webui/src/pages/admin-dashboard/admin-header.tsx index 7a12b9074..796762073 100644 --- a/webui/src/pages/admin-dashboard/admin-header.tsx +++ b/webui/src/pages/admin-dashboard/admin-header.tsx @@ -14,7 +14,7 @@ import { FunctionComponent } from 'react'; import { AppBar, Breadcrumbs, IconButton, Link, Toolbar, Typography } from '@mui/material'; import HighlightOffIcon from '@mui/icons-material/HighlightOff'; -import { Link as RouterLink, useLocation } from 'react-router-dom'; +import { Link as RouterLink, useLocation } from 'react-router'; export interface AdminHeaderProps { routeNames: Record; diff --git a/webui/src/pages/admin-dashboard/admin-sidepanel.tsx b/webui/src/pages/admin-dashboard/admin-sidepanel.tsx index 5b0540f0f..7c9c91e3a 100644 --- a/webui/src/pages/admin-dashboard/admin-sidepanel.tsx +++ b/webui/src/pages/admin-dashboard/admin-sidepanel.tsx @@ -12,7 +12,7 @@ *****************************************************************************/ import { FunctionComponent } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import { Sidepanel } from '../../components/sidepanel/sidepanel'; import { NavigationItem } from '../../components/sidepanel/navigation-item'; import { isNavGroup, NavEntry } from './nav-types'; diff --git a/webui/src/pages/admin-dashboard/customers/customer-details.tsx b/webui/src/pages/admin-dashboard/customers/customer-details.tsx index fb2630c76..f689dcca0 100644 --- a/webui/src/pages/admin-dashboard/customers/customer-details.tsx +++ b/webui/src/pages/admin-dashboard/customers/customer-details.tsx @@ -14,7 +14,7 @@ import { FC, useState } from 'react'; import { Box, Typography, Button, Alert, LinearProgress } from '@mui/material'; import EditIcon from '@mui/icons-material/Edit'; -import { useParams } from 'react-router-dom'; +import { useParams } from 'react-router'; import type { Customer } from '../../../extension-registry-types'; import { handleError } from '../../../utils'; import { useAdminUsageStats } from '../usage-stats/use-usage-stats'; diff --git a/webui/src/pages/admin-dashboard/customers/customer-member-list.tsx b/webui/src/pages/admin-dashboard/customers/customer-member-list.tsx index ed362c2ad..09701650e 100644 --- a/webui/src/pages/admin-dashboard/customers/customer-member-list.tsx +++ b/webui/src/pages/admin-dashboard/customers/customer-member-list.tsx @@ -26,7 +26,7 @@ import { type PaperProps, Paper } from '@mui/material'; -import { Link as RouterLink } from 'react-router-dom'; +import { Link as RouterLink } from 'react-router'; import { AdminDashboardRoutes } from '../admin-dashboard-routes'; import { MainContext } from '../../../context'; import { Customer, UserData } from '../../../extension-registry-types'; diff --git a/webui/src/pages/admin-dashboard/customers/customers.tsx b/webui/src/pages/admin-dashboard/customers/customers.tsx index 6cbe2aded..3020010f9 100644 --- a/webui/src/pages/admin-dashboard/customers/customers.tsx +++ b/webui/src/pages/admin-dashboard/customers/customers.tsx @@ -24,7 +24,7 @@ import { DeleteCustomerDialog } from './delete-customer-dialog'; import { createRoute, handleError } from '../../../utils'; import { createMultiSelectFilterOperators, createArrayContainsFilterOperators } from '../components'; import { AdminDashboardRoutes } from '../admin-dashboard-routes'; -import { Link } from 'react-router-dom'; +import { Link } from 'react-router'; import { useCreateCustomer, useCustomers, useDeleteCustomer, useUpdateCustomer } from './use-customers'; export const Customers: FC = () => { diff --git a/webui/src/pages/admin-dashboard/extension-admin.tsx b/webui/src/pages/admin-dashboard/extension-admin.tsx index 20fefd974..a85fc39d2 100644 --- a/webui/src/pages/admin-dashboard/extension-admin.tsx +++ b/webui/src/pages/admin-dashboard/extension-admin.tsx @@ -9,7 +9,7 @@ ********************************************************************************/ import { FunctionComponent, useContext, useEffect, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router'; import { Button, Typography } from '@mui/material'; import { MainContext } from '../../context'; diff --git a/webui/src/pages/admin-dashboard/namespace-admin.tsx b/webui/src/pages/admin-dashboard/namespace-admin.tsx index 1a108382d..1ce48050d 100644 --- a/webui/src/pages/admin-dashboard/namespace-admin.tsx +++ b/webui/src/pages/admin-dashboard/namespace-admin.tsx @@ -10,7 +10,7 @@ import { FunctionComponent, useState, useContext, useEffect, ReactNode } from 'react'; import { Typography, Box } from '@mui/material'; -import { useParams, useNavigate } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router'; import { NamespaceDetail, NamespaceDetailConfigContext } from '../user/user-settings-namespace-detail'; import { ButtonWithProgress } from '../../components/button-with-progress'; import { MainContext } from '../../context'; diff --git a/webui/src/pages/admin-dashboard/publisher-admin.tsx b/webui/src/pages/admin-dashboard/publisher-admin.tsx index 5524436bf..9a3db2474 100644 --- a/webui/src/pages/admin-dashboard/publisher-admin.tsx +++ b/webui/src/pages/admin-dashboard/publisher-admin.tsx @@ -29,7 +29,7 @@ import SearchIcon from '@mui/icons-material/Search'; import ClearIcon from '@mui/icons-material/Clear'; import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'; import PersonIcon from '@mui/icons-material/Person'; -import { useParams, useNavigate } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router'; import { UserRelationships } from '../../extension-registry-types'; import { ErrorResponse } from '../../server-request'; import { MainContext } from '../../context'; diff --git a/webui/src/pages/admin-dashboard/publisher-details.tsx b/webui/src/pages/admin-dashboard/publisher-details.tsx index 9ce38099e..56728b636 100644 --- a/webui/src/pages/admin-dashboard/publisher-details.tsx +++ b/webui/src/pages/admin-dashboard/publisher-details.tsx @@ -24,7 +24,7 @@ import { Typography } from '@mui/material'; import { useIsMutating } from '@tanstack/react-query'; -import { Link as RouterLink } from 'react-router-dom'; +import { Link as RouterLink } from 'react-router'; import GitHubIcon from '@mui/icons-material/GitHub'; import PersonIcon from '@mui/icons-material/Person'; import FolderSharedIcon from '@mui/icons-material/FolderShared'; diff --git a/webui/src/pages/admin-dashboard/usage-stats/usage-stats.tsx b/webui/src/pages/admin-dashboard/usage-stats/usage-stats.tsx index d9c28508d..ffffb0877 100644 --- a/webui/src/pages/admin-dashboard/usage-stats/usage-stats.tsx +++ b/webui/src/pages/admin-dashboard/usage-stats/usage-stats.tsx @@ -13,7 +13,7 @@ import { FC, useContext, useMemo } from 'react'; import { Box, Alert } from '@mui/material'; -import { useParams, useNavigate } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router'; import { MainContext } from '../../../context'; import type { Customer } from '../../../extension-registry-types'; import { handleError } from '../../../utils'; diff --git a/webui/src/pages/admin-dashboard/welcome.tsx b/webui/src/pages/admin-dashboard/welcome.tsx index de9192aa1..c472dc3ab 100644 --- a/webui/src/pages/admin-dashboard/welcome.tsx +++ b/webui/src/pages/admin-dashboard/welcome.tsx @@ -10,7 +10,7 @@ import { FunctionComponent } from 'react'; import { Box, Card, CardActionArea, CardContent, Divider, Grid, Typography } from '@mui/material'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router'; import { isNavGroup, NavEntry, RouteEntry } from './nav-types'; interface NavSection { diff --git a/webui/src/pages/extension-detail/extension-detail-overview.tsx b/webui/src/pages/extension-detail/extension-detail-overview.tsx index d1b7b514a..2c099119a 100644 --- a/webui/src/pages/extension-detail/extension-detail-overview.tsx +++ b/webui/src/pages/extension-detail/extension-detail-overview.tsx @@ -10,7 +10,7 @@ import { FunctionComponent, ReactNode, useContext, useEffect, useState, useRef, useMemo } from 'react'; import { Box, Theme, Typography, Button, Link, NativeSelect, SxProps, styled, Grid, Stack } from '@mui/material'; -import { Link as RouteLink, useNavigate, useParams } from 'react-router-dom'; +import { Link as RouteLink, useNavigate, useParams } from 'react-router'; import HomeIcon from '@mui/icons-material/Home'; import GitHubIcon from '@mui/icons-material/GitHub'; import BugReportIcon from '@mui/icons-material/BugReport'; diff --git a/webui/src/pages/extension-detail/extension-detail.tsx b/webui/src/pages/extension-detail/extension-detail.tsx index 5707da165..dc36e239f 100644 --- a/webui/src/pages/extension-detail/extension-detail.tsx +++ b/webui/src/pages/extension-detail/extension-detail.tsx @@ -25,7 +25,7 @@ import { decomposeColor } from '@mui/material'; import { alpha, styled } from '@mui/material/styles'; -import { Link as RouteLink, Route, Routes, useNavigate, useParams } from 'react-router-dom'; +import { Link as RouteLink, Route, Routes, useNavigate, useParams } from 'react-router'; import { MainContext } from '../../context'; import { createRoute, formatCompactNumber } from '../../utils'; import { DelayedLoadIndicator } from '../../components/delayed-load-indicator'; diff --git a/webui/src/pages/home/get-involved.tsx b/webui/src/pages/home/get-involved.tsx index f36dd3a48..d11c742e0 100644 --- a/webui/src/pages/home/get-involved.tsx +++ b/webui/src/pages/home/get-involved.tsx @@ -15,7 +15,7 @@ import { FunctionComponent, ReactNode } from 'react'; import { Box, Container, Typography } from '@mui/material'; import { styled } from '@mui/material/styles'; import type { Theme } from '@mui/material/styles'; -import { Link as RouteLink } from 'react-router-dom'; +import { Link as RouteLink } from 'react-router'; import { HomeInvolvementCard } from '../../page-settings'; import { Eyebrow, focusOutline } from '../../components/page-primitives'; diff --git a/webui/src/pages/home/hero-search.tsx b/webui/src/pages/home/hero-search.tsx index 1d09459c1..2c3ca9ad6 100644 --- a/webui/src/pages/home/hero-search.tsx +++ b/webui/src/pages/home/hero-search.tsx @@ -22,7 +22,7 @@ import { useState } from 'react'; import { Box, ButtonBase, Container, Typography } from '@mui/material'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import { flushSync } from 'react-dom'; import { styled, alpha } from '@mui/material/styles'; import { accentHover, focusOutline, focusRing } from '../../components/page-primitives'; diff --git a/webui/src/pages/home/home-page.tsx b/webui/src/pages/home/home-page.tsx index b6bde4aa0..8a3263162 100644 --- a/webui/src/pages/home/home-page.tsx +++ b/webui/src/pages/home/home-page.tsx @@ -12,7 +12,7 @@ ********************************************************************************/ import { FunctionComponent, useContext } from 'react'; -import { Navigate, useSearchParams } from 'react-router-dom'; +import { Navigate, useSearchParams } from 'react-router'; import { PageContainer } from '../../components/page-container'; import { SectionSeparator, SectionStack } from '../../components/page-primitives'; import { MainContext } from '../../context'; diff --git a/webui/src/pages/namespace-detail/namespace-detail.tsx b/webui/src/pages/namespace-detail/namespace-detail.tsx index 555fc91eb..25c70af7a 100644 --- a/webui/src/pages/namespace-detail/namespace-detail.tsx +++ b/webui/src/pages/namespace-detail/namespace-detail.tsx @@ -13,7 +13,7 @@ import { Typography, Box, Container, Link, Divider } from '@mui/material'; import GitHubIcon from '@mui/icons-material/GitHub'; import LinkedInIcon from '@mui/icons-material/LinkedIn'; import TwitterIcon from '@mui/icons-material/Twitter'; -import { useParams } from 'react-router-dom'; +import { useParams } from 'react-router'; import { ExtensionCard } from '../../components/extension-card'; import { ExtensionGrid } from '../../components/page-primitives'; import { MainContext } from '../../context'; diff --git a/webui/src/pages/user/avatar.tsx b/webui/src/pages/user/avatar.tsx index 68c33c2b8..5a2c8fe18 100644 --- a/webui/src/pages/user/avatar.tsx +++ b/webui/src/pages/user/avatar.tsx @@ -10,7 +10,7 @@ import { FunctionComponent, useContext, useRef, useState } from 'react'; import { Avatar, Box, IconButton, Link, Menu, MenuItem, Typography } from '@mui/material'; -import { Link as RouteLink } from 'react-router-dom'; +import { Link as RouteLink } from 'react-router'; import SettingsIcon from '@mui/icons-material/Settings'; import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'; import LogoutIcon from '@mui/icons-material/Logout'; diff --git a/webui/src/pages/user/user-namespace-extension-list-item.tsx b/webui/src/pages/user/user-namespace-extension-list-item.tsx index 79ab92466..a801cf588 100644 --- a/webui/src/pages/user/user-namespace-extension-list-item.tsx +++ b/webui/src/pages/user/user-namespace-extension-list-item.tsx @@ -10,7 +10,7 @@ import type { ReactNode } from 'react'; import { FunctionComponent } from 'react'; -import { Link as RouteLink } from 'react-router-dom'; +import { Link as RouteLink } from 'react-router'; import { Paper, Typography, Box, styled } from '@mui/material'; import { Extension } from '../../extension-registry-types'; import { createRoute } from '../../utils'; diff --git a/webui/src/pages/user/user-setting-tabs.tsx b/webui/src/pages/user/user-setting-tabs.tsx index cad716d44..42b5f447d 100644 --- a/webui/src/pages/user/user-setting-tabs.tsx +++ b/webui/src/pages/user/user-setting-tabs.tsx @@ -10,7 +10,7 @@ import { ReactElement } from 'react'; import { Tabs, Tab, useTheme, useMediaQuery } from '@mui/material'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router'; import { createRoute } from '../../utils'; import { UserSettingsRoutes } from './user-settings-routes'; diff --git a/webui/src/pages/user/user-settings-extension.tsx b/webui/src/pages/user/user-settings-extension.tsx index 350825b3a..b056de809 100644 --- a/webui/src/pages/user/user-settings-extension.tsx +++ b/webui/src/pages/user/user-settings-extension.tsx @@ -12,7 +12,7 @@ *****************************************************************************/ import { FunctionComponent, useContext, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router'; import { MainContext } from '../../context'; import { DelayedLoadIndicator } from '../../components/delayed-load-indicator'; import { ExtensionDetailView } from '../../components/extension/extension-detail-view'; diff --git a/webui/src/pages/user/user-settings-namespace-detail.tsx b/webui/src/pages/user/user-settings-namespace-detail.tsx index 5c1fab944..e64e25980 100644 --- a/webui/src/pages/user/user-settings-namespace-detail.tsx +++ b/webui/src/pages/user/user-settings-namespace-detail.tsx @@ -9,7 +9,7 @@ ********************************************************************************/ import { FunctionComponent, createContext, useState } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import { Box, Button, Link, Paper, Grid, Typography } from '@mui/material'; import { styled, Theme } from '@mui/material/styles'; import WarningIcon from '@mui/icons-material/Warning'; diff --git a/webui/src/pages/user/user-settings-tokens.tsx b/webui/src/pages/user/user-settings-tokens.tsx index 7792f3951..461767e88 100644 --- a/webui/src/pages/user/user-settings-tokens.tsx +++ b/webui/src/pages/user/user-settings-tokens.tsx @@ -23,7 +23,7 @@ import { DialogActions } from '@mui/material'; import { styled } from '@mui/material/styles'; -import { Link as RouteLink } from 'react-router-dom'; +import { Link as RouteLink } from 'react-router'; import { DelayedLoadIndicator } from '../../components/delayed-load-indicator'; import { Timestamp } from '../../components/timestamp'; import { PersonalAccessToken } from '../../extension-registry-types'; diff --git a/webui/src/pages/user/user-settings.tsx b/webui/src/pages/user/user-settings.tsx index 877f0541b..4716335ff 100644 --- a/webui/src/pages/user/user-settings.tsx +++ b/webui/src/pages/user/user-settings.tsx @@ -11,7 +11,7 @@ import { FunctionComponent, ReactNode, useContext } from 'react'; import { Helmet } from 'react-helmet-async'; import { Grid, Box, Typography, Link } from '@mui/material'; -import { useParams } from 'react-router-dom'; +import { useParams } from 'react-router'; import { PageContainer } from '../../components/page-container'; import { DelayedLoadIndicator } from '../../components/delayed-load-indicator'; import { UserSettingTabs } from './user-setting-tabs'; diff --git a/webui/yarn.lock b/webui/yarn.lock index 3de5c858e..f0b676b25 100644 --- a/webui/yarn.lock +++ b/webui/yarn.lock @@ -997,13 +997,6 @@ __metadata: languageName: node linkType: hard -"@remix-run/router@npm:1.23.3": - version: 1.23.3 - resolution: "@remix-run/router@npm:1.23.3" - checksum: 10/a12ae9994bf017d47392ce2be24252b4e2281f4b27eac78f0e9b48afa5f522be9c0ec64b85db013ded1fff140d9512b97ac8ea5dd182138dcd6d3ded3136cbf6 - languageName: node - linkType: hard - "@rolldown/binding-android-arm64@npm:1.1.5": version: 1.1.5 resolution: "@rolldown/binding-android-arm64@npm:1.1.5" @@ -1360,13 +1353,6 @@ __metadata: languageName: node linkType: hard -"@types/history@npm:^4.7.11": - version: 4.7.11 - resolution: "@types/history@npm:4.7.11" - checksum: 10/1da529a3485f3015daf794effa3185493bf7dd2551c26932389c614f5a0aab76ab97645897d1eef9c74ead216a3848fcaa019f165bbd6e4b71da6eff164b4c68 - languageName: node - linkType: hard - "@types/http-errors@npm:*": version: 2.0.4 resolution: "@types/http-errors@npm:2.0.4" @@ -1497,27 +1483,6 @@ __metadata: languageName: node linkType: hard -"@types/react-router-dom@npm:^5.3.0": - version: 5.3.3 - resolution: "@types/react-router-dom@npm:5.3.3" - dependencies: - "@types/history": "npm:^4.7.11" - "@types/react": "npm:*" - "@types/react-router": "npm:*" - checksum: 10/28c4ea48909803c414bf5a08502acbb8ba414669b4b43bb51297c05fe5addc4df0b8fd00e0a9d1e3535ec4073ef38aaafac2c4a2b95b787167d113bc059beff3 - languageName: node - linkType: hard - -"@types/react-router@npm:*": - version: 5.1.20 - resolution: "@types/react-router@npm:5.1.20" - dependencies: - "@types/history": "npm:^4.7.11" - "@types/react": "npm:*" - checksum: 10/72d78d2f4a4752ec40940066b73d7758a0824c4d0cbeb380ae24c8b1cdacc21a6fc835a99d6849b5b295517a3df5466fc28be038f1040bd870f8e39e5ded43a4 - languageName: node - linkType: hard - "@types/react-transition-group@npm:^4.4.0, @types/react-transition-group@npm:^4.4.10, @types/react-transition-group@npm:^4.4.12": version: 4.4.12 resolution: "@types/react-transition-group@npm:4.4.12" @@ -2399,6 +2364,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^1.0.1": + version: 1.1.1 + resolution: "cookie@npm:1.1.1" + checksum: 10/85538153054791155cf4d38d2e807e3b9382d71bf71d92fc46fca348515ea574049d0d9ef8eb84d2d54a681ad1d7a7316b1989b901dace50a6c0f4c3858dbdb2 + languageName: node + linkType: hard + "cookie@npm:~0.7.1": version: 0.7.2 resolution: "cookie@npm:0.7.2" @@ -5234,7 +5206,6 @@ __metadata: "@types/react": "npm:^18.2.0" "@types/react-dom": "npm:^18.2.0" "@types/react-infinite-scroller": "npm:^1.2.0" - "@types/react-router-dom": "npm:^5.3.0" "@types/react-transition-group": "npm:^4.4.0" "@typescript-eslint/eslint-plugin": "npm:^8.56.0" "@typescript-eslint/parser": "npm:^8.56.0" @@ -5264,8 +5235,7 @@ __metadata: react-dropzone: "npm:^14.2.3" react-helmet-async: "npm:^2.0.5" react-infinite-scroller: "npm:^1.2.6" - react-router: "npm:^6.30.4" - react-router-dom: "npm:^6.30.4" + react-router: "npm:^7.18.1" rimraf: "npm:^6.1.0" rollup-plugin-visualizer: "npm:^7.0.1" ts-node: "npm:^10.9.2" @@ -5691,27 +5661,19 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:^6.30.4": - version: 6.30.4 - resolution: "react-router-dom@npm:6.30.4" +"react-router@npm:^7.18.1": + version: 7.18.1 + resolution: "react-router@npm:7.18.1" dependencies: - "@remix-run/router": "npm:1.23.3" - react-router: "npm:6.30.4" + cookie: "npm:^1.0.1" + set-cookie-parser: "npm:^2.6.0" peerDependencies: - react: ">=16.8" - react-dom: ">=16.8" - checksum: 10/e220abac766bb9bcc56a99b78d30d99745cae3618f84baa92bd6ec481eccb6afb12c724d49dc68e6f870b70ff449f751f20f4aa33f1896d3f7d3bda8e47a6ce1 - languageName: node - linkType: hard - -"react-router@npm:6.30.4, react-router@npm:^6.30.4": - version: 6.30.4 - resolution: "react-router@npm:6.30.4" - dependencies: - "@remix-run/router": "npm:1.23.3" - peerDependencies: - react: ">=16.8" - checksum: 10/59c1b64a210cde2f1d069ffc47dce2d58991bd4d81bb0743e22ba7c6b742e63ebbdc976c42c3a3ebd90bf38a2f05258bd0efa82c162b259be000d39787716043 + react: ">=18" + react-dom: ">=18" + peerDependenciesMeta: + react-dom: + optional: true + checksum: 10/f507876822281499759abc53b3230375f706aa45c66b9ba271308f7420501ff1e5430443c560028299210efe1797141d2297c3bb6d09f3ef175220c1ae67a368 languageName: node linkType: hard @@ -6112,6 +6074,13 @@ __metadata: languageName: node linkType: hard +"set-cookie-parser@npm:^2.6.0": + version: 2.7.2 + resolution: "set-cookie-parser@npm:2.7.2" + checksum: 10/4b6f5ec4e3fa1aef471d9207117704d217ba6bb6443400b41f5ea945c4a7f6fc08e405a122c1a32b4ebde41f06dea75e02c2af87cee9abb27f3e3fe911e5839b + languageName: node + linkType: hard + "set-function-length@npm:^1.2.1, set-function-length@npm:^1.2.2": version: 1.2.2 resolution: "set-function-length@npm:1.2.2" From 6bddf3dee821a36362395623bba175033b1e386c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20G=C3=B3mez?= Date: Mon, 27 Jul 2026 11:03:31 +0200 Subject: [PATCH 2/2] fix: hero-to-navbar transition not working after router upgrade --- webui/CHANGELOG.md | 1 + webui/src/context/search/search-context.tsx | 19 ++++++- .../context/search/search-view-transition.ts | 54 +++++++++++++++++++ webui/src/pages/home/hero-search.tsx | 38 +++++-------- 4 files changed, 85 insertions(+), 27 deletions(-) create mode 100644 webui/src/context/search/search-view-transition.ts diff --git a/webui/CHANGELOG.md b/webui/CHANGELOG.md index 76397919f..ea0e1957b 100644 --- a/webui/CHANGELOG.md +++ b/webui/CHANGELOG.md @@ -29,6 +29,7 @@ This change log covers only the frontend library (webui) of Open VSX. ### Fixed - Refresh the extension version list in the delete views when a delete fails with a conflict +- Keep the hero-to-navbar search morph working under react-router 7, which wraps location updates in `React.startTransition` ### Dependencies diff --git a/webui/src/context/search/search-context.tsx b/webui/src/context/search/search-context.tsx index 5cde43715..43f000e52 100644 --- a/webui/src/context/search/search-context.tsx +++ b/webui/src/context/search/search-context.tsx @@ -11,10 +11,20 @@ * SPDX-License-Identifier: EPL-2.0 ********************************************************************************/ -import { createContext, FunctionComponent, ReactNode, useContext, useEffect, useMemo, useState } from 'react'; -import { useSearchParams } from 'react-router'; +import { + createContext, + FunctionComponent, + ReactNode, + useContext, + useEffect, + useLayoutEffect, + useMemo, + useState +} from 'react'; +import { useLocation, useSearchParams } from 'react-router'; import { SearchFocusProvider } from './search-focus-context'; import { PageSearchBarProvider } from './page-search-bar-context'; +import { resolveSearchViewTransition } from './search-view-transition'; export interface SearchContextValue { query: string; @@ -45,6 +55,11 @@ export const SearchProvider: FunctionComponent<{ children: ReactNode }> = ({ chi const [query, setQuery] = useState(''); const [searchParams] = useSearchParams(); const urlQuery = searchParams.get('q') ?? ''; + const location = useLocation(); + + // This provider outlives search navigations, so it releases the hero → nav bar + // morph once the new route has committed. + useLayoutEffect(resolveSearchViewTransition, [location]); // Keep the fields in sync with URL query changes (back/forward, shared links, category tiles). useEffect(() => { diff --git a/webui/src/context/search/search-view-transition.ts b/webui/src/context/search/search-view-transition.ts new file mode 100644 index 000000000..165f521e1 --- /dev/null +++ b/webui/src/context/search/search-view-transition.ts @@ -0,0 +1,54 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +/** Release a transition whose navigation never landed, rather than suspend rendering forever. */ +const COMMIT_TIMEOUT_MS = 250; + +let pendingCommit: (() => void) | undefined; + +/** + * Runs `update` inside a view transition, holding it open until React has + * committed the resulting navigation. react-router wraps location updates in + * `React.startTransition`, which `flushSync` cannot force, so the browser would + * otherwise snapshot the new state while the old route was still mounted and the + * morph would animate an element to its own position. + */ +export function startSearchViewTransition(update: () => void): ViewTransition | undefined { + if (typeof document.startViewTransition !== 'function') { + update(); + return undefined; + } + + return document.startViewTransition( + () => + new Promise(resolve => { + let timeout = 0; + const release = () => { + window.clearTimeout(timeout); + pendingCommit = undefined; + resolve(); + }; + timeout = window.setTimeout(release, COMMIT_TIMEOUT_MS); + pendingCommit = release; + update(); + }) + ); +} + +/** + * Lets the browser snapshot the new state. Call from a component that outlives the + * navigation — the field that starts the morph unmounts as part of it. + */ +export function resolveSearchViewTransition(): void { + pendingCommit?.(); +} diff --git a/webui/src/pages/home/hero-search.tsx b/webui/src/pages/home/hero-search.tsx index 2c3ca9ad6..b6bd7be71 100644 --- a/webui/src/pages/home/hero-search.tsx +++ b/webui/src/pages/home/hero-search.tsx @@ -23,11 +23,11 @@ import { } from 'react'; import { Box, ButtonBase, Container, Typography } from '@mui/material'; import { useLocation } from 'react-router'; -import { flushSync } from 'react-dom'; import { styled, alpha } from '@mui/material/styles'; import { accentHover, focusOutline, focusRing } from '../../components/page-primitives'; import { useSearch, SEARCH_DEBOUNCE_MS } from '../../hooks/use-search'; import { useSearchQuery } from '../../context/search/search-context'; +import { startSearchViewTransition } from '../../context/search/search-view-transition'; import { useSearchFocus } from '../../context/search/search-focus-context'; import { useRegisterPageSearchBar } from '../../context/search/page-search-bar-context'; import { useSignalEffect } from '../../hooks/use-signal-effect'; @@ -167,36 +167,24 @@ export const HeroSearch: FunctionComponent = ({ }, [locationKey, searchFocusSignal.emit]); // Wrap search calls with a view transition so the hero input morphs into the nav bar. - type ViewTransitionDocument = Document & { - startViewTransition?: (callback: () => void) => { finished: Promise }; - }; - const searchWithTransition = useCallback( (q: string) => { // Sync context immediately so the nav bar input value is ready before the // navigation commits (the hero input morphs into the nav field mid-transition). setQuery(q); const shouldFocus = Boolean(q); - // flushSync inside the transition commits the navigation and the focus signal - // synchronously, so the nav field takes focus while the hero input is still - // focused — keeping the mobile keyboard open across the morph. - const go = () => { - flushSync(() => { - search({ query: q }); - if (shouldFocus) searchFocusSignal.emit(); - }); - }; - const doc = document as ViewTransitionDocument; - if (doc.startViewTransition) { - const transition = doc.startViewTransition(go); - // Re-issue the focus request once the transition settles as a fallback: - // the synchronous focus above can be interrupted by the morph, and by now - // the nav field is fully mounted and interactive. - if (shouldFocus) { - transition.finished.then(searchFocusSignal.emit).catch(() => undefined); - } - } else { - go(); + // Navigation and focus request land in the same commit, so the nav field takes + // focus while the hero input still has it — keeping the mobile keyboard open + // across the morph. + const transition = startSearchViewTransition(() => { + search({ query: q }); + if (shouldFocus) searchFocusSignal.emit(); + }); + // Re-issue the focus request once the transition settles as a fallback: the + // focus above can be interrupted by the morph, and by now the nav field is + // fully mounted and interactive. + if (shouldFocus) { + transition?.finished.then(searchFocusSignal.emit).catch(() => undefined); } }, [search, setQuery, searchFocusSignal.emit]