mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
Show external object display key in issue properties
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
95acb60120
commit
4394fcd00d
6 files changed
+95
-20
No files matched your search
@@ -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, {
|
||||
|
||||
@@ -106,7 +106,7 @@ function displayTitleFor(identity: GitHubObjectIdentity) {
|
||||
}
|
||||
|
||||
function displayKeyFor(identity: Pick<GitHubObjectIdentity, "objectType">) {
|
||||
return identity.objectType === "pull_request" ? "GitHub PR" : "GitHub Issue";
|
||||
return identity.objectType === "pull_request" ? "Github Pull Request" : "Github Issue";
|
||||
}
|
||||
|
||||
function retryAfterSeconds(response: Response) {
|
||||
|
||||
@@ -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"');
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<div className="flex items-start gap-3 py-1.5">
|
||||
<span className="text-xs text-muted-foreground shrink-0 w-20 mt-0.5">{label}</span>
|
||||
<span className={cn("text-xs text-muted-foreground shrink-0 w-20 mt-0.5", labelClassName)}>{label}</span>
|
||||
<div className="flex items-center gap-1.5 min-w-0 flex-1 flex-wrap">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<span className="inline-flex min-w-0 items-center gap-1">
|
||||
{Icon ? <Icon aria-hidden="true" className="h-3 w-3 shrink-0" /> : null}
|
||||
<span className="truncate">{displayKey}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const ISSUE_THINKING_EFFORT_OPTIONS = {
|
||||
claude_local: [
|
||||
{ value: "", label: "Default" },
|
||||
@@ -2202,14 +2239,9 @@ export function IssueProperties({
|
||||
<span className="h-4 w-24 animate-pulse rounded bg-muted/40" />
|
||||
</PropertyRow>
|
||||
) : externalObjects && externalObjects.length > 0 ? (
|
||||
<PropertyRow label="External objects">
|
||||
<PropertyRow label={externalObjectRowLabel(externalObjects)} labelClassName="w-32">
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{[...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 }) => (
|
||||
<ExternalObjectPill
|
||||
key={group.object?.id ?? `${pill.providerKey}:${pill.objectType}:${pill.url ?? "anon"}`}
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("MarkdownBody external object decoration", () => {
|
||||
"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");
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user