feat(FN-4176): add general GitHub tracking settings to settings modal

Adds general GitHub tracking controls to the Settings modal, moving those settings out of the merge-specific section into a broader general settings area, with corresponding test coverage and documentation updates.

Fusion-Task-Id: FN-4176
This commit is contained in:
Fusion
2026-05-12 17:42:56 -07:00
committed by gsxdsm
parent fcf1212419
commit 12a08cbd29
4 changed files with 104 additions and 41 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Surface GitHub tracking project options (default on/off for new tasks, default repo) in the project General settings section.

View File

@@ -263,7 +263,7 @@ Override precedence for direct merges is:
| `githubCommentOnDone` | `boolean` | `false` | When enabled, tasks imported from GitHub issues post a completion comment to the source issue when the task moves to `done`. |
| `githubCommentTemplate` | `string` | `undefined` | Optional issue comment template used by `githubCommentOnDone`. Supports `{taskId}` and `{taskTitle}` placeholders. If unset, Fusion uses a default completion message. |
| `githubTrackingEnabledByDefault` | `boolean` | `false` | Project-level default for enabling issue tracking on new tasks. Even when this is false, issue creation can still occur per task if tracking is explicitly enabled. |
| `githubTrackingDefaultRepo` | `string` | `undefined` | Project default issue-tracking repo (`owner/repo`) used before global fallback for tracked task creation (precedence: task override → project default → global default). This key is dual-scope: project saves go through `PUT /api/settings` (Settings → Merge). |
| `githubTrackingDefaultRepo` | `string` | `undefined` | Project default issue-tracking repo (`owner/repo`) used before global fallback for tracked task creation (precedence: task override → project default → global default). This key is dual-scope: project saves go through `PUT /api/settings` (Settings → General → GitHub Tracking) while global saves go through `PUT /api/settings/global` (Settings → Global General). |
| `githubAuthMode` | `"gh-cli" \| "token"` | `"gh-cli"` | Project GitHub auth strategy used by tracking lifecycle integration. `"gh-cli"` requires an installed/authenticated `gh` CLI. `"token"` requires a non-empty `githubAuthToken` (or `GITHUB_TOKEN` env fallback). Tracking lifecycle auth is strict per selected mode (no cross-fallback). |
| `githubAuthToken` | `string` | `undefined` | Optional project PAT used when `githubAuthMode` is `"token"` (takes precedence over server startup token for tracking flows). |
| `autoCreatePr` | `boolean` | `false` | Auto-create PRs for completed tasks. |

View File

