diff --git a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx index 7811cc7b..8e2687f1 100644 --- a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, type FC } from 'react'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; -import { $getSelection, $isRangeSelection, $isTextNode } from 'lexical'; +import { $getSelection, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_HIGH, KEY_ENTER_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ESCAPE_COMMAND } from 'lexical'; import { INSERT_VARIABLE_COMMAND } from '../commands'; import type { NodeProperties } from '../../../types' @@ -45,6 +45,9 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> (textBeforeCursor === '/' && anchorOffset === 1); setShowSuggestions(shouldShow); + if (!shouldShow) { + setSelectedIndex(0); + } if (shouldShow) { const domSelection = window.getSelection(); @@ -113,9 +116,6 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> setShowSuggestions(false); }; - if (!showSuggestions) return null; - - // Group options by node id const groupedSuggestions = options.reduce((groups: Record, suggestion) => { const { nodeData } = suggestion const nodeId = nodeData.id as string; @@ -126,6 +126,93 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> return groups; }, {}); + useEffect(() => { + if (!showSuggestions) return; + + const allOptions = Object.values(groupedSuggestions).flat(); + + return editor.registerCommand( + KEY_ENTER_COMMAND, + (event) => { + if (showSuggestions && allOptions.length > 0) { + const selectedOption = allOptions[selectedIndex]; + if (selectedOption && !selectedOption.disabled) { + event?.preventDefault(); + insertMention(selectedOption); + return true; + } + } + return false; + }, + COMMAND_PRIORITY_HIGH + ); + }, [showSuggestions, selectedIndex, groupedSuggestions, insertMention, editor]); + + useEffect(() => { + if (!showSuggestions) return; + + const allOptions = Object.values(groupedSuggestions).flat(); + + const unregisterArrowDown = editor.registerCommand( + KEY_ARROW_DOWN_COMMAND, + (event) => { + if (showSuggestions && allOptions.length > 0) { + event?.preventDefault(); + setSelectedIndex(prev => { + let nextIndex = prev + 1; + while (nextIndex < allOptions.length && allOptions[nextIndex].disabled) { + nextIndex++; + } + return nextIndex >= allOptions.length ? prev : nextIndex; + }); + return true; + } + return false; + }, + COMMAND_PRIORITY_HIGH + ); + + const unregisterArrowUp = editor.registerCommand( + KEY_ARROW_UP_COMMAND, + (event) => { + if (showSuggestions && allOptions.length > 0) { + event?.preventDefault(); + setSelectedIndex(prev => { + let prevIndex = prev - 1; + while (prevIndex >= 0 && allOptions[prevIndex].disabled) { + prevIndex--; + } + return prevIndex < 0 ? prev : prevIndex; + }); + return true; + } + return false; + }, + COMMAND_PRIORITY_HIGH + ); + + const unregisterEscape = editor.registerCommand( + KEY_ESCAPE_COMMAND, + (event) => { + if (showSuggestions) { + event?.preventDefault(); + setShowSuggestions(false); + return true; + } + return false; + }, + COMMAND_PRIORITY_HIGH + ); + + return () => { + unregisterArrowDown(); + unregisterArrowUp(); + unregisterEscape(); + }; + }, [showSuggestions, selectedIndex, groupedSuggestions, editor]); + + if (!showSuggestions) return null; + if (Object.entries(groupedSuggestions).length === 0) { return null } diff --git a/web/src/views/Workflow/components/Nodes/AddNode.tsx b/web/src/views/Workflow/components/Nodes/AddNode.tsx index f9561a44..13e80150 100644 --- a/web/src/views/Workflow/components/Nodes/AddNode.tsx +++ b/web/src/views/Workflow/components/Nodes/AddNode.tsx @@ -1,8 +1,14 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-09 18:31:30 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-09 18:31:30 + */ import { useState } from 'react'; import { Popover } from 'antd'; import clsx from 'clsx'; import type { ReactShapeConfig } from '@antv/x6-react-shape'; -import { nodeLibrary, graphNodeLibrary, edgeAttrs } from '../../constant'; +import { nodeLibrary, graphNodeLibrary, edgeAttrs, nodeWidth } from '../../constant'; import { useTranslation } from 'react-i18next'; const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { @@ -10,6 +16,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); + // Handle node selection from popover and create new node replacing the add-node placeholder const handleNodeSelect = (selectedNodeType: any) => { const parentBBox = node.getBBox(); const cycleId = data.cycle; @@ -32,7 +39,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { }, }); - // 将新节点添加为父节点的子节点 + // Add new node as child of parent node if (cycleId) { const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId); if (parentNode) { @@ -61,14 +68,14 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { }); }); - // 删除所有add-node类型的节点 + // Remove all add-node type nodes graph.getNodes().forEach((n: any) => { if (n.getData()?.type === 'add-node' && n.getData()?.cycle === cycleId) { n.remove(); } }); - // 自动调整循环节点大小 + // Automatically adjust loop node size const loopNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId); if (loopNode) { const adjustLoopSize = () => { @@ -85,7 +92,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { }, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }); const padding = 20; - const newWidth = Math.max(240, bounds.maxX - bounds.minX + padding * 2); + const newWidth = Math.max(nodeWidth, bounds.maxX - bounds.minX + padding * 2); const newHeight = Math.max(120, bounds.maxY - bounds.minY + padding * 2); loopNode.prop('size', { width: newWidth, height: newHeight }); @@ -94,7 +101,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { adjustLoopSize(); - // 监听子节点移动事件 + // Listen to child node movement events const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId); childNodes.forEach((childNode: any) => { childNode.on('change:position', adjustLoopSize); @@ -104,7 +111,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { }; const content = ( -
+
{nodeLibrary.map((category, categoryIndex) => { const filteredNodes = category.nodes.filter(nodeType => nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'iteration' && nodeType.type !== 'loop' && nodeType.type !== 'cycle-start' diff --git a/web/src/views/Workflow/components/PortClickHandler.tsx b/web/src/views/Workflow/components/PortClickHandler.tsx index 0a1e3906..ec898bc8 100644 --- a/web/src/views/Workflow/components/PortClickHandler.tsx +++ b/web/src/views/Workflow/components/PortClickHandler.tsx @@ -1,7 +1,13 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-09 18:30:28 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-09 18:30:28 + */ import { useEffect, useState } from 'react'; import { Popover } from 'antd'; import { useTranslation } from 'react-i18next'; -import { nodeLibrary, graphNodeLibrary, edgeAttrs } from '../constant'; +import { nodeLibrary, graphNodeLibrary, edgeAttrs, nodeWidth } from '../constant'; interface PortClickHandlerProps { graph: any; @@ -32,13 +38,14 @@ const PortClickHandler: React.FC = ({ graph }) => { }; }, []); + // Handle node selection from popover menu and create new node with edge connection const handleNodeSelect = (selectedNodeType: any) => { if (!sourceNode || !graph) return; const sourceNodeData = sourceNode.getData(); const sourceNodeType = sourceNodeData?.type; - // 如果是cycle-start节点,需要处理add-node节点 + // If it's a cycle-start node, handle the add-node placeholder let addNodePosition = null; if (sourceNodeType === 'cycle-start' && sourceNodeData.cycle) { const cycleId = sourceNodeData.cycle; @@ -53,38 +60,38 @@ const PortClickHandler: React.FC = ({ graph }) => { } } - // 计算新节点位置,避免重叠 + // Calculate new node position to avoid overlapping const sourceBBox = sourceNode.getBBox(); const nodeWidth = graphNodeLibrary[selectedNodeType.type]?.width || 120; const nodeHeight = graphNodeLibrary[selectedNodeType.type]?.height || 88; const horizontalSpacing = sourceNodeType === 'cycle-start' ? 40 : 80; const verticalSpacing = 10; - // 获取源连接桩的group信息 + // Get source port group information const sourcePortInfo = sourceNode.getPorts().find((p: any) => p.id === sourcePort); const sourcePortGroup = sourcePortInfo?.group || sourcePort; console.log('sourcePortGroup', sourcePortGroup, sourcePortInfo) - // 如果有add-node位置,使用该位置,否则计算新位置 + // If add-node position exists, use it; otherwise calculate new position let newX, newY; if (addNodePosition) { newX = addNodePosition.x; newY = addNodePosition.y; } else { - // 根据连接桩位置决定节点放置方向 + // Determine node placement direction based on port position if (sourcePortGroup === 'left') { - // 左侧连接桩,在左侧添加节点 + // Left port: add node to the left newX = sourceBBox.x - nodeWidth*2 - horizontalSpacing; newY = sourceBBox.y; } else { - // 右侧连接桩,在右侧添加节点 + // Right port: add node to the right newX = sourceBBox.x + sourceBBox.width + horizontalSpacing; newY = sourceBBox.y; } - // 检查位置是否与现有节点重叠(只考虑与当前节点相连的节点) + // Check if position overlaps with existing nodes (only consider connected nodes) const checkOverlap = (x: number, y: number) => { - // 获取与源节点相连的节点 + // Get nodes connected to the source node const connectedNodes = new Set(); graph.getConnectedEdges(sourceNode).forEach((edge: any) => { const sourceId = edge.getSourceCellId(); @@ -95,20 +102,20 @@ const PortClickHandler: React.FC = ({ graph }) => { return graph.getNodes().some((node: any) => { if (node.id === sourceNode.id) return false; - if (!connectedNodes.has(node.id)) return false; // 只考虑相连的节点 + if (!connectedNodes.has(node.id)) return false; // Only consider connected nodes const bbox = node.getBBox(); return !(x + nodeWidth < bbox.x || x > bbox.x + bbox.width || y + nodeHeight < bbox.y || y > bbox.y + bbox.height); }); }; - // 如果位置被占用,向下寻找空位 + // If position is occupied, search downward for empty space while (checkOverlap(newX, newY)) { newY += nodeHeight + verticalSpacing; } } - // 创建新节点 + // Create new node const id = `${selectedNodeType.type.replace(/-/g, '_')}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` const newNode = graph.addNode({ ...(graphNodeLibrary[selectedNodeType.type] || graphNodeLibrary.default), @@ -120,12 +127,12 @@ const PortClickHandler: React.FC = ({ graph }) => { type: selectedNodeType.type, icon: selectedNodeType.icon, name: t(`workflow.${selectedNodeType.type}`), - cycle: sourceNodeData.cycle, // 继承源节点的cycle + cycle: sourceNodeData.cycle, // Inherit cycle from source node config: selectedNodeType.config || {} }, }); - // 将新节点添加为父节点的子节点 + // Add new node as child of parent node if (sourceNodeData.cycle) { const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle); if (parentNode) { @@ -133,16 +140,16 @@ const PortClickHandler: React.FC = ({ graph }) => { } } - // 创建连线 + // Create edge connection setTimeout(() => { const targetPorts = newNode.getPorts(); let targetPort; if (sourcePortGroup === 'left') { - // 从左侧连接桩连出,连接到新节点的右侧 + // Connect from left port to new node's right side targetPort = targetPorts.find((port: any) => port.group === 'right')?.id || 'right'; } else { - // 从右侧连接桩连出,连接到新节点的左侧 + // Connect from right port to new node's left side targetPort = targetPorts.find((port: any) => port.group === 'left')?.id || 'left'; } @@ -153,7 +160,7 @@ const PortClickHandler: React.FC = ({ graph }) => { // zIndex: sourceNodeData.cycle && sourceNodeType == 'cycle-start' ? 1 : sourceNodeData.cycle ? 2 : 0 }); - // 循环节点内子节点通过连接桩添加时,调整循环节点大小 + // Adjust loop node size when child node is added via port within loop node const cycleId = sourceNodeData.cycle; if (cycleId) { const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId); @@ -174,7 +181,7 @@ const PortClickHandler: React.FC = ({ graph }) => { const padding = 20; const bottomPadding = 50; - const newWidth = Math.max(240, bounds.maxX - bounds.minX + padding * 2); + const newWidth = Math.max(nodeWidth, bounds.maxX - bounds.minX + padding * 2); const newHeight = Math.max(120, bounds.maxY - bounds.minY + padding + bottomPadding); parentNode.prop('size', { width: newWidth, height: newHeight }); @@ -183,7 +190,7 @@ const PortClickHandler: React.FC = ({ graph }) => { adjustLoopSize(); - // 监听子节点移动事件 + // Listen to child node movement events const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId); childNodes.forEach((childNode: any) => { childNode.on('change:position', adjustLoopSize); @@ -192,7 +199,7 @@ const PortClickHandler: React.FC = ({ graph }) => { } }, 50); - // 清理临时元素 + // Clean up temporary element if (tempElement) { document.body.removeChild(tempElement); setTempElement(null); @@ -210,7 +217,7 @@ const PortClickHandler: React.FC = ({ graph }) => { }; const content = ( -
+
{nodeLibrary.map((category, categoryIndex) => { const sourceNodeData = sourceNode?.getData(); const isChildOfLoop = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'loop'); diff --git a/web/src/views/Workflow/components/Properties/CaseList/index.tsx b/web/src/views/Workflow/components/Properties/CaseList/index.tsx index f76bd7db..34708513 100644 --- a/web/src/views/Workflow/components/Properties/CaseList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CaseList/index.tsx @@ -1,3 +1,9 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-09 18:24:53 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-09 18:24:53 + */ import { type FC } from 'react' import clsx from 'clsx' import { useTranslation } from 'react-i18next'; @@ -6,7 +12,7 @@ import { Form, Button, Select, Space, Divider, InputNumber, Radio, type SelectPr import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import VariableSelect from '../VariableSelect' import Editor from '../../Editor' -import { edgeAttrs, portArgs } from '../../../constant' +import { edgeAttrs, portTextAttrs, nodeWidth } from '../../../constant' interface CaseListProps { value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; input_type?: string; }[] }>; @@ -52,15 +58,16 @@ const CaseList: FC = ({ const { t } = useTranslation(); const form = Form.useFormInstance(); + // Update node ports based on case count changes (add/remove cases) const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => { if (!selectedNode || !graphRef?.current) return; - // 获取当前端口数量来判断是添加还是删除操作 + // Get current port count to determine if it's an add or remove operation const currentPorts = selectedNode.getPorts().filter((port: any) => port.group === 'right'); - const currentCaseCount = currentPorts.length - 1; // 减去ELSE端口 + const currentCaseCount = currentPorts.length - 1; // Exclude ELSE port const isAddingCase = removedCaseIndex === undefined && caseCount > currentCaseCount; - // 保存现有连线信息(包括左侧端口连线) + // Save existing edge connections (including left-side port connections) const existingEdges = graphRef.current.getEdges().filter((edge: any) => edge.getSourceCellId() === selectedNode.id || edge.getTargetCellId() === selectedNode.id ); @@ -73,7 +80,7 @@ const CaseList: FC = ({ isIncoming: edge.getTargetCellId() === selectedNode.id })); - // 移除所有现有的右侧端口 + // Remove all existing right-side ports const existingPorts = selectedNode.getPorts(); existingPorts.forEach((port: any) => { if (port.group === 'right') { @@ -81,43 +88,52 @@ const CaseList: FC = ({ } }); - // 计算新的节点高度:基础高度88px + 每个额外port增加30px + // Calculate new node height: base height 88px + 30px for each additional port const baseHeight = 88; const totalPorts = caseCount + 1; // IF/ELIF + ELSE const newHeight = baseHeight + (totalPorts - 2) * 30; - selectedNode.prop('size', { width: 240, height: newHeight }) - - // 添加 IF 端口 + selectedNode.prop('size', { width: nodeWidth, height: newHeight }) + + // Add IF port selectedNode.addPort({ id: 'CASE1', group: 'right', - args: portArgs, - attrs: { text: { text: 'IF', fontSize: 12, fill: '#5B6167' }} - }); + args: { + x: nodeWidth, + y: 42, + }, + attrs: { text: { text: 'IF', ...portTextAttrs } } + }) - // 添加 ELIF 端口 + // Add ELIF ports for (let i = 1; i < caseCount; i++) { selectedNode.addPort({ id: `CASE${i + 1}`, group: 'right', - args: portArgs, - attrs: { text: { text: 'ELIF', fontSize: 12, fill: '#5B6167' }} + args: { + x: nodeWidth, + y: 30 * i + 42, + }, + attrs: { text: { text: 'ELIF', ...portTextAttrs }} }); } - // 添加 ELSE 端口 + // Add ELSE port selectedNode.addPort({ id: `CASE${caseCount + 1}`, group: 'right', - args: portArgs, - attrs: { text: { text: 'ELSE', fontSize: 12, fill: '#5B6167' }} + args: { + x: nodeWidth, + y: 30 * caseCount + 42, + }, + attrs: { text: { text: 'ELSE', ...portTextAttrs }} }); - // 恢复连线 + // Restore edge connections setTimeout(() => { edgeConnections.forEach(({ edge, sourcePortId, targetCellId, targetPortId, sourceCellId, isIncoming }: any) => { - // 如果是进入连线(左侧端口),直接恢复 + // If it's an incoming connection (left-side port), restore directly if (isIncoming) { const sourceCell = graphRef.current?.getCellById(sourceCellId); if (sourceCell) { @@ -131,10 +147,10 @@ const CaseList: FC = ({ return; } - // 处理右侧端口连线 + // Handle right-side port connections const originalCaseNumber = parseInt(sourcePortId.match(/CASE(\d+)/)?.[1] || '0'); - // 如果是删除操作且是被删除的端口,删除连线 + // If it's a remove operation and the port is being removed, delete the connection if (removedCaseIndex !== undefined && originalCaseNumber === removedCaseIndex + 1) { graphRef.current?.removeCell(edge); return; @@ -142,22 +158,22 @@ const CaseList: FC = ({ let newPortId = sourcePortId; - // 如果是删除操作,需要重新映射端口ID + // If it's a remove operation, remap port IDs if (removedCaseIndex !== undefined) { if (originalCaseNumber > removedCaseIndex + 1) { - // 被删除端口之后的端口,编号向前移动 + // Ports after the removed port, shift numbering forward newPortId = `CASE${originalCaseNumber - 1}`; } - // ELSE端口始终映射到新的ELSE端口位置 + // ELSE port always maps to the new ELSE port position else if (originalCaseNumber === currentCaseCount + 1) { newPortId = `CASE${caseCount + 1}`; } } else if (isAddingCase) { - // 如果是添加操作,ELSE端口需要重新映射 + // If it's an add operation, ELSE port needs to be remapped if (originalCaseNumber === currentCaseCount + 1) { - newPortId = `CASE${caseCount + 1}`; // 新的ELSE端口 + newPortId = `CASE${caseCount + 1}`; // New ELSE port } - // 新添加的端口不恢复任何连线 + // Newly added ports don't restore any connections } const newPorts = selectedNode.getPorts(); diff --git a/web/src/views/Workflow/components/Properties/CategoryList/index.tsx b/web/src/views/Workflow/components/Properties/CategoryList/index.tsx index 22163905..63c64583 100644 --- a/web/src/views/Workflow/components/Properties/CategoryList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CategoryList/index.tsx @@ -1,3 +1,9 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-09 18:34:33 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-09 18:34:33 + */ import { type FC } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Form, Space } from 'antd'; @@ -5,7 +11,7 @@ import { Graph, Node } from '@antv/x6'; import Editor from '../../Editor'; import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' -import { edgeAttrs, portArgs } from '../../../constant' +import { edgeAttrs, portTextAttrs, nodeWidth } from '../../../constant' interface CategoryListProps { parentName: string; @@ -19,10 +25,11 @@ const CategoryList: FC = ({ parentName, selectedNode, graphRe const form = Form.useFormInstance(); const formValues = Form.useWatch([parentName], form); + // Update node ports based on category count changes (add/remove categories) const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => { if (!selectedNode || !graphRef?.current) return; - // 保存现有连线信息(包括左侧端口连线) + // Save existing edge connections (including left-side port connections) const existingEdges = graphRef.current.getEdges().filter((edge: any) => edge.getSourceCellId() === selectedNode.id || edge.getTargetCellId() === selectedNode.id ); @@ -35,7 +42,7 @@ const CategoryList: FC = ({ parentName, selectedNode, graphRe isIncoming: edge.getTargetCellId() === selectedNode.id })); - // 移除所有现有的右侧端口 + // Remove all existing right-side ports const existingPorts = selectedNode.getPorts(); existingPorts.forEach((port: any) => { if (port.group === 'right') { @@ -43,28 +50,30 @@ const CategoryList: FC = ({ parentName, selectedNode, graphRe } }); - // 计算新的节点高度:基础高度88px + 每个额外port增加30px + // Calculate new node height: base height 88px + 30px for each additional port const baseHeight = 88; - const totalPorts = caseCount + 1; // IF/ELIF + ELSE - const newHeight = baseHeight + (totalPorts - 2) * 30; + const newHeight = baseHeight + (caseCount - 2) * 30; - selectedNode.prop('size', { width: 240, height: newHeight < baseHeight ? baseHeight : newHeight }) + selectedNode.prop('size', { width: nodeWidth, height: newHeight < baseHeight ? baseHeight : newHeight }) - // 添加 分类 端口 + // Add category ports for (let i = 0; i < caseCount; i++) { selectedNode.addPort({ id: `CASE${i + 1}`, group: 'right', - args: portArgs, - attrs: { text: { text: `分类${i + 1}`, fontSize: 12, fill: '#5B6167' } } + args: { + x: nodeWidth, + y: 30 * i + 42, + }, + attrs: { text: { text: `分类${i + 1}`, ...portTextAttrs } } }); } - // 恢复连线 + // Restore edge connections setTimeout(() => { edgeConnections.forEach(({ edge, sourcePortId, targetCellId, targetPortId, sourceCellId, isIncoming }: any) => { graphRef.current?.removeCell(edge); - // 如果是进入连线(左侧端口),直接恢复 + // If it's an incoming connection (left-side port), restore directly if (isIncoming) { const sourceCell = graphRef.current?.getCellById(sourceCellId); if (sourceCell) { @@ -77,22 +86,22 @@ const CategoryList: FC = ({ parentName, selectedNode, graphRe return; } - // 处理右侧端口连线 + // Handle right-side port connections const originalCaseNumber = parseInt(sourcePortId.match(/CASE(\d+)/)?.[1] || '0'); - // 如果是被删除的端口,不重新创建连线 + // If it's a removed port, don't recreate the connection if (removedCaseIndex !== undefined && originalCaseNumber === removedCaseIndex + 1) { return; } let newPortId = sourcePortId; - // 如果删除了某个端口,需要重新映射后续端口的ID + // If a port was removed, remap subsequent port IDs if (removedCaseIndex !== undefined && originalCaseNumber > removedCaseIndex + 1) { newPortId = `CASE${originalCaseNumber - 1}`; } - // 检查新端口是否存在 + // Check if the new port exists const newPorts = selectedNode.getPorts(); const matchingPort = newPorts.find((port: any) => port.id === newPortId); diff --git a/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx b/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx index a6b50e33..ae5aab01 100644 --- a/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx +++ b/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx @@ -1,7 +1,14 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-09 18:35:43 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-09 18:35:43 + */ import { type FC, useRef, useState } from "react"; import { useTranslation } from 'react-i18next' import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input } from 'antd' import { CaretDownOutlined, CaretRightOutlined, SettingOutlined } from '@ant-design/icons'; + import Editor from '../../Editor' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import AuthConfigModal from './AuthConfigModal' @@ -9,6 +16,7 @@ import type { AuthConfigModalRef, HttpRequestConfigForm } from './types' import VariableSelect from "../VariableSelect"; import MessageEditor from '../MessageEditor' import EditableTable from './EditableTable' +import { portTextAttrs } from '../../../constant' const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: any; }> = ({ options, @@ -32,6 +40,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an form.setFieldValue(['body', 'data'], undefined) } + // Handle error handling method change and update node ports accordingly const handleChangeErrorHandleMethod = (method: string) => { form.setFieldsValue({ error_handle: { @@ -42,21 +51,21 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an } }) - // 更新节点连接桩 + // Update node ports console.log('handleChangeErrorHandleMethod', selectedNode, graphRef?.current) if (selectedNode && graphRef?.current) { const existingPorts = selectedNode.getPorts(); const errorPort = existingPorts.find((port: any) => port.id === 'ERROR'); if (method === 'branch' && !errorPort) { - // 添加异常节点连接桩 + // Add error branch port selectedNode.addPort({ id: 'ERROR', group: 'right', - attrs: { text: { text: t('workflow.config.http-request.errorBranch'), fontSize: 12, fill: '#5B6167' }} + attrs: { text: { text: t('workflow.config.http-request.errorBranch'), ...portTextAttrs }} }); } else if (method !== 'branch' && errorPort) { - // 移除异常节点连接桩和相关连线 + // Remove error branch port and related edges const edges = graphRef.current.getEdges().filter((edge: any) => edge.getSourceCellId() === selectedNode.id && edge.getSourcePortId() === 'ERROR' ); diff --git a/web/src/views/Workflow/constant.ts b/web/src/views/Workflow/constant.ts index a01dab9d..23b5ca23 100644 --- a/web/src/views/Workflow/constant.ts +++ b/web/src/views/Workflow/constant.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 15:06:18 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-05 14:15:13 + * @Last Modified time: 2026-02-09 17:48:46 */ import LoopNode from './components/Nodes/LoopNode'; import NormalNode from './components/Nodes/NormalNode'; @@ -518,6 +518,7 @@ export const nodeLibrary: NodeLibrary[] = [ // }, ]; +export const nodeWidth = 240; /** * Node registration library for X6 graph * Maps node shapes to their React components @@ -525,13 +526,13 @@ export const nodeLibrary: NodeLibrary[] = [ export const nodeRegisterLibrary: ReactShapeConfig[] = [ { shape: 'loop-node', - width: 240, + width: nodeWidth, height: 120, component: LoopNode, }, { shape: 'iteration-node', - width: 240, + width: nodeWidth, height: 120, component: LoopNode, }, @@ -543,7 +544,7 @@ export const nodeRegisterLibrary: ReactShapeConfig[] = [ }, { shape: 'condition-node', - width: 240, + width: nodeWidth, height: 88, component: ConditionNode, }, @@ -625,8 +626,9 @@ export const portAttrs = { textAnchor: 'middle', textVerticalAnchor: 'middle', pointerEvents: 'none', - } + }, } +export const portTextAttrs = { fontSize: 12, fill: '#5B6167' } /** * Unified port group configuration @@ -638,6 +640,12 @@ const defaultPortGroups = { // bottom: { position: 'bottom', markup: portMarkup, attrs: portAttrs }, left: { position: 'left', markup: portMarkup, attrs: portAttrs }, } +export const defaultAbsolutePortGroups = { + // top: { position: 'top', markup: portMarkup, attrs: portAttrs }, + right: { position: { name: 'absolute' }, markup: portMarkup, attrs: portAttrs }, + // bottom: { position: 'bottom', markup: portMarkup, attrs: portAttrs }, + left: { position: 'left', markup: portMarkup, attrs: portAttrs }, +} /** * Default port items for standard nodes */ @@ -650,7 +658,7 @@ const defaultPortItems = [ /** * Port position arguments */ -export const portArgs = { dy: 18 } +export const portArgs = { x: nodeWidth, y: 42 } /** * Graph node library configuration @@ -658,7 +666,7 @@ export const portArgs = { dy: 18 } */ export const graphNodeLibrary: Record = { iteration: { - width: 240, + width: nodeWidth, height: 120, shape: 'iteration-node', ports: { @@ -667,7 +675,7 @@ export const graphNodeLibrary: Record = { }, }, loop: { - width: 240, + width: nodeWidth, height: 120, shape: 'loop-node', ports: { @@ -676,33 +684,47 @@ export const graphNodeLibrary: Record = { }, }, 'if-else': { - width: 240, + width: nodeWidth, height: 88, shape: 'condition-node', ports: { - groups: defaultPortGroups, + groups: defaultAbsolutePortGroups, items: [ { group: 'left' }, - { group: 'right', id: 'CASE1', args: portArgs, attrs: { text: { text: 'IF', fontSize: 12, color: '#5B6167' }} }, - { group: 'right', id: 'CASE2', args: portArgs, attrs: { text: { text: 'ELSE', fontSize: 12, color: '#5B6167' }} } + ...(['IF', 'ELSE'].map((text, index) => ({ + group: 'right', + id: `CASE${index}`, + args: { + ...portArgs, + y: 30 * index + 42, + }, + attrs: { text: { text: text, ...portTextAttrs } } + }))), ], }, }, 'question-classifier': { - width: 240, + width: nodeWidth, height: 88, shape: 'condition-node', ports: { - groups: defaultPortGroups, + groups: defaultAbsolutePortGroups, items: [ { group: 'left' }, - { group: 'right', id: 'CASE1', args: portArgs, attrs: { text: { text: '分类1', fontSize: 12, color: '#5B6167' } } }, - { group: 'right', id: 'CASE2', args: portArgs, attrs: { text: { text: '分类2', fontSize: 12, color: '#5B6167' } } } + ...(['分类1', '分类2'].map((text, index) => ({ + group: 'right', + id: `CASE${index}`, + args: { + ...portArgs, + y: 30 * index + 42, + }, + attrs: { text: { text: text, ...portTextAttrs } } + }))), ], }, }, start: { - width: 240, + width: nodeWidth, height: 64, shape: 'normal-node', ports: { @@ -711,7 +733,7 @@ export const graphNodeLibrary: Record = { }, }, end: { - width: 240, + width: nodeWidth, height: 64, shape: 'normal-node', ports: { @@ -738,7 +760,7 @@ export const graphNodeLibrary: Record = { }, }, default: { - width: 240, + width: nodeWidth, height: 64, shape: 'normal-node', ports: { diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts index d267faf8..6042d73c 100644 --- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts +++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 15:17:48 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 15:17:48 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-09 18:37:01 */ import { useRef, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; @@ -12,7 +12,7 @@ import { Graph, Node, MiniMap, Snapline, Clipboard, Keyboard, type Edge } from ' import { register } from '@antv/x6-react-shape'; import type { PortMetadata } from '@antv/x6/lib/model/port'; -import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary, portMarkup, portAttrs, edgeAttrs, edge_color, edge_selected_color, portArgs } from '../constant'; +import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary, portMarkup, portAttrs, edgeAttrs, edge_color, edge_selected_color, portTextAttrs, defaultAbsolutePortGroups, nodeWidth } from '../constant'; import type { WorkflowConfig, NodeProperties, ChatVariable } from '../types'; import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application' @@ -168,6 +168,7 @@ export const useWorkflowGraph = ({ } }) } + const nodeConfig = { ...(graphNodeLibrary[type] ?? graphNodeLibrary.default), id, @@ -179,39 +180,28 @@ export const useWorkflowGraph = ({ // Generate ports dynamically for if-else node based on cases if (type === 'if-else' && config.cases && Array.isArray(config.cases)) { - const caseCount = config.cases.length; - const totalPorts = caseCount + 1; // IF/ELIF + ELSE + const totalPorts = config.cases.length + 1; // IF/ELIF + ELSE const baseHeight = 88; const newHeight = baseHeight + (totalPorts - 2) * 30; const portItems: PortMetadata[] = [ { group: 'left' }, - { group: 'right', id: 'CASE1', args: portArgs, attrs: { text: { text: 'IF', fontSize: 12, fill: '#5B6167' }} } ]; - - // Add ELIF ports - for (let i = 1; i < caseCount; i++) { + // Add IF/ELIF/ELSE ports + for (let i = 0; i < totalPorts; i++) { portItems.push({ group: 'right', id: `CASE${i + 1}`, - args: portArgs, - attrs: { text: { text: 'ELIF', fontSize: 12, fill: '#5B6167' }} + args: { + x: nodeWidth, + y: 30 * i + 42, + }, + attrs: { text: { text: i === 0 ? 'IF' : i === totalPorts - 1 ? 'ELSE' : 'ELIF', ...portTextAttrs } } }); } - // Add ELSE port - portItems.push({ - group: 'right', - id: `CASE${caseCount + 1}`, - args: portArgs, - attrs: { text: { text: 'ELSE', fontSize: 12, fill: '#5B6167' }} - }); - nodeConfig.ports = { - groups: { - right: { position: 'right', markup: portMarkup, attrs: portAttrs }, - left: { position: 'left', markup: portMarkup, attrs: portAttrs }, - }, + groups: defaultAbsolutePortGroups, items: portItems }; @@ -222,7 +212,7 @@ export const useWorkflowGraph = ({ if (type === 'question-classifier' && config.categories && Array.isArray(config.categories)) { const categoryCount = config.categories.length; const baseHeight = 88; - const newHeight = baseHeight + (categoryCount - 1) * 30; + const newHeight = baseHeight + (categoryCount - 2) * 30; const portItems: PortMetadata[] = [ { group: 'left' } @@ -233,16 +223,16 @@ export const useWorkflowGraph = ({ portItems.push({ group: 'right', id: `CASE${index + 1}`, - args: portArgs, - attrs: { text: { text: `分类${index + 1}`, fontSize: 12, fill: '#5B6167' }} + args: { + x: nodeWidth, + y: 30 * index + 42, + }, + attrs: { text: { text: `分类${index + 1}`, ...portTextAttrs }} }); }); nodeConfig.ports = { - groups: { - right: { position: 'right', markup: portMarkup, attrs: portAttrs }, - left: { position: 'left', markup: portMarkup, attrs: portAttrs }, - }, + groups: defaultAbsolutePortGroups, items: portItems }; @@ -259,7 +249,7 @@ export const useWorkflowGraph = ({ items: [ { group: 'left' }, { group: 'right', id: 'right' }, - { group: 'right', id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), fontSize: 12, fill: '#5B6167' }}} + { group: 'right', id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), ...portTextAttrs }}} ] }; }