missions sidebar: wrench toggle + multi-select bulk delete
Matches the AGENT tier's manage-mode pattern. Three states on the
sidebar toolbar:
- default: [Wrench] [Refresh] [+]
- select mode active: [Wrench] (highlighted) [Refresh] [+] + rows
grow a checkbox on the left and click-toggles selection instead
of opening the mission
- selection made: [Wrench] [Trash · N] [Refresh] [+] where the
trash pill shows the count and fires window.confirm → bulk
DELETE /api/missions/{id} loop
On success: exits select mode, calls onDeleted with the ids, parent
Dashboard clears missionsSel if it was in the batch. Failures stay
selected + a red error line surfaces the count.
Reuses the CRUD backend added earlier (a1c… PATCH/DELETE) — no new
server work.
This commit is contained in:
@@ -6,9 +6,10 @@
|
||||
// UX without breaking existing workflows.
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Plus, RotateCw } from "lucide-react";
|
||||
import { Plus, RotateCw, Trash2, Wrench } from "lucide-react";
|
||||
|
||||
import {
|
||||
deleteMission,
|
||||
listMissions,
|
||||
type Mission,
|
||||
type MissionStatus,
|
||||
@@ -41,16 +42,23 @@ export function MissionsList({
|
||||
onSelect,
|
||||
refreshKey,
|
||||
onCreated,
|
||||
onDeleted,
|
||||
}: {
|
||||
selectedId: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
refreshKey: number;
|
||||
onCreated: (id: string) => void;
|
||||
/** Called after bulk delete completes; parent clears selection if the
|
||||
* currently-open mission was among the deleted rows. */
|
||||
onDeleted?: (deletedIds: string[]) => void;
|
||||
}) {
|
||||
const [missions, setMissions] = useState<Mission[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [wizardOpen, setWizardOpen] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectMode, setSelectMode] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [bulkBusy, setBulkBusy] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setError(null);
|
||||
@@ -97,6 +105,77 @@ export function MissionsList({
|
||||
MISSIONS · {missions.length}
|
||||
</span>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectMode((v) => {
|
||||
if (v) setSelectedIds(new Set());
|
||||
return !v;
|
||||
});
|
||||
}}
|
||||
title={selectMode ? "Exit select mode" : "Select missions to manage"}
|
||||
aria-label="Toggle select mode"
|
||||
style={{
|
||||
...iconBtn,
|
||||
color: selectMode ? "#ff8a7a" : "#9a9aa2",
|
||||
borderColor: selectMode
|
||||
? "rgba(255,138,122,.5)"
|
||||
: "rgba(255,255,255,.12)",
|
||||
background: selectMode ? "rgba(255,138,122,.08)" : "transparent",
|
||||
}}
|
||||
>
|
||||
<Wrench size={13} />
|
||||
</button>
|
||||
{selectMode && selectedIds.size > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (bulkBusy) return;
|
||||
const ids = Array.from(selectedIds);
|
||||
const ok = window.confirm(
|
||||
`Delete ${ids.length} mission${ids.length === 1 ? "" : "s"}? Cascades to phases, tasks, artifacts, and benchmark snapshots.`,
|
||||
);
|
||||
if (!ok) return;
|
||||
setBulkBusy(true);
|
||||
setError(null);
|
||||
const failures: string[] = [];
|
||||
for (const id of ids) {
|
||||
try {
|
||||
await deleteMission(id);
|
||||
} catch (e) {
|
||||
failures.push(id);
|
||||
console.error(`delete ${id}`, e);
|
||||
}
|
||||
}
|
||||
setBulkBusy(false);
|
||||
const deletedIds = ids.filter((id) => !failures.includes(id));
|
||||
setSelectedIds(new Set(failures));
|
||||
if (failures.length === 0) setSelectMode(false);
|
||||
else
|
||||
setError(`${failures.length} of ${ids.length} deletes failed`);
|
||||
onDeleted?.(deletedIds);
|
||||
await load();
|
||||
}}
|
||||
disabled={bulkBusy}
|
||||
title={`Delete ${selectedIds.size} selected`}
|
||||
aria-label="Delete selected missions"
|
||||
style={{
|
||||
...iconBtn,
|
||||
color: "#ff8a7a",
|
||||
borderColor: "rgba(255,138,122,.5)",
|
||||
background: "rgba(255,138,122,.1)",
|
||||
opacity: bulkBusy ? 0.5 : 1,
|
||||
width: "auto",
|
||||
padding: "0 8px",
|
||||
gap: 4,
|
||||
fontFamily: mono,
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
{selectedIds.size}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={load}
|
||||
@@ -162,13 +241,25 @@ export function MissionsList({
|
||||
</div>
|
||||
) : (
|
||||
missions.map((m) => {
|
||||
const active = selectedId === m.id;
|
||||
const active = !selectMode && selectedId === m.id;
|
||||
const picked = selectMode && selectedIds.has(m.id);
|
||||
const badge = TEMPLATE_BADGE[m.template_kind] ?? TEMPLATE_BADGE.custom;
|
||||
return (
|
||||
<button
|
||||
key={m.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(m.id)}
|
||||
onClick={() => {
|
||||
if (selectMode) {
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(m.id)) next.delete(m.id);
|
||||
else next.add(m.id);
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
onSelect(m.id);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
@@ -177,13 +268,43 @@ export function MissionsList({
|
||||
gap: 4,
|
||||
padding: "10px 11px",
|
||||
borderRadius: 9,
|
||||
border: `1px solid ${active ? "rgba(94,200,216,.5)" : "rgba(255,255,255,.07)"}`,
|
||||
background: active ? "rgba(94,200,216,.08)" : "#101013",
|
||||
border: `1px solid ${
|
||||
picked
|
||||
? "rgba(255,138,122,.55)"
|
||||
: active
|
||||
? "rgba(94,200,216,.5)"
|
||||
: "rgba(255,255,255,.07)"
|
||||
}`,
|
||||
background: picked
|
||||
? "rgba(255,138,122,.1)"
|
||||
: active
|
||||
? "rgba(94,200,216,.08)"
|
||||
: "#101013",
|
||||
cursor: "pointer",
|
||||
color: "#eaeaee",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
{selectMode && (
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 3,
|
||||
border: `1px solid ${picked ? "#ff8a7a" : "rgba(255,255,255,.25)"}`,
|
||||
background: picked ? "#ff8a7a" : "transparent",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "#101013",
|
||||
fontSize: 10,
|
||||
flex: "none",
|
||||
}}
|
||||
>
|
||||
{picked ? "✓" : ""}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
width: 7,
|
||||
|
||||
Reference in New Issue
Block a user