286 lines
11 KiB
TypeScript
286 lines
11 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogFooter,
|
||
} from '@/components/ui/dialog';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Label } from '@/components/ui/label';
|
||
import { Textarea } from '@/components/ui/textarea';
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||
import { Loader2, FileText, Tag, AlignLeft, Folder } from 'lucide-react';
|
||
import { useToast } from '@/components/ui/use-toast';
|
||
import type { WorkflowDefinition, WorkflowCategoryResponse } from '../types';
|
||
import { saveDefinition, updateDefinition, getWorkflowCategoryList } from '../service';
|
||
|
||
interface EditModalProps {
|
||
visible: boolean;
|
||
onClose: () => void;
|
||
onSuccess?: () => void;
|
||
record?: WorkflowDefinition;
|
||
}
|
||
|
||
const EditModal: React.FC<EditModalProps> = ({ visible, onClose, onSuccess, record }) => {
|
||
const { toast } = useToast();
|
||
const isEdit = !!record;
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [categories, setCategories] = useState<WorkflowCategoryResponse[]>([]);
|
||
const [formData, setFormData] = useState({
|
||
name: '',
|
||
key: '',
|
||
categoryId: undefined as number | undefined,
|
||
description: '',
|
||
triggers: [] as string[],
|
||
});
|
||
|
||
useEffect(() => {
|
||
if (visible) {
|
||
loadCategories();
|
||
if (record) {
|
||
setFormData({
|
||
name: record.name,
|
||
key: record.key,
|
||
categoryId: record.categoryId,
|
||
description: record.description || '',
|
||
triggers: record.triggers || [],
|
||
});
|
||
} else {
|
||
setFormData({
|
||
name: '',
|
||
key: '',
|
||
categoryId: undefined,
|
||
description: '',
|
||
triggers: [],
|
||
});
|
||
}
|
||
}
|
||
}, [visible, record]);
|
||
|
||
const loadCategories = async () => {
|
||
try {
|
||
const data = await getWorkflowCategoryList();
|
||
setCategories(data);
|
||
} catch (error) {
|
||
console.error('加载工作流分类失败:', error);
|
||
toast({
|
||
title: '加载失败',
|
||
description: '加载工作流分类失败',
|
||
variant: 'destructive',
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleClose = () => {
|
||
if (!submitting) {
|
||
onClose();
|
||
}
|
||
};
|
||
|
||
const handleSubmit = async () => {
|
||
// 验证
|
||
if (!formData.name.trim()) {
|
||
toast({
|
||
title: '验证失败',
|
||
description: '请输入流程名称',
|
||
variant: 'destructive',
|
||
});
|
||
return;
|
||
}
|
||
if (!formData.key.trim()) {
|
||
toast({
|
||
title: '验证失败',
|
||
description: '请输入流程标识',
|
||
variant: 'destructive',
|
||
});
|
||
return;
|
||
}
|
||
if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(formData.key)) {
|
||
toast({
|
||
title: '验证失败',
|
||
description: '流程标识只能包含字母、数字、下划线和连字符,且必须以字母或下划线开头',
|
||
variant: 'destructive',
|
||
});
|
||
return;
|
||
}
|
||
if (/^xml/i.test(formData.key)) {
|
||
toast({
|
||
title: '验证失败',
|
||
description: '流程标识不能以xml开头',
|
||
variant: 'destructive',
|
||
});
|
||
return;
|
||
}
|
||
if (!formData.categoryId) {
|
||
toast({
|
||
title: '验证失败',
|
||
description: '请选择流程分类',
|
||
variant: 'destructive',
|
||
});
|
||
return;
|
||
}
|
||
|
||
setSubmitting(true);
|
||
try {
|
||
const submitData: WorkflowDefinition = {
|
||
...formData,
|
||
id: record?.id || 0,
|
||
flowVersion: isEdit ? record.flowVersion : 1,
|
||
status: isEdit ? record.status : 'DRAFT',
|
||
graph: record?.graph || { nodes: [], edges: [] },
|
||
formConfig: record?.formConfig || { formItems: [] },
|
||
} as WorkflowDefinition;
|
||
|
||
if (isEdit && record) {
|
||
await updateDefinition(record.id, submitData);
|
||
toast({
|
||
title: '更新成功',
|
||
description: `工作流 "${formData.name}" 已更新`,
|
||
});
|
||
} else {
|
||
await saveDefinition(submitData);
|
||
toast({
|
||
title: '创建成功',
|
||
description: `工作流 "${formData.name}" 已创建`,
|
||
});
|
||
}
|
||
onSuccess?.();
|
||
onClose();
|
||
} catch (error) {
|
||
if (error instanceof Error) {
|
||
toast({
|
||
title: isEdit ? '更新失败' : '创建失败',
|
||
description: error.message,
|
||
variant: 'destructive',
|
||
});
|
||
}
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const selectedCategory = categories.find(c => c.id === formData.categoryId);
|
||
|
||
return (
|
||
<Dialog open={visible} onOpenChange={handleClose}>
|
||
<DialogContent className="sm:max-w-[600px]">
|
||
<DialogHeader>
|
||
<DialogTitle>{isEdit ? '编辑流程' : '新建流程'}</DialogTitle>
|
||
</DialogHeader>
|
||
<div className="grid gap-6 py-4">
|
||
{/* 流程分类 */}
|
||
<div className="space-y-2">
|
||
<Label htmlFor="categoryId" className="flex items-center gap-2">
|
||
<Folder className="h-4 w-4 text-muted-foreground" />
|
||
流程分类 <span className="text-destructive">*</span>
|
||
</Label>
|
||
<Select
|
||
value={formData.categoryId?.toString() || undefined}
|
||
onValueChange={(value) => {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
categoryId: Number(value),
|
||
triggers: [], // 切换分类时清空触发器
|
||
}));
|
||
}}
|
||
disabled={isEdit}
|
||
>
|
||
<SelectTrigger id="categoryId" className="h-10">
|
||
<SelectValue placeholder="请选择流程分类" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{categories.map(cat => (
|
||
<SelectItem key={cat.id} value={cat.id.toString()}>
|
||
{cat.name}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
{isEdit && (
|
||
<p className="text-xs text-muted-foreground">
|
||
编辑时不可修改分类
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* 流程名称 */}
|
||
<div className="space-y-2">
|
||
<Label htmlFor="name" className="flex items-center gap-2">
|
||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||
流程名称 <span className="text-destructive">*</span>
|
||
</Label>
|
||
<Input
|
||
id="name"
|
||
placeholder="例如:Jenkins 构建流程"
|
||
value={formData.name}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||
className="h-10"
|
||
/>
|
||
</div>
|
||
|
||
{/* 流程标识 */}
|
||
<div className="space-y-2">
|
||
<Label htmlFor="key" className="flex items-center gap-2">
|
||
<Tag className="h-4 w-4 text-muted-foreground" />
|
||
流程标识 <span className="text-destructive">*</span>
|
||
{isEdit && <span className="text-xs text-muted-foreground">(不可修改)</span>}
|
||
</Label>
|
||
<Input
|
||
id="key"
|
||
placeholder="例如:jenkins_build_workflow"
|
||
value={formData.key}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, key: e.target.value }))}
|
||
disabled={isEdit}
|
||
className="h-10 font-mono"
|
||
/>
|
||
<p className="text-xs text-muted-foreground">
|
||
只能包含字母、数字、下划线和连字符,建议使用小写字母和下划线
|
||
</p>
|
||
</div>
|
||
|
||
{/* 描述 */}
|
||
<div className="space-y-2">
|
||
<Label htmlFor="description" className="flex items-center gap-2">
|
||
<AlignLeft className="h-4 w-4 text-muted-foreground" />
|
||
流程描述
|
||
</Label>
|
||
<Textarea
|
||
id="description"
|
||
placeholder="简要说明此工作流的用途..."
|
||
value={formData.description}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||
className="min-h-[100px] resize-none"
|
||
/>
|
||
</div>
|
||
|
||
{/* 提示:触发方式在设计阶段配置 */}
|
||
{selectedCategory && selectedCategory.supportedTriggers && selectedCategory.supportedTriggers.length > 0 && (
|
||
<div className="rounded-lg border p-4 bg-muted/30">
|
||
<p className="text-sm text-muted-foreground">
|
||
<strong>支持的触发方式:</strong> {selectedCategory.supportedTriggers.join(', ')}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground mt-1">
|
||
创建后可在工作流设计页面配置具体的触发条件
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={handleClose} disabled={submitting}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={handleSubmit} disabled={submitting}>
|
||
{submitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||
{isEdit ? '更新' : '创建'}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
};
|
||
|
||
export default EditModal;
|