Skip to content

Commit d714c54

Browse files
authored
feat(loops): edit instructions inline on loop detail (#3717)
1 parent 744d66a commit d714c54

1 file changed

Lines changed: 137 additions & 56 deletions

File tree

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

Lines changed: 137 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
import { ArrowLeftIcon, RepeatIcon } from "@phosphor-icons/react";
22
import type { LoopSchemas } from "@posthog/api-client/loops";
3-
import { Switch } from "@posthog/quill";
3+
import {
4+
AlertDialog,
5+
AlertDialogClose,
6+
AlertDialogContent,
7+
AlertDialogDescription,
8+
AlertDialogFooter,
9+
AlertDialogHeader,
10+
AlertDialogTitle,
11+
Badge,
12+
Button,
13+
Switch,
14+
Textarea,
15+
} from "@posthog/quill";
416
import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent";
5-
import { Badge } from "@posthog/ui/primitives/Badge";
6-
import { Button } from "@posthog/ui/primitives/Button";
717
import { toast } from "@posthog/ui/primitives/toast";
818
import {
919
navigateToEditLoop,
1020
navigateToLoops,
1121
} from "@posthog/ui/router/navigationBridge";
12-
import { AlertDialog, Flex, Text } from "@radix-ui/themes";
13-
import { useState } from "react";
22+
import { Flex, Text } from "@radix-ui/themes";
23+
import { useRef, useState } from "react";
1424
import { useLoop } from "../hooks/useLoop";
1525
import { useLoopDisplayModel } from "../hooks/useLoopDisplayModel";
1626
import {
@@ -102,24 +112,25 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
102112
<div className="mx-auto w-full max-w-5xl px-8 py-6">
103113
<Flex direction="column" gap="5">
104114
<Flex direction="column" gap="3">
105-
<button
106-
type="button"
115+
<Button
116+
variant="link-muted"
117+
size="xs"
107118
onClick={navigateToLoops}
108-
className="flex w-fit items-center gap-1.5 border-none bg-transparent p-0 text-[12px] text-gray-11 no-underline hover:text-gray-12"
119+
className="w-fit px-0"
109120
>
110121
<ArrowLeftIcon size={13} />
111122
Loops
112-
</button>
123+
</Button>
113124

114125
<Flex align="center" justify="between" gap="3" wrap="wrap">
115126
<Flex align="center" gap="2" wrap="wrap">
116127
<Text className="font-bold text-[22px] text-gray-12 leading-tight tracking-tight">
117128
{loop.name}
118129
</Text>
119-
<Badge color={loopStatusColor(loop)}>
130+
<Badge variant={loopStatusBadgeVariant(loop)}>
120131
{loopStatusLabel(loop)}
121132
</Badge>
122-
<Badge color="gray">{loop.visibility}</Badge>
133+
<Badge>{loop.visibility}</Badge>
123134
</Flex>
124135
<Flex align="center" gap="2">
125136
<Switch
@@ -129,27 +140,24 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
129140
onCheckedChange={handleToggleEnabled}
130141
/>
131142
<Button
132-
variant="soft"
133-
color="gray"
134-
size="1"
143+
variant="outline"
144+
size="xs"
135145
loading={runLoop.isPending}
136146
disabled={runLoop.isPending}
137147
onClick={handleRunNow}
138148
>
139149
Run now
140150
</Button>
141151
<Button
142-
variant="soft"
143-
color="gray"
144-
size="1"
152+
variant="outline"
153+
size="xs"
145154
onClick={() => navigateToEditLoop(loop.id)}
146155
>
147156
Edit
148157
</Button>
149158
<Button
150-
variant="soft"
151-
color="red"
152-
size="1"
159+
variant="destructive"
160+
size="xs"
153161
onClick={() => setDeleteOpen(true)}
154162
>
155163
Delete
@@ -166,6 +174,8 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
166174

167175
<ConfigSummarySection loop={loop} />
168176

177+
<InstructionsSection loop={loop} />
178+
169179
<Flex direction="column" gap="2">
170180
<Flex align="center" gap="2">
171181
<Text className="font-medium text-[13px] text-gray-12">
@@ -202,41 +212,51 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
202212
</Flex>
203213
</Flex>
204214

205-
<AlertDialog.Root open={deleteOpen} onOpenChange={setDeleteOpen}>
206-
<AlertDialog.Content maxWidth="420px" size="1">
207-
<AlertDialog.Title className="text-sm">Delete loop</AlertDialog.Title>
208-
<AlertDialog.Description className="text-[13px]">
209-
<Text color="gray" className="text-[13px]">
210-
Permanently delete{" "}
211-
<Text className="font-medium text-[13px]">{loop.name}</Text>? This
212-
stops every trigger and cannot be undone.
213-
</Text>
214-
</AlertDialog.Description>
215-
<Flex justify="end" gap="3" mt="3">
216-
<AlertDialog.Cancel>
217-
<Button variant="soft" color="gray" size="1">
218-
Cancel
219-
</Button>
220-
</AlertDialog.Cancel>
221-
<AlertDialog.Action>
222-
<Button
223-
variant="solid"
224-
color="red"
225-
size="1"
226-
loading={deleteLoop.isPending}
227-
disabled={deleteLoop.isPending}
228-
onClick={handleDelete}
229-
>
230-
Delete
231-
</Button>
232-
</AlertDialog.Action>
233-
</Flex>
234-
</AlertDialog.Content>
235-
</AlertDialog.Root>
215+
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
216+
<AlertDialogContent className="max-w-md">
217+
<AlertDialogHeader>
218+
<AlertDialogTitle>Delete loop</AlertDialogTitle>
219+
<AlertDialogDescription>
220+
<Text color="gray" className="text-[13px]">
221+
Permanently delete{" "}
222+
<Text className="font-medium text-[13px]">{loop.name}</Text>?
223+
This stops every trigger and cannot be undone.
224+
</Text>
225+
</AlertDialogDescription>
226+
</AlertDialogHeader>
227+
<AlertDialogFooter>
228+
<AlertDialogClose
229+
render={
230+
<Button variant="outline" size="sm">
231+
Cancel
232+
</Button>
233+
}
234+
/>
235+
<Button
236+
variant="destructive"
237+
size="sm"
238+
loading={deleteLoop.isPending}
239+
disabled={deleteLoop.isPending}
240+
onClick={handleDelete}
241+
>
242+
Delete
243+
</Button>
244+
</AlertDialogFooter>
245+
</AlertDialogContent>
246+
</AlertDialog>
236247
</div>
237248
);
238249
}
239250

