Delete tasks from the board

Adds DELETE /api/orgboard/tasks/{id} (WorkTasks permission) and a "Delete task" button
in the task drawer (with confirm). Children are detached (kept as top-level) rather than
deleted; status-transition history is dropped. There was previously no way to remove a task.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-16 22:03:14 +03:30
parent cb9ce34309
commit 9993ebb2b4
3 changed files with 50 additions and 1 deletions
+16 -1
View File
@@ -8,7 +8,7 @@ import {
useSensors,
type DragEndEvent,
} from '@dnd-kit/core'
import { Bot, Plus } from 'lucide-react'
import { Bot, Plus, Trash2 } from 'lucide-react'
import { toast } from 'sonner'
import { AppShell } from '@/components/AppShell'
import { Badge } from '@/components/ui/badge'
@@ -554,6 +554,21 @@ function TaskDrawer({
</div>
</div>
)}
<div className="mt-2 border-t pt-4">
<Button
variant="destructive"
size="sm"
disabled={busy}
onClick={() => {
if (window.confirm(`Delete "${task.title}"? This can't be undone.`)) {
act(() => api.del(`/api/orgboard/tasks/${task.id}`), 'Task deleted.').then(onClose)
}
}}
>
<Trash2 data-icon="inline-start" /> Delete task
</Button>
</div>
</SheetContent>
</Sheet>
)
@@ -69,6 +69,9 @@ internal sealed class WorkItem : Entity
UpdatedAtUtc = nowUtc;
}
/// <summary>Detach from a parent (used when the parent is deleted, so the child stays on the board).</summary>
public void ClearParent() => ParentId = null;
/// <summary>Appends an approved agent artifact (spec / test plan) to the task.</summary>
public void AttachArtifact(string content, DateTimeOffset nowUtc)
{
@@ -30,6 +30,7 @@ internal static class OrgBoardEndpoints
group.MapGet("/board", GetBoard).RequireAuthorization();
group.MapPatch("/tasks/{id:guid}/move", MoveTask).RequireAuthorization();
group.MapPatch("/tasks/{id:guid}/assign", AssignTask).RequireAuthorization();
group.MapDelete("/tasks/{id:guid}", DeleteTask).RequireAuthorization();
group.MapGet("/cartable", Cartable).RequireAuthorization();
group.MapPost("/seats", CreateSeat).RequireAuthorization();
@@ -273,6 +274,36 @@ internal static class OrgBoardEndpoints
return Results.Ok(ToResponse(item));
}
// Remove a task from the board. Its children are detached (kept as top-level) rather than deleted,
// and its status-transition history is dropped. Any agent runs/reviews it spawned are left as history.
private static async Task<IResult> DeleteTask(
Guid id, ICurrentUser user, IPermissionService permissions,
IAuditLog audit, OrgBoardDbContext db, CancellationToken ct)
{
var item = await db.WorkItems.FirstOrDefaultAsync(w => w.Id == id, ct);
if (item is null)
{
return Results.NotFound();
}
var team = await db.Teams.FirstOrDefaultAsync(t => t.Id == item.TeamId, ct);
if (team is null || !permissions.Has(Capability.WorkTasks, ScopeRef.Team(item.TeamId), ScopeRef.Org(team.OrganizationId)))
{
return Results.Forbid();
}
foreach (var child in await db.WorkItems.Where(w => w.ParentId == id).ToListAsync(ct))
{
child.ClearParent();
}
db.Transitions.RemoveRange(await db.Transitions.Where(t => t.WorkItemId == id).ToListAsync(ct));
db.WorkItems.Remove(item);
await db.SaveChangesAsync(ct);
await audit.WriteAsync(new AuditEvent("task.deleted", "WorkItem", id, user.MemberId, item.Title), ct);
return Results.NoContent();
}
private static async Task<IResult> GetBoard(
Guid teamId, IPermissionService permissions, OrgBoardDbContext db, CancellationToken ct)
{