From a293b78d8054d3c88b87bee41a8dc7b4e3a87115 Mon Sep 17 00:00:00 2001 From: Mminamiyama Date: Fri, 18 Jul 2025 14:21:01 +0800 Subject: [PATCH] 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 --- web/app/components/workflow/custom-edge.tsx | 3 +- .../workflow/hooks/use-nodes-interactions.ts | 138 +++++++++++++++++- .../workflow/hooks/use-shortcuts.ts | 38 +++++ .../_base/components/variable/var-list.tsx | 2 +- .../components/workflow/nodes/_base/node.tsx | 1 + web/app/components/workflow/types.ts | 4 +- 6 files changed, 182 insertions(+), 4 deletions(-) diff --git a/web/app/components/workflow/custom-edge.tsx b/web/app/components/workflow/custom-edge.tsx index 4467b0adb5..4874fc700b 100644 --- a/web/app/components/workflow/custom-edge.tsx +++ b/web/app/components/workflow/custom-edge.tsx @@ -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, }} /> diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index b598951adb..ef9070995b 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -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, } } diff --git a/web/app/components/workflow/hooks/use-shortcuts.ts b/web/app/components/workflow/hooks/use-shortcuts.ts index 118ec94058..f91dcf6b2c 100644 --- a/web/app/components/workflow/hooks/use-shortcuts.ts +++ b/web/app/components/workflow/hooks/use-shortcuts.ts @@ -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'], + }, + ) } diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx index b1a8d52a05..2ff8661598 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx @@ -76,7 +76,7 @@ const VarList: FC = ({ }) onChange(newList) } - }, [list, onVarNameChange, onChange]) + }, [list, onVarNameChange, onChange, t]) const handleVarReferenceChange = useCallback((index: number) => { return (value: ValueSelector | string, varKindType: VarKindType, varInfo?: Var) => { diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index b969702dd7..058ef836bc 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -142,6 +142,7 @@ const BaseNode: FC = ({ 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={{ diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 5f36956798..f5bb4dd14d 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -94,6 +94,7 @@ export type CommonNodeType = { retry_config?: WorkflowRetryConfig default_value?: DefaultValueForm[] credential_id?: string + _dimmed?: boolean } & T & Partial> 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 = ReactFlowNode>