diff --git a/CLAUDE.md b/CLAUDE.md index ea41ef7..11fb2ec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,6 +29,10 @@ Pre-implementation design docs are archived in `docs/archive/`. - **Hexagonal architecture**: Repository ports in `src/db/repositories/*.ts`, Drizzle adapters in `src/db/repositories/drizzle/*.ts`. All re-exported from `src/db/index.ts`. - **tRPC context**: Optional repos accessed via `require*Repository()` helpers in `src/trpc/routers/_helpers.ts`. +## UI Patterns + +- **Shift+click to skip confirmation**: Destructive actions (delete task, etc.) show a `window.confirm()` dialog on click. Holding Shift bypasses the dialog and executes immediately. Apply this pattern to all new destructive buttons. + ## Build ```sh diff --git a/docs/server-api.md b/docs/server-api.md index 26e692c..f761549 100644 --- a/docs/server-api.md +++ b/docs/server-api.md @@ -78,6 +78,7 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE | listInitiativeTasks | query | All tasks for initiative | | listPhaseTasks | query | All tasks for phase | | listPendingApprovals | query | Tasks with status=pending_approval | +| deleteTask | mutation | Delete a task by ID | | approveTask | mutation | Approve and complete task | ### Initiatives diff --git a/packages/web/src/components/TaskRow.tsx b/packages/web/src/components/TaskRow.tsx index bbbbb37..6803a8c 100644 --- a/packages/web/src/components/TaskRow.tsx +++ b/packages/web/src/components/TaskRow.tsx @@ -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 */}
+ + {/* Delete button */} + {onDelete && ( + + )}
{/* Dependency indicator below the row */} diff --git a/packages/web/src/components/execution/PhaseDetailPanel.tsx b/packages/web/src/components/execution/PhaseDetailPanel.tsx index f59ac62..26352d1 100644 --- a/packages/web/src/components/execution/PhaseDetailPanel.tsx +++ b/packages/web/src/components/execution/PhaseDetailPanel.tsx @@ -63,6 +63,14 @@ export function PhaseDetailPanel({ const inputRef = useRef(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 })} /> ))} diff --git a/src/trpc/routers/task.ts b/src/trpc/routers/task.ts index a817cf9..960d89b 100644 --- a/src/trpc/routers/task.ts +++ b/src/trpc/routers/task.ts @@ -143,6 +143,14 @@ export function taskProcedures(publicProcedure: ProcedureBuilder) { return tasks.filter((t) => t.category !== 'detail'); }), + deleteTask: publicProcedure + .input(z.object({ id: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + const taskRepository = requireTaskRepository(ctx); + await taskRepository.delete(input.id); + return { success: true }; + }), + approveTask: publicProcedure .input(z.object({ taskId: z.string().min(1) })) .mutation(async ({ ctx, input }) => {