### What problem does this PR solve? Feat: Add child nodes and their connecting lines by clicking #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.1
| @@ -1,7 +1,7 @@ | |||
| import { IAgentNode } from '@/interfaces/database/flow'; | |||
| import { Handle, NodeProps, Position } from '@xyflow/react'; | |||
| import { memo, useMemo } from 'react'; | |||
| import { Operator } from '../../constant'; | |||
| import { NodeHandleId, Operator } from '../../constant'; | |||
| import useGraphStore from '../../store'; | |||
| import { CommonHandle } from './handle'; | |||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | |||
| @@ -31,21 +31,22 @@ function InnerAgentNode({ | |||
| {isNotParentAgent && ( | |||
| <> | |||
| <CommonHandle | |||
| id="c" | |||
| type="source" | |||
| type="target" | |||
| position={Position.Left} | |||
| isConnectable={isConnectable} | |||
| style={LeftHandleStyle} | |||
| nodeId={id} | |||
| id={NodeHandleId.End} | |||
| ></CommonHandle> | |||
| <CommonHandle | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| id="b" | |||
| style={RightHandleStyle} | |||
| nodeId={id} | |||
| id={NodeHandleId.Start} | |||
| isConnectableEnd={false} | |||
| ></CommonHandle> | |||
| </> | |||
| )} | |||
| @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'; | |||
| import { | |||
| BeginQueryType, | |||
| BeginQueryTypeIconMap, | |||
| NodeHandleId, | |||
| Operator, | |||
| } from '../../constant'; | |||
| import { BeginQuery } from '../../interface'; | |||
| @@ -27,9 +28,9 @@ function InnerBeginNode({ data, id }: NodeProps<IBeginNode>) { | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable | |||
| className={styles.handle} | |||
| style={RightHandleStyle} | |||
| nodeId={id} | |||
| id={NodeHandleId.Start} | |||
| ></CommonHandle> | |||
| <section className="flex items-center justify-center gap-2"> | |||
| @@ -3,6 +3,7 @@ import { ICategorizeNode } from '@/interfaces/database/flow'; | |||
| import { NodeProps, Position } from '@xyflow/react'; | |||
| import { get } from 'lodash'; | |||
| import { memo } from 'react'; | |||
| import { NodeHandleId } from '../../constant'; | |||
| import { CommonHandle } from './handle'; | |||
| import { RightHandleStyle } from './handle-icon'; | |||
| import NodeHeader from './node-header'; | |||
| @@ -23,7 +24,7 @@ export function InnerCategorizeNode({ | |||
| type="target" | |||
| position={Position.Left} | |||
| isConnectable | |||
| id={'a'} | |||
| id={NodeHandleId.End} | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| @@ -47,6 +48,7 @@ export function InnerCategorizeNode({ | |||
| isConnectable | |||
| style={{ ...RightHandleStyle, top: position.top }} | |||
| nodeId={id} | |||
| isConnectableEnd={false} | |||
| ></CommonHandle> | |||
| </div> | |||
| ); | |||
| @@ -30,8 +30,8 @@ function OperatorItemList({ operators }: OperatorItemProps) { | |||
| key={x} | |||
| className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start" | |||
| onClick={addCanvasNode(x, { | |||
| id: nodeId, | |||
| sourceHandle: id, | |||
| nodeId, | |||
| id, | |||
| position, | |||
| })} | |||
| > | |||
| @@ -1,6 +1,7 @@ | |||
| import { IRagNode } from '@/interfaces/database/flow'; | |||
| import { NodeProps, Position } from '@xyflow/react'; | |||
| import { memo } from 'react'; | |||
| import { NodeHandleId } from '../../constant'; | |||
| import { CommonHandle } from './handle'; | |||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | |||
| import NodeHeader from './node-header'; | |||
| @@ -17,8 +18,8 @@ function InnerRagNode({ | |||
| <ToolBar selected={selected} id={id} label={data.label}> | |||
| <NodeWrapper> | |||
| <CommonHandle | |||
| id="c" | |||
| type="source" | |||
| id={NodeHandleId.End} | |||
| type="target" | |||
| position={Position.Left} | |||
| isConnectable={isConnectable} | |||
| style={LeftHandleStyle} | |||
| @@ -28,9 +29,10 @@ function InnerRagNode({ | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable={isConnectable} | |||
| id="b" | |||
| id={NodeHandleId.Start} | |||
| style={RightHandleStyle} | |||
| nodeId={id} | |||
| isConnectableEnd={false} | |||
| ></CommonHandle> | |||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | |||
| </NodeWrapper> | |||
| @@ -4,6 +4,7 @@ import { Flex } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { get } from 'lodash'; | |||
| import { memo } from 'react'; | |||
| import { NodeHandleId } from '../../constant'; | |||
| import { CommonHandle } from './handle'; | |||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | |||
| import styles from './index.less'; | |||
| @@ -22,20 +23,21 @@ function InnerMessageNode({ | |||
| <ToolBar selected={selected} id={id} label={data.label}> | |||
| <NodeWrapper> | |||
| <CommonHandle | |||
| id="c" | |||
| type="source" | |||
| type="target" | |||
| position={Position.Left} | |||
| isConnectable={isConnectable} | |||
| style={LeftHandleStyle} | |||
| nodeId={id} | |||
| id={NodeHandleId.End} | |||
| ></CommonHandle> | |||
| <CommonHandle | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable={isConnectable} | |||
| style={RightHandleStyle} | |||
| id="b" | |||
| id={NodeHandleId.Start} | |||
| nodeId={id} | |||
| isConnectableEnd={false} | |||
| ></CommonHandle> | |||
| <NodeHeader | |||
| id={id} | |||
| @@ -6,6 +6,7 @@ import { Avatar, Flex } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { get } from 'lodash'; | |||
| import { memo, useMemo } from 'react'; | |||
| import { NodeHandleId } from '../../constant'; | |||
| import { CommonHandle } from './handle'; | |||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | |||
| import styles from './index.less'; | |||
| @@ -36,8 +37,8 @@ function InnerRetrievalNode({ | |||
| <ToolBar selected={selected} id={id} label={data.label}> | |||
| <NodeWrapper> | |||
| <CommonHandle | |||
| id="c" | |||
| type="source" | |||
| id={NodeHandleId.End} | |||
| type="target" | |||
| position={Position.Left} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| @@ -45,13 +46,14 @@ function InnerRetrievalNode({ | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| <CommonHandle | |||
| id={NodeHandleId.Start} | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| style={RightHandleStyle} | |||
| id="b" | |||
| nodeId={id} | |||
| isConnectableEnd={false} | |||
| ></CommonHandle> | |||
| <NodeHeader | |||
| id={id} | |||
| @@ -3,7 +3,7 @@ import { Card, CardContent } from '@/components/ui/card'; | |||
| import { ISwitchCondition, ISwitchNode } from '@/interfaces/database/flow'; | |||
| import { NodeProps, Position } from '@xyflow/react'; | |||
| import { memo, useCallback } from 'react'; | |||
| import { SwitchOperatorOptions } from '../../constant'; | |||
| import { NodeHandleId, SwitchOperatorOptions } from '../../constant'; | |||
| import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; | |||
| import { CommonHandle } from './handle'; | |||
| import { RightHandleStyle } from './handle-icon'; | |||
| @@ -65,8 +65,8 @@ function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) { | |||
| type="target" | |||
| position={Position.Left} | |||
| isConnectable | |||
| id={'a'} | |||
| nodeId={id} | |||
| id={NodeHandleId.End} | |||
| ></CommonHandle> | |||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | |||
| <section className="gap-2.5 flex flex-col"> | |||
| @@ -96,6 +96,7 @@ function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) { | |||
| isConnectable | |||
| style={{ ...RightHandleStyle, top: position.top }} | |||
| nodeId={id} | |||
| isConnectableEnd={false} | |||
| ></CommonHandle> | |||
| </div> | |||
| ); | |||
| @@ -3008,3 +3008,8 @@ export const NoDebugOperatorsList = [ | |||
| Operator.Switch, | |||
| Operator.Iteration, | |||
| ]; | |||
| export enum NodeHandleId { | |||
| Start = 'start', | |||
| End = 'end', | |||
| } | |||
| @@ -112,7 +112,7 @@ const AgentForm = ({ node }: INextOperatorForm) => { | |||
| </FormContainer> | |||
| <BlockButton | |||
| onClick={addCanvasNode(Operator.Agent, { | |||
| id: node?.id, | |||
| nodeId: node?.id, | |||
| position: Position.Bottom, | |||
| })} | |||
| > | |||
| @@ -1,10 +1,11 @@ | |||
| import { useFetchModelId } from '@/hooks/logic-hooks'; | |||
| import { Node, Position, ReactFlowInstance } from '@xyflow/react'; | |||
| import { Connection, Node, Position, ReactFlowInstance } from '@xyflow/react'; | |||
| import humanId from 'human-id'; | |||
| import { lowerFirst } from 'lodash'; | |||
| import { useCallback, useMemo } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { | |||
| NodeHandleId, | |||
| NodeMap, | |||
| Operator, | |||
| initialAgentValues, | |||
| @@ -145,8 +146,8 @@ export function useCalculateNewlyChildPosition() { | |||
| const maxY = Math.max(...yAxises); | |||
| const position = { | |||
| y: yAxises.length > 0 ? maxY + 262 : (parentNode?.position.y || 0) + 82, | |||
| x: (parentNode?.position.x || 0) + 140, | |||
| y: yAxises.length > 0 ? maxY + 150 : parentNode?.position.y || 0, | |||
| x: (parentNode?.position.x || 0) + 300, | |||
| }; | |||
| return position; | |||
| @@ -157,6 +158,31 @@ export function useCalculateNewlyChildPosition() { | |||
| return { calculateNewlyBackChildPosition }; | |||
| } | |||
| function useAddChildEdge() { | |||
| const addEdge = useGraphStore((state) => state.addEdge); | |||
| const addChildEdge = useCallback( | |||
| (position: Position = Position.Right, edge: Partial<Connection>) => { | |||
| if ( | |||
| position === Position.Right && | |||
| edge.source && | |||
| edge.target && | |||
| edge.sourceHandle | |||
| ) { | |||
| addEdge({ | |||
| source: edge.source, | |||
| target: edge.target, | |||
| sourceHandle: edge.sourceHandle, | |||
| targetHandle: NodeHandleId.End, | |||
| }); | |||
| } | |||
| }, | |||
| [addEdge], | |||
| ); | |||
| return { addChildEdge }; | |||
| } | |||
| export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { | |||
| const addNode = useGraphStore((state) => state.addNode); | |||
| const getNode = useGraphStore((state) => state.getNode); | |||
| @@ -166,18 +192,19 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { | |||
| const getNodeName = useGetNodeName(); | |||
| const initializeOperatorParams = useInitializeOperatorParams(); | |||
| const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition(); | |||
| const { addChildEdge } = useAddChildEdge(); | |||
| // const [reactFlowInstance, setReactFlowInstance] = | |||
| // useState<ReactFlowInstance<any, any>>(); | |||
| const addCanvasNode = useCallback( | |||
| ( | |||
| type: string, | |||
| params: { id?: string; position?: Position; sourceHandle?: string } = { | |||
| params: { nodeId?: string; position: Position; id?: string } = { | |||
| position: Position.Right, | |||
| }, | |||
| ) => | |||
| (event: React.MouseEvent<HTMLElement>) => { | |||
| const id = params.id; | |||
| const nodeId = params.nodeId; | |||
| // reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition | |||
| // and you don't need to subtract the reactFlowBounds.left/top anymore | |||
| @@ -188,7 +215,7 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { | |||
| }); | |||
| if (params.position === Position.Right) { | |||
| position = calculateNewlyBackChildPosition(id, params.sourceHandle); | |||
| position = calculateNewlyBackChildPosition(nodeId, params.id); | |||
| } | |||
| const newNode: Node<any> = { | |||
| @@ -229,12 +256,15 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { | |||
| }; | |||
| addNode(newNode); | |||
| addNode(iterationStartNode); | |||
| } else if (type === Operator.Agent) { | |||
| const agentNode = getNode(id); | |||
| } else if ( | |||
| type === Operator.Agent && | |||
| params.position === Position.Bottom | |||
| ) { | |||
| const agentNode = getNode(nodeId); | |||
| if (agentNode) { | |||
| // Calculate the coordinates of child nodes to prevent newly added child nodes from covering other child nodes | |||
| const allChildAgentNodeIds = edges | |||
| .filter((x) => x.source === id && x.sourceHandle === 'e') | |||
| .filter((x) => x.source === nodeId && x.sourceHandle === 'e') | |||
| .map((x) => x.target); | |||
| const xAxises = nodes | |||
| @@ -249,9 +279,9 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { | |||
| }; | |||
| } | |||
| addNode(newNode); | |||
| if (id) { | |||
| if (nodeId) { | |||
| addEdge({ | |||
| source: id, | |||
| source: nodeId, | |||
| target: newNode.id, | |||
| sourceHandle: 'e', | |||
| targetHandle: 'f', | |||
| @@ -268,11 +298,18 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { | |||
| newNode.extent = 'parent'; | |||
| } | |||
| addNode(newNode); | |||
| addChildEdge(params.position, { | |||
| source: params.nodeId, | |||
| target: newNode.id, | |||
| sourceHandle: params.id, | |||
| }); | |||
| } | |||
| }, | |||
| [ | |||
| addChildEdge, | |||
| addEdge, | |||
| addNode, | |||
| calculateNewlyBackChildPosition, | |||
| edges, | |||
| getNode, | |||
| getNodeName, | |||