mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
Limit issue property relation previews
This commit is contained in:
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";
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user