diff --git a/packages/dashboard/app/components/Board.tsx b/packages/dashboard/app/components/Board.tsx index 9a2211a94c..4502759b59 100644 --- a/packages/dashboard/app/components/Board.tsx +++ b/packages/dashboard/app/components/Board.tsx @@ -4,13 +4,12 @@ import { sortTasksForDisplayColumn } from "./taskSorting"; import { Column } from "./Column"; import "./Lane.css"; import type { ToastType } from "../hooks/useToast"; -import { useState, useMemo, useEffect, useCallback, useRef, type KeyboardEvent, type MouseEvent } from "react"; +import { useState, useMemo, useEffect, useCallback, useRef } from "react"; import { Pencil, Plus } from "lucide-react"; import { fetchWorkflowSteps, fetchBoardWorkflows, promoteTask, type ModelInfo, type BoardWorkflowDefinition, type BoardWorkflowsPayload } from "../api"; import { useBlockerFanout } from "../hooks/useBlockerFanout"; import { MOBILE_MEDIA_QUERY } from "../hooks/useViewportMode"; import { recordResumeEvent } from "../utils/resumeInstrumentation"; -import { getScopedItem, setScopedItem } from "../utils/projectStorage"; import { subscribeSse } from "../sse-bus"; import { getBoardCanDropTaskRejection } from "./boardCanDropTask"; @@ -93,7 +92,6 @@ function areWorkflowNameLookupsEqual(previous: ReadonlyMap, next export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask, onOpenDetail, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, onLoadArchivedTasks, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, taskStuckTimeoutMs, onOpenMission, staleHighFanoutBlockerAgeThresholdMs, lastFetchTimeMs, prAuthAvailable, onOpenWorkflowEditor, onCreateWorkflow }: BoardProps) { const [archivedCollapsed, setArchivedCollapsed] = useState(true); - const [workflowToolbarCollapsed, setWorkflowToolbarCollapsed] = useState(() => getScopedItem("kb-dashboard-board-workflow-collapsed", projectId) === "1"); const archivedLoadedRef = useRef(false); const [workflowStepNameLookup, setWorkflowStepNameLookup] = useState>(EMPTY_WORKFLOW_STEP_NAME_LOOKUP); const boardRef = useRef(null); @@ -111,29 +109,6 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask archived: [], }); - useEffect(() => { - if (typeof window !== "undefined") { - setScopedItem("kb-dashboard-board-workflow-collapsed", workflowToolbarCollapsed ? "1" : "0", projectId); - } - }, [workflowToolbarCollapsed, projectId]); - - const toggleWorkflowToolbarCollapsed = useCallback(() => { - setWorkflowToolbarCollapsed((collapsed) => !collapsed); - }, []); - - const handleWorkflowToolbarKeyDown = useCallback((event: KeyboardEvent) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - toggleWorkflowToolbarCollapsed(); - } - }, [toggleWorkflowToolbarCollapsed]); - - const handleExpandedWorkflowToolbarClick = useCallback((event: MouseEvent) => { - if (event.target === event.currentTarget) { - toggleWorkflowToolbarCollapsed(); - } - }, [toggleWorkflowToolbarCollapsed]); - useEffect(() => { recordResumeEvent({ view: "Board", @@ -490,71 +465,45 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask return (
{(workflowOptions.length > 1 || onCreateWorkflow || onOpenWorkflowEditor) && ( -
- {workflowToolbarCollapsed ? ( - + {workflowOptions.length > 1 && ( + + )} + {onOpenWorkflowEditor && ( + - )} - {onCreateWorkflow && ( - - )} - + + + )} + {onCreateWorkflow && ( + )}
)} diff --git a/packages/dashboard/app/components/Lane.css b/packages/dashboard/app/components/Lane.css index 240bf16f36..5cd16313ea 100644 --- a/packages/dashboard/app/components/Lane.css +++ b/packages/dashboard/app/components/Lane.css @@ -36,34 +36,6 @@ background: var(--surface); } -.board-workflow-toolbar[data-collapsed] { - justify-content: flex-start; - gap: var(--space-sm); - padding: var(--space-xs) var(--space-md); -} - -.board-workflow-toolbar:not([data-collapsed]), -.board-workflow-collapsed-label { - cursor: pointer; -} - -.board-workflow-toolbar:focus-visible, -.board-workflow-collapsed-label:focus-visible { - outline: none; - box-shadow: var(--focus-ring-strong); -} - -.board-workflow-collapsed-label { - border-radius: var(--radius-sm); - color: var(--text-muted); - font-size: 0.8rem; -} - -.board-workflow-collapsed-label:hover, -.board-workflow-collapsed-label:focus-visible { - color: var(--text); -} - .board-workflow-selector { margin-left: auto; } @@ -199,11 +171,3 @@ scroll-snap-type: x proximity; } } - -@media (max-width: 768px) { - .board-workflow-toolbar[data-collapsed] { - flex-direction: row; - flex-wrap: nowrap; - align-items: center; - } -} diff --git a/packages/dashboard/app/components/__tests__/Board.test.tsx b/packages/dashboard/app/components/__tests__/Board.test.tsx index d11cf98e8f..172a44b499 100644 --- a/packages/dashboard/app/components/__tests__/Board.test.tsx +++ b/packages/dashboard/app/components/__tests__/Board.test.tsx @@ -72,8 +72,6 @@ vi.mock("../Lane", () => ({ ), })); -const BOARD_WORKFLOW_COLLAPSED_KEY = "kb-dashboard-board-workflow-collapsed"; - const DEFAULT_WORKFLOW = { id: "builtin:coding", name: "Coding (built-in)", @@ -1047,173 +1045,28 @@ describe("Board", () => { expect(onOpenWorkflowEditor).toHaveBeenCalledTimes(1); }); - describe("workflow toolbar collapsible", () => { - function renderWorkflowToolbarBoard() { - const onCreateWorkflow = vi.fn(); - const onOpenWorkflowEditor = vi.fn(); - enableFlag( - { "FN-1": "builtin:coding", "FN-2": "wf-custom" }, - [DEFAULT_WORKFLOW, CUSTOM_WORKFLOW], - ); - renderBoard({ - tasks: [mkTask({ id: "FN-1" }), mkTask({ id: "FN-2", column: "intake" })], - onCreateWorkflow, - onOpenWorkflowEditor, - }); - return { onCreateWorkflow, onOpenWorkflowEditor }; - } - - function expectNoEmptyWorkflowToolbarIconButtons() { - const toolbar = document.querySelector(".board-workflow-toolbar"); - expect(toolbar).not.toBeNull(); - const emptyIconButtons = Array.from(toolbar?.querySelectorAll("button.btn-icon") ?? []) - .filter((button) => !button.querySelector("svg") && button.textContent?.trim() === ""); - expect(emptyIconButtons).toHaveLength(0); - expect(toolbar?.querySelector("button.board-workflow-collapse-toggle")).toBeNull(); - } - - it("defaults to expanded (not collapsed)", async () => { - renderWorkflowToolbarBoard(); - - expect(await screen.findByLabelText("Select workflow")).toBeDefined(); - expect(screen.getByRole("button", { name: "New workflow" })).toBeDefined(); - expect(screen.getByRole("button", { name: "Edit workflows" })).toBeDefined(); - expect(screen.getByTestId("board-workflow-collapse-toggle").getAttribute("aria-expanded")).toBe("true"); + it("renders workflow toolbar actions without a collapse affordance", async () => { + const onCreateWorkflow = vi.fn(); + const onOpenWorkflowEditor = vi.fn(); + enableFlag( + { "FN-1": "builtin:coding", "FN-2": "wf-custom" }, + [DEFAULT_WORKFLOW, CUSTOM_WORKFLOW], + ); + renderBoard({ + tasks: [mkTask({ id: "FN-1" }), mkTask({ id: "FN-2", column: "intake" })], + onCreateWorkflow, + onOpenWorkflowEditor, }); - it("does not render a chevron icon in the workflow toolbar collapse toggle", async () => { - renderWorkflowToolbarBoard(); - - await screen.findByLabelText("Select workflow"); - const toolbar = document.querySelector(".board-workflow-toolbar"); - expect(toolbar?.querySelector(".lucide-chevron-left, .lucide-chevron-right")).toBeNull(); - }); - - it("does not render an empty btn-icon button in the workflow toolbar", async () => { - renderWorkflowToolbarBoard(); - - await screen.findByLabelText("Select workflow"); - expectNoEmptyWorkflowToolbarIconButtons(); - - fireEvent.click(screen.getByTestId("board-workflow-collapse-toggle")); - expect(screen.getByText("Workflow")).toBeDefined(); - expectNoEmptyWorkflowToolbarIconButtons(); - }); - - it("clicking the toggle collapses the toolbar", async () => { - renderWorkflowToolbarBoard(); - await screen.findByLabelText("Select workflow"); - - fireEvent.click(screen.getByTestId("board-workflow-collapse-toggle")); - - expect(screen.queryByLabelText("Select workflow")).toBeNull(); - expect(screen.queryByRole("button", { name: "New workflow" })).toBeNull(); - expect(screen.queryByRole("button", { name: "Edit workflows" })).toBeNull(); - expect(screen.getByTestId("board-workflow-collapse-toggle").getAttribute("aria-expanded")).toBe("false"); - expect(screen.getByText("Workflow")).toBeDefined(); - }); - - it("clicking the toggle again expands the toolbar", async () => { - renderWorkflowToolbarBoard(); - await screen.findByLabelText("Select workflow"); - - fireEvent.click(screen.getByTestId("board-workflow-collapse-toggle")); - expect(screen.queryByLabelText("Select workflow")).toBeNull(); - fireEvent.click(screen.getByTestId("board-workflow-collapse-toggle")); - - expect(screen.getByLabelText("Select workflow")).toBeDefined(); - expect(screen.getByRole("button", { name: "New workflow" })).toBeDefined(); - expect(screen.getByRole("button", { name: "Edit workflows" })).toBeDefined(); - expect(screen.getByTestId("board-workflow-collapse-toggle").getAttribute("aria-expanded")).toBe("true"); - }); - - it("toggles the workflow toolbar from the keyboard", async () => { - renderWorkflowToolbarBoard(); - await screen.findByLabelText("Select workflow"); - - fireEvent.keyDown(screen.getByTestId("board-workflow-collapse-toggle"), { key: "Enter" }); - expect(screen.queryByLabelText("Select workflow")).toBeNull(); - expect(screen.getByTestId("board-workflow-collapse-toggle").getAttribute("aria-expanded")).toBe("false"); - - fireEvent.keyDown(screen.getByTestId("board-workflow-collapse-toggle"), { key: " " }); - expect(screen.getByLabelText("Select workflow")).toBeDefined(); - expect(screen.getByTestId("board-workflow-collapse-toggle").getAttribute("aria-expanded")).toBe("true"); - }); - - it("persists collapsed state to localStorage", async () => { - renderWorkflowToolbarBoard(); - await screen.findByLabelText("Select workflow"); - - fireEvent.click(screen.getByTestId("board-workflow-collapse-toggle")); - expect(window.localStorage.getItem(BOARD_WORKFLOW_COLLAPSED_KEY)).toBe("1"); - - fireEvent.click(screen.getByTestId("board-workflow-collapse-toggle")); - expect(window.localStorage.getItem(BOARD_WORKFLOW_COLLAPSED_KEY)).toBe("0"); - }); - - it("reads collapsed state from localStorage on mount", async () => { - window.localStorage.setItem(BOARD_WORKFLOW_COLLAPSED_KEY, "1"); - - renderWorkflowToolbarBoard(); - await waitFor(() => expect(screen.getByTestId("board-workflow-collapse-toggle")).toBeDefined()); - - expect(screen.queryByLabelText("Select workflow")).toBeNull(); - expect(screen.queryByRole("button", { name: "New workflow" })).toBeNull(); - expect(screen.queryByRole("button", { name: "Edit workflows" })).toBeNull(); - expect(screen.getByTestId("board-workflow-collapse-toggle").getAttribute("aria-expanded")).toBe("false"); - expect(screen.getByText("Workflow")).toBeDefined(); - }); - - it("collapses and expands a single-workflow toolbar with action callbacks", async () => { - const onCreateWorkflow = vi.fn(); - const onOpenWorkflowEditor = vi.fn(); - enableFlag({ "FN-1": "builtin:coding" }, [DEFAULT_WORKFLOW]); - renderBoard({ - tasks: [mkTask({ id: "FN-1", column: "triage" })], - onCreateWorkflow, - onOpenWorkflowEditor, - }); - - await waitFor(() => expect(screen.getByTestId("column-triage")).toBeDefined()); - expect(screen.queryByLabelText("Select workflow")).toBeNull(); - expect(screen.getByRole("button", { name: "New workflow" })).toBeDefined(); - expect(screen.getByRole("button", { name: "Edit workflows" })).toBeDefined(); - expectNoEmptyWorkflowToolbarIconButtons(); - - fireEvent.click(screen.getByTestId("board-workflow-collapse-toggle")); - expect(screen.queryByRole("button", { name: "New workflow" })).toBeNull(); - expect(screen.queryByRole("button", { name: "Edit workflows" })).toBeNull(); - expect(screen.getByTestId("board-workflow-collapse-toggle").getAttribute("aria-expanded")).toBe("false"); - expectNoEmptyWorkflowToolbarIconButtons(); - - fireEvent.click(screen.getByTestId("board-workflow-collapse-toggle")); - expect(screen.getByRole("button", { name: "New workflow" })).toBeDefined(); - expect(screen.getByRole("button", { name: "Edit workflows" })).toBeDefined(); - }); - - it("collapses and expands a multi-workflow toolbar without action callbacks", async () => { - enableFlag( - { "FN-1": "builtin:coding", "FN-2": "wf-custom" }, - [DEFAULT_WORKFLOW, CUSTOM_WORKFLOW], - ); - renderBoard({ - tasks: [mkTask({ id: "FN-1" }), mkTask({ id: "FN-2", column: "intake" })], - onCreateWorkflow: undefined, - onOpenWorkflowEditor: undefined, - }); - - expect(await screen.findByLabelText("Select workflow")).toBeDefined(); - expect(screen.queryByRole("button", { name: "New workflow" })).toBeNull(); - expect(screen.queryByRole("button", { name: "Edit workflows" })).toBeNull(); - expectNoEmptyWorkflowToolbarIconButtons(); - - fireEvent.click(screen.getByTestId("board-workflow-collapse-toggle")); - expect(screen.queryByLabelText("Select workflow")).toBeNull(); - expect(screen.getByTestId("board-workflow-collapse-toggle").getAttribute("aria-expanded")).toBe("false"); - - fireEvent.click(screen.getByTestId("board-workflow-collapse-toggle")); - expect(screen.getByLabelText("Select workflow")).toBeDefined(); - }); + expect(await screen.findByLabelText("Select workflow")).toBeDefined(); + expect(screen.getByRole("button", { name: "New workflow" })).toBeDefined(); + expect(screen.getByRole("button", { name: "Edit workflows" })).toBeDefined(); + const toolbar = document.querySelector(".board-workflow-toolbar"); + expect(toolbar).not.toBeNull(); + expect(toolbar?.hasAttribute("data-collapsed")).toBe(false); + expect(toolbar?.querySelector(".board-workflow-collapse-toggle")).toBeNull(); + expect(toolbar?.querySelector(".board-workflow-collapsed-label")).toBeNull(); + expect(screen.queryByTestId("board-workflow-collapse-toggle")).toBeNull(); }); it("renders one selected workflow at a time and switches workflows from the dropdown", async () => { diff --git a/packages/dashboard/app/utils/__tests__/projectStorage.test.ts b/packages/dashboard/app/utils/__tests__/projectStorage.test.ts index efb370805f..aef6e5fa2e 100644 --- a/packages/dashboard/app/utils/__tests__/projectStorage.test.ts +++ b/packages/dashboard/app/utils/__tests__/projectStorage.test.ts @@ -84,7 +84,6 @@ describe("projectStorage", () => { "kb-dashboard-list-selected-task", "kb-dashboard-list-sidebar-width", "kb-dashboard-mailbox-sidebar-width", - "kb-dashboard-board-workflow-collapsed", "kb-quick-entry-text", "kb-inline-create-text", "fn-agent-view", @@ -104,7 +103,7 @@ describe("projectStorage", () => { "fusion-plugin-dependency-graph:positions", ]), ); - expect(PROJECT_STORAGE_KEYS).toHaveLength(26); + expect(PROJECT_STORAGE_KEYS).toHaveLength(25); }); it("stores branch filter values as scoped strings per project", () => { diff --git a/packages/dashboard/app/utils/projectStorage.ts b/packages/dashboard/app/utils/projectStorage.ts index a7d6455598..a7418c9a6d 100644 --- a/packages/dashboard/app/utils/projectStorage.ts +++ b/packages/dashboard/app/utils/projectStorage.ts @@ -17,7 +17,6 @@ export const PROJECT_STORAGE_KEYS: string[] = [ "kb-dashboard-list-selected-task", "kb-dashboard-list-sidebar-width", "kb-dashboard-mailbox-sidebar-width", - "kb-dashboard-board-workflow-collapsed", "kb-quick-entry-text", "kb-inline-create-text", "fn-agent-view",