This commit is contained in:
dengqichen 2025-10-22 13:33:37 +08:00
parent 56338dc3a8
commit 313307a19d
8 changed files with 790 additions and 33 deletions

View File

@ -0,0 +1,256 @@
import React, { useState, useRef, useMemo, useEffect } from 'react';
import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/components/ui/command';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import type { VariableInputProps } from './types';
import { detectTrigger, insertVariable, filterVariables } from './utils';
import { collectNodeVariables, groupVariablesByNode } from '@/utils/workflow/collectNodeVariables';
/**
* ${}
*/
const highlightVariables = (text: string): React.ReactNode[] => {
const regex = /\$\{([^}]+)\}/g;
const parts: React.ReactNode[] = [];
let lastIndex = 0;
let match;
while ((match = regex.exec(text)) !== null) {
// 添加变量前的文本
if (match.index > lastIndex) {
parts.push(text.substring(lastIndex, match.index));
}
// 添加高亮的变量
parts.push(
<span
key={match.index}
className="inline-block px-1.5 py-0.5 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded font-mono text-sm border border-blue-200 dark:border-blue-800"
>
${`{${match[1]}}`}
</span>
);
lastIndex = match.index + match[0].length;
}
// 添加剩余文本
if (lastIndex < text.length) {
parts.push(text.substring(lastIndex));
}
return parts.length > 0 ? parts : [text];
};
/**
*
* ${
*/
const VariableInput: React.FC<VariableInputProps> = ({
value = '',
onChange,
allNodes,
allEdges,
currentNodeId,
variant = 'input',
placeholder,
disabled = false,
rows = 3,
}) => {
const [showSuggestions, setShowSuggestions] = useState(false);
const [triggerInfo, setTriggerInfo] = useState({ shouldShow: false, startPos: -1, searchText: '' });
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
// 点击外部关闭建议框
useEffect(() => {
if (!showSuggestions) return;
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setShowSuggestions(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [showSuggestions]);
// 收集可用变量
const allVariables = useMemo(() => {
return collectNodeVariables(currentNodeId, allNodes, allEdges);
}, [currentNodeId, allNodes, allEdges]);
// 根据搜索文本过滤变量
const filteredVariables = useMemo(() => {
return filterVariables(allVariables, triggerInfo.searchText);
}, [allVariables, triggerInfo.searchText]);
// 按节点分组
const groupedVariables = useMemo(() => {
return groupVariablesByNode(filteredVariables);
}, [filteredVariables]);
// 处理输入变化
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const newValue = e.target.value;
const cursorPos = e.target.selectionStart || 0;
onChange(newValue);
// 检测是否应该触发变量建议
const trigger = detectTrigger(newValue, cursorPos);
setTriggerInfo(trigger);
setShowSuggestions(trigger.shouldShow && !disabled);
};
// 处理键盘事件
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
// Esc 键关闭建议框
if (e.key === 'Escape' && showSuggestions) {
setShowSuggestions(false);
e.preventDefault();
}
};
// 插入变量
const handleSelectVariable = (nodeName: string, fieldName: string) => {
console.log('🎯 handleSelectVariable 被调用:', { nodeName, fieldName, value, triggerInfo });
if (!inputRef.current) {
console.error('❌ inputRef.current 不存在');
return;
}
const cursorPos = inputRef.current.selectionStart || 0;
console.log('📍 当前光标位置:', cursorPos);
// 使用工具函数插入变量
const { newText, newCursorPos } = insertVariable(
value,
cursorPos,
triggerInfo.startPos,
nodeName,
fieldName
);
console.log('✅ 插入结果:', { newText, newCursorPos });
// 更新值
onChange(newText);
// 关闭建议框
setShowSuggestions(false);
// 恢复焦点并设置光标位置
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus();
inputRef.current.setSelectionRange(newCursorPos, newCursorPos);
console.log('✅ 焦点已恢复,光标已设置');
}
}, 0);
};
// 渲染输入框
const renderInput = () => {
const commonProps = {
ref: inputRef as any,
value,
onChange: handleChange,
onKeyDown: handleKeyDown,
placeholder,
disabled,
};
if (variant === 'input') {
return <Input {...commonProps} />;
}
return <Textarea {...commonProps} rows={rows} />;
};
// 渲染变量提示
const renderSuggestions = () => {
if (allVariables.length === 0) {
return (
<div className="p-4 text-center text-sm text-muted-foreground">
</div>
);
}
if (filteredVariables.length === 0) {
return (
<div className="p-4 text-center text-sm text-muted-foreground">
</div>
);
}
return (
<div className="max-h-80 overflow-y-auto">
{Array.from(groupedVariables.entries()).map(([nodeName, variables]) => (
<div key={nodeName} className="py-2">
<div className="px-3 py-1 text-xs font-semibold text-muted-foreground">
📦 {nodeName}
</div>
<div className="space-y-0.5">
{variables.map((variable) => (
<div
key={`${variable.nodeId}.${variable.fieldName}`}
className="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-accent rounded-sm transition-colors"
onClick={() => {
console.log('点击变量:', variable.nodeName, variable.fieldName);
handleSelectVariable(variable.nodeName, variable.fieldName);
}}
>
<span className="font-mono text-sm">{variable.fieldName}</span>
<span className="ml-auto text-xs text-muted-foreground">{variable.fieldType}</span>
</div>
))}
</div>
</div>
))}
</div>
);
};
// 检测是否有变量需要高亮
const hasVariables = value && /\$\{[^}]+\}/.test(value);
return (
<div ref={containerRef} className="relative space-y-2">
{/* 输入框 */}
{renderInput()}
{/* 变量高亮预览 */}
{hasVariables && !showSuggestions && (
<div className="p-2 bg-muted/30 rounded-md border border-border text-sm">
<div className="text-xs text-muted-foreground mb-1"></div>
<div className="break-words leading-relaxed">
{highlightVariables(value)}
</div>
</div>
)}
{/* 变量提示弹窗 - 绝对定位 */}
{showSuggestions && (
<div
className="absolute top-full left-0 w-full mt-2 z-50"
>
<div className="w-full max-w-[500px] bg-popover text-popover-foreground rounded-md border shadow-lg">
{renderSuggestions()}
<div className="px-3 py-2 border-t text-xs text-muted-foreground bg-muted/30">
💡 <code className="px-1 py-0.5 bg-background rounded">${'{'}</code>
<kbd className="px-1 py-0.5 bg-background rounded border">Esc</kbd>
</div>
</div>
</div>
)}
</div>
);
};
export default VariableInput;

