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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 ── */
|
||||
|
||||
Reference in New Issue
Block a user