mirror of
https://github.com/FlowiseAI/Flowise.git
synced 2026-10-12 02:02:17 +00:00
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:
co-authored by
gemini-code-assist[bot]
parent
2e3dfdef03
commit
df5507ce36
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user