更改目录结构

This commit is contained in:
dengqichen 2025-11-03 17:27:48 +08:00
parent 3307b5cb46
commit ae2690899f
10 changed files with 657 additions and 566 deletions

View 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;

View File

@ -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 || [];
// 转换为级联选项格式

View File

@ -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) {

View File

@ -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})`,

View File

@ -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'==!=&gt;&lt;&amp;&amp;||
💡 <strong></strong>
使 JUEL ==!=&gt;&lt;&amp;&amp;||
</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-998999
{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>

View File

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

View File

@ -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"]

View File

@ -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()
};

View File

@ -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; // 允许扩展属性
}

View File

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