deploy-ease-platform/frontend/src/pages/Workflow/Definition/components/EditModal.tsx
2025-10-24 14:28:25 +08:00

286 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;