Limit issue property relation previews

This commit is contained in:
Dotta committed 2026-06-22 15:37:00 +00:00
1 parent 35bcaca643
commit 63c0ca8df3
2 files changed
+110 -3

No files matched your search

@@ -722,6 +722,70 @@ describe("IssueProperties", () => {
act(() => root.unmount());
});
it("collapses long blocked-by and sub-task lists until the more button is clicked", async () => {
const blockedBy = Array.from({ length: 7 }, (_, index) => ({
id: `blocker-${index + 1}`,
identifier: `BLOCK-${index + 1}`,
title: `Blocker ${index + 1}`,
status: "todo",
priority: "medium",
assigneeAgentId: null,
assigneeUserId: null,
})) as NonNullable<Issue["blockedBy"]>;
const childIssues = Array.from({ length: 7 }, (_, index) => createIssue({
id: `child-${index + 1}`,
identifier: `SUB-${index + 1}`,
title: `Sub-task ${index + 1}`,
}));
const root = renderProperties(container, {
issue: createIssue({ blockedBy }),
childIssues,
onUpdate: vi.fn(),
inline: true,
});
await flush();
expect(container.textContent).toContain("BLOCK-5");
expect(container.textContent).not.toContain("BLOCK-6");
expect(container.textContent).toContain("SUB-5");
expect(container.textContent).not.toContain("SUB-6");
expect(
Array.from(container.querySelectorAll("button")).filter((button) =>
button.textContent?.trim() === "and 2 more...",
),
).toHaveLength(2);
const expandBlockedBy = Array.from(container.querySelectorAll("button")).find((button) =>
button.textContent?.trim() === "and 2 more...",
);
expect(expandBlockedBy).not.toBeUndefined();
await act(async () => {
expandBlockedBy!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.textContent).toContain("BLOCK-6");
expect(container.textContent).toContain("BLOCK-7");
expect(container.textContent).not.toContain("SUB-6");
const expandSubTasks = Array.from(container.querySelectorAll("button")).find((button) =>
button.textContent?.trim() === "and 2 more...",
);
expect(expandSubTasks).not.toBeUndefined();
await act(async () => {
expandSubTasks!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.textContent).toContain("SUB-6");
expect(container.textContent).toContain("SUB-7");
expect(
Array.from(container.querySelectorAll("button")).filter((button) =>
button.textContent?.trim() === "and 2 more...",
),
).toHaveLength(0);
act(() => root.unmount());
});
it("shows a green service link above the workspace row for a live non-main workspace", async () => {
mockProjectsApi.list.mockResolvedValue([createProject()]);
const serviceUrl = "http://127.0.0.1:62475";
+46 -3
View File
@@ -154,6 +154,7 @@ interface IssuePropertiesProps {
}
const ISSUE_BLOCKER_SEARCH_LIMIT = 50;
const ISSUE_PROPERTY_RELATION_PREVIEW_COUNT = 5;
function PropertyRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
@@ -321,6 +322,25 @@ function RemovableIssueReferencePill({
);
}
function ExpandRelationListButton({
hiddenCount,
onClick,
}: {
hiddenCount: number;
onClick: () => void;
}) {
if (hiddenCount <= 0) return null;
return (
<button
type="button"
className="inline-flex items-center gap-1 rounded-full border border-border px-2 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
onClick={onClick}
>
and {hiddenCount} more...
</button>
);
}
/** Renders a Popover on desktop, or an inline collapsible section on mobile (inline mode). */
function PropertyPicker({
inline,
@@ -408,6 +428,8 @@ export function IssueProperties({
const [projectSearch, setProjectSearch] = useState("");
const [blockedByOpen, setBlockedByOpen] = useState(false);
const [blockedBySearch, setBlockedBySearch] = useState("");
const [blockedByExpanded, setBlockedByExpanded] = useState(false);
const [subTasksExpanded, setSubTasksExpanded] = useState(false);
const [parentOpen, setParentOpen] = useState(false);
const [parentSearch, setParentSearch] = useState("");
const [reviewersOpen, setReviewersOpen] = useState(false);
@@ -1673,6 +1695,15 @@ export function IssueProperties({
);
const blockedByIds = issue.blockedBy?.map((relation) => relation.id) ?? [];
const blockedByRelations = issue.blockedBy ?? [];
const visibleBlockedByRelations = blockedByExpanded
? blockedByRelations
: blockedByRelations.slice(0, ISSUE_PROPERTY_RELATION_PREVIEW_COUNT);
const hiddenBlockedByCount = blockedByRelations.length - visibleBlockedByRelations.length;
const visibleChildIssues = subTasksExpanded
? childIssues
: childIssues.slice(0, ISSUE_PROPERTY_RELATION_PREVIEW_COUNT);
const hiddenChildIssueCount = childIssues.length - visibleChildIssues.length;
const descendantIssueIds = useMemo(() => {
if (!allIssues?.length) return new Set<string>();
const childrenByParentId = new Map<string, string[]>();
@@ -1985,9 +2016,13 @@ export function IssueProperties({
{inline ? (
<div>
<PropertyRow label="Blocked by">
{(issue.blockedBy ?? []).map((relation) => (
{visibleBlockedByRelations.map((relation) => (
<RemovableIssueReferencePill key={relation.id} issue={relation} onRemove={removeBlockedBy} />
))}
<ExpandRelationListButton
hiddenCount={hiddenBlockedByCount}
onClick={() => setBlockedByExpanded(true)}
/>
{renderAddBlockedByButton(() => setBlockedByOpen((open) => !open))}
</PropertyRow>
{blockedByOpen && (
@@ -1998,9 +2033,13 @@ export function IssueProperties({
</div>
) : (
<PropertyRow label="Blocked by">
{(issue.blockedBy ?? []).map((relation) => (
{visibleBlockedByRelations.map((relation) => (
<RemovableIssueReferencePill key={relation.id} issue={relation} onRemove={removeBlockedBy} />
))}
<ExpandRelationListButton
hiddenCount={hiddenBlockedByCount}
onClick={() => setBlockedByExpanded(true)}
/>
<Popover
open={blockedByOpen}
onOpenChange={(open) => {
@@ -2031,10 +2070,14 @@ export function IssueProperties({
<PropertyRow label="Sub-tasks">
<div className="flex flex-wrap items-center gap-1.5">
{childIssues.length > 0
? childIssues.map((child) => (
? visibleChildIssues.map((child) => (
<IssueReferencePill key={child.id} issue={child} />
))
: null}
<ExpandRelationListButton
hiddenCount={hiddenChildIssueCount}
onClick={() => setSubTasksExpanded(true)}
/>
{onAddSubIssue ? (
<button
type="button"