FN-6080: remove workflow selector collapse controls

Always render the workflow selector expanded across the dashboard.

- remove collapsible workflow selector props, state, icons, and toggle styles
- stop InlineCreateCard from passing collapse persistence options
- delete project storage support and tests for collapsed workflow selector state
- add a patch changeset for the published CLI package

Files changed:
 .changeset/fn-6080-removal.md                      |  5 ++
 .../dashboard/app/components/InlineCreateCard.tsx  |  3 -
 .../dashboard/app/components/WorkflowSelector.css  | 14 +----
 .../dashboard/app/components/WorkflowSelector.tsx  | 65 +--------------------
 .../components/__tests__/InlineCreateCard.test.tsx | 50 ----------------
 .../components/__tests__/WorkflowSelector.test.tsx | 68 ----------------------
 .../app/utils/__tests__/projectStorage.test.ts     |  3 +-
 packages/dashboard/app/utils/projectStorage.ts     |  1 -
 8 files changed, 8 insertions(+), 201 deletions(-)

Fusion-Task-Id: FN-6080

Fusion-Task-Lineage: cca26111-9e20-47d0-8473-d18a3b488ae1
This commit is contained in:
gsxdsm
2026-06-09 08:14:16 -07:00
parent d2b98f14b2
commit a2f4bb14c0
8 changed files with 8 additions and 201 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Removed the `collapsible`, `collapseStorageKey`, and `collapsedLabel` props from `WorkflowSelector`. Callers should stop passing these props; workflow selectors now always render expanded.

View File

