feat: Add task deletion with shift+click auto-confirm

- Add deleteTask tRPC mutation (repo already had delete method)
- Add X button to TaskRow, hidden until hover, with confirmation dialog
- Shift+click bypasses confirmation for fast bulk deletion
- Invalidates listInitiativeTasks on success
- Document shift+click pattern in CLAUDE.md as standard for destructive actions
This commit is contained in:
Lukas May
2026-02-10 15:58:24 +01:00
parent bfefbc85af
commit 62a542116d
5 changed files with 42 additions and 1 deletions

View File

@@ -1,4 +1,5 @@
import { Link } from "@tanstack/react-router";
import { X } from "lucide-react";
import { StatusBadge } from "@/components/StatusBadge";
import { DependencyIndicator } from "@/components/DependencyIndicator";
import { cn } from "@/lib/utils";
@@ -27,6 +28,7 @@ interface TaskRowProps {
blockedBy: Array<{ name: string; status: string }>;
isLast: boolean;
onClick: () => void;
onDelete?: () => void;
}
export function TaskRow({
@@ -35,6 +37,7 @@ export function TaskRow({
blockedBy,
isLast,
onClick,
onDelete,
}: TaskRowProps) {
const connector = isLast ? "└──" : "├──";
@@ -43,7 +46,7 @@ export function TaskRow({
{/* Task row */}
<div
className={cn(
"flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-accent",
"group flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-accent",
!isLast && "border-l-2 border-muted-foreground/20",
)}
onClick={onClick}
@@ -69,6 +72,22 @@ export function TaskRow({
{/* Status badge */}
<StatusBadge status={task.status} className="shrink-0" />
{/* Delete button */}
{onDelete && (
<button
className="shrink-0 rounded p-0.5 text-muted-foreground opacity-0 transition-opacity hover:text-destructive group-hover:opacity-100"
title="Delete task (Shift+click to skip confirmation)"
onClick={(e) => {
e.stopPropagation();
if (e.shiftKey || window.confirm(`Delete "${task.name}"?`)) {
onDelete();
}
}}
>
<X className="h-3.5 w-3.5" />
</button>
)}
</div>
{/* Dependency indicator below the row */}

View File

@@ -63,6 +63,14 @@ export function PhaseDetailPanel({
const inputRef = useRef<HTMLInputElement>(null);
const updatePhase = trpc.updatePhase.useMutation();
const utils = trpc.useUtils();
const deleteTask = trpc.deleteTask.useMutation({
onSuccess: () => {
utils.listInitiativeTasks.invalidate({ initiativeId });
toast.success("Task deleted");
},
onError: () => toast.error("Failed to delete task"),
});
function startEditing() {
setEditName(phase.name);
@@ -374,6 +382,7 @@ export function PhaseDetailPanel({
blockedBy={[]}
isLast={idx === sortedTasks.length - 1}
onClick={() => setSelectedTaskId(task.id)}
onDelete={() => deleteTask.mutate({ id: task.id })}
/>
))}
</div>