Commit f1b868d5 authored by StyleZhang's avatar StyleZhang

next step

parent 76ff004e
......@@ -8,14 +8,26 @@ const initialNodes = [
{
id: '1',
type: 'custom',
// position: { x: 130, y: 130 },
position: { x: 130, y: 130 },
data: { type: 'start' },
},
{
id: '2',
type: 'custom',
position: { x: 434, y: 130 },
data: { type: 'if-else' },
data: {
type: 'if-else',
branches: [
{
id: 'if-true',
name: 'IS TRUE',
},
{
id: 'if-false',
name: 'IS FALSE',
},
],
},
},
{
id: '3',
......@@ -50,7 +62,7 @@ const initialEdges = [
id: '1',
type: 'custom',
source: '2',
sourceHandle: 'condition1',
sourceHandle: 'if-true',
target: '3',
targetHandle: 'target',
},
......@@ -58,7 +70,7 @@ const initialEdges = [
id: '2',
type: 'custom',
source: '2',
sourceHandle: 'condition2',
sourceHandle: 'if-false',
target: '4',
targetHandle: 'target',
},
......
......@@ -92,3 +92,7 @@ export const NodeInitialData = {
desc: '',
},
}
export const NODE_WIDTH = 220
export const X_OFFSET = 64
export const Y_OFFSET = 39
......@@ -10,6 +10,7 @@ import {
} from 'reactflow'
import type {
BlockEnum,
Node,
SelectedNode,
} from './types'
import { NodeInitialData } from './constants'
......@@ -123,13 +124,14 @@ export const useWorkflow = () => {
setNodes(newNodes)
}
}, [setSelectedNode, store])
const handleUpdateNodeData = useCallback(({ id, data }: SelectedNode) => {
const {
getNodes,
setNodes,
} = store.getState()
const newNodes = produce(getNodes(), (draft) => {
const currentNode = draft.find(n => n.id === id)!
const currentNode = draft.find(node => node.id === id)!
currentNode.data = { ...currentNode.data, ...data }
})
......@@ -137,7 +139,7 @@ export const useWorkflow = () => {
setSelectedNode({ id, data })
}, [store, setSelectedNode])
const handleAddNextNode = useCallback((currentNodeId: string, nodeType: BlockEnum, branchId?: string) => {
const handleAddNextNode = useCallback((currentNodeId: string, nodeType: BlockEnum, sourceHandle: string) => {
const {
getNodes,
setNodes,
......@@ -146,35 +148,85 @@ export const useWorkflow = () => {
} = store.getState()
const nodes = getNodes()
const currentNode = nodes.find(node => node.id === currentNodeId)!
const nextNode = {
const nextNode: Node = {
id: `${Date.now()}`,
type: 'custom',
data: { ...NodeInitialData[nodeType], selected: true },
data: {
...NodeInitialData[nodeType],
selected: true,
},
position: {
x: currentNode.position.x + 304,
y: currentNode.position.y,
},
}
const newEdge = {
id: `${currentNode.id}-${nextNode.id}`,
type: 'custom',
source: currentNode.id,
sourceHandle,
target: nextNode.id,
targetHandle: 'target',
}
const newNodes = produce(nodes, (draft) => {
draft.forEach((item) => {
item.data = { ...item.data, selected: false }
draft.forEach((node) => {
node.data = { ...node.data, selected: false }
})
draft.push(nextNode)
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
draft.push({
id: `${currentNode.id}-${nextNode.id}`,
type: 'custom',
source: currentNode.id,
sourceHandle: branchId || 'source',
target: nextNode.id,
targetHandle: 'target',
})
draft.push(newEdge)
})
setEdges(newEdges)
setSelectedNode(nextNode)
}, [store, setSelectedNode])
const handleChangeCurrentNode = useCallback((parentNodeId: string, currentNodeId: string, nodeType: BlockEnum, sourceHandle: string) => {
const {
getNodes,
setNodes,
edges,
setEdges,
} = store.getState()
const nodes = getNodes()
const currentNode = nodes.find(node => node.id === currentNodeId)!
const connectedEdges = getConnectedEdges([currentNode], edges)
const newCurrentNode: Node = {
id: `${Date.now()}`,
type: 'custom',
data: {
...NodeInitialData[nodeType],
selected: true,
},
position: {
x: currentNode.position.x,
y: currentNode.position.y,
},
}
const newEdge = {
id: `${parentNodeId}-${newCurrentNode.id}`,
type: 'custom',
source: parentNodeId,
sourceHandle,
target: newCurrentNode.id,
targetHandle: 'target',
}
const newNodes = produce(nodes, (draft) => {
const index = draft.findIndex(node => node.id === currentNodeId)
draft.splice(index, 1, newCurrentNode)
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
const filtered = draft.filter(edge => !connectedEdges.find(connectedEdge => connectedEdge.id === edge.id))
filtered.push(newEdge)
return filtered
})
setEdges(newEdges)
}, [store])
const handleInitialLayoutNodes = useCallback(() => {
const {
getNodes,
......@@ -191,6 +243,43 @@ export const useWorkflow = () => {
}))
}, [store])
const handleUpdateNodesPosition = useCallback(() => {
const {
getNodes,
setNodes,
} = store.getState()
const nodes = getNodes()
const groups = nodes.reduce((acc, cur) => {
const x = cur.data.position.x
if (!acc[x])
acc[x] = [cur]
else
acc[x].push(cur)
return acc
}, {} as Record<string, Node[]>)
const heightMap: Record<string, number> = {}
Object.keys(groups).forEach((key) => {
let baseHeight = 0
groups[key].sort((a, b) => a.data.position!.y - b.data.position!.y).forEach((node) => {
heightMap[node.id] = baseHeight
baseHeight = node.height! + 39
})
})
setNodes(produce(nodes, (draft) => {
draft.forEach((node) => {
node.position = {
...node.position,
x: node.data.position.x * (220 + 64),
y: heightMap[node.id],
}
})
}))
}, [store])
return {
handleEnterNode,
handleLeaveNode,
......@@ -199,6 +288,8 @@ export const useWorkflow = () => {
handleSelectNode,
handleUpdateNodeData,
handleAddNextNode,
handleChangeCurrentNode,
handleInitialLayoutNodes,
handleUpdateNodesPosition,
}
}
......@@ -10,7 +10,7 @@ import ReactFlow, {
Background,
ReactFlowProvider,
useEdgesState,
useNodesInitialized,
// useNodesInitialized,
useNodesState,
} from 'reactflow'
import 'reactflow/dist/style.css'
......@@ -70,10 +70,10 @@ const Workflow: FC<WorkflowProps> = memo(({
needUpdatePosition: true,
}
}, [initialNodes, initialEdges])
const nodesInitialized = useNodesInitialized({
includeHiddenNodes: true,
})
const [nodes] = useNodesState(initialData.nodes)
// const nodesInitialized = useNodesInitialized({
// includeHiddenNodes: true,
// })
const [nodes, setNodes, onNodesChange] = useNodesState(initialData.nodes)
const [edges, setEdges, onEdgesChange] = useEdgesState(initialData.edges)
const {
......@@ -85,11 +85,10 @@ const Workflow: FC<WorkflowProps> = memo(({
handleInitialLayoutNodes,
} = useWorkflow()
useEffect(() => {
console.log(nodesInitialized, '2')
if (nodesInitialized && initialData.needUpdatePosition)
handleInitialLayoutNodes()
}, [nodesInitialized])
// useEffect(() => {
// if (nodesInitialized)
// handleInitialLayoutNodes()
// }, [nodesInitialized])
useEffect(() => {
if (initialSelectedNodeId) {
......
import {
memo,
useCallback,
} from 'react'
import {
getOutgoers,
useStoreApi,
} from 'reactflow'
import BlockIcon from '../../../block-icon'
import type { Node } from '../../../types'
import { useStore } from '../../../store'
import BlockSelector from '../../../block-selector'
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
import Button from '@/app/components/base/button'
const NextStep = () => {
const store = useStoreApi()
const selectedNode = useStore(state => state.selectedNode)
const outgoers: Node[] = getOutgoers(selectedNode as Node, store.getState().getNodes(), store.getState().edges)
const svgHeight = outgoers.length > 1 ? (outgoers.length + 1) * 36 + 12 * outgoers.length : 36
const renderAddNextNodeTrigger = useCallback((open: boolean) => {
return (
<div
className={`
relative flex items-center px-2 w-[328px] h-9 rounded-lg border border-dashed border-gray-200 bg-gray-50
hover:bg-gray-100 text-xs text-gray-500 cursor-pointer
${open && '!bg-gray-100'}
`}
>
<div className='flex items-center justify-center mr-1.5 w-5 h-5 rounded-[5px] bg-gray-200'>
<Plus className='w-3 h-3' />
</div>
SELECT NEXT BLOCK
</div>
)
}, [])
const renderChangeCurrentNodeTrigger = useCallback((open: boolean) => {
return (
<Button
className={`
hidden group-hover:flex px-2 py-0 h-6 bg-white text-xs text-gray-700 font-medium rounded-md
${open && '!bg-gray-100 !flex'}
`}
>
Change
</Button>
)
}, [])
return (
<div className='flex py-1'>
<div className='shrink-0 relative flex items-center justify-center w-9 h-9 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-xs'>
<BlockIcon type={selectedNode!.data.type} />
</div>
<svg className='shrink-0 w-6' style={{ height: svgHeight }}>
{
outgoers.length < 2 && (
<g>
<path
d='M0,18 L24,18'
strokeWidth={1}
stroke='#D0D5DD'
fill='none'
/>
<rect
x={0}
y={16}
width={1}
height={4}
fill='#98A2B3'
/>
<rect
x={23}
y={16}
width={1}
height={4}
fill='#98A2B3'
/>
</g>
)
}
{
outgoers.length > 1 && (
<g>
{
Array(outgoers.length + 1).fill(0).map((_, index) => (
<g key={index}>
{
index === 0 && (
<path
d='M0,18 L24,18'
strokeWidth={1}
stroke='#D0D5DD'
fill='none'
/>
)
}
{
index > 0 && (
<path
d={`M0,18 Q12,18 12,28 L12,${index * 48 + 18 - 10} Q12,${index * 48 + 18} 24,${index * 48 + 18}`}
strokeWidth={1}
stroke='#D0D5DD'
fill='none'
/>
)
}
<rect
x={23}
y={index * 48 + 18 - 2}
width={1}
height={4}
fill='#98A2B3'
/>
</g>
))
}
<rect
x={0}
y={16}
width={1}
height={4}
fill='#98A2B3'
/>
</g>
)
}
</svg>
<div className='grow'>
{
!!outgoers.length && outgoers.map(outgoer => (
<div
key={outgoer.id}
className='relative group flex items-center mb-3 last-of-type:mb-0 px-2 h-9 rounded-lg border-[0.5px] border-gray-200 bg-white hover:bg-gray-50 shadow-xs text-xs text-gray-700 cursor-pointer'
>
<div className='absolute left-1 -top-[7.5px] flex items-center px-0.5 h-3 bg-white text-[10px] text-gray-500 font-semibold rounded-[5px]'>
IS TRUE
</div>
<BlockIcon
type={outgoer.data.type}
className='shrink-0 mr-1.5'
/>
<div className='grow'>{outgoer.data.title}</div>
<BlockSelector
onSelect={() => {}}
placement='top-end'
offset={{
mainAxis: 6,
crossAxis: 8,
}}
trigger={renderChangeCurrentNodeTrigger}
popupClassName='!w-[328px]'
/>
</div>
))
}
{
(!outgoers.length || outgoers.length > 1) && (
<BlockSelector
onSelect={() => {}}
placement='top'
offset={0}
trigger={renderAddNextNodeTrigger}
popupClassName='!w-[328px]'
/>
)
}
</div>
</div>
)
}
export default memo(NextStep)
import {
memo,
useCallback,
} from 'react'
import BlockSelector from '../../../../block-selector'
import { useWorkflow } from '../../../../hooks'
import type { BlockEnum } from '../../../../types'
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
type AddProps = {
nodeId: string
sourceHandle: string
branchName?: string
}
const Add = ({
nodeId,
sourceHandle,
branchName,
}: AddProps) => {
const { handleAddNextNode } = useWorkflow()
const handleSelect = useCallback((type: BlockEnum) => {
handleAddNextNode(nodeId, type, sourceHandle)
}, [nodeId, sourceHandle, handleAddNextNode])
const renderTrigger = useCallback((open: boolean) => {
return (
<div
className={`
relative flex items-center px-2 w-[328px] h-9 rounded-lg border border-dashed border-gray-200 bg-gray-50
hover:bg-gray-100 text-xs text-gray-500 cursor-pointer
${open && '!bg-gray-100'}
`}
>
{
branchName && (
<div className='absolute left-1 -top-[7.5px] flex items-center px-0.5 h-3 bg-white text-[10px] text-gray-500 font-semibold rounded-[5px]'>
{branchName.toLocaleUpperCase()}
</div>
)
}
<div className='flex items-center justify-center mr-1.5 w-5 h-5 rounded-[5px] bg-gray-200'>
<Plus className='w-3 h-3' />
</div>
SELECT NEXT BLOCK
</div>
)
}, [branchName])
return (
<BlockSelector
onSelect={handleSelect}
placement='top'
offset={0}
trigger={renderTrigger}
popupClassName='!w-[328px]'
/>
)
}
export default memo(Add)
import { memo } from 'react'
import {
getConnectedEdges,
getOutgoers,
useEdges,
useStoreApi,
} from 'reactflow'
import BlockIcon from '../../../../block-icon'
import type { Node } from '../../../../types'
import { useStore } from '../../../../store'
import Add from './add'
import Item from './item'
import Line from './line'
const NextStep = () => {
const store = useStoreApi()
const selectedNode = useStore(state => state.selectedNode)
const branches = selectedNode?.data.branches
const edges = useEdges()
const outgoers = getOutgoers(selectedNode as Node, store.getState().getNodes(), edges)
const connectedEdges = getConnectedEdges([selectedNode] as Node[], edges).filter(edge => edge.source === selectedNode!.id)
return (
<div className='flex py-1'>
<div className='shrink-0 relative flex items-center justify-center w-9 h-9 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-xs'>
<BlockIcon type={selectedNode!.data.type} />
</div>
<Line linesNumber={branches ? branches.length : 1} />
<div className='grow'>
{
!branches && !!outgoers.length && (
<Item
parentNodeId={selectedNode!.id}
nodeId={outgoers[0].id}
sourceHandle='source'
data={outgoers[0].data}
/>
)
}
{
!branches && !outgoers.length && (
<Add
nodeId={selectedNode!.id}
sourceHandle='source'
/>
)
}
{
branches?.length && (
branches.map((branch) => {
const connected = connectedEdges.find(edge => edge.sourceHandle === branch.id)
const target = outgoers.find(outgoer => outgoer.id === connected?.target)
return (
<div
key={branch.id}
className='mb-3 last-of-type:mb-0'
>
{
connected && (
<Item
data={target!.data!}
parentNodeId={selectedNode!.id}
nodeId={target!.id}
sourceHandle={branch.id}
branchName={branch.name}
/>
)
}
{
!connected && (
<Add
key={branch.id}
nodeId={selectedNode!.id}
sourceHandle={branch.id}
branchName={branch.name}
/>
)
}
</div>
)
})
)
}
</div>
</div>
)
}
export default memo(NextStep)
import {
memo,
useCallback,
} from 'react'
import type {
BlockEnum,
CommonNodeType,
} from '../../../../types'
import BlockIcon from '../../../../block-icon'
import BlockSelector from '../../../../block-selector'
import { useWorkflow } from '../../../../hooks'
import Button from '@/app/components/base/button'
type ItemProps = {
parentNodeId: string
nodeId: string
sourceHandle: string
branchName?: string
data: CommonNodeType
}
const Item = ({
parentNodeId,
nodeId,
sourceHandle,
branchName,
data,
}: ItemProps) => {
const { handleChangeCurrentNode } = useWorkflow()
const handleSelect = useCallback((type: BlockEnum) => {
handleChangeCurrentNode(parentNodeId, nodeId, type, sourceHandle)
}, [parentNodeId, nodeId, sourceHandle, handleChangeCurrentNode])
const renderTrigger = useCallback((open: boolean) => {
return (
<Button
className={`
hidden group-hover:flex px-2 py-0 h-6 bg-white text-xs text-gray-700 font-medium rounded-md
${open && '!bg-gray-100 !flex'}
`}
>
Change
</Button>
)
}, [])
return (
<div
className='relative group flex items-center mb-3 last-of-type:mb-0 px-2 h-9 rounded-lg border-[0.5px] border-gray-200 bg-white hover:bg-gray-50 shadow-xs text-xs text-gray-700 cursor-pointer'
>
{
branchName && (
<div className='absolute left-1 -top-[7.5px] flex items-center px-0.5 h-3 bg-white text-[10px] text-gray-500 font-semibold rounded-[5px]'>
{branchName.toLocaleUpperCase()}
</div>
)
}
<BlockIcon
type={data.type}
className='shrink-0 mr-1.5'
/>
<div className='grow'>{data.title}</div>
<BlockSelector
onSelect={handleSelect}
placement='top-end'
offset={{
mainAxis: 6,
crossAxis: 8,
}}
trigger={renderTrigger}
popupClassName='!w-[328px]'
/>
</div>
)
}
export default memo(Item)
import { memo } from 'react'
type LineProps = {
linesNumber: number
}
const Line = ({
linesNumber,
}: LineProps) => {
const svgHeight = linesNumber * 36 + (linesNumber - 1) * 12
return (
<svg className='shrink-0 w-6' style={{ height: svgHeight }}>
{
Array(linesNumber).fill(0).map((_, index) => (
<g key={index}>
{
index === 0 && (
<>
<rect
x={0}
y={16}
width={1}
height={4}
fill='#98A2B3'
/>
<path
d='M0,18 L24,18'
strokeWidth={1}
stroke='#D0D5DD'
fill='none'
/>
</>
)
}
{
index > 0 && (
<path
d={`M0,18 Q12,18 12,28 L12,${index * 48 + 18 - 10} Q12,${index * 48 + 18} 24,${index * 48 + 18}`}
strokeWidth={1}
stroke='#D0D5DD'
fill='none'
/>
)
}
<rect
x={23}
y={index * 48 + 18 - 2}
width={1}
height={4}
fill='#98A2B3'
/>
</g>
))
}
</svg>
)
}
export default memo(Line)
......@@ -7,7 +7,7 @@ import {
Handle,
Position,
getConnectedEdges,
useStoreApi,
useEdges,
} from 'reactflow'
import { BlockEnum } from '../../../types'
import type { Node } from '../../../types'
......@@ -15,7 +15,7 @@ import BlockSelector from '../../../block-selector'
import { useWorkflow } from '../../../hooks'
type NodeHandleProps = {
handleId?: string
handleId: string
handleClassName?: string
nodeSelectorClassName?: string
} & Pick<NodeProps, 'id' | 'data'>
......@@ -28,9 +28,10 @@ export const NodeTargetHandle = ({
nodeSelectorClassName,
}: NodeHandleProps) => {
const [open, setOpen] = useState(false)
const store = useStoreApi()
const connectedEdges = getConnectedEdges([{ id } as Node], store.getState().edges)
const edges = useEdges()
const connectedEdges = getConnectedEdges([{ id } as Node], edges)
const connected = connectedEdges.find(edge => edge.targetHandle === handleId && edge.target === id)
const handleOpenChange = useCallback((v: boolean) => {
setOpen(v)
}, [])
......@@ -86,8 +87,8 @@ export const NodeSourceHandle = ({
}: NodeHandleProps) => {
const [open, setOpen] = useState(false)
const { handleAddNextNode } = useWorkflow()
const store = useStoreApi()
const connectedEdges = getConnectedEdges([{ id } as Node], store.getState().edges)
const edges = useEdges()
const connectedEdges = getConnectedEdges([{ id } as Node], edges)
const connected = connectedEdges.find(edge => edge.sourceHandle === handleId && edge.source === id)
const handleOpenChange = useCallback((v: boolean) => {
setOpen(v)
......@@ -97,8 +98,8 @@ export const NodeSourceHandle = ({
handleOpenChange(!open)
}
const handleSelect = useCallback((type: BlockEnum) => {
handleAddNextNode(id, type)
}, [handleAddNextNode, id])
handleAddNextNode(id, type, handleId)
}, [handleAddNextNode, id, handleId])
return (
<>
......
......@@ -17,7 +17,7 @@ const IfElseNode: FC<Pick<NodeProps, 'id' | 'data'>> = (props) => {
<div className='w-full text-right text-gray-700 text-xs font-semibold'>IF</div>
<NodeSourceHandle
{...props}
handleId='condition1'
handleId='if-true'
handleClassName='!top-1 !-right-[21px]'
/>
</div>
......@@ -41,7 +41,7 @@ const IfElseNode: FC<Pick<NodeProps, 'id' | 'data'>> = (props) => {
<div className='w-full text-right text-gray-700 text-xs font-semibold'>ELSE</div>
<NodeSourceHandle
{...props}
handleId='condition2'
handleId='if-false'
handleClassName='!top-1 !-right-[21px]'
/>
</div>
......
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