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,
|
useSensors,
|
||||||
type DragEndEvent,
|
type DragEndEvent,
|
||||||
} from '@dnd-kit/core'
|
} from '@dnd-kit/core'
|
||||||
import { Bot, Plus } from 'lucide-react'
|
import { Bot, Plus, Trash2 } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { AppShell } from '@/components/AppShell'
|
import { AppShell } from '@/components/AppShell'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
@@ -554,6 +554,21 @@ function TaskDrawer({
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ internal sealed class WorkItem : Entity
|
|||||||
UpdatedAtUtc = nowUtc;
|
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>
|
/// <summary>Appends an approved agent artifact (spec / test plan) to the task.</summary>
|
||||||
public void AttachArtifact(string content, DateTimeOffset nowUtc)
|
public void AttachArtifact(string content, DateTimeOffset nowUtc)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ internal static class OrgBoardEndpoints
|
|||||||
group.MapGet("/board", GetBoard).RequireAuthorization();
|
group.MapGet("/board", GetBoard).RequireAuthorization();
|
||||||
group.MapPatch("/tasks/{id:guid}/move", MoveTask).RequireAuthorization();
|
group.MapPatch("/tasks/{id:guid}/move", MoveTask).RequireAuthorization();
|
||||||
group.MapPatch("/tasks/{id:guid}/assign", AssignTask).RequireAuthorization();
|
group.MapPatch("/tasks/{id:guid}/assign", AssignTask).RequireAuthorization();
|
||||||
|
group.MapDelete("/tasks/{id:guid}", DeleteTask).RequireAuthorization();
|
||||||
group.MapGet("/cartable", Cartable).RequireAuthorization();
|
group.MapGet("/cartable", Cartable).RequireAuthorization();
|
||||||
|
|
||||||
group.MapPost("/seats", CreateSeat).RequireAuthorization();
|
group.MapPost("/seats", CreateSeat).RequireAuthorization();
|
||||||
@@ -273,6 +274,36 @@ internal static class OrgBoardEndpoints
|
|||||||
return Results.Ok(ToResponse(item));
|
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(
|
private static async Task<IResult> GetBoard(
|
||||||
Guid teamId, IPermissionService permissions, OrgBoardDbContext db, CancellationToken ct)
|
Guid teamId, IPermissionService permissions, OrgBoardDbContext db, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user