feat: Introduce CanvasControls and dropdown components for enhanced canvas interaction

- Added CanvasControls component to manage canvas interactions and state based on flow lock status.
- Implemented CanvasControlsDropdown and HelpDropdown for zoom and help functionalities, respectively.
- Created DropdownControlButton for reusable dropdown actions with keyboard shortcuts.
- Removed deprecated dropdowns and refactored related tests to align with new component structure.
- Introduced utility functions for zoom percentage formatting and modifier key detection.
This commit is contained in:
Deon Sanchez
2025-08-18 12:34:20 -06:00
parent 9cc199c48a
commit 619c8be4bd
11 changed files with 396 additions and 426 deletions

View File

@ -1,6 +1,3 @@
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";
@ -23,8 +20,12 @@ 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 currentFlow = useFlowStore((state) => state.currentFlow);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
const [openSettings, setOpenSettings] = useState(false);
@ -140,6 +141,7 @@ export const MenuBar = memo((): JSX.Element => {
>
{currentFlowName || "Untitled Flow"}
</span>
{currentFlow?.locked && <IconComponent name="Lock" className="h-5 w-3.5" />}
<IconComponent
name="pencil"

View File

@ -0,0 +1,44 @@
import { Panel, useStoreApi } from "@xyflow/react";
import { type ReactNode, useEffect } from "react";
import { useShallow } from "zustand/react/shallow";
import { Separator } from "@/components/ui/separator";
import useFlowStore from "@/stores/flowStore";
import CanvasControlsDropdown from "./CanvasControlsDropdown";
import HelpDropdown from "./HelpDropdown";
const CanvasControls = ({ children }: { children?: ReactNode }) => {
const reactFlowStoreApi = useStoreApi();
const isFlowLocked = useFlowStore(
useShallow((state) => state.currentFlow?.locked),
);
useEffect(() => {
reactFlowStoreApi.setState({
nodesDraggable: !isFlowLocked,
nodesConnectable: !isFlowLocked,
elementsSelectable: !isFlowLocked,
});
}, [isFlowLocked, reactFlowStoreApi]);
return (
<Panel
data-testid="main_canvas_controls"
className="react-flow__controls !left-auto !m-2 flex !flex-row rounded-md border border-border bg-background fill-foreground stroke-foreground text-primary [&>button]:border-0"
position="bottom-right"
>
{children}
{children && (
<span>
<Separator orientation="vertical" />
</span>
)}
<CanvasControlsDropdown />
<span>
<Separator orientation="vertical" />
</span>
<HelpDropdown />
</Panel>
);
};
export default CanvasControls;

View File

@ -0,0 +1,146 @@
import { useReactFlow, useStore } from "@xyflow/react";
import { useCallback, useEffect, useState } from "react";
import { shallow } from "zustand/shallow";
import IconComponent from "@/components/common/genericIconComponent";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import DropdownControlButton from "./DropdownControlButton";
import { formatZoomPercentage, reactFlowSelector } from "./utils/canvasUtils";
export const KEYBOARD_SHORTCUTS = {
ZOOM_IN: { key: "+", code: "Equal" },
ZOOM_OUT: { key: "-", code: "Minus" },
FIT_VIEW: { key: "1", code: "Digit1" },
RESET_ZOOM: { key: "0", code: "Digit0" },
} as const;
const CanvasControlsDropdown = () => {
const [isOpen, setIsOpen] = useState(false);
const { fitView, zoomIn, zoomOut, zoomTo } = useReactFlow();
const { minZoomReached, maxZoomReached, zoom } = useStore(
reactFlowSelector,
shallow,
);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
const isModifierPressed = event.metaKey || event.ctrlKey;
if (!isModifierPressed) return;
switch (event.code) {
case KEYBOARD_SHORTCUTS.ZOOM_IN.code:
event.preventDefault();
if (!maxZoomReached) {
zoomIn();
}
break;
case KEYBOARD_SHORTCUTS.ZOOM_OUT.code:
event.preventDefault();
if (minZoomReached || zoom <= 0.6) {
zoomTo(1);
} else {
zoomOut();
}
break;
case KEYBOARD_SHORTCUTS.FIT_VIEW.code:
event.preventDefault();
fitView();
break;
case KEYBOARD_SHORTCUTS.RESET_ZOOM.code:
event.preventDefault();
zoomTo(1);
break;
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [zoomIn, zoomOut, fitView, zoomTo, maxZoomReached, minZoomReached, zoom]);
const handleZoomIn = useCallback(() => {
zoomIn();
}, [zoomIn]);
const handleZoomOut = useCallback(() => {
zoomOut();
}, [zoomOut]);
const handleFitView = useCallback(() => {
fitView();
}, [fitView]);
const handleResetZoom = useCallback(() => {
zoomTo(1);
}, [zoomTo]);
return (
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
data-testid="canvas_controls_dropdown"
className="group rounded-none px-2 py-2 hover:bg-muted"
unstyled
title="Canvas Controls"
>
<div className="flex items-center justify-center ">
<div className="text-sm text-primary pr-1">
{formatZoomPercentage(zoom)}
</div>
<IconComponent
name={isOpen ? "ChevronDown" : "ChevronUp"}
aria-hidden="true"
className="text-primary group-hover:text-primary !h-5 !w-5"
/>
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="end"
className="flex flex-col w-full"
>
<DropdownControlButton
tooltipText="Zoom In"
onClick={handleZoomIn}
disabled={maxZoomReached}
testId="zoom_in"
label="Zoom In"
shortcut={KEYBOARD_SHORTCUTS.ZOOM_IN.key}
/>
<DropdownControlButton
tooltipText="Zoom Out"
onClick={handleZoomOut}
disabled={minZoomReached}
testId="zoom_out"
label="Zoom Out"
shortcut={KEYBOARD_SHORTCUTS.ZOOM_OUT.key}
/>
<Separator />
<DropdownControlButton
tooltipText="Reset zoom to 100%"
onClick={handleResetZoom}
testId="reset_zoom"
label="Zoom To 100%"
shortcut={KEYBOARD_SHORTCUTS.RESET_ZOOM.key}
/>
<DropdownControlButton
tooltipText="Fit view to show all nodes"
onClick={handleFitView}
testId="fit_view"
label="Zoom To Fit"
shortcut={KEYBOARD_SHORTCUTS.FIT_VIEW.key}
/>
</DropdownMenuContent>
</DropdownMenu>
);
};
export default CanvasControlsDropdown;

View File

@ -0,0 +1,73 @@
import React from "react";
import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
import { Button } from "@/components/ui/button";
import { cn } from "@/utils/utils";
import ToggleShadComponent from "../parameterRenderComponent/components/toggleShadComponent";
import { getModifierKey } from "./utils/canvasUtils";
export type DropdownControlButtonProps = {
tooltipText?: string;
onClick?: () => void;
disabled?: boolean;
testId?: string;
label?: string;
shortcut?: string;
iconName?: string;
hasToogle?: boolean;
toggleValue?: boolean;
};
const DropdownControlButton: React.FC<DropdownControlButtonProps> = ({
tooltipText,
onClick = () => {},
disabled,
testId,
label = "",
shortcut = "",
iconName,
hasToogle = false,
toggleValue = false,
}) => (
<Button
data-testid={testId}
className={cn(
"group flex items-center justify-center !py-1.5 !px-2 hover:bg-accent h-full rounded-none",
disabled && "cursor-not-allowed opacity-50",
)}
onClick={onClick}
variant="ghost"
disabled={disabled}
title={tooltipText || ""}
>
{iconName && (
<ForwardedIconComponent
name={iconName}
className="text-muted-foreground group-hover:text-primary"
/>
)}
<div className="flex flex-row items-center justify-between w-full h-full">
<span className="text-muted-foreground text-sm mr-2 group-hover:text-primary">
{label}
</span>
<div className="flex flex-row items-center justify-center gap-1 text-sm text-placeholder-foreground">
{shortcut && (
<>
<span className="mr-1">{getModifierKey()}</span>
<span>{shortcut}</span>
</>
)}
</div>
</div>
{hasToogle && (
<ToggleShadComponent
value={toggleValue}
handleOnNewValue={onClick}
editNode={true}
id="helper_lines"
disabled={false}
/>
)}
</Button>
);
export default DropdownControlButton;

View File

@ -0,0 +1,99 @@
import { useCallback, useState } from "react";
import { useNavigate } from "react-router-dom";
import IconComponent from "@/components/common/genericIconComponent";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import {
BUG_REPORT_URL,
DATASTAX_DOCS_URL,
DESKTOP_URL,
DOCS_URL,
} from "@/constants/constants";
import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags";
import DropdownControlButton from "./DropdownControlButton";
const HelpDropdown = () => {
const navigate = useNavigate();
const [helperLineEnabled, setHelperLineEnabled] = useState(false);
const [isHelpMenuOpen, setIsHelpMenuOpen] = useState(false);
const onToggleHelperLines = useCallback(() => {
setHelperLineEnabled(!helperLineEnabled);
}, [helperLineEnabled]);
return (
<DropdownMenu open={isHelpMenuOpen} onOpenChange={setIsHelpMenuOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="group flex items-center justify-center px-2 rounded-none"
title="Help"
>
<IconComponent
name="Circle-Help"
aria-hidden="true"
className="text-muted-foreground group-hover:text-primary !h-5 !w-5"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="end"
className="flex flex-col w-full"
>
<DropdownControlButton
iconName="book-open"
testId="canvas_controls_dropdown_docs"
label="Docs"
onClick={() => {
window.open(
ENABLE_DATASTAX_LANGFLOW ? DATASTAX_DOCS_URL : DOCS_URL,
"_blank",
);
}}
/>
<DropdownControlButton
iconName="keyboard"
testId="canvas_controls_dropdown_shortcuts"
label="Shortcuts"
onClick={() => {
navigate("/settings/shortcuts");
}}
/>
<DropdownControlButton
iconName="bug"
testId="canvas_controls_dropdown_report_a_bug"
label="Report a bug"
onClick={() => {
window.open(BUG_REPORT_URL, "_blank");
}}
/>
<Separator />
<DropdownControlButton
iconName="download"
testId="canvas_controls_dropdown_get_langflow_desktop"
label="Get Langflow Desktop"
onClick={() => {
window.open(DESKTOP_URL, "_blank");
}}
/>
<DropdownControlButton
iconName={!helperLineEnabled ? "UnfoldHorizontal" : "FoldHorizontal"}
testId="canvas_controls_dropdown_enable_smart_guides"
onClick={onToggleHelperLines}
toggleValue={helperLineEnabled}
label="Enable smart guides"
hasToogle={true}
/>
</DropdownMenuContent>
</DropdownMenu>
);
};
export default HelpDropdown;

View File

@ -1,5 +1,5 @@
import { fireEvent, render, screen } from "@testing-library/react";
import CanvasControls from "../index";
import CanvasControls from "../CanvasControls";
// Capture flow functions for assertions
const reactFlowFns = {
@ -38,11 +38,14 @@ jest.mock("@/components/ui/separator", () => ({
}));
// Mock dropdowns to a simple render that exposes props for assertions
jest.mock("../dropdowns", () => ({
CanvasControlsDropdown: (props: any) => (
<div data-testid="controls-dropdown" {...props} />
),
HelpDropdown: (props: any) => <div data-testid="help-dropdown" {...props} />,
jest.mock("../CanvasControlsDropdown", () => ({
__esModule: true,
default: (props: any) => <div data-testid="controls-dropdown" {...props} />,
}));
jest.mock("../HelpDropdown", () => ({
__esModule: true,
default: (props: any) => <div data-testid="help-dropdown" {...props} />,
}));
describe("CanvasControls", () => {

View File

@ -1,6 +1,7 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { MemoryRouter, useNavigate } from "react-router-dom";
import { CanvasControlsDropdown, HelpDropdown } from "../dropdowns";
import CanvasControlsDropdown from "../CanvasControlsDropdown";
import HelpDropdown from "../HelpDropdown";
jest.mock("@/components/ui/button", () => ({
Button: ({ children, ...props }: any) => (

View File

@ -1,272 +0,0 @@
import IconComponent, {
ForwardedIconComponent,
} from "@/components/common/genericIconComponent";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import {
BUG_REPORT_URL,
DATASTAX_DOCS_URL,
DESKTOP_URL,
DOCS_URL,
} from "@/constants/constants";
import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags";
import { cn, getOS } from "@/utils/utils";
import React, { useCallback, useState } from "react";
import { useNavigate } from "react-router-dom";
import ToggleShadComponent from "../parameterRenderComponent/components/toggleShadComponent";
type DropdownControlButtonProps = {
tooltipText?: string;
onClick?: () => void;
disabled?: boolean;
testId?: string;
label?: string;
shortcut?: string;
iconName?: string;
hasToogle?: boolean;
toggleValue?: boolean;
};
const getModifierKey = (): string => {
const os = getOS();
return os === "macos" ? "⌘" : "Ctrl";
};
const DropdownControlButton: React.FC<DropdownControlButtonProps> = ({
tooltipText,
onClick = () => {},
disabled,
testId,
label = "",
shortcut = "",
iconName,
hasToogle=false,
toggleValue = false,
}) => (
<Button
data-testid={testId}
className={cn(
"group flex items-center justify-center !py-1.5 !px-2 hover:bg-accent h-full rounded-none",
disabled && "cursor-not-allowed opacity-50",
)}
onClick={onClick}
variant="ghost"
disabled={disabled}
title={tooltipText || ""}
>
{iconName && (
<ForwardedIconComponent
name={iconName}
className="text-muted-foreground group-hover:text-primary"
/>
)}
<div className="flex flex-row items-center justify-between w-full h-full">
<span className="text-muted-foreground text-sm mr-2 group-hover:text-primary">
{label}
</span>
<div className="flex flex-row items-center justify-center gap-1 text-sm text-placeholder-foreground">
{shortcut && (
<>
<span className="mr-1">{getModifierKey()}</span>
<span>{shortcut}</span>
</>
)}
</div>
</div>
{hasToogle && (
<ToggleShadComponent
value={toggleValue}
handleOnNewValue={onClick}
editNode={true}
id="enable_smart_guides"
disabled={false}
/>
)}
</Button>
);
const formatZoomPercentage = (zoom: number): string =>
`${Math.round(zoom * 100)}%`;
export type CanvasControlsDropdownProps = {
zoom: number;
minZoomReached: boolean;
maxZoomReached: boolean;
isOpen: boolean;
onOpenChange: (open: boolean) => void;
onZoomIn: () => void;
onZoomOut: () => void;
onResetZoom: () => void;
onFitView: () => void;
shortcuts: {
ZOOM_IN: { key: string };
ZOOM_OUT: { key: string };
RESET_ZOOM: { key: string };
FIT_VIEW: { key: string };
};
};
export const CanvasControlsDropdown: React.FC<CanvasControlsDropdownProps> = ({
zoom,
minZoomReached,
maxZoomReached,
isOpen,
onOpenChange,
onZoomIn,
onZoomOut,
onResetZoom,
onFitView,
shortcuts,
}) => (
<DropdownMenu open={isOpen} onOpenChange={onOpenChange}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
data-testid="canvas_controls_dropdown"
className="group rounded-none px-2 py-2 hover:bg-muted"
unstyled
title="Canvas Controls"
>
<div className="flex items-center justify-center ">
<div className="text-sm text-primary pr-1">
{formatZoomPercentage(zoom)}
</div>
<IconComponent
name={isOpen ? "ChevronDown" : "ChevronUp"}
aria-hidden="true"
className="text-primary group-hover:text-primary !h-5 !w-5"
/>
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="end"
className="flex flex-col w-full"
>
<DropdownControlButton
tooltipText="Zoom In"
onClick={onZoomIn}
disabled={maxZoomReached}
testId="zoom_in_dropdown"
label="Zoom In"
shortcut={shortcuts.ZOOM_IN.key}
/>
<DropdownControlButton
tooltipText="Zoom Out"
onClick={onZoomOut}
disabled={minZoomReached}
testId="zoom_out_dropdown"
label="Zoom Out"
shortcut={shortcuts.ZOOM_OUT.key}
/>
<Separator />
<DropdownControlButton
tooltipText="Reset zoom to 100%"
onClick={onResetZoom}
testId="reset_zoom_dropdown"
label="Zoom To 100%"
shortcut={shortcuts.RESET_ZOOM.key}
/>
<DropdownControlButton
tooltipText="Fit view to show all nodes"
onClick={onFitView}
testId="fit_view_dropdown"
label="Zoom To Fit"
shortcut={shortcuts.FIT_VIEW.key}
/>
</DropdownMenuContent>
</DropdownMenu>
);
export type HelpDropdownProps = {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
onSelectAction?: (action: "mock1" | "mock2" | "mock3") => void;
};
export const HelpDropdown: React.FC<HelpDropdownProps> = ({
isOpen,
onOpenChange,
onSelectAction,
}) => {
const navigate = useNavigate();
const [helperLineEnabled, setHelperLineEnabled] = useState(false);
const onToggleHelperLines = useCallback(() => {
setHelperLineEnabled(!helperLineEnabled);
}, [helperLineEnabled]);
return (
<DropdownMenu open={isOpen} onOpenChange={onOpenChange}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="group flex items-center justify-center px-2 rounded-none"
title="Help"
>
<IconComponent
name="Circle-Help"
aria-hidden="true"
className="text-muted-foreground group-hover:text-primary !h-5 !w-5"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="end"
className="flex flex-col w-full"
>
<DropdownControlButton
iconName="book-open"
testId="canvas_controls_dropdown_docs"
label="Docs"
onClick={() => {
window.open(
ENABLE_DATASTAX_LANGFLOW ? DATASTAX_DOCS_URL : DOCS_URL,
"_blank",
);
}}
/>
<DropdownControlButton
iconName="keyboard"
testId="canvas_controls_dropdown_shortcuts"
label="Shortcuts"
onClick={() => {
navigate("/settings/shortcuts");
}}
/>
<DropdownControlButton
iconName="bug"
testId="canvas_controls_dropdown_report_a_bug"
label="Report a bug"
onClick={() => {
window.open(BUG_REPORT_URL, "_blank");
}}
/>
<Separator />
<DropdownControlButton
iconName="download"
testId="canvas_controls_dropdown_get_langflow_desktop"
label="Get Langflow Desktop"
onClick={() => {
window.open(DESKTOP_URL, "_blank");
}}
/>
<DropdownControlButton
iconName={!helperLineEnabled ? "UnfoldHorizontal" : "FoldHorizontal"}
testId="canvas_controls_dropdown_enable_smart_guides"
onClick={onToggleHelperLines}
toggleValue={helperLineEnabled}
label="Enable smart guides"
hasToogle={true}
/>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@ -1,143 +0,0 @@
import { Separator } from "@/components/ui/separator";
import useFlowStore from "@/stores/flowStore";
import {
Panel,
type ReactFlowState,
useReactFlow,
useStore,
useStoreApi,
} from "@xyflow/react";
import { type ReactNode, useCallback, useEffect, useState } from "react";
import { useShallow } from "zustand/react/shallow";
import { shallow } from "zustand/shallow";
import { CanvasControlsDropdown, HelpDropdown } from "./dropdowns";
const KEYBOARD_SHORTCUTS = {
ZOOM_IN: { key: "+", code: "Equal" },
ZOOM_OUT: { key: "-", code: "Minus" },
FIT_VIEW: { key: "1", code: "Digit1" },
RESET_ZOOM: { key: "0", code: "Digit0" },
} as const;
const reactFlowSelector = (s: ReactFlowState) => ({
isInteractive: s.nodesDraggable || s.nodesConnectable || s.elementsSelectable,
minZoomReached: s.transform[2] <= s.minZoom,
maxZoomReached: s.transform[2] >= s.maxZoom,
zoom: s.transform[2],
});
const CanvasControls = ({ children }: { children?: ReactNode }) => {
const reactFlowStoreApi = useStoreApi();
const { fitView, zoomIn, zoomOut, zoomTo } = useReactFlow();
const { minZoomReached, maxZoomReached, zoom } = useStore(
reactFlowSelector,
shallow,
);
const [isControlsMenuOpen, setIsControlsMenuOpen] = useState(false);
const [isHelpMenuOpen, setIsHelpMenuOpen] = useState(false);
const isFlowLocked = useFlowStore(
useShallow((state) => state.currentFlow?.locked),
);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
const isModifierPressed = event.metaKey || event.ctrlKey;
if (!isModifierPressed) return;
switch (event.code) {
case KEYBOARD_SHORTCUTS.ZOOM_IN.code:
event.preventDefault();
if (!maxZoomReached) {
zoomIn();
}
break;
case KEYBOARD_SHORTCUTS.ZOOM_OUT.code:
event.preventDefault();
if (minZoomReached || zoom <= 0.6) {
zoomTo(1);
} else {
zoomOut();
}
break;
case KEYBOARD_SHORTCUTS.FIT_VIEW.code:
event.preventDefault();
fitView();
break;
case KEYBOARD_SHORTCUTS.RESET_ZOOM.code:
event.preventDefault();
zoomTo(1);
break;
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [zoomIn, zoomOut, fitView, zoomTo, maxZoomReached, minZoomReached]);
useEffect(() => {
reactFlowStoreApi.setState({
nodesDraggable: !isFlowLocked,
nodesConnectable: !isFlowLocked,
elementsSelectable: !isFlowLocked,
});
}, [isFlowLocked, reactFlowStoreApi]);
const handleZoomIn = useCallback(() => {
zoomIn();
setIsControlsMenuOpen(false);
}, [zoomIn]);
const handleZoomOut = useCallback(() => {
zoomOut();
setIsControlsMenuOpen(false);
}, [zoomOut]);
const handleFitView = useCallback(() => {
fitView();
setIsControlsMenuOpen(false);
}, [fitView]);
const handleResetZoom = useCallback(() => {
zoomTo(1);
setIsControlsMenuOpen(false);
}, [zoomTo]);
return (
<Panel
data-testid="main_canvas_controls"
className="react-flow__controls !left-auto !m-2 flex !flex-row rounded-md border border-border bg-background fill-foreground stroke-foreground text-primary [&>button]:border-0"
position="bottom-right"
>
{children}
{children && (
<span>
<Separator orientation="vertical" />
</span>
)}
<CanvasControlsDropdown
zoom={zoom}
minZoomReached={minZoomReached}
maxZoomReached={maxZoomReached}
isOpen={isControlsMenuOpen}
onOpenChange={setIsControlsMenuOpen}
onZoomIn={handleZoomIn}
onZoomOut={handleZoomOut}
onResetZoom={handleResetZoom}
onFitView={handleFitView}
shortcuts={KEYBOARD_SHORTCUTS}
/>
<span>
<Separator orientation="vertical" />
</span>
<HelpDropdown
isOpen={isHelpMenuOpen}
onOpenChange={setIsHelpMenuOpen}
onSelectAction={() => setIsHelpMenuOpen(false)}
/>
</Panel>
);
};
export default CanvasControls;

View File

@ -0,0 +1,17 @@
import { ReactFlowState } from "@xyflow/react";
import { getOS } from "@/utils/utils";
export const getModifierKey = (): string => {
const os = getOS();
return os === "macos" ? "⌘" : "Ctrl";
};
export const formatZoomPercentage = (zoom: number): string =>
`${Math.round(zoom * 100)}%`;
export const reactFlowSelector = (s: ReactFlowState) => ({
isInteractive: s.nodesDraggable || s.nodesConnectable || s.elementsSelectable,
minZoomReached: s.transform[2] <= s.minZoom,
maxZoomReached: s.transform[2] >= s.maxZoom,
zoom: s.transform[2],
});

View File

@ -4,7 +4,7 @@ import {
default as ForwardedIconComponent,
default as IconComponent,
} from "@/components/common/genericIconComponent";
import CanvasControls from "@/components/core/canvasControlsComponent";
import CanvasControls from "@/components/core/canvasControlsComponent/CanvasControls";
import LogCanvasControls from "@/components/core/logCanvasControlsComponent";
import { Button } from "@/components/ui/button";
import { SidebarTrigger } from "@/components/ui/sidebar";