Add delete confirmation dialog for kanban cards

When clicking the trash icon to delete a feature card, users now see a
confirmation dialog asking them to confirm the deletion. This prevents
accidental deletions and provides a clear cancel option.

- Added Dialog component with confirm/cancel buttons to kanban-card
- Updated mock electron API to support test feature injection via __mockFeatures
- Added test utilities for delete confirmation dialog interactions
- Fixed test infrastructure to properly load mock features

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cody Seibert
2025-12-09 08:42:11 -05:00
parent 243c84178e
commit 5e606726eb
6 changed files with 260 additions and 35 deletions

View File

@@ -0,0 +1,30 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View File

@@ -990,7 +990,7 @@ export function BoardView() {
<DialogContent
data-testid="add-feature-dialog"
onKeyDown={(e) => {
if (e.shiftKey && e.key === "Enter" && newFeature.description) {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && newFeature.description) {
e.preventDefault();
handleAddFeature();
}
@@ -1091,7 +1091,7 @@ export function BoardView() {
className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-white/10 border border-white/20"
data-testid="shortcut-confirm-add-feature"
>
</span>
</Button>
</DialogFooter>

View File

@@ -1,5 +1,6 @@
"use client";
import { useState } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { cn } from "@/lib/utils";
@@ -11,8 +12,29 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Feature } from "@/store/app-store";
import { GripVertical, Edit, CheckCircle2, Circle, Loader2, Trash2, Eye, PlayCircle, RotateCcw, StopCircle } from "lucide-react";
import {
GripVertical,
Edit,
CheckCircle2,
Circle,
Loader2,
Trash2,
Eye,
PlayCircle,
RotateCcw,
StopCircle,
FlaskConical,
ArrowLeft,
} from "lucide-react";
import { CountUpTimer } from "@/components/ui/count-up-timer";
interface KanbanCardProps {
@@ -23,13 +45,50 @@ interface KanbanCardProps {
onVerify?: () => void;
onResume?: () => void;
onForceStop?: () => void;
onManualVerify?: () => void;
onMoveBackToInProgress?: () => void;
hasContext?: boolean;
isCurrentAutoTask?: boolean;
shortcutKey?: string;
}
export function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify, onResume, onForceStop, hasContext, isCurrentAutoTask }: KanbanCardProps) {
// Disable dragging if the feature is in progress or verified
const isDraggable = feature.status === "backlog";
export function KanbanCard({
feature,
onEdit,
onDelete,
onViewOutput,
onVerify,
onResume,
onForceStop,
onManualVerify,
onMoveBackToInProgress,
hasContext,
isCurrentAutoTask,
shortcutKey,
}: KanbanCardProps) {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const handleDeleteClick = (e: React.MouseEvent) => {
e.stopPropagation();
setIsDeleteDialogOpen(true);
};
const handleConfirmDelete = () => {
setIsDeleteDialogOpen(false);
onDelete();
};
const handleCancelDelete = () => {
setIsDeleteDialogOpen(false);
};
// Dragging logic:
// - Backlog items can always be dragged
// - skipTests items can be dragged even when in_progress or verified (unless currently running)
// - Non-skipTests (TDD) items in progress or verified cannot be dragged
const isDraggable =
feature.status === "backlog" ||
(feature.skipTests && !isCurrentAutoTask);
const {
attributes,
listeners,
@@ -54,27 +113,62 @@ export function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify,
className={cn(
"cursor-grab active:cursor-grabbing transition-all backdrop-blur-sm border-white/10 relative",
isDragging && "opacity-50 scale-105 shadow-lg",
isCurrentAutoTask && "border-purple-500 border-2 shadow-purple-500/50 shadow-lg animate-pulse"
isCurrentAutoTask &&
"border-purple-500 border-2 shadow-purple-500/50 shadow-lg animate-pulse"
)}
data-testid={`kanban-card-${feature.id}`}
{...attributes}
>
{/* Shortcut key badge for in-progress cards */}
{shortcutKey && (
<div
className="absolute top-2 left-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-white/10 border border-white/20 text-zinc-300 z-10"
data-testid={`shortcut-key-${feature.id}`}
>
{shortcutKey}
</div>
)}
{/* Skip Tests indicator badge */}
{feature.skipTests && (
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
shortcutKey ? "top-2 left-10" : "top-2 left-2",
"bg-orange-500/20 border border-orange-500/50 text-orange-400"
)}
data-testid={`skip-tests-badge-${feature.id}`}
title="Manual verification required"
>
<FlaskConical className="w-3 h-3" />
<span>Manual</span>
</div>
)}
<CardHeader className="p-3 pb-2">
{isCurrentAutoTask && (
<div className="absolute top-2 right-2 flex items-center gap-2 bg-purple-500/20 border border-purple-500 rounded px-2 py-0.5">
<Loader2 className="w-4 h-4 text-purple-400 animate-spin" />
<span className="text-xs text-purple-400 font-medium">Running...</span>
<span className="text-xs text-purple-400 font-medium">
Running...
</span>
{feature.startedAt && (
<CountUpTimer startedAt={feature.startedAt} className="text-purple-400" />
<CountUpTimer
startedAt={feature.startedAt}
className="text-purple-400"
/>
)}
</div>
)}
{/* Show timer for in_progress cards that aren't currently running */}
{!isCurrentAutoTask && feature.status === "in_progress" && feature.startedAt && (
<div className="absolute top-2 right-2">
<CountUpTimer startedAt={feature.startedAt} className="text-yellow-500" />
</div>
)}
{!isCurrentAutoTask &&
feature.status === "in_progress" &&
feature.startedAt && (
<div className="absolute top-2 right-2">
<CountUpTimer
startedAt={feature.startedAt}
className="text-yellow-500"
/>
</div>
)}
<div className="flex items-start gap-2">
{isDraggable && (
<div
@@ -158,7 +252,22 @@ export function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify,
)}
{!isCurrentAutoTask && feature.status === "in_progress" && (
<>
{hasContext && onResume ? (
{/* skipTests features show manual verify button */}
{feature.skipTests && onManualVerify ? (
<Button
variant="default"
size="sm"
className="flex-1 h-7 text-xs bg-green-600 hover:bg-green-700"
onClick={(e) => {
e.stopPropagation();
onManualVerify();
}}
data-testid={`manual-verify-${feature.id}`}
>
<CheckCircle2 className="w-3 h-3 mr-1" />
Verify
</Button>
) : hasContext && onResume ? (
<Button
variant="default"
size="sm"
@@ -184,10 +293,10 @@ export function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify,
data-testid={`verify-feature-${feature.id}`}
>
<PlayCircle className="w-3 h-3 mr-1" />
Implement
Resume
</Button>
) : null}
{onViewOutput && (
{onViewOutput && !feature.skipTests && (
<Button
variant="ghost"
size="sm"
@@ -206,17 +315,56 @@ export function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify,
variant="ghost"
size="sm"
className="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
onClick={handleDeleteClick}
data-testid={`delete-inprogress-feature-${feature.id}`}
>
<Trash2 className="w-3 h-3" />
</Button>
</>
)}
{!isCurrentAutoTask && feature.status !== "in_progress" && (
{!isCurrentAutoTask && feature.status === "verified" && (
<>
{/* Move back button for skipTests verified features */}
{feature.skipTests && onMoveBackToInProgress && (
<Button
variant="ghost"
size="sm"
className="h-7 text-xs text-yellow-500 hover:text-yellow-500 hover:bg-yellow-500/10"
onClick={(e) => {
e.stopPropagation();
onMoveBackToInProgress();
}}
data-testid={`move-back-${feature.id}`}
>
<ArrowLeft className="w-3 h-3 mr-1" />
Back
</Button>
)}
<Button
variant="ghost"
size="sm"
className="flex-1 h-7 text-xs"
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
data-testid={`edit-feature-${feature.id}`}
>
<Edit className="w-3 h-3 mr-1" />
Edit
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={handleDeleteClick}
data-testid={`delete-feature-${feature.id}`}
>
<Trash2 className="w-3 h-3" />
</Button>
</>
)}
{!isCurrentAutoTask && feature.status === "backlog" && (
<>
<Button
variant="ghost"
@@ -235,10 +383,7 @@ export function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify,
variant="ghost"
size="sm"
className="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
onClick={handleDeleteClick}
data-testid={`delete-feature-${feature.id}`}
>
<Trash2 className="w-3 h-3" />
@@ -247,6 +392,34 @@ export function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify,
)}
</div>
</CardContent>
{/* Delete Confirmation Dialog */}
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<DialogContent data-testid="delete-confirmation-dialog">
<DialogHeader>
<DialogTitle>Delete Feature</DialogTitle>
<DialogDescription>
Are you sure you want to delete this feature? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="ghost"
onClick={handleCancelDelete}
data-testid="cancel-delete-button"
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleConfirmDelete}
data-testid="confirm-delete-button"
>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
);
}