Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 05b4aa0

Browse files
authored
fix(loops): group loops by ownership rather than visibility (#3846)
1 parent 42bd848 commit 05b4aa0

4 files changed

Lines changed: 62 additions & 9 deletions

File tree

packages/ui/src/features/loops/components/LoopsListView.test.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ vi.mock("./LoopRow", () => ({
1818
function loop(
1919
id: string,
2020
visibility: LoopSchemas.LoopVisibilityEnum,
21+
createdById = 1,
2122
): LoopSchemas.Loop {
2223
return {
2324
id,
2425
name: `${visibility} loop`,
2526
visibility,
27+
created_by_id: createdById,
2628
} as LoopSchemas.Loop;
2729
}
2830

@@ -34,22 +36,45 @@ function controlledPanel(tab: HTMLElement): HTMLElement {
3436
}
3537

3638
describe("LoopsListViewPresentation", () => {
37-
it("shows only the selected ownership tab", async () => {
39+
it("does not render ownership groups while identity is loading", () => {
3840
render(
3941
<Theme>
4042
<LoopsListViewPresentation
41-
loops={[loop("personal", "personal"), loop("team", "team")]}
43+
loops={[loop("mine-team", "team")]}
44+
currentUserId={null}
45+
isLoading
4246
onStartBlank={vi.fn()}
4347
onStartFromTemplate={vi.fn()}
4448
/>
4549
</Theme>,
4650
);
4751

48-
const personalTab = screen.getByRole("tab", { name: "My loops (1)" });
52+
expect(screen.queryByRole("tab")).not.toBeInTheDocument();
53+
});
54+
55+
it("groups loops by ownership rather than visibility", async () => {
56+
render(
57+
<Theme>
58+
<LoopsListViewPresentation
59+
loops={[
60+
loop("personal", "personal"),
61+
loop("mine-team", "team"),
62+
loop("teammate-team", "team", 2),
63+
]}
64+
currentUserId={1}
65+
onStartBlank={vi.fn()}
66+
onStartFromTemplate={vi.fn()}
67+
/>
68+
</Theme>,
69+
);
70+
71+
const personalTab = screen.getByRole("tab", { name: "My loops (2)" });
4972
expect(
5073
within(controlledPanel(personalTab)).getByText("personal loop"),
5174
).toBeVisible();
52-
expect(screen.queryByText("team loop")).not.toBeInTheDocument();
75+
expect(
76+
within(controlledPanel(personalTab)).getByText("team loop"),
77+
).toBeVisible();
5378

5479
const teamTab = screen.getByRole("tab", { name: "Team loops (1)" });
5580
await userEvent.click(teamTab);

packages/ui/src/features/loops/components/LoopsListView.tsx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import type { LoopSchemas } from "@posthog/api-client/loops";
88
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@posthog/quill";
99
import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events";
1010
import type { UserBasic } from "@posthog/shared/domain-types";
11+
import { useOptionalAuthenticatedClient } from "@posthog/ui/features/auth/authClient";
12+
import { useCurrentUser } from "@posthog/ui/features/auth/useCurrentUser";
1113
import { useOrgMembers } from "@posthog/ui/features/canvas/hooks/useOrgMembers";
1214
import { StopCloudRunDialog } from "@posthog/ui/features/sessions/components/StopCloudRunDialog";
1315
import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent";
@@ -67,9 +69,22 @@ function startLoopFromTemplate(template: LoopTemplate): void {
6769

6870
export function LoopsListView() {
6971
const { data: loops, isLoading, isError, error } = useLoops();
72+
const authenticatedClient = useOptionalAuthenticatedClient();
73+
const {
74+
data: currentUser,
75+
isLoading: currentUserLoading,
76+
isError: currentUserError,
77+
error: currentUserQueryError,
78+
} = useCurrentUser({ client: authenticatedClient });
7079
const limits = useLoopLimits();
7180
const limitReason =
7281
limits?.atLimit === true ? loopLimitReason(limits.max) : null;
82+
let listError: unknown = null;
83+
if (isError) {
84+
listError = error;
85+
} else if (currentUserError) {
86+
listError = currentUserQueryError;
87+
}
7388

7489
const headerContent = useMemo(
7590
() => (
@@ -134,8 +149,9 @@ export function LoopsListView() {
134149
return (
135150
<LoopsListViewPresentation
136151
loops={allLoops}
137-
isLoading={isLoading}
138-
error={isError ? error : null}
152+
currentUserId={currentUser?.id ?? null}
153+
isLoading={isLoading || currentUserLoading}
154+
error={listError}
139155
limitReason={limitReason}
140156
members={members}
141157
membersLoading={membersLoading}
@@ -152,6 +168,7 @@ export function LoopsListView() {
152168

153169
interface LoopsListViewPresentationProps {
154170
loops: LoopSchemas.Loop[];
171+
currentUserId?: number | null;
155172
isLoading?: boolean;
156173
error?: unknown;
157174
limitReason?: string | null;
@@ -168,6 +185,7 @@ interface LoopsListViewPresentationProps {
168185

169186
export function LoopsListViewPresentation({
170187
loops,
188+
currentUserId = null,
171189
isLoading = false,
172190
error = null,
173191
limitReason = null,
@@ -181,8 +199,16 @@ export function LoopsListViewPresentation({
181199
onResumeBuilderSession,
182200
onBuilderSessionStopped,
183201
}: LoopsListViewPresentationProps) {
184-
const personalLoops = loops.filter((loop) => loop.visibility === "personal");
185-
const teamLoops = loops.filter((loop) => loop.visibility === "team");
202+
const personalLoops = loops.filter(
203+
(loop) =>
204+
loop.visibility === "personal" ||
205+
(currentUserId !== null && loop.created_by_id === currentUserId),
206+
);
207+
const teamLoops = loops.filter(
208+
(loop) =>
209+
loop.visibility === "team" &&
210+
(currentUserId === null || loop.created_by_id !== currentUserId),
211+
);
186212

187213
return (
188214
<Flex direction="column" className="h-full min-h-0">

packages/ui/src/shell/HedgehogMode.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ describe("HedgehogMode", () => {
100100
expect(mocks.mount).toHaveBeenCalledTimes(1);
101101
expect(overlay.querySelector("canvas")).not.toBeNull();
102102
expect(overlay.style.visibility).toBe("visible");
103+
expect(overlay).toHaveClass("absolute");
104+
expect(overlay).not.toHaveClass("fixed");
103105
});
104106

105107
it("destroys the game and reports when the context loss callback fires", async () => {

packages/ui/src/shell/HedgehogMode.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export function HedgehogMode() {
134134
zIndex: 999998,
135135
visibility: hedgehogMode && !gameDead ? "visible" : "hidden",
136136
}}
137-
className="pointer-events-none fixed inset-0"
137+
className="pointer-events-none absolute inset-0"
138138
/>
139139
);
140140
}

0 commit comments

Comments
 (0)