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:
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user