mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
Collapse extra sub-issue blocker chips
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
e670b510d5
commit
8b5cfb2613
2 files changed
+128
-28
No files matched your search
@@ -507,6 +507,96 @@ describe("IssuesList", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("collapses multiple workflow blocker chips to the first blocker and a count", async () => {
|
||||
const issueDone = createIssue({
|
||||
id: "issue-done",
|
||||
identifier: "PAP-1",
|
||||
title: "Done first",
|
||||
status: "done",
|
||||
createdAt: new Date("2026-04-01T00:00:00.000Z"),
|
||||
});
|
||||
const firstBlocker = createIssue({
|
||||
id: "issue-first-blocker",
|
||||
identifier: "PAP-2",
|
||||
title: "First blocker",
|
||||
status: "todo",
|
||||
createdAt: new Date("2026-04-02T00:00:00.000Z"),
|
||||
});
|
||||
const secondBlocker = createIssue({
|
||||
id: "issue-second-blocker",
|
||||
identifier: "PAP-3",
|
||||
title: "Second blocker",
|
||||
status: "todo",
|
||||
createdAt: new Date("2026-04-03T00:00:00.000Z"),
|
||||
});
|
||||
const thirdBlocker = createIssue({
|
||||
id: "issue-third-blocker",
|
||||
identifier: "PAP-4",
|
||||
title: "Third blocker",
|
||||
status: "todo",
|
||||
createdAt: new Date("2026-04-04T00:00:00.000Z"),
|
||||
});
|
||||
const issueBlocked = createIssue({
|
||||
id: "issue-blocked",
|
||||
identifier: "PAP-5",
|
||||
title: "Blocked issue",
|
||||
status: "blocked",
|
||||
blockedBy: [
|
||||
{
|
||||
id: "issue-first-blocker",
|
||||
identifier: "PAP-2",
|
||||
title: "First blocker",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
assigneeAgentId: null,
|
||||
assigneeUserId: null,
|
||||
},
|
||||
{
|
||||
id: "issue-second-blocker",
|
||||
identifier: "PAP-3",
|
||||
title: "Second blocker",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
assigneeAgentId: null,
|
||||
assigneeUserId: null,
|
||||
},
|
||||
{
|
||||
id: "issue-third-blocker",
|
||||
identifier: "PAP-4",
|
||||
title: "Third blocker",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
assigneeAgentId: null,
|
||||
assigneeUserId: null,
|
||||
},
|
||||
],
|
||||
createdAt: new Date("2026-04-05T00:00:00.000Z"),
|
||||
});
|
||||
|
||||
const { root } = renderWithQueryClient(
|
||||
<IssuesList
|
||||
issues={[issueBlocked, thirdBlocker, secondBlocker, firstBlocker, issueDone]}
|
||||
agents={[]}
|
||||
projects={[]}
|
||||
viewStateKey="paperclip:test-issues"
|
||||
defaultSortField="workflow"
|
||||
onUpdateIssue={() => undefined}
|
||||
/>,
|
||||
container,
|
||||
);
|
||||
|
||||
await waitForAssertion(() => {
|
||||
expect(container.textContent).toContain("blocked by PAP-2");
|
||||
expect(container.textContent).toContain("... and 2 more");
|
||||
expect(container.textContent).not.toContain("blocked by PAP-3");
|
||||
expect(container.textContent).not.toContain("blocked by PAP-4");
|
||||
});
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("uses hierarchical checklist step numbers when nested rows render inline", async () => {
|
||||
const firstRoot = createIssue({
|
||||
id: "issue-first-root",
|
||||
|
||||
@@ -1303,35 +1303,45 @@ export function IssuesList({
|
||||
const doneRowTitleClass = checklistMeta && issue.status === "done"
|
||||
? "text-muted-foreground"
|
||||
: undefined;
|
||||
const checklistDependencyChips = checklistMeta && unresolvedVisibleBlockers.length > 0 ? (
|
||||
const visibleBlockerChips = unresolvedVisibleBlockers
|
||||
.map((blockerId) => {
|
||||
const blockerIssue = issueById.get(blockerId);
|
||||
if (!blockerIssue) return null;
|
||||
const label = blockerIssue.identifier ?? blockerIssue.id.slice(0, 8);
|
||||
const blockerStep = checklistMeta?.stepNumberByIssueId.get(blockerId);
|
||||
const blockerStepSuffix = blockerStep ? ` \u00b7 step ${blockerStep}` : "";
|
||||
return { blockerId, chipLabel: `blocked by ${label}${blockerStepSuffix}` };
|
||||
})
|
||||
.filter((chip): chip is { blockerId: string; chipLabel: string } => chip !== null);
|
||||
const firstVisibleBlockerChip = visibleBlockerChips[0] ?? null;
|
||||
const additionalVisibleBlockerCount = Math.max(visibleBlockerChips.length - 1, 0);
|
||||
const checklistDependencyChips = checklistMeta && firstVisibleBlockerChip ? (
|
||||
<>
|
||||
{unresolvedVisibleBlockers.map((blockerId) => {
|
||||
const blockerIssue = issueById.get(blockerId);
|
||||
if (!blockerIssue) return null;
|
||||
const label = blockerIssue.identifier ?? blockerIssue.id.slice(0, 8);
|
||||
const blockerStep = checklistMeta.stepNumberByIssueId.get(blockerId);
|
||||
const blockerStepSuffix = blockerStep ? ` \u00b7 step ${blockerStep}` : "";
|
||||
const chipLabel = `blocked by ${label}${blockerStepSuffix}`;
|
||||
return (
|
||||
<button
|
||||
key={blockerId}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const target = document.getElementById(`issue-workflow-row-${blockerId}`);
|
||||
if (!target) return;
|
||||
target.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||
target.focus?.();
|
||||
}}
|
||||
className="inline-flex items-center rounded-full border border-amber-400/45 bg-amber-50/60 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 hover:bg-amber-100/80 dark:border-amber-300/35 dark:bg-amber-400/10 dark:text-amber-300"
|
||||
title={chipLabel}
|
||||
aria-label={chipLabel}
|
||||
>
|
||||
{chipLabel}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
key={firstVisibleBlockerChip.blockerId}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const target = document.getElementById(`issue-workflow-row-${firstVisibleBlockerChip.blockerId}`);
|
||||
if (!target) return;
|
||||
target.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||
target.focus?.();
|
||||
}}
|
||||
className="inline-flex items-center rounded-full border border-amber-400/45 bg-amber-50/60 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 hover:bg-amber-100/80 dark:border-amber-300/35 dark:bg-amber-400/10 dark:text-amber-300"
|
||||
title={firstVisibleBlockerChip.chipLabel}
|
||||
aria-label={firstVisibleBlockerChip.chipLabel}
|
||||
>
|
||||
{firstVisibleBlockerChip.chipLabel}
|
||||
</button>
|
||||
{additionalVisibleBlockerCount > 0 ? (
|
||||
<span
|
||||
className="inline-flex items-center rounded-full border border-amber-400/30 bg-amber-50/40 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:border-amber-300/25 dark:bg-amber-400/10 dark:text-amber-300"
|
||||
title={visibleBlockerChips.slice(1).map((chip) => chip.chipLabel).join(", ")}
|
||||
>
|
||||
... and {additionalVisibleBlockerCount} more
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
) : null;
|
||||
|
||||
|
||||
Reference in new issue
Block a user