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:
5
.changeset/fn-6080-removal.md
Normal file
5
.changeset/fn-6080-removal.md
Normal 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.
|
||||
@@ -18,7 +18,6 @@ import { WorkflowSelector } from "./WorkflowSelector";
|
||||
|
||||
const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
||||
const STORAGE_KEY = "kb-inline-create-text";
|
||||
const WORKFLOW_SELECTOR_COLLAPSE_STORAGE_KEY = "kb-board-workflow-selector-collapsed";
|
||||
|
||||
interface PendingImage {
|
||||
file: File;
|
||||
@@ -1037,8 +1036,6 @@ export function InlineCreateCard({
|
||||
addToast={addToast}
|
||||
label="Workflow"
|
||||
disabled={submitting}
|
||||
collapsible
|
||||
collapseStorageKey={WORKFLOW_SELECTOR_COLLAPSE_STORAGE_KEY}
|
||||
/>
|
||||
|
||||
<label className="inline-create-priority-wrap" htmlFor="inline-create-priority-select">
|
||||
|
||||
@@ -26,17 +26,6 @@
|
||||
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 {
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
background: var(--bg-secondary);
|
||||
@@ -71,8 +60,7 @@
|
||||
}
|
||||
|
||||
.workflow-selector select,
|
||||
.workflow-selector-manage,
|
||||
.workflow-selector-collapsed-button {
|
||||
.workflow-selector-manage {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import "./WorkflowSelector.css";
|
||||
import { useCallback, useEffect, useId, useState } from "react";
|
||||
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 { getErrorMessage } from "@fusion/core";
|
||||
import { fetchWorkflow, fetchWorkflows, fetchProjectDefaultWorkflow, setProjectDefaultWorkflow } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { useConfirm } from "../hooks/useConfirm";
|
||||
import { getScopedItem, setScopedItem } from "../utils/projectStorage";
|
||||
|
||||
interface WorkflowSelectorProps {
|
||||
/** Currently selected workflow id, or null for none. */
|
||||
@@ -20,12 +19,6 @@ interface WorkflowSelectorProps {
|
||||
label?: string;
|
||||
/** Optional affordance to open the graph editor. */
|
||||
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,
|
||||
* switching aborts that session and re-homes the card into the new workflow's
|
||||
@@ -43,9 +36,6 @@ export function WorkflowSelector({
|
||||
disabled,
|
||||
label = "Workflow",
|
||||
onManage,
|
||||
collapsible = false,
|
||||
collapseStorageKey,
|
||||
collapsedLabel = "Workflow",
|
||||
hasActiveSession,
|
||||
}: WorkflowSelectorProps) {
|
||||
const { t } = useTranslation("app");
|
||||
@@ -54,29 +44,6 @@ export function WorkflowSelector({
|
||||
const [workflows, setWorkflows] = useState<WorkflowDefinition[]>([]);
|
||||
const [loading, setLoading] = 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(() => {
|
||||
let cancelled = false;
|
||||
@@ -134,40 +101,10 @@ export function WorkflowSelector({
|
||||
[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 (
|
||||
<div className="workflow-selector" data-testid="workflow-selector">
|
||||
<div className="workflow-selector-label">
|
||||
<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">
|
||||
<WorkflowIcon size={14} aria-hidden /> {label}
|
||||
</label>
|
||||
|
||||
@@ -135,8 +135,6 @@ vi.mock("../../api", () => ({
|
||||
|
||||
const TEST_PROJECT_ID = "proj-123";
|
||||
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[] = [
|
||||
{
|
||||
@@ -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)", () => {
|
||||
it("textarea spans full container width in board view", () => {
|
||||
renderCard();
|
||||
|
||||
@@ -2,12 +2,9 @@ import React from "react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { WorkflowSelector } from "../WorkflowSelector";
|
||||
import { scopedKey } from "../../utils/projectStorage";
|
||||
|
||||
vi.mock("lucide-react", () => ({
|
||||
Workflow: () => null,
|
||||
ChevronDown: () => null,
|
||||
ChevronRight: () => null,
|
||||
}));
|
||||
|
||||
const fetchWorkflowsMock = vi.fn();
|
||||
@@ -34,71 +31,6 @@ beforeEach(() => {
|
||||
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)", () => {
|
||||
it("shows the abort-warning confirm and applies the switch when confirmed", async () => {
|
||||
mockConfirm.mockResolvedValue(true);
|
||||
|
||||
@@ -87,7 +87,6 @@ describe("projectStorage", () => {
|
||||
"kb-dashboard-board-workflow-collapsed",
|
||||
"kb-quick-entry-text",
|
||||
"kb-inline-create-text",
|
||||
"kb-board-workflow-selector-collapsed",
|
||||
"fn-agent-view",
|
||||
"kb-terminal-tabs",
|
||||
"kb-planning-last-description",
|
||||
@@ -105,7 +104,7 @@ describe("projectStorage", () => {
|
||||
"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", () => {
|
||||
|
||||
@@ -20,7 +20,6 @@ export const PROJECT_STORAGE_KEYS: string[] = [
|
||||
"kb-dashboard-board-workflow-collapsed",
|
||||
"kb-quick-entry-text",
|
||||
"kb-inline-create-text",
|
||||
"kb-board-workflow-selector-collapsed",
|
||||
"fn-agent-view",
|
||||
"kb-terminal-tabs",
|
||||
"kb-planning-last-description",
|
||||
|
||||
Reference in New Issue
Block a user