diff --git a/ui/src/components/IssueProperties.test.tsx b/ui/src/components/IssueProperties.test.tsx index c6006169f60..d11059a7a23 100644 --- a/ui/src/components/IssueProperties.test.tsx +++ b/ui/src/components/IssueProperties.test.tsx @@ -1814,7 +1814,7 @@ describe("IssueProperties", () => { displayKey: null, iconKey: "github", statusCategory: "succeeded", - statusIconKey: "git-merge", + statusIconKey: null, statusLabel: "Merged", liveness: "fresh", displayTitle: "acme/web#241: Add rich object presentation metadata", @@ -1868,6 +1868,9 @@ describe("IssueProperties", () => { expect(pullRequestLink?.textContent).not.toContain("acme/web#241"); expect(pullRequestLink?.textContent).not.toContain("Github Pull Request"); expect(pullRequestLink?.querySelectorAll("svg")).toHaveLength(1); + expect(pullRequestLink?.className).not.toContain("paperclip-mention-chip"); + expect(pullRequestLink?.className).not.toContain("rounded-full"); + expect(pullRequestLink?.className).not.toContain("border"); act(() => root.unmount()); }); diff --git a/ui/src/components/IssueProperties.tsx b/ui/src/components/IssueProperties.tsx index 9ee63246a43..7bd5d8ca831 100644 --- a/ui/src/components/IssueProperties.tsx +++ b/ui/src/components/IssueProperties.tsx @@ -35,14 +35,19 @@ import { PriorityIcon } from "./PriorityIcon"; import { Identity } from "./Identity"; import { IssueReferencePill } from "./IssueReferencePill"; import { formatDate, formatDateTime, cn, projectUrl } from "../lib/utils"; -import { ExternalObjectPill } from "./ExternalObjectPill"; +import { ExternalObjectStatusIcon } from "./ExternalObjectStatusIcon"; import type { IssueExternalObjectGroup } from "../hooks/useIssueExternalObjects"; import { + externalObjectCategoryLabel, externalObjectIconForKey, externalObjectProviderLabel, externalObjectToneSeverity, externalObjectTypeLabel, } from "../lib/external-objects"; +import { + externalObjectStatusIcon, + externalObjectStatusIconDefault, +} from "../lib/status-colors"; import { timeAgo } from "../lib/timeAgo"; import { Button } from "@/components/ui/button"; import { ToggleSwitch } from "@/components/ui/toggle-switch"; @@ -218,6 +223,116 @@ function externalObjectRowLabel(group: IssueExternalObjectGroup): React.ReactNod ); } +function githubObjectPropertyValue(url: string | null | undefined): string | null { + if (!url) return null; + try { + const parsed = new URL(url); + if (parsed.hostname !== "github.com") return null; + const [, owner, repo, kind, number] = parsed.pathname.split("/"); + if (!owner || !repo || !number) return null; + if (kind === "pull") return `PR ${number}`; + if (kind === "issues") return `Issue ${number}`; + return null; + } catch { + return null; + } +} + +function externalObjectPropertyStatusLabel(group: IssueExternalObjectGroup): string { + return group.pill.statusLabel ?? externalObjectCategoryLabel(group.pill.statusCategory); +} + +function externalObjectPropertyValue(group: IssueExternalObjectGroup): string { + const { pill } = group; + const statusLabel = externalObjectPropertyStatusLabel(group); + const githubLabel = pill.providerKey === "github" ? githubObjectPropertyValue(pill.url) : null; + const base = githubLabel ?? pill.displayTitle?.trim() ?? externalObjectRowDisplayKey(group); + return statusLabel ? `${base} - ${statusLabel}` : base; +} + +function isMergedExternalObject(group: IssueExternalObjectGroup): boolean { + const statusLabel = externalObjectPropertyStatusLabel(group); + return group.pill.statusIconKey === "git-merge" || statusLabel.toLowerCase() === "merged"; +} + +function externalObjectPropertyTone(group: IssueExternalObjectGroup): string { + if (isMergedExternalObject(group)) { + return "text-violet-600 dark:text-violet-400"; + } + const tone = externalObjectStatusIcon[group.pill.statusCategory] ?? externalObjectStatusIconDefault; + return tone.split(" ").filter((c) => c.startsWith("text-")).join(" "); +} + +function externalObjectPropertyStatusIconKey(group: IssueExternalObjectGroup): string | null | undefined { + if (isMergedExternalObject(group)) return group.pill.statusIconKey ?? "git-merge"; + return group.pill.statusIconKey; +} + +function externalObjectPropertyTitle(group: IssueExternalObjectGroup): string { + const { pill, sourceLabels } = group; + const base = pill.displayTitle ?? externalObjectPropertyValue(group); + return sourceLabels.length > 0 ? `${base} - ${sourceLabels.join(", ")}` : base; +} + +function ExternalObjectPropertyValue({ group }: { group: IssueExternalObjectGroup }) { + const { pill, mentionCount } = group; + const statusLabel = externalObjectPropertyStatusLabel(group); + const providerLabel = externalObjectProviderLabel(pill.providerKey); + const typeLabel = externalObjectTypeLabel(pill.objectType); + const value = externalObjectPropertyValue(group); + const content = ( + <> + + {value} + {mentionCount > 1 ? ( + x{mentionCount} + ) : null} + + ); + const className = cn( + "inline-flex min-w-0 max-w-full items-center gap-1 text-xs font-medium no-underline", + externalObjectPropertyTone(group), + pill.url ? "hover:underline focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring" : "", + ); + + if (pill.url) { + return ( + + {content} + + ); + } + + return ( + + {content} + + ); +} + const ISSUE_THINKING_EFFORT_OPTIONS = { claude_local: [ { value: "", label: "Default" }, @@ -2249,19 +2364,14 @@ export function IssueProperties({ <> {sortExternalObjectGroups(externalObjects) .map((externalObject) => { - const { pill, mentionCount, sourceLabels, group } = externalObject; + const { pill, group } = externalObject; return ( - + ); })}