Render external object properties as plain links

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-23 00:35:05 +00:00
1 parent ba59f64dea
commit 89f7feb741
2 files changed
+122 -9

No files matched your search

+4 -1
View File
@@ -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());
});
+118 -8
View File
@@ -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 = (
<>
<ExternalObjectStatusIcon
category={pill.statusCategory}
liveness={pill.liveness}
statusIconKey={externalObjectPropertyStatusIconKey(group)}
sizeClassName="h-3 w-3"
label={`${providerLabel}: ${statusLabel}`}
/>
<span className="min-w-0 truncate">{value}</span>
{mentionCount > 1 ? (
<span className="tabular-nums text-[10px] font-medium opacity-80">x{mentionCount}</span>
) : 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 (
<a
href={pill.url}
target="_blank"
rel="noopener noreferrer"
data-mention-kind="external-object"
data-external-status={pill.statusCategory}
data-external-liveness={pill.liveness}
className={className}
title={externalObjectPropertyTitle(group)}
aria-label={`${providerLabel} ${typeLabel} - ${statusLabel}: ${pill.displayTitle ?? value}`}
>
{content}
</a>
);
}
return (
<span
data-mention-kind="external-object"
data-external-status={pill.statusCategory}
data-external-liveness={pill.liveness}
className={className}
title={externalObjectPropertyTitle(group)}
aria-label={`${providerLabel} ${typeLabel} - ${statusLabel}: ${pill.displayTitle ?? value}`}
>
{content}
</span>
);
}
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 (
<PropertyRow
key={group.object?.id ?? `${pill.providerKey}:${pill.objectType}:${pill.url ?? "anon"}`}
label={externalObjectRowLabel(externalObject)}
labelClassName="w-24 max-w-24 whitespace-normal leading-tight"
>
<ExternalObjectPill
object={pill}
sourceCount={mentionCount}
sourceSummary={sourceLabels.join(", ")}
showProviderIcon={false}
/>
<ExternalObjectPropertyValue group={externalObject} />
</PropertyRow>
);
})}