FN-7183: link PR numbers to GitHub
Make task-detail PR numbers open their GitHub pull request when a URL is present. - Render the PR number as a secure external link when prInfo.url is available. - Keep the existing plain text PR number fallback when no URL is present. - Add focus/hover styling, dashboard documentation, regression tests, and a patch changeset. Files changed: .changeset/fn-7183-pr-number-link.md | 7 +++++++ docs/dashboard-guide.md | 1 + packages/dashboard/app/components/PrPanel.css | 13 ++++++++++++ packages/dashboard/app/components/PrPanel.tsx | 12 ++++++++++- .../app/components/__tests__/PrPanel.test.tsx | 24 ++++++++++++++++++++++ 5 files changed, 56 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-7183 Fusion-Task-Lineage: 0a349723-9dda-4784-b747-b140b85a3013 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7183-pr-number-link.md
Normal file
7
.changeset/fn-7183-pr-number-link.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: The PR number in a task's Pull Request tab now links to the pull request on GitHub.
|
||||||
|
category: feature
|
||||||
|
dev: PrCard (PrPanel.tsx) wraps the pr-number in an anchor to prInfo.url (new tab, rel=noopener); plain-span fallback when no URL.
|
||||||
@@ -997,6 +997,7 @@ Inspect task definition, logs, review feedback, comments, artifacts, workflow ou
|
|||||||
- In shared task edit/create forms, GitHub Tracking appears at the bottom of **More options**, after **Workflow Steps**.
|
- In shared task edit/create forms, GitHub Tracking appears at the bottom of **More options**, after **Workflow Steps**.
|
||||||
- From this section you can explicitly enable/disable tracking and manage a per-task repo override (`owner/repo`). Clearing the override saves `null` and falls back to project/global defaults.
|
- From this section you can explicitly enable/disable tracking and manage a per-task repo override (`owner/repo`). Clearing the override saves `null` and falls back to project/global defaults.
|
||||||
- In `in-review`, pull-request controls/status (including stall badges) are in a dedicated **Pull Request** tab instead of the Definition tab.
|
- In `in-review`, pull-request controls/status (including stall badges) are in a dedicated **Pull Request** tab instead of the Definition tab.
|
||||||
|
- In the task detail **Pull Request** tab, PR numbers open the linked pull request on GitHub when a PR URL is available.
|
||||||
- Task Detail and list split-pane PR affordances follow the live project auto-merge setting: when auto-merge is off, manual **Create PR** / merge actions are shown; when it is on, the tab shows the automatic auto-merge hint unless a per-task override changes the effective behavior.
|
- Task Detail and list split-pane PR affordances follow the live project auto-merge setting: when auto-merge is off, manual **Create PR** / merge actions are shown; when it is on, the tab shows the automatic auto-merge hint unless a per-task override changes the effective behavior.
|
||||||
- The **Workflow** tab resolves the effective workflow for both explicitly selected and default-inherited tasks. Its overview, expandable graph preview, configured step details, and live step results refresh when switching tasks or projects without showing stale rows from the previous task.
|
- The **Workflow** tab resolves the effective workflow for both explicitly selected and default-inherited tasks. Its overview, expandable graph preview, configured step details, and live step results refresh when switching tasks or projects without showing stale rows from the previous task.
|
||||||
- The **Create Pull Request** modal now offers in-app remediation for every blocking preflight check. If `branchOnRemote` is false, use **Push branch to remote** and Fusion will publish `fusion/<task-id-lower>` to `origin` and refresh preflight. If `conflictsWithBase` is true, use **Resolve conflicts with AI** and Fusion will use an AI coding agent to resolve merge markers on the task branch, commit and push real merge changes, or report success without an empty commit when the selected base is already merged; preflight then refreshes so normal PR creation can continue once all checks pass.
|
- The **Create Pull Request** modal now offers in-app remediation for every blocking preflight check. If `branchOnRemote` is false, use **Push branch to remote** and Fusion will publish `fusion/<task-id-lower>` to `origin` and refresh preflight. If `conflictsWithBase` is true, use **Resolve conflicts with AI** and Fusion will use an AI coding agent to resolve merge markers on the task branch, commit and push real merge changes, or report success without an empty commit when the selected base is already merged; preflight then refreshes so normal PR creation can continue once all checks pass.
|
||||||
|
|||||||
@@ -143,6 +143,19 @@
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pr-card a.pr-number {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-card a.pr-number:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-card a.pr-number:focus-visible {
|
||||||
|
box-shadow: var(--focus-ring-strong);
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.pr-panel-refresh-icon--muted {
|
.pr-panel-refresh-icon--muted {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,6 +215,8 @@ function PrCard({
|
|||||||
const hasConflictBlockingReason = blockingReasons.some((reason) => reason.toLowerCase().includes("conflict"));
|
const hasConflictBlockingReason = blockingReasons.some((reason) => reason.toLowerCase().includes("conflict"));
|
||||||
const showConflictHint = prInfo.mergeable === "conflicting" || hasConflictBlockingReason;
|
const showConflictHint = prInfo.mergeable === "conflicting" || hasConflictBlockingReason;
|
||||||
const conflictDiagnostics = refreshState?.conflictDiagnostics ?? prInfo.conflictDiagnostics;
|
const conflictDiagnostics = refreshState?.conflictDiagnostics ?? prInfo.conflictDiagnostics;
|
||||||
|
const prUrl = prInfo.url?.trim();
|
||||||
|
const prNumberLinkLabel = t("git.viewPrNumberOnGithub", "View PR #{{number}} on GitHub", { number: prInfo.number });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setConflictsExpanded((conflictDiagnostics?.conflictingFiles.length ?? 0) > 0);
|
setConflictsExpanded((conflictDiagnostics?.conflictingFiles.length ?? 0) > 0);
|
||||||
@@ -225,7 +227,15 @@ function PrCard({
|
|||||||
<div className="pr-header">
|
<div className="pr-header">
|
||||||
<span className="pr-status-icon">{statusIcon}</span>
|
<span className="pr-status-icon">{statusIcon}</span>
|
||||||
<span className={`pr-status-badge pr-status-badge--${prInfo.status}`}>{prInfo.status}</span>
|
<span className={`pr-status-badge pr-status-badge--${prInfo.status}`}>{prInfo.status}</span>
|
||||||
<span className="pr-number">#{prInfo.number}</span>
|
{/*
|
||||||
|
FNXC:PullRequests 2026-06-27-23:21:
|
||||||
|
The task-detail Pull Request tab should let operators open GitHub directly from the PR number while preserving a plain-span fallback when no URL exists.
|
||||||
|
*/}
|
||||||
|
{prUrl ? (
|
||||||
|
<a className="pr-number" href={prUrl} target="_blank" rel="noopener noreferrer" aria-label={prNumberLinkLabel} title={prNumberLinkLabel}>#{prInfo.number}</a>
|
||||||
|
) : (
|
||||||
|
<span className="pr-number">#{prInfo.number}</span>
|
||||||
|
)}
|
||||||
<div className="pr-spacer" />
|
<div className="pr-spacer" />
|
||||||
<button className="btn btn-sm pr-refresh-btn" onClick={handleRefresh} disabled={isRefreshing} title={t("git.refreshPrStatus", "Refresh PR status")}>
|
<button className="btn btn-sm pr-refresh-btn" onClick={handleRefresh} disabled={isRefreshing} title={t("git.refreshPrStatus", "Refresh PR status")}>
|
||||||
<RefreshCw size={14} className={isRefreshing ? "spin pr-panel-refresh-icon--muted" : undefined} />
|
<RefreshCw size={14} className={isRefreshing ? "spin pr-panel-refresh-icon--muted" : undefined} />
|
||||||
|
|||||||
@@ -141,6 +141,30 @@ describe("PrPanel", () => {
|
|||||||
expect(screen.getByRole("link", { name: /View on GitHub/i })).toBeInTheDocument();
|
expect(screen.getByRole("link", { name: /View on GitHub/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("links the PR number to GitHub when a PR URL is available", () => {
|
||||||
|
render(<PrPanel taskId="FN-001" prInfo={mockPrInfo} prAuthAvailable={true} onPrUpdated={mockOnPrUpdated} addToast={mockAddToast} />);
|
||||||
|
|
||||||
|
const numberLink = screen.getByRole("link", { name: "View PR #42 on GitHub" });
|
||||||
|
expect(numberLink).toHaveTextContent("#42");
|
||||||
|
expect(numberLink).toHaveAttribute("href", mockPrInfo.url);
|
||||||
|
expect(numberLink).toHaveAttribute("target", "_blank");
|
||||||
|
expect(numberLink.getAttribute("rel")).toContain("noopener");
|
||||||
|
expect(numberLink.getAttribute("rel")).toContain("noreferrer");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the PR number as a plain span when the PR URL is empty", () => {
|
||||||
|
render(<PrPanel taskId="FN-001" prInfo={{ ...mockPrInfo, url: "" }} prAuthAvailable={true} onPrUpdated={mockOnPrUpdated} addToast={mockAddToast} />);
|
||||||
|
|
||||||
|
expect(screen.queryByRole("link", { name: "View PR #42 on GitHub" })).toBeNull();
|
||||||
|
expect(screen.getByText("#42").tagName).toBe("SPAN");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["open", "merged", "closed"] as const)("links the PR number for status %s", (status) => {
|
||||||
|
render(<PrPanel taskId="FN-001" prInfo={{ ...mockPrInfo, status }} prAuthAvailable={true} onPrUpdated={mockOnPrUpdated} addToast={mockAddToast} />);
|
||||||
|
|
||||||
|
expect(screen.getByRole("link", { name: "View PR #42 on GitHub" })).toHaveAttribute("href", mockPrInfo.url);
|
||||||
|
});
|
||||||
|
|
||||||
it("refreshes PR status and updates toast/callback", async () => {
|
it("refreshes PR status and updates toast/callback", async () => {
|
||||||
(refreshPrStatus as ReturnType<typeof vi.fn>).mockResolvedValue({
|
(refreshPrStatus as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
prInfo: { ...mockPrInfo, status: "merged" },
|
prInfo: { ...mockPrInfo, status: "merged" },
|
||||||
|
|||||||
Reference in New Issue
Block a user