feat(FN-4147): add mobile-friendly github tracking enable button in header

Adds a GitHub tracking toggle affordance to the task detail header with accompanying tests, documentation updates, and a changeset, plus a CSS fix for mobile header wrapping.

Fusion-Task-Id: FN-4147
This commit is contained in:
Fusion
2026-05-12 09:47:18 -07:00
committed by gsxdsm
parent 6124535759
commit c55494eda7
5 changed files with 124 additions and 4 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Add a one-click inline "Enable GitHub tracking" button to the task detail GitHub tracking header when tracking is disabled.

View File

@@ -427,7 +427,7 @@ Tracking behavior is controlled per task:
- `task.githubTracking.enabled` turns tracking on for that task. - `task.githubTracking.enabled` turns tracking on for that task.
- `task.githubTracking.repoOverride` optionally forces a specific target repo (`owner/repo`). - `task.githubTracking.repoOverride` optionally forces a specific target repo (`owner/repo`).
- In the dashboard **Task Detail** modal, eligible existing tasks (`triage`, `todo`, `in-progress`, `in-review`) always show a compact GitHub tracking summary row; linked-issue details and tracking controls are behind a disclosure arrow so tracking can still be enabled, disabled, or retargeted without reopening the task in a creation flow. - In the dashboard **Task Detail** modal, eligible existing tasks (`triage`, `todo`, `in-progress`, `in-review`) always show a compact GitHub tracking summary row. When tracking is currently disabled and editable, the header exposes a one-click **Enable GitHub tracking** button; linked-issue details and the rest of the tracking controls remain behind the disclosure arrow for disable/retarget flows.
- When a task is already tracking-enabled but still unlinked, Task Detail exposes a **Create tracking issue** action in the disclosure content (including non-editable columns like `done`) so "Issue not yet created" is not a dead-end state. - When a task is already tracking-enabled but still unlinked, Task Detail exposes a **Create tracking issue** action in the disclosure content (including non-editable columns like `done`) so "Issue not yet created" is not a dead-end state.
- Clearing the Task Detail repo override stores `null`, which reverts repo resolution to project/global defaults. - Clearing the Task Detail repo override stores `null`, which reverts repo resolution to project/global defaults.
- Explicit task-level enablement is honored even when project/global GitHub tracking defaults are unset. If `enabled: true` and the repo resolves at task scope (for example via `repoOverride`), Fusion attempts tracking-issue creation on both create-time and eligible edit-time flows. - Explicit task-level enablement is honored even when project/global GitHub tracking defaults are unset. If `enabled: true` and the repo resolves at task scope (for example via `repoOverride`), Fusion attempts tracking-issue creation on both create-time and eligible edit-time flows.

View File

