Commit 58d8b0dd authored by StyleZhang's avatar StyleZhang

node handle connection line

parent 49f78bac
...@@ -24,6 +24,8 @@ import { ...@@ -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 = { ...@@ -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> = ({ ...@@ -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> = ({ ...@@ -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}
......
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 = ({ ...@@ -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' ...@@ -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(({ ...@@ -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]}
......
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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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'>) => { ...@@ -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'>) => { ...@@ -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) => { ...@@ -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'
/> />
) )
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment