From ab0316eb57af410b088538c62ba83b82847eea62 Mon Sep 17 00:00:00 2001 From: Matt Pua Date: Mon, 27 Jul 2026 13:16:09 -0400 Subject: [PATCH 1/3] Allow inline loop metadata editing Generated-By: PostHog Code Task-Id: beb0b035-943b-4a49-a2e4-04b47c3cbf07 --- .../loops/components/LoopDetailView.tsx | 175 ++++++++++++++++-- 1 file changed, 164 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/features/loops/components/LoopDetailView.tsx b/packages/ui/src/features/loops/components/LoopDetailView.tsx index ff8685713f..061f8d526e 100644 --- a/packages/ui/src/features/loops/components/LoopDetailView.tsx +++ b/packages/ui/src/features/loops/components/LoopDetailView.tsx @@ -1,4 +1,8 @@ -import { ArrowLeftIcon, RepeatIcon } from "@phosphor-icons/react"; +import { + ArrowLeftIcon, + PencilSimpleIcon, + RepeatIcon, +} from "@phosphor-icons/react"; import type { LoopSchemas } from "@posthog/api-client/loops"; import { isUploadableSkillSource } from "@posthog/core/message-editor/skillTags"; import { useHostTRPC } from "@posthog/host-router/react"; @@ -32,7 +36,7 @@ import { } from "@posthog/ui/router/navigationBridge"; import { track } from "@posthog/ui/shell/analytics"; import { useHostCapabilities } from "@posthog/ui/shell/useHostCapabilities"; -import { Flex, Text } from "@radix-ui/themes"; +import { Flex, Text, TextField } from "@radix-ui/themes"; import { useQuery } from "@tanstack/react-query"; import { useEffect, useRef, useState } from "react"; import { useLoop } from "../hooks/useLoop"; @@ -215,10 +219,8 @@ export function LoopDetailView({ loopId }: { loopId: string }) { - - - {loop.name} - + + {loopStatusLabel(loop)} @@ -257,11 +259,7 @@ export function LoopDetailView({ loopId }: { loopId: string }) { - {loop.description.trim() ? ( - - {loop.description} - - ) : null} + @@ -347,6 +345,161 @@ export function LoopDetailView({ loopId }: { loopId: string }) { ); } +function EditableLoopTitle({ loop }: { loop: LoopSchemas.Loop }) { + const updateLoop = useUpdateLoop(loop.id); + const [draft, setDraft] = useState(null); + const skipCommit = useRef(false); + + const commit = (value: string) => { + if (skipCommit.current) { + skipCommit.current = false; + return; + } + const name = value.trim(); + if (!name || name === loop.name.trim()) { + setDraft(null); + return; + } + updateLoop.mutate( + { name }, + { + onSuccess: () => { + setDraft(null); + toast.success("Loop title updated"); + }, + onError: (error) => { + setDraft(null); + toast.error("Failed to update loop title", { + description: error.message, + }); + }, + }, + ); + }; + + if (draft === null) { + return ( + + ); + } + + return ( + setDraft(event.currentTarget.value)} + onBlur={(event) => commit(event.currentTarget.value)} + onKeyDown={(event) => { + if (event.key === "Escape") { + skipCommit.current = true; + setDraft(null); + event.currentTarget.blur(); + } else if (event.key === "Enter") { + event.preventDefault(); + event.currentTarget.blur(); + } + }} + /> + ); +} + +function EditableLoopDescription({ loop }: { loop: LoopSchemas.Loop }) { + const updateLoop = useUpdateLoop(loop.id); + const [draft, setDraft] = useState(null); + const skipCommit = useRef(false); + + const commit = (value: string) => { + if (skipCommit.current) { + skipCommit.current = false; + return; + } + const description = value.trim(); + if (description === loop.description.trim()) { + setDraft(null); + return; + } + updateLoop.mutate( + { description }, + { + onSuccess: () => { + setDraft(null); + toast.success("Loop description updated"); + }, + onError: (error) => { + setDraft(null); + toast.error("Failed to update loop description", { + description: error.message, + }); + }, + }, + ); + }; + + if (draft === null) { + return ( + + ); + } + + return ( +