Commit d2d6904c authored by StyleZhang's avatar StyleZhang

panel-operator

parent 510f0593
......@@ -57,6 +57,7 @@ const initialEdges = [
sourceHandle: 'source',
target: '2',
targetHandle: 'target',
deletable: false,
},
{
id: '1',
......
......@@ -14,12 +14,10 @@ import type {
SelectedNode,
} from './types'
import { NodeInitialData } from './constants'
import { useStore } from './store'
import { initialNodesPosition } from './utils'
export const useWorkflow = () => {
const store = useStoreApi()
const setSelectedNode = useStore(state => state.setSelectedNode)
const handleEnterNode = useCallback<NodeMouseHandler>((_, node) => {
const {
......@@ -71,6 +69,25 @@ export const useWorkflow = () => {
setEdges(newEdges)
}, [store])
const handleSelectNode = useCallback((nodeId: string, cancelSelection?: boolean) => {
const {
getNodes,
setNodes,
} = store.getState()
const newNodes = produce(getNodes(), (draft) => {
const selectedNode = draft.find(node => node.id === nodeId)
if (selectedNode) {
if (cancelSelection)
selectedNode.selected = false
else
selectedNode.selected = true
}
})
setNodes(newNodes)
}, [store])
const handleEnterEdge = useCallback<EdgeMouseHandler>((_, edge) => {
const {
edges,
......@@ -97,33 +114,19 @@ export const useWorkflow = () => {
setEdges(newEdges)
}, [store])
const handleSelectNode = useCallback((selectNode: SelectedNode, cancelSelection?: boolean) => {
const handleDeleteEdge = useCallback(() => {
const {
getNodes,
setNodes,
edges,
setEdges,
} = store.getState()
if (cancelSelection) {
setSelectedNode(null)
const newNodes = produce(getNodes(), (draft) => {
draft.forEach((item) => {
item.data.selected = false
})
})
setNodes(newNodes)
}
else {
setSelectedNode(selectNode)
const newNodes = produce(getNodes(), (draft) => {
draft.forEach((item) => {
if (item.id === selectNode.id)
item.data.selected = true
else
item.data.selected = false
})
})
setNodes(newNodes)
}
}, [setSelectedNode, store])
const newEdges = produce(edges, (draft) => {
const index = draft.findIndex(edge => edge.selected)
if (index > -1)
draft.splice(index, 1)
})
setEdges(newEdges)
}, [store])
const handleUpdateNodeData = useCallback(({ id, data }: SelectedNode) => {
const {
......@@ -136,8 +139,7 @@ export const useWorkflow = () => {
currentNode.data = { ...currentNode.data, ...data }
})
setNodes(newNodes)
setSelectedNode({ id, data })
}, [store, setSelectedNode])
}, [store])
const handleAddNextNode = useCallback((currentNodeId: string, nodeType: BlockEnum, sourceHandle: string) => {
const {
......@@ -151,10 +153,7 @@ export const useWorkflow = () => {
const nextNode: Node = {
id: `${Date.now()}`,
type: 'custom',
data: {
...NodeInitialData[nodeType],
selected: true,
},
data: NodeInitialData[nodeType],
position: {
x: currentNode.position.x + 304,
y: currentNode.position.y,
......@@ -179,8 +178,7 @@ export const useWorkflow = () => {
draft.push(newEdge)
})
setEdges(newEdges)
setSelectedNode(nextNode)
}, [store, setSelectedNode])
}, [store])
const handleChangeCurrentNode = useCallback((parentNodeId: string, currentNodeId: string, nodeType: BlockEnum, sourceHandle: string) => {
const {
......@@ -195,10 +193,7 @@ export const useWorkflow = () => {
const newCurrentNode: Node = {
id: `${Date.now()}`,
type: 'custom',
data: {
...NodeInitialData[nodeType],
selected: true,
},
data: NodeInitialData[nodeType],
position: {
x: currentNode.position.x,
y: currentNode.position.y,
......@@ -227,6 +222,28 @@ export const useWorkflow = () => {
setEdges(newEdges)
}, [store])
const handleDeleteNode = useCallback((nodeId: string) => {
const {
getNodes,
setNodes,
edges,
setEdges,
} = store.getState()
const newNodes = produce(getNodes(), (draft) => {
const index = draft.findIndex(node => node.id === nodeId)
if (index > -1)
draft.splice(index, 1)
})
setNodes(newNodes)
const connectedEdges = getConnectedEdges([{ id: nodeId } as Node], edges)
const newEdges = produce(edges, (draft) => {
return draft.filter(edge => !connectedEdges.find(connectedEdge => connectedEdge.id === edge.id))
})
setEdges(newEdges)
}, [store])
const handleInitialLayoutNodes = useCallback(() => {
const {
getNodes,
......@@ -283,12 +300,14 @@ export const useWorkflow = () => {
return {
handleEnterNode,
handleLeaveNode,
handleSelectNode,
handleEnterEdge,
handleLeaveEdge,
handleSelectNode,
handleDeleteEdge,
handleUpdateNodeData,
handleAddNextNode,
handleChangeCurrentNode,
handleDeleteNode,
handleInitialLayoutNodes,
handleUpdateNodesPosition,
}
......
import type { FC } from 'react'
import {
memo,
useEffect,
useMemo,
} from 'react'
import produce from 'immer'
import type { Edge } from 'reactflow'
import { memo } from 'react'
import { useKeyPress } from 'ahooks'
import ReactFlow, {
Background,
ReactFlowProvider,
useEdgesState,
// useNodesInitialized,
useNodesState,
} from 'reactflow'
import 'reactflow/dist/style.css'
import type {
Edge,
Node,
} from './types'
import { useWorkflow } from './hooks'
import Header from './header'
import CustomNode from './nodes'
......@@ -21,7 +19,6 @@ import ZoomInOut from './zoom-in-out'
import CustomEdge from './custom-edge'
import CustomConnectionLine from './custom-connection-line'
import Panel from './panel'
import { BlockEnum, type Node } from './types'
const nodeTypes = {
custom: CustomNode,
......@@ -31,73 +28,25 @@ const edgeTypes = {
}
type WorkflowProps = {
selectedNodeId?: string
nodes: Node[]
edges: Edge[]
}
const Workflow: FC<WorkflowProps> = memo(({
nodes: initialNodes,
edges: initialEdges,
selectedNodeId: initialSelectedNodeId,
}) => {
const initialData: {
nodes: Node[]
edges: Edge[]
needUpdatePosition: boolean
} = useMemo(() => {
const start = initialNodes.find(node => node.data.type === BlockEnum.Start)
if (start?.position) {
return {
nodes: initialNodes,
edges: initialEdges,
needUpdatePosition: false,
}
}
return {
nodes: produce(initialNodes, (draft) => {
draft.forEach((node) => {
node.position = { x: 0, y: 0 }
node.data = { ...node.data, hidden: true }
})
}),
edges: produce(initialEdges, (draft) => {
draft.forEach((edge) => {
edge.hidden = true
})
}),
needUpdatePosition: true,
}
}, [initialNodes, initialEdges])
// const nodesInitialized = useNodesInitialized({
// includeHiddenNodes: true,
// })
const [nodes, setNodes, onNodesChange] = useNodesState(initialData.nodes)
const [edges, setEdges, onEdgesChange] = useEdgesState(initialData.edges)
const [nodes] = useNodesState(initialNodes)
const [edges, _, onEdgesChange] = useEdgesState(initialEdges)
const {
handleEnterNode,
handleLeaveNode,
handleEnterEdge,
handleLeaveEdge,
handleSelectNode,
handleInitialLayoutNodes,
handleDeleteEdge,
} = useWorkflow()
// useEffect(() => {
// if (nodesInitialized)
// handleInitialLayoutNodes()
// }, [nodesInitialized])
useEffect(() => {
if (initialSelectedNodeId) {
const initialSelectedNode = nodes.find(n => n.id === initialSelectedNodeId)
if (initialSelectedNode)
handleSelectNode({ id: initialSelectedNodeId, data: initialSelectedNode.data })
}
}, [initialSelectedNodeId])
useKeyPress('Backspace', handleDeleteEdge)
return (
<div className='relative w-full h-full'>
......@@ -111,11 +60,12 @@ const Workflow: FC<WorkflowProps> = memo(({
edges={edges}
onNodeMouseEnter={handleEnterNode}
onNodeMouseLeave={handleLeaveNode}
onEdgesChange={onEdgesChange}
onEdgeMouseEnter={handleEnterEdge}
onEdgeMouseLeave={handleLeaveEdge}
onEdgesChange={onEdgesChange}
multiSelectionKeyCode={null}
connectionLineComponent={CustomConnectionLine}
deleteKeyCode={null}
>
<Background
gap={[14, 14]}
......@@ -129,15 +79,12 @@ const Workflow: FC<WorkflowProps> = memo(({
Workflow.displayName = 'Workflow'
const WorkflowWrap: FC<WorkflowProps> = ({
selectedNodeId,
nodes,
edges,
}) => {
return (
<ReactFlowProvider>
{selectedNodeId}
<Workflow
selectedNodeId={selectedNodeId}
nodes={nodes}
edges={edges}
/>
......
......@@ -7,14 +7,17 @@ import {
} 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 = () => {
type NextStepProps = {
selectedNode: Node
}
const NextStep = ({
selectedNode,
}: NextStepProps) => {
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)
......
import {
memo,
useState,
} from 'react'
import { useWorkflow } from '../../../hooks'
import { DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
type PanelOperatorProps = {
nodeId: string
}
const PanelOperator = ({
nodeId,
}: PanelOperatorProps) => {
const { handleDeleteNode } = useWorkflow()
const [open, setOpen] = useState(false)
return (
<PortalToFollowElem
placement='bottom-end'
offset={{
mainAxis: 4,
crossAxis: 53,
}}
open={open}
onOpenChange={setOpen}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<div
className={`
flex items-center justify-center w-6 h-6 rounded-md cursor-pointer
hover:bg-black/5
${open && 'bg-black/5'}
`}
>
<DotsHorizontal className='w-4 h-4 text-gray-700' />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>
<div className='w-[240px] border-[0.5px] border-gray-200 rounded-2xl shadow-xl bg-white'>
<div className='p-1'>
<div className='flex items-center px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'>Change Block</div>
<div className='flex items-center px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'>Help Link</div>
</div>
<div className='h-[1px] bg-gray-100'></div>
<div className='p-1'>
<div
className='flex items-center px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
onClick={() => handleDeleteNode(nodeId)}
>
Delete
</div>
</div>
<div className='h-[1px] bg-gray-100'></div>
<div className='p-1'>
<div className='px-3 py-2 text-xs text-gray-500'>
<div className='flex items-center mb-1 h-[22px] font-medium'>
ABOUT
</div>
<div className='text-gray-500 leading-[18px]'>A tool for performing a Google SERP search and extracting snippets and webpages.Input should be a search query.</div>
<div className='my-2 h-[0.5px] bg-black/5'></div>
<div className='leading-[18px]'>
Created By Dify
</div>
</div>
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(PanelOperator)
......@@ -16,8 +16,9 @@ type BaseNodeProps = {
} & NodeProps
const BaseNode: FC<BaseNodeProps> = ({
id: nodeId,
id,
data,
selected,
children,
}) => {
const { handleSelectNode } = useWorkflow()
......@@ -27,10 +28,9 @@ const BaseNode: FC<BaseNodeProps> = ({
className={`
group relative w-[240px] bg-[#fcfdff] rounded-2xl shadow-xs
hover:shadow-lg
${data.hidden && 'opacity-0'}
${data.selected ? 'border-[2px] border-primary-600' : 'border border-white'}
${selected ? 'border-[2px] border-primary-600' : 'border border-white'}
`}
onClick={() => handleSelectNode({ id: nodeId, data })}
onClick={() => handleSelectNode(id)}
>
<NodeControl />
<div className='flex items-center px-3 pt-3 pb-2'>
......@@ -49,7 +49,7 @@ const BaseNode: FC<BaseNodeProps> = ({
{
children && (
<div className='mb-1'>
{cloneElement(children, { id: nodeId, data })}
{cloneElement(children, { id, data })}
</div>
)
}
......
......@@ -7,23 +7,23 @@ import {
memo,
useCallback,
} from 'react'
import type { SelectedNode } from '../../types'
import type { Node } from '../../types'
import BlockIcon from '../../block-icon'
import { useWorkflow } from '../../hooks'
import NextStep from './components/next-step'
import PanelOperator from './components/panel-operator'
import {
DescriptionInput,
TitleInput,
} from './components/title-description-input'
import {
DotsHorizontal,
XClose,
} from '@/app/components/base/icons/src/vender/line/general'
import { GitBranch01 } from '@/app/components/base/icons/src/vender/line/development'
type BasePanelProps = {
children: ReactElement
} & SelectedNode
} & Node
const BasePanel: FC<BasePanelProps> = ({
id,
......@@ -42,7 +42,7 @@ const BasePanel: FC<BasePanelProps> = ({
}, [handleUpdateNodeData, id, data])
return (
<div className='mr-2 w-[420px] h-full bg-white shadow-lg border-[0.5px] border-gray-200 rounded-2xl z-10 overflow-y-auto'>
<div className='mr-2 w-[420px] h-full bg-white shadow-lg border-[0.5px] border-gray-200 rounded-2xl overflow-y-auto'>
<div className='sticky top-0 bg-white border-b-[0.5px] border-black/5'>
<div className='flex items-center px-4 pt-4 pb-1'>
<BlockIcon
......@@ -55,13 +55,11 @@ const BasePanel: FC<BasePanelProps> = ({
onChange={handleTitleChange}
/>
<div className='shrink-0 flex items-center text-gray-500'>
<div className='flex items-center justify-center w-6 h-6 cursor-pointer'>
<DotsHorizontal className='w-4 h-4' />
</div>
<PanelOperator nodeId={id} />
<div className='mx-3 w-[1px] h-3.5 bg-gray-200' />
<div
className='flex items-center justify-center w-6 h-6 cursor-pointer'
onClick={() => handleSelectNode({ id, data }, true)}
onClick={() => handleSelectNode(id, true)}
>
<XClose className='w-4 h-4' />
</div>
......@@ -85,7 +83,7 @@ const BasePanel: FC<BasePanelProps> = ({
<div className='mb-2 text-xs text-gray-400'>
Add the next block in this workflow
</div>
<NextStep />
<NextStep selectedNode={{ id, data } as Node} />
</div>
</div>
)
......
import { memo } from 'react'
import type { NodeProps } from 'reactflow'
import { BlockEnum, type SelectedNode } from '../types'
import type { Node } from '../types'
import { BlockEnum } from '../types'
import {
NodeComponentMap,
PanelComponentMap,
......@@ -44,7 +45,7 @@ const CustomNode = memo((props: NodeProps) => {
})
CustomNode.displayName = 'CustomNode'
export const Panel = memo((props: SelectedNode) => {
export const Panel = memo((props: Node) => {
const nodeData = props.data
const PanelComponent = PanelComponentMap[nodeData.type]
......
......@@ -4,7 +4,7 @@ import ChatWrapper from './chat-wrapper'
const DebugAndPreview: FC = () => {
return (
<div
className='flex flex-col w-[400px] h-full rounded-l-2xl border border-black/[0.02] shadow-xl z-10'
className='flex flex-col w-[400px] h-full rounded-l-2xl border border-black/[0.02] shadow-xl'
style={{ background: 'linear-gradient(156deg, rgba(242, 244, 247, 0.80) 0%, rgba(242, 244, 247, 0.00) 99.43%), var(--white, #FFF)' }}
>
<div className='shrink-0 flex items-center justify-between px-4 pt-3 pb-2'>
......
......@@ -3,6 +3,8 @@ import {
memo,
useMemo,
} from 'react'
import { useNodes } from 'reactflow'
import type { CommonNodeType } from '../types'
import { Panel as NodePanel } from '../nodes'
import { useStore } from '../store'
import WorkflowInfo from './workflow-info'
......@@ -11,7 +13,8 @@ import RunHistory from './run-history'
const Panel: FC = () => {
const mode = useStore(state => state.mode)
const selectedNode = useStore(state => state.selectedNode)
const nodes = useNodes<CommonNodeType>()
const selectedNode = nodes.find(node => node.selected)
const showRunHistory = useStore(state => state.showRunHistory)
const {
showWorkflowInfoPanel,
......@@ -26,7 +29,7 @@ const Panel: FC = () => {
}, [mode, selectedNode])
return (
<div className='absolute top-14 right-0 bottom-2 flex'>
<div className='absolute top-14 right-0 bottom-2 flex z-10'>
{
showNodePanel && (
<NodePanel {...selectedNode!} />
......
......@@ -10,7 +10,7 @@ const RunHistory = () => {
const setShowRunHistory = useStore(state => state.setShowRunHistory)
return (
<div className='w-[200px] h-full bg-white border-[0.5px] border-gray-200 shadow-xl rounded-l-2xl z-10'>
<div className='w-[200px] h-full bg-white border-[0.5px] border-gray-200 shadow-xl rounded-l-2xl'>
<div className='flex items-center justify-between px-4 pt-3 text-base font-semibold text-gray-900'>
Run History
<div
......
......@@ -7,7 +7,7 @@ import { FileCheck02 } from '@/app/components/base/icons/src/vender/line/files'
const WorkflowInfo: FC = () => {
return (
<div className='mr-2 w-[420px] h-full bg-white shadow-lg border-[0.5px] border-gray-200 rounded-2xl z-10 overflow-y-auto'>
<div className='mr-2 w-[420px] h-full bg-white shadow-lg border-[0.5px] border-gray-200 rounded-2xl overflow-y-auto'>
<div className='sticky top-0 bg-white border-b-[0.5px] border-black/5'>
<div className='flex pt-4 px-4 pb-1'>
<div className='mr-3 w-10 h-10'></div>
......
import { create } from 'zustand'
import type { SelectedNode } from './types'
type State = {
mode: string
selectedNode: SelectedNode | null
showRunHistory: boolean
}
type Action = {
setSelectedNode: (node: SelectedNode | null) => void
setShowRunHistory: (showRunHistory: boolean) => void
}
export const useStore = create<State & Action>(set => ({
mode: 'workflow',
selectedNode: null,
setSelectedNode: node => set(() => ({ selectedNode: node })),
showRunHistory: false,
setShowRunHistory: showRunHistory => set(() => ({ showRunHistory })),
}))
......@@ -24,13 +24,11 @@ export type Branch = {
}
export type CommonNodeType = {
hidden?: boolean
position?: {
x: number
y: number
}
sortIndexInBranches?: number
selected?: boolean
hovering?: boolean
branches?: Branch[]
title: string
......
......@@ -12,7 +12,6 @@ export const initialNodesPosition = (oldNodes: Node[], edges: Edge[]) => {
const nodes = cloneDeep(oldNodes)
const start = nodes.find(node => node.data.type === BlockEnum.Start)!
start.data.hidden = false
start.position.x = 0
start.position.y = 0
start.data.position = {
......@@ -38,7 +37,6 @@ export const initialNodesPosition = (oldNodes: Node[], edges: Edge[]) => {
if (outgoers.length) {
queue.push(...outgoers.map((outgoer) => {
outgoer.data.hidden = false
outgoer.data.position = {
x: depth + 1,
y: breadth,
......
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