mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 16:27:18 +02:00
fix: refine issue reference UI
This commit is contained in:
1 parent
4d4eb80230
commit
951eba9d31
6 files changed
+145
-35
No files matched your search
@@ -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());
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user