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

@@ -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 ── */