mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 15:03:57 +08:00
- Implemented share token creation and revocation for CLI sessions. - Added a new page for viewing shared CLI sessions with SSE support. - Introduced hooks for fetching and managing CLI session shares. - Enhanced the IssueTerminalTab component to handle share tokens and display active shares. - Updated API routes to support fetching and revoking share tokens. - Added unit tests for the CLI session share manager and rate limiter. - Updated localization files to include new strings for sharing functionality.
292 lines
8.7 KiB
TypeScript
292 lines
8.7 KiB
TypeScript
// ========================================
|
|
// IssueCard Component
|
|
// ========================================
|
|
// Card component for displaying issues with actions
|
|
|
|
import { useState } from 'react';
|
|
import { useIntl } from 'react-intl';
|
|
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 { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/Dropdown';
|
|
import type { DraggableProvidedDragHandleProps, DraggableProvidedDraggableProps } from '@hello-pangea/dnd';
|
|
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?: DraggableProvidedDraggableProps;
|
|
dragHandleProps?: DraggableProvidedDragHandleProps | null;
|
|
innerRef?: React.Ref<HTMLDivElement>;
|
|
}
|
|
|
|
// ========== Priority Helpers ==========
|
|
|
|
// Priority icon and color configuration (without labels for i18n)
|
|
const priorityVariantConfig: Record<Issue['priority'], { icon: React.ElementType; color: string }> = {
|
|
critical: { icon: AlertCircle, color: 'destructive' },
|
|
high: { icon: AlertTriangle, color: 'warning' },
|
|
medium: { icon: Info, color: 'info' },
|
|
low: { icon: Info, color: 'secondary' },
|
|
};
|
|
|
|
// Priority label keys for i18n
|
|
const priorityLabelKeys: Record<Issue['priority'], string> = {
|
|
critical: 'issues.priority.critical',
|
|
high: 'issues.priority.high',
|
|
medium: 'issues.priority.medium',
|
|
low: 'issues.priority.low',
|
|
};
|
|
|
|
// Status icon and color configuration (without labels for i18n)
|
|
const statusVariantConfig: Record<Issue['status'], { icon: React.ElementType; color: string }> = {
|
|
open: { icon: AlertCircle, color: 'info' },
|
|
in_progress: { icon: Clock, color: 'warning' },
|
|
resolved: { icon: CheckCircle, color: 'success' },
|
|
closed: { icon: XCircle, color: 'muted' },
|
|
completed: { icon: CheckCircle, color: 'success' },
|
|
};
|
|
|
|
// Status label keys for i18n
|
|
const statusLabelKeys: Record<Issue['status'], string> = {
|
|
open: 'issues.status.open',
|
|
in_progress: 'issues.status.inProgress',
|
|
resolved: 'issues.status.resolved',
|
|
closed: 'issues.status.closed',
|
|
completed: 'issues.status.completed',
|
|
};
|
|
|
|
// ========== Priority Badge ==========
|
|
|
|
export function PriorityBadge({ priority }: { priority: Issue['priority'] }) {
|
|
const { formatMessage } = useIntl();
|
|
const config = priorityVariantConfig[priority];
|
|
|
|
// Defensive check: handle unknown priority values
|
|
if (!config) {
|
|
return (
|
|
<Badge variant="secondary" className="gap-1">
|
|
{priority}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
const Icon = config.icon;
|
|
const label = priorityLabelKeys[priority]
|
|
? formatMessage({ id: priorityLabelKeys[priority] })
|
|
: priority;
|
|
|
|
return (
|
|
<Badge variant={config.color as 'default' | 'secondary' | 'destructive' | 'outline'} className="gap-1">
|
|
<Icon className="w-3 h-3" />
|
|
{label}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
// ========== Status Badge ==========
|
|
|
|
export function StatusBadge({ status }: { status: Issue['status'] }) {
|
|
const { formatMessage } = useIntl();
|
|
const config = statusVariantConfig[status];
|
|
|
|
// Defensive check: handle unknown status values
|
|
if (!config) {
|
|
return (
|
|
<Badge variant="outline" className="gap-1">
|
|
{status}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
const Icon = config.icon;
|
|
const label = statusLabelKeys[status]
|
|
? formatMessage({ id: statusLabelKeys[status] })
|
|
: status;
|
|
|
|
return (
|
|
<Badge variant="outline" className="gap-1">
|
|
<Icon className="w-3 h-3" />
|
|
{label}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
// ========== Main IssueCard Component ==========
|
|
|
|
export function IssueCard({
|
|
issue,
|
|
onEdit,
|
|
onDelete,
|
|
onClick,
|
|
onStatusChange,
|
|
className,
|
|
compact = false,
|
|
showActions = true,
|
|
draggableProps,
|
|
dragHandleProps,
|
|
innerRef,
|
|
}: IssueCardProps) {
|
|
const { formatMessage } = useIntl();
|
|
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 hover-glow',
|
|
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 hover-glow',
|
|
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 && (
|
|
<DropdownMenu open={isMenuOpen} onOpenChange={setIsMenuOpen}>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 w-8 p-0"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<MoreVertical className="w-4 h-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={handleEdit}>
|
|
<Edit className="w-4 h-4 mr-2" />
|
|
{formatMessage({ id: 'issues.actions.edit' })}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => onStatusChange?.(issue, 'in_progress')}>
|
|
<Clock className="w-4 h-4 mr-2" />
|
|
{formatMessage({ id: 'issues.actions.startProgress' })}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => onStatusChange?.(issue, 'resolved')}>
|
|
<CheckCircle className="w-4 h-4 mr-2" />
|
|
{formatMessage({ id: 'issues.actions.markResolved' })}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={handleDelete} className="text-destructive">
|
|
<Trash2 className="w-4 h-4 mr-2" />
|
|
{formatMessage({ id: 'issues.actions.delete' })}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
</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} {formatMessage(
|
|
{ id: 'issues.card.solutions' },
|
|
{ count: issue.solutions.length }
|
|
)}
|
|
</div>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export default IssueCard;
|