diff --git a/webui2/package.json b/webui2/package.json index 0c7a515fbe5ce1a72ed5329495ec5ef23ec92aa9..347b178d009f0518d56f2228c23e4657eb706d36 100644 --- a/webui2/package.json +++ b/webui2/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@apollo/client": "^4.1.6", + "@tanstack/react-router": "^1.168.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -26,7 +27,6 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-markdown": "^10.1.0", - "react-router": "^7.13.2", "rehype-autolink-headings": "^7.1.0", "rehype-external-links": "^3.0.0", "rehype-raw": "^7.0.0", diff --git a/webui2/pnpm-lock.yaml b/webui2/pnpm-lock.yaml index f4a88b0d814093730a2e6469c53c18110fb6d6fb..3129899040a9bc69ceb2b6c305a4c2802f59928a 100644 --- a/webui2/pnpm-lock.yaml +++ b/webui2/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@apollo/client': specifier: ^4.1.6 version: 4.1.6(graphql-ws@6.0.8(graphql@16.13.2)(ws@8.20.0))(graphql@16.13.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2) + '@tanstack/react-router': + specifier: ^1.168.8 + version: 1.168.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -41,9 +44,6 @@ importers: react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.4) - react-router: - specifier: ^7.13.2 - version: 7.13.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) rehype-autolink-headings: specifier: ^7.1.0 version: 7.1.0 @@ -1847,6 +1847,31 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tanstack/history@1.161.6': + resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==} + engines: {node: '>=20.19'} + + '@tanstack/react-router@1.168.8': + resolution: {integrity: sha512-t0S0QueXubBKmI9eLPcN/A1sLQgTu8/yHerjrvvsGeD12zMdw0uJPKwEKpStQF2OThQtw64cs34uUSYXBUTSNw==} + engines: {node: '>=20.19'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.9.3': + resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-core@1.168.7': + resolution: {integrity: sha512-z4UEdlzMrFaKBsG4OIxlZEm+wsYBtEp//fnX6kW18jhQpETNcM6u2SXNdX+bcIYp6AaR7ERS3SBENzjC/xxwQQ==} + engines: {node: '>=20.19'} + hasBin: true + + '@tanstack/store@0.9.3': + resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -2087,9 +2112,8 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie@1.1.1: - resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} - engines: {node: '>=18'} + cookie-es@2.0.0: + resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} @@ -2470,6 +2494,10 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} + isbot@5.1.36: + resolution: {integrity: sha512-C/ZtXyJqDPZ7G7JPr06ApWyYoHjYexQbS6hPYD4WYCzpv2Qes6Z+CCEfTX4Owzf+1EJ933PoI2p+B9v7wpGZBQ==} + engines: {node: '>=18'} + isomorphic-ws@5.0.0: resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} peerDependencies: @@ -2961,16 +2989,6 @@ packages: '@types/react': optional: true - react-router@7.13.2: - resolution: {integrity: sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: '>=18' - react-dom: '>=18' - peerDependenciesMeta: - react-dom: - optional: true - react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -3072,8 +3090,15 @@ packages: sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} - set-cookie-parser@2.7.2: - resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + seroval-plugins@1.5.1: + resolution: {integrity: sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.1: + resolution: {integrity: sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA==} + engines: {node: '>=10'} shell-quote@1.8.3: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} @@ -5163,6 +5188,33 @@ snapshots: tailwindcss: 4.2.2 vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(jiti@2.6.1)(yaml@2.8.3) + '@tanstack/history@1.161.6': {} + + '@tanstack/react-router@1.168.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/history': 1.161.6 + '@tanstack/react-store': 0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/router-core': 1.168.7 + isbot: 5.1.36 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@tanstack/react-store@0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/store': 0.9.3 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) + + '@tanstack/router-core@1.168.7': + dependencies: + '@tanstack/history': 1.161.6 + cookie-es: 2.0.0 + seroval: 1.5.1 + seroval-plugins: 1.5.1(seroval@1.5.1) + + '@tanstack/store@0.9.3': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -5403,7 +5455,7 @@ snapshots: convert-source-map@2.0.0: {} - cookie@1.1.1: {} + cookie-es@2.0.0: {} cosmiconfig@8.3.6(typescript@6.0.2): dependencies: @@ -5778,6 +5830,8 @@ snapshots: is-windows@1.0.2: {} + isbot@5.1.36: {} + isomorphic-ws@5.0.0(ws@8.20.0): dependencies: ws: 8.20.0 @@ -6556,14 +6610,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - react-router@7.13.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - cookie: 1.1.1 - react: 19.2.4 - set-cookie-parser: 2.7.2 - optionalDependencies: - react-dom: 19.2.4(react@19.2.4) - react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4): dependencies: get-nonce: 1.0.1 @@ -6718,7 +6764,11 @@ snapshots: tslib: 2.8.1 upper-case-first: 2.0.2 - set-cookie-parser@2.7.2: {} + seroval-plugins@1.5.1(seroval@1.5.1): + dependencies: + seroval: 1.5.1 + + seroval@1.5.1: {} shell-quote@1.8.3: {} diff --git a/webui2/src/App.tsx b/webui2/src/App.tsx index 46d5242c2ca42453dcfdda837410b22543b1644f..790dabc4ffc2507ac816846e923fb24f74a19666 100644 --- a/webui2/src/App.tsx +++ b/webui2/src/App.tsx @@ -1,4 +1,4 @@ -import { createBrowserRouter, RouterProvider } from "react-router"; +import { createRootRoute, createRoute, createRouter, RouterProvider } from "@tanstack/react-router"; import { Shell } from "@/components/layout/Shell"; import { RepoShell } from "@/lib/repo"; @@ -12,35 +12,103 @@ import { NewBugPage } from "@/pages/NewBugPage"; import { RepoPickerPage } from "@/pages/RepoPickerPage"; import { UserProfilePage } from "@/pages/UserProfilePage"; -// Route structure: -// / → repo picker (or redirect if single repo) -// /:repo → code browser (repo home) -// /:repo/issues → issue list -// /auth/select-identity → OAuth identity adoption (first-time login) -const router = createBrowserRouter([ - { - path: "/", - element: , - errorElement: , - children: [ - { index: true, element: }, - { path: "auth/select-identity", element: }, - { - path: ":repo", - element: , - children: [ - { index: true, element: }, - { path: "issues", element: }, - { path: "issues/new", element: }, - { path: "issues/:id", element: }, - { path: "user/:id", element: }, - { path: "commit/:hash", element: }, - ], - }, - ], - }, +// ── Route tree ─────────────────────────────────────────────────────────────── + +const rootRoute = createRootRoute({ + component: Shell, + errorComponent: ErrorPage, +}); + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/", + component: RepoPickerPage, +}); + +const authSelectIdentityRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/auth/select-identity", + component: IdentitySelectPage, +}); + +const repoRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/$repo", + component: RepoShell, +}); + +export type CodePageSearch = { + ref: string; + path: string; + type: "tree" | "blob" | "commits"; +}; + +const repoIndexRoute = createRoute({ + getParentRoute: () => repoRoute, + path: "/", + component: CodePage, + validateSearch: (search: Record): CodePageSearch => ({ + ref: (search.ref as string) ?? "", + path: (search.path as string) ?? "", + type: ["tree", "blob", "commits"].includes(search.type as string) + ? (search.type as CodePageSearch["type"]) + : "tree", + }), +}); + +const bugListRoute = createRoute({ + getParentRoute: () => repoRoute, + path: "/issues", + component: BugListPage, +}); + +const newBugRoute = createRoute({ + getParentRoute: () => repoRoute, + path: "/issues/new", + component: NewBugPage, +}); + +const bugDetailRoute = createRoute({ + getParentRoute: () => repoRoute, + path: "/issues/$id", + component: BugDetailPage, +}); + +const userProfileRoute = createRoute({ + getParentRoute: () => repoRoute, + path: "/user/$id", + component: UserProfilePage, +}); + +const commitRoute = createRoute({ + getParentRoute: () => repoRoute, + path: "/commit/$hash", + component: CommitPage, +}); + +const routeTree = rootRoute.addChildren([ + indexRoute, + authSelectIdentityRoute, + repoRoute.addChildren([ + repoIndexRoute, + bugListRoute, + newBugRoute, + bugDetailRoute, + userProfileRoute, + commitRoute, + ]), ]); +// ── Router instance ────────────────────────────────────────────────────────── + +const router = createRouter({ routeTree }); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + export function App() { return ; } diff --git a/webui2/src/components/bugs/BugRow.tsx b/webui2/src/components/bugs/BugRow.tsx index c0ad08dd3ad5e8d656e4823f7db0416efc644ac1..f5f6fd1976e3d01ad3a6a5e778b5b7630d898228 100644 --- a/webui2/src/components/bugs/BugRow.tsx +++ b/webui2/src/components/bugs/BugRow.tsx @@ -1,6 +1,6 @@ +import { Link } from "@tanstack/react-router"; import { formatDistanceToNow } from "date-fns"; import { MessageSquare, CircleDot, CircleCheck } from "lucide-react"; -import { Link } from "react-router"; import { Status } from "@/__generated__/graphql"; diff --git a/webui2/src/components/bugs/Timeline.tsx b/webui2/src/components/bugs/Timeline.tsx index 722fe82ebcfc42c7f4c4b60ca3b824a09e37c25a..7a10da2469f98a5e4ba44aaf6dc25d88714faa3b 100644 --- a/webui2/src/components/bugs/Timeline.tsx +++ b/webui2/src/components/bugs/Timeline.tsx @@ -1,7 +1,7 @@ +import { Link } from "@tanstack/react-router"; import { formatDistanceToNow } from "date-fns"; import { Tag, GitPullRequestClosed, Pencil, CircleDot } from "lucide-react"; import { useState } from "react"; -import { Link } from "react-router"; import { Status, @@ -98,7 +98,8 @@ function CommentItem({ item, bugPrefix }: { item: CommentItem; bugPrefix: string
{item.author.displayName} @@ -177,7 +178,8 @@ function LabelChangeItem({ item }: { item: LabelChangeItem }) { }> {item.author.displayName} @@ -219,7 +221,8 @@ function StatusChangeItem({ item }: { item: StatusChangeItem }) { > {item.author.displayName} @@ -237,7 +240,8 @@ function TitleChangeItem({ item }: { item: TitleChangeItem }) { }> {item.author.displayName} diff --git a/webui2/src/components/code/CommitList.tsx b/webui2/src/components/code/CommitList.tsx index 7a956ce1cd539affa21b0cb533adcd96d6a86736..ef756e3dc0cc3835a23b178d4a909a72f0bcfac1 100644 --- a/webui2/src/components/code/CommitList.tsx +++ b/webui2/src/components/code/CommitList.tsx @@ -3,10 +3,10 @@ import { gql } from "@apollo/client"; import { useQuery } from "@apollo/client/react"; +import { Link } from "@tanstack/react-router"; import { formatDistanceToNow } from "date-fns"; import { GitCommit } from "lucide-react"; import { useEffect, useState } from "react"; -import { Link } from "react-router"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; diff --git a/webui2/src/components/code/FileTree.tsx b/webui2/src/components/code/FileTree.tsx index 690998fed84d5cef4081c764c6e2086fda077a87..6b847c1bb250d5de61a69d9567d14e0cb86146e2 100644 --- a/webui2/src/components/code/FileTree.tsx +++ b/webui2/src/components/code/FileTree.tsx @@ -1,6 +1,6 @@ +import { Link } from "@tanstack/react-router"; import { formatDistanceToNow } from "date-fns"; import { Folder, File } from "lucide-react"; -import { Link } from "react-router"; import type { GitTreeEntry } from "@/__generated__/graphql"; import { Skeleton } from "@/components/ui/skeleton"; @@ -84,9 +84,8 @@ function FileTreeRow({ {entry.lastCommit && ( e.stopPropagation()} > diff --git a/webui2/src/components/layout/Header.tsx b/webui2/src/components/layout/Header.tsx index 154b65822fbc9522ea30c724a0c09d0602911247..b6504a1f408cdbf42ab22525fcd1c3256dec2788 100644 --- a/webui2/src/components/layout/Header.tsx +++ b/webui2/src/components/layout/Header.tsx @@ -6,8 +6,8 @@ // In external mode, shows a "Sign in" button when logged out and a sign-out // action when logged in. +import { Link, useMatchRoute, useParams } from "@tanstack/react-router"; import { Bug, Plus, Sun, Moon, LogIn, LogOut } from "lucide-react"; -import { Link, useMatch, NavLink } from "react-router"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; @@ -34,15 +34,21 @@ export function Header() { const { user, mode, loginProviders } = useAuth(); const { theme, toggle } = useTheme(); - // Detect if we're inside a /:repo route and grab the slug. - // useMatch works from any component in the tree, unlike useParams which is - // scoped to the nearest Route element. - const repoMatch = useMatch({ path: "/:repo/*", end: false }); - const repo = repoMatch?.params.repo ?? null; + // Detect if we're inside a /$repo route and grab the slug. + const params = useParams({ strict: false }); + const repo = params.repo ?? null; // Don't show repo nav on the /auth/* pages. const effectiveRepo = repo === "auth" ? null : repo; + const matchRoute = useMatchRoute(); + const isCodeActive = effectiveRepo + ? !!matchRoute({ to: "/$repo", params: { repo: effectiveRepo }, fuzzy: false }) + : false; + const isIssuesActive = effectiveRepo + ? !!matchRoute({ to: "/$repo/issues", params: { repo: effectiveRepo }, fuzzy: true }) + : false; + return (
@@ -55,33 +61,31 @@ export function Header() { {/* Repo-scoped nav links — only shown when inside a repo */} {effectiveRepo && ( )} @@ -107,12 +111,12 @@ export function Header() { {user && effectiveRepo && ( <> - + diff --git a/webui2/src/components/layout/Shell.tsx b/webui2/src/components/layout/Shell.tsx index e7b31fc73372bc00d87526afea306de2ffed0ba2..9c509ba863ace75a127bef959011b347215a7da1 100644 --- a/webui2/src/components/layout/Shell.tsx +++ b/webui2/src/components/layout/Shell.tsx @@ -1,4 +1,4 @@ -import { Outlet } from "react-router"; +import { Outlet } from "@tanstack/react-router"; import { Header } from "./Header"; diff --git a/webui2/src/lib/repo.tsx b/webui2/src/lib/repo.tsx index 49de30c9697e45bcbef9425d2453c8dfa0a2fd66..c4f688714f35695b72ac00ef1c96f1e749db132d 100644 --- a/webui2/src/lib/repo.tsx +++ b/webui2/src/lib/repo.tsx @@ -1,20 +1,20 @@ -// Provides the current repository slug (the :repo URL segment) to all -// components rendered inside a /:repo/* route. +// Provides the current repository slug (the $repo URL segment) to all +// components rendered inside a /$repo/* route. // // Usage: -// - Wrap the /:repo route subtree with as the route element. +// - Wrap the /$repo route subtree with as the route element. // - Read the current slug in any child component with useRepo(). // - Pass the slug as `ref` to all GraphQL repository queries. +import { Outlet, useParams } from "@tanstack/react-router"; import { createContext, useContext } from "react"; -import { useParams, Outlet } from "react-router"; const RepoContext = createContext(null); -// Route element for /:repo routes. Reads the :repo param and provides it +// Route element for /$repo routes. Reads the $repo param and provides it // via context so any descendant can call useRepo() without prop drilling. export function RepoShell() { - const { repo } = useParams<{ repo: string }>(); + const { repo } = useParams({ strict: false }); return ( @@ -23,7 +23,7 @@ export function RepoShell() { } // Returns the current repo slug from the nearest RepoShell ancestor. -// Returns null when rendered outside of a /:repo route (e.g. the picker page). +// Returns null when rendered outside of a /$repo route (e.g. the picker page). export function useRepo(): string | null { return useContext(RepoContext); } diff --git a/webui2/src/pages/BugDetailPage.tsx b/webui2/src/pages/BugDetailPage.tsx index de7307345843b6f1248ab9f024638be738db885e..bf7ac1acd446db8d6616f0254d6c538c5c016514 100644 --- a/webui2/src/pages/BugDetailPage.tsx +++ b/webui2/src/pages/BugDetailPage.tsx @@ -1,6 +1,6 @@ +import { useParams, Link } from "@tanstack/react-router"; import { formatDistanceToNow } from "date-fns"; import { ArrowLeft } from "lucide-react"; -import { useParams, Link } from "react-router"; import { useBugDetailQuery } from "@/__generated__/graphql"; import { CommentBox } from "@/components/bugs/CommentBox"; @@ -16,7 +16,7 @@ import { useRepo } from "@/lib/repo"; // Issue detail page (/:repo/issues/:id). Shows title, status, timeline of // comments and events, and a sidebar with labels and participants. export function BugDetailPage() { - const { id } = useParams<{ id: string }>(); + const { id } = useParams({ strict: false }); const repo = useRepo(); const { data, loading, error } = useBugDetailQuery({ variables: { ref: repo, prefix: id! }, diff --git a/webui2/src/pages/CodePage.tsx b/webui2/src/pages/CodePage.tsx index 2700faced45f80a246b3a73727eb05704a74b01f..824bae1951833926ba8de5c0e08264b8c070bb6c 100644 --- a/webui2/src/pages/CodePage.tsx +++ b/webui2/src/pages/CodePage.tsx @@ -3,9 +3,9 @@ import { gql } from "@apollo/client"; import { useQuery } from "@apollo/client/react"; +import { useNavigate, useSearch } from "@tanstack/react-router"; import { AlertCircle, GitCommit } from "lucide-react"; import { useEffect } from "react"; -import { useSearchParams } from "react-router"; import type { GitRef, GitTreeEntry, GitBlob, GitLastCommit } from "@/__generated__/graphql"; import { CodeBreadcrumb } from "@/components/code/CodeBreadcrumb"; @@ -104,15 +104,14 @@ interface BlobQueryData { } | null; } -type ViewMode = "tree" | "blob" | "commits"; +import type { CodePageSearch } from "@/App"; + +type ViewMode = CodePageSearch["type"]; export function CodePage() { const repo = useRepo(); - const [searchParams, setSearchParams] = useSearchParams(); - - const currentRef = searchParams.get("ref") ?? ""; - const currentPath = searchParams.get("path") ?? ""; - const viewMode: ViewMode = (searchParams.get("type") as ViewMode) ?? "tree"; + const navigate = useNavigate({ from: "/$repo/" }); + const { ref: currentRef, path: currentPath, type: viewMode } = useSearch({ from: "/$repo/" }); const { data: refsData, @@ -125,16 +124,13 @@ export function CodePage() { // Set default ref from query result once loaded useEffect(() => { - if (refsLoading || refs.length === 0 || searchParams.get("ref")) return; + if (refsLoading || refs.length === 0 || currentRef) return; const defaultRef = refs.find((r: GitRef) => r.isDefault) ?? refs[0]; if (defaultRef) { - setSearchParams( - (prev) => { - prev.set("ref", defaultRef.shortName); - return prev; - }, - { replace: true }, - ); + void navigate({ + search: (prev) => ({ ...prev, ref: defaultRef.shortName }), + replace: true, + }); } }, [refsLoading, refs.length]); // eslint-disable-line react-hooks/exhaustive-deps @@ -182,32 +178,23 @@ export function CodePage() { const repoName = refsData?.repository?.name ?? repo ?? "default-repo"; - function navigate(path: string, type: ViewMode = "tree") { - setSearchParams((prev) => { - prev.set("path", path); - prev.set("type", type); - return prev; - }); + function navigateTo(path: string, type: ViewMode = "tree") { + void navigate({ search: (prev) => ({ ...prev, path, type }) }); } function handleEntryClick(entry: TreeEntryWithCommit) { const newPath = currentPath ? `${currentPath}/${entry.name}` : entry.name; - navigate(newPath, entry.type === "BLOB" ? "blob" : "tree"); + navigateTo(newPath, entry.type === "BLOB" ? "blob" : "tree"); } function handleNavigateUp() { const parts = currentPath.split("/").filter(Boolean); parts.pop(); - navigate(parts.join("/"), "tree"); + navigateTo(parts.join("/"), "tree"); } function handleRefSelect(ref: GitRef) { - setSearchParams((prev) => { - prev.set("ref", ref.shortName); - prev.set("path", ""); - prev.set("type", "tree"); - return prev; - }); + void navigate({ search: { ref: ref.shortName, path: "", type: "tree" } }); } if (refsError) { @@ -230,7 +217,7 @@ export function CodePage() { repoName={repoName} ref={currentRef} path={currentPath} - onNavigate={(p) => navigate(p, "tree")} + onNavigate={(p) => navigateTo(p, "tree")} /> )}
@@ -238,7 +225,7 @@ export function CodePage() { +
+ + +
); } diff --git a/webui2/src/pages/NewBugPage.tsx b/webui2/src/pages/NewBugPage.tsx index 4ffbd3db1c842d1601861a3c6b91912ac012b662..17353f7915403a268ace7739a60e7b963d15a61d 100644 --- a/webui2/src/pages/NewBugPage.tsx +++ b/webui2/src/pages/NewBugPage.tsx @@ -1,6 +1,6 @@ +import { useNavigate, Link } from "@tanstack/react-router"; import { ArrowLeft } from "lucide-react"; import { useState } from "react"; -import { useNavigate, Link } from "react-router"; import { useBugCreateMutation } from "@/__generated__/graphql"; import { Markdown } from "@/components/content/Markdown"; @@ -27,7 +27,7 @@ export function NewBugPage() { }); const humanId = result.data?.bugCreate.bug.humanId; if (humanId) { - void navigate(repo ? `/${repo}/issues/${humanId}` : `/issues/${humanId}`); + void navigate({ to: repo ? `/${repo}/issues/${humanId}` : `/issues/${humanId}` }); } } @@ -115,7 +115,7 @@ export function NewBugPage() { type="button" variant="ghost" onClick={() => { - void navigate(issuesHref); + void navigate({ to: issuesHref }); }} disabled={loading} > diff --git a/webui2/src/pages/RepoPickerPage.tsx b/webui2/src/pages/RepoPickerPage.tsx index 489017cbc6d1cf346428cde237e6aee50facbce1..43179cf098362c6a5cbf375229a2b8cbee120c80 100644 --- a/webui2/src/pages/RepoPickerPage.tsx +++ b/webui2/src/pages/RepoPickerPage.tsx @@ -1,9 +1,9 @@ // Repository picker page (/). Auto-redirects when there is exactly one repo. // Shows a list when multiple repos are registered. +import { Link, useNavigate } from "@tanstack/react-router"; import { GitFork, FolderOpen, AlertCircle } from "lucide-react"; import { useEffect } from "react"; -import { Link, useNavigate } from "react-router"; import { useRepositoriesQuery } from "@/__generated__/graphql"; import { Skeleton } from "@/components/ui/skeleton"; @@ -23,7 +23,12 @@ export function RepoPickerPage() { // Auto-redirect when there is exactly one repo — no need to pick. useEffect(() => { if (data?.repositories.nodes.length === 1) { - void navigate("/" + repoSlug(data.repositories.nodes[0].name), { replace: true }); + void navigate({ + to: "/$repo", + params: { repo: repoSlug(data.repositories.nodes[0].name) }, + search: { ref: "", path: "", type: "tree" as const }, + replace: true, + }); } }, [data, navigate]); @@ -53,7 +58,9 @@ export function RepoPickerPage() { {data?.repositories.nodes.map((repo) => ( diff --git a/webui2/src/pages/UserProfilePage.tsx b/webui2/src/pages/UserProfilePage.tsx index 5530cf610afcec75048c4c1977a4f5c69bc57d07..63c0d2c5a4cc025217aed73776a599f69b6af69d 100644 --- a/webui2/src/pages/UserProfilePage.tsx +++ b/webui2/src/pages/UserProfilePage.tsx @@ -6,6 +6,7 @@ // The :id param is treated as a humanId prefix and passed directly to the // identity(prefix) and allBugs(query:"author:...") GraphQL arguments. +import { useParams, Link } from "@tanstack/react-router"; import { formatDistanceToNow } from "date-fns"; import { ArrowLeft, @@ -17,7 +18,6 @@ import { ChevronRight, } from "lucide-react"; import { useState } from "react"; -import { useParams, Link } from "react-router"; import { Status, useUserProfileQuery } from "@/__generated__/graphql"; import { LabelBadge } from "@/components/bugs/LabelBadge"; @@ -30,7 +30,7 @@ import { cn } from "@/lib/utils"; const PAGE_SIZE = 25; export function UserProfilePage() { - const { id } = useParams<{ id: string }>(); + const { id } = useParams({ strict: false }); const repo = useRepo(); const [statusFilter, setStatusFilter] = useState<"open" | "closed">("open"); @@ -91,12 +91,11 @@ export function UserProfilePage() { setCursors((prev) => prev.slice(0, -1)); } - const issuesHref = repo ? `/${repo}/issues` : "/issues"; - return (
@@ -214,7 +213,8 @@ export function UserProfilePage() {
{bug.title}