From 4394fcd00db3eef8d75fbe5cf3740dc059757ea2 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 22 Jun 2026 22:36:34 +0000 Subject: [PATCH] Show external object display key in issue properties Co-Authored-By: Paperclip --- .../external-objects-service.test.ts | 10 ++-- .../github-external-object-provider.ts | 2 +- ui/src/components/ExternalObjectPill.test.tsx | 4 +- ui/src/components/IssueProperties.test.tsx | 43 +++++++++++++++ ui/src/components/IssueProperties.tsx | 52 +++++++++++++++---- .../components/MarkdownExternalLink.test.tsx | 4 +- 6 files changed, 95 insertions(+), 20 deletions(-) diff --git a/server/src/__tests__/external-objects-service.test.ts b/server/src/__tests__/external-objects-service.test.ts index 0b2eaee0969..03a81f09ab6 100644 --- a/server/src/__tests__/external-objects-service.test.ts +++ b/server/src/__tests__/external-objects-service.test.ts @@ -182,7 +182,7 @@ describe("GitHub external object provider", () => { providerKey: "github", objectType: "pull_request", externalId: "acme/app#pull/42", - displayKey: "GitHub PR", + displayKey: "Github Pull Request", iconKey: "github", displayTitle: "Acme/App#42", }), @@ -190,7 +190,7 @@ describe("GitHub external object provider", () => { providerKey: "github", objectType: "issue", externalId: "acme/app#issues/7", - displayKey: "GitHub Issue", + displayKey: "Github Issue", iconKey: "github", displayTitle: "Acme/App#7", }), @@ -239,7 +239,7 @@ describe("GitHub external object provider", () => { ok: true, snapshot: expect.objectContaining({ ...expected, - displayKey: "GitHub PR", + displayKey: "Github Pull Request", iconKey: "github", displayTitle: expect.stringContaining(String(body.title)), remoteVersion: "2026-04-24T01:02:03Z", @@ -281,7 +281,7 @@ describe("GitHub external object provider", () => { ok: true, snapshot: expect.objectContaining({ ...expected, - displayKey: "GitHub Issue", + displayKey: "Github Issue", iconKey: "github", data: expect.objectContaining({ provider: "github", @@ -330,7 +330,7 @@ describe("GitHub external object provider", () => { [ "not-found", new Response("", { status: 404, headers: { etag: '"missing"' } }), - { ok: true, snapshot: expect.objectContaining({ displayKey: "GitHub PR", iconKey: "github", statusKey: "not_found", statusIconKey: "archive", statusCategory: "archived", statusTone: "muted" }) }, + { ok: true, snapshot: expect.objectContaining({ displayKey: "Github Pull Request", iconKey: "github", statusKey: "not_found", statusIconKey: "archive", statusCategory: "archived", statusTone: "muted" }) }, ], ])("maps %s responses to provider-safe results", async (_name, githubResponse, expected) => { const provider = createGitHubExternalObjectProvider({} as any, { diff --git a/server/src/services/github-external-object-provider.ts b/server/src/services/github-external-object-provider.ts index 6d8a7c91714..9cfa4d6ddeb 100644 --- a/server/src/services/github-external-object-provider.ts +++ b/server/src/services/github-external-object-provider.ts @@ -106,7 +106,7 @@ function displayTitleFor(identity: GitHubObjectIdentity) { } function displayKeyFor(identity: Pick) { - return identity.objectType === "pull_request" ? "GitHub PR" : "GitHub Issue"; + return identity.objectType === "pull_request" ? "Github Pull Request" : "Github Issue"; } function retryAfterSeconds(response: Response) { diff --git a/ui/src/components/ExternalObjectPill.test.tsx b/ui/src/components/ExternalObjectPill.test.tsx index b1adcb7fea1..be9325903ca 100644 --- a/ui/src/components/ExternalObjectPill.test.tsx +++ b/ui/src/components/ExternalObjectPill.test.tsx @@ -97,7 +97,7 @@ describe("ExternalObjectPill", () => { object={{ providerKey: "github", objectType: "pull_request", - displayKey: "GitHub PR", + displayKey: "Github Pull Request", iconKey: "github", statusCategory: "succeeded", statusIconKey: "git-merge", @@ -108,7 +108,7 @@ describe("ExternalObjectPill", () => { }} />, ); - expect(html).toContain("GitHub PR"); + expect(html).toContain("Github Pull Request"); expect(html).toContain("Merged"); expect(html).toContain("acme/web#241: Add rich object presentation metadata"); expect(html).toContain('aria-label="GitHub pull request — Merged: acme/web#241: Add rich object presentation metadata"'); diff --git a/ui/src/components/IssueProperties.test.tsx b/ui/src/components/IssueProperties.test.tsx index 4545c41a51a..c92e5785a33 100644 --- a/ui/src/components/IssueProperties.test.tsx +++ b/ui/src/components/IssueProperties.test.tsx @@ -1797,4 +1797,47 @@ describe("IssueProperties", () => { act(() => root.unmount()); }); + + it("uses external object display metadata for the properties row label", async () => { + const root = renderProperties(container, { + issue: createIssue(), + childIssues: [], + onUpdate: vi.fn(), + inline: true, + externalObjects: [ + { + mentionCount: 1, + sourceLabels: ["Description"], + pill: { + providerKey: "github", + objectType: "pull_request", + displayKey: "Github Pull Request", + iconKey: "github", + statusCategory: "succeeded", + statusIconKey: "git-merge", + statusLabel: "Merged", + liveness: "fresh", + displayTitle: "acme/web#241: Add rich object presentation metadata", + url: "https://github.com/acme/web/pull/241", + }, + group: { + object: null, + mentions: [], + mentionCount: 1, + sourceLabels: ["Description"], + }, + }, + ], + }); + await flush(); + + expect(container.textContent).toContain("Github Pull Request"); + expect(container.textContent).toContain("Merged"); + expect(container.textContent).not.toContain("External objects"); + const label = Array.from(container.querySelectorAll("span")) + .find((span) => span.textContent === "Github Pull Request"); + expect(label?.querySelector("svg")).toBeTruthy(); + + act(() => root.unmount()); + }); }); diff --git a/ui/src/components/IssueProperties.tsx b/ui/src/components/IssueProperties.tsx index c4de23bf8a4..4843c38778c 100644 --- a/ui/src/components/IssueProperties.tsx +++ b/ui/src/components/IssueProperties.tsx @@ -37,7 +37,7 @@ import { IssueReferencePill } from "./IssueReferencePill"; import { formatDate, formatDateTime, cn, projectUrl } from "../lib/utils"; import { ExternalObjectPill } from "./ExternalObjectPill"; import type { IssueExternalObjectGroup } from "../hooks/useIssueExternalObjects"; -import { externalObjectToneSeverity } from "../lib/external-objects"; +import { externalObjectIconForKey, externalObjectToneSeverity } from "../lib/external-objects"; import { timeAgo } from "../lib/timeAgo"; import { Button } from "@/components/ui/button"; import { ToggleSwitch } from "@/components/ui/toggle-switch"; @@ -165,15 +165,52 @@ interface IssuePropertiesProps { const ISSUE_BLOCKER_SEARCH_LIMIT = 50; const ISSUE_PROPERTY_RELATION_PREVIEW_COUNT = 5; -function PropertyRow({ label, children }: { label: string; children: React.ReactNode }) { +function PropertyRow({ + label, + children, + labelClassName, +}: { + label: React.ReactNode; + children: React.ReactNode; + labelClassName?: string; +}) { return (
- {label} + {label}
{children}
); } +function sortExternalObjectGroups(groups: IssueExternalObjectGroup[]) { + return [...groups].sort((a, b) => { + const aTone = externalObjectToneSeverity(a.group.object?.statusTone); + const bTone = externalObjectToneSeverity(b.group.object?.statusTone); + return bTone - aTone; + }); +} + +function externalObjectRowLabel(groups: IssueExternalObjectGroup[] | undefined): React.ReactNode { + if (!groups || groups.length === 0) return "External objects"; + const sorted = sortExternalObjectGroups(groups); + const primary = sorted[0]?.pill; + const displayKey = primary?.displayKey?.trim(); + if (!primary || !displayKey) return "External objects"; + const iconKey = primary.iconKey ?? null; + const allSameDisplay = sorted.every(({ pill }) => + (pill.displayKey?.trim() ?? "") === displayKey && (pill.iconKey ?? null) === iconKey, + ); + if (!allSameDisplay) return "External objects"; + + const Icon = externalObjectIconForKey(iconKey); + return ( + + {Icon ? + ); +} + const ISSUE_THINKING_EFFORT_OPTIONS = { claude_local: [ { value: "", label: "Default" }, @@ -2202,14 +2239,9 @@ export function IssueProperties({ ) : externalObjects && externalObjects.length > 0 ? ( - +
- {[...externalObjects] - .sort((a, b) => { - const aTone = externalObjectToneSeverity(a.group.object?.statusTone); - const bTone = externalObjectToneSeverity(b.group.object?.statusTone); - return bTone - aTone; - }) + {sortExternalObjectGroups(externalObjects) .map(({ pill, mentionCount, sourceLabels, group }) => ( { "https://github.com/acme/web/pull/241": { providerKey: "github", objectType: "pull_request", - displayKey: "GitHub PR", + displayKey: "Github Pull Request", iconKey: "github", statusCategory: "succeeded", statusIconKey: "git-merge", @@ -53,7 +53,7 @@ describe("MarkdownBody external object decoration", () => { expect(html).toContain('data-external-link="resolved"'); expect(html).toContain('data-external-status="succeeded"'); expect(html).toContain('data-external-liveness="fresh"'); - expect(html).toContain('aria-label="GitHub PR Merged: Add external refs"'); + expect(html).toContain('aria-label="Github Pull Request Merged: Add external refs"'); expect(html).toContain("github.com/acme/web/pull/241"); });