feat: implement FlowExecutor for executing flow definitions with DAG traversal and node execution

This commit is contained in:
catlog22
2026-01-30 16:59:18 +08:00
parent 0a7c1454d9
commit a5c3dff8d3
92 changed files with 23875 additions and 542 deletions

View File

@@ -0,0 +1,238 @@
// ========================================
// IssueCard Component
// ========================================
// Card component for displaying issues with actions
import { useState } from 'react';
import {
AlertCircle,
AlertTriangle,
Info,
MoreVertical,
Edit,
Trash2,
ExternalLink,
CheckCircle,
Clock,
XCircle,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Card } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { Dropdown, DropdownTrigger, DropdownContent, DropdownItem } from '@/components/ui/Dropdown';
import type { Issue } from '@/lib/api';
// ========== Types ==========
export interface IssueCardProps {
issue: Issue;
onEdit?: (issue: Issue) => void;
onDelete?: (issue: Issue) => void;
onClick?: (issue: Issue) => void;
onStatusChange?: (issue: Issue, status: Issue['status']) => void;
className?: string;
compact?: boolean;
showActions?: boolean;
draggableProps?: Record<string, unknown>;
dragHandleProps?: Record<string, unknown>;
innerRef?: React.Ref<HTMLDivElement>;
}
// ========== Priority Helpers ==========
const priorityConfig: Record<Issue['priority'], { icon: React.ElementType; color: string; label: string }> = {
critical: { icon: AlertCircle, color: 'destructive', label: 'Critical' },
high: { icon: AlertTriangle, color: 'warning', label: 'High' },
medium: { icon: Info, color: 'info', label: 'Medium' },
low: { icon: Info, color: 'secondary', label: 'Low' },
};
const statusConfig: Record<Issue['status'], { icon: React.ElementType; color: string; label: string }> = {
open: { icon: AlertCircle, color: 'info', label: 'Open' },
in_progress: { icon: Clock, color: 'warning', label: 'In Progress' },
resolved: { icon: CheckCircle, color: 'success', label: 'Resolved' },
closed: { icon: XCircle, color: 'muted', label: 'Closed' },
completed: { icon: CheckCircle, color: 'success', label: 'Completed' },
};
// ========== Priority Badge ==========
export function PriorityBadge({ priority }: { priority: Issue['priority'] }) {
const config = priorityConfig[priority];
const Icon = config.icon;
return (
<Badge variant={config.color as 'default' | 'secondary' | 'destructive' | 'outline'} className="gap-1">
<Icon className="w-3 h-3" />
{config.label}
</Badge>
);
}
// ========== Status Badge ==========
export function StatusBadge({ status }: { status: Issue['status'] }) {
const config = statusConfig[status];
const Icon = config.icon;
return (
<Badge variant="outline" className="gap-1">
<Icon className="w-3 h-3" />
{config.label}
</Badge>
);
}
// ========== Main IssueCard Component ==========
export function IssueCard({
issue,
onEdit,
onDelete,
onClick,
onStatusChange,
className,
compact = false,
showActions = true,
draggableProps,
dragHandleProps,
innerRef,
}: IssueCardProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const handleClick = () => {
if (!isMenuOpen) {
onClick?.(issue);
}
};
const handleEdit = (e: React.MouseEvent) => {
e.stopPropagation();
setIsMenuOpen(false);
onEdit?.(issue);
};
const handleDelete = (e: React.MouseEvent) => {
e.stopPropagation();
setIsMenuOpen(false);
onDelete?.(issue);
};
if (compact) {
return (
<div
ref={innerRef}
{...draggableProps}
{...dragHandleProps}
onClick={handleClick}
className={cn(
'p-3 bg-card border border-border rounded-lg cursor-pointer',
'hover:shadow-md hover:border-primary/50 transition-all',
className
)}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">{issue.title}</p>
<p className="text-xs text-muted-foreground mt-0.5">#{issue.id}</p>
</div>
<PriorityBadge priority={issue.priority} />
</div>
</div>
);
}
return (
<Card
ref={innerRef}
{...draggableProps}
onClick={handleClick}
className={cn(
'p-4 cursor-pointer hover:shadow-md hover:border-primary/50 transition-all',
className
)}
>
{/* Header */}
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0" {...dragHandleProps}>
<h3 className="text-sm font-medium text-foreground line-clamp-2">
{issue.title}
</h3>
<p className="text-xs text-muted-foreground mt-1">#{issue.id}</p>
</div>
{showActions && (
<Dropdown open={isMenuOpen} onOpenChange={setIsMenuOpen}>
<DropdownTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={(e) => e.stopPropagation()}
>
<MoreVertical className="w-4 h-4" />
</Button>
</DropdownTrigger>
<DropdownContent align="end">
<DropdownItem onClick={handleEdit}>
<Edit className="w-4 h-4 mr-2" />
Edit
</DropdownItem>
<DropdownItem onClick={() => onStatusChange?.(issue, 'in_progress')}>
<Clock className="w-4 h-4 mr-2" />
Start Progress
</DropdownItem>
<DropdownItem onClick={() => onStatusChange?.(issue, 'resolved')}>
<CheckCircle className="w-4 h-4 mr-2" />
Mark Resolved
</DropdownItem>
<DropdownItem onClick={handleDelete} className="text-destructive">
<Trash2 className="w-4 h-4 mr-2" />
Delete
</DropdownItem>
</DropdownContent>
</Dropdown>
)}
</div>
{/* Context Preview */}
{issue.context && (
<p className="text-sm text-muted-foreground mt-2 line-clamp-2">
{issue.context}
</p>
)}
{/* Labels */}
{issue.labels && issue.labels.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{issue.labels.slice(0, 3).map((label) => (
<Badge key={label} variant="outline" className="text-xs">
{label}
</Badge>
))}
{issue.labels.length > 3 && (
<Badge variant="outline" className="text-xs">
+{issue.labels.length - 3}
</Badge>
)}
</div>
)}
{/* Footer */}
<div className="flex items-center justify-between mt-3 pt-3 border-t border-border">
<PriorityBadge priority={issue.priority} />
<StatusBadge status={issue.status} />
</div>
{/* Solutions Count */}
{issue.solutions && issue.solutions.length > 0 && (
<div className="flex items-center gap-1 mt-2 text-xs text-muted-foreground">
<ExternalLink className="w-3 h-3" />
{issue.solutions.length} solution{issue.solutions.length !== 1 ? 's' : ''}
</div>
)}
</Card>
);
}
export default IssueCard;