feat(FN-792): redesign Mission Manager detail hierarchy, actions, and responsive layout

- Redesign detail panel hierarchy with clearer visual structure and action layout
- Add responsive behavior for mission detail views across screen sizes
- Update MissionManager component with improved action buttons and layout
- Add comprehensive tests for MissionManager component (172 lines)
- Update Mission Manager description in README
This commit is contained in:
gsxdsm
2026-04-04 09:40:57 -07:00
parent 4f6326e1cb
commit 3a51bf6e15
4 changed files with 305 additions and 5 deletions

View File

@@ -81,7 +81,7 @@ fn mission delete M-LZ7DN0-A2B5 [--force] # Delet
fn mission activate-slice SL-P4T2WX-D5E8 # Manually activate slice
```
**Dashboard:** Click the Target icon in the dashboard header to open the Mission Manager, then create missions, add milestones, slices, and features. Link features to tasks for automatic progress tracking. On mobile devices, the Mission Manager opens as a full-screen overlay with touch-friendly controls and stacked hierarchy cards.
**Dashboard:** Click the Target icon in the dashboard header to open the Mission Manager, then create missions, add milestones, slices, and features. Link features to tasks for automatic progress tracking. The detail view shows a visual hierarchy with left-border accents distinguishing milestones, slices, and features, with inline edit and delete controls. On mobile devices (≤768px), the Mission Manager opens as a full-screen overlay with touch-friendly controls, stacked action rows, and no horizontal overflow.
## Quick Start

View File

@@ -699,8 +699,75 @@ export function MissionManager({ isOpen, onClose, addToast, projectId, onSelectT
{selectedMission.milestones.length} milestones
</span>
</div>
<div className="mission-detail__actions">
<button
className="mission-icon-btn"
onClick={() => handleEditMission(selectedMission)}
title="Edit mission"
aria-label="Edit mission"
>
<Pencil size={14} />
</button>
<button
className="mission-icon-btn mission-icon-btn--danger"
onClick={() => setDeleteConfirmId({ type: "mission", id: selectedMission.id })}
title="Delete mission"
aria-label="Delete mission"
>
<Trash2 size={14} />
</button>
</div>
</div>
{/* Inline edit mission form (detail view) */}
{editingMissionId === selectedMission.id && (
<div className="mission-form-card">
<input
type="text"
placeholder="Mission title"
value={missionForm.title}
onChange={(e) => setMissionForm({ ...missionForm, title: e.target.value })}
onKeyDown={handleMissionFormKeyDown}
autoFocus
/>
<textarea
placeholder="Description (optional)"
value={missionForm.description}
onChange={(e) => setMissionForm({ ...missionForm, description: e.target.value })}
rows={2}
/>
<div className="mission-form-card__row">
<select
value={missionForm.status}
onChange={(e) => setMissionForm({ ...missionForm, status: e.target.value as MissionStatus })}
>
<option value="planning">Planning</option>
<option value="active">Active</option>
<option value="blocked">Blocked</option>
<option value="complete">Complete</option>
<option value="archived">Archived</option>
</select>
<label className="mission-checkbox">
<input
type="checkbox"
checked={missionForm.autoAdvance}
onChange={(e) => setMissionForm({ ...missionForm, autoAdvance: e.target.checked })}
/>
Auto-advance slices
</label>
</div>
<div className="mission-form-card__actions">
<button className="mission-btn mission-btn--primary" onClick={handleSaveMission} disabled={saving}>
{saving ? <Loader2 size={14} className="spinner" /> : <Check size={14} />}
Update
</button>
<button className="mission-btn mission-btn--ghost" onClick={handleCancelMission}>
Cancel
</button>
</div>
</div>
)}
<div className="mission-detail__milestones">
{selectedMission.milestones.map((milestone) => (
<div key={milestone.id} className="mission-milestone">
@@ -1261,7 +1328,7 @@ export function MissionManager({ isOpen, onClose, addToast, projectId, onSelectT
{/* ── Delete confirmation panel ── */}
{deleteConfirmId && (
<div className="mission-confirm-panel">
<div className="mission-confirm-panel mission-confirm-panel--danger">
<div className="mission-confirm-panel__content">
<p>
Delete this {deleteConfirmId.type}? This cannot be undone.
@@ -1293,7 +1360,7 @@ export function MissionManager({ isOpen, onClose, addToast, projectId, onSelectT
{/* ── Link task panel ── */}
{linkTaskFeatureId && (
<div className="mission-confirm-panel">
<div className="mission-confirm-panel mission-confirm-panel--link">
<div className="mission-confirm-panel__content">
<p>Link feature to task:</p>
<input

View File

@@ -502,4 +502,176 @@ describe("MissionManager", () => {
});
});
});
// ── Step 2: Detail hierarchy, action layout, confirm panels ──────────
describe("detail view hierarchy and action layout", () => {
it("renders full milestone → slice → feature hierarchy in detail", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
// Milestone auto-expanded
expect(screen.getByText("Database Schema")).toBeDefined();
// Slice auto-expanded
expect(screen.getByText("User Tables")).toBeDefined();
// Feature visible
expect(screen.getByText("User model")).toBeDefined();
// Feature status badge
expect(screen.getByText("defined")).toBeDefined();
// Acceptance criteria
expect(screen.getByText(/Model exists with required fields/)).toBeDefined();
});
});
it("shows edit and delete mission buttons in detail header", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
});
// Detail header should have edit/delete buttons
const editBtns = screen.getAllByLabelText("Edit mission");
const deleteBtns = screen.getAllByLabelText("Delete mission");
// At least one of each in the detail header area
expect(editBtns.length).toBeGreaterThanOrEqual(1);
expect(deleteBtns.length).toBeGreaterThanOrEqual(1);
});
it("opens inline edit form when edit mission is clicked in detail view", async () => {
let callCount = 0;
globalThis.fetch = vi.fn().mockImplementation(() => {
callCount++;
if (callCount === 1) return Promise.resolve(mockApiResponse(mockMissions));
return Promise.resolve(mockApiResponse(mockMissionDetail));
});
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
});
// Click edit mission in detail header
const editBtns = screen.getAllByLabelText("Edit mission");
fireEvent.click(editBtns[0]);
// Should show inline form with pre-filled title
await waitFor(() => {
const input = screen.getByDisplayValue("Build Auth System");
expect(input).toBeDefined();
expect(screen.getByText("Update")).toBeDefined();
expect(screen.getByText("Cancel")).toBeDefined();
});
});
it("shows delete confirmation with danger variant class", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
});
// Click delete mission in detail header
const deleteBtns = screen.getAllByLabelText("Delete mission");
fireEvent.click(deleteBtns[0]);
// Confirmation panel should show
await waitFor(() => {
const confirmPanel = screen.getByText(/Delete this mission/).closest(".mission-confirm-panel");
expect(confirmPanel).toBeDefined();
expect(confirmPanel!.className).toContain("mission-confirm-panel--danger");
});
});
it("shows milestone count in detail header meta", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByText("1 milestones")).toBeDefined();
});
});
it("shows slice and feature counts in hierarchy headers", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByText("1 slices")).toBeDefined();
expect(screen.getByText("1 features")).toBeDefined();
});
});
it("renders milestone expand/collapse chevrons", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
// Milestone is auto-expanded — should see the title visible
expect(screen.getByText("Database Schema")).toBeDefined();
// Slice visible (auto-expanded)
expect(screen.getByText("User Tables")).toBeDefined();
});
});
it("shows add milestone button in detail view", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByText("Add Milestone")).toBeDefined();
});
});
});
});

View File

@@ -16327,10 +16327,23 @@ html .column.drag-over * {
/* ── Confirm & Link Panels ── */
.mission-confirm-panel {
flex-shrink: 0;
border-top: 1px solid var(--border);
background: var(--card);
}
/* Delete-confirmation variant — danger tint */
.mission-confirm-panel--danger {
background: rgba(248, 81, 73, 0.05);
border-top-color: var(--color-error);
}
/* Link-task variant — accent tint */
.mission-confirm-panel--link {
background: rgba(88, 166, 255, 0.04);
border-top-color: var(--todo);
}
.mission-confirm-panel__content {
display: flex;
flex-direction: column;
@@ -16478,12 +16491,15 @@ html .column.drag-over * {
/* ================================================================
Mission Detail View
================================================================ */
/* ── Detail root ── */
.mission-detail {
display: flex;
flex-direction: column;
gap: var(--space-lg);
}
/* ── Detail header (title + meta + actions) ── */
.mission-detail__header {
display: flex;
flex-direction: column;
@@ -16536,6 +16552,14 @@ html .column.drag-over * {
color: var(--text-muted);
}
/* ── Detail-level action bar (edit / delete the mission itself) ── */
.mission-detail__actions {
display: flex;
align-items: center;
gap: var(--space-sm);
padding-top: var(--space-xs);
}
/* ── Milestones ── */
.mission-detail__milestones {
display: flex;
@@ -16546,8 +16570,14 @@ html .column.drag-over * {
.mission-milestone {
background: var(--card);
border: 1px solid var(--border);
border-left: 3px solid var(--text-dim);
border-radius: var(--radius-md);
overflow: hidden;
transition: border-color var(--transition-fast);
}
.mission-milestone:hover {
border-left-color: var(--text-muted);
}
.mission-milestone__header {
@@ -16606,6 +16636,7 @@ html .column.drag-over * {
align-items: center;
gap: 2px;
flex-shrink: 0;
margin-left: var(--space-xs);
}
.mission-milestone__body {
@@ -16613,7 +16644,7 @@ html .column.drag-over * {
border-top: 1px solid var(--border);
}
/* ── Slices ── */
/* ── Slices (nested inside milestones) ── */
.mission-slices {
display: flex;
flex-direction: column;
@@ -16624,6 +16655,7 @@ html .column.drag-over * {
.mission-slice {
background: var(--surface);
border: 1px solid var(--border);
border-left: 3px solid var(--todo);
border-radius: var(--radius-md);
overflow: hidden;
}
@@ -16686,6 +16718,7 @@ html .column.drag-over * {
align-items: center;
gap: 2px;
flex-shrink: 0;
margin-left: var(--space-xs);
}
.mission-slice__body {
@@ -16693,7 +16726,7 @@ html .column.drag-over * {
border-top: 1px solid var(--border);
}
/* ── Features ── */
/* ── Features (nested inside slices) ── */
.mission-features {
display: flex;
flex-direction: column;
@@ -16704,6 +16737,7 @@ html .column.drag-over * {
.mission-feature {
background: var(--card);
border: 1px solid var(--border);
border-left: 3px solid var(--text-dim);
border-radius: var(--radius-sm);
padding: var(--space-sm) var(--space-md);
}
@@ -16801,25 +16835,47 @@ html .column.drag-over * {
font-size: 16px;
}
/* Detail header actions stack on mobile */
.mission-detail__actions {
flex-wrap: wrap;
}
/* Milestone headers wrap actions below title row */
.mission-milestone__header {
padding: var(--space-sm) var(--space-md);
flex-wrap: wrap;
gap: var(--space-xs);
}
.mission-milestone__actions {
width: 100%;
justify-content: flex-end;
margin-left: 0;
padding-top: var(--space-xs);
}
.mission-milestone__body {
padding: 0 var(--space-sm) var(--space-sm);
}
/* Slice headers wrap similarly */
.mission-slice__header {
padding: var(--space-sm);
gap: var(--space-xs);
}
.mission-slice__actions {
width: 100%;
justify-content: flex-end;
margin-left: 0;
padding-top: var(--space-xs);
}
.mission-slice__body {
padding: 0 var(--space-sm) var(--space-sm);
}
/* Feature actions wrap cleanly */
.mission-feature {
padding: var(--space-sm);
}
@@ -16855,6 +16911,11 @@ html .column.drag-over * {
align-items: flex-start;
gap: var(--space-sm);
}
/* Confirm panels: full-width, no horizontal overflow */
.mission-confirm-panel__content {
padding: var(--space-md);
}
}
/* ── Workflow Results ── */