From 862da074e1c8ce0b70837c465bf6c2decfdc0ce2 Mon Sep 17 00:00:00 2001 From: Matt Pua Date: Fri, 24 Jul 2026 09:24:07 -0400 Subject: [PATCH 1/4] feat: improve loops list organization Generated-By: PostHog Code Task-Id: 9cac87f7-3d2e-4240-8e3f-722952dd6987 --- .../canvas/components/WebsiteChannelLoops.tsx | 20 +++- .../src/features/loops/components/LoopRow.tsx | 17 ++- .../loops/components/LoopTemplatesSection.tsx | 36 +++--- .../loops/components/LoopTriggerEditor.tsx | 4 +- .../loops/components/LoopsEmptyState.test.tsx | 37 ++++++ .../loops/components/LoopsEmptyState.tsx | 51 +++++--- .../loops/components/LoopsListView.test.tsx | 63 ++++++++++ .../loops/components/LoopsListView.tsx | 109 +++++++++++++----- 8 files changed, 263 insertions(+), 74 deletions(-) create mode 100644 packages/ui/src/features/loops/components/LoopsEmptyState.test.tsx create mode 100644 packages/ui/src/features/loops/components/LoopsListView.test.tsx diff --git a/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx b/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx index 1fc20914fb..1fab5d69d1 100644 --- a/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx +++ b/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx @@ -81,11 +81,15 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) { - - - +
+ + Automate #{contextName} @@ -119,7 +123,7 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) { Create manually - +
{isLoading ? ( @@ -140,7 +144,11 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) {
) : ( - + )}
diff --git a/packages/ui/src/features/loops/components/LoopRow.tsx b/packages/ui/src/features/loops/components/LoopRow.tsx index cc3ec524b9..2b53b2f4a2 100644 --- a/packages/ui/src/features/loops/components/LoopRow.tsx +++ b/packages/ui/src/features/loops/components/LoopRow.tsx @@ -1,5 +1,6 @@ import { CaretRightIcon, RepeatIcon } from "@phosphor-icons/react"; import type { LoopSchemas } from "@posthog/api-client/loops"; +import { formatRelativeTimeLong } from "@posthog/shared"; import type { UserBasic } from "@posthog/shared/domain-types"; import { userDisplayName } from "@posthog/ui/features/canvas/utils/userDisplay"; import { Badge } from "@posthog/ui/primitives/Badge"; @@ -71,11 +72,19 @@ export function LoopRow({ {description} ) : null} - {metadata.length > 0 ? ( - - {metadata.join(" · ")} + + {metadata.length > 0 ? ( + + {metadata.join(" · ")} + + ) : null} + {metadata.length > 0 ? ( + · + ) : null} + + Created {formatRelativeTimeLong(loop.created_at)} - ) : null} +
diff --git a/packages/ui/src/features/loops/components/LoopTemplatesSection.tsx b/packages/ui/src/features/loops/components/LoopTemplatesSection.tsx index 5da7af0104..52d94b89a4 100644 --- a/packages/ui/src/features/loops/components/LoopTemplatesSection.tsx +++ b/packages/ui/src/features/loops/components/LoopTemplatesSection.tsx @@ -28,8 +28,8 @@ export function LoopTemplatesSection({ useState("engineering"); return ( - - +
+
Start from a template @@ -49,19 +49,20 @@ export function LoopTemplatesSection({ ))} - -
+
+
{LOOP_TEMPLATES.filter( (template) => template.category === templateCategory, ).map((template) => ( - onSelect(template)} - /> +
+ onSelect(template)} + /> +
))}
- +
); } @@ -80,7 +81,7 @@ function TemplateCard({
- + + + +
-
+ ); } diff --git a/packages/ui/src/features/loops/components/LoopsListView.test.tsx b/packages/ui/src/features/loops/components/LoopsListView.test.tsx new file mode 100644 index 0000000000..cdcba77f7c --- /dev/null +++ b/packages/ui/src/features/loops/components/LoopsListView.test.tsx @@ -0,0 +1,63 @@ +import type { LoopSchemas } from "@posthog/api-client/loops"; +import { Theme } from "@radix-ui/themes"; +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; +import { LoopsListViewPresentation } from "./LoopsListView"; + +vi.mock("./LoopBuilderComposer", () => ({ + LoopBuilderComposer: () => null, +})); +vi.mock("./LoopTemplatesSection", () => ({ + LoopTemplatesSection: () => null, +})); +vi.mock("./LoopRow", () => ({ + LoopRow: ({ loop }: { loop: LoopSchemas.Loop }) =>
{loop.name}
, +})); + +function loop( + id: string, + visibility: LoopSchemas.LoopVisibilityEnum, +): LoopSchemas.Loop { + return { + id, + name: `${visibility} loop`, + visibility, + } as LoopSchemas.Loop; +} + +function controlledPanel(tab: HTMLElement): HTMLElement { + const panelId = tab.getAttribute("aria-controls"); + const panel = document.getElementById(panelId ?? ""); + if (!panel) throw new Error("Tab does not control a panel"); + return panel; +} + +describe("LoopsListViewPresentation", () => { + it("shows only the selected ownership tab", async () => { + render( + + + , + ); + + const personalTab = screen.getByRole("tab", { name: "My loops (1)" }); + expect( + within(controlledPanel(personalTab)).getByText("personal loop"), + ).toBeVisible(); + expect(screen.queryByText("team loop")).not.toBeInTheDocument(); + + const teamTab = screen.getByRole("tab", { name: "Team loops (1)" }); + await userEvent.click(teamTab); + + expect(teamTab).toHaveAttribute("aria-selected", "true"); + expect( + within(controlledPanel(teamTab)).getByText("team loop"), + ).toBeVisible(); + expect(controlledPanel(personalTab)).toHaveAttribute("inert"); + }); +}); diff --git a/packages/ui/src/features/loops/components/LoopsListView.tsx b/packages/ui/src/features/loops/components/LoopsListView.tsx index 44ec788d21..2c0ed9f602 100644 --- a/packages/ui/src/features/loops/components/LoopsListView.tsx +++ b/packages/ui/src/features/loops/components/LoopsListView.tsx @@ -6,6 +6,7 @@ import { } from "@phosphor-icons/react"; import type { LoopSchemas } from "@posthog/api-client/loops"; import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@posthog/quill"; import type { UserBasic } from "@posthog/shared/domain-types"; import { useOrgMembers } from "@posthog/ui/features/canvas/hooks/useOrgMembers"; import { StopCloudRunDialog } from "@posthog/ui/features/sessions/components/StopCloudRunDialog"; @@ -189,11 +190,11 @@ export function LoopsListViewPresentation({ - +
- + Loops - + Runs entirely in the cloud @@ -226,7 +227,7 @@ export function LoopsListViewPresentation({ Create manually - +
{isLoading ? ( @@ -240,23 +241,21 @@ export function LoopsListViewPresentation({ } /> ) : loops.length > 0 ? ( - - {personalLoops.length > 0 ? ( - - ) : null} - {teamLoops.length > 0 ? ( - - ) : null} - + ) : ( - + )} @@ -284,6 +283,69 @@ export function LoopsListViewPresentation({ ); } +function LoopListTabs({ + personalLoops, + teamLoops, + members, + membersLoading, + membersError, + membersComplete, + onCreate, + disabledReason, +}: { + personalLoops: LoopSchemas.Loop[]; + teamLoops: LoopSchemas.Loop[]; + members: UserBasic[]; + membersLoading: boolean; + membersError: boolean; + membersComplete: boolean; + onCreate: () => void; + disabledReason: string | null; +}) { + return ( + + + + + My loops ({personalLoops.length}) + + + + + Team loops ({teamLoops.length}) + + + + + {personalLoops.length > 0 ? ( + + ) : ( + + )} + + + {teamLoops.length > 0 ? ( + + ) : ( + + )} + + + ); +} + function BuilderSessionRow({ session, onResume, @@ -343,14 +405,12 @@ function BuilderSessionRow({ } function LoopListSection({ - title, loops, members = EMPTY_MEMBERS, membersLoading = false, membersError = false, membersComplete = true, }: { - title: string; loops: LoopSchemas.Loop[]; members?: UserBasic[]; membersLoading?: boolean; @@ -362,9 +422,6 @@ function LoopListSection({ return ( - - {title} - {visibleLoops.map((loop) => ( Date: Fri, 24 Jul 2026 09:24:08 -0400 Subject: [PATCH 2/4] fix: restore stacked loops empty state Generated-By: PostHog Code Task-Id: 9cac87f7-3d2e-4240-8e3f-722952dd6987 --- .../loops/components/LoopsEmptyState.tsx | 107 ++++++++++-------- 1 file changed, 57 insertions(+), 50 deletions(-) diff --git a/packages/ui/src/features/loops/components/LoopsEmptyState.tsx b/packages/ui/src/features/loops/components/LoopsEmptyState.tsx index fe8c50d617..3258f4562c 100644 --- a/packages/ui/src/features/loops/components/LoopsEmptyState.tsx +++ b/packages/ui/src/features/loops/components/LoopsEmptyState.tsx @@ -25,58 +25,65 @@ export function LoopsEmptyState({ disabledReason?: string | null; }) { return ( -
- - - - - - - {contextName - ? `Create a loop for #${contextName}` - : "Create your first loop"} - - - Set it up once and it keeps running on its own, even with your - laptop closed. - +
+
+ + -
- {GETTING_STARTED_STEPS.map((step, index) => ( -
- - {index + 1} - - {step} -
- ))} -
- - - + + + + {contextName + ? `Create a loop for #${contextName}` + : "Create your first loop"} + + + Set it up once and it keeps running on its own, even with your + laptop closed. + + +
+ {GETTING_STARTED_STEPS.map((step, index) => ( +
+ + {index + 1} + + {step} +
+ ))} +
+ + + +
-
+
); } From 2dd57f54b7e00e9d75a9fb3fd4a8eccf4711c522 Mon Sep 17 00:00:00 2001 From: Matt Pua Date: Fri, 24 Jul 2026 09:24:10 -0400 Subject: [PATCH 3/4] chore(visual): update storybook baselines 6 updated Run: 9bc8cfca-b5f8-4ad4-ad02-2eca6e724a46 Co-authored-by: MattPua <3376526+MattPua@users.noreply.github.com> --- apps/code/snapshots.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/code/snapshots.yml b/apps/code/snapshots.yml index 5d93040e53..e2fc980da7 100644 --- a/apps/code/snapshots.yml +++ b/apps/code/snapshots.yml @@ -561,17 +561,17 @@ snapshots: git-dialogs--sync-success--light: hash: v1.k4693efd2.8d79d77c9338aeaa73734836f0e17fde987f6d3239914f4013889d3110e5088d.Rq-w2o1uhes_a5hUWqMXeiDkfnoPCZPt932LkEQXCco loops-loopslistview--comprehensive--dark: - hash: v1.k4693efd2.3c2a6b87092674a3d395a6f5d36889ea231696d788b159f357be0926f3804c8a.Ihzlq7XIy9rRoA7DTbpLePlBhjP34o8ppgP__NXx-Yw + hash: v1.k4693efd2.b9223e477701b5ef4587310a1de71500dd5214222531648ef76d66766b049a9f.2lckIm16WmmwZv1xVZnUL42InIHDzemQwU3EJzzw9_U loops-loopslistview--comprehensive--light: - hash: v1.k4693efd2.42c2994def56651de08f5be20e98dbe67bfbdb03e7edd8bdf008401468c1653a.EnH7NwNkvclS7g5Htf7OCZuYEm7fI5W6hEr4DmwZC8I + hash: v1.k4693efd2.1f172c831aefdfd4b3bae97af84f80aae3c09c4919f6b1b68315240cc482af50.hnrwbv8il_Ay6DcYxmQXNtxKU3rqOIWAkjnGB8n4AT4 loops-loopslistview--long-mixed-list--dark: - hash: v1.k4693efd2.384486583f8d9b66a4bf1d9cc45036a07310cb9e1943ea5b52ff466f55a03d7b.F_sXrJ13XWHzAF8yhoKun_k4BY8Jcb3EDtT0L1I6nKw + hash: v1.k4693efd2.654250bc74fff46c496747eb13031e0e7a7ef264be5bb7be41451d82a497a515.vvrxXbbABkVY3hcHS3oThQ339JFgwIzB8E4n1usVWf0 loops-loopslistview--long-mixed-list--light: - hash: v1.k4693efd2.d80a0c55c4490276df53f781ac88ed9d9c6ec282bc14ddb80140b3ddcc44d32f.L6hw9ZixUiyTFKOybYzmjfUSIOayvmhueMTcmYt-IWo + hash: v1.k4693efd2.a8e53087efdaf58e351809775aaf410f53a781a97e10e973ee53fd0db2f94e73.7GcprWAYAgATLtNckNBd-44EyctqKNcb4FXiHA08QJs loops-loopslistview--with-builder-sessions--dark: - hash: v1.k4693efd2.bc59812bf49ff161cf20e75dcf043367ae3d2817aa2c2cf91c91d9771c6b62f8.yOBz-llmwDVay7GIHlMwlMT3EuKjkoW0Vwmwvi4noCM + hash: v1.k4693efd2.84cfc28bedc22a6728ba4ecf69274ab03537faeed5333775991716bf0b5f439e.fHsD0XKPnud-oDM4PLJ7edCbMnVcW6uVMdMHBYp3K08 loops-loopslistview--with-builder-sessions--light: - hash: v1.k4693efd2.fb97e3fe3205ddf71d98c4e35446c73e394aaa9761d3bb35b146ddafc438df4d.UUEWc0G12NQibZTyskIHGS62mB36gKJ594GrJp1E9II + hash: v1.k4693efd2.fb6c9b85bd660a6be4645cf7ba8fe3ab08a169e3823e5354325b7d7ecbe13aba.4FbKrdxiUN1PZOGwFa_kl7Rpz3Z8DnTVNJvUM_3FIp0 scouts-scoutsfleetlist--filtered-to-you--dark: hash: v1.k4693efd2.a3af6e76ef36598eaa347bedc4430878ed62754660166398e0576a9978cd084b.x3Ky-O6HOw0srWdOmnnKJwFNpmGmQVWip0t7CwVaRXY scouts-scoutsfleetlist--filtered-to-you--light: From 3c694643afb7e62e7fddd41fe0351c0b44551311 Mon Sep 17 00:00:00 2001 From: Matt Pua Date: Fri, 24 Jul 2026 10:26:24 -0400 Subject: [PATCH 4/4] fix: sort loops list imports Generated-By: PostHog Code Task-Id: 726a62ad-ada6-4718-a5a8-8b7a5c466172 --- packages/ui/src/features/loops/components/LoopsListView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/features/loops/components/LoopsListView.tsx b/packages/ui/src/features/loops/components/LoopsListView.tsx index 2c0ed9f602..a584c27750 100644 --- a/packages/ui/src/features/loops/components/LoopsListView.tsx +++ b/packages/ui/src/features/loops/components/LoopsListView.tsx @@ -5,8 +5,8 @@ import { RepeatIcon, } from "@phosphor-icons/react"; import type { LoopSchemas } from "@posthog/api-client/loops"; -import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@posthog/quill"; +import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; import type { UserBasic } from "@posthog/shared/domain-types"; import { useOrgMembers } from "@posthog/ui/features/canvas/hooks/useOrgMembers"; import { StopCloudRunDialog } from "@posthog/ui/features/sessions/components/StopCloudRunDialog";