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:
@@ -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}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user