mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[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:
co-authored by
Frontend Developer 1
parent
d7888feed1
commit
2e9e05f38d
@@ -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"),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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,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",
|
||||
|
||||
Reference in New Issue
Block a user