📝 (handleRenderComponent/index.tsx): Add useShallow import for zustand/react/shallow to optimize re-renders

📝 (handleRenderComponent/index.tsx): Introduce isLocked state to handle locked flow functionality
📝 (handleRenderComponent/index.tsx): Update tooltip visibility based on isLocked state
📝 (handleRenderComponent/index.tsx): Update handle style based on isLocked state
📝 (PageComponent/index.tsx): Add useShallow import for zustand/react/shallow to optimize re-renders
📝 (PageComponent/index.tsx): Introduce isLocked state to handle locked flow functionality
📝 (PageComponent/index.tsx): Prevent edge click actions when flow is locked
📝 (PageComponent/index.tsx): Disable edge actions when flow is locked
📝 (PageComponent/index.tsx): Update edge actions based on isLocked state
📝 (lock-flow.spec.ts): Add test to simulate deleting edges when flow is locked
This commit is contained in:
cristhianzl 2025-06-20 10:43:56 -03:00
commit 9898d1cacd
3 changed files with 63 additions and 14 deletions

View file

@ -3,6 +3,7 @@ import useFlowStore from "@/stores/flowStore";
import { nodeColorsName } from "@/utils/styleUtils"; import { nodeColorsName } from "@/utils/styleUtils";
import { Connection, Handle, Position } from "@xyflow/react"; import { Connection, Handle, Position } from "@xyflow/react";
import { memo, useCallback, useEffect, useMemo, useState } from "react"; import { memo, useCallback, useEffect, useMemo, useState } from "react";
import { useShallow } from "zustand/react/shallow";
import ShadTooltip from "../../../../components/common/shadTooltipComponent"; import ShadTooltip from "../../../../components/common/shadTooltipComponent";
import { import {
isValidConnection, isValidConnection,
@ -179,6 +180,10 @@ const HandleRenderComponent = memo(function HandleRenderComponent({
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const [openTooltip, setOpenTooltip] = useState(false); const [openTooltip, setOpenTooltip] = useState(false);
const isLocked = useFlowStore(
useShallow((state) => state.currentFlow?.locked),
);
const { const {
setHandleDragging, setHandleDragging,
setFilterType, setFilterType,
@ -384,16 +389,10 @@ const HandleRenderComponent = memo(function HandleRenderComponent({
[], [],
); );
// Memoize the validation function
const validateConnection = useCallback(
(connection: any) => isValidConnection(connection),
[],
);
return ( return (
<div> <div>
<ShadTooltip <ShadTooltip
open={openTooltip} open={openTooltip && !isLocked}
setOpen={setOpenTooltip} setOpen={setOpenTooltip}
styleClasses={cn("tooltip-fixed-width custom-scroll nowheel bottom-2")} styleClasses={cn("tooltip-fixed-width custom-scroll nowheel bottom-2")}
delayDuration={1000} delayDuration={1000}
@ -414,13 +413,16 @@ const HandleRenderComponent = memo(function HandleRenderComponent({
position={left ? Position.Left : Position.Right} position={left ? Position.Left : Position.Right}
id={myId} id={myId}
isValidConnection={(connection) => isValidConnection={(connection) =>
isValidConnection(connection as Connection) isLocked ? false : isValidConnection(connection as Connection)
} }
className={cn( className={cn(
`group/handle z-50 transition-all`, `group/handle z-50 transition-all`,
!showNode && "no-show", !showNode && "no-show",
)} )}
style={BASE_HANDLE_STYLES} style={{
...BASE_HANDLE_STYLES,
pointerEvents: isLocked ? "none" : "auto",
}}
onClick={handleClick} onClick={handleClick}
onMouseUp={handleMouseUp} onMouseUp={handleMouseUp}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}

View file

@ -34,6 +34,7 @@ import {
useState, useState,
} from "react"; } from "react";
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from "react-hotkeys-hook";
import { useShallow } from "zustand/react/shallow";
import GenericNode from "../../../../CustomNodes/GenericNode"; import GenericNode from "../../../../CustomNodes/GenericNode";
import { import {
INVALID_SELECTION_ERROR_ALERT, INVALID_SELECTION_ERROR_ALERT,
@ -124,6 +125,10 @@ export default function Page({
const [selectionMenuVisible, setSelectionMenuVisible] = useState(false); const [selectionMenuVisible, setSelectionMenuVisible] = useState(false);
const edgeUpdateSuccessful = useRef(true); const edgeUpdateSuccessful = useRef(true);
const isLocked = useFlowStore(
useShallow((state) => state.currentFlow?.locked),
);
const position = useRef({ x: 0, y: 0 }); const position = useRef({ x: 0, y: 0 });
const [lastSelection, setLastSelection] = const [lastSelection, setLastSelection] =
useState<OnSelectionChangeParams | null>(null); useState<OnSelectionChangeParams | null>(null);
@ -521,6 +526,11 @@ export default function Page({
); );
const handleEdgeClick = (event, edge) => { const handleEdgeClick = (event, edge) => {
if (isLocked) {
event.preventDefault();
event.stopPropagation();
return;
}
const color = const color =
nodeColorsName[edge?.data?.sourceHandle?.output_types[0]] || "cyan"; nodeColorsName[edge?.data?.sourceHandle?.output_types[0]] || "cyan";
@ -584,13 +594,13 @@ export default function Page({
edges={edges} edges={edges}
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onConnect={onConnectMod} onConnect={isLocked ? undefined : onConnectMod}
disableKeyboardA11y={true} disableKeyboardA11y={true}
onInit={setReactFlowInstance} onInit={setReactFlowInstance}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
onReconnect={onEdgeUpdate} onReconnect={isLocked ? undefined : onEdgeUpdate}
onReconnectStart={onEdgeUpdateStart} onReconnectStart={isLocked ? undefined : onEdgeUpdateStart}
onReconnectEnd={onEdgeUpdateEnd} onReconnectEnd={isLocked ? undefined : onEdgeUpdateEnd}
onNodeDragStart={onNodeDragStart} onNodeDragStart={onNodeDragStart}
onSelectionDragStart={onSelectionDragStart} onSelectionDragStart={onSelectionDragStart}
elevateEdgesOnSelect={true} elevateEdgesOnSelect={true}

View file

@ -1,4 +1,4 @@
import { test } from "@playwright/test"; import { expect, Page, test } from "@playwright/test";
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
import path from "path"; import path from "path";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
@ -74,5 +74,42 @@ test(
timeout: 3000, timeout: 3000,
state: "visible", state: "visible",
}); });
await tryDeleteEdge(page);
await page.locator(".react-flow__edge-path").nth(0).click();
await page.keyboard.press("Delete");
let numberOfEdges = await page.locator(".react-flow__edge-path").count();
expect(numberOfEdges).toBe(2);
await page.locator(".react-flow__edge-path").nth(0).click();
await page.keyboard.press("Delete");
numberOfEdges = await page.locator(".react-flow__edge-path").count();
expect(numberOfEdges).toBe(1);
await page.locator(".react-flow__edge-path").nth(0).click();
await page.keyboard.press("Delete");
numberOfEdges = await page.locator(".react-flow__edge-path").count();
expect(numberOfEdges).toBe(0);
}, },
); );
async function tryDeleteEdge(page: Page) {
await page.getByTestId("lock_unlock").click();
let numberOfEdges = await page.locator(".react-flow__edge-path").count();
expect(numberOfEdges).toBe(3);
const numberOfTries = 50;
for (let i = 0; i < numberOfTries; i++) {
await page.locator(".react-flow__edge-path").nth(0).click();
await page.keyboard.press("Delete");
await page.locator(".react-flow__edge-path").nth(1).click();
await page.keyboard.press("Delete");
await page.locator(".react-flow__edge-path").nth(2).click();
await page.keyboard.press("Delete");
numberOfEdges = await page.locator(".react-flow__edge-path").count();
expect(numberOfEdges).toBe(3);
}
//unlock the flow
await page.getByTestId("lock_unlock").click();
}