Skip to content

Commit 30a0f11

Browse files
authored
fix: implement locked flow interaction restrictions (#8645)
* 📝 (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 * ✨ (lock-flow.spec.ts): add functionality to try connecting nodes and handle edge cases when connecting nodes in the flow diagram.
1 parent 8f4d1cb commit 30a0f11

3 files changed

Lines changed: 120 additions & 14 deletions

File tree

src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import useFlowStore from "@/stores/flowStore";
33
import { nodeColorsName } from "@/utils/styleUtils";
44
import { Connection, Handle, Position } from "@xyflow/react";
55
import { memo, useCallback, useEffect, useMemo, useState } from "react";
6+
import { useShallow } from "zustand/react/shallow";
67
import ShadTooltip from "../../../../components/common/shadTooltipComponent";
78
import {
89
isValidConnection,
@@ -179,6 +180,10 @@ const HandleRenderComponent = memo(function HandleRenderComponent({
179180
const [isHovered, setIsHovered] = useState(false);
180181
const [openTooltip, setOpenTooltip] = useState(false);
181182

183+
const isLocked = useFlowStore(
184+
useShallow((state) => state.currentFlow?.locked),
185+
);
186+
182187
const {
183188
setHandleDragging,
184189
setFilterType,
@@ -384,16 +389,10 @@ const HandleRenderComponent = memo(function HandleRenderComponent({
384389
[],
385390
);
386391

387-
// Memoize the validation function
388-
const validateConnection = useCallback(
389-
(connection: any) => isValidConnection(connection),
390-
[],
391-
);
392-
393392
return (
394393
<div>
395394
<ShadTooltip
396-
open={openTooltip}
395+
open={openTooltip && !isLocked}
397396
setOpen={setOpenTooltip}
398397
styleClasses={cn("tooltip-fixed-width custom-scroll nowheel bottom-2")}
399398
delayDuration={1000}
@@ -414,13 +413,16 @@ const HandleRenderComponent = memo(function HandleRenderComponent({
414413
position={left ? Position.Left : Position.Right}
415414
id={myId}
416415
isValidConnection={(connection) =>
417-
isValidConnection(connection as Connection)
416+
isLocked ? false : isValidConnection(connection as Connection)
418417
}
419418
className={cn(
420419
`group/handle z-50 transition-all`,
421420
!showNode && "no-show",
422421
)}
423-
style={BASE_HANDLE_STYLES}
422+
style={{
423+
...BASE_HANDLE_STYLES,
424+
pointerEvents: isLocked ? "none" : "auto",
425+
}}
424426
onClick={handleClick}
425427
onMouseUp={handleMouseUp}
426428
onContextMenu={handleContextMenu}

src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
useState,
3535
} from "react";
3636
import { useHotkeys } from "react-hotkeys-hook";
37+
import { useShallow } from "zustand/react/shallow";
3738
import GenericNode from "../../../../CustomNodes/GenericNode";
3839
import {
3940
INVALID_SELECTION_ERROR_ALERT,
@@ -124,6 +125,10 @@ export default function Page({
124125
const [selectionMenuVisible, setSelectionMenuVisible] = useState(false);
125126
const edgeUpdateSuccessful = useRef(true);
126127

128+
const isLocked = useFlowStore(
129+
useShallow((state) => state.currentFlow?.locked),
130+
);
131+
127132
const position = useRef({ x: 0, y: 0 });
128133
const [lastSelection, setLastSelection] =
129134
useState<OnSelectionChangeParams | null>(null);
@@ -521,6 +526,11 @@ export default function Page({
521526
);
522527

523528
const handleEdgeClick = (event, edge) => {
529+
if (isLocked) {
530+
event.preventDefault();
531+
event.stopPropagation();
532+
return;
533+
}
524534
const color =
525535
nodeColorsName[edge?.data?.sourceHandle?.output_types[0]] || "cyan";
526536

@@ -584,13 +594,13 @@ export default function Page({
584594
edges={edges}
585595
onNodesChange={onNodesChange}
586596
onEdgesChange={onEdgesChange}
587-
onConnect={onConnectMod}
597+
onConnect={isLocked ? undefined : onConnectMod}
588598
disableKeyboardA11y={true}
589599
onInit={setReactFlowInstance}
590600
nodeTypes={nodeTypes}
591-
onReconnect={onEdgeUpdate}
592-
onReconnectStart={onEdgeUpdateStart}
593-
onReconnectEnd={onEdgeUpdateEnd}
601+
onReconnect={isLocked ? undefined : onEdgeUpdate}
602+
onReconnectStart={isLocked ? undefined : onEdgeUpdateStart}
603+
onReconnectEnd={isLocked ? undefined : onEdgeUpdateEnd}
594604
onNodeDragStart={onNodeDragStart}
595605
onSelectionDragStart={onSelectionDragStart}
596606
elevateEdgesOnSelect={true}

src/frontend/tests/extended/features/lock-flow.spec.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { test } from "@playwright/test";
1+
import { expect, Page, test } from "@playwright/test";
22
import * as dotenv from "dotenv";
33
import path from "path";
44
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
@@ -74,5 +74,99 @@ test(
7474
timeout: 3000,
7575
state: "visible",
7676
});
77+
78+
await tryDeleteEdge(page);
79+
await page.locator(".react-flow__edge-path").nth(0).click();
80+
await page.keyboard.press("Delete");
81+
let numberOfEdges = await page.locator(".react-flow__edge-path").count();
82+
expect(numberOfEdges).toBe(2);
83+
84+
await page.locator(".react-flow__edge-path").nth(0).click();
85+
await page.keyboard.press("Delete");
86+
numberOfEdges = await page.locator(".react-flow__edge-path").count();
87+
expect(numberOfEdges).toBe(1);
88+
89+
await page.locator(".react-flow__edge-path").nth(0).click();
90+
await page.keyboard.press("Delete");
91+
numberOfEdges = await page.locator(".react-flow__edge-path").count();
92+
expect(numberOfEdges).toBe(0);
93+
94+
await tryConnectNodes(page);
95+
96+
await page.getByTestId("handle-prompt-shownode-prompt-right").click();
97+
await page
98+
.getByTestId("handle-languagemodelcomponent-shownode-system message-left")
99+
.click();
100+
101+
await page
102+
.getByTestId("handle-chatinput-shownode-chat message-right")
103+
.click();
104+
await page
105+
.getByTestId("handle-languagemodelcomponent-shownode-input-left")
106+
.click();
107+
108+
await page
109+
.getByTestId(
110+
"handle-languagemodelcomponent-shownode-model response-right",
111+
)
112+
.click();
113+
await page.getByTestId("handle-chatoutput-shownode-inputs-left").click();
114+
numberOfEdges = await page.locator(".react-flow__edge-path").count();
115+
116+
expect(numberOfEdges).toBe(3);
77117
},
78118
);
119+
120+
async function tryConnectNodes(page: Page) {
121+
await page.getByTestId("lock_unlock").click();
122+
123+
const numberOfTries = 5;
124+
let numberOfEdges = await page.locator(".react-flow__edge-path").count();
125+
126+
for (let i = 0; i < numberOfTries; i++) {
127+
try {
128+
await page.getByTestId("handle-prompt-shownode-prompt-right").click({
129+
timeout: 500,
130+
});
131+
} catch (e) {
132+
numberOfEdges = await page.locator(".react-flow__edge-path").count();
133+
expect(numberOfEdges).toBe(0);
134+
}
135+
136+
try {
137+
await page
138+
.getByTestId(
139+
"handle-languagemodelcomponent-shownode-system message-left",
140+
)
141+
.click({
142+
timeout: 500,
143+
});
144+
} catch (e) {
145+
numberOfEdges = await page.locator(".react-flow__edge-path").count();
146+
expect(numberOfEdges).toBe(0);
147+
}
148+
}
149+
150+
await page.getByTestId("lock_unlock").click();
151+
}
152+
153+
async function tryDeleteEdge(page: Page) {
154+
await page.getByTestId("lock_unlock").click();
155+
156+
let numberOfEdges = await page.locator(".react-flow__edge-path").count();
157+
expect(numberOfEdges).toBe(3);
158+
const numberOfTries = 50;
159+
160+
for (let i = 0; i < numberOfTries; i++) {
161+
await page.locator(".react-flow__edge-path").nth(0).click();
162+
await page.keyboard.press("Delete");
163+
await page.locator(".react-flow__edge-path").nth(1).click();
164+
await page.keyboard.press("Delete");
165+
await page.locator(".react-flow__edge-path").nth(2).click();
166+
await page.keyboard.press("Delete");
167+
numberOfEdges = await page.locator(".react-flow__edge-path").count();
168+
expect(numberOfEdges).toBe(3);
169+
}
170+
//unlock the flow
171+
await page.getByTestId("lock_unlock").click();
172+
}

0 commit comments

Comments
 (0)