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 8e585aab2f
commit ed9ecd112c
3 changed files with 69 additions and 3 deletions

View File

@@ -197,6 +197,14 @@ function AppInner() {
} = useAppSettings(currentProject?.id); } = useAppSettings(currentProject?.id);
const skillsEnabled = experimentalFeatures.skillsView === true; 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 { const {
availableModels, availableModels,
favoriteProviders, favoriteProviders,
@@ -275,8 +283,9 @@ function AppInner() {
}, [modalManager]); }, [modalManager]);
const handleOpenNodes = useCallback(() => { const handleOpenNodes = useCallback(() => {
if (!nodesEnabled) return;
setNodesOpen((prev) => !prev); setNodesOpen((prev) => !prev);
}, []); }, [nodesEnabled]);
const handleOpenMissionsView = useCallback(() => { const handleOpenMissionsView = useCallback(() => {
setMissionTargetId(undefined); setMissionTargetId(undefined);
@@ -547,6 +556,7 @@ function AppInner() {
onOpenSchedules={modalManager.openSchedules} onOpenSchedules={modalManager.openSchedules}
onOpenGitManager={modalManager.openGitManager} onOpenGitManager={modalManager.openGitManager}
onOpenNodes={handleOpenNodes} onOpenNodes={handleOpenNodes}
showNodesButton={nodesEnabled}
onOpenWorkflowSteps={modalManager.openWorkflowSteps} onOpenWorkflowSteps={modalManager.openWorkflowSteps}
onOpenScripts={modalManager.openScripts} onOpenScripts={modalManager.openScripts}
onRunScript={modalManager.runScript} onRunScript={modalManager.runScript}

View File

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

View File

@@ -1648,4 +1648,57 @@ describe("Header", () => {
expect(slot).toBeNull(); 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();
});
});
});
}); });