View File

@ -0,0 +1,48 @@
import type { FlowNode, FlowEdge } from '@/pages/Workflow/Design/types';
/**
* Props
*/
export interface VariableInputProps {
/** 输入值(显示名称格式)*/
value: string;
/** 值变化回调 */
onChange: (value: string) => void;
/** 所有节点(用于收集变量)*/
allNodes: FlowNode[];
/** 所有连接边(用于确定前序节点)*/
allEdges: FlowEdge[];
/** 当前节点ID用于过滤前序节点*/
currentNodeId: string;
/** 渲染类型 */
variant?: 'input' | 'textarea';
/** 占位符 */
placeholder?: string;
/** 是否禁用 */
disabled?: boolean;
/** Textarea 行数 */
rows?: number;
}
/**
*
*/
export interface TriggerInfo {
/** 是否应该显示变量提示 */
shouldShow: boolean;
/** ${ 的起始位置 */
startPos: number;
/** 搜索文本(${ 之后的文本)*/
searchText: string;
}

View File

@ -0,0 +1,95 @@
import type { TriggerInfo } from './types';
import type { NodeVariable } from '@/utils/workflow/collectNodeVariables';
/**
*
*
* @param text
* @param cursorPos
* @returns
*/
export const detectTrigger = (text: string, cursorPos: number): TriggerInfo => {
// 获取光标前的文本
const beforeCursor = text.substring(0, cursorPos);
// 找到最后一个 ${
const lastOpenIndex = beforeCursor.lastIndexOf('${');
// 如果没有找到 ${或者找到的位置在很早之前超过100个字符不触发
if (lastOpenIndex === -1 || (cursorPos - lastOpenIndex) > 100) {
return { shouldShow: false, startPos: -1, searchText: '' };
}
// 获取 ${ 之后到光标的文本
const textAfterOpen = beforeCursor.substring(lastOpenIndex + 2);
// 如果这段文本中已经有 },说明表达式已经闭合,不触发
if (textAfterOpen.includes('}')) {
return { shouldShow: false, startPos: -1, searchText: '' };
}
// 返回触发信息
return {
shouldShow: true,
startPos: lastOpenIndex,
searchText: textAfterOpen.trim(),
};
};
/**
*
*
* @param originalText
* @param cursorPos
* @param triggerStartPos ${
* @param nodeName
* @param fieldName
* @returns
*/
export const insertVariable = (
originalText: string,
cursorPos: number,
triggerStartPos: number,
nodeName: string,
fieldName: string
): { newText: string; newCursorPos: number } => {
// ${ 之前的文本
const before = originalText.substring(0, triggerStartPos);
// 光标之后的文本
const after = originalText.substring(cursorPos);
// 构建新文本before + ${nodeName.fieldName} + after
const variableText = `\${${nodeName}.${fieldName}}`;
const newText = `${before}${variableText}${after}`;
// 计算新的光标位置(在 } 之后)
const newCursorPos = before.length + variableText.length;
return { newText, newCursorPos };
};
/**
*
*
* @param variables
* @param searchText
* @returns
*/
export const filterVariables = (variables: NodeVariable[], searchText: string): NodeVariable[] => {
if (!searchText) {
return variables;
}
const lowerSearch = searchText.toLowerCase();
return variables.filter(v => {
// 匹配节点名称或字段名
const matchesNodeName = v.nodeName?.toLowerCase().includes(lowerSearch);
const matchesFieldName = v.fieldName?.toLowerCase().includes(lowerSearch);
const matchesFullText = v.fullText?.toLowerCase().includes(lowerSearch);
return matchesNodeName || matchesFieldName || matchesFullText;
});
};

