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