FN-5665: allow editing mission target branch

Enable mission target branch to be viewed and updated from the mission edit form.

- Add `baseBranch` to mission form state and initialize it when opening edit mode.
- Include trimmed `baseBranch` in mission PATCH updates.
- Add target branch input to both MissionManager edit form render paths.
- Add MissionManager tests for pre-filling and saving edited target branch values.

Files changed:
 .../dashboard/app/components/MissionManager.tsx    | 24 ++++++++
 .../components/__tests__/MissionManager.test.tsx   | 66 ++++++++++++++++++++++
 2 files changed, 90 insertions(+)

Fusion-Task-Id: FN-5665
Fusion-Task-Lineage: b25cf171-b681-4dd8-b5f4-70f0e8714774
This commit is contained in:
gsxdsm
2026-05-29 07:45:42 -07:00
parent 52b6e2bb52
commit 6e30060079
2 changed files with 90 additions and 0 deletions

View File

@@ -253,6 +253,7 @@ interface MissionFormData {
description: string; description: string;
status: MissionStatus; status: MissionStatus;
autopilotEnabled: boolean; autopilotEnabled: boolean;
baseBranch: string;
} }
interface MilestoneFormData { interface MilestoneFormData {
@@ -281,6 +282,7 @@ const EMPTY_MISSION_FORM: MissionFormData = {
description: "", description: "",
status: "planning", status: "planning",
autopilotEnabled: false, autopilotEnabled: false,
baseBranch: "",
}; };
const EMPTY_MILESTONE_FORM: MilestoneFormData = { const EMPTY_MILESTONE_FORM: MilestoneFormData = {
@@ -1407,6 +1409,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
description: mission.description || "", description: mission.description || "",
status: mission.status, status: mission.status,
autopilotEnabled: mission.autopilotEnabled ?? false, autopilotEnabled: mission.autopilotEnabled ?? false,
baseBranch: mission.baseBranch ?? "",
}); });
}, []); }, []);
@@ -1439,6 +1442,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
description: missionForm.description.trim() || undefined, description: missionForm.description.trim() || undefined,
status: missionForm.status, status: missionForm.status,
autopilotEnabled: missionForm.autopilotEnabled, autopilotEnabled: missionForm.autopilotEnabled,
baseBranch: missionForm.baseBranch.trim() || "",
}; };
if (missionForm.autopilotEnabled) { if (missionForm.autopilotEnabled) {
updates.autoAdvance = true; updates.autoAdvance = true;
@@ -2436,6 +2440,16 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
onChange={(e) => setMissionForm({ ...missionForm, description: e.target.value })} onChange={(e) => setMissionForm({ ...missionForm, description: e.target.value })}
rows={2} rows={2}
/> />
<label>
Target branch
<input
type="text"
placeholder="e.g. main"
value={missionForm.baseBranch}
onChange={(e) => setMissionForm({ ...missionForm, baseBranch: e.target.value })}
aria-label="Mission target branch"
/>
</label>
<div className="mission-form-card__row"> <div className="mission-form-card__row">
<select <select
value={missionForm.status} value={missionForm.status}
@@ -4083,6 +4097,16 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
onChange={(e) => setMissionForm({ ...missionForm, description: e.target.value })} onChange={(e) => setMissionForm({ ...missionForm, description: e.target.value })}
rows={2} rows={2}
/> />
<label>
Target branch
<input
type="text"
placeholder="e.g. main"
value={missionForm.baseBranch}
onChange={(e) => setMissionForm({ ...missionForm, baseBranch: e.target.value })}
aria-label="Mission target branch"
/>
</label>
<div className="mission-form-card__row"> <div className="mission-form-card__row">
<select <select
value={missionForm.status} value={missionForm.status}

View File

@@ -2923,6 +2923,72 @@ describe("MissionManager", () => {
}); });
}); });
it("pre-fills target branch when editing a mission", async () => {
const missionDetailWithBranch = {
...mockMissionDetail,
baseBranch: "develop",
};
globalThis.fetch = createDetailFetchMockForMissionDetail(missionDetailWithBranch);
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitForDetailLoaded();
fireEvent.click(screen.getAllByLabelText("Edit mission")[0]);
const targetBranchInput = await screen.findByLabelText("Mission target branch");
expect(targetBranchInput).toHaveValue("develop");
});
it("saves edited target branch via mission patch", async () => {
const fetchMock = vi.fn().mockImplementation((url: string, init?: RequestInit) => {
if (url.includes("/health")) {
const missionId = extractMissionId(url) ?? "M-001";
return Promise.resolve(mockApiResponse(getMockMissionHealth(missionId)));
}
if (url === "/api/missions" || url.includes("/api/missions?")) {
return Promise.resolve(mockApiResponse(mockMissions));
}
if (url === "/api/missions/M-001" && (!init?.method || init.method === "GET")) {
return Promise.resolve(mockApiResponse(mockMissionDetail));
}
if (url === "/api/missions/M-001" && init?.method === "PATCH") {
return Promise.resolve(mockApiResponse({ ...mockMissionDetail, baseBranch: "release/2026.05" }));
}
return Promise.resolve(mockApiResponse({}));
});
globalThis.fetch = fetchMock;
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitForDetailLoaded();
fireEvent.click(screen.getAllByLabelText("Edit mission")[0]);
const targetBranchInput = await screen.findByLabelText("Mission target branch");
fireEvent.change(targetBranchInput, { target: { value: " release/2026.05 " } });
fireEvent.click(screen.getByRole("button", { name: "Update" }));
await waitFor(() => {
const patchCall = fetchMock.mock.calls.find(
(call) => call[0] === "/api/missions/M-001" && call[1]?.method === "PATCH",
);
expect(patchCall).toBeDefined();
const body = JSON.parse((patchCall![1] as RequestInit).body as string);
expect(body.baseBranch).toBe("release/2026.05");
});
});
it("shows delete confirmation with danger variant class", async () => { it("shows delete confirmation with danger variant class", async () => {
globalThis.fetch = createDetailFetchMock(); globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />); render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);