更改目录结构
This commit is contained in:
parent
3307b5cb46
commit
ae2690899f
177
frontend/src/components/SelectOrVariableInput/index.tsx
Normal file
177
frontend/src/components/SelectOrVariableInput/index.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import VariableInput from '@/components/VariableInput';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowLeftRight } from 'lucide-react';
|
||||
import type { FlowNode, FlowEdge } from '@/pages/Workflow/Design/types';
|
||||
import type { FormField } from '@/components/VariableInput/types';
|
||||
|
||||
export interface SelectOrVariableInputProps {
|
||||
// 基本属性
|
||||
value: string | number | undefined;
|
||||
onChange: (value: string | number) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
|
||||
// 下拉选项
|
||||
options: Array<{ label: string; value: any }>;
|
||||
|
||||
// 变量输入配置
|
||||
allNodes: FlowNode[];
|
||||
allEdges: FlowEdge[];
|
||||
currentNodeId: string;
|
||||
formFields?: FormField[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 混合输入组件:支持下拉选择 + 变量输入
|
||||
*
|
||||
* 使用场景:
|
||||
* - 固定值场景:用户从下拉框选择(如:选择 Jenkins 服务器)
|
||||
* - 动态值场景:用户输入变量(如:${approval.jenkinsServerId})
|
||||
*
|
||||
* 特性:
|
||||
* - 自动识别值类型(包含 ${} → 变量模式)
|
||||
* - 一键切换模式
|
||||
* - 保留变量输入的自动补全功能
|
||||
*/
|
||||
export const SelectOrVariableInput: React.FC<SelectOrVariableInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = '请选择或输入变量',
|
||||
disabled = false,
|
||||
options,
|
||||
allNodes,
|
||||
allEdges,
|
||||
currentNodeId,
|
||||
formFields
|
||||
}) => {
|
||||
// 判断当前值是否为变量
|
||||
const isVariableValue = (val: any): boolean => {
|
||||
return typeof val === 'string' && val.includes('${');
|
||||
};
|
||||
|
||||
// 模式:'select' 下拉选择 | 'variable' 变量输入
|
||||
const [mode, setMode] = useState<'select' | 'variable'>(() => {
|
||||
return isVariableValue(value) ? 'variable' : 'select';
|
||||
});
|
||||
|
||||
// 内部显示值(用于切换时立即清空显示)
|
||||
const [internalValue, setInternalValue] = useState<string | number | undefined>(value);
|
||||
|
||||
// 是否是手动切换(防止自动模式识别覆盖手动切换)
|
||||
const isManualToggleRef = useRef(false);
|
||||
|
||||
// 同步外部 value 到内部 internalValue(仅在非手动切换时)
|
||||
useEffect(() => {
|
||||
if (!isManualToggleRef.current) {
|
||||
setInternalValue(value);
|
||||
|
||||
// 自动模式识别
|
||||
if (isVariableValue(value) && mode === 'select') {
|
||||
setMode('variable');
|
||||
} else if (typeof value === 'number' && mode === 'variable') {
|
||||
setMode('select');
|
||||
}
|
||||
}
|
||||
}, [value, mode]);
|
||||
|
||||
// 切换模式
|
||||
const handleToggleMode = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
// 阻止默认行为和冒泡(防止触发表单提交)
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const newMode = mode === 'select' ? 'variable' : 'select';
|
||||
|
||||
// 标记为手动切换
|
||||
isManualToggleRef.current = true;
|
||||
|
||||
// 立即切换模式和清空值
|
||||
setMode(newMode);
|
||||
setInternalValue(undefined);
|
||||
onChange(undefined as any);
|
||||
|
||||
// 短暂延迟后重置标记,允许后续同步
|
||||
requestAnimationFrame(() => {
|
||||
isManualToggleRef.current = false;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 主输入区域 */}
|
||||
<div className="flex-1">
|
||||
{mode === 'select' ? (
|
||||
// 下拉选择模式
|
||||
<Select
|
||||
disabled={disabled}
|
||||
value={internalValue?.toString() || ''}
|
||||
onValueChange={(v) => {
|
||||
// 尝试转换为数字(如果原值是number类型)
|
||||
const numValue = Number(v);
|
||||
const newValue = isNaN(numValue) ? v : numValue;
|
||||
setInternalValue(newValue);
|
||||
onChange(newValue);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
// 变量输入模式
|
||||
<VariableInput
|
||||
value={String(internalValue || '')}
|
||||
onChange={(v) => {
|
||||
// 更新内部值
|
||||
setInternalValue(v || undefined);
|
||||
|
||||
// 如果包含变量语法(${),认为是变量表达式
|
||||
if (v.includes('${')) {
|
||||
// 变量表达式始终保持字符串类型
|
||||
onChange(v);
|
||||
} else if (v === '') {
|
||||
// 空值传递 undefined,符合表单 number 类型验证
|
||||
onChange(undefined as any);
|
||||
} else {
|
||||
// 普通文本:尝试转换为数字
|
||||
const numValue = Number(v);
|
||||
onChange(isNaN(numValue) ? v : numValue);
|
||||
}
|
||||
}}
|
||||
allNodes={allNodes}
|
||||
allEdges={allEdges}
|
||||
currentNodeId={currentNodeId}
|
||||
formFields={formFields}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 模式切换按钮 */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleToggleMode}
|
||||
disabled={disabled}
|
||||
title={mode === 'select' ? '切换到变量输入' : '切换到下拉选择'}
|
||||
className="shrink-0"
|
||||
>
|
||||
<ArrowLeftRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectOrVariableInput;
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
* 提供懒加载级联数据功能
|
||||
*/
|
||||
import request from '@/utils/request';
|
||||
import { CascadeDataSourceType, type CascadeOption, type CascadeLevelConfig } from './types';
|
||||
import { getCascadeDataSourceConfig } from './CascadeDataSourceRegistry';
|
||||
import {CascadeDataSourceType, type CascadeOption, type CascadeLevelConfig} from './types';
|
||||
import {getCascadeDataSourceConfig} from './CascadeDataSourceRegistry';
|
||||
|
||||
/**
|
||||
* 级联数据源服务类
|
||||
@ -99,7 +99,7 @@ class CascadeDataSourceService {
|
||||
): Promise<CascadeOption[]> {
|
||||
try {
|
||||
// 构建请求参数
|
||||
const params: Record<string, any> = { ...recursiveConfig.params };
|
||||
const params: Record<string, any> = {...recursiveConfig.params};
|
||||
|
||||
// 添加父级参数
|
||||
if (parentValue !== null) {
|
||||
@ -107,7 +107,7 @@ class CascadeDataSourceService {
|
||||
}
|
||||
|
||||
// 发起请求
|
||||
const response = await request.get(recursiveConfig.url, { params });
|
||||
const response = await request.get(recursiveConfig.url, {params});
|
||||
const data = response || [];
|
||||
|
||||
// 转换为级联选项格式
|
||||
@ -154,7 +154,7 @@ class CascadeDataSourceService {
|
||||
): Promise<CascadeOption[]> {
|
||||
try {
|
||||
// 构建请求参数
|
||||
const params: Record<string, any> = { ...levelConfig.params };
|
||||
const params: Record<string, any> = {...levelConfig.params};
|
||||
|
||||
// 如果有父级值且配置了父级参数名,添加到请求参数中
|
||||
if (parentValue !== null && levelConfig.parentParam) {
|
||||
@ -162,7 +162,7 @@ class CascadeDataSourceService {
|
||||
}
|
||||
|
||||
// 发起请求
|
||||
const response = await request.get(levelConfig.url, { params });
|
||||
const response = await request.get(levelConfig.url, {params});
|
||||
const data = response || [];
|
||||
|
||||
// 转换为级联选项格式
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
* 提供数据源加载和管理功能
|
||||
*/
|
||||
import request from '@/utils/request';
|
||||
import { DataSourceType, type DataSourceOption } from './types';
|
||||
import { getDataSourceConfig } from './DataSourceRegistry';
|
||||
import {DataSourceType, type DataSourceOption} from './types';
|
||||
import {getDataSourceConfig, getAllDataSourceTypes} from './DataSourceRegistry';
|
||||
|
||||
/**
|
||||
* 数据源服务类
|
||||
@ -15,16 +15,27 @@ class DataSourceService {
|
||||
* @param type 数据源类型
|
||||
* @returns 选项列表
|
||||
*/
|
||||
async load(type: DataSourceType): Promise<DataSourceOption[]> {
|
||||
const config = getDataSourceConfig(type);
|
||||
async load(type: DataSourceType | string): Promise<DataSourceOption[]> {
|
||||
const config = getDataSourceConfig(type as DataSourceType);
|
||||
|
||||
if (!config) {
|
||||
console.error(`❌ 数据源类型 ${type} 未配置`);
|
||||
const registeredTypes = getAllDataSourceTypes();
|
||||
console.warn(`⚠️ 数据源类型 "${type}" 未配置,请检查:
|
||||
|
||||
📋 可能的原因:
|
||||
1. 节点定义中使用了字符串而非 DataSourceType 枚举(如 "jenkins-servers" 应改为 DataSourceType.JENKINS_SERVERS)
|
||||
2. 该数据源尚未在 DataSourceRegistry 中注册
|
||||
3. 如果是新数据源,需要先在 src/domain/dataSource/presets/ 中添加配置并注册
|
||||
|
||||
✅ 当前已注册的数据源类型(${registeredTypes.length}个):
|
||||
${registeredTypes.map(t => ` - ${t}`).join('\n')}
|
||||
|
||||
💡 临时解决:返回空数组,界面将显示空下拉列表(可切换到变量输入模式)。`);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request.get(config.url, { params: config.params });
|
||||
const response = await request.get(config.url, {params: config.params});
|
||||
// request 拦截器已经提取了 data 字段,response 直接是数组
|
||||
return config.transform(response || []);
|
||||
} catch (error) {
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Jenkins 服务器数据源
|
||||
*/
|
||||
import type { DataSourceConfig } from '../types';
|
||||
import type {DataSourceConfig} from '../types';
|
||||
|
||||
export const jenkinsServersConfig: DataSourceConfig = {
|
||||
url: '/api/v1/external-system/list',
|
||||
params: { type: 'JENKINS', enabled: true },
|
||||
params: {type: 'JENKINS', enabled: true},
|
||||
transform: (data: any[]) => {
|
||||
return data.map((item: any) => ({
|
||||
label: `${item.name} (${item.url})`,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
@ -6,7 +6,6 @@ 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 { Label } from '@/components/ui/label';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import type { FlowEdge, FlowNode } from '../types';
|
||||
import { convertToUUID, convertToDisplayName } from '@/utils/workflow/variableConversion';
|
||||
@ -29,24 +28,12 @@ export interface EdgeCondition {
|
||||
priority: number;
|
||||
}
|
||||
|
||||
// Zod 表单验证 Schema
|
||||
// ✅ 简化的 Zod 验证 Schema(不再需要type字段)
|
||||
const edgeConditionSchema = z.object({
|
||||
type: z.enum(['EXPRESSION', 'DEFAULT'], {
|
||||
required_error: '请选择条件类型',
|
||||
}),
|
||||
expression: z.string().optional(),
|
||||
expression: z.string().optional(), // 可选,为空表示默认路径
|
||||
priority: z.number()
|
||||
.min(1, '优先级最小为 1')
|
||||
.max(999, '优先级最大为 999'),
|
||||
}).refine((data) => {
|
||||
// 如果是表达式类型,expression 必填
|
||||
if (data.type === 'EXPRESSION') {
|
||||
return data.expression && data.expression.trim().length > 0;
|
||||
}
|
||||
return true;
|
||||
}, {
|
||||
message: '请输入条件表达式',
|
||||
path: ['expression'],
|
||||
.max(998, '优先级最大为 998'), // 条件分支的优先级范围
|
||||
});
|
||||
|
||||
type EdgeConditionFormValues = z.infer<typeof edgeConditionSchema>;
|
||||
@ -67,17 +54,19 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
||||
}) => {
|
||||
// ✅ 所有 Hooks 必须在最顶部调用,不能放在条件语句后面
|
||||
const { toast } = useToast();
|
||||
const [conditionType, setConditionType] = useState<'EXPRESSION' | 'DEFAULT'>('EXPRESSION');
|
||||
|
||||
const form = useForm<EdgeConditionFormValues>({
|
||||
resolver: zodResolver(edgeConditionSchema),
|
||||
defaultValues: {
|
||||
type: 'EXPRESSION',
|
||||
expression: '',
|
||||
priority: 10,
|
||||
},
|
||||
});
|
||||
|
||||
// 监听表达式字段,判断是否为默认路径
|
||||
const expression = form.watch('expression');
|
||||
const isDefaultBranch = !expression?.trim();
|
||||
|
||||
// 当 edge 变化时,更新表单值
|
||||
useEffect(() => {
|
||||
if (visible && edge && allNodes.length > 0) {
|
||||
@ -88,13 +77,15 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
||||
? convertToDisplayName(condition.expression, allNodes)
|
||||
: '';
|
||||
|
||||
const values = {
|
||||
type: (condition?.type || 'EXPRESSION') as 'EXPRESSION' | 'DEFAULT',
|
||||
// 如果是默认路径,不显示优先级(自动为999)
|
||||
const priority = condition?.type === 'DEFAULT'
|
||||
? 10 // UI默认值
|
||||
: (condition?.priority || 10);
|
||||
|
||||
form.reset({
|
||||
expression: displayExpression,
|
||||
priority: condition?.priority || 10,
|
||||
};
|
||||
form.reset(values);
|
||||
setConditionType(values.type);
|
||||
priority,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible, edge?.id]);
|
||||
@ -105,82 +96,42 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
||||
// ✅ 网关类型存储在 inputMapping 中,不是 configs 中
|
||||
const gatewayType = isFromGateway ? (sourceNode?.data?.inputMapping?.gatewayType || sourceNode?.data?.configs?.gatewayType) : null;
|
||||
|
||||
// 获取该网关的所有出口连线
|
||||
const gatewayEdges = isFromGateway && edge
|
||||
// 获取源节点的所有出口连线(不管是不是网关节点)
|
||||
const sourceEdges = edge
|
||||
? allEdges.filter(e => e.source === edge.source)
|
||||
: [];
|
||||
|
||||
// 检查是否已有默认分支
|
||||
const hasDefaultBranch = gatewayEdges.some(e => e.data?.condition?.type === 'DEFAULT' && e.id !== edge?.id);
|
||||
// 检查是否已有其他默认分支
|
||||
const hasOtherDefaultBranch = sourceEdges.some(e =>
|
||||
e.id !== edge?.id &&
|
||||
e.data?.condition?.type === 'DEFAULT'
|
||||
);
|
||||
|
||||
// 检查优先级是否重复
|
||||
const usedPriorities = gatewayEdges
|
||||
const usedPriorities = sourceEdges
|
||||
.filter(e => e.id !== edge?.id && e.data?.condition?.type === 'EXPRESSION')
|
||||
.map(e => e.data?.condition?.priority)
|
||||
.filter((p): p is number => p !== undefined);
|
||||
|
||||
// ⚠️ 如果源节点不是网关节点,显示引导信息
|
||||
if (!isFromGateway) {
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>规范化提示</DialogTitle>
|
||||
<DialogDescription>
|
||||
为了符合 BPMN 2.0 标准,我们采用网关节点来处理分支逻辑
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="rounded-md bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-2xl">ℹ️</span>
|
||||
<div className="flex-1 space-y-2">
|
||||
<p className="font-medium text-blue-900 dark:text-blue-200">
|
||||
多分支场景请使用网关节点
|
||||
</p>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-300">
|
||||
网关节点提供了清晰的可视化分支逻辑,并且完全符合 BPMN 标准,便于:
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-sm text-blue-800 dark:text-blue-300 space-y-1 ml-2">
|
||||
<li>前后端数据结构一致(所见即所得)</li>
|
||||
<li>与主流工作流引擎(Camunda/Flowable)对接</li>
|
||||
<li>导出标准的 BPMN 2.0 XML</li>
|
||||
<li>支持复杂的分支逻辑(排他/并行/包容)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md bg-muted p-4">
|
||||
<p className="text-sm font-medium mb-2">💡 如何使用网关节点?</p>
|
||||
<ol className="list-decimal list-inside text-sm text-muted-foreground space-y-1 ml-2">
|
||||
<li>从左侧节点面板拖入"网关节点"</li>
|
||||
<li>双击网关节点,选择类型(排他/并行/包容)</li>
|
||||
<li>连接源节点到网关节点</li>
|
||||
<li>连接网关节点到目标节点(可多条)</li>
|
||||
<li>双击网关的出口连线,配置分支条件</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
我知道了
|
||||
</Button>
|
||||
<Button onClick={onCancel}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSubmit = (values: EdgeConditionFormValues) => {
|
||||
if (!edge) return;
|
||||
|
||||
// ⚠️ 网关节点必须先配置类型
|
||||
// 判断是否为默认路径(表达式为空)
|
||||
const expr = values.expression?.trim();
|
||||
const isDefault = !expr;
|
||||
|
||||
// 1. 检查多个默认分支
|
||||
if (isDefault && hasOtherDefaultBranch) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '冲突:多个默认分支',
|
||||
description: `节点"${sourceNode?.data.label}"已有默认分支。一个节点只能有一个默认分支,请为此分支配置条件表达式。`,
|
||||
duration: 5000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 网关节点必须先配置类型
|
||||
if (isFromGateway && !gatewayType) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
@ -191,10 +142,9 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// 网关节点特殊验证
|
||||
if (isFromGateway && gatewayType) {
|
||||
// 排他网关/包容网关:检查优先级重复
|
||||
if ((gatewayType === 'exclusiveGateway' || gatewayType === 'inclusiveGateway') && values.type === 'EXPRESSION') {
|
||||
// 3. 条件分支的验证
|
||||
if (!isDefault) {
|
||||
// 3.1 检查优先级重复
|
||||
if (usedPriorities.includes(values.priority)) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
@ -204,49 +154,11 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查默认分支唯一性
|
||||
if (values.type === 'DEFAULT' && hasDefaultBranch) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '默认分支已存在',
|
||||
description: `该网关已有默认分支,一个${gatewayType === 'exclusiveGateway' ? '排他' : '包容'}网关只能有一个默认分支。请将此分支设置为"表达式"类型,并配置条件表达式。`,
|
||||
duration: 6000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 3.2 转换显示名称为 UUID
|
||||
const uuidExpression = convertToUUID(expr, allNodes);
|
||||
|
||||
// 并行网关:不需要条件表达式(仅提示,不阻止)
|
||||
if (gatewayType === 'parallelGateway' && values.type === 'EXPRESSION') {
|
||||
toast({
|
||||
title: '💡 温馨提示',
|
||||
description: '并行网关的所有分支都会同时执行,配置条件表达式不会影响执行逻辑。建议保持默认配置即可。',
|
||||
duration: 4000,
|
||||
});
|
||||
}
|
||||
|
||||
// 排他网关/包容网关:检查是否配置了条件表达式
|
||||
if ((gatewayType === 'exclusiveGateway' || gatewayType === 'inclusiveGateway') && values.type === 'EXPRESSION') {
|
||||
if (!values.expression || values.expression.trim() === '') {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '条件表达式为空',
|
||||
description: '请输入条件表达式,或选择"默认路径"类型。表达式格式:${上游节点名称.字段名 == \'值\'}',
|
||||
duration: 5000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 转换 expression 中的显示名称为 UUID
|
||||
const uuidExpression = values.expression
|
||||
? convertToUUID(values.expression, allNodes)
|
||||
: '';
|
||||
|
||||
// 检查表达式是否包含变量引用(排他/包容网关的表达式必须包含变量)
|
||||
if (isFromGateway && (gatewayType === 'exclusiveGateway' || gatewayType === 'inclusiveGateway') && values.type === 'EXPRESSION') {
|
||||
// 3.3 检查是否包含变量引用
|
||||
const hasVariable = /\$\{[^}]+\}/.test(uuidExpression);
|
||||
if (!hasVariable) {
|
||||
toast({
|
||||
@ -257,30 +169,44 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 3.4 并行网关提示(不阻止)
|
||||
if (isFromGateway && gatewayType === 'parallelGateway') {
|
||||
toast({
|
||||
title: '💡 温馨提示',
|
||||
description: '并行网关的所有分支都会同时执行,配置条件表达式不会影响执行逻辑。建议保持默认配置即可。',
|
||||
duration: 4000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 提交成功提示
|
||||
// 4. 构建条件数据
|
||||
const condition: EdgeCondition = isDefault
|
||||
? {
|
||||
type: 'DEFAULT',
|
||||
priority: 999 // 默认路径自动设为最低优先级
|
||||
}
|
||||
: {
|
||||
type: 'EXPRESSION',
|
||||
expression: convertToUUID(expr, allNodes),
|
||||
priority: values.priority
|
||||
};
|
||||
|
||||
// 5. 成功提示
|
||||
toast({
|
||||
title: '✅ 保存成功',
|
||||
description: values.type === 'DEFAULT'
|
||||
? '已设置为默认分支'
|
||||
: `已设置条件表达式,优先级:${values.priority}`,
|
||||
description: isDefault
|
||||
? '已设置为默认路径(当所有其他条件都不满足时执行)'
|
||||
: `已设置条件分支,优先级:${values.priority}`,
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
// 提交 UUID 格式的数据
|
||||
onOk(edge.id, {
|
||||
type: values.type,
|
||||
expression: uuidExpression,
|
||||
// 默认分支自动设置优先级为 999(最低优先级,最后执行)
|
||||
priority: values.type === 'DEFAULT' ? 999 : values.priority
|
||||
});
|
||||
onOk(edge.id, condition);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
form.reset();
|
||||
setConditionType('EXPRESSION');
|
||||
onCancel();
|
||||
};
|
||||
|
||||
@ -333,8 +259,8 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 🔴 错误提示:多个默认分支(仅排他/包容网关,且用户选择 DEFAULT 类型时显示) */}
|
||||
{isFromGateway && gatewayType && gatewayType !== 'parallelGateway' && hasDefaultBranch && conditionType === 'DEFAULT' && (
|
||||
{/* 🔴 错误提示:多个默认分支(网关节点) */}
|
||||
{isFromGateway && gatewayType && gatewayType !== 'parallelGateway' && hasOtherDefaultBranch && isDefaultBranch && (
|
||||
<div className="rounded-md bg-red-50 dark:bg-red-950/20 border-2 border-red-400 dark:border-red-600 p-4 text-sm mb-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-2xl">❌</span>
|
||||
@ -353,64 +279,6 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ℹ️ 网关规则说明 */}
|
||||
{isFromGateway && gatewayType && (
|
||||
<div className="rounded-md bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 p-3 text-sm mb-6">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-blue-600 dark:text-blue-400">ℹ️</span>
|
||||
<div className="flex-1 space-y-1">
|
||||
{gatewayType === 'exclusiveGateway' && (
|
||||
<>
|
||||
<p className="font-medium text-blue-900 dark:text-blue-200">排他网关规则:</p>
|
||||
<ul className="list-disc list-inside text-blue-800 dark:text-blue-300 space-y-0.5">
|
||||
<li>按优先级顺序评估条件(数字越小越优先)</li>
|
||||
<li>第一个满足的条件分支被执行,其他分支跳过</li>
|
||||
<li>必须有一个默认分支(当所有条件都不满足时执行)</li>
|
||||
<li>已使用的优先级:{usedPriorities.length > 0 ? usedPriorities.join(', ') : '无'}</li>
|
||||
</ul>
|
||||
<div className="mt-2 p-2 bg-blue-100 dark:bg-blue-900/30 rounded">
|
||||
<p className="font-medium text-blue-900 dark:text-blue-200 text-xs">💡 条件表达式示例:</p>
|
||||
<code className="text-xs text-blue-800 dark:text-blue-300">
|
||||
${'{上游节点名称.status == \'SUCCESS\'}'}
|
||||
</code>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{gatewayType === 'parallelGateway' && (
|
||||
<>
|
||||
<p className="font-medium text-blue-900 dark:text-blue-200">并行网关规则:</p>
|
||||
<ul className="list-disc list-inside text-blue-800 dark:text-blue-300 space-y-0.5">
|
||||
<li>所有出口分支同时执行(Fork模式)</li>
|
||||
<li>不需要条件表达式(无条件执行)</li>
|
||||
<li>下游汇聚点等待所有分支完成(Join模式)</li>
|
||||
</ul>
|
||||
<div className="mt-2 p-2 bg-blue-100 dark:bg-blue-900/30 rounded">
|
||||
<p className="text-xs text-blue-800 dark:text-blue-300">
|
||||
💡 并行网关的所有边线可以保持默认配置(自动执行)
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{gatewayType === 'inclusiveGateway' && (
|
||||
<>
|
||||
<p className="font-medium text-blue-900 dark:text-blue-200">包容网关规则:</p>
|
||||
<ul className="list-disc list-inside text-blue-800 dark:text-blue-300 space-y-0.5">
|
||||
<li>所有满足条件的分支都会执行(可能是1条或多条)</li>
|
||||
<li>建议为每条分支设置条件表达式</li>
|
||||
<li>默认分支在所有条件不满足时执行</li>
|
||||
</ul>
|
||||
<div className="mt-2 p-2 bg-blue-100 dark:bg-blue-900/30 rounded">
|
||||
<p className="font-medium text-blue-900 dark:text-blue-200 text-xs">💡 条件表达式示例:</p>
|
||||
<code className="text-xs text-blue-800 dark:text-blue-300">
|
||||
${'{上游节点.score >= 80}'}
|
||||
</code>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 并行网关:直接显示说明,不允许配置条件 */}
|
||||
{isFromGateway && gatewayType === 'parallelGateway' ? (
|
||||
@ -433,55 +301,41 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
||||
) : (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>条件类型</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="type-expression"
|
||||
value="EXPRESSION"
|
||||
checked={field.value === 'EXPRESSION'}
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
setConditionType('EXPRESSION');
|
||||
}}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Label htmlFor="type-expression" className="cursor-pointer font-normal">
|
||||
表达式
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="type-default"
|
||||
value="DEFAULT"
|
||||
checked={field.value === 'DEFAULT'}
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
setConditionType('DEFAULT');
|
||||
}}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Label htmlFor="type-default" className="cursor-pointer font-normal">
|
||||
{/* ✅ 智能提示:默认路径说明 */}
|
||||
{isDefaultBranch && (
|
||||
<div className="rounded-md bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 p-4 text-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-blue-600 dark:text-blue-400">ℹ️</span>
|
||||
<div className="flex-1 space-y-2">
|
||||
<p className="font-medium text-blue-900 dark:text-blue-200">
|
||||
默认路径
|
||||
</Label>
|
||||
</p>
|
||||
<p className="text-blue-800 dark:text-blue-300">
|
||||
当前未配置条件表达式,此分支将作为<strong>默认路径</strong>,在所有其他条件分支都不满足时执行。优先级自动设为999(最低)。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{conditionType === 'EXPRESSION' ? (
|
||||
<>
|
||||
{/* ✅ 错误提示:多个默认分支 */}
|
||||
{isDefaultBranch && hasOtherDefaultBranch && (
|
||||
<div className="rounded-md bg-red-50 dark:bg-red-950/20 border-2 border-red-400 dark:border-red-600 p-4 text-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-red-600 dark:text-red-400">❌</span>
|
||||
<div className="flex-1 space-y-2">
|
||||
<p className="font-bold text-red-900 dark:text-red-200">
|
||||
冲突:多个默认分支
|
||||
</p>
|
||||
<p className="text-red-800 dark:text-red-300">
|
||||
节点"<strong>{sourceNode?.data.label}</strong>"已有默认分支。一个节点只能有一个默认分支,请为此分支配置条件表达式。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ✅ 条件表达式输入(核心字段) */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="expression"
|
||||
@ -497,18 +351,21 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
||||
currentNodeId={edge?.target || ''}
|
||||
formFields={formFields}
|
||||
variant="textarea"
|
||||
placeholder="请输入条件表达式,如:${form.环境选择} 或 ${Jenkins构建.buildStatus == 'SUCCESS'}"
|
||||
placeholder="例如: ${Jenkins构建.status} == 'SUCCESS',留空表示默认路径"
|
||||
rows={4}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
使用 JUEL 表达式语法,完整表达式需在 {'${}'} 内。选择变量后在 {'}'} 前添加比较逻辑(如 == 'SUCCESS')。支持比较运算符(==、!=、>、<)和逻辑运算符(&&、||)
|
||||
💡 <strong>留空此字段</strong>,该分支将作为默认路径(当所有其他条件都不满足时执行)。
|
||||
填写时使用 JUEL 表达式语法,支持比较运算符(==、!=、>、<)和逻辑运算符(&&、||)
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* ✅ 优先级(仅条件分支显示) */}
|
||||
{!isDefaultBranch && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="priority"
|
||||
@ -519,27 +376,20 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={999}
|
||||
placeholder="请输入优先级"
|
||||
max={998}
|
||||
placeholder="请输入优先级(数字越小越优先)"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
数字越小优先级越高,按优先级顺序评估条件(1-999)。已使用:{usedPriorities.length > 0 ? usedPriorities.join(', ') : '无'}
|
||||
数字越小优先级越高,按优先级顺序评估条件(1-998),默认路径自动为999。
|
||||
{usedPriorities.length > 0 && ` 已使用:${usedPriorities.join(', ')}`}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="rounded-md bg-muted p-4 text-sm">
|
||||
<p className="font-medium text-foreground mb-2">默认路径</p>
|
||||
<p className="text-muted-foreground">
|
||||
当所有条件分支都不满足时,将执行此路径。默认分支不需要配置条件表达式和优先级。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@ -22,6 +22,7 @@ import { convertJsonSchemaToZod, extractDataSourceTypes } from '../utils/schemaC
|
||||
import { loadDataSource, DataSourceType, type DataSourceOption } from '@/domain/dataSource';
|
||||
import { convertObjectToUUID, convertObjectToDisplayName } from '@/utils/workflow/variableConversion';
|
||||
import VariableInput from '@/components/VariableInput';
|
||||
import SelectOrVariableInput from '@/components/SelectOrVariableInput';
|
||||
import type { FormField as FormFieldType } from '@/components/VariableInput/types';
|
||||
|
||||
interface NodeConfigModalProps {
|
||||
@ -455,7 +456,24 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ 单选类型
|
||||
// ✅ 单选类型 + 支持变量输入
|
||||
if (prop['x-allow-variable']) {
|
||||
return (
|
||||
<SelectOrVariableInput
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
options={options}
|
||||
allNodes={allNodesRef.current}
|
||||
allEdges={allEdgesRef.current}
|
||||
currentNodeId={node?.id || ''}
|
||||
formFields={formFields}
|
||||
placeholder={prop.description || `请选择${prop.title || ''}或输入变量`}
|
||||
disabled={loadingDataSources || loading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ 单选类型(纯下拉)
|
||||
return renderSelect(
|
||||
options,
|
||||
field,
|
||||
@ -476,6 +494,25 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
const enumLabel = typeof value === 'object' ? value.label : (prop.enumNames?.[index] || value);
|
||||
return { label: enumLabel, value: enumValue };
|
||||
});
|
||||
|
||||
// ✅ 支持变量输入
|
||||
if (prop['x-allow-variable']) {
|
||||
return (
|
||||
<SelectOrVariableInput
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
options={options}
|
||||
allNodes={allNodesRef.current}
|
||||
allEdges={allEdgesRef.current}
|
||||
currentNodeId={node?.id || ''}
|
||||
formFields={formFields}
|
||||
placeholder={prop.description || `请选择${prop.title || ''}或输入变量`}
|
||||
disabled={loading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ 纯下拉
|
||||
return renderSelect(options, field, prop, loading);
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {ConfigurableNodeDefinition, NodeType, NodeCategory, defineNodeOutputs} from './types';
|
||||
import { DataSourceType } from '@/domain/dataSource';
|
||||
|
||||
/**
|
||||
* Jenkins构建节点定义(纯配置)
|
||||
@ -44,16 +45,19 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
|
||||
description: "当前节点所需数据配置",
|
||||
properties: {
|
||||
serverId: {
|
||||
type: "string",
|
||||
title: "服务器",
|
||||
description: "输入要使用的服务器",
|
||||
default: "${jenkins.serverId}"
|
||||
type: "number",
|
||||
title: "Jenkins服务器",
|
||||
description: "选择Jenkins服务器或输入变量",
|
||||
"x-dataSource": DataSourceType.JENKINS_SERVERS,
|
||||
"x-allow-variable": true
|
||||
},
|
||||
jobName: {
|
||||
type: "string",
|
||||
title: "项目",
|
||||
description: "要触发构建的项目",
|
||||
default: "${jenkins.jobName}"
|
||||
title: "构建任务",
|
||||
description: "输入构建任务名称或输入变量",
|
||||
// 注意:jobName 暂时使用手动输入,因为需要先选择 serverId 才能级联加载 jobs
|
||||
// 未来如果需要级联下拉,需要使用 CascadeDataSourceType.JENKINS_SERVER_VIEWS_JOBS
|
||||
"x-allow-variable": true
|
||||
},
|
||||
},
|
||||
required: ["serverId", "jobName"]
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {ConfigurableNodeDefinition, NodeType, NodeCategory, defineNodeOutputs} from './types';
|
||||
import { DataSourceType } from "@/domain/dataSource";
|
||||
import { DataSourceType } from '@/domain/dataSource';
|
||||
|
||||
/**
|
||||
* 通知节点定义
|
||||
@ -58,27 +58,28 @@ export const NotificationNodeDefinition: ConfigurableNodeDefinition = {
|
||||
// description: "选择通知发送的渠道",
|
||||
// 'x-dataSource': DataSourceType.NOTIFICATION_CHANNEL_TYPES
|
||||
// },
|
||||
notificationChannel: {
|
||||
channelId: {
|
||||
type: "number",
|
||||
title: "通知渠道",
|
||||
description: "选择通知发送的渠道",
|
||||
'x-dataSource': DataSourceType.NOTIFICATION_CHANNELS
|
||||
"x-dataSource": DataSourceType.NOTIFICATION_CHANNELS,
|
||||
"x-allow-variable": true
|
||||
},
|
||||
title: {
|
||||
type: "string",
|
||||
title: "通知标题",
|
||||
description: "通知消息的标题",
|
||||
default: "工作流通知"
|
||||
default: "${notification.title}"
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
title: "通知内容",
|
||||
description: "通知消息的正文内容,支持变量表达式",
|
||||
format: "textarea",
|
||||
default: ""
|
||||
default: "${notification.context}"
|
||||
}
|
||||
},
|
||||
required: ["notificationChannel", "title", "content"]
|
||||
required: ["channelId", "title", "content"]
|
||||
},
|
||||
outputs: defineNodeOutputs()
|
||||
};
|
||||
|
||||
@ -67,6 +67,7 @@ export interface JSONSchema {
|
||||
'x-component'?: string;
|
||||
'x-component-props'?: Record<string, any>;
|
||||
'x-dataSource'?: string;
|
||||
'x-allow-variable'?: boolean; // ✨ 是否允许变量输入(用于 enum/dataSource 字段)
|
||||
[key: string]: any; // 允许扩展属性
|
||||
}
|
||||
|
||||
|
||||
@ -62,15 +62,25 @@ export const convertJsonSchemaToZod = (jsonSchema: JSONSchema): z.ZodObject<any>
|
||||
}
|
||||
|
||||
case 'number':
|
||||
case 'integer':
|
||||
fieldSchema = prop.type === 'integer' ? z.number().int() : z.number();
|
||||
case 'integer': {
|
||||
let numberSchema: z.ZodNumber = prop.type === 'integer' ? z.number().int() : z.number();
|
||||
|
||||
if (prop.minimum !== undefined) {
|
||||
fieldSchema = (fieldSchema as z.ZodNumber).min(prop.minimum, `${prop.title || key}不能小于${prop.minimum}`);
|
||||
numberSchema = numberSchema.min(prop.minimum, `${prop.title || key}不能小于${prop.minimum}`);
|
||||
}
|
||||
if (prop.maximum !== undefined) {
|
||||
fieldSchema = (fieldSchema as z.ZodNumber).max(prop.maximum, `${prop.title || key}不能大于${prop.maximum}`);
|
||||
numberSchema = numberSchema.max(prop.maximum, `${prop.title || key}不能大于${prop.maximum}`);
|
||||
}
|
||||
|
||||
// ✅ 如果允许变量输入,支持 string | number 联合类型
|
||||
if (prop['x-allow-variable']) {
|
||||
// 允许变量表达式(字符串)或数字
|
||||
fieldSchema = z.union([numberSchema, z.string()]);
|
||||
} else {
|
||||
fieldSchema = numberSchema;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'boolean':
|
||||
fieldSchema = z.boolean();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user