feat(FN-2032): gate Header nodes button by nodesView flag

- Add optional showNodesButton prop to Header while keeping default behavior backward compatible
- Hide desktop and mobile overflow Nodes actions when showNodesButton is false
- Wire App.tsx to experimentalFeatures.nodesView, guard node toggle handling, and auto-close the overlay when disabled
- Add Header tests for desktop/mobile visibility and click behavior across showNodesButton states
This commit is contained in:
Fusion
2026-04-17 18:03:46 -07:00
committed by gsxdsm
parent 3538a5c99d
commit 77f384ca66
3 changed files with 69 additions and 3 deletions

View File

@@ -197,6 +197,14 @@ function AppInner() {
} = useAppSettings(currentProject?.id);
const skillsEnabled = experimentalFeatures.skillsView === true;
const nodesEnabled = experimentalFeatures.nodesView === true;
// Auto-close nodes overlay if feature flag is toggled off while overlay is open
useEffect(() => {
if (nodesOpen && !nodesEnabled) {
setNodesOpen(false);
}
}, [nodesOpen, nodesEnabled]);
const {
availableModels,
favoriteProviders,
@@ -275,8 +283,9 @@ function AppInner() {
}, [modalManager]);
const handleOpenNodes = useCallback(() => {
if (!nodesEnabled) return;
setNodesOpen((prev) => !prev);
}, []);
}, [nodesEnabled]);
const handleOpenMissionsView = useCallback(() => {
setMissionTargetId(undefined);
@@ -547,6 +556,7 @@ function AppInner() {
onOpenSchedules={modalManager.openSchedules}
onOpenGitManager={modalManager.openGitManager}
onOpenNodes={handleOpenNodes}
showNodesButton={nodesEnabled}
onOpenWorkflowSteps={modalManager.openWorkflowSteps}
onOpenScripts={modalManager.openScripts}
onRunScript={modalManager.runScript}

View File

@@ -170,6 +170,8 @@ export interface HeaderProps {
onOpenSchedules?: () => void;
onOpenGitManager?: () => void;
onOpenNodes?: () => void;
/** When false, hides the Nodes management button. Defaults to true for backward compat. */
showNodesButton?: boolean;
onOpenWorkflowSteps?: () => void;
onOpenScripts?: () => void;
onRunScript?: (name: string, command: string) => void;
@@ -219,6 +221,7 @@ export function Header({
onOpenSchedules,
onOpenGitManager,
onOpenNodes,
showNodesButton,
onOpenWorkflowSteps,
onOpenScripts,
onRunScript,
@@ -878,7 +881,7 @@ export function Header({
)}
{/* Nodes button - desktop only (moved to overflow on mobile/tablet) */}
{!isCompact && onOpenNodes && (
{!isCompact && onOpenNodes && showNodesButton !== false && (
<button
className="btn-icon"
onClick={onOpenNodes}
@@ -1015,7 +1018,7 @@ export function Header({
</button>
)}
{/* Nodes - in overflow on mobile */}
{onOpenNodes && (
{onOpenNodes && showNodesButton !== false && (
<button
className="mobile-overflow-item"
onClick={() => handleOverflowAction(onOpenNodes)}

View File

@@ -1648,4 +1648,57 @@ describe("Header", () => {
expect(slot).toBeNull();
});
});
describe("Nodes button visibility", () => {
describe("desktop viewport", () => {
it("shows nodes button by default when onOpenNodes is provided without showNodesButton", () => {
render(<Header onOpenSettings={vi.fn()} onOpenNodes={vi.fn()} />);
expect(screen.getByTestId("nodes-btn")).toBeDefined();
});
it("shows nodes button when showNodesButton is true and onOpenNodes is provided", () => {
render(<Header onOpenSettings={vi.fn()} onOpenNodes={vi.fn()} showNodesButton={true} />);
expect(screen.getByTestId("nodes-btn")).toBeDefined();
});
it("hides nodes button when showNodesButton is false", () => {
render(<Header onOpenSettings={vi.fn()} onOpenNodes={vi.fn()} showNodesButton={false} />);
expect(screen.queryByTestId("nodes-btn")).toBeNull();
});
it("calls onOpenNodes when nodes button is clicked", () => {
const onOpenNodes = vi.fn();
render(<Header onOpenSettings={vi.fn()} onOpenNodes={onOpenNodes} />);
fireEvent.click(screen.getByTestId("nodes-btn"));
expect(onOpenNodes).toHaveBeenCalledOnce();
});
});
describe("mobile viewport", () => {
beforeEach(() => {
mockMatchMedia("mobile");
});
it("hides mobile overflow nodes button when showNodesButton is false", () => {
render(<Header onOpenSettings={vi.fn()} onOpenNodes={vi.fn()} showNodesButton={false} />);
// Open the overflow menu to check mobile overflow items
fireEvent.click(screen.getByTitle("More header actions"));
expect(screen.queryByTestId("overflow-nodes-btn")).toBeNull();
});
it("shows mobile overflow nodes button by default when onOpenNodes is provided", () => {
render(<Header onOpenSettings={vi.fn()} onOpenNodes={vi.fn()} />);
// Open the overflow menu to check mobile overflow items
fireEvent.click(screen.getByTitle("More header actions"));
expect(screen.getByTestId("overflow-nodes-btn")).toBeDefined();
});
it("shows mobile overflow nodes button when showNodesButton is true", () => {
render(<Header onOpenSettings={vi.fn()} onOpenNodes={vi.fn()} showNodesButton={true} />);
// Open the overflow menu to check mobile overflow items
fireEvent.click(screen.getByTitle("More header actions"));
expect(screen.getByTestId("overflow-nodes-btn")).toBeDefined();
});
});
});
});