mission canvas: add / edit / delete toolbar controls

Top-right toolbar grows three CRUD controls per your request:

  - Plus (always visible) — opens MissionWizard, selects the new
    mission on create
  - Pencil (draft-only) — opens EditMissionModal for title +
    description; PATCHes /api/missions/{id}
  - Trash (always visible) — window.confirm then DELETEs; sidebar
    selection clears via new onDeleted callback

Backend:
  - cm-db::repo::missions::update_meta(id, ws, title?, description?)
    — COALESCE-based partial patch
  - cm-db::repo::missions::delete(id, ws) — hard delete, cascades
    via FKs on phases/tasks/artifacts/benchmark_snapshots
  - PATCH /api/missions/{id} (draft-only) + DELETE /api/missions/{id}

Frontend:
  - lib/api/missions — updateMission + deleteMission clients
  - MissionCanvas — three toolbar buttons, EditMissionModal
    (title + textarea for description), local wizard state
  - Dashboard — passes onSelect + onDeleted so sidebar reacts to
    create + delete without stale selection

Edit is draft-only (backend enforces + button hidden past draft) so
in-flight missions can't have their brief mutated out from under
running agents. Delete is unconditional — operator responsibility to
Cancel first if a run is live.
This commit is contained in:
Omar Sobh
2026-07-20 08:32:52 -07:00
parent 4c32799906
commit 1f0117e35a
6 changed files with 408 additions and 2 deletions
+41
View File
@@ -252,6 +252,47 @@ pub async fn set_description(
Ok(())
}
/// Patch title + description in one shot. Either field `None` = leave
/// as-is (uses COALESCE so partial edits don't clobber the other).
pub async fn update_meta(
pool: &PgPool,
id: Uuid,
workspace_id: Uuid,
title: Option<&str>,
description: Option<&str>,
) -> Result<(), DbError> {
sqlx::query(
"UPDATE missions
SET title = COALESCE($3, title),
description = COALESCE($4, description),
updated_at = now()
WHERE id = $1 AND workspace_id = $2",
)
.bind(id)
.bind(workspace_id)
.bind(title)
.bind(description)
.execute(pool)
.await?;
Ok(())
}
/// Hard-delete a mission. Cascades via FKs on mission_phases /
/// mission_tasks / mission_artifacts / benchmark_snapshots (all
/// declared ON DELETE CASCADE in 0047).
pub async fn delete(
pool: &PgPool,
id: Uuid,
workspace_id: Uuid,
) -> Result<u64, DbError> {
let r = sqlx::query("DELETE FROM missions WHERE id = $1 AND workspace_id = $2")
.bind(id)
.bind(workspace_id)
.execute(pool)
.await?;
Ok(r.rows_affected())
}
pub async fn set_status(
pool: &PgPool,
id: Uuid,