@@ -436,12 +436,18 @@
font-family: var(--font-mono); font-family: var(--font-mono);
} }
.detail-github-tracking-enable {
margin-left: auto;
align-self: center;
}
.detail-source-toggle { .detail-source-toggle {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-width: 36px; flex-shrink: 0;
min-height: 36px; min-width: calc(var(--space-lg) + var(--space-xl) - var(--space-xs));
min-height: calc(var(--space-lg) + var(--space-xl) - var(--space-xs));
padding: var(--space-xs); padding: var(--space-xs);
border: 0; border: 0;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
@@ -451,6 +457,10 @@
transition: background var(--transition-fast), color var(--transition-fast); transition: background var(--transition-fast), color var(--transition-fast);
} }
.detail-source-header .detail-source-toggle {
margin-left: var(--space-sm);
}
.detail-source-toggle:hover { .detail-source-toggle:hover {
color: var(--text); color: var(--text);
background: var(--card-hover); background: var(--card-hover);
@@ -1576,12 +1586,23 @@
.detail-source-header { .detail-source-header {
align-items: flex-start; align-items: flex-start;
flex-wrap: wrap;
} }
.detail-source-summary { .detail-source-summary {
flex: 1 1 100%;
flex-wrap: wrap; flex-wrap: wrap;
} }
.detail-github-tracking-enable {
margin-left: auto;
justify-content: center;
}
.detail-source-header .detail-source-toggle {
margin-left: 0;
}
.detail-body { .detail-body {
padding: calc(var(--space-md) + var(--space-xs) / 2); padding: calc(var(--space-md) + var(--space-xs) / 2);
overflow-x: hidden; overflow-x: hidden;

View File

@@ -2391,6 +2391,17 @@ export function TaskDetailContent({
</span> </span>
)} )}
</div> </div>
{canEditGithubTracking && !githubTrackingEnabled && !githubTrackedIssue && !isSavingGithubTracking && (
<button
type="button"
className="btn btn-sm btn-primary touch-target detail-github-tracking-enable"
aria-label="Enable GitHub tracking"
onClick={() => void handleToggleGithubTracking()}
disabled={isSavingGithubTracking}
>
Enable GitHub tracking
</button>
)}
<button <button
type="button" type="button"
className="detail-source-toggle" className="detail-source-toggle"

View File

@@ -2203,7 +2203,90 @@ describe("TaskDetailModal", () => {
expect(screen.getByText("GitHub tracking")).toBeTruthy(); expect(screen.getByText("GitHub tracking")).toBeTruthy();
expect(screen.getByText("Tracking is currently disabled")).toBeTruthy(); expect(screen.getByText("Tracking is currently disabled")).toBeTruthy();
expect(screen.queryByLabelText("Enable GitHub tracking")).toBeNull(); expect(screen.getByRole("button", { name: "Enable GitHub tracking" })).toBeInTheDocument();
});
it("enables GitHub tracking via the inline header button without expanding the disclosure", async () => {
const { updateTask } = await import("../../api");
const mockUpdate = vi.mocked(updateTask);
const addToast = vi.fn();
mockUpdate.mockResolvedValueOnce({ id: "FN-001" } as Task);
render(
<TaskDetailModal
task={makeTask({
id: "FN-001",
column: "todo",
githubTracking: {
enabled: false,
},
})}
onClose={noop}
onOpenDetail={noopOpenDetail}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
addToast={addToast}
/>,
);
const expandButton = screen.getByRole("button", { name: "Expand GitHub tracking details" });
expect(expandButton).toHaveAttribute("aria-expanded", "false");
fireEvent.click(screen.getByRole("button", { name: "Enable GitHub tracking" }));
await waitFor(() => {
expect(mockUpdate).toHaveBeenCalledWith("FN-001", { githubTracking: { enabled: true } }, undefined);
});
expect(addToast).not.toHaveBeenCalledWith(expect.stringContaining("Failed to update FN-001"), "error");
expect(screen.getByRole("button", { name: "Expand GitHub tracking details" })).toHaveAttribute("aria-expanded", "false");
expect(screen.queryByRole("button", { name: "Collapse GitHub tracking details" })).toBeNull();
});
it("hides the inline enable button when tracking is already enabled", () => {
render(
<TaskDetailModal
task={makeTask({ column: "todo", githubTracking: { enabled: true } })}
onClose={noop}
onOpenDetail={noopOpenDetail}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
addToast={noop}
/>,
);
expect(screen.queryByRole("button", { name: "Enable GitHub tracking" })).toBeNull();
expect(screen.getByRole("button", { name: "Expand GitHub tracking details" })).toBeInTheDocument();
});
it("hides the inline enable button when an issue is already linked", () => {
render(
<TaskDetailModal
task={makeTask({
column: "todo",
githubTracking: {
enabled: false,
issue: {
owner: "runfusion",
repo: "fusion",
number: 456,
url: "https://github.com/runfusion/fusion/issues/456",
createdAt: "2026-01-01T00:00:00Z",
},
},
})}
onClose={noop}
onOpenDetail={noopOpenDetail}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
addToast={noop}
/>,
);
expect(screen.queryByRole("button", { name: "Enable GitHub tracking" })).toBeNull();
expect(screen.getByRole("button", { name: "Expand GitHub tracking details" })).toBeInTheDocument();
}); });
it("hides section when tracking is disabled and task is not in an eligible column", () => { it("hides section when tracking is disabled and task is not in an eligible column", () => {