Merge branch 'lfoss-1889' of https://github.com/langflow-ai/langflow into lfoss-1889

This commit is contained in:
Deon Sanchez
2025-08-12 13:12:07 -06:00
4 changed files with 119 additions and 83 deletions

View File

@ -1,3 +1,6 @@
import { memo, useMemo, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { useShallow } from "zustand/react/shallow";
import IconComponent from "@/components/common/genericIconComponent";
import ShadTooltip from "@/components/common/shadTooltipComponent";
import FlowSettingsComponent from "@/components/core/flowSettingsComponent";
@ -20,9 +23,6 @@ import useFlowsManagerStore from "@/stores/flowsManagerStore";
import { useShortcutsStore } from "@/stores/shortcuts";
import { swatchColors } from "@/utils/styleUtils";
import { cn, getNumberFromString } from "@/utils/utils";
import { memo, useMemo, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { useShallow } from "zustand/react/shallow";
export const MenuBar = memo((): JSX.Element => {
const setSuccessData = useAlertStore((state) => state.setSuccessData);

View File

@ -1,5 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react';
import CanvasControls from '../index';
import { fireEvent, render, screen } from "@testing-library/react";
import CanvasControls from "../index";
// Capture flow functions for assertions
const reactFlowFns = {
@ -10,8 +10,12 @@ const reactFlowFns = {
};
// Mocks for external dependencies used internally
jest.mock('@xyflow/react', () => ({
Panel: ({ children, ...props }: any) => <div data-testid="panel" {...props}>{children}</div>,
jest.mock("@xyflow/react", () => ({
Panel: ({ children, ...props }: any) => (
<div data-testid="panel" {...props}>
{children}
</div>
),
useReactFlow: () => reactFlowFns,
useStore: (_selector: any) => ({
isInteractive: true,
@ -22,38 +26,40 @@ jest.mock('@xyflow/react', () => ({
useStoreApi: () => ({ setState: jest.fn() }),
}));
jest.mock('@/stores/flowStore', () => ({
jest.mock("@/stores/flowStore", () => ({
__esModule: true,
default: jest.fn(() => false),
}));
jest.mock('@/components/ui/separator', () => ({
Separator: ({ orientation }: { orientation: 'vertical' | 'horizontal' }) => (
jest.mock("@/components/ui/separator", () => ({
Separator: ({ orientation }: { orientation: "vertical" | "horizontal" }) => (
<div data-testid={`separator-${orientation}`} />
),
}));
// Mock dropdowns to a simple render that exposes props for assertions
jest.mock('../dropdowns', () => ({
jest.mock("../dropdowns", () => ({
CanvasControlsDropdown: (props: any) => (
<div data-testid="controls-dropdown" {...props} />
),
HelpDropdown: (props: any) => (
<div data-testid="help-dropdown" {...props} />
),
HelpDropdown: (props: any) => <div data-testid="help-dropdown" {...props} />,
}));
describe('CanvasControls', () => {
it('renders panel and separators when children present', () => {
render(<CanvasControls><div>child</div></CanvasControls>);
expect(screen.getByTestId('main_canvas_controls')).toBeInTheDocument();
const seps = screen.getAllByTestId('separator-vertical');
describe("CanvasControls", () => {
it("renders panel and separators when children present", () => {
render(
<CanvasControls>
<div>child</div>
</CanvasControls>,
);
expect(screen.getByTestId("main_canvas_controls")).toBeInTheDocument();
const seps = screen.getAllByTestId("separator-vertical");
expect(seps.length).toBeGreaterThanOrEqual(1);
expect(screen.getByTestId('controls-dropdown')).toBeInTheDocument();
expect(screen.getByTestId('help-dropdown')).toBeInTheDocument();
expect(screen.getByTestId("controls-dropdown")).toBeInTheDocument();
expect(screen.getByTestId("help-dropdown")).toBeInTheDocument();
});
it('handles keyboard shortcuts with meta/ctrl keys', () => {
it("handles keyboard shortcuts with meta/ctrl keys", () => {
render(<CanvasControls />);
reactFlowFns.fitView.mockClear();
reactFlowFns.zoomIn.mockClear();
@ -61,13 +67,13 @@ describe('CanvasControls', () => {
reactFlowFns.zoomTo.mockClear();
// Press + (Equal) with meta
fireEvent.keyDown(document, { key: '+', code: 'Equal', metaKey: true });
fireEvent.keyDown(document, { key: "+", code: "Equal", metaKey: true });
// Press - (Minus) with ctrl
fireEvent.keyDown(document, { key: '-', code: 'Minus', ctrlKey: true });
fireEvent.keyDown(document, { key: "-", code: "Minus", ctrlKey: true });
// Press 1 (Digit1) with meta
fireEvent.keyDown(document, { key: '1', code: 'Digit1', metaKey: true });
fireEvent.keyDown(document, { key: "1", code: "Digit1", metaKey: true });
// Press 0 (Digit0) with ctrl
fireEvent.keyDown(document, { key: '0', code: 'Digit0', ctrlKey: true });
fireEvent.keyDown(document, { key: "0", code: "Digit0", ctrlKey: true });
expect(reactFlowFns.zoomIn).toHaveBeenCalled();
expect(reactFlowFns.zoomOut).toHaveBeenCalled();
@ -75,5 +81,3 @@ describe('CanvasControls', () => {
expect(reactFlowFns.zoomTo).toHaveBeenCalledWith(1);
});
});

View File

@ -1,52 +1,68 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { MemoryRouter, useNavigate } from 'react-router-dom';
import { CanvasControlsDropdown, HelpDropdown } from '../dropdowns';
import { fireEvent, render, screen } from "@testing-library/react";
import { MemoryRouter, useNavigate } from "react-router-dom";
import { CanvasControlsDropdown, HelpDropdown } from "../dropdowns";
jest.mock('@/components/ui/button', () => ({
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
jest.mock("@/components/ui/button", () => ({
Button: ({ children, ...props }: any) => (
<button {...props}>{children}</button>
),
}));
jest.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenu: ({ children, ...props }: any) => <div data-testid="dropdown-menu" {...props}>{children}</div>,
DropdownMenuTrigger: ({ children, ...props }: any) => <div data-testid="dropdown-trigger" {...props}>{children}</div>,
DropdownMenuContent: ({ children, ...props }: any) => <div data-testid="dropdown-content" {...props}>{children}</div>,
jest.mock("@/components/ui/dropdown-menu", () => ({
DropdownMenu: ({ children, ...props }: any) => (
<div data-testid="dropdown-menu" {...props}>
{children}
</div>
),
DropdownMenuTrigger: ({ children, ...props }: any) => (
<div data-testid="dropdown-trigger" {...props}>
{children}
</div>
),
DropdownMenuContent: ({ children, ...props }: any) => (
<div data-testid="dropdown-content" {...props}>
{children}
</div>
),
}));
jest.mock('@/components/ui/separator', () => ({
jest.mock("@/components/ui/separator", () => ({
Separator: () => <div data-testid="separator" />,
}));
jest.mock('@/components/common/genericIconComponent', () => ({
jest.mock("@/components/common/genericIconComponent", () => ({
__esModule: true,
default: () => <span data-testid="icon" />,
ForwardedIconComponent: ({ name }: { name: string }) => <span data-testid={`icon-${name}`} />,
ForwardedIconComponent: ({ name }: { name: string }) => (
<span data-testid={`icon-${name}`} />
),
}));
jest.mock('@/constants/constants', () => ({
jest.mock("@/constants/constants", () => ({
__esModule: true,
DATASTAX_DOCS_URL: 'https://docs.datastax.com',
DOCS_URL: 'https://docs.langflow.org',
DESKTOP_URL: 'https://desktop.langflow.org',
DATASTAX_DOCS_URL: "https://docs.datastax.com",
DOCS_URL: "https://docs.langflow.org",
DESKTOP_URL: "https://desktop.langflow.org",
}));
jest.mock('@/customization/feature-flags', () => ({
jest.mock("@/customization/feature-flags", () => ({
ENABLE_DATASTAX_LANGFLOW: false,
}));
jest.mock('@/utils/utils', () => ({
cn: (...args: any[]) => args.filter(Boolean).join(' '),
getOS: () => 'macos',
jest.mock("@/utils/utils", () => ({
cn: (...args: any[]) => args.filter(Boolean).join(" "),
getOS: () => "macos",
}));
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
jest.mock("react-router-dom", () => {
const actual = jest.requireActual("react-router-dom");
return {
...actual,
useNavigate: jest.fn(),
};
});
describe('CanvasControlsDropdown', () => {
describe("CanvasControlsDropdown", () => {
const baseProps = {
zoom: 1,
minZoomReached: false,
@ -58,21 +74,21 @@ describe('CanvasControlsDropdown', () => {
onResetZoom: jest.fn(),
onFitView: jest.fn(),
shortcuts: {
ZOOM_IN: { key: '+' },
ZOOM_OUT: { key: '-' },
RESET_ZOOM: { key: '0' },
FIT_VIEW: { key: '1' },
ZOOM_IN: { key: "+" },
ZOOM_OUT: { key: "-" },
RESET_ZOOM: { key: "0" },
FIT_VIEW: { key: "1" },
},
};
it('renders zoom percentage and calls handlers', () => {
it("renders zoom percentage and calls handlers", () => {
render(<CanvasControlsDropdown {...baseProps} />);
expect(screen.getByText('100%')).toBeInTheDocument();
expect(screen.getByText("100%")).toBeInTheDocument();
fireEvent.click(screen.getByTestId('zoom_in_dropdown'));
fireEvent.click(screen.getByTestId('zoom_out_dropdown'));
fireEvent.click(screen.getByTestId('reset_zoom_dropdown'));
fireEvent.click(screen.getByTestId('fit_view_dropdown'));
fireEvent.click(screen.getByTestId("zoom_in_dropdown"));
fireEvent.click(screen.getByTestId("zoom_out_dropdown"));
fireEvent.click(screen.getByTestId("reset_zoom_dropdown"));
fireEvent.click(screen.getByTestId("fit_view_dropdown"));
expect(baseProps.onZoomIn).toHaveBeenCalled();
expect(baseProps.onZoomOut).toHaveBeenCalled();
@ -80,43 +96,49 @@ describe('CanvasControlsDropdown', () => {
expect(baseProps.onFitView).toHaveBeenCalled();
});
it('disables buttons based on min/max zoom flags', () => {
it("disables buttons based on min/max zoom flags", () => {
render(
<CanvasControlsDropdown
{...baseProps}
minZoomReached={true}
maxZoomReached={true}
/>
/>,
);
expect(screen.getByTestId('zoom_in_dropdown')).toBeDisabled();
expect(screen.getByTestId('zoom_out_dropdown')).toBeDisabled();
expect(screen.getByTestId("zoom_in_dropdown")).toBeDisabled();
expect(screen.getByTestId("zoom_out_dropdown")).toBeDisabled();
});
});
describe('HelpDropdown', () => {
describe("HelpDropdown", () => {
beforeEach(() => {
(window.open as jest.Mock).mockClear();
});
it('opens docs in new tab and navigates to shortcuts', () => {
it("opens docs in new tab and navigates to shortcuts", () => {
const mockNavigate = jest.fn();
(useNavigate as unknown as jest.Mock).mockReturnValue(mockNavigate);
render(
<MemoryRouter>
<HelpDropdown isOpen={true} onOpenChange={() => {}} />
</MemoryRouter>
</MemoryRouter>,
);
fireEvent.click(screen.getByTestId('canvas_controls_dropdown_docs'));
expect(window.open).toHaveBeenCalledWith('https://docs.langflow.org', '_blank');
fireEvent.click(screen.getByTestId("canvas_controls_dropdown_docs"));
expect(window.open).toHaveBeenCalledWith(
"https://docs.langflow.org",
"_blank",
);
fireEvent.click(screen.getByTestId('canvas_controls_dropdown_shortcuts'));
expect(mockNavigate).toHaveBeenCalledWith('/settings/shortcuts');
fireEvent.click(screen.getByTestId("canvas_controls_dropdown_shortcuts"));
expect(mockNavigate).toHaveBeenCalledWith("/settings/shortcuts");
fireEvent.click(screen.getByTestId('canvas_controls_dropdown_get_langflow_desktop'));
expect(window.open).toHaveBeenCalledWith('https://desktop.langflow.org', '_blank');
fireEvent.click(
screen.getByTestId("canvas_controls_dropdown_get_langflow_desktop"),
);
expect(window.open).toHaveBeenCalledWith(
"https://desktop.langflow.org",
"_blank",
);
});
});

View File

@ -1,15 +1,19 @@
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import { Switch } from "@/components/ui/switch";
import * as Form from "@radix-ui/react-form";
import type React from "react";
import { useState } from "react";
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import { Switch } from "@/components/ui/switch";
import type { InputProps } from "../../../types/components";
import { cn } from "../../../utils/utils";
import { Input } from "../../ui/input";
import { Textarea } from "../../ui/textarea";
export const EditFlowSettings: React.FC<
InputProps & { submitForm?: () => void; locked?: boolean; setLocked?: (v: boolean) => void }
InputProps & {
submitForm?: () => void;
locked?: boolean;
setLocked?: (v: boolean) => void;
}
> = ({
name,
invalidNameList = [],
@ -22,7 +26,11 @@ export const EditFlowSettings: React.FC<
submitForm,
locked = false,
setLocked,
}: InputProps & { submitForm?: () => void; locked?: boolean; setLocked?: (v: boolean) => void }): JSX.Element => {
}: InputProps & {
submitForm?: () => void;
locked?: boolean;
setLocked?: (v: boolean) => void;
}): JSX.Element => {
const [isMaxLength, setIsMaxLength] = useState(false);
const [isMaxDescriptionLength, setIsMaxDescriptionLength] = useState(false);
const [isMinLength, setIsMinLength] = useState(false);
@ -171,8 +179,11 @@ export const EditFlowSettings: React.FC<
<Form.Message match="valueMissing" className="field-invalid">
Please enter a description
</Form.Message>
<div className="flex items-center gap-2 mt-3">
<ForwardedIconComponent name={locked ? "Lock" : "Unlock"} className="text-muted-foreground !w-5 !h-5" />
<div className="flex items-center gap-2 mt-3">
<ForwardedIconComponent
name={locked ? "Lock" : "Unlock"}
className="text-muted-foreground !w-5 !h-5"
/>
<Form.Label className="text-mmd font-medium">Lock Flow</Form.Label>
<Switch
checked={!!locked}
@ -182,7 +193,6 @@ export const EditFlowSettings: React.FC<
/>
</div>
</Form.Field>
</>
);
};