{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() {
|