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:
@@ -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