feat: refactor workflow branch selection logic and introduce context provider

This commit is contained in:
mlogclub
2026-06-28 10:35:19 +08:00
parent 64c10ba6dd
commit af9c28033a
5 changed files with 113 additions and 67 deletions
@@ -20,25 +20,16 @@ import {
syncConditionBranchTargetsFromEdges, syncConditionBranchTargetsFromEdges,
} from "./workflow-utils" } from "./workflow-utils"
export type SelectedWorkflowBranch = {
nodeId: string
branchId: string
}
export function useFlowgramEditorProps({ export function useFlowgramEditorProps({
definition, definition,
nodeSpecs, nodeSpecs,
selectedBranch,
readonly = false, readonly = false,
onDefinitionChange, onDefinitionChange,
onSelectBranch,
}: { }: {
definition: AIWorkflowDefinition definition: AIWorkflowDefinition
nodeSpecs: AIWorkflowNodeSpec[] nodeSpecs: AIWorkflowNodeSpec[]
selectedBranch?: SelectedWorkflowBranch | null
readonly?: boolean readonly?: boolean
onDefinitionChange?: (definition: AIWorkflowDefinition) => void onDefinitionChange?: (definition: AIWorkflowDefinition) => void
onSelectBranch?: (branch: SelectedWorkflowBranch | null) => void
}) { }) {
return useMemo<FreeLayoutProps>( return useMemo<FreeLayoutProps>(
() => { () => {
@@ -59,7 +50,7 @@ export function useFlowgramEditorProps({
materials: { materials: {
renderDefaultNode: FlowgramNodeRenderer, renderDefaultNode: FlowgramNodeRenderer,
}, },
nodeRegistries: buildFlowgramNodeRegistries(nodeSpecs, selectedBranch, onSelectBranch), nodeRegistries: buildFlowgramNodeRegistries(nodeSpecs),
nodeEngine: { nodeEngine: {
enable: true, enable: true,
}, },
@@ -123,7 +114,7 @@ export function useFlowgramEditorProps({
], ],
} }
}, },
[definition, nodeSpecs, onDefinitionChange, onSelectBranch, readonly, selectedBranch] [definition, nodeSpecs, onDefinitionChange, readonly]
) )
} }
@@ -8,13 +8,9 @@ import {
normalizeNodeConfig, normalizeNodeConfig,
type WorkflowConditionBranch, type WorkflowConditionBranch,
} from "./workflow-utils" } from "./workflow-utils"
import type { SelectedWorkflowBranch } from "./flowgram-editor-provider" import { useWorkflowBranchSelection } from "./workflow-branch-selection"
export function buildFlowgramNodeRegistries( export function buildFlowgramNodeRegistries(nodeSpecs: AIWorkflowNodeSpec[]): WorkflowNodeRegistry[] {
nodeSpecs: AIWorkflowNodeSpec[],
selectedBranch?: SelectedWorkflowBranch | null,
onSelectBranch?: (branch: SelectedWorkflowBranch | null) => void
): WorkflowNodeRegistry[] {
const seen = new Set<string>() const seen = new Set<string>()
const specs = nodeSpecs.length > 0 const specs = nodeSpecs.length > 0
? nodeSpecs ? nodeSpecs
@@ -45,8 +41,6 @@ export function buildFlowgramNodeRegistries(
<FlowgramNodeForm <FlowgramNodeForm
nodeType={spec.type} nodeType={spec.type}
fallbackTitle={spec.title || spec.type} fallbackTitle={spec.title || spec.type}
selectedBranch={selectedBranch}
onSelectBranch={onSelectBranch}
/> />
), ),
}, },
@@ -56,13 +50,9 @@ export function buildFlowgramNodeRegistries(
function FlowgramNodeForm({ function FlowgramNodeForm({
nodeType, nodeType,
fallbackTitle, fallbackTitle,
selectedBranch,
onSelectBranch,
}: { }: {
nodeType: string nodeType: string
fallbackTitle: string fallbackTitle: string
selectedBranch?: SelectedWorkflowBranch | null
onSelectBranch?: (branch: SelectedWorkflowBranch | null) => void
}) { }) {
const { node } = useNodeRender() const { node } = useNodeRender()
const nodeId = String(node.id ?? "") const nodeId = String(node.id ?? "")
@@ -72,8 +62,6 @@ function FlowgramNodeForm({
<ConditionNodeForm <ConditionNodeForm
fallbackTitle={fallbackTitle} fallbackTitle={fallbackTitle}
nodeId={nodeId} nodeId={nodeId}
selectedBranch={selectedBranch}
onSelectBranch={onSelectBranch}
/> />
) )
} }
@@ -108,14 +96,12 @@ function defaultPortsForNodeType(type: string) {
function ConditionNodeForm({ function ConditionNodeForm({
fallbackTitle, fallbackTitle,
nodeId, nodeId,
selectedBranch,
onSelectBranch,
}: { }: {
fallbackTitle: string fallbackTitle: string
nodeId: string nodeId: string
selectedBranch?: SelectedWorkflowBranch | null
onSelectBranch?: (branch: SelectedWorkflowBranch | null) => void
}) { }) {
const { selectedBranch, onSelectBranch } = useWorkflowBranchSelection()
return ( return (
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<Field<string> name="title"> <Field<string> name="title">
@@ -154,10 +140,16 @@ function ConditionNodeForm({
? "bg-[#dfe4ff] text-[#3327b9] ring-1 ring-inset ring-[#4e40e5]/45 before:w-1" ? "bg-[#dfe4ff] text-[#3327b9] ring-1 ring-inset ring-[#4e40e5]/45 before:w-1"
: "bg-[#eef1f7] hover:bg-[#e6eaff]", : "bg-[#eef1f7] hover:bg-[#e6eaff]",
].join(" ")} ].join(" ")}
onClick={(event) => { onPointerDownCapture={(event) => {
event.stopPropagation() event.stopPropagation()
onSelectBranch?.({ nodeId, branchId: branch.id }) onSelectBranch?.({ nodeId, branchId: branch.id })
}} }}
onMouseDownCapture={(event) => {
event.stopPropagation()
}}
onClick={(event) => {
event.stopPropagation()
}}
> >
<span className="min-w-0 flex-1 truncate font-medium text-foreground/90"> <span className="min-w-0 flex-1 truncate font-medium text-foreground/90">
{branch.name || (branch.default ? "默认分支" : branch.id)} {branch.name || (branch.default ? "默认分支" : branch.id)}
@@ -170,6 +162,12 @@ function ConditionNodeForm({
type="button" type="button"
className="flex size-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive" className="flex size-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
aria-label={`删除条件 ${branch.name || branch.id}`} aria-label={`删除条件 ${branch.name || branch.id}`}
onPointerDownCapture={(event) => {
event.stopPropagation()
}}
onMouseDownCapture={(event) => {
event.stopPropagation()
}}
onClick={(event) => { onClick={(event) => {
event.stopPropagation() event.stopPropagation()
deleteBranch(branch.id) deleteBranch(branch.id)
@@ -0,0 +1,41 @@
"use client"
import { createContext, useContext, useMemo, type ReactNode } from "react"
export type SelectedWorkflowBranch = {
nodeId: string
branchId: string
}
type WorkflowBranchSelectionContextValue = {
selectedBranch: SelectedWorkflowBranch | null
onSelectBranch: (branch: SelectedWorkflowBranch | null) => void
}
const WorkflowBranchSelectionContext = createContext<WorkflowBranchSelectionContextValue>({
selectedBranch: null,
onSelectBranch: () => {},
})
export function WorkflowBranchSelectionProvider({
selectedBranch,
onSelectBranch,
children,
}: WorkflowBranchSelectionContextValue & {
children: ReactNode
}) {
const value = useMemo(
() => ({ selectedBranch, onSelectBranch }),
[onSelectBranch, selectedBranch]
)
return (
<WorkflowBranchSelectionContext.Provider value={value}>
{children}
</WorkflowBranchSelectionContext.Provider>
)
}
export function useWorkflowBranchSelection() {
return useContext(WorkflowBranchSelectionContext)
}
@@ -5,7 +5,7 @@ import { XIcon } from "lucide-react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import type { AIWorkflowDefinition, AIWorkflowNodeSpec } from "@/lib/api/admin" import type { AIWorkflowDefinition, AIWorkflowNodeSpec } from "@/lib/api/admin"
import type { SelectedWorkflowBranch } from "./flowgram-editor-provider" import type { SelectedWorkflowBranch } from "./workflow-branch-selection"
import { ConditionBranchConfigPanel, NodeConfigPanel } from "./node-config-panel" import { ConditionBranchConfigPanel, NodeConfigPanel } from "./node-config-panel"
import { import {
getAvailableVariables, getAvailableVariables,
@@ -1,6 +1,6 @@
"use client" "use client"
import { useEffect, useMemo, useState, type ReactNode } from "react" import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"
import { import {
EditorRenderer, EditorRenderer,
@@ -16,7 +16,11 @@ import {
import type { AIWorkflowDefinition, AIWorkflowNodeSpec } from "@/lib/api/admin" import type { AIWorkflowDefinition, AIWorkflowNodeSpec } from "@/lib/api/admin"
import { useFlowgramEditorProps, type SelectedWorkflowBranch } from "./flowgram-editor-provider" import { useFlowgramEditorProps } from "./flowgram-editor-provider"
import {
WorkflowBranchSelectionProvider,
type SelectedWorkflowBranch,
} from "./workflow-branch-selection"
import { WorkflowConfigPanel } from "./workflow-config-sidebar" import { WorkflowConfigPanel } from "./workflow-config-sidebar"
import { WorkflowEditorToolbar } from "./workflow-editor-toolbar" import { WorkflowEditorToolbar } from "./workflow-editor-toolbar"
import { WorkflowNodePalette } from "./workflow-node-palette" import { WorkflowNodePalette } from "./workflow-node-palette"
@@ -66,6 +70,7 @@ export function WorkflowEditor({
const [localDefinition, setLocalDefinition] = useState(definition) const [localDefinition, setLocalDefinition] = useState(definition)
const [selectedNodeId, setSelectedNodeId] = useState("") const [selectedNodeId, setSelectedNodeId] = useState("")
const [selectedBranch, setSelectedBranch] = useState<SelectedWorkflowBranch | null>(null) const [selectedBranch, setSelectedBranch] = useState<SelectedWorkflowBranch | null>(null)
const branchSelectAtRef = useRef(0)
const validation = useMemo( const validation = useMemo(
() => validateWorkflowDefinition(localDefinition, nodeSpecs), () => validateWorkflowDefinition(localDefinition, nodeSpecs),
@@ -75,22 +80,30 @@ export function WorkflowEditor({
const editorProps = useFlowgramEditorProps({ const editorProps = useFlowgramEditorProps({
definition: localDefinition, definition: localDefinition,
nodeSpecs, nodeSpecs,
selectedBranch,
onDefinitionChange: (next) => { onDefinitionChange: (next) => {
setLocalDefinition(next) setLocalDefinition(next)
onDefinitionChange(next) onDefinitionChange(next)
}, },
onSelectBranch: (branch) => { })
const handleSelectBranch = useCallback(
(branch: SelectedWorkflowBranch | null) => {
if (!branch) { if (!branch) {
setSelectedBranch(null) setSelectedBranch(null)
return return
} }
branchSelectAtRef.current = Date.now()
setSelectedNodeId(branch.nodeId) setSelectedNodeId(branch.nodeId)
setSelectedBranch(branch) setSelectedBranch(branch)
}, },
}) []
)
return ( return (
<WorkflowBranchSelectionProvider
selectedBranch={selectedBranch}
onSelectBranch={handleSelectBranch}
>
<FreeLayoutEditorProvider {...editorProps}> <FreeLayoutEditorProvider {...editorProps}>
<WorkflowEditorInner <WorkflowEditorInner
definition={localDefinition} definition={localDefinition}
@@ -105,9 +118,11 @@ export function WorkflowEditor({
}} }}
onSelectNode={(nodeId) => { onSelectNode={(nodeId) => {
setSelectedNodeId(nodeId) setSelectedNodeId(nodeId)
if (!nodeId || Date.now() - branchSelectAtRef.current > 160) {
setSelectedBranch(null) setSelectedBranch(null)
}
}} }}
onSelectBranch={setSelectedBranch} onSelectBranch={handleSelectBranch}
onUndo={onUndo} onUndo={onUndo}
undoDisabled={undoDisabled} undoDisabled={undoDisabled}
onRedo={onRedo} onRedo={onRedo}
@@ -122,6 +137,7 @@ export function WorkflowEditor({
publishDisabled={publishDisabled} publishDisabled={publishDisabled}
/> />
</FreeLayoutEditorProvider> </FreeLayoutEditorProvider>
</WorkflowBranchSelectionProvider>
) )
} }