feat(agentFlow): Fix duplicate node and add test cases for delete and duplicate nodes (#5840)

* Test cases added for delete node

* Fix incorrect node id being assigned to duplicate node

* Test cases added for duplicate node

* Fix for bug FLOWISE-257

* lint errors fix

* Fix gemini comments

* FIx comment

* Apply suggestion from @gemini-code-assist[bot]

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Fix lint error

* Fixed review comments

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
j-sanaa
2026-02-25 09:17:22 -08:00
committed by GitHub
co-authored by gemini-code-assist[bot]
parent 2e3dfdef03
commit df5507ce36
5 changed files with 1131 additions and 25 deletions
+2 -1
View File
@@ -34,7 +34,7 @@ These modules carry the highest risk. Test in the same PR when modifying.
| `src/infrastructure/api/client.ts` | `createApiClient` — headers, auth token, 401 interceptor | ✅ Done |
| `src/infrastructure/api/chatflows.ts` | All CRUD + `generateAgentflow` + `getChatModels`, FlowData serialization | ✅ Done |
| `src/infrastructure/api/nodes.ts` | `getAllNodes`, `getNodeByName`, `getNodeIconUrl` | ✅ Done |
| `src/infrastructure/store/AgentflowContext.tsx` | `agentflowReducer` (all actions), `normalizeNodes`. Remaining: `deleteNode()`, `duplicateNode()`, `updateNodeData()`, `getFlowData()` | 🟡 Partial |
| `src/infrastructure/store/AgentflowContext.tsx` | `agentflowReducer` (all actions), `normalizeNodes`, `deleteNode()`, `duplicateNode()`. Remaining: `updateNodeData()`, `getFlowData()` | 🟡 Partial|
| `src/useAgentflow.ts` | `getFlow()`, `toJSON()`, `validate()`, `addNode()`, `clear()` | ⬜ Not yet — thin wrapper |
| `src/features/canvas/hooks/useFlowHandlers.ts` | `handleConnect`, `handleNodesChange`, `handleEdgesChange`, `handleAddNode` — synchronous `onFlowChange` callbacks, dirty tracking, viewport resolution, change filtering | ✅ Done |
@@ -130,6 +130,7 @@ Key features:
- `./src/features/canvas/hooks/useFlowHandlers.ts`
- `./src/features/node-palette/search.ts`
- `./src/infrastructure/api/`
- ⏳ `./src/infrastructure/store/AgentflowContext.tsx` — will be added when coverage reaches 80%
- **Coverage exclusions**:
- `src/__test_utils__/**` — test utilities
- `src/__mocks__/**` — module mocks
+2 -1
View File
@@ -40,7 +40,8 @@ module.exports = {
'./src/core/': { branches: 80, functions: 80, lines: 80, statements: 80 },
'./src/features/canvas/hooks/useFlowHandlers.ts': { branches: 80, functions: 80, lines: 80, statements: 80 },
'./src/features/node-palette/search.ts': { branches: 80, functions: 80, lines: 80, statements: 80 },
'./src/infrastructure/api/': { branches: 80, functions: 80, lines: 80, statements: 80 }
'./src/infrastructure/api/': { branches: 80, functions: 80, lines: 80, statements: 80 },
'./src/infrastructure/store/AgentflowContext.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 }
},
projects: [
// .test.ts → node (fast, no DOM)
+13 -5
View File
@@ -51,7 +51,15 @@ function AgentflowCanvas({
renderHeader?: AgentflowProps['renderHeader']
renderNodePalette?: AgentflowProps['renderNodePalette']
}) {
const { state, setNodes, setEdges, setDirty, setReactFlowInstance, closeEditDialog, registerLocalStateSetters } = useAgentflowContext()
const {
state,
syncNodesFromReactFlow,
syncEdgesFromReactFlow,
setDirty,
setReactFlowInstance,
closeEditDialog,
registerLocalStateSetters
} = useAgentflowContext()
const { isDarkMode } = useConfigContext()
const agentflow = useAgentflow()
const reactFlowWrapper = useRef<HTMLDivElement>(null)
@@ -82,12 +90,12 @@ function AgentflowCanvas({
// Sync local ReactFlow state to context (when user interacts with canvas)
useEffect(() => {
setNodes(nodes as FlowNode[])
}, [nodes, setNodes])
syncNodesFromReactFlow(nodes as FlowNode[])
}, [nodes, syncNodesFromReactFlow])
useEffect(() => {
setEdges(edges as FlowEdge[])
}, [edges, setEdges])
syncEdgesFromReactFlow(edges as FlowEdge[])
}, [edges, syncEdgesFromReactFlow])
// Flow handlers
const { handleConnect, handleNodesChange, handleNodeDragStop, handleEdgesChange, handleAddNode } = useFlowHandlers({
File diff suppressed because it is too large Load Diff
@@ -5,14 +5,20 @@ import type { AgentflowAction, AgentflowState, FlowConfig, FlowData, FlowEdge, F
import { agentflowReducer, initialState, normalizeNodes } from './agentflowReducer'
// Local state setter types
type NodesSetter = (nodes: FlowNode[]) => void
type EdgesSetter = (edges: FlowEdge[]) => void
// Context value interface
export interface AgentflowContextValue {
state: AgentflowState
dispatch: Dispatch<AgentflowAction>
// Convenience methods
setNodes: (nodes: FlowNode[]) => void
setEdges: (edges: FlowEdge[]) => void
setNodes: NodesSetter
setEdges: EdgesSetter
syncNodesFromReactFlow: NodesSetter
syncEdgesFromReactFlow: EdgesSetter
setChatflow: (chatflow: FlowConfig | null) => void
setDirty: (dirty: boolean) => void
setReactFlowInstance: (instance: ReactFlowInstance | null) => void
@@ -34,7 +40,7 @@ export interface AgentflowContextValue {
closeEditDialog: () => void
// Register ReactFlow local state setters
registerLocalStateSetters: (setLocalNodes: (nodes: FlowNode[]) => void, setLocalEdges: (edges: FlowEdge[]) => void) => void
registerLocalStateSetters: (setLocalNodes: NodesSetter, setLocalEdges: EdgesSetter) => void
}
const AgentflowContext = createContext<AgentflowContextValue | null>(null)
@@ -52,16 +58,26 @@ export function AgentflowStateProvider({ children, initialFlow }: AgentflowState
})
// Store ReactFlow local state setters in refs which are populated by AgentflowCanvas
const localNodesSetterRef = useRef<((nodes: FlowNode[]) => void) | null>(null)
const localEdgesSetterRef = useRef<((edges: FlowEdge[]) => void) | null>(null)
const localNodesSetterRef = useRef<NodesSetter | null>(null)
const localEdgesSetterRef = useRef<EdgesSetter | null>(null)
const registerLocalStateSetters = useCallback(
(setLocalNodes: (nodes: FlowNode[]) => void, setLocalEdges: (edges: FlowEdge[]) => void) => {
localNodesSetterRef.current = setLocalNodes
localEdgesSetterRef.current = setLocalEdges
},
[]
)
const registerLocalStateSetters = useCallback((setLocalNodes: NodesSetter, setLocalEdges: EdgesSetter) => {
localNodesSetterRef.current = setLocalNodes
localEdgesSetterRef.current = setLocalEdges
}, [])
// Helper function to generate unique copy IDs
const getUniqueCopyId = useCallback((baseId: string, nodes: FlowNode[]): string => {
const existingIds = new Set(nodes.map((node) => node.id))
for (let i = 1; i < Number.MAX_SAFE_INTEGER; i++) {
const newId = `${baseId}_copy_${i}`
if (!existingIds.has(newId)) {
return newId
}
}
//Fallback
return `${baseId}_copy_${Date.now()}`
}, [])
// Helper function to synchronize state updates between context and ReactFlow
const syncStateUpdate = useCallback(({ nodes, edges }: { nodes?: FlowNode[]; edges?: FlowEdge[] }) => {
@@ -80,11 +96,25 @@ export function AgentflowStateProvider({ children, initialFlow }: AgentflowState
}, [])
// Convenience setters
const setNodes = useCallback((nodes: FlowNode[]) => {
dispatch({ type: 'SET_NODES', payload: nodes })
const setNodes = useCallback<NodesSetter>(
(nodes) => {
syncStateUpdate({ nodes: nodes })
},
[syncStateUpdate]
)
const setEdges = useCallback<EdgesSetter>(
(edges) => {
syncStateUpdate({ edges: edges })
},
[syncStateUpdate]
)
const syncNodesFromReactFlow = useCallback<NodesSetter>((nodes) => {
dispatch({ type: 'SET_NODES', payload: normalizeNodes(nodes) })
}, [])
const setEdges = useCallback((edges: FlowEdge[]) => {
const syncEdgesFromReactFlow = useCallback<EdgesSetter>((edges) => {
dispatch({ type: 'SET_EDGES', payload: edges })
}, [])
@@ -116,20 +146,24 @@ export function AgentflowStateProvider({ children, initialFlow }: AgentflowState
const nodeToDuplicate = state.nodes.find((node) => node.id === nodeId)
if (!nodeToDuplicate) return
const newNodeId = getUniqueCopyId(nodeToDuplicate.id, state.nodes)
const newNode: FlowNode = {
...nodeToDuplicate,
id: `${nodeToDuplicate.id}_copy_${Date.now()}`,
id: newNodeId,
position: {
x: nodeToDuplicate.position.x + 50,
y: nodeToDuplicate.position.y + 50
},
data: { ...nodeToDuplicate.data }
data: {
...nodeToDuplicate.data,
id: newNodeId
}
}
const newNodes = [...state.nodes, newNode]
syncStateUpdate({ nodes: newNodes })
},
[state.nodes, syncStateUpdate]
[state.nodes, syncStateUpdate, getUniqueCopyId]
)
const updateNodeData = useCallback(
@@ -198,6 +232,8 @@ export function AgentflowStateProvider({ children, initialFlow }: AgentflowState
dispatch,
setNodes,
setEdges,
syncNodesFromReactFlow,
syncEdgesFromReactFlow,
setChatflow,
setDirty,
setReactFlowInstance,