Collapse extra sub-issue blocker chips

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-04-27 19:45:07 -05:00
1 parent e670b510d5
commit 8b5cfb2613
2 files changed
+128 -28

No files matched your search

+90
View File
@@ -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",
+38 -28
View File
@@ -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;