From f6c07c996b670544509baa4744112e3732e88ce1 Mon Sep 17 00:00:00 2001 From: StyleZhang Date: Fri, 23 Feb 2024 14:15:43 +0800 Subject: [PATCH] workflow store --- web/app/components/workflow/context.tsx | 26 ---- web/app/components/workflow/hooks.ts | 124 ++++++++---------- web/app/components/workflow/index.tsx | 112 +++++----------- .../nodes/_base/components/next-step.tsx | 27 +--- .../components/workflow/nodes/_base/node.tsx | 18 +-- .../components/workflow/nodes/_base/panel.tsx | 23 +--- web/app/components/workflow/nodes/index.tsx | 23 ++-- web/app/components/workflow/panel/index.tsx | 16 +-- web/app/components/workflow/store.ts | 16 +-- web/app/components/workflow/style.css | 3 - web/app/components/workflow/types.ts | 14 +- 11 files changed, 127 insertions(+), 275 deletions(-) delete mode 100644 web/app/components/workflow/context.tsx delete mode 100644 web/app/components/workflow/style.css diff --git a/web/app/components/workflow/context.tsx b/web/app/components/workflow/context.tsx deleted file mode 100644 index 56e68da31d..0000000000 --- a/web/app/components/workflow/context.tsx +++ /dev/null @@ -1,26 +0,0 @@ -'use client' - -import { createContext, useContext } from 'use-context-selector' -import type { Edge } from 'reactflow' -import type { - BlockEnum, - Node, -} from './types' - -export type WorkflowContextValue = { - mode: string - nodes: Node[] - edges: Edge[] - selectedNode?: Node - handleAddNextNode: (prevNode: Node, nextNodeType: BlockEnum) => void - handleUpdateNodeData: (nodeId: string, data: Node['data']) => void -} - -export const WorkflowContext = createContext({ - mode: 'workflow', - nodes: [], - edges: [], - handleAddNextNode: () => {}, - handleUpdateNodeData: () => {}, -}) -export const useWorkflowContext = () => useContext(WorkflowContext) diff --git a/web/app/components/workflow/hooks.ts b/web/app/components/workflow/hooks.ts index e799a76bc7..5aa0e06a93 100644 --- a/web/app/components/workflow/hooks.ts +++ b/web/app/components/workflow/hooks.ts @@ -1,84 +1,68 @@ -import type { - Dispatch, - SetStateAction, -} from 'react' import { useCallback } from 'react' import produce from 'immer' -import type { - Edge, - EdgeMouseHandler, -} from 'reactflow' -import type { - BlockEnum, - Node, -} from './types' -import { NodeInitialData } from './constants' +import type { EdgeMouseHandler } from 'reactflow' +import { useStoreApi } from 'reactflow' +import type { SelectedNode } from './types' +import { useStore } from './store' -export const useWorkflow = ( - nodes: Node[], - edges: Edge[], - setNodes: Dispatch>, - setEdges: Dispatch>, -) => { - const handleAddNextNode = useCallback((prevNode: Node, nextNodeType: BlockEnum) => { - const nextNode = { - id: `node-${Date.now()}`, - type: 'custom', - position: { - x: prevNode.position.x + 304, - y: prevNode.position.y, - }, - data: NodeInitialData[nextNodeType], - } - const newEdge = { - id: `edge-${Date.now()}`, - source: prevNode.id, - target: nextNode.id, - } - setNodes((oldNodes) => { - return produce(oldNodes, (draft) => { - draft.push(nextNode) - }) - }) - setEdges((oldEdges) => { - return produce(oldEdges, (draft) => { - draft.push(newEdge) - }) - }) - }, [setNodes, setEdges]) +export const useWorkflow = () => { + const store = useStoreApi() + const setSelectedNode = useStore(state => state.setSelectedNode) - const handleUpdateNodeData = useCallback((nodeId: string, data: Node['data']) => { - setNodes((oldNodes) => { - return produce(oldNodes, (draft) => { - const node = draft.find(node => node.id === nodeId) - if (node) - node.data = data - }) - }) - }, [setNodes]) const handleEnterEdge = useCallback((_, edge) => { - setEdges((oldEdges) => { - return produce(oldEdges, (draft) => { - const currentEdge = draft.find(e => e.id === edge.id) - if (currentEdge) - currentEdge.data = { ...currentEdge.data, hovering: true } - }) + const { + edges, + setEdges, + } = store.getState() + const newEdges = produce(edges, (draft) => { + const currentEdge = draft.find(e => e.id === edge.id) + if (currentEdge) + currentEdge.data = { ...currentEdge.data, hovering: true } }) - }, [setEdges]) + setEdges(newEdges) + }, [store]) const handleLeaveEdge = useCallback((_, edge) => { - setEdges((oldEdges) => { - return produce(oldEdges, (draft) => { - const currentEdge = draft.find(e => e.id === edge.id) - if (currentEdge) - currentEdge.data = { ...currentEdge.data, hovering: false } - }) + const { + edges, + setEdges, + } = store.getState() + const newEdges = produce(edges, (draft) => { + const currentEdge = draft.find(e => e.id === edge.id) + if (currentEdge) + currentEdge.data = { ...currentEdge.data, hovering: false } }) - }, [setEdges]) + setEdges(newEdges) + }, [store]) + const handleSelectNode = useCallback((selectNode: SelectedNode, cancelSelection?: boolean) => { + const { + getNodes, + setNodes, + } = store.getState() + if (cancelSelection) { + setSelectedNode(null) + const newNodes = produce(getNodes(), (draft) => { + const currentNode = draft.find(n => n.id === selectNode.id) + + if (currentNode) + currentNode.data = { ...currentNode.data, selected: false } + }) + setNodes(newNodes) + } + else { + setSelectedNode(selectNode) + const newNodes = produce(getNodes(), (draft) => { + const currentNode = draft.find(n => n.id === selectNode.id) + + if (currentNode) + currentNode.data = { ...currentNode.data, selected: true } + }) + setNodes(newNodes) + } + }, [setSelectedNode, store]) return { - handleAddNextNode, - handleUpdateNodeData, handleEnterEdge, handleLeaveEdge, + handleSelectNode, } } diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 270057cc48..4abe22a56b 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -8,11 +8,6 @@ import ReactFlow, { useNodesState, } from 'reactflow' import 'reactflow/dist/style.css' -import './style.css' -import { - WorkflowContext, - useWorkflowContext, -} from './context' import { useWorkflow } from './hooks' import Header from './header' import CustomNode from './nodes' @@ -20,7 +15,6 @@ import ZoomInOut from './zoom-in-out' import CustomEdge from './custom-edge' import Panel from './panel' import type { Node } from './types' -import { useStore } from './store' const nodeTypes = { custom: CustomNode, @@ -29,13 +23,33 @@ const edgeTypes = { custom: CustomEdge, } -const Workflow = memo(() => { +type WorkflowProps = { + selectedNodeId?: string + nodes: Node[] + edges: Edge[] +} +const Workflow: FC = memo(({ + nodes: initialNodes, + edges: initialEdges, + selectedNodeId: initialSelectedNodeId, +}) => { + const [nodes] = useNodesState(initialNodes) + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges) + const { - nodes, - edges, - } = useWorkflowContext() - const handleEnterEdge = useStore(state => state.handleEnterEdge) - const handleLeaveEdge = useStore(state => state.handleLeaveEdge) + handleEnterEdge, + handleLeaveEdge, + handleSelectNode, + } = useWorkflow() + + useEffect(() => { + if (initialSelectedNodeId) { + const initialSelectedNode = nodes.find(n => n.id === initialSelectedNodeId) + + if (initialSelectedNode) + handleSelectNode({ id: initialSelectedNodeId, data: initialSelectedNode.data }) + } + }, [initialSelectedNodeId]) return (
@@ -47,8 +61,10 @@ const Workflow = memo(() => { edgeTypes={edgeTypes} nodes={nodes} edges={edges} + onEdgesChange={onEdgesChange} onEdgeMouseEnter={handleEnterEdge} onEdgeMouseLeave={handleLeaveEdge} + multiSelectionKeyCode={null} > {
) }) + Workflow.displayName = 'Workflow' -type WorkflowWrapProps = { - selectedNodeId?: string - nodes: Node[] - edges: Edge[] -} -const WorkflowWrap: FC = memo(({ - nodes: initialNodes, - edges: initialEdges, - selectedNodeId: initialSelectedNodeId, -}) => { - const [nodes, setNodes] = useNodesState(initialNodes) - const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges) - - const { - handleAddNextNode, - handleUpdateNodeData, - handleEnterEdge, - handleLeaveEdge, - } = useWorkflow( - nodes, - edges, - setNodes, - setEdges, - ) - const handleSelectedNodeId = useStore(state => state.handleSelectedNodeId) - useEffect(() => { - if (initialSelectedNodeId) - handleSelectedNodeId(initialSelectedNodeId) - }, [initialSelectedNodeId, handleSelectedNodeId]) - // const handleEnterEdge = useStore(state => state.handleEnterEdge) - // const handleLeaveEdge = useStore(state => state.handleLeaveEdge) - - return ( - -
-
- - - - - -
-
- ) -}) - -WorkflowWrap.displayName = 'WorkflowWrap' - -const WorkflowWrapWithReactFlowProvider: FC = ({ +const WorkflowWrap: FC = ({ selectedNodeId, nodes, edges, @@ -133,7 +85,7 @@ const WorkflowWrapWithReactFlowProvider: FC = ({ return ( {selectedNodeId} - = ({ ) } -export default memo(WorkflowWrapWithReactFlowProvider) +export default memo(WorkflowWrap) diff --git a/web/app/components/workflow/nodes/_base/components/next-step.tsx b/web/app/components/workflow/nodes/_base/components/next-step.tsx index 0626405376..8b59f0e979 100644 --- a/web/app/components/workflow/nodes/_base/components/next-step.tsx +++ b/web/app/components/workflow/nodes/_base/components/next-step.tsx @@ -1,31 +1,18 @@ -import type { FC } from 'react' import { memo, useCallback, - useMemo, } from 'react' -import { getOutgoers } from 'reactflow' import BlockIcon from '../../../block-icon' import type { Node } from '../../../types' import { BlockEnum } from '../../../types' -import { useWorkflowContext } from '../../../context' +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' -type NextStepProps = { - selectedNode: Node -} -const NextStep: FC = ({ - selectedNode, -}) => { - const { - nodes, - edges, - } = useWorkflowContext() - const outgoers = useMemo(() => { - return getOutgoers(selectedNode, nodes, edges) - }, [selectedNode, nodes, edges]) +const NextStep = () => { + const selectedNode = useStore(state => state.selectedNode) + const outgoers: Node[] = [] const renderAddNextNodeTrigger = useCallback((open: boolean) => { return ( @@ -60,7 +47,7 @@ const NextStep: FC = ({ return (
- +
@@ -74,7 +61,7 @@ const NextStep: FC = ({ type={outgoer.data.type} className='shrink-0 mr-1.5' /> -
{outgoer.data.name}
+
{outgoer.data.title}
{}} placement='top-end' @@ -89,7 +76,7 @@ const NextStep: FC = ({ )) } { - (!outgoers.length || selectedNode.data.type === BlockEnum.IfElse) && ( + (!outgoers.length || selectedNode!.data.type === BlockEnum.IfElse) && ( {}} placement='top' diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index 1d6bad7081..f0aa5721c0 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -5,13 +5,10 @@ import type { import { cloneElement, memo, - useMemo, } from 'react' import type { NodeProps } from 'reactflow' -import { useNodes } from 'reactflow' -import { useStore } from '../../store' -import type { NodeData } from '../../types' import BlockIcon from '../../block-icon' +import { useWorkflow } from '../../hooks' import BlockSelector from '../../block-selector' import NodeControl from './components/node-control' @@ -25,27 +22,22 @@ const BaseNode: FC = ({ selected, children, }) => { - const nodes = useNodes() - const selectedNodeId = useStore(state => state.selectedNodeId) - const handleSelectedNodeId = useStore(state => state.handleSelectedNodeId) - const currentNode = useMemo(() => { - return nodes.find(node => node.id === nodeId) - }, [nodeId, nodes]) + const { handleSelectNode } = useWorkflow() return (
handleSelectedNodeId(nodeId || '')} + onClick={() => handleSelectNode({ id: nodeId, data })} >
diff --git a/web/app/components/workflow/nodes/_base/panel.tsx b/web/app/components/workflow/nodes/_base/panel.tsx index 60e1cca137..4749cfb0c4 100644 --- a/web/app/components/workflow/nodes/_base/panel.tsx +++ b/web/app/components/workflow/nodes/_base/panel.tsx @@ -5,12 +5,10 @@ import type { import { cloneElement, memo, - useMemo, } from 'react' -import type { NodeProps } from 'reactflow' -import { useWorkflowContext } from '../../context' -import { useStore } from '../../store' +import type { SelectedNode } from '../../types' import BlockIcon from '../../block-icon' +import { useWorkflow } from '../../hooks' import NextStep from './components/next-step' import { DotsHorizontal, @@ -20,21 +18,14 @@ import { GitBranch01 } from '@/app/components/base/icons/src/vender/line/develop type BasePanelProps = { children: ReactElement -} & Pick +} & SelectedNode const BasePanel: FC = ({ id, data, children, }) => { - const { - nodes, - } = useWorkflowContext() - const selectedNodeId = useStore(state => state.selectedNodeId) - const handleSelectedNodeId = useStore(state => state.handleSelectedNodeId) - const selectedNode = useMemo(() => { - return nodes.find(node => node.id === selectedNodeId) - }, [nodes, selectedNodeId]) + const { handleSelectNode } = useWorkflow() return (
@@ -42,7 +33,7 @@ const BasePanel: FC = ({
{data.title}
@@ -53,7 +44,7 @@ const BasePanel: FC = ({
handleSelectedNodeId('')} + onClick={() => handleSelectNode({ id, data }, true)} >
@@ -76,7 +67,7 @@ const BasePanel: FC = ({
Add the next block in this workflow
- +
) diff --git a/web/app/components/workflow/nodes/index.tsx b/web/app/components/workflow/nodes/index.tsx index 9b954b5487..88782fc61c 100644 --- a/web/app/components/workflow/nodes/index.tsx +++ b/web/app/components/workflow/nodes/index.tsx @@ -1,11 +1,10 @@ -import type { FC } from 'react' import { memo } from 'react' import type { NodeProps } from 'reactflow' import { Handle, Position, } from 'reactflow' -import type { Node } from '../types' +import type { SelectedNode } from '../types' import { BlockEnum } from '../types' import { NodeComponentMap, @@ -14,7 +13,7 @@ import { import BaseNode from './_base/node' import BasePanel from './_base/panel' -const CustomNode = (props: NodeProps) => { +const CustomNode = memo((props: NodeProps) => { const nodeData = props.data const NodeComponent = NodeComponentMap[nodeData.type] @@ -43,21 +42,15 @@ const CustomNode = (props: NodeProps) => { /> ) -} +}) +CustomNode.displayName = 'CustomNode' -type PanelProps = { - node: Node -} -export const Panel: FC = memo(({ - node, -}) => { - const PanelComponent = PanelComponentMap[node.data.type] +export const Panel = memo((props: SelectedNode) => { + const nodeData = props.data + const PanelComponent = PanelComponentMap[nodeData.type] return ( - + ) diff --git a/web/app/components/workflow/panel/index.tsx b/web/app/components/workflow/panel/index.tsx index e53771c68f..2f1d3f1c51 100644 --- a/web/app/components/workflow/panel/index.tsx +++ b/web/app/components/workflow/panel/index.tsx @@ -3,22 +3,14 @@ import { memo, useMemo, } from 'react' -import { useNodes } from 'reactflow' -import { useWorkflowContext } from '../context' import { Panel as NodePanel } from '../nodes' import { useStore } from '../store' import WorkflowInfo from './workflow-info' import DebugAndPreview from './debug-and-preview' const Panel: FC = () => { - const { - mode, - } = useWorkflowContext() - const nodes = useNodes() - const selectedNodeId = useStore(state => state.selectedNodeId) - const selectedNode = useMemo(() => { - return nodes.find(node => node.id === selectedNodeId) - }, [nodes, selectedNodeId]) + const mode = useStore(state => state.mode) + const selectedNode = useStore(state => state.selectedNode) const { showWorkflowInfoPanel, showNodePanel, @@ -26,7 +18,7 @@ const Panel: FC = () => { } = useMemo(() => { return { showWorkflowInfoPanel: mode === 'workflow' && !selectedNode, - showNodePanel: selectedNode, + showNodePanel: !!selectedNode, showDebugAndPreviewPanel: mode === 'chatbot' && !selectedNode, } }, [mode, selectedNode]) @@ -35,7 +27,7 @@ const Panel: FC = () => {
{ showNodePanel && ( - + ) } { diff --git a/web/app/components/workflow/store.ts b/web/app/components/workflow/store.ts index 075557babf..cd37fecec9 100644 --- a/web/app/components/workflow/store.ts +++ b/web/app/components/workflow/store.ts @@ -1,23 +1,17 @@ import { create } from 'zustand' -import type { EdgeMouseHandler } from 'reactflow' +import type { SelectedNode } from './types' type State = { mode: string - selectedNodeId: string - hoveringEdgeId: string + selectedNode: SelectedNode | null } type Action = { - handleSelectedNodeId: (selectedNodeId: State['selectedNodeId']) => void - handleEnterEdge: EdgeMouseHandler - handleLeaveEdge: EdgeMouseHandler + setSelectedNode: (node: SelectedNode | null) => void } export const useStore = create(set => ({ mode: 'workflow', - selectedNodeId: '', - handleSelectedNodeId: selectedNodeId => set(() => ({ selectedNodeId })), - hoveringEdgeId: '', - handleEnterEdge: (_, edge) => set(() => ({ hoveringEdgeId: edge.id })), - handleLeaveEdge: () => set(() => ({ hoveringEdgeId: '' })), + selectedNode: null, + setSelectedNode: node => set(() => ({ selectedNode: node })), })) diff --git a/web/app/components/workflow/style.css b/web/app/components/workflow/style.css deleted file mode 100644 index e820a05164..0000000000 --- a/web/app/components/workflow/style.css +++ /dev/null @@ -1,3 +0,0 @@ -.react-flow__edge-path { - stroke-width: 2px !important; -} \ No newline at end of file diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 69847ac480..62a368c5f6 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -15,20 +15,16 @@ export enum BlockEnum { Tool = 'tool', } -export type NodeData = { - type: BlockEnum - name?: string - icon?: any - description?: string -} -export type Node = ReactFlowNode - export type CommonNodeType = { title: string desc: string - type: string + type: BlockEnum + selected?: boolean } +export type Node = ReactFlowNode +export type SelectedNode = Pick + export type ValueSelector = string[] // [nodeId, key | obj key path] export type Variable = {