View File

@ -6,14 +6,17 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
import { Button } from '@/components/ui/button';
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { useToast } from '@/components/ui/use-toast';
import type { FlowEdge } from '../types';
import type { FlowEdge, FlowNode } from '../types';
import { convertToUUID, convertToDisplayName } from '@/utils/workflow/variableConversion';
import VariableInput from '@/components/VariableInput';
interface EdgeConfigModalProps {
visible: boolean;
edge: FlowEdge | null;
allNodes: FlowNode[];
allEdges: FlowEdge[];
onOk: (edgeId: string, condition: EdgeCondition) => void;
onCancel: () => void;
}
@ -53,6 +56,8 @@ type EdgeConditionFormValues = z.infer<typeof edgeConditionSchema>;
const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
visible,
edge,
allNodes,
allEdges,
onOk,
onCancel
}) => {
@ -72,22 +77,36 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
useEffect(() => {
if (visible && edge) {
const condition = edge.data?.condition;
// 转换 expression 中的 UUID 为显示名称
const displayExpression = condition?.expression
? convertToDisplayName(condition.expression, allNodes)
: '';
const values = {
type: (condition?.type || 'EXPRESSION') as 'EXPRESSION' | 'DEFAULT',
expression: condition?.expression || '',
expression: displayExpression,
priority: condition?.priority || 10,
};
form.reset(values);
setConditionType(values.type);
}
}, [visible, edge, form]);
// 只在 visible 或 edge.id 改变时重置表单
// allNodes 改变不应该触发重置(用户正在编辑时)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible, edge?.id]);
const handleSubmit = (values: EdgeConditionFormValues) => {
if (!edge) return;
// 转换 expression 中的显示名称为 UUID
const uuidExpression = values.expression
? convertToUUID(values.expression, allNodes)
: '';
// 检查表达式是否包含变量引用
if (values.type === 'EXPRESSION' && values.expression) {
const hasVariable = /\$\{[\w.]+\}/.test(values.expression);
if (values.type === 'EXPRESSION' && uuidExpression) {
const hasVariable = /\$\{[\w.-]+\}/.test(uuidExpression);
if (!hasVariable) {
toast({
title: '提示',
@ -96,7 +115,11 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
}
}
onOk(edge.id, values);
// 提交 UUID 格式的数据
onOk(edge.id, {
...values,
expression: uuidExpression
});
handleClose();
};
@ -173,14 +196,19 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Textarea
placeholder="请输入条件表达式,如:${amount} > 1000"
<VariableInput
value={field.value || ''}
onChange={field.onChange}
allNodes={allNodes}
allEdges={allEdges}
currentNodeId={edge?.source || ''}
variant="textarea"
placeholder="请输入条件表达式,如:${Jenkins构建.buildStatus} == 'SUCCESS'"
rows={4}
{...field}
/>
</FormControl>
<FormDescription>
使 {'${变量名}'} {'${amount} > 1000'}
使 {'${节点名称.字段名}'} ${'{'}
</FormDescription>
<FormMessage />
</FormItem>

View File

@ -10,15 +10,19 @@ import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { useToast } from '@/components/ui/use-toast';
import type { FlowNode, FlowNodeData } from '../types';
import type { FlowNode, FlowNodeData, FlowEdge } from '../types';
import type { WorkflowNodeDefinition, JSONSchema } from '../nodes/types';
import { isConfigurableNode } from '../nodes/types';
import { convertJsonSchemaToZod, extractDataSourceTypes } from '../utils/schemaConverter';
import { loadDataSource, DataSourceType, type DataSourceOption } from '../utils/dataSourceLoader';
import { convertObjectToUUID, convertObjectToDisplayName } from '@/utils/workflow/variableConversion';
import VariableInput from '@/components/VariableInput';
interface NodeConfigModalProps {
visible: boolean;
node: FlowNode | null;
allNodes: FlowNode[];
allEdges: FlowEdge[];
onCancel: () => void;
onOk: (nodeId: string, updatedData: Partial<FlowNodeData>) => void;
}
@ -26,6 +30,8 @@ interface NodeConfigModalProps {
const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
visible,
node,
allNodes,
allEdges,
onCancel,
onOk
}) => {
@ -116,14 +122,27 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
nodeCode: nodeDefinition.nodeCode,
description: nodeDefinition.description || ''
};
configForm.reset({ ...defaultConfig, ...(nodeData.configs || {}) });
// 设置输入映射默认值
// 转换 UUID 格式为显示名称格式
const displayConfigs = convertObjectToDisplayName(
{ ...defaultConfig, ...(nodeData.configs || {}) },
allNodes
);
configForm.reset(displayConfigs);
// 设置输入映射默认值(也需要转换)
if (isConfigurableNode(nodeDefinition)) {
inputForm.reset(nodeData.inputMapping || {});
const displayInputMapping = convertObjectToDisplayName(
nodeData.inputMapping || {},
allNodes
);
inputForm.reset(displayInputMapping);
}
}
}, [visible, node, nodeDefinition, configForm, inputForm]);
// 只在 visible 或 node.id 改变时重置表单
// allNodes 改变不应该触发重置(用户正在编辑时)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible, node?.id]);
// ✅ 保存配置
const handleSave = () => {
@ -139,11 +158,15 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
inputData = inputForm.getValues();
}
// 转换显示名称格式为 UUID 格式
const uuidConfigs = convertObjectToUUID(configData as Record<string, any>, allNodes);
const uuidInputMapping = convertObjectToUUID(inputData as Record<string, any>, allNodes);
// 构建更新数据
const updatedData: Partial<FlowNodeData> = {
label: (configData as any).nodeName || nodeDefinition.nodeName,
configs: configData as Record<string, any>,
inputMapping: inputData,
configs: uuidConfigs,
inputMapping: uuidInputMapping,
outputs: isConfigurableNode(nodeDefinition) ? nodeDefinition.outputs || [] : []
};
@ -244,7 +267,14 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
// 根据类型渲染
switch (prop.type) {
case 'string':
case 'string': {
// 黑名单字段:不使用变量输入
const blacklist = ['nodeName', 'nodeCode', 'description'];
const isBlacklisted = blacklist.includes(key);
const isPassword = prop.format === 'password';
// 密码字段或黑名单字段:使用普通输入
if (isPassword || isBlacklisted) {
if (prop.format === 'textarea' || key.includes('description')) {
return (
<Textarea
@ -258,11 +288,31 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
return (
<Input
{...field}
type={prop.format === 'password' ? 'password' : 'text'}
type={isPassword ? 'password' : 'text'}
disabled={loading}
placeholder={prop.description || `请输入${prop.title || ''}`}
/>
);
}
// 其他字符串字段:使用 VariableInput
const isTextarea = prop.format === 'textarea' ||
['content', 'body', 'expression', 'script'].includes(key);
return (
<VariableInput
value={field.value || ''}
onChange={field.onChange}
allNodes={allNodes}
allEdges={allEdges}
currentNodeId={node?.id || ''}
variant={isTextarea ? 'textarea' : 'input'}
placeholder={prop.description || `请输入${prop.title || ''}`}
disabled={loading}
rows={3}
/>
);
}
case 'number':
case 'integer':
@ -299,7 +349,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
/>
);
}
}, [dataSourceCache, loadingDataSources, loading]);
}, [dataSourceCache, loadingDataSources, loading, node, allNodes, allEdges]);
// ✅ 渲染表单字段
const renderFormFields = useCallback((

View File

@ -525,6 +525,8 @@ const WorkflowDesignInner: React.FC = () => {
<NodeConfigModal
visible={configModalVisible}
node={configNode}
allNodes={getNodes() as FlowNode[]}
allEdges={getEdges() as FlowEdge[]}
onCancel={handleCloseConfigModal}
onOk={handleNodeConfigUpdate}
/>
@ -533,6 +535,8 @@ const WorkflowDesignInner: React.FC = () => {
<EdgeConfigModal
visible={edgeConfigModalVisible}
edge={configEdge}
allNodes={getNodes() as FlowNode[]}
allEdges={getEdges() as FlowEdge[]}
onOk={handleEdgeConditionUpdate}
onCancel={handleCloseEdgeConfigModal}
/>

View File

@ -0,0 +1,120 @@
import type { FlowNode, FlowEdge } from '@/pages/Workflow/Design/types';
import { isConfigurableNode } from '@/pages/Workflow/Design/nodes/types';
/**
*
*/
export interface NodeVariable {
nodeId: string; // 节点UUID
nodeName: string; // 节点显示名称
fieldName: string; // 字段名
fieldType: string; // 字段类型
displayText: string; // 界面显示格式: ${节点名称.字段名}
fullText: string; // 完整文本格式: 节点名称.字段名(用于搜索)
}
/**
* DFS反向遍历
*
* @param currentNodeId ID
* @param allNodes
* @param allEdges
* @returns START节点
*/
export const getAllPrecedingNodes = (
currentNodeId: string,
allNodes: FlowNode[],
allEdges: FlowEdge[]
): FlowNode[] => {
const precedingNodeIds = new Set<string>();
const visited = new Set<string>();
// DFS 反向遍历
const dfs = (nodeId: string) => {
if (visited.has(nodeId)) return;
visited.add(nodeId);
// 找到所有指向当前节点的边
const incomingEdges = allEdges.filter(e => e.target === nodeId);
incomingEdges.forEach(edge => {
precedingNodeIds.add(edge.source);
dfs(edge.source); // 递归向上遍历
});
};
dfs(currentNodeId);
// 返回节点对象排除START节点它没有输出
return allNodes.filter(node =>
precedingNodeIds.has(node.id) &&
node.data?.nodeType !== 'START_EVENT'
);
};
/**
*
*
* @param currentNodeId ID
* @param allNodes
* @param allEdges
* @returns
*/
export const collectNodeVariables = (
currentNodeId: string,
allNodes: FlowNode[],
allEdges: FlowEdge[]
): NodeVariable[] => {
const variables: NodeVariable[] = [];
// 获取所有前序节点
const precedingNodes = getAllPrecedingNodes(currentNodeId, allNodes, allEdges);
// 遍历前序节点,提取输出字段
precedingNodes.forEach(node => {
const nodeDefinition = node.data?.nodeDefinition;
// 检查节点是否有输出定义
if (!nodeDefinition || !isConfigurableNode(nodeDefinition) || !nodeDefinition.outputs) {
return;
}
// 获取节点显示名称优先使用用户自定义的label
const nodeName = node.data.label || nodeDefinition.nodeName || 'Unknown';
// 为每个输出字段创建变量
nodeDefinition.outputs.forEach(output => {
variables.push({
nodeId: node.id,
nodeName: nodeName,
fieldName: output.name,
fieldType: output.type,
displayText: `\${${nodeName}.${output.name}}`,
fullText: `${nodeName}.${output.name}`,
});
});
});
return variables;
};
/**
*
*
* @param variables
* @returns Map<节点名称, 变量列表>
*/
export const groupVariablesByNode = (variables: NodeVariable[]): Map<string, NodeVariable[]> => {
const groups = new Map<string, NodeVariable[]>();
variables.forEach(variable => {
const key = variable.nodeName;
if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key)!.push(variable);
});
return groups;
};

View File

@ -0,0 +1,156 @@
import type { FlowNode } from '@/pages/Workflow/Design/types';
/**
*
* ${xxx.yyy}
* xxx: 节点名称或UUID
* yyy: 字段名
*/
const VARIABLE_PATTERN = /\$\{([^.}]+)\.([^}]+)\}/g;
/**
* UUID格式
* ${.} ${UUID.}
*
* @param displayText
* @param allNodes UUID
* @returns UUID格式文本
*
* @example
* convertToUUID("构建${Jenkins构建.buildStatus}完成", allNodes)
* // 返回: "构建${uuid-123.buildStatus}完成"
*/
export const convertToUUID = (
displayText: string,
allNodes: FlowNode[]
): string => {
if (!displayText || typeof displayText !== 'string') {
return displayText;
}
return displayText.replace(VARIABLE_PATTERN, (match, nodeNameOrId, fieldName) => {
// 尝试通过名称查找节点
const nodeByName = allNodes.find(n => {
const nodeName = n.data?.label || n.data?.nodeDefinition?.nodeName;
return nodeName === nodeNameOrId;
});
if (nodeByName) {
// 找到了替换为UUID
return `\${${nodeByName.id}.${fieldName}}`;
}
// 可能已经是UUID格式或者节点已被删除
// 保持原样
return match;
});
};
/**
* UUID格式转换为显示名称格式
* ${UUID.} ${.}
*
* @param uuidText UUID的文本
* @param allNodes
* @returns
*
* @example
* convertToDisplayName("构建${uuid-123.buildStatus}完成", allNodes)
* // 返回: "构建${Jenkins构建.buildStatus}完成"
*/
export const convertToDisplayName = (
uuidText: string,
allNodes: FlowNode[]
): string => {
if (!uuidText || typeof uuidText !== 'string') {
return uuidText;
}
return uuidText.replace(VARIABLE_PATTERN, (match, nodeIdOrName, fieldName) => {
// 尝试通过UUID查找节点
const nodeById = allNodes.find(n => n.id === nodeIdOrName);
if (nodeById) {
// 找到了,替换为显示名称
const nodeName = nodeById.data?.label || nodeById.data?.nodeDefinition?.nodeName || nodeIdOrName;
return `\${${nodeName}.${fieldName}}`;
}
// 可能已经是显示名称格式,或者节点已被删除
// 保持原样
return match;
});
};
/**
* UUID
*
*
* @param obj
* @param allNodes
* @returns
*/
export const convertObjectToUUID = <T extends Record<string, any>>(
obj: T,
allNodes: FlowNode[]
): T => {
if (!obj || typeof obj !== 'object') {
return obj;
}
const result: any = Array.isArray(obj) ? [] : {};
for (const key in obj) {
const value = obj[key];
if (typeof value === 'string') {
// 字符串类型:进行转换
result[key] = convertToUUID(value, allNodes);
} else if (value && typeof value === 'object') {
// 对象或数组:递归转换
result[key] = convertObjectToUUID(value, allNodes);
} else {
// 其他类型:保持原样
result[key] = value;
}
}
return result as T;
};
/**
* UUID到显示名称
*
*
* @param obj
* @param allNodes
* @returns
*/
export const convertObjectToDisplayName = <T extends Record<string, any>>(
obj: T,
allNodes: FlowNode[]
): T => {
if (!obj || typeof obj !== 'object') {
return obj;
}
const result: any = Array.isArray(obj) ? [] : {};
for (const key in obj) {
const value = obj[key];
if (typeof value === 'string') {
// 字符串类型:进行转换
result[key] = convertToDisplayName(value, allNodes);
} else if (value && typeof value === 'object') {
// 对象或数组:递归转换
result[key] = convertObjectToDisplayName(value, allNodes);
} else {
// 其他类型:保持原样
result[key] = value;
}
}
return result as T;
};