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>
160 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
};
|