FN-6135: remove workflow toolbar collapse toggle

Simplify the workflow board toolbar by removing its collapse state and persistence.

- remove the workflow toolbar collapse toggle, handlers, and scoped localStorage persistence from Board
- drop collapse-specific workflow toolbar styling and mobile collapsed-layout rules
- replace collapse-focused board tests with assertions that the toolbar renders without a collapse affordance
- remove the unused workflow toolbar storage key and update project storage expectations

Files changed:
 packages/dashboard/app/components/Board.tsx        | 129 +++++---------
 packages/dashboard/app/components/Lane.css         |  36 ----
 .../app/components/__tests__/Board.test.tsx        | 187 +++------------------
 .../app/utils/__tests__/projectStorage.test.ts     |   3 +-
 packages/dashboard/app/utils/projectStorage.ts     |   1 -
 5 files changed, 60 insertions(+), 296 deletions(-)

Fusion-Task-Id: FN-6135

Fusion-Task-Lineage: 11c17edf-bbed-4ec5-a06e-00a2537ffac1
This commit is contained in:
gsxdsm
2026-06-09 16:02:08 -07:00
parent 733489b010
commit d67f61d03e
5 changed files with 60 additions and 296 deletions

View File

@@ -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<string, string>, 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<boolean>(() => getScopedItem("kb-dashboard-board-workflow-collapsed", projectId) === "1");
const archivedLoadedRef = useRef(false);
const [workflowStepNameLookup, setWorkflowStepNameLookup] = useState<ReadonlyMap<string, string>>(EMPTY_WORKFLOW_STEP_NAME_LOOKUP);
const boardRef = useRef<HTMLElement | null>(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<HTMLElement>) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
toggleWorkflowToolbarCollapsed();
}
}, [toggleWorkflowToolbarCollapsed]);
const handleExpandedWorkflowToolbarClick = useCallback((event: MouseEvent<HTMLDivElement>) => {
if (event.target === event.currentTarget) {
toggleWorkflowToolbarCollapsed();
}
}, [toggleWorkflowToolbarCollapsed]);
useEffect(() => {
recordResumeEvent({
view: "Board",
@@ -490,71 +465,45 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
return (
<div className="board-workflow-view">
{(workflowOptions.length > 1 || onCreateWorkflow || onOpenWorkflowEditor) && (
<div
className="board-workflow-toolbar"
data-collapsed={workflowToolbarCollapsed || undefined}
onClick={workflowToolbarCollapsed ? undefined : handleExpandedWorkflowToolbarClick}
onKeyDown={workflowToolbarCollapsed ? undefined : handleWorkflowToolbarKeyDown}
tabIndex={workflowToolbarCollapsed ? undefined : 0}
aria-expanded={workflowToolbarCollapsed ? undefined : true}
aria-label={workflowToolbarCollapsed ? undefined : "Collapse workflow toolbar"}
data-testid={workflowToolbarCollapsed ? undefined : "board-workflow-collapse-toggle"}
>
{workflowToolbarCollapsed ? (
<span
className="board-workflow-collapsed-label"
role="button"
tabIndex={0}
onClick={toggleWorkflowToolbarCollapsed}
onKeyDown={handleWorkflowToolbarKeyDown}
aria-expanded={false}
aria-label="Expand workflow toolbar"
data-testid="board-workflow-collapse-toggle"
<div className="board-workflow-toolbar">
{workflowOptions.length > 1 && (
<label className="list-workflow-selector board-workflow-selector">
<span>Workflow</span>
<select
className="select list-workflow-select"
value={selectedWorkflow.id}
onChange={(event) => setSelectedWorkflowId(event.target.value)}
aria-label="Select workflow"
>
{workflowOptions.map((workflow) => (
<option key={workflow.id} value={workflow.id}>
{workflow.name}
</option>
))}
</select>
</label>
)}
{onOpenWorkflowEditor && (
<button
type="button"
className="btn btn-icon btn-sm board-workflow-edit-btn"
onClick={() => onOpenWorkflowEditor(selectedWorkflow.id)}
title="Edit workflows"
aria-label="Edit workflows"
>
Workflow
</span>
) : (
<>
{workflowOptions.length > 1 && (
<label className="list-workflow-selector board-workflow-selector">
<span>Workflow</span>
<select
className="select list-workflow-select"
value={selectedWorkflow.id}
onChange={(event) => setSelectedWorkflowId(event.target.value)}
aria-label="Select workflow"
>
{workflowOptions.map((workflow) => (
<option key={workflow.id} value={workflow.id}>
{workflow.name}
</option>
))}
</select>
</label>
)}
{onOpenWorkflowEditor && (
<button
type="button"
className="btn btn-icon btn-sm board-workflow-edit-btn"
onClick={() => onOpenWorkflowEditor(selectedWorkflow.id)}
title="Edit workflows"
aria-label="Edit workflows"
>
<Pencil size={15} />
</button>
)}
{onCreateWorkflow && (
<button
type="button"
className="btn btn-icon btn-sm board-workflow-create-btn"
onClick={onCreateWorkflow}
title="New workflow"
aria-label="New workflow"
>
<Plus size={15} />
</button>
)}
</>
<Pencil size={15} />
</button>
)}
{onCreateWorkflow && (
<button
type="button"
className="btn btn-icon btn-sm board-workflow-create-btn"
onClick={onCreateWorkflow}
title="New workflow"
aria-label="New workflow"
>
<Plus size={15} />
</button>
)}
</div>
)}

View File

@@ -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;
}
}

View File

@@ -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 () => {

View File

@@ -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", () => {

View File

@@ -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",