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