1
This commit is contained in:
parent
541a7f821a
commit
337320bfcd
@ -173,7 +173,10 @@ const VariableInput: React.FC<VariableInputProps> = ({
|
||||
inputElement.removeEventListener('scroll', syncScroll);
|
||||
}
|
||||
};
|
||||
}, [value, variant]);
|
||||
// ✅ 性能优化:移除 value 依赖,避免每次输入都重新同步样式
|
||||
// ResizeObserver 会自动处理元素大小变化
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [variant]);
|
||||
|
||||
// 监听滚动和窗口大小变化,更新下拉框位置
|
||||
useEffect(() => {
|
||||
|
||||
@ -4,9 +4,18 @@ import type { NodeVariable } from '@/utils/workflow/collectNodeVariables';
|
||||
/**
|
||||
* 检测是否应该触发变量建议
|
||||
*
|
||||
* @param text 完整文本
|
||||
* @param cursorPos 光标位置
|
||||
* @returns 触发信息
|
||||
* 检测逻辑:
|
||||
* 1. 查找光标前最后一个 ${ 的位置
|
||||
* 2. 确保 ${ 和光标之间没有 }(未闭合)
|
||||
* 3. 确保距离不超过 100 个字符(避免过长的搜索)
|
||||
*
|
||||
* @param text 完整文本内容
|
||||
* @param cursorPos 光标位置(从 0 开始)
|
||||
* @returns 触发信息(包含是否显示、起始位置、搜索文本)
|
||||
*
|
||||
* @example
|
||||
* detectTrigger("Hello ${jen", 11)
|
||||
* // => { shouldShow: true, startPos: 6, searchText: "jen" }
|
||||
*/
|
||||
export const detectTrigger = (text: string, cursorPos: number): TriggerInfo => {
|
||||
// 获取光标前的文本
|
||||
@ -39,12 +48,18 @@ export const detectTrigger = (text: string, cursorPos: number): TriggerInfo => {
|
||||
/**
|
||||
* 插入变量到文本中
|
||||
*
|
||||
* @param originalText 原始文本
|
||||
* @param cursorPos 光标位置
|
||||
* @param triggerStartPos ${ 的起始位置
|
||||
* @param nodeName 节点名称
|
||||
* @param fieldName 字段名
|
||||
* @returns 新的文本和光标位置
|
||||
* 将用户选择的变量插入到文本中,替换 ${ 和光标之间的部分
|
||||
*
|
||||
* @param originalText 原始文本内容
|
||||
* @param cursorPos 当前光标位置
|
||||
* @param triggerStartPos ${ 符号的起始位置
|
||||
* @param nodeName 节点显示名称(用于生成 ${nodeName.fieldName})
|
||||
* @param fieldName 字段名称
|
||||
* @returns 包含新文本和新光标位置的对象
|
||||
*
|
||||
* @example
|
||||
* insertVariable("Hello ${jen", 11, 6, "Jenkins构建", "buildNumber")
|
||||
* // => { newText: "Hello ${Jenkins构建.buildNumber}", newCursorPos: 31 }
|
||||
*/
|
||||
export const insertVariable = (
|
||||
originalText: string,
|
||||
|
||||
@ -50,10 +50,12 @@ const CustomEdge: React.FC<EdgeProps> = ({
|
||||
transition: 'all 0.2s ease',
|
||||
};
|
||||
|
||||
// ✅ 修复:确保 markerEnd 是对象后再 spread
|
||||
const markerEndStyle = (() => {
|
||||
if (!markerEnd || typeof markerEnd !== 'object') return markerEnd;
|
||||
if (selected) return { ...markerEnd, color: '#3b82f6' };
|
||||
if (isHovered) return { ...markerEnd, color: '#64748b' };
|
||||
const markerObj = markerEnd as Record<string, any>;
|
||||
if (selected) return { ...markerObj, color: '#3b82f6' };
|
||||
if (isHovered) return { ...markerObj, color: '#64748b' };
|
||||
return markerEnd;
|
||||
})();
|
||||
|
||||
@ -63,7 +65,7 @@ const CustomEdge: React.FC<EdgeProps> = ({
|
||||
id={id}
|
||||
path={edgePath}
|
||||
style={edgeStyle}
|
||||
markerEnd={markerEndStyle}
|
||||
markerEnd={markerEndStyle as any}
|
||||
/>
|
||||
|
||||
{/* 增加点击区域 */}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Save, RotateCcw } from 'lucide-react';
|
||||
@ -46,6 +46,16 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
const [dataSourceCache, setDataSourceCache] = useState<Record<string, DataSourceOption[]>>({});
|
||||
const [loadingDataSources, setLoadingDataSources] = useState(false);
|
||||
|
||||
// ✅ 性能优化:使用 ref 保存 allNodes 和 allEdges 的最新引用
|
||||
// 避免这些频繁变化的数组导致 useCallback 重建
|
||||
const allNodesRef = useRef<FlowNode[]>(allNodes);
|
||||
const allEdgesRef = useRef<FlowEdge[]>(allEdges);
|
||||
|
||||
useEffect(() => {
|
||||
allNodesRef.current = allNodes;
|
||||
allEdgesRef.current = allEdges;
|
||||
}, [allNodes, allEdges]);
|
||||
|
||||
// 获取节点定义
|
||||
const nodeDefinition: WorkflowNodeDefinition | null = node?.data?.nodeDefinition || null;
|
||||
|
||||
@ -77,24 +87,34 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
}
|
||||
|
||||
setLoadingDataSources(true);
|
||||
try {
|
||||
const cache: Record<string, DataSourceOption[]> = {};
|
||||
await Promise.all(
|
||||
inputTypes.map(async (type) => {
|
||||
|
||||
const cache: Record<string, DataSourceOption[]> = {};
|
||||
let failedCount = 0;
|
||||
|
||||
// ✅ 改进:单独处理每个数据源的加载,避免一个失败影响全部
|
||||
await Promise.allSettled(
|
||||
inputTypes.map(async (type) => {
|
||||
try {
|
||||
const data = await loadDataSource(type as DataSourceType);
|
||||
cache[type] = data;
|
||||
})
|
||||
);
|
||||
setDataSourceCache(cache);
|
||||
} catch (error) {
|
||||
console.error('加载动态数据源失败:', error);
|
||||
} catch (error) {
|
||||
console.error(`加载数据源 ${type} 失败:`, error);
|
||||
failedCount++;
|
||||
cache[type] = []; // 失败时使用空数组
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
setDataSourceCache(cache);
|
||||
setLoadingDataSources(false);
|
||||
|
||||
// 如果有失败的数据源,显示提示
|
||||
if (failedCount > 0) {
|
||||
toast({
|
||||
title: '数据加载失败',
|
||||
description: '无法加载部分选项数据,请刷新重试',
|
||||
title: '部分数据加载失败',
|
||||
description: `${failedCount} 个选项数据加载失败,请检查网络后重新打开节点配置`,
|
||||
variant: 'destructive'
|
||||
});
|
||||
} finally {
|
||||
setLoadingDataSources(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -212,59 +232,58 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
// ✅ 通用 Select 渲染函数(减少代码重复)
|
||||
const renderSelect = useCallback((
|
||||
options: Array<{ label: string; value: any }>,
|
||||
field: any,
|
||||
prop: any,
|
||||
disabled: boolean,
|
||||
onValueChange?: (value: string) => void
|
||||
) => (
|
||||
<Select
|
||||
disabled={disabled}
|
||||
value={field.value?.toString()}
|
||||
onValueChange={onValueChange || field.onChange}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={`请选择${prop.title || ''}`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
), []);
|
||||
|
||||
// ✅ 渲染字段控件
|
||||
const renderFieldControl = useCallback((prop: any, field: any, key: string) => {
|
||||
// 动态数据源
|
||||
if (prop['x-dataSource']) {
|
||||
const options = dataSourceCache[prop['x-dataSource']] || [];
|
||||
return (
|
||||
<Select
|
||||
disabled={loadingDataSources || loading}
|
||||
value={field.value?.toString()}
|
||||
onValueChange={(value) => {
|
||||
// 如果是数字类型,转换为数字
|
||||
const numValue = Number(value);
|
||||
field.onChange(isNaN(numValue) ? value : numValue);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={`请选择${prop.title || ''}`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
return renderSelect(
|
||||
options,
|
||||
field,
|
||||
prop,
|
||||
loadingDataSources || loading,
|
||||
(value) => {
|
||||
// 如果是数字类型,转换为数字
|
||||
const numValue = Number(value);
|
||||
field.onChange(isNaN(numValue) ? value : numValue);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 静态枚举值
|
||||
if (prop.enum && Array.isArray(prop.enum)) {
|
||||
return (
|
||||
<Select
|
||||
disabled={loading}
|
||||
value={field.value?.toString()}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={`请选择${prop.title || ''}`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{prop.enum.map((value: any, index: number) => {
|
||||
const enumValue = typeof value === 'object' ? value.value : value;
|
||||
const enumLabel = typeof value === 'object' ? value.label : (prop.enumNames?.[index] || value);
|
||||
return (
|
||||
<SelectItem key={enumValue} value={enumValue.toString()}>
|
||||
{enumLabel}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
const options = prop.enum.map((value: any, index: number) => {
|
||||
const enumValue = typeof value === 'object' ? value.value : value;
|
||||
const enumLabel = typeof value === 'object' ? value.label : (prop.enumNames?.[index] || value);
|
||||
return { label: enumLabel, value: enumValue };
|
||||
});
|
||||
return renderSelect(options, field, prop, loading);
|
||||
}
|
||||
|
||||
// 根据类型渲染
|
||||
@ -305,8 +324,8 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
<VariableInput
|
||||
value={field.value || ''}
|
||||
onChange={field.onChange}
|
||||
allNodes={allNodes}
|
||||
allEdges={allEdges}
|
||||
allNodes={allNodesRef.current}
|
||||
allEdges={allEdgesRef.current}
|
||||
currentNodeId={node?.id || ''}
|
||||
variant={isTextarea ? 'textarea' : 'input'}
|
||||
placeholder={prop.description || `请输入${prop.title || ''}`}
|
||||
@ -351,7 +370,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
/>
|
||||
);
|
||||
}
|
||||
}, [dataSourceCache, loadingDataSources, loading, node, allNodes, allEdges]);
|
||||
}, [dataSourceCache, loadingDataSources, loading, node, renderSelect]);
|
||||
|
||||
// ✅ 渲染表单字段
|
||||
const renderFormFields = useCallback((
|
||||
@ -437,7 +456,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
<p className="text-xs text-muted-foreground">
|
||||
节点的唯一标识符(只读)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 节点名称 */}
|
||||
<div className="space-y-2">
|
||||
@ -453,7 +472,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
<p className="text-xs text-muted-foreground">
|
||||
节点在流程图中显示的名称
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 节点描述 */}
|
||||
<div className="space-y-2">
|
||||
@ -470,7 +489,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
<p className="text-xs text-muted-foreground">
|
||||
节点的详细说明
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
|
||||
@ -39,7 +39,6 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
|
||||
},
|
||||
|
||||
// 基本配置Schema(仅业务配置,元数据已移至固定表单)
|
||||
configSchema: undefined,
|
||||
inputMappingSchema: {
|
||||
type: "object",
|
||||
title: "输入",
|
||||
|
||||
@ -39,7 +39,6 @@ export const NotificationNodeDefinition: ConfigurableNodeDefinition = {
|
||||
},
|
||||
|
||||
// 基本配置Schema(仅业务配置,元数据已移至固定表单)
|
||||
configSchema: undefined,
|
||||
inputMappingSchema: {
|
||||
type: "object",
|
||||
title: "输入",
|
||||
@ -60,7 +59,7 @@ export const NotificationNodeDefinition: ConfigurableNodeDefinition = {
|
||||
// 'x-dataSource': DataSourceType.NOTIFICATION_CHANNEL_TYPES
|
||||
// },
|
||||
notificationChannel: {
|
||||
type: "string",
|
||||
type: "number",
|
||||
title: "通知渠道",
|
||||
description: "选择通知发送的渠道",
|
||||
'x-dataSource': DataSourceType.NOTIFICATION_CHANNELS
|
||||
|
||||
@ -141,7 +141,6 @@ export interface BaseNodeDefinition {
|
||||
category: NodeCategory;
|
||||
description: string;
|
||||
renderConfig: RenderConfig; // ✨ 渲染配置(配置驱动)
|
||||
configSchema: JSONSchema; // 基本配置Schema(包含基本信息+节点配置)
|
||||
}
|
||||
|
||||
// 可配置节点定义(有3个面板:基本配置、输入映射、输出能力)
|
||||
|
||||
@ -16,10 +16,21 @@ export interface NodeVariable {
|
||||
/**
|
||||
* 获取当前节点的所有前序节点(通过DFS反向遍历)
|
||||
*
|
||||
* @param currentNodeId 当前节点ID
|
||||
* 算法说明:
|
||||
* 1. 从当前节点开始,找到所有指向它的边(incoming edges)
|
||||
* 2. 通过这些边找到源节点,递归向上遍历
|
||||
* 3. 使用 visited 集合避免重复遍历(处理循环依赖)
|
||||
* 4. 过滤掉 START_EVENT 节点(它没有输出变量)
|
||||
*
|
||||
* @param currentNodeId 当前节点ID(UUID)
|
||||
* @param allNodes 所有节点列表
|
||||
* @param allEdges 所有连接边列表
|
||||
* @returns 所有前序节点(不包括START节点)
|
||||
*
|
||||
* @example
|
||||
* // 工作流:开始 -> Jenkins -> 通知 -> 结束
|
||||
* getAllPrecedingNodes("通知节点ID", allNodes, allEdges)
|
||||
* // => [Jenkins节点] (不包含开始节点)
|
||||
*/
|
||||
export const getAllPrecedingNodes = (
|
||||
currentNodeId: string,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user