### What problem does this PR solve? Feat: The delete button is displayed only when the cursor is hovered over the connection line #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.0
| @@ -1,5 +1,6 @@ | |||
| import { | |||
| BaseEdge, | |||
| Edge, | |||
| EdgeLabelRenderer, | |||
| EdgeProps, | |||
| getBezierPath, | |||
| @@ -8,7 +9,9 @@ import useGraphStore from '../../store'; | |||
| import { useTheme } from '@/components/theme-provider'; | |||
| import { useFetchAgent } from '@/hooks/use-agent-request'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { useMemo } from 'react'; | |||
| import { NodeHandleId, Operator } from '../../constant'; | |||
| import styles from './index.less'; | |||
| export function ButtonEdge({ | |||
| @@ -24,7 +27,9 @@ export function ButtonEdge({ | |||
| style = {}, | |||
| markerEnd, | |||
| selected, | |||
| }: EdgeProps) { | |||
| data, | |||
| sourceHandleId, | |||
| }: EdgeProps<Edge<{ isHovered: boolean }>>) { | |||
| const deleteEdgeById = useGraphStore((state) => state.deleteEdgeById); | |||
| const [edgePath, labelX, labelY] = getBezierPath({ | |||
| sourceX, | |||
| @@ -72,6 +77,14 @@ export function ButtonEdge({ | |||
| return {}; | |||
| }, [source, target, graphPath]); | |||
| const visible = useMemo(() => { | |||
| return ( | |||
| data?.isHovered && | |||
| sourceHandleId !== NodeHandleId.Tool && // The connection between the agent node and the tool node does not need to display the delete button | |||
| !target.startsWith(Operator.Tool) | |||
| ); | |||
| }, [data?.isHovered, sourceHandleId, target]); | |||
| return ( | |||
| <> | |||
| <BaseEdge | |||
| @@ -79,6 +92,7 @@ export function ButtonEdge({ | |||
| markerEnd={markerEnd} | |||
| style={{ ...style, ...selectedStyle, ...highlightStyle }} | |||
| /> | |||
| <EdgeLabelRenderer> | |||
| <div | |||
| style={{ | |||
| @@ -93,9 +107,11 @@ export function ButtonEdge({ | |||
| className="nodrag nopan" | |||
| > | |||
| <button | |||
| className={ | |||
| theme === 'dark' ? styles.edgeButtonDark : styles.edgeButton | |||
| } | |||
| className={cn( | |||
| theme === 'dark' ? styles.edgeButtonDark : styles.edgeButton, | |||
| 'invisible', | |||
| { visible }, | |||
| )} | |||
| type="button" | |||
| onClick={onEdgeClick} | |||
| > | |||
| @@ -84,6 +84,8 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { | |||
| onEdgesChange, | |||
| onNodesChange, | |||
| onSelectionChange, | |||
| onEdgeMouseEnter, | |||
| onEdgeMouseLeave, | |||
| } = useSelectCanvasData(); | |||
| const isValidConnection = useValidateConnection(); | |||
| @@ -170,6 +172,8 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { | |||
| onSelectionChange={onSelectionChange} | |||
| nodeOrigin={[0.5, 0]} | |||
| isValidConnection={isValidConnection} | |||
| onEdgeMouseEnter={onEdgeMouseEnter} | |||
| onEdgeMouseLeave={onEdgeMouseLeave} | |||
| defaultEdgeOptions={{ | |||
| type: 'buttonEdge', | |||
| markerEnd: 'logo', | |||
| @@ -5,14 +5,19 @@ import { | |||
| } from '@/components/xyflow/tooltip-node'; | |||
| import { Position } from '@xyflow/react'; | |||
| import { Copy, Play, Trash2 } from 'lucide-react'; | |||
| import { MouseEventHandler, PropsWithChildren, useCallback } from 'react'; | |||
| import { | |||
| HTMLAttributes, | |||
| MouseEventHandler, | |||
| PropsWithChildren, | |||
| useCallback, | |||
| } from 'react'; | |||
| import { Operator } from '../../constant'; | |||
| import { useDuplicateNode } from '../../hooks'; | |||
| import useGraphStore from '../../store'; | |||
| function IconWrapper({ children }: PropsWithChildren) { | |||
| function IconWrapper({ children, ...props }: HTMLAttributes<HTMLDivElement>) { | |||
| return ( | |||
| <div className="p-1.5 bg-text-title rounded-sm cursor-pointer"> | |||
| <div className="p-1.5 bg-text-title rounded-sm cursor-pointer" {...props}> | |||
| {children} | |||
| </div> | |||
| ); | |||
| @@ -30,7 +35,7 @@ export function ToolBar({ selected, children, label, id }: ToolBarProps) { | |||
| (store) => store.deleteIterationNodeById, | |||
| ); | |||
| const deleteNode: MouseEventHandler<SVGElement> = useCallback( | |||
| const deleteNode: MouseEventHandler<HTMLDivElement> = useCallback( | |||
| (e) => { | |||
| e.stopPropagation(); | |||
| if (label === Operator.Iteration) { | |||
| @@ -44,7 +49,7 @@ export function ToolBar({ selected, children, label, id }: ToolBarProps) { | |||
| const duplicateNode = useDuplicateNode(); | |||
| const handleDuplicate: MouseEventHandler<SVGElement> = useCallback( | |||
| const handleDuplicate: MouseEventHandler<HTMLDivElement> = useCallback( | |||
| (e) => { | |||
| e.stopPropagation(); | |||
| duplicateNode(id, label); | |||
| @@ -61,11 +66,11 @@ export function ToolBar({ selected, children, label, id }: ToolBarProps) { | |||
| <IconWrapper> | |||
| <Play className="size-3.5" /> | |||
| </IconWrapper> | |||
| <IconWrapper> | |||
| <Copy className="size-3.5" onClick={handleDuplicate} /> | |||
| <IconWrapper onClick={handleDuplicate}> | |||
| <Copy className="size-3.5" /> | |||
| </IconWrapper> | |||
| <IconWrapper> | |||
| <Trash2 className="size-3.5" onClick={deleteNode} /> | |||
| <IconWrapper onClick={deleteNode}> | |||
| <Trash2 className="size-3.5" /> | |||
| </IconWrapper> | |||
| </section> | |||
| </TooltipContent> | |||
| @@ -90,6 +90,8 @@ const selector = (state: RFState) => ({ | |||
| onConnect: state.onConnect, | |||
| setNodes: state.setNodes, | |||
| onSelectionChange: state.onSelectionChange, | |||
| onEdgeMouseEnter: state.onEdgeMouseEnter, | |||
| onEdgeMouseLeave: state.onEdgeMouseLeave, | |||
| }); | |||
| export const useSelectCanvasData = () => { | |||
| @@ -4,6 +4,7 @@ import { | |||
| Connection, | |||
| Edge, | |||
| EdgeChange, | |||
| EdgeMouseHandler, | |||
| OnConnect, | |||
| OnEdgesChange, | |||
| OnNodesChange, | |||
| @@ -27,6 +28,7 @@ import { | |||
| generateNodeNamesWithIncreasingIndex, | |||
| getOperatorIndex, | |||
| isEdgeEqual, | |||
| mapEdgeMouseEvent, | |||
| } from './utils'; | |||
| export type RFState = { | |||
| @@ -38,6 +40,9 @@ export type RFState = { | |||
| clickedToolId: string; // currently selected tool id | |||
| onNodesChange: OnNodesChange<RAGFlowNodeType>; | |||
| onEdgesChange: OnEdgesChange; | |||
| onEdgeMouseEnter?: EdgeMouseHandler<Edge>; | |||
| /** This event handler is called when mouse of a user leaves an edge */ | |||
| onEdgeMouseLeave?: EdgeMouseHandler<Edge>; | |||
| onConnect: OnConnect; | |||
| setNodes: (nodes: RAGFlowNodeType[]) => void; | |||
| setEdges: (edges: Edge[]) => void; | |||
| @@ -98,6 +103,20 @@ const useGraphStore = create<RFState>()( | |||
| edges: applyEdgeChanges(changes, get().edges), | |||
| }); | |||
| }, | |||
| onEdgeMouseEnter: (event, edge) => { | |||
| const { edges, setEdges } = get(); | |||
| const edgeId = edge.id; | |||
| // Updates edge | |||
| setEdges(mapEdgeMouseEvent(edges, edgeId, true)); | |||
| }, | |||
| onEdgeMouseLeave: (event, edge) => { | |||
| const { edges, setEdges } = get(); | |||
| const edgeId = edge.id; | |||
| // Updates edge | |||
| setEdges(mapEdgeMouseEvent(edges, edgeId, false)); | |||
| }, | |||
| onConnect: (connection: Connection) => { | |||
| const { | |||
| deletePreviousEdgeOfClassificationNode, | |||
| @@ -466,3 +466,23 @@ export function getAgentNodeTools(agentNode?: RAGFlowNodeType) { | |||
| const tools: IAgentForm['tools'] = get(agentNode, 'data.form.tools', []); | |||
| return tools; | |||
| } | |||
| export function mapEdgeMouseEvent( | |||
| edges: Edge[], | |||
| edgeId: string, | |||
| isHovered: boolean, | |||
| ) { | |||
| const nextEdges = edges.map((element) => | |||
| element.id === edgeId | |||
| ? { | |||
| ...element, | |||
| data: { | |||
| ...element.data, | |||
| isHovered, | |||
| }, | |||
| } | |||
| : element, | |||
| ); | |||
| return nextEdges; | |||
| } | |||