Merge pull request #374 from SuanmoSuanyangTechnology/feature/workflow_zy

Feature/workflow zy
This commit is contained in:
yingzhao
2026-02-09 18:42:15 +08:00
committed by GitHub
8 changed files with 279 additions and 132 deletions

View File

@@ -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<string, any[]>, 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
}

View File

@@ -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 = (
<div style={{ maxHeight: '300px', overflowY: 'auto', minWidth: '240px' }}>
<div style={{ maxHeight: '300px', overflowY: 'auto', minWidth: `${nodeWidth}px'` }}>
{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'

View File

@@ -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<PortClickHandlerProps> = ({ 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<PortClickHandlerProps> = ({ 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<PortClickHandlerProps> = ({ 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<PortClickHandlerProps> = ({ 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<PortClickHandlerProps> = ({ 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<PortClickHandlerProps> = ({ 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<PortClickHandlerProps> = ({ 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<PortClickHandlerProps> = ({ 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<PortClickHandlerProps> = ({ graph }) => {
}
}, 50);
// 清理临时元素
// Clean up temporary element
if (tempElement) {
document.body.removeChild(tempElement);
setTempElement(null);
@@ -210,7 +217,7 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
};
const content = (
<div style={{ maxHeight: '300px', overflowY: 'auto', minWidth: '240px' }}>
<div style={{ maxHeight: '300px', overflowY: 'auto', minWidth: `${nodeWidth}px` }}>
{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');

View File

@@ -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<CaseListProps> = ({
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<CaseListProps> = ({
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<CaseListProps> = ({
}
});
// 计算新的节点高度基础高度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<CaseListProps> = ({
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<CaseListProps> = ({
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();

View File

@@ -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<CategoryListProps> = ({ 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<CategoryListProps> = ({ 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<CategoryListProps> = ({ 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<CategoryListProps> = ({ 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);

View File

@@ -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'
);

View File

@@ -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<string, NodeConfig> = {
iteration: {
width: 240,
width: nodeWidth,
height: 120,
shape: 'iteration-node',
ports: {
@@ -667,7 +675,7 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
},
},
loop: {
width: 240,
width: nodeWidth,
height: 120,
shape: 'loop-node',
ports: {
@@ -676,33 +684,47 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
},
},
'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<string, NodeConfig> = {
},
},
end: {
width: 240,
width: nodeWidth,
height: 64,
shape: 'normal-node',
ports: {
@@ -738,7 +760,7 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
},
},
default: {
width: 240,
width: nodeWidth,
height: 64,
shape: 'normal-node',
ports: {

View File

@@ -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 }}}
]
};
}