loops wizard: expose 'until' repeat policy

Third radio option on the repeat step. Two inputs: event name (defaults
to 'ok') and a within_iters cap (defaults to 20). Ships the full
{kind: 'until', event, within_iters} payload the schema already accepts.
LoopsCanvas repeat summary now formats iters/until/infinite via one
helper — the previous inline expression was rendering 'until' as a bare
label with no event context.
This commit is contained in:
Omar Sobh
2026-07-06 12:33:42 -07:00
parent 8a4e222aec
commit 9079184bb2
2 changed files with 98 additions and 10 deletions
@@ -36,8 +36,12 @@ export function LoopsWizard({
const [cron, setCron] = useState("0 */6 * * *");
const [onCompletion, setOnCompletion] = useState(false);
const [webhookEnabled, setWebhookEnabled] = useState(false);
const [repeatKind, setRepeatKind] = useState<"infinite" | "iters">("infinite");
const [repeatKind, setRepeatKind] = useState<"infinite" | "iters" | "until">(
"infinite",
);
const [iters, setIters] = useState(10);
const [untilEvent, setUntilEvent] = useState("ok");
const [untilWithin, setUntilWithin] = useState(20);
const [submitting, setSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const [created, setCreated] = useState<LoopCreated | null>(null);
@@ -60,10 +64,24 @@ export function LoopsWizard({
setSubmitting(false);
return;
}
const repeat_policy: LoopRepeatPolicy =
repeatKind === "infinite"
? { kind: "infinite" }
: { kind: "iters", n: Math.max(1, Math.floor(iters)) };
let repeat_policy: LoopRepeatPolicy;
if (repeatKind === "infinite") {
repeat_policy = { kind: "infinite" };
} else if (repeatKind === "iters") {
repeat_policy = { kind: "iters", n: Math.max(1, Math.floor(iters)) };
} else {
const event = untilEvent.trim();
if (!event) {
setSubmitError("Until: event name is required");
setSubmitting(false);
return;
}
repeat_policy = {
kind: "until",
event,
within_iters: Math.max(1, Math.floor(untilWithin)),
};
}
const out = await createLoop({
title: title.trim(),
description: description.trim(),
@@ -260,6 +278,69 @@ export function LoopsWizard({
/>
)}
</label>
<label style={radioRowStyle(repeatKind === "until")}>
<input
type="radio"
checked={repeatKind === "until"}
onChange={() => setRepeatKind("until")}
/>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, color: "#f3f3f5" }}>Until event</div>
<div style={hintStyle}>
Stop the first time an iteration emits the named run event.
Give up after the iteration cap regardless.
</div>
{repeatKind === "until" && (
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 120px",
gap: 8,
marginTop: 8,
}}
>
<label
style={{
display: "flex",
flexDirection: "column",
gap: 4,
}}
>
<span style={{ ...hintStyle, fontSize: 10, letterSpacing: ".1em", textTransform: "uppercase" }}>
Event
</span>
<input
type="text"
value={untilEvent}
placeholder="ok"
onChange={(e) => setUntilEvent(e.target.value)}
style={fieldStyle}
/>
</label>
<label
style={{
display: "flex",
flexDirection: "column",
gap: 4,
}}
>
<span style={{ ...hintStyle, fontSize: 10, letterSpacing: ".1em", textTransform: "uppercase" }}>
Cap
</span>
<input
type="number"
value={untilWithin}
min={1}
onChange={(e) =>
setUntilWithin(parseInt(e.target.value, 10) || 1)
}
style={fieldStyle}
/>
</label>
</div>
)}
</div>
</label>
{submitError && (
<p style={{ fontFamily: mono, fontSize: 12, color: "#ff8a7a" }}>
{submitError}