feat(FN-4144): add GitHub tracking indicator to task cards

Adds a GitHub link indicator to task cards with styling and hover behavior, accompanied by tests to cover the new icon display logic.

Fusion-Task-Id: FN-4144
This commit is contained in:
Fusion
2026-05-12 12:36:33 -07:00
committed by gsxdsm
parent 2d250e6067
commit 0a03ad8a8a
4 changed files with 178 additions and 1 deletions

View File

@@ -615,6 +615,30 @@
align-items: center;
}
.card-github-tracking-link {
min-width: calc(var(--space-xl) + var(--space-md));
min-height: calc(var(--space-xl) + var(--space-md));
padding: var(--space-xs);
border-radius: var(--radius-md);
text-decoration: none;
transition:
background-color var(--transition-fast),
color var(--transition-fast),
box-shadow var(--transition-fast);
}
.card-github-tracking-link:hover {
background: var(--card-hover);
color: var(--text);
}
.card-github-tracking-link:focus-visible {
background: var(--card-hover);
color: var(--text);
box-shadow: var(--focus-ring-strong);
outline: none;
}
.card-time-indicator {
display: inline-flex;
align-items: center;
@@ -1139,6 +1163,11 @@
font-size: 0.625rem;
}
.card-github-tracking-link {
min-width: calc(var(--space-xl) + var(--space-xl));
min-height: calc(var(--space-xl) + var(--space-xl));
}
/* Card: smaller status badges for 280px width */
.card-status-badge {
font-size: 0.5625rem;

View File

@@ -736,8 +736,13 @@ function TaskCardComponent({
// Check if this card can be edited inline
const canEdit = EDITABLE_COLUMNS.has(task.column) && !isAgentActive && !isPaused && !queued && onUpdateTask;
const githubTrackedIssue = task.githubTracking?.issue;
const hasGithubTrackingLink = Boolean(githubTrackedIssue);
const hasGitHubBadge = Boolean(task.prInfo || task.issueInfo);
const isGitHubImportedTask = task.sourceType === "github_import";
const showTrackingIndicator = hasGithubTrackingLink
&& !isGitHubImportedTask
&& !(task.issueInfo && task.issueInfo.number === githubTrackedIssue?.number);
const branchMetadata = useMemo(() => getVisibleTaskCardBranches(task), [task.id, task.branch, task.baseBranch]);
const hasBranchMetadata = Boolean(branchMetadata.branch || branchMetadata.baseBranch);
const sourceIssueUrl = getIssueUrlFromMetadata(task.sourceMetadata);
@@ -1602,7 +1607,7 @@ function TaskCardComponent({
</>
);
})()}
{(filesChangedButton || timeIndicator || isGitHubImportedTask) && (
{(filesChangedButton || timeIndicator || isGitHubImportedTask || showTrackingIndicator) && (
<div className="card-footer-row">
{filesChangedButton}
{isGitHubImportedTask && (
@@ -1614,6 +1619,19 @@ function TaskCardComponent({
<ProviderIcon provider="github" size="sm" />
</span>
)}
{showTrackingIndicator && githubTrackedIssue && (
<a
className="card-source-provenance card-github-tracking-link"
href={githubTrackedIssue.url}
target="_blank"
rel="noopener noreferrer"
title={`Linked GitHub issue: ${githubTrackedIssue.owner}/${githubTrackedIssue.repo}#${githubTrackedIssue.number}`}
aria-label={`Linked GitHub issue #${githubTrackedIssue.number}`}
onClick={(e) => e.stopPropagation()}
>
<ProviderIcon provider="github" size="sm" />
</a>
)}
{timeIndicator && (
<span
className="card-time-indicator"

View File

@@ -1019,6 +1019,131 @@ describe("TaskCard", () => {
expect(screen.queryByTestId("provider-icon-github")).toBeNull();
});
it("renders a GitHub tracking link for tracked issues on non-imported tasks", () => {
render(
<TaskCard
task={makeTask({
column: "todo",
sourceType: "dashboard_ui",
githubTracking: {
issue: {
owner: "owner",
repo: "repo",
number: 42,
url: "https://github.com/owner/repo/issues/42",
createdAt: "2026-05-12T00:00:00.000Z",
},
},
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
const link = screen.getByRole("link", { name: "Linked GitHub issue #42" });
expect(link.getAttribute("href")).toBe("https://github.com/owner/repo/issues/42");
expect(link.getAttribute("title")).toBe("Linked GitHub issue: owner/repo#42");
expect(link).toHaveClass("card-github-tracking-link");
expect(screen.getByTestId("provider-icon-github")).toBeDefined();
});
it("does not render a GitHub tracking link when githubTracking is absent", () => {
render(
<TaskCard
task={makeTask({
column: "todo",
sourceType: "dashboard_ui",
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
expect(screen.queryByRole("link", { name: /Linked GitHub issue/i })).toBeNull();
});
it("does not render a GitHub tracking link for github_import tasks", () => {
render(
<TaskCard
task={makeTask({
column: "todo",
sourceType: "github_import",
sourceMetadata: { issueUrl: "https://github.com/owner/repo/issues/42" },
githubTracking: {
issue: {
owner: "owner",
repo: "repo",
number: 42,
url: "https://github.com/owner/repo/issues/42",
createdAt: "2026-05-12T00:00:00.000Z",
},
},
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
expect(screen.queryByRole("link", { name: /Linked GitHub issue/i })).toBeNull();
expect(screen.getByLabelText("Imported from GitHub")).toBeDefined();
});
it("does not render a GitHub tracking link when a matching issue badge is already shown", () => {
render(
<TaskCard
task={makeTask({
column: "todo",
sourceType: "dashboard_ui",
githubTracking: {
issue: {
owner: "owner",
repo: "repo",
number: 42,
url: "https://github.com/owner/repo/issues/42",
createdAt: "2026-05-12T00:00:00.000Z",
},
},
issueInfo: {
url: "https://github.com/owner/repo/issues/42",
number: 42,
state: "open",
title: "Issue",
},
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
expect(screen.queryByRole("link", { name: /Linked GitHub issue/i })).toBeNull();
});
it("clicking the GitHub tracking link does not open the task detail modal", () => {
const onOpenDetail = vi.fn();
render(
<TaskCard
task={makeTask({
column: "todo",
sourceType: "dashboard_ui",
githubTracking: {
issue: {
owner: "owner",
repo: "repo",
number: 42,
url: "https://github.com/owner/repo/issues/42",
createdAt: "2026-05-12T00:00:00.000Z",
},
},
})}
onOpenDetail={onOpenDetail}
addToast={noop}
/>,
);
fireEvent.click(screen.getByRole("link", { name: "Linked GitHub issue #42" }));
expect(onOpenDetail).not.toHaveBeenCalled();
});
it("renders agent-created provenance badge for automation tasks and prefers sourceMetadata.agentName", () => {
const { container } = render(
<TaskCard