mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 04:26:30 +08:00
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'
|
} from '@/app/components/base/icons/src/vender/line/general'
|
||||||
|
|
||||||
type NodeSelectorProps = {
|
type NodeSelectorProps = {
|
||||||
|
open?: boolean
|
||||||
|
onOpenChange?: (open: boolean) => void
|
||||||
onSelect: (type: BlockEnum) => void
|
onSelect: (type: BlockEnum) => void
|
||||||
trigger?: (open: boolean) => React.ReactNode
|
trigger?: (open: boolean) => React.ReactNode
|
||||||
placement?: Placement
|
placement?: Placement
|
||||||
@ -34,6 +36,8 @@ type NodeSelectorProps = {
|
|||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
}
|
}
|
||||||
const NodeSelector: FC<NodeSelectorProps> = ({
|
const NodeSelector: FC<NodeSelectorProps> = ({
|
||||||
|
open: openFromProps,
|
||||||
|
onOpenChange,
|
||||||
onSelect,
|
onSelect,
|
||||||
trigger,
|
trigger,
|
||||||
placement = 'right',
|
placement = 'right',
|
||||||
@ -43,18 +47,25 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
|||||||
popupClassName,
|
popupClassName,
|
||||||
asChild,
|
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) => {
|
const handleTrigger = useCallback<MouseEventHandler<HTMLDivElement>>((e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setOpen(v => !v)
|
handleOpenChange(!open)
|
||||||
}, [])
|
}, [open, handleOpenChange])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PortalToFollowElem
|
<PortalToFollowElem
|
||||||
placement={placement}
|
placement={placement}
|
||||||
offset={offset}
|
offset={offset}
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={setOpen}
|
onOpenChange={handleOpenChange}
|
||||||
>
|
>
|
||||||
<PortalToFollowElemTrigger
|
<PortalToFollowElemTrigger
|
||||||
asChild={asChild}
|
asChild={asChild}
|
||||||
@ -67,7 +78,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
|||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
flex items-center justify-center
|
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)}
|
${triggerClassName?.(open)}
|
||||||
`}
|
`}
|
||||||
style={triggerStyle}
|
style={triggerStyle}
|
||||||
|
|||||||
39
web/app/components/workflow/custom-connection-line.tsx
Normal file
39
web/app/components/workflow/custom-connection-line.tsx
Normal file
@ -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,
|
labelX,
|
||||||
labelY,
|
labelY,
|
||||||
] = getSimpleBezierPath({
|
] = getSimpleBezierPath({
|
||||||
sourceX,
|
sourceX: sourceX - 8,
|
||||||
sourceY,
|
sourceY,
|
||||||
sourcePosition: Position.Right,
|
sourcePosition: Position.Right,
|
||||||
targetX,
|
targetX: targetX + 8,
|
||||||
targetY,
|
targetY,
|
||||||
targetPosition: Position.Left,
|
targetPosition: Position.Left,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import Header from './header'
|
|||||||
import CustomNode from './nodes'
|
import CustomNode from './nodes'
|
||||||
import ZoomInOut from './zoom-in-out'
|
import ZoomInOut from './zoom-in-out'
|
||||||
import CustomEdge from './custom-edge'
|
import CustomEdge from './custom-edge'
|
||||||
|
import CustomConnectionLine from './custom-connection-line'
|
||||||
import Panel from './panel'
|
import Panel from './panel'
|
||||||
import type { Node } from './types'
|
import type { Node } from './types'
|
||||||
|
|
||||||
@ -69,6 +70,7 @@ const Workflow: FC<WorkflowProps> = memo(({
|
|||||||
onEdgeMouseEnter={handleEnterEdge}
|
onEdgeMouseEnter={handleEnterEdge}
|
||||||
onEdgeMouseLeave={handleLeaveEdge}
|
onEdgeMouseLeave={handleLeaveEdge}
|
||||||
multiSelectionKeyCode={null}
|
multiSelectionKeyCode={null}
|
||||||
|
connectionLineComponent={CustomConnectionLine}
|
||||||
>
|
>
|
||||||
<Background
|
<Background
|
||||||
gap={[14, 14]}
|
gap={[14, 14]}
|
||||||
|
|||||||
@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import type { NodeProps } from 'reactflow'
|
import type { NodeProps } from 'reactflow'
|
||||||
import {
|
import {
|
||||||
Handle,
|
Handle,
|
||||||
@ -14,8 +18,16 @@ export const NodeTargetHandle = ({
|
|||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
}: NodeProps) => {
|
}: NodeProps) => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
const store = useStoreApi()
|
const store = useStoreApi()
|
||||||
const incomers = getIncomers({ id } as Node, store.getState().getNodes(), store.getState().edges)
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -23,26 +35,31 @@ export const NodeTargetHandle = ({
|
|||||||
type='target'
|
type='target'
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
className={`
|
className={`
|
||||||
!top-[17px] !left-0 !w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none !translate-y-0 z-[1]
|
!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-0.5 after:top-1 after:bg-primary-500
|
after:absolute after:w-0.5 after:h-2 after:left-1.5 after:top-1 after:bg-primary-500
|
||||||
${(data.type === BlockEnum.Start || !incomers.length) && 'opacity-0'}
|
${!incomers.length && 'after:opacity-0'}
|
||||||
|
${data.type === BlockEnum.Start && 'opacity-0'}
|
||||||
`}
|
`}
|
||||||
isConnectable={data.type !== BlockEnum.Start}
|
isConnectable={data.type !== BlockEnum.Start}
|
||||||
/>
|
onClick={handleHandleClick}
|
||||||
{
|
>
|
||||||
incomers.length === 0 && data.type !== BlockEnum.Start && (
|
{
|
||||||
<BlockSelector
|
incomers.length === 0 && data.type !== BlockEnum.Start && (
|
||||||
onSelect={() => {}}
|
<BlockSelector
|
||||||
asChild
|
open={open}
|
||||||
placement='left'
|
onOpenChange={handleOpenChange}
|
||||||
triggerClassName={open => `
|
onSelect={() => {}}
|
||||||
hidden absolute -left-2 top-4
|
asChild
|
||||||
${data.hovering && '!flex'}
|
placement='left'
|
||||||
${open && '!flex'}
|
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,
|
handleClassName,
|
||||||
nodeSelectorClassName,
|
nodeSelectorClassName,
|
||||||
}: NodeSourceHandleProps) => {
|
}: NodeSourceHandleProps) => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
const store = useStoreApi()
|
const store = useStoreApi()
|
||||||
const connectedEdges = getConnectedEdges([{ id } as Node], store.getState().edges)
|
const connectedEdges = getConnectedEdges([{ id } as Node], store.getState().edges)
|
||||||
const connected = connectedEdges.find(edge => edge.sourceHandle === handleId)
|
const connected = connectedEdges.find(edge => edge.sourceHandle === handleId)
|
||||||
|
const handleOpenChange = useCallback((v: boolean) => {
|
||||||
|
setOpen(v)
|
||||||
|
}, [])
|
||||||
|
const handleHandleClick = () => {
|
||||||
|
if (!connected)
|
||||||
|
handleOpenChange(!open)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -71,25 +96,29 @@ export const NodeSourceHandle = ({
|
|||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
className={`
|
className={`
|
||||||
!w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none !translate-y-0 z-[1]
|
!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
|
after:absolute after:w-0.5 after:h-2 after:right-1.5 after:top-1 after:bg-primary-500
|
||||||
${!connected && 'opacity-0'}
|
${!connected && 'after:opacity-0'}
|
||||||
${handleClassName}
|
${handleClassName}
|
||||||
`}
|
`}
|
||||||
/>
|
onClick={handleHandleClick}
|
||||||
{
|
>
|
||||||
!connected && (
|
{
|
||||||
<BlockSelector
|
!connected && (
|
||||||
onSelect={() => {}}
|
<BlockSelector
|
||||||
asChild
|
open={open}
|
||||||
triggerClassName={open => `
|
onOpenChange={handleOpenChange}
|
||||||
hidden
|
onSelect={() => {}}
|
||||||
${nodeSelectorClassName}
|
asChild
|
||||||
${data.hovering && '!flex'}
|
triggerClassName={open => `
|
||||||
${open && '!flex'}
|
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
|
<NodeSourceHandle
|
||||||
{...props}
|
{...props}
|
||||||
handleId='condition1'
|
handleId='condition1'
|
||||||
handleClassName='!top-1 !-right-3'
|
handleClassName='!top-1 !-right-5'
|
||||||
nodeSelectorClassName='absolute top-1 -right-5'
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='mb-0.5 leading-4 text-[10px] font-medium text-gray-500 uppercase'>{t(`${i18nPrefix}.conditions`)}</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
|
<NodeSourceHandle
|
||||||
{...props}
|
{...props}
|
||||||
handleId='condition2'
|
handleId='condition2'
|
||||||
handleClassName='!top-1 !-right-3'
|
handleClassName='!top-1 !-right-5'
|
||||||
nodeSelectorClassName='absolute top-1 -right-5'
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -26,8 +26,7 @@ const CustomNode = memo((props: NodeProps) => {
|
|||||||
nodeData.type !== BlockEnum.IfElse && (
|
nodeData.type !== BlockEnum.IfElse && (
|
||||||
<NodeSourceHandle
|
<NodeSourceHandle
|
||||||
{ ...props }
|
{ ...props }
|
||||||
handleClassName='!top-[17px] !right-0'
|
handleClassName='!top-[17px] !-right-2'
|
||||||
nodeSelectorClassName='absolute -right-2 top-4'
|
|
||||||
handleId='source'
|
handleId='source'
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user