@@ -18,7 +18,6 @@ import { WorkflowSelector } from "./WorkflowSelector";
const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]; const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
const STORAGE_KEY = "kb-inline-create-text"; const STORAGE_KEY = "kb-inline-create-text";
const WORKFLOW_SELECTOR_COLLAPSE_STORAGE_KEY = "kb-board-workflow-selector-collapsed";
interface PendingImage { interface PendingImage {
file: File; file: File;
@@ -1037,8 +1036,6 @@ export function InlineCreateCard({
addToast={addToast} addToast={addToast}
label="Workflow" label="Workflow"
disabled={submitting} disabled={submitting}
collapsible
collapseStorageKey={WORKFLOW_SELECTOR_COLLAPSE_STORAGE_KEY}
/> />
<label className="inline-create-priority-wrap" htmlFor="inline-create-priority-select"> <label className="inline-create-priority-wrap" htmlFor="inline-create-priority-select">

View File

@@ -26,17 +26,6 @@
gap: var(--space-xs); gap: var(--space-xs);
} }
.workflow-selector-toggle {
flex-shrink: 0;
}
.workflow-selector-collapsed-button {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
white-space: nowrap;
}
.workflow-selector select { .workflow-selector select {
padding: var(--space-xs) var(--space-sm); padding: var(--space-xs) var(--space-sm);
background: var(--bg-secondary); background: var(--bg-secondary);
@@ -71,8 +60,7 @@
} }
.workflow-selector select, .workflow-selector select,
.workflow-selector-manage, .workflow-selector-manage {
.workflow-selector-collapsed-button {
width: 100%; width: 100%;
} }

View File

@@ -1,13 +1,12 @@
import "./WorkflowSelector.css"; import "./WorkflowSelector.css";
import { useCallback, useEffect, useId, useState } from "react"; import { useCallback, useEffect, useId, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ChevronDown, ChevronRight, Workflow as WorkflowIcon } from "lucide-react"; import { Workflow as WorkflowIcon } from "lucide-react";
import type { WorkflowDefinition } from "@fusion/core"; import type { WorkflowDefinition } from "@fusion/core";
import { getErrorMessage } from "@fusion/core"; import { getErrorMessage } from "@fusion/core";
import { fetchWorkflow, fetchWorkflows, fetchProjectDefaultWorkflow, setProjectDefaultWorkflow } from "../api"; import { fetchWorkflow, fetchWorkflows, fetchProjectDefaultWorkflow, setProjectDefaultWorkflow } from "../api";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";
import { useConfirm } from "../hooks/useConfirm"; import { useConfirm } from "../hooks/useConfirm";
import { getScopedItem, setScopedItem } from "../utils/projectStorage";
interface WorkflowSelectorProps { interface WorkflowSelectorProps {
/** Currently selected workflow id, or null for none. */ /** Currently selected workflow id, or null for none. */
@@ -20,12 +19,6 @@ interface WorkflowSelectorProps {
label?: string; label?: string;
/** Optional affordance to open the graph editor. */ /** Optional affordance to open the graph editor. */
onManage?: () => void; onManage?: () => void;
/** Render an opt-in collapsed/expanded shell around the selector. */
collapsible?: boolean;
/** Base projectStorage key used to persist collapsible state. */
collapseStorageKey?: string;
/** Label shown when the selector is collapsed. */
collapsedLabel?: string;
/** /**
* U9: when the task whose workflow is being switched has an active session, * U9: when the task whose workflow is being switched has an active session,
* switching aborts that session and re-homes the card into the new workflow's * switching aborts that session and re-homes the card into the new workflow's
@@ -43,9 +36,6 @@ export function WorkflowSelector({
disabled, disabled,
label = "Workflow", label = "Workflow",
onManage, onManage,
collapsible = false,
collapseStorageKey,
collapsedLabel = "Workflow",
hasActiveSession, hasActiveSession,
}: WorkflowSelectorProps) { }: WorkflowSelectorProps) {
const { t } = useTranslation("app"); const { t } = useTranslation("app");
@@ -54,29 +44,6 @@ export function WorkflowSelector({
const [workflows, setWorkflows] = useState<WorkflowDefinition[]>([]); const [workflows, setWorkflows] = useState<WorkflowDefinition[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [applying, setApplying] = useState(false); const [applying, setApplying] = useState(false);
const [collapsed, setCollapsed] = useState(() => {
if (!collapsible || !collapseStorageKey) return false;
return getScopedItem(collapseStorageKey, projectId) === "true";
});
useEffect(() => {
if (!collapsible || !collapseStorageKey) {
setCollapsed(false);
return;
}
setCollapsed(getScopedItem(collapseStorageKey, projectId) === "true");
}, [collapsible, collapseStorageKey, projectId]);
const setPersistedCollapsed = useCallback(
(nextCollapsed: boolean) => {
setCollapsed(nextCollapsed);
if (collapsible && collapseStorageKey) {
setScopedItem(collapseStorageKey, nextCollapsed ? "true" : "false", projectId);
}
},
[collapsible, collapseStorageKey, projectId],
);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -134,40 +101,10 @@ export function WorkflowSelector({
[onChange, addToast, hasActiveSession, confirm, t], [onChange, addToast, hasActiveSession, confirm, t],
); );
if (collapsible && collapsed) {
return (
<div className="workflow-selector workflow-selector--collapsed" data-testid="workflow-selector">
<button
type="button"
className="btn btn-sm workflow-selector-toggle workflow-selector-collapsed-button"
aria-expanded="false"
aria-controls={selectId}
onClick={() => setPersistedCollapsed(false)}
>
<ChevronRight size={14} aria-hidden />
<WorkflowIcon size={14} aria-hidden />
{collapsedLabel}
</button>
</div>
);
}
return ( return (
<div className="workflow-selector" data-testid="workflow-selector"> <div className="workflow-selector" data-testid="workflow-selector">
<div className="workflow-selector-label"> <div className="workflow-selector-label">
<div className="workflow-selector-label-text"> <div className="workflow-selector-label-text">
{collapsible && (
<button
type="button"
className="btn btn-icon btn-sm workflow-selector-toggle"
aria-expanded="true"
aria-controls={selectId}
aria-label={t("workflowSelector.collapse", "Collapse workflow selector")}
onClick={() => setPersistedCollapsed(true)}
>
<ChevronDown size={14} aria-hidden />
</button>
)}
<label htmlFor={selectId} className="workflow-selector-title"> <label htmlFor={selectId} className="workflow-selector-title">
<WorkflowIcon size={14} aria-hidden /> {label} <WorkflowIcon size={14} aria-hidden /> {label}
</label> </label>

View File

@@ -135,8 +135,6 @@ vi.mock("../../api", () => ({
const TEST_PROJECT_ID = "proj-123"; const TEST_PROJECT_ID = "proj-123";
const INLINE_CREATE_STORAGE_KEY = scopedKey("kb-inline-create-text", TEST_PROJECT_ID); const INLINE_CREATE_STORAGE_KEY = scopedKey("kb-inline-create-text", TEST_PROJECT_ID);
const WORKFLOW_SELECTOR_STORAGE_KEY = "kb-board-workflow-selector-collapsed";
const SCOPED_WORKFLOW_SELECTOR_STORAGE_KEY = scopedKey(WORKFLOW_SELECTOR_STORAGE_KEY, TEST_PROJECT_ID);
const MOCK_MODELS: ModelInfo[] = [ const MOCK_MODELS: ModelInfo[] = [
{ {
@@ -236,54 +234,6 @@ beforeEach(() => {
]); ]);
}); });
describe("InlineCreateCard workflow selector", () => {
it("opts the board inline-create workflow selector into collapsible mode by default", async () => {
renderCard();
expandCard();
expect(screen.getByRole("button", { name: /Collapse workflow selector/i })).toHaveAttribute("aria-expanded", "true");
expect(screen.getByRole("combobox", { name: /Workflow/i })).toBeDefined();
await waitFor(() => expect(screen.getByRole("option", { name: "Workflow A" })).toBeDefined());
});
it("restores persisted collapsed workflow selector state for the current project", () => {
localStorage.setItem(SCOPED_WORKFLOW_SELECTOR_STORAGE_KEY, "true");
renderCard();
expandCard();
const collapsedButton = screen.getByRole("button", { name: /^Workflow$/i });
expect(collapsedButton).toHaveAttribute("aria-expanded", "false");
expect(collapsedButton).toHaveClass("workflow-selector-collapsed-button");
expect(screen.queryByRole("combobox", { name: /Workflow/i })).toBeNull();
});
it("persists collapsed and expanded workflow selector state under the project-scoped key", () => {
renderCard();
expandCard();
fireEvent.click(screen.getByRole("button", { name: /Collapse workflow selector/i }));
expect(localStorage.getItem(SCOPED_WORKFLOW_SELECTOR_STORAGE_KEY)).toBe("true");
expect(screen.getByRole("button", { name: /^Workflow$/i })).toBeDefined();
fireEvent.click(screen.getByRole("button", { name: /^Workflow$/i }));
expect(localStorage.getItem(SCOPED_WORKFLOW_SELECTOR_STORAGE_KEY)).toBe("false");
expect(screen.getByRole("combobox", { name: /Workflow/i })).toBeDefined();
});
it("keeps project workflow selector preferences isolated between projects", () => {
localStorage.setItem(SCOPED_WORKFLOW_SELECTOR_STORAGE_KEY, "true");
renderCard([], { projectId: "other-project" });
expandCard();
expect(screen.getByRole("combobox", { name: /Workflow/i })).toBeDefined();
expect(screen.queryByRole("button", { name: /^Workflow$/i })).toBeNull();
});
});
describe("InlineCreateCard textarea width (FN-1608)", () => { describe("InlineCreateCard textarea width (FN-1608)", () => {
it("textarea spans full container width in board view", () => { it("textarea spans full container width in board view", () => {
renderCard(); renderCard();

View File

@@ -2,12 +2,9 @@ import React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { WorkflowSelector } from "../WorkflowSelector"; import { WorkflowSelector } from "../WorkflowSelector";
import { scopedKey } from "../../utils/projectStorage";
vi.mock("lucide-react", () => ({ vi.mock("lucide-react", () => ({
Workflow: () => null, Workflow: () => null,
ChevronDown: () => null,
ChevronRight: () => null,
})); }));
const fetchWorkflowsMock = vi.fn(); const fetchWorkflowsMock = vi.fn();
@@ -34,71 +31,6 @@ beforeEach(() => {
fetchWorkflowMock.mockResolvedValue({ id: "builtin:hidden", name: "Hidden built-in" }); fetchWorkflowMock.mockResolvedValue({ id: "builtin:hidden", name: "Hidden built-in" });
}); });
describe("WorkflowSelector collapsible mode", () => {
const storageKey = "kb-board-workflow-selector-collapsed";
it("defaults to expanded when no persisted collapsed state exists", async () => {
render(<WorkflowSelector value={null} onChange={vi.fn()} collapsible collapseStorageKey={storageKey} projectId="project-a" />);
expect(screen.getByRole("button", { name: /Collapse workflow selector/i })).toHaveAttribute("aria-expanded", "true");
expect(screen.getByRole("combobox", { name: /Workflow/i })).toBeDefined();
await waitFor(() => expect(screen.getByRole("option", { name: "Workflow A" })).toBeDefined());
});
it("collapses and expands while persisting state by project", async () => {
render(<WorkflowSelector value={null} onChange={vi.fn()} collapsible collapseStorageKey={storageKey} projectId="project-a" />);
fireEvent.click(screen.getByRole("button", { name: /Collapse workflow selector/i }));
expect(screen.getByRole("button", { name: /^Workflow$/i })).toHaveAttribute("aria-expanded", "false");
expect(screen.queryByRole("combobox")).toBeNull();
expect(localStorage.getItem(scopedKey(storageKey, "project-a"))).toBe("true");
fireEvent.click(screen.getByRole("button", { name: /^Workflow$/i }));
expect(screen.getByRole("combobox", { name: /Workflow/i })).toBeDefined();
expect(localStorage.getItem(scopedKey(storageKey, "project-a"))).toBe("false");
});
it("restores persisted collapsed state for the current project only", () => {
localStorage.setItem(scopedKey(storageKey, "project-a"), "true");
const { rerender } = render(
<WorkflowSelector value={null} onChange={vi.fn()} collapsible collapseStorageKey={storageKey} projectId="project-a" />,
);
expect(screen.getByRole("button", { name: /^Workflow$/i })).toBeDefined();
expect(screen.queryByRole("combobox")).toBeNull();
rerender(<WorkflowSelector value={null} onChange={vi.fn()} collapsible collapseStorageKey={storageKey} projectId="project-b" />);
expect(screen.getByRole("combobox", { name: /Workflow/i })).toBeDefined();
});
it("keeps the select disabled while loading and when disabled is passed", async () => {
fetchWorkflowsMock.mockReturnValue(new Promise(() => undefined));
const { rerender } = render(
<WorkflowSelector value={null} onChange={vi.fn()} collapsible collapseStorageKey={storageKey} />,
);
expect(screen.getByRole("combobox", { name: /Workflow/i })).toBeDisabled();
fetchWorkflowsMock.mockResolvedValue([]);
rerender(<WorkflowSelector value={null} onChange={vi.fn()} collapsible collapseStorageKey={storageKey} disabled />);
await waitFor(() => expect(screen.getByRole("combobox", { name: /Workflow/i })).toBeDisabled());
});
it("renders the None option when the workflow list is empty", async () => {
fetchWorkflowsMock.mockResolvedValue([]);
render(<WorkflowSelector value={null} onChange={vi.fn()} collapsible collapseStorageKey={storageKey} />);
await waitFor(() => expect(screen.getByRole("option", { name: "None" })).toBeDefined());
expect(screen.queryByRole("option", { name: "Workflow A" })).toBeNull();
});
});
describe("WorkflowSelector switch-with-active-session confirm (U9)", () => { describe("WorkflowSelector switch-with-active-session confirm (U9)", () => {
it("shows the abort-warning confirm and applies the switch when confirmed", async () => { it("shows the abort-warning confirm and applies the switch when confirmed", async () => {
mockConfirm.mockResolvedValue(true); mockConfirm.mockResolvedValue(true);

View File

@@ -87,7 +87,6 @@ describe("projectStorage", () => {
"kb-dashboard-board-workflow-collapsed", "kb-dashboard-board-workflow-collapsed",
"kb-quick-entry-text", "kb-quick-entry-text",
"kb-inline-create-text", "kb-inline-create-text",
"kb-board-workflow-selector-collapsed",
"fn-agent-view", "fn-agent-view",
"kb-terminal-tabs", "kb-terminal-tabs",
"kb-planning-last-description", "kb-planning-last-description",
@@ -105,7 +104,7 @@ describe("projectStorage", () => {
"fusion-plugin-dependency-graph:positions", "fusion-plugin-dependency-graph:positions",
]), ]),
); );
expect(PROJECT_STORAGE_KEYS).toHaveLength(27); expect(PROJECT_STORAGE_KEYS).toHaveLength(26);
}); });
it("stores branch filter values as scoped strings per project", () => { it("stores branch filter values as scoped strings per project", () => {

View File

@@ -20,7 +20,6 @@ export const PROJECT_STORAGE_KEYS: string[] = [
"kb-dashboard-board-workflow-collapsed", "kb-dashboard-board-workflow-collapsed",
"kb-quick-entry-text", "kb-quick-entry-text",
"kb-inline-create-text", "kb-inline-create-text",
"kb-board-workflow-selector-collapsed",
"fn-agent-view", "fn-agent-view",
"kb-terminal-tabs", "kb-terminal-tabs",
"kb-planning-last-description", "kb-planning-last-description",