Fix project sidebar stuck on the report page.

Open finished projects on the hub instead of /report, and hard-navigate when leaving nested report/progress/placement routes for Dashboard, Library, and hub tabs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-13 16:22:16 +02:00
co-authored by Cursor
parent 4c4b604cca
commit d947128951
4 changed files with 78 additions and 37 deletions
+7
View File
@@ -2,6 +2,13 @@
What's new in Pinscope. What's new in Pinscope.
## 2.28.2 — 2026-09-13 — Fix project sidebar navigation
Opening a finished project no longer dumps you on the report with a stuck left menu. Hub first; Report stays in the sidebar. Leaving `/report` for BOM/tabs uses a full navigation so the page actually changes.
- [Fixed] Dashboard project cards/table open `/project/{id}` (not `/report`).
- [Fixed] Sidebar Dashboard / Library / BOM tabs escape nested report/progress/placement routes.
## 2.28.1 — 2026-09-12 — Placement pipeline (parallel) ## 2.28.1 — 2026-09-12 — Placement pipeline (parallel)
Dedicated Placement job builds the routing-first topology plan without touching the analysis pipeline status or spending credits. No millimetres — domains, IC groups, satellites only. Dedicated Placement job builds the routing-first topology plan without touching the analysis pipeline status or spending credits. No millimetres — domains, IC groups, satellites only.
@@ -155,8 +155,9 @@ export function ProjectCard({
); );
} }
// Open the report by default — that's what users want to see for a finished // Open the project hub by default so BOM / derating / settings in the
// project. In-flight or paused runs go to the progress page where the SSE // sidebar work immediately. Report stays one click away in ProjectNav.
// In-flight or paused runs go to the progress page where the SSE
// stepper and resume controls live. // stepper and resume controls live.
const inFlight = const inFlight =
project.status === "running" project.status === "running"
@@ -164,6 +165,6 @@ export function ProjectCard({
|| project.status === "paused_by_user"; || project.status === "paused_by_user";
const href = inFlight const href = inFlight
? `/project/${project.id}/progress` ? `/project/${project.id}/progress`
: `/project/${project.id}/report`; : `/project/${project.id}`;
return <Link href={href}>{card}</Link>; return <Link href={href}>{card}</Link>;
} }
@@ -105,7 +105,7 @@ function ProjectRow({
|| project.status === "paused_by_user"; || project.status === "paused_by_user";
const href = inFlight const href = inFlight
? `/project/${project.id}/progress` ? `/project/${project.id}/progress`
: `/project/${project.id}/report`; : `/project/${project.id}`;
const nameCell = ( const nameCell = (
<div className="flex items-center gap-1.5 min-w-0"> <div className="flex items-center gap-1.5 min-w-0">
+66 -33
View File
@@ -2,7 +2,7 @@
import Link from "next/link"; import Link from "next/link";
import { usePathname, useSearchParams } from "next/navigation"; import { usePathname, useSearchParams } from "next/navigation";
import { useState, useEffect } from "react"; import { Suspense, useState, useEffect, type ReactNode } from "react";
import { import {
LayoutDashboard, LayoutDashboard,
Cpu, Cpu,
@@ -77,7 +77,14 @@ export function Sidebar() {
<div className="flex-1 min-h-0 overflow-y-auto flex flex-col"> <div className="flex-1 min-h-0 overflow-y-auto flex flex-col">
{projectId ? ( {projectId ? (
<ProjectNav pathname={pathname} projectId={projectId} project={project} isAdmin={isAdmin} /> <Suspense fallback={<nav className="flex-1 px-2 py-3" aria-hidden />}>
<ProjectNav
pathname={pathname}
projectId={projectId}
project={project}
isAdmin={isAdmin}
/>
</Suspense>
) : ( ) : (
<DefaultNav pathname={pathname} isAdmin={isAdmin} /> <DefaultNav pathname={pathname} isAdmin={isAdmin} />
)} )}
@@ -185,6 +192,37 @@ const PROJECT_NAV_ITEMS: NavItem[] = [
{ type: "tab", tab: "settings", label: "Settings", icon: Settings }, { type: "tab", tab: "settings", label: "Settings", icon: Settings },
]; ];
function NavLink({
href,
active,
children,
forceDocument,
}: {
href: string;
active?: boolean;
children: ReactNode;
forceDocument?: boolean;
}) {
const className = cn(
"flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors",
active
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-accent/50"
);
if (forceDocument) {
return (
<a href={href} className={className}>
{children}
</a>
);
}
return (
<Link href={href} className={className}>
{children}
</Link>
);
}
function ProjectNav({ function ProjectNav({
pathname, pathname,
projectId, projectId,
@@ -201,11 +239,18 @@ function ProjectNav({
const currentTab = searchParams.get("tab"); const currentTab = searchParams.get("tab");
const isRunning = project?.status === "running"; const isRunning = project?.status === "running";
const isOnProgress = pathname === `${base}/progress`; const isOnProgress = pathname === `${base}/progress`;
// Nested routes (report / progress / placement): soft-nav to `?tab=` can
// leave the report page mounted — use a full document navigation instead.
const onNestedRoute = pathname.startsWith(`${base}/`);
function isActive(item: NavItem): boolean { function isActive(item: NavItem): boolean {
if (item.type === "route") { if (item.type === "route") {
return pathname === `${base}${item.path}` && !currentTab; return pathname === `${base}${item.path}` && !currentTab;
} }
// Hub with no tab defaults to BOM in the project page.
if (item.tab === "bom") {
return pathname === base && (currentTab === "bom" || currentTab === null);
}
return pathname === base && currentTab === item.tab; return pathname === base && currentTab === item.tab;
} }
@@ -216,20 +261,14 @@ function ProjectNav({
return ( return (
<nav className="flex-1 px-2 py-3 space-y-1"> <nav className="flex-1 px-2 py-3 space-y-1">
<Link <NavLink href="/dashboard" forceDocument={onNestedRoute}>
href="/dashboard"
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
>
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
Dashboard Dashboard
</Link> </NavLink>
<Link <NavLink href="/library" forceDocument={onNestedRoute}>
href="/library"
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
>
<Library className="h-4 w-4" /> <Library className="h-4 w-4" />
Library Library
</Link> </NavLink>
<div className="px-3 pt-3 pb-1"> <div className="px-3 pt-3 pb-1">
<p className="text-xs font-semibold text-foreground truncate"> <p className="text-xs font-semibold text-foreground truncate">
@@ -239,22 +278,16 @@ function ProjectNav({
<div className="space-y-0.5"> <div className="space-y-0.5">
{isRunning && ( {isRunning && (
<Link <NavLink href={`${base}/progress`} active={isOnProgress}>
href={`${base}/progress`}
className={cn(
"flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors",
isOnProgress
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-accent/50"
)}
>
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
Processing Processing
</Link> </NavLink>
)} )}
{PROJECT_NAV_ITEMS.filter((item) => !item.adminOnly || isAdmin).map((item) => { {PROJECT_NAV_ITEMS.filter((item) => !item.adminOnly || isAdmin).map((item) => {
const active = isActive(item); const active = isActive(item);
const disabled = isRunning; const disabled = isRunning;
const href = getHref(item);
const forceDocument = item.type === "tab" && onNestedRoute;
return disabled ? ( return disabled ? (
<span <span
key={item.label} key={item.label}
@@ -262,23 +295,23 @@ function ProjectNav({
> >
<item.icon className="h-4 w-4" /> <item.icon className="h-4 w-4" />
{item.label} {item.label}
{item.adminOnly && <Shield className="h-3 w-3 ml-auto text-amber-600/60 dark:text-amber-500/60" />} {item.adminOnly && (
<Shield className="h-3 w-3 ml-auto text-amber-600/60 dark:text-amber-500/60" />
)}
</span> </span>
) : ( ) : (
<Link <NavLink
key={item.label} key={item.label}
href={getHref(item)} href={href}
className={cn( active={active}
"flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors", forceDocument={forceDocument}
active
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-accent/50"
)}
> >
<item.icon className="h-4 w-4" /> <item.icon className="h-4 w-4" />
{item.label} {item.label}
{item.adminOnly && <Shield className="h-3 w-3 ml-auto text-amber-600/60 dark:text-amber-500/60" />} {item.adminOnly && (
</Link> <Shield className="h-3 w-3 ml-auto text-amber-600/60 dark:text-amber-500/60" />
)}
</NavLink>
); );
})} })}
</div> </div>