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:
@@ -287,6 +287,63 @@ pub async fn set_description(
|
||||
Ok(Json(mission))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateMissionRequest {
|
||||
#[serde(default)]
|
||||
pub title: Option<String>,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// PATCH /api/missions/{id} — edit title + description. Draft-only.
|
||||
pub async fn update_meta(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateMissionRequest>,
|
||||
) -> Result<Json<Mission>, ApiError> {
|
||||
let mission = cm_db::repo::missions::get(&state.pool, id, user.workspace_id.as_uuid())
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
if mission.status != "draft" {
|
||||
return Err(ApiError::BadRequest);
|
||||
}
|
||||
let title = body
|
||||
.title
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty());
|
||||
let description = body.description.as_deref();
|
||||
cm_db::repo::missions::update_meta(
|
||||
&state.pool,
|
||||
id,
|
||||
user.workspace_id.as_uuid(),
|
||||
title,
|
||||
description,
|
||||
)
|
||||
.await?;
|
||||
let mission = cm_db::repo::missions::get(&state.pool, id, user.workspace_id.as_uuid())
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
Ok(Json(mission))
|
||||
}
|
||||
|
||||
/// DELETE /api/missions/{id} — hard-delete. Allowed in any status;
|
||||
/// the operator is expected to Cancel first if a run is in flight
|
||||
/// (cascades will still fire either way).
|
||||
pub async fn delete(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let deleted =
|
||||
cm_db::repo::missions::delete(&state.pool, id, user.workspace_id.as_uuid()).await?;
|
||||
if deleted == 0 {
|
||||
return Err(ApiError::NotFound);
|
||||
}
|
||||
Ok(Json(serde_json::json!({ "deleted": true })))
|
||||
}
|
||||
|
||||
pub async fn set_status(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
|
||||
Reference in New Issue
Block a user