mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-27 10:34:38 +08:00
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:
@ -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}
|
||||
/>
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
Reference in New Issue
Block a user