mirror of https://github.com/langgenius/dify.git
node handle connection line
This commit is contained in:
parent
49f78bacef
commit
58d8b0dd01
|
|
@ -24,6 +24,8 @@ import {
|
|||
} from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
type NodeSelectorProps = {
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
onSelect: (type: BlockEnum) => void
|
||||
trigger?: (open: boolean) => React.ReactNode
|
||||
placement?: Placement
|
||||
|
|
@ -34,6 +36,8 @@ type NodeSelectorProps = {
|
|||
asChild?: boolean
|
||||
}
|
||||
const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
open: openFromProps,
|
||||
onOpenChange,
|
||||
onSelect,
|
||||
trigger,
|
||||
placement = 'right',
|
||||
|
|
@ -43,18 +47,25 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
|||
popupClassName,
|
||||
asChild,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [localOpen, setLocalOpen] = useState(false)
|
||||
const open = openFromProps === undefined ? localOpen : openFromProps
|
||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||
setLocalOpen(newOpen)
|
||||
|
||||
if (onOpenChange)
|
||||
onOpenChange(newOpen)
|
||||
}, [onOpenChange])
|
||||
const handleTrigger = useCallback<MouseEventHandler<HTMLDivElement>>((e) => {
|
||||
e.stopPropagation()
|
||||
setOpen(v => !v)
|
||||
}, [])
|
||||
handleOpenChange(!open)
|
||||
}, [open, handleOpenChange])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
placement={placement}
|
||||
offset={offset}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
asChild={asChild}
|
||||
|
|
@ -67,7 +78,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
|||
<div
|
||||
className={`
|
||||
flex items-center justify-center
|
||||
w-4 h-4 rounded-full bg-primary-600 cursor-pointer z-10 group-hover:flex
|
||||
w-4 h-4 rounded-full bg-primary-600 cursor-pointer z-10
|
||||
${triggerClassName?.(open)}
|
||||
`}
|
||||
style={triggerStyle}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react'
|
||||
import type { ConnectionLineComponentProps } from 'reactflow'
|
||||
import {
|
||||
Position,
|
||||
getSimpleBezierPath,
|
||||
} from 'reactflow'
|
||||
|
||||
const CustomConnectionLine = ({ fromX, fromY, toX, toY }: ConnectionLineComponentProps) => {
|
||||
const [
|
||||
edgePath,
|
||||
] = getSimpleBezierPath({
|
||||
sourceX: fromX,
|
||||
sourceY: fromY,
|
||||
sourcePosition: Position.Right,
|
||||
targetX: toX,
|
||||
targetY: toY,
|
||||
targetPosition: Position.Left,
|
||||
})
|
||||
|
||||
return (
|
||||
<g>
|
||||
<path
|
||||
fill="none"
|
||||
stroke='#D0D5DD'
|
||||
strokeWidth={2}
|
||||
d={edgePath}
|
||||
/>
|
||||
<rect
|
||||
x={toX - 2}
|
||||
y={toY - 4}
|
||||
width={2}
|
||||
height={8}
|
||||
fill='#2970FF'
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomConnectionLine
|
||||
|
|
@ -22,10 +22,10 @@ const CustomEdge = ({
|
|||
labelX,
|
||||
labelY,
|
||||
] = getSimpleBezierPath({
|
||||
sourceX,
|
||||
sourceX: sourceX - 8,
|
||||
sourceY,
|
||||
sourcePosition: Position.Right,
|
||||
targetX,
|
||||
targetX: targetX + 8,
|
||||
targetY,
|
||||
targetPosition: Position.Left,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import Header from './header'
|
|||
import CustomNode from './nodes'
|
||||
import ZoomInOut from './zoom-in-out'
|
||||
import CustomEdge from './custom-edge'
|
||||
import CustomConnectionLine from './custom-connection-line'
|
||||
import Panel from './panel'
|
||||
import type { Node } from './types'
|
||||
|
||||
|
|
@ -69,6 +70,7 @@ const Workflow: FC<WorkflowProps> = memo(({
|
|||
onEdgeMouseEnter={handleEnterEdge}
|
||||
onEdgeMouseLeave={handleLeaveEdge}
|
||||
multiSelectionKeyCode={null}
|
||||
connectionLineComponent={CustomConnectionLine}
|
||||
>
|
||||
<Background
|
||||
gap={[14, 14]}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import {
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import {
|
||||
Handle,
|
||||
|
|
@ -14,8 +18,16 @@ export const NodeTargetHandle = ({
|
|||
id,
|
||||
data,
|
||||
}: NodeProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const store = useStoreApi()
|
||||
const incomers = getIncomers({ id } as Node, store.getState().getNodes(), store.getState().edges)
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
setOpen(v)
|
||||
}, [])
|
||||
const handleHandleClick = () => {
|
||||
if (incomers.length === 0 && data.type !== BlockEnum.Start)
|
||||
handleOpenChange(!open)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -23,26 +35,31 @@ export const NodeTargetHandle = ({
|
|||
type='target'
|
||||
position={Position.Left}
|
||||
className={`
|
||||
!top-[17px] !left-0 !w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none !translate-y-0 z-[1]
|
||||
after:absolute after:w-0.5 after:h-2 after:-left-0.5 after:top-1 after:bg-primary-500
|
||||
${(data.type === BlockEnum.Start || !incomers.length) && 'opacity-0'}
|
||||
!top-[17px] !-left-2 !w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none !translate-y-0 z-[1]
|
||||
after:absolute after:w-0.5 after:h-2 after:left-1.5 after:top-1 after:bg-primary-500
|
||||
${!incomers.length && 'after:opacity-0'}
|
||||
${data.type === BlockEnum.Start && 'opacity-0'}
|
||||
`}
|
||||
isConnectable={data.type !== BlockEnum.Start}
|
||||
/>
|
||||
{
|
||||
incomers.length === 0 && data.type !== BlockEnum.Start && (
|
||||
<BlockSelector
|
||||
onSelect={() => {}}
|
||||
asChild
|
||||
placement='left'
|
||||
triggerClassName={open => `
|
||||
hidden absolute -left-2 top-4
|
||||
${data.hovering && '!flex'}
|
||||
${open && '!flex'}
|
||||
`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClick={handleHandleClick}
|
||||
>
|
||||
{
|
||||
incomers.length === 0 && data.type !== BlockEnum.Start && (
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSelect={() => {}}
|
||||
asChild
|
||||
placement='left'
|
||||
triggerClassName={open => `
|
||||
hidden absolute left-0 top-0 pointer-events-none
|
||||
${data.hovering && '!flex'}
|
||||
${open && '!flex'}
|
||||
`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Handle>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -59,9 +76,17 @@ export const NodeSourceHandle = ({
|
|||
handleClassName,
|
||||
nodeSelectorClassName,
|
||||
}: NodeSourceHandleProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const store = useStoreApi()
|
||||
const connectedEdges = getConnectedEdges([{ id } as Node], store.getState().edges)
|
||||
const connected = connectedEdges.find(edge => edge.sourceHandle === handleId)
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
setOpen(v)
|
||||
}, [])
|
||||
const handleHandleClick = () => {
|
||||
if (!connected)
|
||||
handleOpenChange(!open)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -71,25 +96,29 @@ export const NodeSourceHandle = ({
|
|||
position={Position.Right}
|
||||
className={`
|
||||
!w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none !translate-y-0 z-[1]
|
||||
after:absolute after:w-0.5 after:h-2 after:-right-0.5 after:top-1 after:bg-primary-500
|
||||
${!connected && 'opacity-0'}
|
||||
after:absolute after:w-0.5 after:h-2 after:right-1.5 after:top-1 after:bg-primary-500
|
||||
${!connected && 'after:opacity-0'}
|
||||
${handleClassName}
|
||||
`}
|
||||
/>
|
||||
{
|
||||
!connected && (
|
||||
<BlockSelector
|
||||
onSelect={() => {}}
|
||||
asChild
|
||||
triggerClassName={open => `
|
||||
hidden
|
||||
${nodeSelectorClassName}
|
||||
${data.hovering && '!flex'}
|
||||
${open && '!flex'}
|
||||
`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClick={handleHandleClick}
|
||||
>
|
||||
{
|
||||
!connected && (
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSelect={() => {}}
|
||||
asChild
|
||||
triggerClassName={open => `
|
||||
hidden absolute top-0 left-0 pointer-events-none
|
||||
${nodeSelectorClassName}
|
||||
${data.hovering && '!flex'}
|
||||
${open && '!flex'}
|
||||
`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Handle>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,7 @@ const Node = (props: Pick<NodeProps, 'id' | 'data'>) => {
|
|||
<NodeSourceHandle
|
||||
{...props}
|
||||
handleId='condition1'
|
||||
handleClassName='!top-1 !-right-3'
|
||||
nodeSelectorClassName='absolute top-1 -right-5'
|
||||
handleClassName='!top-1 !-right-5'
|
||||
/>
|
||||
</div>
|
||||
<div className='mb-0.5 leading-4 text-[10px] font-medium text-gray-500 uppercase'>{t(`${i18nPrefix}.conditions`)}</div>
|
||||
|
|
@ -50,8 +49,7 @@ const Node = (props: Pick<NodeProps, 'id' | 'data'>) => {
|
|||
<NodeSourceHandle
|
||||
{...props}
|
||||
handleId='condition2'
|
||||
handleClassName='!top-1 !-right-3'
|
||||
nodeSelectorClassName='absolute top-1 -right-5'
|
||||
handleClassName='!top-1 !-right-5'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,8 +26,7 @@ const CustomNode = memo((props: NodeProps) => {
|
|||
nodeData.type !== BlockEnum.IfElse && (
|
||||
<NodeSourceHandle
|
||||
{ ...props }
|
||||
handleClassName='!top-[17px] !right-0'
|
||||
nodeSelectorClassName='absolute -right-2 top-4'
|
||||
handleClassName='!top-[17px] !-right-2'
|
||||
handleId='source'
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue