feat(workflow): add node dimming and temporary edges for dependency visualization

- Implement dimming of non-related nodes when Shift key is pressed to highlight dependencies
- Add temporary dashed edges to visualize connections between selected node and its dependencies
- Introduce _dimmed and _isTemp flags in node and edge types to control visual states
- Add keyboard shortcuts (Shift key) to trigger dimming and undimming functionality
pull/21998/head
Mminamiyama 7 months ago
parent e43ff49f73
commit a293b78d80

@ -134,7 +134,8 @@ const CustomEdge = ({
style={{
stroke,
strokeWidth: 2,
opacity: data._waitingRun ? 0.7 : 1,
opacity: data._dimmed ? 0.3 : (data._waitingRun ? 0.7 : 1),
strokeDasharray: data._isTemp ? '8 8' : undefined,
}}
/>
<EdgeLabelRenderer>

@ -1,5 +1,5 @@
import type { MouseEvent } from 'react'
import { useCallback, useRef } from 'react'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import type {
@ -61,6 +61,7 @@ import {
} from './use-workflow'
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
import useInspectVarsCrud from './use-inspect-vars-crud'
import { getNodeUsedVars } from '../nodes/_base/components/variable/utils'
export const useNodesInteractions = () => {
const { t } = useTranslation()
@ -1530,6 +1531,139 @@ export const useNodesInteractions = () => {
setNodes(nodes)
}, [redo, store, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly])
const [isDimming, setIsDimming] = useState(false)
/** 让除 nodeId 以外的节点都加上 opacity-30 */
const dimOtherNodes = useCallback(() => {
if (isDimming)
return
const { getNodes, setNodes, edges, setEdges } = store.getState()
const nodes = getNodes()
const selectedNode = nodes.find(n => n.data.selected)
if (!selectedNode)
return
setIsDimming(true)
// const workflowNodes = useStore(s => s.getNodes())
const workflowNodes = nodes
const usedVars = getNodeUsedVars(selectedNode)
const dependencyNodes: Node[] = []
usedVars.forEach((valueSelector) => {
const node = workflowNodes.find(node => node.id === valueSelector?.[0])
if (node) {
if (!dependencyNodes.includes(node))
dependencyNodes.push(node)
}
})
const outgoers = getOutgoers(selectedNode as Node, nodes as Node[], edges)
for (let currIdx = 0; currIdx < outgoers.length; currIdx++) {
const node = outgoers[currIdx]
const outgoersForNode = getOutgoers(node, nodes as Node[], edges)
outgoersForNode.forEach((item) => {
const existed = outgoers.some(v => v.id === item.id)
if (!existed)
outgoers.push(item)
})
}
const dependentNodes: Node[] = []
outgoers.forEach((node) => {
const usedVars = getNodeUsedVars(node)
const used = usedVars.some(v => v?.[0] === selectedNode.id)
if (used) {
const existed = dependentNodes.some(v => v.id === node.id)
if (!existed)
dependentNodes.push(node)
}
})
const dimNodes = [...dependencyNodes, ...dependentNodes, selectedNode]
const newNodes = produce(nodes, (draft) => {
console.log('==========selecteds: ')
draft.forEach((n) => {
const dimNode = dimNodes.find(v => v.id === n.id)
if (!dimNode)
n.data._dimmed = true
})
console.log('====================end of selecteds')
})
setNodes(newNodes)
/* == ② 生成临时连线 == */
const tempEdges: Edge[] = []
dependencyNodes.forEach((n) => {
tempEdges.push({
id: `tmp_${n.id}-source-${selectedNode.id}-target`,
type: CUSTOM_EDGE, // 复用自定义 Edge也可以写一个 DIM_EDGE
source: n.id, // 依赖 → 选中
sourceHandle: 'source_tmp',
target: selectedNode.id,
targetHandle: 'target_tmp',
animated: true,
data: {
sourceType: n.data.type,
targetType: selectedNode.data.type,
_isTemp: true,
_connectedNodeIsHovering: true,
},
})
})
dependentNodes.forEach((n) => {
tempEdges.push({
id: `tmp_${selectedNode.id}-source-${n.id}-target`,
type: CUSTOM_EDGE, // 复用自定义 Edge也可以写一个 DIM_EDGE
source: selectedNode.id, // 依赖 → 选中
sourceHandle: 'source_tmp',
target: n.id,
targetHandle: 'target_tmp',
animated: true,
data: {
sourceType: selectedNode.data.type,
targetType: n.data.type,
_isTemp: true,
_connectedNodeIsHovering: true,
},
})
})
const newEdges = produce(edges, (draft) => {
draft.forEach((e) => {
e.data._dimmed = true
})
draft.push(...tempEdges)
})
setEdges(newEdges)
}, [isDimming, store])
/** 把所有节点恢复为不透明 */
const undimAllNodes = useCallback(() => {
const { getNodes, setNodes, edges, setEdges } = store.getState()
const nodes = getNodes()
setIsDimming(false)
const newNodes = produce(nodes, (draft) => {
draft.forEach((n) => {
n.data._dimmed = false
// handleNodeLeave(null as unknown as MouseEvent, n)
})
})
setNodes(newNodes)
const newEdges = produce(edges.filter(e => !e.data._isTemp), (draft) => {
draft.forEach((e) => {
e.data._dimmed = false
})
})
setEdges(newEdges)
}, [store])
return {
handleNodeDragStart,
handleNodeDrag,
@ -1554,5 +1688,7 @@ export const useNodesInteractions = () => {
handleNodeDisconnect,
handleHistoryBack,
handleHistoryForward,
dimOtherNodes,
undimAllNodes,
}
}

@ -25,6 +25,8 @@ export const useShortcuts = (): void => {
handleNodesDelete,
handleHistoryBack,
handleHistoryForward,
dimOtherNodes,
undimAllNodes,
} = useNodesInteractions()
const { handleStartWorkflowRun } = useWorkflowStartRun()
const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore()
@ -211,4 +213,40 @@ export const useShortcuts = (): void => {
exactMatch: true,
useCapture: true,
})
// Shift ↓(按下)
useKeyPress(
'shift',
(e) => {
console.log('Shift down', e)
if (shouldHandleShortcut(e)) {
// handleSelectedNodeHighlight()
dimOtherNodes()
}
},
{
exactMatch: true,
useCapture: true,
events: ['keydown'],
},
)
// Shift ↑
useKeyPress(
(e) => {
return e.key === 'Shift'
},
(e) => {
if (shouldHandleShortcut(e)) {
console.log('Shift up 2: ', e)
// handleSelectedNodeUnhighlight()
undimAllNodes()
}
},
{
exactMatch: true,
useCapture: true,
events: ['keyup'],
},
)
}

@ -76,7 +76,7 @@ const VarList: FC<Props> = ({
})
onChange(newList)
}
}, [list, onVarNameChange, onChange])
}, [list, onVarNameChange, onChange, t])
const handleVarReferenceChange = useCallback((index: number) => {
return (value: ValueSelector | string, varKindType: VarKindType, varInfo?: Var) => {

@ -142,6 +142,7 @@ const BaseNode: FC<BaseNodeProps> = ({
showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent',
!showSelectedBorder && data._inParallelHovering && 'border-workflow-block-border-highlight',
data._waitingRun && 'opacity-70',
data._dimmed && 'opacity-30',
)}
ref={nodeRef}
style={{

@ -94,6 +94,7 @@ export type CommonNodeType<T = {}> = {
retry_config?: WorkflowRetryConfig
default_value?: DefaultValueForm[]
credential_id?: string
_dimmed?: boolean
} & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>>
export type CommonEdgeType = {
@ -109,7 +110,8 @@ export type CommonEdgeType = {
isInLoop?: boolean
loop_id?: string
sourceType: BlockEnum
targetType: BlockEnum
targetType: BlockEnum,
_isTemp?: boolean,
}
export type Node<T = {}> = ReactFlowNode<CommonNodeType<T>>

Loading…
Cancel
Save