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:
5
.changeset/fn-4144-github-link-card-icon.md
Normal file
5
.changeset/fn-4144-github-link-card-icon.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Board task cards now show a GitHub icon when linked to a tracked GitHub issue.
|
||||||
@@ -615,6 +615,30 @@
|
|||||||
align-items: center;
|
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 {
|
.card-time-indicator {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1139,6 +1163,11 @@
|
|||||||
font-size: 0.625rem;
|
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: smaller status badges for 280px width */
|
||||||
.card-status-badge {
|
.card-status-badge {
|
||||||
font-size: 0.5625rem;
|
font-size: 0.5625rem;
|
||||||
|
|||||||
@@ -736,8 +736,13 @@ function TaskCardComponent({
|
|||||||
|
|
||||||
// Check if this card can be edited inline
|
// Check if this card can be edited inline
|
||||||
const canEdit = EDITABLE_COLUMNS.has(task.column) && !isAgentActive && !isPaused && !queued && onUpdateTask;
|
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 hasGitHubBadge = Boolean(task.prInfo || task.issueInfo);
|
||||||
const isGitHubImportedTask = task.sourceType === "github_import";
|
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 branchMetadata = useMemo(() => getVisibleTaskCardBranches(task), [task.id, task.branch, task.baseBranch]);
|
||||||
const hasBranchMetadata = Boolean(branchMetadata.branch || branchMetadata.baseBranch);
|
const hasBranchMetadata = Boolean(branchMetadata.branch || branchMetadata.baseBranch);
|
||||||
const sourceIssueUrl = getIssueUrlFromMetadata(task.sourceMetadata);
|
const sourceIssueUrl = getIssueUrlFromMetadata(task.sourceMetadata);
|
||||||
@@ -1602,7 +1607,7 @@ function TaskCardComponent({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
{(filesChangedButton || timeIndicator || isGitHubImportedTask) && (
|
{(filesChangedButton || timeIndicator || isGitHubImportedTask || showTrackingIndicator) && (
|
||||||
<div className="card-footer-row">
|
<div className="card-footer-row">
|
||||||
{filesChangedButton}
|
{filesChangedButton}
|
||||||
{isGitHubImportedTask && (
|
{isGitHubImportedTask && (
|
||||||
@@ -1614,6 +1619,19 @@ function TaskCardComponent({
|
|||||||
<ProviderIcon provider="github" size="sm" />
|
<ProviderIcon provider="github" size="sm" />
|
||||||
</span>
|
</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 && (
|
{timeIndicator && (
|
||||||
<span
|
<span
|
||||||
className="card-time-indicator"
|
className="card-time-indicator"
|
||||||
|
|||||||
@@ -1019,6 +1019,131 @@ describe("TaskCard", () => {
|
|||||||
expect(screen.queryByTestId("provider-icon-github")).toBeNull();
|
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", () => {
|
it("renders agent-created provenance badge for automation tasks and prefers sourceMetadata.agentName", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TaskCard
|
<TaskCard
|
||||||
|
|||||||
Reference in New Issue
Block a user