[18c52802] feat(panel): redirect /kanban to Tasks kanban tab, remove sidebar entry, swap bottom tab bar to Agents (#679)

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
This commit is contained in:
roboco-app[bot]
2026-07-24 03:06:57 +00:00
committed by GitHub
co-authored by Frontend Developer 1
parent d7888feed1
commit 2e9e05f38d
5 changed files with 61 additions and 133 deletions
@@ -0,0 +1,38 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, waitFor } from "@testing-library/react";
// /kanban and /kanban?view=X used to render the full kanban board; Stream3-A
// moved that board into the Tasks page's Kanban tab, so this route now only
// redirects there — this test locks in the redirect target for both the
// bare route and the view-preserving query-param case.
const { replace } = vi.hoisted(() => ({ replace: vi.fn() }));
let currentSearch = "";
vi.mock("next/navigation", () => ({
useRouter: () => ({ replace }),
useSearchParams: () => new URLSearchParams(currentSearch),
}));
import KanbanPage from "../page";
describe("KanbanPage redirect (Stream3-B)", () => {
beforeEach(() => {
replace.mockReset();
currentSearch = "";
});
it("redirects /kanban to /tasks?tab=kanban", async () => {
render(<KanbanPage />);
await waitFor(() =>
expect(replace).toHaveBeenCalledWith("/tasks?tab=kanban"),
);
});
it("redirects /kanban?view=qa to /tasks?tab=kanban&view=qa, preserving the view", async () => {
currentSearch = "view=qa";
render(<KanbanPage />);
await waitFor(() =>
expect(replace).toHaveBeenCalledWith("/tasks?tab=kanban&view=qa"),
);
});
});
+20 -123
View File
@@ -1,124 +1,32 @@
"use client";
import { Suspense } from "react";
import { Suspense, useEffect } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import {
DevKanban,
QaKanban,
PrReviewKanban,
PmKanban,
} from "@/components/kanban";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Skeleton } from "@/components/ui/skeleton";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { pickTab } from "@/lib/tabs";
import { Code, TestTube, GitPullRequest, ClipboardList } from "lucide-react";
type KanbanView = "dev" | "qa" | "pr-review" | "pm";
const KANBAN_VIEWS = ["dev", "qa", "pr-review", "pm"] as const satisfies readonly KanbanView[];
function KanbanPageContent() {
// The kanban views (dev/qa/pr-review/pm) now live as the Tasks page's Kanban
// tab (see (dashboard)/tasks/page.tsx). This route only exists so old links
// and bookmarks to /kanban and /kanban?view=qa keep working — it forwards
// straight to /tasks?tab=kanban(&view=X) and never renders any board itself.
function KanbanRedirect() {
const router = useRouter();
const searchParams = useSearchParams();
const view = searchParams.get("view");
// Read view from URL params; fall back to "dev" on null/empty/invalid.
const view: KanbanView = pickTab(searchParams.get("view"), KANBAN_VIEWS, "dev");
const handleViewChange = (newView: string) => {
if (newView === "dev") {
router.push("/kanban");
} else {
router.push(`/kanban?view=${newView}`);
}
};
useEffect(() => {
// replace (not push): the redirect itself shouldn't become a history
// entry a user has to hit "back" through.
router.replace(view ? `/tasks?tab=kanban&view=${view}` : "/tasks?tab=kanban");
}, [router, view]);
return (
<div className="space-y-6">
<Tabs value={view} onValueChange={handleViewChange}>
<TabsList>
{/* TooltipTrigger's asChild Slot merge clobbers TabsTrigger's own
data-state with the tooltip's — re-assert the real selection
state explicitly so data-[state=active] styling survives
(same fix as task-detail/task-tabs.tsx). */}
<Tooltip>
<TooltipTrigger asChild>
<TabsTrigger
value="dev"
data-state={view === "dev" ? "active" : "inactive"}
className="gap-2"
>
<Code className="h-4 w-4" />
Developer
</TabsTrigger>
</TooltipTrigger>
<TooltipContent>
Tasks claimed and worked by developers backlog through
completion
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<TabsTrigger
value="qa"
data-state={view === "qa" ? "active" : "inactive"}
className="gap-2"
>
<TestTube className="h-4 w-4" />
QA
</TabsTrigger>
</TooltipTrigger>
<TooltipContent>Quality assurance review workflow</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<TabsTrigger
value="pr-review"
data-state={view === "pr-review" ? "active" : "inactive"}
className="gap-2"
>
<GitPullRequest className="h-4 w-4" />
PR Review
</TabsTrigger>
</TooltipTrigger>
<TooltipContent>
In-path PR-review gate for assembled PRs, before the PM merges
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<TabsTrigger
value="pm"
data-state={view === "pm" ? "active" : "inactive"}
className="gap-2"
>
<ClipboardList className="h-4 w-4" />
PM
</TabsTrigger>
</TooltipTrigger>
<TooltipContent>
Project management overview every lifecycle state, including
recovery states
</TooltipContent>
</Tooltip>
</TabsList>
<TabsContent value="dev" className="mt-6">
<DevKanban />
</TabsContent>
<TabsContent value="qa" className="mt-6">
<QaKanban />
</TabsContent>
<TabsContent value="pr-review" className="mt-6">
<PrReviewKanban />
</TabsContent>
<TabsContent value="pm" className="mt-6">
<PmKanban />
</TabsContent>
</Tabs>
<Skeleton className="h-10 w-72" />
<div className="grid grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-96 w-full" />
))}
</div>
</div>
);
}
@@ -126,19 +34,8 @@ function KanbanPageContent() {
// Wrap in Suspense for useSearchParams
export default function KanbanPage() {
return (
<Suspense
fallback={
<div className="space-y-6">
<Skeleton className="h-10 w-72" />
<div className="grid grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-96 w-full" />
))}
</div>
</div>
}
>
<KanbanPageContent />
<Suspense fallback={<Skeleton className="h-96 w-full" />}>
<KanbanRedirect />
</Suspense>
);
}
@@ -52,7 +52,7 @@ export const QUICK_ACTIONS_REGISTRY: QuickAction[] = [
id: "kanban",
label: "Kanban",
icon: Kanban,
href: "/kanban",
href: "/tasks?tab=kanban",
tip: "Task board grouped by lifecycle status",
},
{
@@ -2,7 +2,7 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { LayoutDashboard, ListTodo, Kanban, Sparkles } from "lucide-react";
import { LayoutDashboard, ListTodo, Bot, Sparkles } from "lucide-react";
import { cn } from "@/lib/utils";
import { HelpTip } from "@/components/ui/help-tip";
import { navItems } from "./sidebar";
@@ -16,7 +16,7 @@ function tipFor(href: string): string {
const BOTTOM_NAV_ITEMS = [
{ title: "Overview", href: "/overview", icon: LayoutDashboard },
{ title: "Tasks", href: "/tasks", icon: ListTodo },
{ title: "Kanban", href: "/kanban", icon: Kanban },
{ title: "Agents", href: "/agents", icon: Bot },
{ title: "Chat", href: "/prompter", icon: Sparkles },
];
-7
View File
@@ -7,7 +7,6 @@ import { cn } from "@/lib/utils";
import {
LayoutDashboard,
ListTodo,
Kanban,
Activity,
ChevronLeft,
Settings,
@@ -54,12 +53,6 @@ export const navItems = [
icon: ListTodo,
tip: "Full task list — filter, search, and open any task's detail",
},
{
title: "Kanban",
href: "/kanban",
icon: Kanban,
tip: "Task board grouped by lifecycle status",
},
{
title: "Git",
href: "/git",