@@ -2006,6 +2006,41 @@ export function SettingsModal({
</label>
<small>Show the floating chat button in the dashboard. Chat is still accessible from the Chat tab in the mobile navigation.</small>
</div>
<h4 className="settings-section-heading settings-section-heading--spaced">GitHub Tracking</h4>
<div className="form-group">
<label htmlFor="githubTrackingMode">Default tracking mode for new tasks</label>
<select
id="githubTrackingMode"
className="select"
value={form.githubTrackingEnabledByDefault ? "new-tasks" : "off"}
onChange={(e) =>
setForm((f) => ({
...f,
githubTrackingEnabledByDefault: e.target.value === "new-tasks",
}))
}
>
<option value="off">Off (default)</option>
<option value="new-tasks">On for new tasks</option>
</select>
<small>
Controls whether newly created tasks have GitHub issue tracking enabled by default. Individual tasks can still override this from the task detail modal.
</small>
</div>
<div className="form-group">
<label htmlFor="projectGithubTrackingDefaultRepoGeneral">Project default tracking repo</label>
<input
id="projectGithubTrackingDefaultRepoGeneral"
type="text"
className="input"
placeholder="owner/repo"
value={form.githubTrackingDefaultRepo ?? ""}
onChange={(e) =>
setForm((f) => ({ ...f, githubTrackingDefaultRepo: e.target.value || undefined }))
}
/>
<small>Default repo used when creating GitHub issues for tracked tasks. Falls back to the global default if blank.</small>
</div>
</>
);
case "global-general":
@@ -3750,33 +3785,7 @@ export function SettingsModal({
</details>
</div>
)}
<h4 className="settings-section-heading settings-section-heading--spaced">GitHub Issue Tracking</h4>
<div className="form-group">
<label htmlFor="githubTrackingEnabledByDefault" className="checkbox-label">
<input
id="githubTrackingEnabledByDefault"
type="checkbox"
checked={form.githubTrackingEnabledByDefault ?? false}
onChange={(e) =>
setForm((f) => ({ ...f, githubTrackingEnabledByDefault: e.target.checked }))
}
/>
Default GitHub tracking ON for new tasks
</label>
</div>
<div className="form-group">
<label htmlFor="projectGithubTrackingDefaultRepo">Project default tracking repo</label>
<input
id="projectGithubTrackingDefaultRepo"
type="text"
className="input"
placeholder="owner/repo"
value={form.githubTrackingDefaultRepo ?? ""}
onChange={(e) =>
setForm((f) => ({ ...f, githubTrackingDefaultRepo: e.target.value || undefined }))
}
/>
</div>
<h4 className="settings-section-heading settings-section-heading--spaced">GitHub Authentication</h4>
<div className="form-group">
<label htmlFor="githubAuthMode">GitHub auth mode</label>
<select

View File

@@ -680,6 +680,64 @@ describe("SettingsModal", () => {
expect(globalPayload.completionDocumentationMode).toBeUndefined();
}
});
it("renders and saves GitHub tracking controls in the General section", async () => {
renderModal({ initialSection: "general" });
await waitForSettingsModalReady();
expect(screen.getByRole("heading", { name: "GitHub Tracking" })).toBeInTheDocument();
const modeSelect = screen.getByLabelText("Default tracking mode for new tasks") as HTMLSelectElement;
const repoInput = screen.getByLabelText("Project default tracking repo") as HTMLInputElement;
expect(modeSelect.value).toBe("off");
expect(repoInput.value).toBe("");
await userEvent.selectOptions(modeSelect, "new-tasks");
await userEvent.type(repoInput, "octo/repo");
await userEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(mockUpdateSettings).toHaveBeenCalled();
});
const payload = mockUpdateSettings.mock.calls[0][0] as Record<string, unknown>;
expect(payload.githubTrackingEnabledByDefault).toBe(true);
expect(payload.githubTrackingDefaultRepo).toBe("octo/repo");
if (mockUpdateGlobalSettings.mock.calls.length > 0) {
const globalPayload = mockUpdateGlobalSettings.mock.calls[0]?.[0] as Record<string, unknown>;
expect(globalPayload.githubTrackingDefaultRepo).toBeUndefined();
}
});
it("saves GitHub tracking defaults as disabled and clears the repo when emptied", async () => {
mockFetchSettings.mockResolvedValueOnce({
...defaultSettings,
githubTrackingEnabledByDefault: true,
githubTrackingDefaultRepo: "octo/existing",
});
renderModal({ initialSection: "general" });
await waitForSettingsModalReady();
const modeSelect = screen.getByLabelText("Default tracking mode for new tasks") as HTMLSelectElement;
const repoInput = screen.getByLabelText("Project default tracking repo") as HTMLInputElement;
expect(modeSelect.value).toBe("new-tasks");
expect(repoInput.value).toBe("octo/existing");
await userEvent.selectOptions(modeSelect, "off");
await userEvent.clear(repoInput);
await userEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(mockUpdateSettings).toHaveBeenCalled();
});
const payload = mockUpdateSettings.mock.calls[0][0] as Record<string, unknown>;
expect(payload.githubTrackingEnabledByDefault).toBe(false);
expect(payload.githubTrackingDefaultRepo).toBeUndefined();
});
});
describe("Appearance", () => {
@@ -2206,20 +2264,18 @@ describe("SettingsModal", () => {
});
});
it("renders and saves github issue tracking controls", async () => {
it("keeps GitHub tracking controls out of Merge and preserves GitHub authentication controls", async () => {
renderModal({ initialSection: "merge" });
await waitForSettingsModalReady();
expect(screen.getByRole("heading", { name: "GitHub Issue Tracking" })).toBeInTheDocument();
expect(screen.getByRole("checkbox", { name: "Default GitHub tracking ON for new tasks" })).not.toBeChecked();
expect(screen.getByLabelText("Project default tracking repo")).toBeInTheDocument();
expect(screen.queryByLabelText("Default tracking mode for new tasks")).not.toBeInTheDocument();
expect(screen.queryByLabelText("Project default tracking repo")).not.toBeInTheDocument();
expect(screen.getByRole("heading", { name: "GitHub Authentication" })).toBeInTheDocument();
const authModeSelect = screen.getByLabelText("GitHub auth mode") as HTMLSelectElement;
expect(authModeSelect.value).toBe("gh-cli");
expect(screen.queryByLabelText("GitHub personal access token")).not.toBeInTheDocument();
await userEvent.click(screen.getByRole("checkbox", { name: "Default GitHub tracking ON for new tasks" }));
await userEvent.type(screen.getByLabelText("Project default tracking repo"), "octo/repo");
await userEvent.selectOptions(authModeSelect, "token");
await userEvent.type(screen.getByLabelText("GitHub personal access token"), "ghp_test_token");
await userEvent.click(screen.getByRole("button", { name: "Save" }));
@@ -2229,15 +2285,8 @@ describe("SettingsModal", () => {
});
const payload = mockUpdateSettings.mock.calls[0][0] as Record<string, unknown>;
expect(payload.githubTrackingEnabledByDefault).toBe(true);
expect(payload.githubTrackingDefaultRepo).toBe("octo/repo");
expect(payload.githubAuthMode).toBe("token");
expect(payload.githubAuthToken).toBe("ghp_test_token");
if (mockUpdateGlobalSettings.mock.calls.length > 0) {
const globalPayload = mockUpdateGlobalSettings.mock.calls[0]?.[0] as Record<string, unknown>;
expect(globalPayload.githubTrackingDefaultRepo).toBeUndefined();
}
});
});