251+
function loopStatusBadgeVariant(
252+
loop: LoopSchemas.Loop,
253+
): "default" | "destructive" | "success" {
254+
const color = loopStatusColor(loop);
255+
if (color === "green") return "success";
256+
if (color === "red") return "destructive";
257+
return "default";
258+
}
259+
240260
function ConfigSummarySection({ loop }: { loop: LoopSchemas.Loop }) {
241261
const displayModel = useLoopDisplayModel(loop.runtime_adapter, loop.model);
242262

@@ -281,13 +301,74 @@ function ConfigSummarySection({ loop }: { loop: LoopSchemas.Loop }) {
281301
</Flex>
282302
)}
283303
</SummaryRow>
304+
</Flex>
305+
</Flex>
306+
);
307+
}
284308

285-
<SummaryRow label="Instructions">
286-
<pre className="max-h-[200px] overflow-auto whitespace-pre-wrap text-[12px] text-gray-12 [font-family:var(--font-mono)]">
287-
{loop.instructions}
288-
</pre>
289-
</SummaryRow>
309+
function InstructionsSection({ loop }: { loop: LoopSchemas.Loop }) {
310+
const updateLoop = useUpdateLoop(loop.id);
311+
const [draft, setDraft] = useState<string | null>(null);
312+
// Escape reverts and blurs; skip the resulting onBlur save.
313+
const skipCommit = useRef(false);
314+
315+
const commit = (value: string) => {
316+
if (skipCommit.current) {
317+
skipCommit.current = false;
318+
return;
319+
}
320+
const trimmed = value.trim();
321+
if (!trimmed) {
322+
setDraft(null);
323+
return;
324+
}
325+
if (updateLoop.isPending) return;
326+
if (trimmed === loop.instructions.trim()) {
327+
setDraft(null);
328+
return;
329+
}
330+
updateLoop.mutate(
331+
{ instructions: trimmed },
332+
{
333+
onSuccess: () => {
334+
setDraft(null);
335+
toast.success("Instructions updated");
336+
},
337+
onError: (error) => {
338+
setDraft(null);
339+
toast.error("Failed to update instructions", {
340+
description: error.message,
341+
});
342+
},
343+
},
344+
);
345+
};
346+
347+
return (
348+
<Flex direction="column" gap="3">
349+
<Flex align="center" gap="2">
350+
<Text className="font-medium text-[13px] text-gray-12">
351+
Instructions
352+
</Text>
353+
{updateLoop.isPending ? (
354+
<Text className="text-[11px] text-gray-10">Saving…</Text>
355+
) : null}
290356
</Flex>
357+
<Textarea
358+
value={draft ?? loop.instructions}
359+
disabled={updateLoop.isPending}
360+
aria-label="Loop instructions"
361+
className="min-h-[200px] bg-(--color-panel-solid) text-[12.5px] leading-relaxed"
362+
onChange={(e) => setDraft(e.currentTarget.value)}
363+
onBlur={(e) => commit(e.currentTarget.value)}
364+
onKeyDown={(e) => {
365+
if (e.key === "Escape") {
366+
skipCommit.current = true;
367+
setDraft(null);
368+
e.currentTarget.blur();
369+
}
370+
}}
371+
/>
291372
</Flex>
292373
);
293374
}

0 commit comments

Comments
 (0)