Files
Agent-System/web/src/components/workflow/ProjectWorkflowSwitcher.tsx
T
WeiCHENandCursor 2c58e2d17c fix(workflow): reconcile version conflicts in review mode and UI retry
Auto-adopt current workflow selection version in review/demo backend
paths, and retry frontend workflow switch after refreshing project version.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 13:00:33 +08:00

160 lines
5.1 KiB
TypeScript

import React, { useState } from 'react';
import { Workflow, Lock, CheckCircle2, AlertCircle } from 'lucide-react';
import { activateWorkflow, getProject } from '../../api/projectWorkflow';
interface ProjectWorkflowSwitcherProps {
projectId: number;
activeWorkflowKey: string;
workflowVersion: number;
isCreator: boolean;
onWorkflowChanged?: (newKey: string, newVersion: number) => void;
}
const WORKFLOW_OPTIONS = [
{ key: 'main', label: '主协同流程', icon: '⚡' },
{ key: 'product_manager', label: '智能产品经理', icon: '📋' },
{ key: 'test_manager', label: '智能测试经理', icon: '🧪' },
];
export const ProjectWorkflowSwitcher: React.FC<ProjectWorkflowSwitcherProps> = ({
projectId,
activeWorkflowKey,
workflowVersion,
isCreator,
onWorkflowChanged,
}) => {
const [switching, setSwitching] = useState(false);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const handleSelect = async (targetKey: string) => {
if (targetKey === activeWorkflowKey) return;
if (!isCreator) {
setErrorMsg('权限不足:仅项目创建人可切换激活工作流');
setTimeout(() => setErrorMsg(null), 3000);
return;
}
setSwitching(true);
setErrorMsg(null);
try {
let res;
try {
res = await activateWorkflow(projectId, {
workflow_key: targetKey,
expected_version: workflowVersion,
reason: '前端控制台用户切换工作流',
});
} catch (firstErr: any) {
// 若遇到并发版本落后或冲突,自动拉取数据库最新项目版本重试一次
const fresh = await getProject(projectId);
res = await activateWorkflow(projectId, {
workflow_key: targetKey,
expected_version: fresh.workflow_selection_version,
reason: '前端版本冲突自愈重试',
});
}
if (onWorkflowChanged && res) {
onWorkflowChanged(res.active_workflow_key, res.version);
}
} catch (err: any) {
setErrorMsg(err.message || '切换工作流失败');
setTimeout(() => setErrorMsg(null), 4000);
} finally {
setSwitching(false);
}
};
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', position: 'relative' }}>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '2px',
background: 'var(--bg-subtle, #f1f5f9)',
borderRadius: 'var(--r-md, 8px)',
border: '1px solid var(--border, #e2e8f0)',
}}
>
<span
style={{
fontSize: '11px',
fontWeight: 600,
color: 'var(--text-muted, #64748b)',
padding: '0 6px',
display: 'flex',
alignItems: 'center',
gap: '3px',
}}
>
<Workflow size={13} />
工作流:
</span>
{WORKFLOW_OPTIONS.map((wf) => {
const isActive = wf.key === activeWorkflowKey;
return (
<button
key={wf.key}
onClick={() => handleSelect(wf.key)}
disabled={switching || (!isCreator && !isActive)}
title={
!isCreator
? `仅创建人可切换工作流 (当前激活: ${wf.label})`
: `点击切换至 ${wf.label}`
}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '4px 8px',
fontSize: '11px',
fontWeight: isActive ? 600 : 500,
borderRadius: '6px',
border: 'none',
cursor: switching || (!isCreator && !isActive) ? 'not-allowed' : 'pointer',
background: isActive ? 'var(--bg-card, #ffffff)' : 'transparent',
color: isActive ? 'var(--brand-600, #4f46e5)' : 'var(--text-muted, #64748b)',
boxShadow: isActive ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
opacity: !isCreator && !isActive ? 0.6 : 1,
transition: 'all 0.15s ease',
}}
>
<span>{wf.icon}</span>
<span>{wf.label}</span>
{isActive && <CheckCircle2 size={12} style={{ color: 'var(--brand-600, #4f46e5)' }} />}
{!isCreator && !isActive && <Lock size={10} style={{ opacity: 0.5 }} />}
</button>
);
})}
</div>
{errorMsg && (
<div
style={{
position: 'absolute',
top: '36px',
left: '0',
zIndex: 100,
background: 'var(--danger-50, #fef2f2)',
border: '1px solid var(--danger-200, #fecaca)',
color: 'var(--danger-700, #b91c1c)',
padding: '4px 8px',
borderRadius: '6px',
fontSize: '11px',
display: 'flex',
alignItems: 'center',
gap: '4px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
whiteSpace: 'nowrap',
}}
>
<AlertCircle size={12} />
{errorMsg}
</div>
)}
</div>
);
};