fix: refine issue reference UI

This commit is contained in:
Dotta committed 2026-04-21 09:07:05 -05:00
1 parent 4d4eb80230
commit 951eba9d31
6 files changed
+145 -35

No files matched your search

+107 -4
View File
@@ -416,19 +416,122 @@ describe("IssueProperties", () => {
act(() => root.unmount());
});
it("shows referenced task ids when the issue points at other tasks", async () => {
it("shows related task references below sub-issues", async () => {
const root = renderProperties(container, {
issue: createIssue({
referencedIssueIdentifiers: ["PAP-22", "PAP-44"],
relatedWork: {
outbound: [
{
issue: {
id: "issue-22",
identifier: "PAP-22",
title: "Related task",
status: "todo",
priority: "medium",
assigneeAgentId: null,
assigneeUserId: null,
},
mentionCount: 1,
sources: [{ kind: "description", sourceRecordId: null, label: "description", matchedText: "PAP-22" }],
},
],
inbound: [],
},
}),
childIssues: [],
onUpdate: vi.fn(),
});
await flush();
expect(container.textContent).toContain("Task ids");
expect(container.textContent).not.toContain("Task ids");
expect(container.textContent).toContain("Related Tasks");
expect(container.textContent).toContain("PAP-22");
expect(container.textContent).toContain("PAP-44");
act(() => root.unmount());
});
it("hides related task references already covered by blockers, blocking, and sub-issues", async () => {
const root = renderProperties(container, {
issue: createIssue({
blockedBy: [
{
id: "issue-22",
identifier: "PAP-22",
title: "Blocker",
status: "todo",
priority: "medium",
assigneeAgentId: null,
assigneeUserId: null,
},
],
blocks: [
{
id: "issue-33",
identifier: "PAP-33",
title: "Blocked issue",
status: "todo",
priority: "medium",
assigneeAgentId: null,
assigneeUserId: null,
},
],
relatedWork: {
outbound: [
{
issue: {
id: "issue-22",
identifier: "PAP-22",
title: "Blocker",
status: "todo",
priority: "medium",
assigneeAgentId: null,
assigneeUserId: null,
},
mentionCount: 1,
sources: [{ kind: "description", sourceRecordId: null, label: "description", matchedText: "PAP-22" }],
},
{
issue: {
id: "issue-33",
identifier: "PAP-33",
title: "Blocked issue",
status: "todo",
priority: "medium",
assigneeAgentId: null,
assigneeUserId: null,
},
mentionCount: 1,
sources: [{ kind: "description", sourceRecordId: null, label: "description", matchedText: "PAP-33" }],
},
{
issue: {
id: "child-44",
identifier: "PAP-44",
title: "Child issue",
status: "todo",
priority: "medium",
assigneeAgentId: null,
assigneeUserId: null,
},
mentionCount: 1,
sources: [{ kind: "description", sourceRecordId: null, label: "description", matchedText: "PAP-44" }],
},
],
inbound: [],
},
}),
childIssues: [
createIssue({
id: "child-44",
identifier: "PAP-44",
title: "Child issue",
}),
],
onUpdate: vi.fn(),
});
await flush();
expect(container.textContent).not.toContain("Related Tasks");
act(() => root.unmount());
});
+34 -16
View File
@@ -297,6 +297,29 @@ export function IssueProperties({
return runningRuntimeServiceWithUrl(issue.currentExecutionWorkspace?.runtimeServices);
}, [issue, issueProject]);
const referencedIssueIdentifiers = issue.referencedIssueIdentifiers ?? [];
const relatedTasks = useMemo(() => {
const excluded = new Set<string>();
const addExcluded = (candidate: { id: string; identifier?: string | null }) => {
excluded.add(candidate.id);
if (candidate.identifier) excluded.add(candidate.identifier);
};
for (const blocker of issue.blockedBy ?? []) addExcluded(blocker);
for (const blocked of issue.blocks ?? []) addExcluded(blocked);
for (const child of childIssues) addExcluded(child);
const referencedIssues = issue.relatedWork?.outbound.map((item) => item.issue) ?? [];
if (referencedIssues.length > 0) {
return referencedIssues.filter((referenced) => {
const label = referenced.identifier ?? referenced.id;
return !excluded.has(referenced.id) && !excluded.has(label);
});
}
return referencedIssueIdentifiers
.filter((identifier) => !excluded.has(identifier))
.map((identifier) => ({ id: identifier, identifier, title: identifier }));
}, [childIssues, issue.blockedBy, issue.blocks, issue.relatedWork?.outbound, referencedIssueIdentifiers]);
const projectLink = (id: string | null) => {
if (!id) return null;
const project = projects?.find((p) => p.id === id) ?? null;
@@ -1095,21 +1118,6 @@ export function IssueProperties({
) : null}
</PropertyRow>
<PropertyRow label="Task ids">
{referencedIssueIdentifiers.length > 0 ? (
<div className="flex flex-wrap gap-1">
{referencedIssueIdentifiers.map((identifier) => (
<IssueReferencePill
key={identifier}
issue={{ id: identifier, identifier, title: identifier }}
/>
))}
</div>
) : (
<span className="text-sm text-muted-foreground">None</span>
)}
</PropertyRow>
<PropertyRow label="Sub-issues">
<div className="flex flex-wrap items-center gap-1.5">
{childIssues.length > 0
@@ -1130,12 +1138,22 @@ export function IssueProperties({
onClick={onAddSubIssue}
>
<Plus className="h-3 w-3" />
Add sub-issue
Add sub-issue
</button>
) : null}
</div>
</PropertyRow>
{relatedTasks.length > 0 ? (
<PropertyRow label="Related Tasks">
<div className="flex flex-wrap gap-1">
{relatedTasks.map((related) => (
<IssueReferencePill key={related.id} issue={related} />
))}
</div>
</PropertyRow>
) : null}
<PropertyPicker
inline={inline}
label="Reviewers"
+1 -5
View File
@@ -29,11 +29,7 @@ export function IssueReferencePill({
title={issue.title}
aria-label={`Issue ${issueLabel}: ${issue.title}`}
>
{issue.status ? (
<StatusIcon status={issue.status} className="h-3 w-3 shrink-0" />
) : (
<span className="h-1.5 w-1.5 rounded-[0.14rem] bg-current opacity-70" />
)}
{issue.status ? <StatusIcon status={issue.status} className="h-3 w-3 shrink-0" /> : null}
<span>{issue.identifier ?? issue.title}</span>
</Link>
);
+2
View File
@@ -156,6 +156,7 @@ describe("MarkdownBody", () => {
expect(html).toContain('href="/issues/PAP-1271"');
expect(html).toContain("text-green-600");
expect(html).toContain(">PAP-1271<");
expect(html).not.toContain("paperclip-mention-chip--issue");
});
it("rewrites full issue URLs to internal issue links", () => {
@@ -166,6 +167,7 @@ describe("MarkdownBody", () => {
expect(html).toContain('href="/issues/PAP-1179"');
expect(html).toContain("text-red-600");
expect(html).toContain(">http://localhost:3100/PAP/issues/PAP-1179<");
expect(html).not.toContain("paperclip-mention-chip--issue");
});
it("rewrites issue scheme links to internal issue links", () => {
+1 -1
View File
@@ -44,7 +44,7 @@ function MarkdownIssueLink({
return (
<Link
to={href}
className="paperclip-mention-chip paperclip-mention-chip--issue inline-flex items-center gap-1.5 align-baseline"
className="inline-flex items-center gap-1 align-baseline font-medium"
data-mention-kind="issue"
>
{data ? <StatusIcon status={data.status} className="h-3.5 w-3.5" /> : null}
-9
View File
@@ -482,15 +482,6 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
mask-size: contain;
}
.paperclip-mdxeditor-content a.paperclip-mention-chip[data-mention-kind="issue"]::before,
a.paperclip-mention-chip[data-mention-kind="issue"]::before {
width: 0.45rem;
height: 0.45rem;
border-radius: 0.14rem;
background-color: currentColor;
opacity: 0.75;
}
.paperclip-mdxeditor-content ul,
.paperclip-mdxeditor-content ol {
margin: 1.1em 0;