+jest.mock("@/components/ui/disclosure", () => ({
+ Disclosure: ({ children, open, onOpenChange }: any) => (
+
{children}
),
+ DisclosureContent: ({ children }: any) => (
+
{children}
+ ),
+ DisclosureTrigger: ({ children }: any) => (
+
{children}
+ ),
}));
jest.mock("@/components/ui/button", () => ({
@@ -35,21 +36,20 @@ jest.mock("@/components/ui/button", () => ({
),
}));
-jest.mock("@/components/ui/disclosure", () => ({
- Disclosure: ({ children, open, onOpenChange }: any) => (
-
- {children}
+jest.mock("@/components/common/genericIconComponent", () => ({
+ ForwardedIconComponent: ({ name, className }: any) => (
+
+ {name}
),
- DisclosureContent: ({ children }: any) => (
-
{children}
- ),
- DisclosureTrigger: ({ children }: any) => (
-
{children}
+}));
+
+jest.mock("@/components/common/shadTooltipComponent", () => ({
+ __esModule: true,
+ default: ({ children, content, styleClasses }: any) => (
+
+ {children}
+
),
}));
@@ -112,11 +112,16 @@ jest.mock("../sidebarFilterComponent", () => ({
data-color={color}
data-reset-filters={resetFilters?.toString()}
>
- Filter Component
+ Sidebar Filter
),
}));
+// Mock feature flags
+jest.mock("@/customization/feature-flags", () => ({
+ ENABLE_NEW_SIDEBAR: false, // Default to old sidebar for most tests
+}));
+
describe("SidebarHeaderComponent", () => {
const mockSetShowConfig = jest.fn();
const mockSetShowBeta = jest.fn();
@@ -126,7 +131,6 @@ describe("SidebarHeaderComponent", () => {
const mockHandleInputChange = jest.fn();
const mockSetFilterEdge = jest.fn();
const mockSetFilterData = jest.fn();
- const mockSearchInputRef = { current: null };
const defaultProps: SidebarHeaderComponentProps = {
showConfig: false,
@@ -135,7 +139,7 @@ describe("SidebarHeaderComponent", () => {
setShowBeta: mockSetShowBeta,
showLegacy: false,
setShowLegacy: mockSetShowLegacy,
- searchInputRef: mockSearchInputRef,
+ searchInputRef: { current: null },
isInputFocused: false,
search: "",
handleInputFocus: mockHandleInputFocus,
@@ -144,7 +148,7 @@ describe("SidebarHeaderComponent", () => {
filterType: undefined,
setFilterEdge: mockSetFilterEdge,
setFilterData: mockSetFilterData,
- data: {},
+ data: mockAPIData,
};
beforeEach(() => {
@@ -152,75 +156,111 @@ describe("SidebarHeaderComponent", () => {
});
describe("Basic Rendering", () => {
- it("should render sidebar header with correct structure", () => {
- render(
);
+ describe("Legacy Sidebar (!ENABLE_NEW_SIDEBAR)", () => {
+ it("should render sidebar header with legacy structure", () => {
+ render(
);
- expect(screen.getByTestId("sidebar-header")).toBeInTheDocument();
- expect(screen.getByTestId("disclosure")).toBeInTheDocument();
- expect(screen.getByTestId("sidebar-trigger")).toBeInTheDocument();
- expect(screen.getByText("Components")).toBeInTheDocument();
- expect(screen.getByTestId("search-input")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-header")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-trigger")).toBeInTheDocument();
+ expect(screen.getByText("Components")).toBeInTheDocument();
+ expect(
+ screen.getByTestId("sidebar-options-trigger"),
+ ).toBeInTheDocument();
+ expect(screen.getByTestId("disclosure-trigger")).toBeInTheDocument();
+ expect(screen.getByTestId("search-input")).toBeInTheDocument();
+ expect(screen.getByTestId("disclosure")).toBeInTheDocument();
+ expect(screen.getByTestId("disclosure-content")).toBeInTheDocument();
+ expect(screen.getByTestId("feature-toggles")).toBeInTheDocument();
+ });
+
+ it("should render sidebar trigger with correct icon", () => {
+ render(
);
+
+ expect(screen.getByTestId("sidebar-trigger")).toBeInTheDocument();
+ expect(screen.getByTestId("icon-PanelLeftClose")).toBeInTheDocument();
+ });
+
+ it("should render settings button with correct props", () => {
+ render(
);
+
+ const settingsButton = screen.getByTestId("sidebar-options-trigger");
+ expect(settingsButton).toHaveAttribute("data-variant", "ghost");
+ expect(settingsButton).toHaveAttribute("data-size", "iconMd");
+ expect(
+ screen.getByTestId("icon-SlidersHorizontal"),
+ ).toBeInTheDocument();
+ });
+
+ it("should show ghostActive variant when config is open", () => {
+ const propsWithOpenConfig = { ...defaultProps, showConfig: true };
+ render(
);
+
+ const settingsButton = screen.getByTestId("sidebar-options-trigger");
+ expect(settingsButton).toHaveAttribute("data-variant", "ghostActive");
+ });
+
+ it("should render tooltip with correct content", () => {
+ render(
);
+
+ expect(screen.getByTestId("tooltip")).toHaveAttribute(
+ "data-content",
+ "Component settings",
+ );
+ expect(screen.getByTestId("tooltip")).toHaveClass("z-50");
+ });
});
- it("should display correct title", () => {
+ it("should apply correct CSS classes to header", () => {
render(
);
- expect(screen.getByText("Components")).toBeInTheDocument();
- });
-
- it("should render sidebar trigger with icon", () => {
- render(
);
-
- expect(screen.getByTestId("sidebar-trigger")).toBeInTheDocument();
- expect(
- screen.getByTestId("forwarded-icon-PanelLeftClose"),
- ).toBeInTheDocument();
- });
-
- it("should render settings button", () => {
- render(
);
-
- expect(screen.getByTestId("sidebar-options-trigger")).toBeInTheDocument();
- expect(
- screen.getByTestId("forwarded-icon-SlidersHorizontal"),
- ).toBeInTheDocument();
- });
-
- it("should render tooltip with correct content", () => {
- render(
);
-
- expect(screen.getByTestId("tooltip")).toHaveAttribute(
- "data-content",
- "Component settings",
+ const header = screen.getByTestId("sidebar-header");
+ expect(header).toHaveClass(
+ "flex",
+ "w-full",
+ "flex-col",
+ "gap-2",
+ "p-4",
+ "pb-1",
+ "group-data-[collapsible=icon]:hidden",
);
- expect(screen.getByTestId("tooltip")).toHaveClass("z-50");
+ });
+
+ it("should render search input component with correct props", () => {
+ render(
);
+
+ const searchInput = screen.getByTestId("search-input");
+ expect(searchInput).toBeInTheDocument();
+ expect(searchInput).toHaveAttribute("data-search", "");
+ expect(searchInput).toHaveAttribute("data-is-focused", "false");
});
});
describe("Disclosure Functionality", () => {
- it("should render disclosure with correct open state", () => {
+ it("should render disclosure with correct closed state", () => {
render(
);
- expect(screen.getByTestId("disclosure")).toHaveAttribute(
- "data-open",
- "false",
- );
+ const disclosure = screen.getByTestId("disclosure");
+ expect(disclosure).toBeInTheDocument();
+ expect(disclosure).toHaveAttribute("data-open", "false");
});
it("should render disclosure as open when showConfig is true", () => {
- const propsWithOpenConfig = { ...defaultProps, showConfig: true };
+ const propsWithOpenConfig = {
+ ...defaultProps,
+ showConfig: true,
+ };
+
render(
);
- expect(screen.getByTestId("disclosure")).toHaveAttribute(
- "data-open",
- "true",
- );
+ const disclosure = screen.getByTestId("disclosure");
+ expect(disclosure).toHaveAttribute("data-open", "true");
});
it("should pass setShowConfig to disclosure", () => {
render(
);
- expect(screen.getByTestId("disclosure")).toHaveAttribute(
+ const disclosure = screen.getByTestId("disclosure");
+ expect(disclosure).toHaveAttribute(
"data-on-open-change",
mockSetShowConfig.toString(),
);
@@ -232,34 +272,13 @@ describe("SidebarHeaderComponent", () => {
expect(screen.getByTestId("disclosure-content")).toBeInTheDocument();
});
- it("should render disclosure trigger", () => {
+ it("should contain feature toggles within disclosure content", () => {
render(
);
- expect(screen.getByTestId("disclosure-trigger")).toBeInTheDocument();
- });
- });
+ const disclosureContent = screen.getByTestId("disclosure-content");
+ const featureToggles = screen.getByTestId("feature-toggles");
- describe("Settings Button Variants", () => {
- it("should show ghost variant when config is closed", () => {
- render(
);
-
- const settingsButton = screen.getByTestId("sidebar-options-trigger");
- expect(settingsButton).toHaveAttribute("data-variant", "ghost");
- });
-
- it("should show ghostActive variant when config is open", () => {
- const propsWithOpenConfig = { ...defaultProps, showConfig: true };
- render(
);
-
- const settingsButton = screen.getByTestId("sidebar-options-trigger");
- expect(settingsButton).toHaveAttribute("data-variant", "ghostActive");
- });
-
- it("should have correct size", () => {
- render(
);
-
- const settingsButton = screen.getByTestId("sidebar-options-trigger");
- expect(settingsButton).toHaveAttribute("data-size", "iconMd");
+ expect(disclosureContent).toContainElement(featureToggles);
});
});
@@ -299,15 +318,21 @@ describe("SidebarHeaderComponent", () => {
it("should render search input with correct props", () => {
const propsWithSearch = {
...defaultProps,
- isInputFocused: true,
search: "test search",
+ isInputFocused: true,
};
render(
);
const searchInput = screen.getByTestId("search-input");
- expect(searchInput).toHaveAttribute("data-is-focused", "true");
expect(searchInput).toHaveAttribute("data-search", "test search");
+ expect(searchInput).toHaveAttribute("data-is-focused", "true");
+ });
+
+ it("should pass search input ref and callbacks", () => {
+ render(
);
+
+ const searchInput = screen.getByTestId("search-input");
expect(searchInput).toHaveAttribute(
"data-handle-focus",
mockHandleInputFocus.toString(),
@@ -321,12 +346,6 @@ describe("SidebarHeaderComponent", () => {
mockHandleInputChange.toString(),
);
});
-
- it("should pass search input ref", () => {
- render(
);
-
- expect(screen.getByTestId("search-input")).toBeInTheDocument();
- });
});
describe("Filter Component Conditional Rendering", () => {
@@ -340,39 +359,22 @@ describe("SidebarHeaderComponent", () => {
const propsWithFilter = {
...defaultProps,
filterType: {
- source: "test_source",
- sourceHandle: undefined,
+ source: "input",
+ sourceHandle: "input",
target: undefined,
targetHandle: undefined,
- type: "string",
- color: "blue",
+ type: "input",
+ color: "#FF0000",
},
};
render(
);
- expect(screen.getByTestId("sidebar-filter")).toBeInTheDocument();
- });
-
- it("should pass correct props to filter component for input", () => {
- const propsWithInputFilter = {
- ...defaultProps,
- filterType: {
- source: "test_source",
- sourceHandle: undefined,
- target: undefined,
- targetHandle: undefined,
- type: "string",
- color: "blue",
- },
- };
-
- render(
);
-
const filterComponent = screen.getByTestId("sidebar-filter");
+ expect(filterComponent).toBeInTheDocument();
expect(filterComponent).toHaveAttribute("data-is-input", "true");
- expect(filterComponent).toHaveAttribute("data-type", "string");
- expect(filterComponent).toHaveAttribute("data-color", "blue");
+ expect(filterComponent).toHaveAttribute("data-type", "input");
+ expect(filterComponent).toHaveAttribute("data-color", "#FF0000");
});
it("should pass correct props to filter component for output", () => {
@@ -381,10 +383,10 @@ describe("SidebarHeaderComponent", () => {
filterType: {
source: undefined,
sourceHandle: undefined,
- target: "test_target",
- targetHandle: undefined,
- type: "number",
- color: "red",
+ target: "output",
+ targetHandle: "output",
+ type: "output",
+ color: "#00FF00",
},
};
@@ -392,30 +394,30 @@ describe("SidebarHeaderComponent", () => {
const filterComponent = screen.getByTestId("sidebar-filter");
expect(filterComponent).toHaveAttribute("data-is-input", "false");
- expect(filterComponent).toHaveAttribute("data-type", "number");
- expect(filterComponent).toHaveAttribute("data-color", "red");
+ expect(filterComponent).toHaveAttribute("data-type", "output");
+ expect(filterComponent).toHaveAttribute("data-color", "#00FF00");
});
- it("should handle filter reset correctly", async () => {
+ it("should handle filter reset correctly", () => {
const propsWithFilter = {
...defaultProps,
filterType: {
- source: "test_source",
- sourceHandle: undefined,
- target: undefined,
- targetHandle: undefined,
- type: "string",
- color: "blue",
+ source: "input",
+ sourceHandle: "input",
+ target: "output",
+ targetHandle: "output",
+ type: "input",
+ color: "#FF0000",
},
- data: mockAPIData,
};
render(
);
- // Since resetFilters is passed as a function, we can't directly test it
- // but we can verify the filter component receives the function
const filterComponent = screen.getByTestId("sidebar-filter");
- expect(filterComponent).toHaveAttribute("data-reset-filters");
+ expect(filterComponent).toHaveAttribute(
+ "data-reset-filters",
+ expect.stringContaining("function"),
+ );
});
});
@@ -423,138 +425,83 @@ describe("SidebarHeaderComponent", () => {
it("should have correct DOM hierarchy", () => {
render(
);
- const sidebarHeader = screen.getByTestId("sidebar-header");
- const disclosure = screen.getByTestId("disclosure");
+ const header = screen.getByTestId("sidebar-header");
const searchInput = screen.getByTestId("search-input");
+ const disclosure = screen.getByTestId("disclosure");
+ const disclosureContent = screen.getByTestId("disclosure-content");
+ const featureToggles = screen.getByTestId("feature-toggles");
- expect(sidebarHeader).toContainElement(disclosure);
- expect(sidebarHeader).toContainElement(searchInput);
+ expect(header).toContainElement(searchInput);
+ expect(header).toContainElement(disclosure);
+ expect(disclosure).toContainElement(disclosureContent);
+ expect(disclosureContent).toContainElement(featureToggles);
});
- it("should apply correct CSS classes", () => {
- render(
);
+ it("should maintain structure with filter component", () => {
+ const propsWithFilter = {
+ ...defaultProps,
+ filterType: {
+ source: "input",
+ sourceHandle: "input",
+ target: "output",
+ targetHandle: "output",
+ type: "input",
+ color: "#FF0000",
+ },
+ };
- const sidebarHeader = screen.getByTestId("sidebar-header");
- expect(sidebarHeader).toHaveClass(
- "flex",
- "w-full",
- "flex-col",
- "gap-4",
- "p-4",
- "pb-1",
- );
- });
+ render(
);
- it("should contain all expected child elements", () => {
- render(
);
+ const header = screen.getByTestId("sidebar-header");
+ const searchInput = screen.getByTestId("search-input");
+ const filterComponent = screen.getByTestId("sidebar-filter");
+ const disclosure = screen.getByTestId("disclosure");
- expect(screen.getByTestId("sidebar-trigger")).toBeInTheDocument();
- expect(screen.getByText("Components")).toBeInTheDocument();
- expect(screen.getByTestId("sidebar-options-trigger")).toBeInTheDocument();
- expect(screen.getByTestId("feature-toggles")).toBeInTheDocument();
- expect(screen.getByTestId("search-input")).toBeInTheDocument();
- });
- });
-
- describe("CSS Classes", () => {
- it("should apply correct classes to sidebar trigger", () => {
- render(
);
-
- const sidebarTrigger = screen.getByTestId("sidebar-trigger");
- expect(sidebarTrigger).toHaveClass("text-muted-foreground");
- });
-
- it("should apply correct classes to title", () => {
- render(
);
-
- const title = screen.getByText("Components");
- expect(title).toHaveClass(
- "flex-1",
- "cursor-default",
- "text-sm",
- "font-semibold",
- );
- });
-
- it("should apply correct classes to settings icon", () => {
- render(
);
-
- const settingsIcon = screen.getByTestId(
- "forwarded-icon-SlidersHorizontal",
- );
- expect(settingsIcon).toHaveClass("h-4", "w-4");
+ expect(header).toContainElement(searchInput);
+ expect(header).toContainElement(filterComponent);
+ expect(header).toContainElement(disclosure);
});
});
describe("Props Handling", () => {
it("should handle different showConfig values", () => {
- const { rerender } = render(
-
,
- );
- expect(screen.getByTestId("disclosure")).toHaveAttribute(
- "data-open",
- "false",
- );
- expect(screen.getByTestId("sidebar-options-trigger")).toHaveAttribute(
- "data-variant",
- "ghost",
- );
+ const { rerender } = render(
);
+
+ let disclosure = screen.getByTestId("disclosure");
+ expect(disclosure).toHaveAttribute("data-open", "false");
rerender(
);
- expect(screen.getByTestId("disclosure")).toHaveAttribute(
- "data-open",
- "true",
- );
- expect(screen.getByTestId("sidebar-options-trigger")).toHaveAttribute(
- "data-variant",
- "ghostActive",
- );
+ disclosure = screen.getByTestId("disclosure");
+ expect(disclosure).toHaveAttribute("data-open", "true");
});
it("should handle different search values", () => {
- const { rerender } = render(
-
,
- );
+ const { rerender } = render(
);
+
let searchInput = screen.getByTestId("search-input");
expect(searchInput).toHaveAttribute("data-search", "");
- expect(searchInput).toHaveAttribute("data-is-focused", "false");
rerender(
-
,
+
,
);
searchInput = screen.getByTestId("search-input");
- expect(searchInput).toHaveAttribute("data-search", "test");
- expect(searchInput).toHaveAttribute("data-is-focused", "true");
+ expect(searchInput).toHaveAttribute("data-search", "new search");
});
it("should handle different callback functions", () => {
const alternativeSetShowConfig = jest.fn();
- const alternativeHandleInputChange = jest.fn();
+ const alternativeProps = {
+ ...defaultProps,
+ setShowConfig: alternativeSetShowConfig,
+ };
- render(
-
,
- );
+ render(
);
- expect(screen.getByTestId("disclosure")).toHaveAttribute(
+ const disclosure = screen.getByTestId("disclosure");
+ expect(disclosure).toHaveAttribute(
"data-on-open-change",
alternativeSetShowConfig.toString(),
);
- expect(screen.getByTestId("search-input")).toHaveAttribute(
- "data-handle-change",
- alternativeHandleInputChange.toString(),
- );
});
});
@@ -563,6 +510,8 @@ describe("SidebarHeaderComponent", () => {
const propsWithoutCallbacks = {
...defaultProps,
setShowConfig: undefined as any,
+ setShowBeta: undefined as any,
+ setShowLegacy: undefined as any,
handleInputFocus: undefined as any,
handleInputBlur: undefined as any,
handleInputChange: undefined as any,
@@ -573,116 +522,96 @@ describe("SidebarHeaderComponent", () => {
}).not.toThrow();
});
- it("should handle null filterType gracefully", () => {
- render(
-
,
- );
-
- expect(screen.queryByTestId("sidebar-filter")).not.toBeInTheDocument();
- });
-
it("should handle undefined filterType gracefully", () => {
const propsWithUndefinedFilter = {
...defaultProps,
- filterType: undefined as any,
+ filterType: undefined,
};
render(
);
-
expect(screen.queryByTestId("sidebar-filter")).not.toBeInTheDocument();
});
it("should handle complex filterType objects", () => {
- const complexFilterType = {
- source: "test_source",
- sourceHandle: undefined,
- target: undefined,
- targetHandle: undefined,
- type: "List[str, int]",
- color: "custom-color",
- additionalProp: "ignored",
- };
-
const propsWithComplexFilter = {
...defaultProps,
- filterType: complexFilterType,
+ filterType: {
+ source: "input",
+ sourceHandle: "input",
+ target: undefined,
+ targetHandle: undefined,
+ type: "complex-input",
+ color: "#ABCDEF",
+ additionalProp: "ignored",
+ },
};
- render(
);
+ expect(() => {
+ render(
);
+ }).not.toThrow();
const filterComponent = screen.getByTestId("sidebar-filter");
- expect(filterComponent).toHaveAttribute("data-type", "List[str, int]");
- expect(filterComponent).toHaveAttribute("data-color", "custom-color");
+ expect(filterComponent).toBeInTheDocument();
});
});
describe("Memo Functionality", () => {
it("should render component name correctly", () => {
- render(
);
-
- // Component should render without issues
- expect(screen.getByTestId("sidebar-header")).toBeInTheDocument();
+ expect(SidebarHeaderComponent.displayName).toBe("SidebarHeaderComponent");
});
it("should handle prop changes correctly", () => {
const { rerender } = render(
);
- expect(screen.getByTestId("disclosure")).toHaveAttribute(
- "data-open",
- "false",
- );
+ expect(screen.getByTestId("sidebar-header")).toBeInTheDocument();
- rerender(
);
+ const newProps = { ...defaultProps, search: "updated search" };
+ rerender(
);
- expect(screen.getByTestId("disclosure")).toHaveAttribute(
- "data-open",
- "true",
- );
- });
- });
-
- describe("Accessibility", () => {
- it("should have proper heading structure", () => {
- render(
);
-
- const title = screen.getByText("Components");
- expect(title.tagName).toBe("H3");
- });
-
- it("should render tooltip for settings button", () => {
- render(
);
-
- expect(screen.getByTestId("tooltip")).toHaveAttribute(
- "data-content",
- "Component settings",
- );
+ const searchInput = screen.getByTestId("search-input");
+ expect(searchInput).toHaveAttribute("data-search", "updated search");
});
});
describe("Integration", () => {
it("should integrate all child components correctly", () => {
- const propsWithFilter = {
+ const fullProps = {
...defaultProps,
+ search: "integration test",
showConfig: true,
showBeta: true,
- showLegacy: true,
+ showLegacy: false,
isInputFocused: true,
- search: "test search",
filterType: {
- source: "test_source",
- sourceHandle: undefined,
+ source: "input",
+ sourceHandle: "input",
target: undefined,
targetHandle: undefined,
- type: "string",
- color: "blue",
+ type: "input",
+ color: "#123456",
},
};
- render(
);
+ render(
);
- expect(screen.getByTestId("feature-toggles")).toBeInTheDocument();
- expect(screen.getByTestId("search-input")).toBeInTheDocument();
- expect(screen.getByTestId("sidebar-filter")).toBeInTheDocument();
+ // Verify all components are integrated correctly
+ expect(screen.getByTestId("sidebar-header")).toBeInTheDocument();
+ expect(screen.getByTestId("search-input")).toHaveAttribute(
+ "data-search",
+ "integration test",
+ );
+ expect(screen.getByTestId("sidebar-filter")).toHaveAttribute(
+ "data-type",
+ "input",
+ );
+ expect(screen.getByTestId("disclosure")).toHaveAttribute(
+ "data-open",
+ "true",
+ );
+ expect(screen.getByTestId("feature-toggles")).toHaveAttribute(
+ "data-show-beta",
+ "true",
+ );
});
});
});
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarSegmentedNav.test.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarSegmentedNav.test.tsx
new file mode 100644
index 000000000..947fd02f0
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarSegmentedNav.test.tsx
@@ -0,0 +1,306 @@
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import type { SidebarSection } from "@/components/ui/sidebar";
+import SidebarSegmentedNav, { NAV_ITEMS } from "../sidebarSegmentedNav";
+
+// Mock the hooks and components
+const mockUseSidebar: {
+ activeSection: SidebarSection;
+ setActiveSection: jest.Mock;
+ toggleSidebar: jest.Mock;
+ open: boolean;
+} = {
+ activeSection: "components" as SidebarSection,
+ setActiveSection: jest.fn(),
+ toggleSidebar: jest.fn(),
+ open: true,
+};
+
+const mockUseSearchContext = {
+ focusSearch: jest.fn(),
+ isSearchFocused: false,
+ setSearch: jest.fn(),
+};
+
+jest.mock("@/components/ui/sidebar", () => ({
+ useSidebar: () => mockUseSidebar,
+ SidebarMenu: ({ children, className }: any) => (
+
+ {children}
+
+ ),
+ SidebarMenuButton: ({
+ children,
+ onClick,
+ isActive,
+ className,
+ size,
+ "data-testid": testId,
+ }: any) => (
+
+ ),
+ SidebarMenuItem: ({ children }: any) => (
+
{children}
+ ),
+}));
+
+jest.mock("../../index", () => ({
+ useSearchContext: () => mockUseSearchContext,
+}));
+
+jest.mock("@/components/common/genericIconComponent", () => ({
+ __esModule: true,
+ default: ({ name, className }: any) => (
+
+ {name}
+
+ ),
+}));
+
+jest.mock("@/components/common/shadTooltipComponent", () => ({
+ __esModule: true,
+ default: ({ children, content, side }: any) => (
+
+ {children}
+
+ ),
+}));
+
+jest.mock("@/utils/utils", () => ({
+ cn: (...args: any[]) => args.filter(Boolean).join(" "),
+}));
+
+describe("SidebarSegmentedNav", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ // Reset to default values
+ mockUseSidebar.activeSection = "components";
+ mockUseSidebar.open = true;
+ mockUseSearchContext.isSearchFocused = false;
+ jest.clearAllTimers();
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it("renders all navigation items", () => {
+ render(
);
+
+ // Check that all nav items are rendered
+ NAV_ITEMS.forEach((item) => {
+ expect(screen.getByTestId(`sidebar-nav-${item.id}`)).toBeInTheDocument();
+ expect(screen.getByTestId(`icon-${item.icon}`)).toBeInTheDocument();
+ });
+ });
+
+ it("renders correct structure", () => {
+ render(
);
+
+ expect(screen.getByTestId("sidebar-menu")).toBeInTheDocument();
+ expect(screen.getAllByTestId("sidebar-menu-item")).toHaveLength(
+ NAV_ITEMS.length,
+ );
+ expect(screen.getAllByTestId("tooltip")).toHaveLength(NAV_ITEMS.length);
+ });
+
+ it("displays correct tooltips for each item", () => {
+ render(
);
+
+ NAV_ITEMS.forEach((item) => {
+ const tooltips = screen.getAllByTestId("tooltip");
+ const itemTooltip = tooltips.find(
+ (tooltip) => tooltip.getAttribute("data-content") === item.tooltip,
+ );
+ expect(itemTooltip).toBeInTheDocument();
+ expect(itemTooltip).toHaveAttribute("data-side", "right");
+ });
+ });
+
+ it("sets active state for current active section", () => {
+ mockUseSidebar.activeSection = "mcp";
+ render(
);
+
+ const mcpButton = screen.getByTestId("sidebar-nav-mcp");
+ expect(mcpButton).toHaveAttribute("data-active", "true");
+
+ // Other buttons should not be active
+ const componentsButton = screen.getByTestId("sidebar-nav-components");
+ expect(componentsButton).toHaveAttribute("data-active", "false");
+ });
+
+ it("sets active state for search when search is focused", () => {
+ mockUseSidebar.activeSection = "components";
+ mockUseSearchContext.isSearchFocused = true;
+ render(
);
+
+ const searchButton = screen.getByTestId("sidebar-nav-search");
+ expect(searchButton).toHaveAttribute("data-active", "true");
+ });
+
+ it("calls setActiveSection when clicking on different section", () => {
+ render(
);
+
+ const mcpButton = screen.getByTestId("sidebar-nav-mcp");
+ fireEvent.click(mcpButton);
+
+ expect(mockUseSidebar.setActiveSection).toHaveBeenCalledWith("mcp");
+ expect(mockUseSidebar.setActiveSection).toHaveBeenCalledTimes(1);
+ });
+
+ it("resets search when changing active sections", () => {
+ render(
);
+
+ const mcpButton = screen.getByTestId("sidebar-nav-mcp");
+ fireEvent.click(mcpButton);
+
+ expect(mockUseSearchContext.setSearch).toHaveBeenCalledWith("");
+ expect(mockUseSearchContext.setSearch).toHaveBeenCalledTimes(1);
+ });
+
+ it("toggles sidebar when clicking on currently active section", () => {
+ mockUseSidebar.activeSection = "components";
+ render(
);
+
+ const componentsButton = screen.getByTestId("sidebar-nav-components");
+ fireEvent.click(componentsButton);
+
+ expect(mockUseSidebar.toggleSidebar).toHaveBeenCalledTimes(1);
+ expect(mockUseSidebar.setActiveSection).not.toHaveBeenCalled();
+ });
+
+ it("resets search when toggling sidebar on active section", () => {
+ mockUseSidebar.activeSection = "components";
+ render(
);
+
+ const componentsButton = screen.getByTestId("sidebar-nav-components");
+ fireEvent.click(componentsButton);
+
+ expect(mockUseSearchContext.setSearch).toHaveBeenCalledWith("");
+ expect(mockUseSearchContext.setSearch).toHaveBeenCalledTimes(1);
+ });
+
+ it("opens sidebar and sets active section when sidebar is closed", () => {
+ mockUseSidebar.open = false;
+ render(
);
+
+ const bundlesButton = screen.getByTestId("sidebar-nav-bundles");
+ fireEvent.click(bundlesButton);
+
+ expect(mockUseSidebar.setActiveSection).toHaveBeenCalledWith("bundles");
+ expect(mockUseSidebar.toggleSidebar).toHaveBeenCalledTimes(1);
+ });
+
+ it("resets search when opening sidebar and changing sections", () => {
+ mockUseSidebar.open = false;
+ render(
);
+
+ const bundlesButton = screen.getByTestId("sidebar-nav-bundles");
+ fireEvent.click(bundlesButton);
+
+ expect(mockUseSearchContext.setSearch).toHaveBeenCalledWith("");
+ expect(mockUseSearchContext.setSearch).toHaveBeenCalledTimes(1);
+ });
+
+ it("focuses search input when search section is clicked", async () => {
+ render(
);
+
+ const searchButton = screen.getByTestId("sidebar-nav-search");
+ fireEvent.click(searchButton);
+
+ expect(mockUseSidebar.setActiveSection).toHaveBeenCalledWith("search");
+
+ // Fast-forward the setTimeout
+ jest.advanceTimersByTime(100);
+
+ await waitFor(() => {
+ expect(mockUseSearchContext.focusSearch).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it("focuses search input even when sidebar is closed", async () => {
+ mockUseSidebar.open = false;
+ render(
);
+
+ const searchButton = screen.getByTestId("sidebar-nav-search");
+ fireEvent.click(searchButton);
+
+ expect(mockUseSidebar.setActiveSection).toHaveBeenCalledWith("search");
+ expect(mockUseSidebar.toggleSidebar).toHaveBeenCalledTimes(1);
+
+ // Fast-forward the setTimeout
+ jest.advanceTimersByTime(100);
+
+ await waitFor(() => {
+ expect(mockUseSearchContext.focusSearch).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it("renders accessibility labels correctly", () => {
+ render(
);
+
+ NAV_ITEMS.forEach((item) => {
+ const button = screen.getByTestId(`sidebar-nav-${item.id}`);
+ expect(button).toHaveTextContent(item.label);
+ });
+ });
+
+ it("applies correct CSS classes", () => {
+ mockUseSidebar.activeSection = "mcp";
+ render(
);
+
+ const mcpButton = screen.getByTestId("sidebar-nav-mcp");
+ expect(mcpButton).toHaveClass("bg-accent", "text-accent-foreground");
+
+ const componentsButton = screen.getByTestId("sidebar-nav-components");
+ expect(componentsButton).toHaveClass("text-muted-foreground");
+ });
+
+ it("renders icons with correct styling", () => {
+ render(
);
+
+ NAV_ITEMS.forEach((item) => {
+ const icon = screen.getByTestId(`icon-${item.icon}`);
+ expect(icon).toHaveClass("h-5", "w-5");
+ });
+ });
+
+ it("handles multiple rapid clicks correctly", () => {
+ render(
);
+
+ const mcpButton = screen.getByTestId("sidebar-nav-mcp");
+
+ // Click multiple times rapidly
+ fireEvent.click(mcpButton);
+ fireEvent.click(mcpButton);
+ fireEvent.click(mcpButton);
+
+ // Should have called setActiveSection for each click since activeSection !== mcp
+ expect(mockUseSidebar.setActiveSection).toHaveBeenCalledTimes(3);
+ expect(mockUseSidebar.setActiveSection).toHaveBeenCalledWith("mcp");
+ });
+
+ it("exports NAV_ITEMS correctly", () => {
+ expect(NAV_ITEMS).toHaveLength(4);
+ expect(NAV_ITEMS[0]).toEqual({
+ id: "search",
+ icon: "search",
+ label: "Search",
+ tooltip: "Search",
+ });
+ expect(NAV_ITEMS[3]).toEqual({
+ id: "bundles",
+ icon: "blocks",
+ label: "Bundles",
+ tooltip: "Bundles",
+ });
+ });
+});
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryGroup.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryGroup.tsx
index 5515ec5f6..76d9a08f8 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryGroup.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryGroup.tsx
@@ -1,12 +1,15 @@
-import { memo, useState } from "react";
+import { memo } from "react";
import {
SidebarGroup,
SidebarGroupContent,
+ SidebarGroupLabel,
SidebarMenu,
} from "@/components/ui/sidebar";
+import { ENABLE_NEW_SIDEBAR } from "@/customization/feature-flags";
import { SIDEBAR_BUNDLES } from "@/utils/styleUtils";
import type { CategoryGroupProps } from "../types";
import { CategoryDisclosure } from "./categoryDisclouse";
+import { SearchConfigTrigger } from "./searchConfigTrigger";
export const CategoryGroup = memo(function CategoryGroup({
dataFilter,
@@ -18,17 +21,29 @@ export const CategoryGroup = memo(function CategoryGroup({
nodeColors,
onDragStart,
sensitiveSort,
+ showConfig,
+ setShowConfig,
}: CategoryGroupProps) {
return (
+ {ENABLE_NEW_SIDEBAR && (
+
+ Components
+
+
+ )}
{Object.entries(dataFilter)
.filter(
([categoryName, items]) =>
- // filter out bundles
+ // filter out bundles and MCP
!SIDEBAR_BUNDLES.some((cat) => cat.name === categoryName) &&
categoryName !== "custom_component" &&
+ categoryName !== "MCP" &&
Object.keys(items).length > 0,
)
.sort(([aName], [bName]) => {
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/emptySearchComponent.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/emptySearchComponent.tsx
index 1eb49cd5f..2bc03684f 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/emptySearchComponent.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/emptySearchComponent.tsx
@@ -1,21 +1,45 @@
+import { ENABLE_NEW_SIDEBAR } from "@/customization/feature-flags";
+import { SearchConfigTrigger } from "./searchConfigTrigger";
+
+interface NoResultsMessageProps {
+ onClearSearch: () => void;
+ message?: string;
+ clearSearchText?: string;
+ additionalText?: string;
+ showConfig?: boolean;
+ setShowConfig?: (show: boolean) => void;
+}
+
const NoResultsMessage = ({
onClearSearch,
message = "No components found.",
clearSearchText = "Clear your search",
additionalText = "or filter and try a different query.",
-}) => {
+ showConfig = false,
+ setShowConfig,
+}: NoResultsMessageProps) => {
return (
-
-
- {message}{" "}
-
- {clearSearchText}
- {" "}
- {additionalText}
-
+
+ {ENABLE_NEW_SIDEBAR && setShowConfig && (
+
+
+
+ )}
+
);
};
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/searchConfigTrigger.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/searchConfigTrigger.tsx
new file mode 100644
index 000000000..0a10bf642
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/searchConfigTrigger.tsx
@@ -0,0 +1,31 @@
+import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
+import ShadTooltip from "@/components/common/shadTooltipComponent";
+import { Button } from "@/components/ui/button";
+
+interface SearchConfigTriggerProps {
+ showConfig: boolean;
+ setShowConfig: (show: boolean) => void;
+}
+
+export const SearchConfigTrigger = ({
+ showConfig,
+ setShowConfig,
+}: SearchConfigTriggerProps) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarBundles.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarBundles.tsx
index 505ebb8f5..9fb244614 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarBundles.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarBundles.tsx
@@ -5,8 +5,10 @@ import {
SidebarGroupLabel,
SidebarMenu,
} from "@/components/ui/sidebar";
+import { ENABLE_NEW_SIDEBAR } from "@/customization/feature-flags";
import type { SidebarGroupProps } from "../types";
import { BundleItem } from "./bundleItems";
+import { SearchConfigTrigger } from "./searchConfigTrigger";
export const MemoizedSidebarGroup = memo(
({
@@ -20,6 +22,9 @@ export const MemoizedSidebarGroup = memo(
handleKeyDownInput,
openCategories,
setOpenCategories,
+ showSearchConfigTrigger,
+ showConfig,
+ setShowConfig,
}: SidebarGroupProps) => {
const sortedBundles = useMemo(() => {
return BUNDLES.toSorted((a, b) => {
@@ -37,8 +42,14 @@ export const MemoizedSidebarGroup = memo(
return (
-
- Bundles
+
+ Bundles
+ {showSearchConfigTrigger && ENABLE_NEW_SIDEBAR && (
+
+ )}
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarFooterButtons.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarFooterButtons.tsx
index 1a7d34098..0cec6d12f 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarFooterButtons.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarFooterButtons.tsx
@@ -1,63 +1,89 @@
+import { useState } from "react";
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import { Button } from "@/components/ui/button";
-import { SidebarMenuButton } from "@/components/ui/sidebar";
-import { CustomLink } from "@/customization/components/custom-link";
-import { ENABLE_LANGFLOW_STORE } from "@/customization/feature-flags";
+import { SidebarMenuButton, useSidebar } from "@/components/ui/sidebar";
+import { ENABLE_NEW_SIDEBAR } from "@/customization/feature-flags";
+import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
+import AddMcpServerModal from "@/modals/addMcpServerModal";
const SidebarMenuButtons = ({
- hasStore = false,
customComponent,
addComponent,
isLoading = false,
}) => {
+ const { activeSection } = useSidebar();
+ const [addMcpOpen, setAddMcpOpen] = useState(false);
+ const navigate = useCustomNavigate();
+
+ const handleAddMcpServerClick = () => {
+ setAddMcpOpen(true);
+ };
+
return (
<>
- {/* TODO: Remove this on cleanup */}
- {ENABLE_LANGFLOW_STORE && hasStore && (
-
-
-
+ {ENABLE_NEW_SIDEBAR && activeSection === "mcp" ? (
+ <>
+
+
+
+
+
-
+
+ Manage Servers
+
+
+
+
+ >
+ ) : (
+
+
)}
-
-
-
>
);
};
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader.tsx
index 1347346c7..11e08d0ff 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader.tsx
@@ -9,6 +9,7 @@ import {
DisclosureTrigger,
} from "@/components/ui/disclosure";
import { SidebarHeader, SidebarTrigger } from "@/components/ui/sidebar";
+import { ENABLE_NEW_SIDEBAR } from "@/customization/feature-flags";
import type { SidebarHeaderComponentProps } from "../types";
import FeatureToggles from "./featureTogglesComponent";
import { SearchInput } from "./searchInput";
@@ -33,41 +34,43 @@ export const SidebarHeaderComponent = memo(function SidebarHeaderComponent({
data,
}: SidebarHeaderComponentProps) {
return (
-
-
-
-
-
-
-
- Components
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ {!ENABLE_NEW_SIDEBAR && (
+
+
+
+
+
+
+ Components
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
)}
+ {ENABLE_NEW_SIDEBAR && (
+
+
+
+
+
+ )}
);
});
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarSegmentedNav.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarSegmentedNav.tsx
new file mode 100644
index 000000000..ea9556a64
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarSegmentedNav.tsx
@@ -0,0 +1,98 @@
+import ForwardedIconComponent from "@/components/common/genericIconComponent";
+import ShadTooltip from "@/components/common/shadTooltipComponent";
+import {
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ type SidebarSection,
+ useSidebar,
+} from "@/components/ui/sidebar";
+import { cn } from "@/utils/utils";
+import { useSearchContext } from "../index";
+
+export type { SidebarSection };
+
+interface NavItem {
+ id: SidebarSection;
+ icon: string;
+ label: string;
+ tooltip: string;
+}
+
+export const NAV_ITEMS: NavItem[] = [
+ {
+ id: "search",
+ icon: "search",
+ label: "Search",
+ tooltip: "Search",
+ },
+ {
+ id: "components",
+ icon: "component",
+ label: "Components",
+ tooltip: "Components",
+ },
+ {
+ id: "mcp",
+ icon: "Mcp",
+ label: "MCP",
+ tooltip: "MCP",
+ },
+ {
+ id: "bundles",
+ icon: "blocks",
+ label: "Bundles",
+ tooltip: "Bundles",
+ },
+];
+
+export default function SidebarSegmentedNav() {
+ const { activeSection, setActiveSection, toggleSidebar, open } = useSidebar();
+ const { focusSearch, isSearchFocused, setSearch } = useSearchContext();
+ return (
+
+
+ {NAV_ITEMS.map((item) => (
+
+
+ {
+ setSearch?.("");
+ if (activeSection === item.id && open) {
+ toggleSidebar();
+ } else {
+ setActiveSection(item.id);
+ if (!open) {
+ toggleSidebar();
+ }
+ // Focus search input when search section is selected
+ if (item.id === "search") {
+ // Add a small delay to ensure the sidebar is open and input is rendered
+ setTimeout(() => focusSearch(), 100);
+ }
+ }
+ }}
+ isActive={
+ activeSection === item.id ||
+ (item.id === "search" && isSearchFocused)
+ }
+ className={cn(
+ "flex h-8 w-8 items-center justify-center rounded-md p-0 transition-all duration-200",
+ activeSection === item.id ||
+ (item.id === "search" && isSearchFocused)
+ ? "bg-accent text-accent-foreground"
+ : "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
+ )}
+ data-testid={`sidebar-nav-${item.id}`}
+ >
+
+ {item.label}
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/index.tsx
index 3b354ec24..f1918d981 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/index.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/index.tsx
@@ -1,6 +1,15 @@
import Fuse from "fuse.js";
import { cloneDeep } from "lodash";
-import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
+import {
+ createContext,
+ memo,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { useShallow } from "zustand/react/shallow";
import {
@@ -10,25 +19,27 @@ import {
useSidebar,
} from "@/components/ui/sidebar";
import SkeletonGroup from "@/components/ui/skeletonGroup";
+import { useGetMCPServers } from "@/controllers/API/queries/mcp/use-get-mcp-servers";
+import { ENABLE_NEW_SIDEBAR } from "@/customization/feature-flags";
import { useAddComponent } from "@/hooks/use-add-component";
import { useShortcutsStore } from "@/stores/shortcuts";
-import { useStoreStore } from "@/stores/storeStore";
-import { checkChatInput, checkWebhookInput } from "@/utils/reactflowUtils";
import {
nodeColors,
SIDEBAR_BUNDLES,
SIDEBAR_CATEGORIES,
} from "@/utils/styleUtils";
-import useAlertStore from "../../../../stores/alertStore";
+import { cn } from "@/utils/utils";
import useFlowStore from "../../../../stores/flowStore";
import { useTypesStore } from "../../../../stores/typesStore";
import type { APIClassType } from "../../../../types/api";
import isWrappedWithClass from "../PageComponent/utils/is-wrapped-with-class";
import { CategoryGroup } from "./components/categoryGroup";
import NoResultsMessage from "./components/emptySearchComponent";
+import McpSidebarGroup from "./components/McpSidebarGroup";
import MemoizedSidebarGroup from "./components/sidebarBundles";
import SidebarMenuButtons from "./components/sidebarFooterButtons";
import { SidebarHeaderComponent } from "./components/sidebarHeader";
+import SidebarSegmentedNav from "./components/sidebarSegmentedNav";
import { applyBetaFilter } from "./helpers/apply-beta-filter";
import { applyEdgeFilter } from "./helpers/apply-edge-filter";
import { applyLegacyFilter } from "./helpers/apply-legacy-filter";
@@ -42,6 +53,95 @@ import { UniqueInputsComponents } from "./types";
const CATEGORIES = SIDEBAR_CATEGORIES;
const BUNDLES = SIDEBAR_BUNDLES;
+// Search context for the sidebar
+export type SearchContextType = {
+ focusSearch: () => void;
+ isSearchFocused: boolean;
+ // Additional properties for the sidebar to use
+ search?: string;
+ setSearch?: (value: string) => void;
+ searchInputRef?: React.RefObject;
+ handleInputFocus?: () => void;
+ handleInputBlur?: () => void;
+ handleInputChange?: (event: React.ChangeEvent) => void;
+};
+
+export const SearchContext = createContext(null);
+
+export function useSearchContext() {
+ const context = useContext(SearchContext);
+ if (!context) {
+ throw new Error("useSearchContext must be used within SearchProvider");
+ }
+ return context;
+}
+
+interface SearchProviderProps {
+ children: React.ReactNode;
+ searchInputRef: React.RefObject;
+ isSearchFocused: boolean;
+}
+
+// Create a provider that can be used at the FlowPage level
+export function FlowSearchProvider({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const [search, setSearch] = useState("");
+ const [isInputFocused, setIsInputFocused] = useState(false);
+ const searchInputRef = useRef(null);
+
+ const focusSearchInput = useCallback(() => {
+ if (searchInputRef.current) {
+ searchInputRef.current.focus();
+ }
+ }, []);
+
+ const handleInputFocus = useCallback(() => {
+ setIsInputFocused(true);
+ }, []);
+
+ const handleInputBlur = useCallback(() => {
+ setIsInputFocused(false);
+ }, []);
+
+ const handleInputChange = useCallback(
+ (event: React.ChangeEvent) => {
+ setSearch(event.target.value);
+ },
+ [],
+ );
+
+ const searchContextValue = useMemo(
+ () => ({
+ focusSearch: focusSearchInput,
+ isSearchFocused: isInputFocused,
+ // Also expose the search state and handlers for the sidebar to use
+ search,
+ setSearch,
+ searchInputRef,
+ handleInputFocus,
+ handleInputBlur,
+ handleInputChange,
+ }),
+ [
+ focusSearchInput,
+ isInputFocused,
+ search,
+ handleInputFocus,
+ handleInputBlur,
+ handleInputChange,
+ ],
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
interface FlowSidebarComponentProps {
isLoading?: boolean;
showLegacy?: boolean;
@@ -59,21 +159,72 @@ export function FlowSidebarComponent({ isLoading }: FlowSidebarComponentProps) {
})),
);
- const hasStore = useStoreStore((state) => state.hasStore);
- const { setOpen } = useSidebar();
+ const { activeSection, setOpen, setActiveSection } = useSidebar();
const addComponent = useAddComponent();
+ // Get MCP servers for search functionality (only when new sidebar is enabled)
+ const {
+ data: mcpServers,
+ isLoading: mcpLoading,
+ isSuccess: mcpSuccess,
+ isError: mcpError,
+ } = useGetMCPServers({ enabled: ENABLE_NEW_SIDEBAR });
+
+ // Get search state from context
+ const context = useSearchContext();
+ // Unconditional fallback ref to satisfy Rules of Hooks
+ const fallbackSearchInputRef = useRef(null);
+ const {
+ search = "",
+ setSearch = () => {},
+ searchInputRef = fallbackSearchInputRef,
+ isSearchFocused = false,
+ handleInputFocus = () => {},
+ handleInputBlur = () => {},
+ handleInputChange = () => {},
+ } = context;
+
// State
- const [dataFilter, setFilterData] = useState(data);
- const [search, setSearch] = useState("");
const [fuse, setFuse] = useState | null>(null);
const [openCategories, setOpenCategories] = useState([]);
const [showConfig, setShowConfig] = useState(false);
const [showBeta, setShowBeta] = useState(true);
const [showLegacy, setShowLegacy] = useState(false);
- const [isInputFocused, setIsInputFocused] = useState(false);
+ const [mcpSearchData, setMcpSearchData] = useState([]);
- const searchInputRef = useRef(null);
+ // Create base data that includes MCP category when available
+ const baseData = useMemo(() => {
+ if (mcpSuccess && mcpServers && data["agents"]?.["MCPTools"]) {
+ const mcpComponent = data["agents"]["MCPTools"];
+ const newMcpSearchData = mcpServers.map((mcpServer) => ({
+ ...mcpComponent,
+ display_name: mcpServer.name,
+ description: `MCP Server: ${mcpServer.name}`,
+ category: "MCP",
+ key: `mcp_${mcpServer.name}`,
+ template: {
+ ...mcpComponent.template,
+ mcp_server: {
+ ...mcpComponent.template.mcp_server,
+ value: mcpServer,
+ },
+ },
+ }));
+
+ const mcpCategoryData: Record = {};
+ newMcpSearchData.forEach((mcp) => {
+ mcpCategoryData[mcp.display_name] = mcp;
+ });
+
+ return {
+ ...data,
+ MCP: mcpCategoryData,
+ };
+ }
+ return data;
+ }, [data, mcpSuccess, mcpServers]);
+
+ const [dataFilter, setFilterData] = useState(baseData);
const customComponent = useMemo(() => {
return data?.["custom_component"]?.["CustomComponent"] ?? null;
@@ -88,23 +239,29 @@ export function FlowSidebarComponent({ isLoading }: FlowSidebarComponentProps) {
item: { ...result.item, score: result.score },
}));
+ const fuseCategories = fuseResults.map((result) => result.item.category);
+ const combinedResults = combinedResultsFn(fuseResults, baseData);
+ const traditionalResults = traditionalSearchMetadata(baseData, searchTerm);
+
return {
fuseResults,
- fuseCategories: fuseResults.map((result) => result.item.category),
- combinedResults: combinedResultsFn(fuseResults, data),
- traditionalResults: traditionalSearchMetadata(data, searchTerm),
+ fuseCategories,
+ combinedResults,
+ traditionalResults,
};
- }, [search, fuse, data]);
+ }, [search, fuse, baseData]);
const searchFilteredData = useMemo(() => {
- if (!search || !searchResults) return cloneDeep(data);
+ if (!search || !searchResults) return cloneDeep(baseData);
- return filteredDataFn(
- data,
+ const filteredData = filteredDataFn(
+ baseData,
searchResults.combinedResults,
searchResults.traditionalResults,
);
- }, [data, search, searchResults]);
+
+ return filteredData;
+ }, [baseData, search, searchResults]);
const sortedCategories = useMemo(() => {
if (!searchResults || !searchFilteredData) return [];
@@ -138,9 +295,10 @@ export function FlowSidebarComponent({ isLoading }: FlowSidebarComponentProps) {
const hasResults = useMemo(() => {
return Object.entries(dataFilter).some(
([category, items]) =>
- Object.keys(items).length > 0 &&
- (CATEGORIES.find((c) => c.name === category) ||
- BUNDLES.find((b) => b.name === category)),
+ (Object.keys(items).length > 0 &&
+ (CATEGORIES.find((c) => c.name === category) ||
+ BUNDLES.find((b) => b.name === category))) ||
+ (dataFilter["MCP"] && Object.keys(dataFilter["MCP"]).length > 0),
);
}, [dataFilter]);
@@ -160,32 +318,14 @@ export function FlowSidebarComponent({ isLoading }: FlowSidebarComponentProps) {
const handleClearSearch = useCallback(() => {
setSearch("");
- setFilterData(data);
+ setFilterData(baseData);
setOpenCategories([]);
- }, [data]);
-
- const handleInputFocus = useCallback(() => {
- setIsInputFocused(true);
- }, []);
-
- const handleInputBlur = useCallback(() => {
- setIsInputFocused(false);
- }, []);
-
- const handleSearchInput = useCallback((value: string) => {
- setSearch(value);
- }, []);
-
- const handleInputChange = useCallback(
- (event: React.ChangeEvent) => {
- handleSearchInput(event.target.value);
- },
- [handleSearchInput],
- );
+ }, [baseData, setSearch]);
useEffect(() => {
if (filterType) {
setOpen(true);
+ setActiveSection("search");
}
}, [filterType, setOpen]);
@@ -200,14 +340,25 @@ export function FlowSidebarComponent({ isLoading }: FlowSidebarComponentProps) {
}
}, [finalFilteredData, search, filterType, getFilterEdge]);
+ // Update dataFilter when baseData changes
+ useEffect(() => {
+ setFilterData(baseData);
+ }, [baseData]);
+
useEffect(() => {
const options = {
- keys: ["display_name", "description", "type", "category"],
+ keys: [
+ "display_name",
+ "description",
+ "type",
+ "category",
+ "mcpServerName",
+ ],
threshold: 0.2,
includeScore: true,
};
- const fuseData = Object.entries(data).flatMap(([category, items]) =>
+ const fuseData = Object.entries(baseData).flatMap(([category, items]) =>
Object.entries(items).map(([key, value]) => ({
...value,
category,
@@ -215,14 +366,36 @@ export function FlowSidebarComponent({ isLoading }: FlowSidebarComponentProps) {
})),
);
+ // MCP data is already included in baseData, but we still need mcpSearchData for non-search display
+ if (mcpSuccess && mcpServers && data["agents"]?.["MCPTools"]) {
+ const mcpComponent = data["agents"]["MCPTools"];
+ const newMcpSearchData = mcpServers.map((mcpServer) => ({
+ ...mcpComponent,
+ mcpServerName: mcpServer.name, // adds this field and makes it searchable
+ category: "MCP",
+ key: `mcp_${mcpServer.name}`,
+ template: {
+ ...mcpComponent.template,
+ mcp_server: {
+ ...mcpComponent.template.mcp_server,
+ value: mcpServer,
+ },
+ },
+ }));
+
+ setMcpSearchData(newMcpSearchData);
+ // No need to push to fuseData since it's already in baseData
+ } else {
+ setMcpSearchData([]);
+ }
setFuse(new Fuse(fuseData, options));
- }, [data]);
+ }, [baseData, mcpSuccess, mcpServers]);
useEffect(() => {
if (getFilterEdge.length !== 0) {
setSearch("");
}
- }, [getFilterEdge, data]);
+ }, [getFilterEdge, baseData]);
useEffect(() => {
if (search === "" && getFilterEdge.length === 0) {
@@ -255,7 +428,7 @@ export function FlowSidebarComponent({ isLoading }: FlowSidebarComponentProps) {
},
{
enableOnFormTags: true,
- enabled: isInputFocused,
+ enabled: isSearchFocused,
},
);
@@ -277,15 +450,46 @@ export function FlowSidebarComponent({ isLoading }: FlowSidebarComponentProps) {
[],
);
- const hasBundleItems = useMemo(
- () =>
- BUNDLES.some(
- (item) =>
- dataFilter[item.name] &&
- Object.keys(dataFilter[item.name]).length > 0,
- ),
- [dataFilter],
- );
+ const hasCoreComponents = useMemo(() => {
+ const categoriesWithItems = CATEGORIES.filter(
+ (item) =>
+ dataFilter[item.name] && Object.keys(dataFilter[item.name]).length > 0,
+ );
+ const result = categoriesWithItems.length > 0;
+ return result;
+ }, [dataFilter]);
+
+ const hasBundleItems = useMemo(() => {
+ const bundlesWithItems = BUNDLES.filter(
+ (item) =>
+ dataFilter[item.name] && Object.keys(dataFilter[item.name]).length > 0,
+ );
+ const result = bundlesWithItems.length > 0;
+ return result;
+ }, [dataFilter]);
+
+ const hasMcpComponents = useMemo(() => {
+ return dataFilter["MCP"] && Object.keys(dataFilter["MCP"]).length > 0;
+ }, [dataFilter]);
+
+ const hasMcpServers = Boolean(mcpServers && mcpServers.length > 0);
+
+ const hasSearchInput = search !== "" || filterType !== undefined;
+ console.log("hasSearchInput", hasSearchInput);
+
+ const showComponents =
+ (ENABLE_NEW_SIDEBAR &&
+ hasCoreComponents &&
+ (activeSection === "components" || activeSection === "search")) ||
+ (hasSearchInput && hasCoreComponents && ENABLE_NEW_SIDEBAR) ||
+ !ENABLE_NEW_SIDEBAR;
+ const showBundles =
+ (hasBundleItems && ENABLE_NEW_SIDEBAR && activeSection === "bundles") ||
+ (hasSearchInput && hasBundleItems && ENABLE_NEW_SIDEBAR) ||
+ !ENABLE_NEW_SIDEBAR;
+ const showMcp =
+ (ENABLE_NEW_SIDEBAR && activeSection === "mcp") ||
+ (hasSearchInput && hasMcpComponents && ENABLE_NEW_SIDEBAR);
return (
-
+
+ {ENABLE_NEW_SIDEBAR &&
}
+
+
-
- {isLoading ? (
-
- ) : (
- <>
- {hasResults ? (
+
+ {isLoading ? (
+
+ ) : (
<>
-
-
- {hasBundleItems && (
-
+ {showComponents && (
+
+ )}
+ {showMcp && (
+
+ )}
+ {showBundles && (
+
+ )}
+ >
+ ) : (
+
)}
>
- ) : (
-
)}
- >
- )}
-
-
-
-
+
+ {ENABLE_NEW_SIDEBAR &&
+ activeSection === "mcp" &&
+ !hasMcpServers ? null : (
+
+
+
+ )}
+
+
);
}
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/types/index.ts b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/types/index.ts
index 76525281a..faf7ec9ee 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/types/index.ts
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/types/index.ts
@@ -22,6 +22,8 @@ export interface CategoryGroupProps {
data: { type: string; node?: APIClassType },
) => void;
sensitiveSort: (a: string, b: string) => number;
+ showConfig: boolean;
+ setShowConfig: (show: boolean) => void;
}
export interface SidebarGroupProps {
@@ -41,6 +43,9 @@ export interface SidebarGroupProps {
) => void;
openCategories: string[];
setOpenCategories: Dispatch>;
+ showSearchConfigTrigger: boolean;
+ showConfig: boolean;
+ setShowConfig: (show: boolean) => void;
}
export interface BundleItemProps {
diff --git a/src/frontend/src/pages/FlowPage/index.tsx b/src/frontend/src/pages/FlowPage/index.tsx
index a0ce1bff6..f59457ffe 100644
--- a/src/frontend/src/pages/FlowPage/index.tsx
+++ b/src/frontend/src/pages/FlowPage/index.tsx
@@ -3,6 +3,7 @@ import { useBlocker, useParams } from "react-router-dom";
import { SidebarProvider } from "@/components/ui/sidebar";
import { useGetFlow } from "@/controllers/API/queries/flows/use-get-flow";
import { useGetTypes } from "@/controllers/API/queries/flows/use-get-types";
+import { ENABLE_NEW_SIDEBAR } from "@/customization/feature-flags";
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
import useSaveFlow from "@/hooks/flows/use-save-flow";
import { useIsMobile } from "@/hooks/use-mobile";
@@ -12,7 +13,10 @@ import { useTypesStore } from "@/stores/typesStore";
import { customStringify } from "@/utils/reactflowUtils";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
-import { FlowSidebarComponent } from "./components/flowSidebarComponent";
+import {
+ FlowSearchProvider,
+ FlowSidebarComponent,
+} from "./components/flowSidebarComponent";
import Page from "./components/PageComponent";
export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
@@ -160,13 +164,19 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
{currentFlow && (
-
- {!view && }
-
-
-
+
+
+ {!view && }
+
+
+
+
)}
diff --git a/src/frontend/tests/core/features/keyboardComponentSearch.spec.ts b/src/frontend/tests/core/features/keyboardComponentSearch.spec.ts
index a624834b3..468f78d22 100644
--- a/src/frontend/tests/core/features/keyboardComponentSearch.spec.ts
+++ b/src/frontend/tests/core/features/keyboardComponentSearch.spec.ts
@@ -37,6 +37,7 @@ test(
// Press Tab to focus first result
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
+ await page.keyboard.press("Tab");
// Verify some expected chat-related components are visible
await expect(page.getByTestId("input_outputChat Input")).toBeVisible();
@@ -60,6 +61,7 @@ test(
// Verify disclosures open with new search
await expect(page.getByTestId("processingPrompt Template")).toBeVisible();
+ await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
await page.keyboard.press("Enter");
diff --git a/src/frontend/tests/core/features/stop-building.spec.ts b/src/frontend/tests/core/features/stop-building.spec.ts
index ea6c1a497..8fd906c55 100644
--- a/src/frontend/tests/core/features/stop-building.spec.ts
+++ b/src/frontend/tests/core/features/stop-building.spec.ts
@@ -23,7 +23,7 @@ test(
await page
.getByTestId("input_outputText Input")
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
- targetPosition: { x: 0, y: 0 },
+ targetPosition: { x: 50, y: 50 },
});
await page.getByTestId("canvas_controls_dropdown").click();
@@ -38,7 +38,7 @@ test(
await page
.getByTestId("dataURL")
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
- targetPosition: { x: 100, y: 200 },
+ targetPosition: { x: 50, y: 300 },
});
//third component
@@ -49,7 +49,7 @@ test(
await page
.getByTestId("processingSplit Text")
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
- targetPosition: { x: 300, y: 300 },
+ targetPosition: { x: 300, y: 500 },
});
//fourth component
diff --git a/src/frontend/tests/core/integrations/similarity.spec.ts b/src/frontend/tests/core/integrations/similarity.spec.ts
index 08a96f3d6..1191cfa67 100644
--- a/src/frontend/tests/core/integrations/similarity.spec.ts
+++ b/src/frontend/tests/core/integrations/similarity.spec.ts
@@ -30,7 +30,7 @@ test(
await page
.getByText("OpenAI Embeddings", { exact: true })
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
- targetPosition: { x: 0, y: 0 },
+ targetPosition: { x: 100, y: 100 },
});
await page.getByTestId("canvas_controls_dropdown").click();
diff --git a/src/frontend/tests/extended/features/filterEdge-shard-1.spec.ts b/src/frontend/tests/extended/features/filterEdge-shard-1.spec.ts
index a14571523..8be90b01f 100644
--- a/src/frontend/tests/extended/features/filterEdge-shard-1.spec.ts
+++ b/src/frontend/tests/extended/features/filterEdge-shard-1.spec.ts
@@ -70,7 +70,6 @@ test(
const elementTestIds = [
"input_outputChat Output",
"dataAPI Request",
- "datastaxAstra DB",
"langchain_utilitiesTool Calling Agent",
"langchain_utilitiesConversationChain",
"mem0Mem0 Chat Memory",
@@ -104,7 +103,6 @@ test(
"groqGroq",
"lmstudioLM Studio",
"maritalkMariTalk",
- "mistralMistralAI",
"perplexityPerplexity",
"baiduQianfan",
"sambanovaSambaNova",
diff --git a/src/frontend/tests/extended/features/integration-side-bar.spec.ts b/src/frontend/tests/extended/features/integration-side-bar.spec.ts
index 154ccb920..964f1e973 100644
--- a/src/frontend/tests/extended/features/integration-side-bar.spec.ts
+++ b/src/frontend/tests/extended/features/integration-side-bar.spec.ts
@@ -2,7 +2,7 @@ import { expect, test } from "@playwright/test";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
test(
- "user should be able to see integrations in the sidebar if mvp_components is true",
+ "user should be able to see integrations in the sidebar when bundles is selected",
{ tag: ["@release", "@api", "@workspace"] },
async ({ page }) => {
await awaitBootstrapTest(page);
@@ -11,7 +11,10 @@ test(
await page.waitForSelector('[data-testid="shad-sidebar"]', {
timeout: 30000,
});
- await expect(page.getByText("Bundles")).toBeVisible();
+ await page.getByTestId("sidebar-nav-bundles").click();
+ await expect(
+ page.locator('[data-sidebar="group-label"]', { hasText: "Bundles" }),
+ ).toBeVisible();
await expect(page.getByText("Notion")).toBeVisible();
await expect(page.getByText("AssemblyAI")).toBeVisible();
},
diff --git a/src/frontend/tests/extended/features/mcp-server-tab.spec.ts b/src/frontend/tests/extended/features/mcp-server-tab.spec.ts
index 0dc0edf9c..4bd7db542 100644
--- a/src/frontend/tests/extended/features/mcp-server-tab.spec.ts
+++ b/src/frontend/tests/extended/features/mcp-server-tab.spec.ts
@@ -56,6 +56,8 @@ test(
// Verify actions modal is open
await expect(page.getByText("MCP Server Tools")).toBeVisible();
+ await page.waitForSelector("text=Flow Name", { timeout: 3000 });
+
// Select some actions
const rowsCount = await page.getByRole("row").count();
expect(rowsCount).toBeGreaterThan(0);
@@ -241,7 +243,7 @@ test(
await page
.getByTestId("agentsMCP Tools")
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
- targetPosition: { x: 0, y: 0 },
+ targetPosition: { x: 50, y: 50 },
});
await page.getByTestId("canvas_controls_dropdown").click();
diff --git a/src/frontend/tests/extended/features/mcp-server.spec.ts b/src/frontend/tests/extended/features/mcp-server.spec.ts
index 966ea29ff..d29bd9f50 100644
--- a/src/frontend/tests/extended/features/mcp-server.spec.ts
+++ b/src/frontend/tests/extended/features/mcp-server.spec.ts
@@ -22,7 +22,7 @@ test(
await page
.getByTestId("agentsMCP Tools")
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
- targetPosition: { x: 0, y: 0 },
+ targetPosition: { x: 100, y: 100 },
});
await page.getByTestId("canvas_controls_dropdown").click();
@@ -235,7 +235,7 @@ test(
await page
.getByTestId("agentsMCP Tools")
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
- targetPosition: { x: 0, y: 0 },
+ targetPosition: { x: 100, y: 100 },
});
await page.getByTestId("canvas_controls_dropdown").click();
@@ -410,7 +410,7 @@ test(
await page
.getByTestId("agentsMCP Tools")
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
- targetPosition: { x: 0, y: 0 },
+ targetPosition: { x: 100, y: 100 },
});
await page.getByTestId("canvas_controls_dropdown").click();
diff --git a/src/frontend/tests/extended/features/tool-mode.spec.ts b/src/frontend/tests/extended/features/tool-mode.spec.ts
index 8ae9ee735..ab11415a6 100644
--- a/src/frontend/tests/extended/features/tool-mode.spec.ts
+++ b/src/frontend/tests/extended/features/tool-mode.spec.ts
@@ -34,7 +34,7 @@ test(
await page.keyboard.press("ControlOrMeta+Shift+m");
await page.waitForSelector("text=toolset", {
- timeout: 3000,
+ timeout: 5000,
state: "visible",
});
@@ -43,7 +43,7 @@ test(
await page.keyboard.press("ControlOrMeta+Shift+m");
await page.waitForSelector("text=toolset", {
- timeout: 3000,
+ timeout: 5000,
state: "hidden",
});
@@ -52,7 +52,7 @@ test(
await page.getByTestId("tool-mode-button").click();
await page.waitForSelector("text=toolset", {
- timeout: 3000,
+ timeout: 5000,
state: "visible",
});
@@ -61,7 +61,7 @@ test(
await page.getByTestId("tool-mode-button").click();
await page.waitForSelector("text=toolset", {
- timeout: 3000,
+ timeout: 5000,
state: "hidden",
});
@@ -70,7 +70,7 @@ test(
await page.getByTestId("tool-mode-button").click();
await page.waitForSelector("text=toolset", {
- timeout: 3000,
+ timeout: 5000,
state: "visible",
});
@@ -79,7 +79,7 @@ test(
await page.getByTestId("tool-mode-button").click();
await page.waitForSelector("text=toolset", {
- timeout: 3000,
+ timeout: 5000,
state: "hidden",
});
@@ -88,7 +88,7 @@ test(
await page.getByTestId("tool-mode-button").click();
await page.waitForSelector("text=toolset", {
- timeout: 3000,
+ timeout: 5000,
state: "visible",
});
@@ -97,7 +97,7 @@ test(
await page.getByTestId("tool-mode-button").click();
await page.waitForSelector("text=toolset", {
- timeout: 3000,
+ timeout: 5000,
state: "hidden",
});
@@ -106,7 +106,7 @@ test(
await page.getByTestId("tool-mode-button").click();
await page.waitForSelector("text=toolset", {
- timeout: 3000,
+ timeout: 5000,
state: "visible",
});
@@ -127,7 +127,7 @@ test(
await page
.getByTestId("agentsAgent")
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
- targetPosition: { x: 0, y: 500 },
+ targetPosition: { x: 50, y: 500 },
});
await page.getByTestId("canvas_controls_dropdown").click();