mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 15:03:57 +08:00
feat: implement FlowExecutor for executing flow definitions with DAG traversal and node execution
This commit is contained in:
238
ccw/frontend/src/components/shared/IssueCard.tsx
Normal file
238
ccw/frontend/src/components/shared/IssueCard.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user