From 563e9ed4dff66060bbf6c4f181fd7d41b6affde7 Mon Sep 17 00:00:00 2001 From: 0xRoy <1997roylee@gmail.com> Date: Thu, 4 Jun 2026 00:33:07 +0800 Subject: [PATCH] Add Issues nav and normalize board statuses --- .../chat-room/chat-room-settings-sidebar.tsx | 6 +- .../chat-room/chat-room-sidebar-nav.tsx | 66 ++++++++++++++++ .../chat-room/chat-room-sidebar.tsx | 33 ++------ .../types/chat-room-sidebar.types.ts | 6 ++ .../issue-detail-editor-utils.tsx | 4 +- .../issues-board/issues-board-utils.ts | 25 +++--- .../issues-board/issues-board.constants.ts | 12 ++- .../web-shell/web-shell.constants.ts | 1 + .../tests/issue-detail-editor-utils.test.ts | 36 ++++++++- packages/web/tests/issues-board-utils.test.ts | 77 +++++++++++++++++++ .../web/tests/web-shell-navigation.test.ts | 9 +++ 11 files changed, 223 insertions(+), 52 deletions(-) create mode 100644 packages/web/src/components/chat-room/chat-room-sidebar-nav.tsx create mode 100644 packages/web/tests/issues-board-utils.test.ts diff --git a/packages/web/src/components/chat-room/chat-room-settings-sidebar.tsx b/packages/web/src/components/chat-room/chat-room-settings-sidebar.tsx index 7e569ebc..915f63d2 100644 --- a/packages/web/src/components/chat-room/chat-room-settings-sidebar.tsx +++ b/packages/web/src/components/chat-room/chat-room-settings-sidebar.tsx @@ -1,7 +1,10 @@ "use client"; +import { Button } from "@/components/ui/button"; +import { Typography } from "@/components/ui/typography"; import type { SidebarNavItem } from "@/components/web-shell/types/web-shell.types"; import { navItems } from "@/components/web-shell/web-shell.constants"; +import { cn } from "@/lib/utils"; import { ArrowLeft, BookOpen, @@ -23,9 +26,6 @@ import { import Link from "next/link"; import { usePathname } from "next/navigation"; import type { ComponentType, ReactElement } from "react"; -import { Button } from "@/components/ui/button"; -import { Typography } from "@/components/ui/typography"; -import { cn } from "@/lib/utils"; import type { ChatRoomSettingsSidebarProps } from "./types/chat-room.types"; const iconByKey: Record< diff --git a/packages/web/src/components/chat-room/chat-room-sidebar-nav.tsx b/packages/web/src/components/chat-room/chat-room-sidebar-nav.tsx new file mode 100644 index 00000000..4a7c9324 --- /dev/null +++ b/packages/web/src/components/chat-room/chat-room-sidebar-nav.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { ListChecks, Settings } from "lucide-react"; +import Link from "next/link"; +import type { ReactElement } from "react"; + +import { Button } from "@/components/ui/button"; +import { Typography } from "@/components/ui/typography"; +import { cn } from "@/lib/utils"; +import type { ChatRoomSidebarNavProps } from "./types/chat-room-sidebar.types"; + +export function ChatRoomSidebarNav({ + isCollapsed, + onCloseSidebar, + onSettingsClick, +}: ChatRoomSidebarNavProps): ReactElement { + return ( + + ); +} diff --git a/packages/web/src/components/chat-room/chat-room-sidebar.tsx b/packages/web/src/components/chat-room/chat-room-sidebar.tsx index 85c3bae0..edff56f8 100644 --- a/packages/web/src/components/chat-room/chat-room-sidebar.tsx +++ b/packages/web/src/components/chat-room/chat-room-sidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { MessageSquarePlus, Search, Settings, X } from "lucide-react"; +import { MessageSquarePlus, Search, X } from "lucide-react"; import { type ReactElement, useState } from "react"; import { Button } from "@/components/ui/button"; @@ -10,6 +10,7 @@ import { cn } from "@/lib/utils"; import { ChatRoomSessionList } from "./chat-room-session-list"; import { ChatRoomSettingsSidebar } from "./chat-room-settings-sidebar"; import { ChatRoomSidebarHeader } from "./chat-room-sidebar-header"; +import { ChatRoomSidebarNav } from "./chat-room-sidebar-nav"; import { buildChatSessionSidebarContent } from "./chat-room-sidebar-utils"; import type { ChatRoomSidebarProps } from "./types/chat-room-sidebar.types"; import type { ChatRoomSidebarView } from "./types/chat-room.types"; @@ -64,7 +65,6 @@ export function ChatRoomSidebar({ } function handleSettingsClick(): void { - console.log("sidebarView", sidebarView) if (isCollapsed) { onToggleCollapsed(); } @@ -199,30 +199,11 @@ export function ChatRoomSidebar({ onUnpinSession={unpinSession} /> - + void; } +export interface ChatRoomSidebarNavProps { + isCollapsed: boolean; + onCloseSidebar: () => void; + onSettingsClick: () => void; +} + export interface ChatRoomSessionListProps { activeSessionId: string; collapsedProjectIds: Set; diff --git a/packages/web/src/components/issues-board/issue-detail-editor-utils.tsx b/packages/web/src/components/issues-board/issue-detail-editor-utils.tsx index 6281a890..1823cac2 100644 --- a/packages/web/src/components/issues-board/issue-detail-editor-utils.tsx +++ b/packages/web/src/components/issues-board/issue-detail-editor-utils.tsx @@ -4,7 +4,7 @@ import type { ReactElement, ReactNode } from "react"; import { Typography } from "@/components/ui/typography"; import type { ProjectBoardTaskRecord, TaskMutationRequest } from "@/lib/api"; -import { normalizeDueDate } from "./issues-board-utils"; +import { normalizeBoardStatus, normalizeDueDate } from "./issues-board-utils"; import type { IssueDetailDraft } from "./types/issues-board.types"; export function DetailField({ @@ -53,7 +53,7 @@ export function createDetailDraft( title: task.title, content: task.content, priority: String(task.priority), - status: task.status, + status: normalizeBoardStatus(task.status), creatorId: task.creatorId, dueDate: toDateInputValue(task.dueDate), linkedPr: task.linkedPr ?? "", diff --git a/packages/web/src/components/issues-board/issues-board-utils.ts b/packages/web/src/components/issues-board/issues-board-utils.ts index 84c65408..e93e6b94 100644 --- a/packages/web/src/components/issues-board/issues-board-utils.ts +++ b/packages/web/src/components/issues-board/issues-board-utils.ts @@ -11,9 +11,8 @@ import { import type { IssueDraft, IssueTab } from "./types/issues-board.types"; const LEGACY_PR_CREATED_STATUS = "pr_created"; -const LEGACY_PLANNING_STATUS = "planning"; -const LEGACY_PLAN_STATUS = "todo"; -const LEGACY_IN_PROGRESS_STATUS = "implementing"; +const TODO_STATUSES = ["planning", "plan", "todo"] as const; +const RUNNING_STATUSES = ["implementing", "in_progress", "running"] as const; const LEGACY_REVIEW_STATUSES = ["reviewing", "testing"] as const; export function getStatusLabel(status: string): string { @@ -147,17 +146,17 @@ function normalizeIndex(index: number): number { } export function normalizeBoardStatus(status: string): string { - if (status === LEGACY_PLANNING_STATUS) { - return "plan"; + if ((TODO_STATUSES as readonly string[]).includes(status)) { + return "todo"; } - if (status === LEGACY_PLAN_STATUS) { - return "plan"; + if ((RUNNING_STATUSES as readonly string[]).includes(status)) { + return "running"; } - if (status === LEGACY_IN_PROGRESS_STATUS) { - return "in_progress"; - } - return status === LEGACY_PR_CREATED_STATUS || + if ( + status === LEGACY_PR_CREATED_STATUS || (LEGACY_REVIEW_STATUSES as readonly string[]).includes(status) - ? "in_review" - : status; + ) { + return "in_review"; + } + return status === "failed" ? "canceled" : status; } diff --git a/packages/web/src/components/issues-board/issues-board.constants.ts b/packages/web/src/components/issues-board/issues-board.constants.ts index f2bf8bbb..c102040c 100644 --- a/packages/web/src/components/issues-board/issues-board.constants.ts +++ b/packages/web/src/components/issues-board/issues-board.constants.ts @@ -8,19 +8,18 @@ export const DEFAULT_CREATOR_ID = "member-1"; export const STATUS_ORDER = [ "backlog", - "plan", - "in_progress", + "todo", + "running", "in_review", "done", "canceled", - "failed", ] as const; export const STATUS_PRESENTATION: Record = { backlog: { label: "Backlog", tone: "border-slate-700/70 bg-surface-panel" }, - plan: { label: "Plan", tone: "border-slate-700/70 bg-surface-panel" }, - in_progress: { - label: "In Progress", + todo: { label: "To Do", tone: "border-slate-700/70 bg-surface-panel" }, + running: { + label: "Running", tone: "border-yellow-900/50 bg-yellow-950/35", }, in_review: { @@ -29,7 +28,6 @@ export const STATUS_PRESENTATION: Record = { }, done: { label: "Done", tone: "border-indigo-900/50 bg-indigo-950/35" }, canceled: { label: "Canceled", tone: "border-zinc-700/70 bg-zinc-950/35" }, - failed: { label: "Failed", tone: "border-red-900/50 bg-red-950/35" }, }; export const PRIORITY_OPTIONS: readonly PriorityOption[] = [ diff --git a/packages/web/src/components/web-shell/web-shell.constants.ts b/packages/web/src/components/web-shell/web-shell.constants.ts index 7a3c774b..c96d0b93 100644 --- a/packages/web/src/components/web-shell/web-shell.constants.ts +++ b/packages/web/src/components/web-shell/web-shell.constants.ts @@ -7,6 +7,7 @@ import type { export const navItems: SidebarNavItem[] = [ { key: "agents", label: "Agents", href: "/agents" }, { key: "projects", label: "Projects", href: "/projects" }, + { key: "issues", label: "Issues", href: "/issues" }, { key: "integrations", label: "Integrations", href: "/integrations" }, { key: "git", label: "Git", href: "/git" }, { key: "usage", label: "Usage", href: "/usage" }, diff --git a/packages/web/tests/issue-detail-editor-utils.test.ts b/packages/web/tests/issue-detail-editor-utils.test.ts index 1e4b44f3..5d043cb7 100644 --- a/packages/web/tests/issue-detail-editor-utils.test.ts +++ b/packages/web/tests/issue-detail-editor-utils.test.ts @@ -1,8 +1,21 @@ import { describe, expect, it } from "bun:test"; -import { createSaveRequest } from "../src/components/issues-board/issue-detail-editor-utils"; +import { + createDetailDraft, + createSaveRequest, +} from "../src/components/issues-board/issue-detail-editor-utils"; +import type { ProjectBoardTaskRecord } from "../src/lib/api"; describe("issue detail editor utilities", () => { + it("normalizes existing task status values for the issue status picker", () => { + expect(createDetailDraft(buildTask({ status: "plan" })).status).toBe( + "todo", + ); + expect(createDetailDraft(buildTask({ status: "in_progress" })).status).toBe( + "running", + ); + }); + it("allows saving tasks with empty description content", () => { const result = createSaveRequest( { @@ -32,3 +45,24 @@ describe("issue detail editor utilities", () => { }); }); }); + +function buildTask( + overrides: Partial = {}, +): ProjectBoardTaskRecord { + return { + id: "task-1", + taskKey: "TASK-1", + projectId: "project-1", + title: "Task", + content: "Do the work", + priority: 1, + status: "backlog", + dueDate: null, + creatorId: "member-1", + assigneeId: null, + linkedPr: null, + createdAt: "2026-06-01T00:00:00.000Z", + updatedAt: "2026-06-01T00:00:00.000Z", + ...overrides, + }; +} diff --git a/packages/web/tests/issues-board-utils.test.ts b/packages/web/tests/issues-board-utils.test.ts new file mode 100644 index 00000000..552efeca --- /dev/null +++ b/packages/web/tests/issues-board-utils.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from "bun:test"; + +import { + buildStatusColumns, + getStatusLabel, + normalizeBoardStatus, +} from "../src/components/issues-board/issues-board-utils"; +import { STATUS_ORDER } from "../src/components/issues-board/issues-board.constants"; +import type { ProjectBoardTaskRecord } from "../src/lib/api"; + +describe("issues board utilities", () => { + it("uses the requested issue status columns in order", () => { + expect([...STATUS_ORDER]).toEqual([ + "backlog", + "todo", + "running", + "in_review", + "done", + "canceled", + ]); + }); + + it("normalizes existing task statuses into the six board columns", () => { + const columns = buildStatusColumns([ + buildTask({ id: "task-plan", status: "plan" }), + buildTask({ id: "task-progress", status: "in_progress" }), + buildTask({ id: "task-failed", status: "failed" }), + buildTask({ id: "task-reviewing", status: "reviewing" }), + ]); + + expect(columns.map((column) => column.status)).toEqual([ + "backlog", + "todo", + "running", + "in_review", + "done", + "canceled", + ]); + expect(columns.find((column) => column.status === "todo")?.tasks).toEqual([ + expect.objectContaining({ id: "task-plan", status: "todo" }), + ]); + expect( + columns.find((column) => column.status === "running")?.tasks, + ).toEqual([ + expect.objectContaining({ id: "task-progress", status: "running" }), + ]); + expect( + columns.find((column) => column.status === "canceled")?.tasks, + ).toEqual([ + expect.objectContaining({ id: "task-failed", status: "canceled" }), + ]); + expect(normalizeBoardStatus("todo")).toBe("todo"); + expect(getStatusLabel("running")).toBe("Running"); + expect(getStatusLabel("plan")).toBe("To Do"); + }); +}); + +function buildTask( + overrides: Partial = {}, +): ProjectBoardTaskRecord { + return { + id: "task-1", + taskKey: "TASK-1", + projectId: "project-1", + title: "Task", + content: "Do the work", + priority: 1, + status: "backlog", + dueDate: null, + creatorId: "member-1", + assigneeId: null, + linkedPr: null, + createdAt: "2026-06-01T00:00:00.000Z", + updatedAt: "2026-06-01T00:00:00.000Z", + ...overrides, + }; +} diff --git a/packages/web/tests/web-shell-navigation.test.ts b/packages/web/tests/web-shell-navigation.test.ts index 0f00b4ce..71e1be22 100644 --- a/packages/web/tests/web-shell-navigation.test.ts +++ b/packages/web/tests/web-shell-navigation.test.ts @@ -14,4 +14,13 @@ describe("web shell navigation", () => { }); expect(hrefForNavKey("git")).toBe("/git"); }); + + it("shows Issues in workspace navigation and routes it to the task board", () => { + expect(navItems).toContainEqual({ + key: "issues", + label: "Issues", + href: "/issues", + }); + expect(hrefForNavKey("issues")).toBe("/issues"); + }); });