refactor: performance improvements for canvas controls/toolbar (#7930)

* performance improvements for canvas controls/toolbar

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Mike Fortman 2025-05-07 09:12:22 -05:00 • committed by GitHub
commit d887646383
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 90 additions and 131 deletions

View file

@ -13,7 +13,8 @@ import {
type ReactFlowState, type ReactFlowState,
} from "@xyflow/react"; } from "@xyflow/react";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { useEffect } from "react"; import { useCallback, useEffect } from "react";
import { useShallow } from "zustand/react/shallow";
import { shallow } from "zustand/shallow"; import { shallow } from "zustand/shallow";
type CustomControlButtonProps = { type CustomControlButtonProps = {
@ -70,20 +71,22 @@ const CanvasControls = ({ children }) => {
shallow, shallow,
); );
const saveFlow = useSaveFlow(); const saveFlow = useSaveFlow();
const currentFlow = useFlowStore((state) => state.currentFlow); const isLocked = useFlowStore(
useShallow((state) => state.currentFlow?.locked),
);
const setCurrentFlow = useFlowStore((state) => state.setCurrentFlow); const setCurrentFlow = useFlowStore((state) => state.setCurrentFlow);
const autoSaving = useFlowsManagerStore((state) => state.autoSaving); const autoSaving = useFlowsManagerStore((state) => state.autoSaving);
useEffect(() => { useEffect(() => {
const isLocked = currentFlow?.locked;
store.setState({ store.setState({
nodesDraggable: !isLocked, nodesDraggable: !isLocked,
nodesConnectable: !isLocked, nodesConnectable: !isLocked,
elementsSelectable: !isLocked, elementsSelectable: !isLocked,
}); });
}, [currentFlow?.locked]); }, [isLocked]);
const handleSaveFlow = () => { const handleSaveFlow = useCallback(() => {
const currentFlow = useFlowStore.getState().currentFlow;
if (!currentFlow) return; if (!currentFlow) return;
const newFlow = cloneDeep(currentFlow); const newFlow = cloneDeep(currentFlow);
newFlow.locked = isInteractive; newFlow.locked = isInteractive;
@ -92,16 +95,16 @@ const CanvasControls = ({ children }) => {
} else { } else {
setCurrentFlow(newFlow); setCurrentFlow(newFlow);
} }
}; }, [isInteractive, autoSaving, saveFlow, setCurrentFlow]);
const onToggleInteractivity = () => { const onToggleInteractivity = useCallback(() => {
store.setState({ store.setState({
nodesDraggable: !isInteractive, nodesDraggable: !isInteractive,
nodesConnectable: !isInteractive, nodesConnectable: !isInteractive,
elementsSelectable: !isInteractive, elementsSelectable: !isInteractive,
}); });
handleSaveFlow(); handleSaveFlow();
}; }, [isInteractive, store, handleSaveFlow]);
return ( return (
<Panel <Panel

View file

@ -1,7 +1,7 @@
import ShadTooltip from "@/components/common/shadTooltipComponent"; import ShadTooltip from "@/components/common/shadTooltipComponent";
import { track } from "@/customization/utils/analytics"; import { track } from "@/customization/utils/analytics";
import { Panel } from "@xyflow/react"; import { Panel } from "@xyflow/react";
import { useEffect, useMemo, useState } from "react"; import { memo, useEffect, useMemo, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from "react-hotkeys-hook";
import ShareModal from "../../../modals/shareModal"; import ShareModal from "../../../modals/shareModal";
import useFlowStore from "../../../stores/flowStore"; import useFlowStore from "../../../stores/flowStore";
@ -11,7 +11,7 @@ import { classNames, cn, isThereModal } from "../../../utils/utils";
import ForwardedIconComponent from "../../common/genericIconComponent"; import ForwardedIconComponent from "../../common/genericIconComponent";
import FlowToolbarOptions from "./components/flow-toolbar-options"; import FlowToolbarOptions from "./components/flow-toolbar-options";
export default function FlowToolbar(): JSX.Element { const FlowToolbar = memo(function FlowToolbar(): JSX.Element {
const preventDefault = true; const preventDefault = true;
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
const [openCodeModal, setOpenCodeModal] = useState<boolean>(false); const [openCodeModal, setOpenCodeModal] = useState<boolean>(false);
@ -41,75 +41,12 @@ export default function FlowToolbar(): JSX.Element {
useHotkeys(api, handleAPIWShortcut, { preventDefault }); useHotkeys(api, handleAPIWShortcut, { preventDefault });
useHotkeys(flow, handleShareWShortcut, { preventDefault }); useHotkeys(flow, handleShareWShortcut, { preventDefault });
const hasIO = useFlowStore((state) => state.hasIO);
const hasStore = useStoreStore((state) => state.hasStore);
const validApiKey = useStoreStore((state) => state.validApiKey);
const hasApiKey = useStoreStore((state) => state.hasApiKey);
const currentFlow = useFlowStore((state) => state.currentFlow);
useEffect(() => { useEffect(() => {
if (open) { if (open) {
track("Playground Button Clicked"); track("Playground Button Clicked");
} }
}, [open]); }, [open]);
const ModalMemo = useMemo(
() => (
<ShareModal
is_component={false}
component={currentFlow!}
disabled={!hasApiKey || !validApiKey || !hasStore}
open={openShareModal}
setOpen={setOpenShareModal}
>
<ShadTooltip
content={
!hasApiKey || !validApiKey || !hasStore
? "Store API Key Required"
: ""
}
side="bottom"
align="end"
>
<button
disabled={!hasApiKey || !validApiKey || !hasStore}
className={classNames(
"relative inline-flex h-8 w-full items-center justify-center gap-1.5 rounded px-3 py-1.5 text-sm font-semibold text-foreground transition-all duration-150 ease-in-out",
!hasApiKey || !validApiKey || !hasStore
? "cursor-not-allowed text-muted-foreground"
: "hover:bg-accent",
)}
data-testid="shared-button-flow"
onClick={() => {
setOpenShareModal(true);
}}
>
<>
<ForwardedIconComponent
name="Share2"
className={classNames(
"h-4 w-4",
!hasApiKey || !validApiKey || !hasStore
? "extra-side-bar-save-disable"
: "",
)}
/>
<span className="hidden md:block">Share</span>
</>
</button>
</ShadTooltip>
</ShareModal>
),
[
hasApiKey,
validApiKey,
currentFlow,
hasStore,
openShareModal,
setOpenShareModal,
],
);
return ( return (
<> <>
<Panel className="!m-2" position="top-right"> <Panel className="!m-2" position="top-right">
@ -123,4 +60,6 @@ export default function FlowToolbar(): JSX.Element {
</Panel> </Panel>
</> </>
); );
} });
export default FlowToolbar;

View file

@ -0,0 +1,61 @@
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import CanvasControls, {
CustomControlButton,
} from "@/components/core/canvasControlsComponent";
import { SidebarTrigger } from "@/components/ui/sidebar";
import { cn } from "@/utils/utils";
import { Background, Panel } from "@xyflow/react";
import { memo } from "react";
export const MemoizedBackground = memo(() => (
<Background size={2} gap={20} className="" />
));
interface MemoizedCanvasControlsProps {
setIsAddingNote: (value: boolean) => void;
position: { x: number; y: number };
shadowBoxWidth: number;
shadowBoxHeight: number;
}
export const MemoizedCanvasControls = memo(
({
setIsAddingNote,
position,
shadowBoxWidth,
shadowBoxHeight,
}: MemoizedCanvasControlsProps) => (
<CanvasControls>
<CustomControlButton
iconName="sticky-note"
tooltipText="Add Note"
onClick={() => {
setIsAddingNote(true);
const shadowBox = document.getElementById("shadow-box");
if (shadowBox) {
shadowBox.style.display = "block";
shadowBox.style.left = `${position.x - shadowBoxWidth / 2}px`;
shadowBox.style.top = `${position.y - shadowBoxHeight / 2}px`;
}
}}
iconClasses="text-primary"
testId="add_note"
/>
</CanvasControls>
),
);
export const MemoizedSidebarTrigger = memo(() => (
<Panel
className={cn(
"react-flow__controls !m-2 flex gap-1.5 rounded-md border border-secondary-hover bg-background fill-foreground stroke-foreground p-1.5 text-primary shadow transition-all duration-300 [&>button]:border-0 [&>button]:bg-background hover:[&>button]:bg-accent",
"pointer-events-auto opacity-100 group-data-[open=true]/sidebar-wrapper:pointer-events-none group-data-[open=true]/sidebar-wrapper:-translate-x-full group-data-[open=true]/sidebar-wrapper:opacity-0",
)}
position="top-left"
>
<SidebarTrigger className="h-fit w-fit px-3 py-1.5">
<ForwardedIconComponent name="PanelRightClose" className="h-4 w-4" />
<span className="text-foreground">Components</span>
</SidebarTrigger>
</Panel>
));

View file

@ -1,12 +1,6 @@
import { DefaultEdge } from "@/CustomEdges"; import { DefaultEdge } from "@/CustomEdges";
import NoteNode from "@/CustomNodes/NoteNode"; import NoteNode from "@/CustomNodes/NoteNode";
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import CanvasControls, {
CustomControlButton,
} from "@/components/core/canvasControlsComponent";
import FlowToolbar from "@/components/core/flowToolbarComponent"; import FlowToolbar from "@/components/core/flowToolbarComponent";
import { SidebarTrigger } from "@/components/ui/sidebar";
import { import {
COLOR_OPTIONS, COLOR_OPTIONS,
NOTE_NODE_MIN_HEIGHT, NOTE_NODE_MIN_HEIGHT,
@ -21,12 +15,10 @@ import { useAddComponent } from "@/hooks/use-add-component";
import { nodeColorsName } from "@/utils/styleUtils"; import { nodeColorsName } from "@/utils/styleUtils";
import { cn, isSupportedNodeTypes } from "@/utils/utils"; import { cn, isSupportedNodeTypes } from "@/utils/utils";
import { import {
Background,
Connection, Connection,
Edge, Edge,
OnNodeDrag, OnNodeDrag,
OnSelectionChangeParams, OnSelectionChangeParams,
Panel,
ReactFlow, ReactFlow,
reconnectEdge, reconnectEdge,
SelectionDragHandler, SelectionDragHandler,
@ -67,6 +59,11 @@ import {
import ConnectionLineComponent from "../ConnectionLineComponent"; import ConnectionLineComponent from "../ConnectionLineComponent";
import SelectionMenu from "../SelectionMenuComponent"; import SelectionMenu from "../SelectionMenuComponent";
import UpdateAllComponents from "../UpdateAllComponents"; import UpdateAllComponents from "../UpdateAllComponents";
import {
MemoizedBackground,
MemoizedCanvasControls,
MemoizedSidebarTrigger,
} from "./MemoizedComponents";
import getRandomName from "./utils/get-random-name"; import getRandomName from "./utils/get-random-name";
import isWrappedWithClass from "./utils/is-wrapped-with-class"; import isWrappedWithClass from "./utils/is-wrapped-with-class";
@ -591,44 +588,19 @@ export default function Page({
onPaneClick={onPaneClick} onPaneClick={onPaneClick}
onEdgeClick={handleEdgeClick} onEdgeClick={handleEdgeClick}
> >
<Background size={2} gap={20} className="" /> <MemoizedBackground />
{!view && ( {!view && (
<> <>
<CanvasControls> <MemoizedCanvasControls
<CustomControlButton setIsAddingNote={setIsAddingNote}
iconName="sticky-note" position={position.current}
tooltipText="Add Note" shadowBoxWidth={shadowBoxWidth}
onClick={() => { shadowBoxHeight={shadowBoxHeight}
setIsAddingNote(true); />
const shadowBox = document.getElementById("shadow-box");
if (shadowBox) {
shadowBox.style.display = "block";
shadowBox.style.left = `${position.current.x - shadowBoxWidth / 2}px`;
shadowBox.style.top = `${position.current.y - shadowBoxHeight / 2}px`;
}
}}
iconClasses="text-primary"
testId="add_note"
/>
</CanvasControls>
<FlowToolbar /> <FlowToolbar />
</> </>
)} )}
<Panel <MemoizedSidebarTrigger />
className={cn(
"react-flow__controls !m-2 flex gap-1.5 rounded-md border border-secondary-hover bg-background fill-foreground stroke-foreground p-1.5 text-primary shadow transition-all duration-300 [&>button]:border-0 [&>button]:bg-background hover:[&>button]:bg-accent",
"pointer-events-auto opacity-100 group-data-[open=true]/sidebar-wrapper:pointer-events-none group-data-[open=true]/sidebar-wrapper:-translate-x-full group-data-[open=true]/sidebar-wrapper:opacity-0",
)}
position="top-left"
>
<SidebarTrigger className="h-fit w-fit px-3 py-1.5">
<ForwardedIconComponent
name="PanelRightClose"
className="h-4 w-4"
/>
<span className="text-foreground">Components</span>
</SidebarTrigger>
</Panel>
<div className={cn(componentsToUpdate.length === 0 && "hidden")}> <div className={cn(componentsToUpdate.length === 0 && "hidden")}>
<UpdateAllComponents /> <UpdateAllComponents />
</div> </div>

View file

@ -73,7 +73,6 @@ const NodeToolbarComponent = memo(
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId); const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const [openModal, setOpenModal] = useState(false); const [openModal, setOpenModal] = useState(false);
const frozen = data.node?.frozen ?? false; const frozen = data.node?.frozen ?? false;
const currentFlow = useFlowStore((state) => state.currentFlow);
const updateNodeInternals = useUpdateNodeInternals(); const updateNodeInternals = useUpdateNodeInternals();
const paste = useFlowStore((state) => state.paste); const paste = useFlowStore((state) => state.paste);
@ -93,21 +92,6 @@ const NodeToolbarComponent = memo(
}, },
}); });
const flowDataNodes = useMemo(
() => currentFlow?.data?.nodes,
[currentFlow],
);
const node = useMemo(
() => flowDataNodes?.find((n) => n.id === data.id),
[flowDataNodes, data.id],
);
const index = useMemo(
() => flowDataNodes?.indexOf(node!)!,
[flowDataNodes, node],
);
const postToolModeValue = usePostTemplateValue({ const postToolModeValue = usePostTemplateValue({
node: data.node!, node: data.node!,
nodeId: data.id, nodeId: data.id,