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