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:
@@ -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`.
|
- **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`.
|
- **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
|
## Build
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE
|
|||||||
| listInitiativeTasks | query | All tasks for initiative |
|
| listInitiativeTasks | query | All tasks for initiative |
|
||||||
| listPhaseTasks | query | All tasks for phase |
|
| listPhaseTasks | query | All tasks for phase |
|
||||||
| listPendingApprovals | query | Tasks with status=pending_approval |
|
| listPendingApprovals | query | Tasks with status=pending_approval |
|
||||||
|
| deleteTask | mutation | Delete a task by ID |
|
||||||
| approveTask | mutation | Approve and complete task |
|
| approveTask | mutation | Approve and complete task |
|
||||||
|
|
||||||
### Initiatives
|
### Initiatives
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { X } from "lucide-react";
|
||||||
import { StatusBadge } from "@/components/StatusBadge";
|
import { StatusBadge } from "@/components/StatusBadge";
|
||||||
import { DependencyIndicator } from "@/components/DependencyIndicator";
|
import { DependencyIndicator } from "@/components/DependencyIndicator";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -27,6 +28,7 @@ interface TaskRowProps {
|
|||||||
blockedBy: Array<{ name: string; status: string }>;
|
blockedBy: Array<{ name: string; status: string }>;
|
||||||
isLast: boolean;
|
isLast: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
onDelete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TaskRow({
|
export function TaskRow({
|
||||||
@@ -35,6 +37,7 @@ export function TaskRow({
|
|||||||
blockedBy,
|
blockedBy,
|
||||||
isLast,
|
isLast,
|
||||||
onClick,
|
onClick,
|
||||||
|
onDelete,
|
||||||
}: TaskRowProps) {
|
}: TaskRowProps) {
|
||||||
const connector = isLast ? "└──" : "├──";
|
const connector = isLast ? "└──" : "├──";
|
||||||
|
|
||||||
@@ -43,7 +46,7 @@ export function TaskRow({
|
|||||||
{/* Task row */}
|
{/* Task row */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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",
|
!isLast && "border-l-2 border-muted-foreground/20",
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
@@ -69,6 +72,22 @@ export function TaskRow({
|
|||||||
|
|
||||||
{/* Status badge */}
|
{/* Status badge */}
|
||||||
<StatusBadge status={task.status} className="shrink-0" />
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Dependency indicator below the row */}
|
{/* Dependency indicator below the row */}
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ export function PhaseDetailPanel({
|
|||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const updatePhase = trpc.updatePhase.useMutation();
|
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() {
|
function startEditing() {
|
||||||
setEditName(phase.name);
|
setEditName(phase.name);
|
||||||
@@ -374,6 +382,7 @@ export function PhaseDetailPanel({
|
|||||||
blockedBy={[]}
|
blockedBy={[]}
|
||||||
isLast={idx === sortedTasks.length - 1}
|
isLast={idx === sortedTasks.length - 1}
|
||||||
onClick={() => setSelectedTaskId(task.id)}
|
onClick={() => setSelectedTaskId(task.id)}
|
||||||
|
onDelete={() => deleteTask.mutate({ id: task.id })}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -143,6 +143,14 @@ export function taskProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
return tasks.filter((t) => t.category !== 'detail');
|
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
|
approveTask: publicProcedure
|
||||||
.input(z.object({ taskId: z.string().min(1) }))
|
.input(z.object({ taskId: z.string().min(1) }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user