fix: Remove duplicate Lock flow control from bottom-center menubar (#13088)

* remove flow lock button form canvas controls

* [autofix.ci] apply automated fixes

* fix testcaes

* fix testcaes

---------

Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
olayinkaadelakun
2026-05-16 09:24:27 -07:00
committed by GitHub
parent 682c97846f
commit ec924b1af4
4 changed files with 21 additions and 203 deletions

View File

@ -1,19 +1,14 @@
import { Background, Panel } from "@xyflow/react";
import { cloneDeep } from "lodash";
import { memo, useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { memo } from "react";
import { useShallow } from "zustand/react/shallow";
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import CanvasControlButton from "@/components/core/canvasControlsComponent/CanvasControlButton";
import CanvasControls from "@/components/core/canvasControlsComponent/CanvasControls";
import { Button } from "@/components/ui/button";
import { SidebarTrigger, useSidebar } from "@/components/ui/sidebar";
import { ENABLE_NEW_SIDEBAR } from "@/customization/feature-flags";
import useSaveFlow from "@/hooks/flows/use-save-flow";
import useFlowStore from "@/stores/flowStore";
import { AllNodeType } from "@/types/flow";
import { cn } from "@/utils/utils";
import { useSearchContext } from "../flowSidebarComponent";
import { NAV_ITEMS } from "../flowSidebarComponent/components/sidebarSegmentedNav";
export const MemoizedBackground = memo(() => (
@ -29,91 +24,22 @@ interface MemoizedCanvasControlsProps {
}
export const MemoizedCanvasControls = memo(
({
setIsAddingNote,
shadowBoxWidth,
shadowBoxHeight,
selectedNode,
isAgentWorking,
}: MemoizedCanvasControlsProps) => {
const { t } = useTranslation();
({ selectedNode, isAgentWorking }: MemoizedCanvasControlsProps) => {
const currentFlow = useFlowStore(useShallow((state) => state.currentFlow));
const setCurrentFlow = useFlowStore((state) => state.setCurrentFlow);
const saveFlow = useSaveFlow();
const isLocked = currentFlow?.locked ?? false;
const effectiveLocked = isLocked || isAgentWorking;
const [isSaving, setIsSaving] = useState(false);
const handleToggleLock = useCallback(async () => {
if (isAgentWorking || isSaving || !currentFlow) return;
const newFlow = cloneDeep(currentFlow);
newFlow.locked = !isLocked;
setIsSaving(true);
try {
await saveFlow(newFlow);
setCurrentFlow(newFlow);
} finally {
setIsSaving(false);
}
}, [
currentFlow,
isLocked,
isAgentWorking,
isSaving,
saveFlow,
setCurrentFlow,
]);
return (
<CanvasControls
selectedNode={selectedNode}
effectiveLocked={effectiveLocked}
>
<Button
unstyled
size="icon"
data-testid="lock-status"
disabled={isAgentWorking || isSaving}
className={cn(
"flex items-center justify-center px-2 rounded-none gap-1",
isAgentWorking || isSaving
? "cursor-default opacity-70"
: "cursor-pointer",
)}
title={
isAgentWorking
? t("canvas.agentWorking")
: isSaving
? t("canvas.saving")
: isLocked
? t("canvas.unlockFlow")
: t("canvas.lockFlow")
}
onClick={handleToggleLock}
>
<ForwardedIconComponent
name={effectiveLocked ? "Lock" : "Unlock"}
className={cn(
"!h-[18px] !w-[18px] text-muted-foreground",
effectiveLocked && "text-destructive",
)}
/>
{effectiveLocked && (
<span className="text-xs text-destructive">
{isAgentWorking
? t("canvas.agentWorking")
: t("canvas.flowLocked")}
</span>
)}
</Button>
</CanvasControls>
/>
);
},
);
export const MemoizedSidebarTrigger = memo(() => {
const { open, toggleSidebar, setActiveSection } = useSidebar();
const { focusSearch, isSearchFocused } = useSearchContext();
if (ENABLE_NEW_SIDEBAR) {
return (
<Panel
@ -135,10 +61,6 @@ export const MemoizedSidebarTrigger = memo(() => {
if (!open) {
toggleSidebar();
}
if (item.id === "search") {
// Add a small delay to ensure the sidebar is open and input is rendered
setTimeout(() => focusSearch(), 100);
}
}}
testId={item.id}
/>

View File

@ -1,9 +1,4 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
const mockSaveFlow: jest.Mock<Promise<void>, unknown[]> = jest.fn(() =>
Promise.resolve(),
);
const mockSetCurrentFlow = jest.fn();
import { render, screen } from "@testing-library/react";
const mockCurrentFlow = {
id: "test-flow-id",
@ -15,13 +10,6 @@ jest.mock("nanoid", () => ({
nanoid: () => "test-id",
}));
jest.mock("../../flowSidebarComponent", () => ({
useSearchContext: () => ({
focusSearch: jest.fn(),
isSearchFocused: false,
}),
}));
jest.mock("../../flowSidebarComponent/components/sidebarSegmentedNav", () => ({
NAV_ITEMS: [],
}));
@ -49,17 +37,12 @@ jest.mock("@/stores/flowStore", () => ({
default: jest.fn((selector) => {
const state = {
currentFlow: mockCurrentFlow,
setCurrentFlow: mockSetCurrentFlow,
setCurrentFlow: jest.fn(),
};
return selector(state);
}),
}));
jest.mock("@/hooks/flows/use-save-flow", () => ({
__esModule: true,
default: () => mockSaveFlow,
}));
jest.mock("@/components/core/canvasControlsComponent/CanvasControls", () => ({
__esModule: true,
default: ({ children }) => (
@ -84,14 +67,6 @@ jest.mock("@/components/common/genericIconComponent", () => ({
),
}));
jest.mock("@/components/ui/button", () => ({
Button: ({ children, onClick, title, className, ...rest }) => (
<button onClick={onClick} title={title} className={className} {...rest}>
{children}
</button>
),
}));
jest.mock("@/utils/utils", () => ({
cn: (...args: string[]) => args.filter(Boolean).join(" "),
}));
@ -112,91 +87,24 @@ describe("MemoizedCanvasControls", () => {
mockCurrentFlow.locked = false;
});
it("should_render_unlock_icon_when_flow_is_not_locked", () => {
render(<MemoizedCanvasControls {...defaultProps} />);
expect(screen.getByTestId("icon-Unlock")).toBeInTheDocument();
expect(screen.queryByTestId("icon-Lock")).not.toBeInTheDocument();
});
it("should_render_lock_icon_when_flow_is_locked", () => {
mockCurrentFlow.locked = true;
render(<MemoizedCanvasControls {...defaultProps} />);
expect(screen.getByTestId("icon-Lock")).toBeInTheDocument();
expect(screen.queryByTestId("icon-Unlock")).not.toBeInTheDocument();
});
it("should_show_unlock_flow_title_when_flow_is_locked", () => {
mockCurrentFlow.locked = true;
render(<MemoizedCanvasControls {...defaultProps} />);
expect(screen.getByTitle("Unlock flow")).toBeInTheDocument();
});
it("should_show_lock_flow_title_when_flow_is_not_locked", () => {
render(<MemoizedCanvasControls {...defaultProps} />);
expect(screen.getByTitle("Lock flow")).toBeInTheDocument();
});
it("should_call_save_flow_and_set_current_flow_when_lock_button_clicked", async () => {
render(<MemoizedCanvasControls {...defaultProps} />);
const lockButton = screen.getByTestId("lock-status");
fireEvent.click(lockButton);
await waitFor(() => {
expect(mockSaveFlow).toHaveBeenCalledTimes(1);
expect(mockSetCurrentFlow).toHaveBeenCalledTimes(1);
});
const savedFlow = (mockSaveFlow.mock.calls as unknown[][])[0]?.[0] as
| Record<string, unknown>
| undefined;
expect(savedFlow?.locked).toBe(true);
});
it("should_toggle_lock_state_from_locked_to_unlocked", async () => {
mockCurrentFlow.locked = true;
render(<MemoizedCanvasControls {...defaultProps} />);
const lockButton = screen.getByTestId("lock-status");
fireEvent.click(lockButton);
await waitFor(() => {
expect(mockSaveFlow).toHaveBeenCalledTimes(1);
});
const savedFlow = (mockSaveFlow.mock.calls as unknown[][])[0]?.[0] as
| Record<string, unknown>
| undefined;
expect(savedFlow?.locked).toBe(false);
});
it("should_apply_destructive_color_class_when_flow_is_locked", () => {
mockCurrentFlow.locked = true;
render(<MemoizedCanvasControls {...defaultProps} />);
const icon = screen.getByTestId("icon-Lock");
expect(icon.className).toContain("text-destructive");
});
it("should_not_apply_destructive_color_class_when_flow_is_unlocked", () => {
render(<MemoizedCanvasControls {...defaultProps} />);
const icon = screen.getByTestId("icon-Unlock");
expect(icon.className).not.toContain("text-destructive");
});
it("should_render_inside_canvas_controls_wrapper", () => {
it("should_render_canvas_controls_wrapper", () => {
render(<MemoizedCanvasControls {...defaultProps} />);
expect(screen.getByTestId("canvas-controls")).toBeInTheDocument();
expect(screen.getByTestId("lock-status")).toBeInTheDocument();
});
it("should_not_render_lock_icon", () => {
render(<MemoizedCanvasControls {...defaultProps} />);
expect(screen.queryByTestId("icon-Lock")).not.toBeInTheDocument();
expect(screen.queryByTestId("icon-Unlock")).not.toBeInTheDocument();
});
it("should_not_render_lock_icon_when_flow_is_locked", () => {
mockCurrentFlow.locked = true;
render(<MemoizedCanvasControls {...defaultProps} />);
expect(screen.queryByTestId("icon-Lock")).not.toBeInTheDocument();
});
});

View File

@ -16,10 +16,6 @@ test.describe("Flow Lock Feature", () => {
timeout: 5000,
});
// Verify initially the flow is not locked (no lock icon should be visible)
const initialLockIcon = page.getByTestId("icon-Lock");
await expect(initialLockIcon).toHaveCount(0);
// Open flow settings by clicking on the flow name
await page.getByTestId("flow_name").click();
@ -70,10 +66,6 @@ test.describe("Flow Lock Feature", () => {
timeout: 10000,
});
// Verify lock icon now appears in the flow header
const lockIconInHeader = page.getByTestId("icon-Lock");
await expect(lockIconInHeader).toBeVisible();
// Try to open settings again to unlock
await page.getByTestId("flow_name").click();

View File

@ -47,10 +47,6 @@ test(
//ensure the UI is updated
await page.waitForSelector('[data-testid="icon-Lock"]', {
timeout: 3000,
});
await unlockFlow(page);
await page.getByTestId("icon-ChevronLeft").click();