Skip to content

Commit 08dc7ba

Browse files
committed
fix(webapp): keep the watch card in step with its draft, and frozen while it submits
1 parent c3ee6a4 commit 08dc7ba

1 file changed

Lines changed: 53 additions & 9 deletions

File tree

apps/webapp/app/components/dashboard-agent/WatchCard.tsx

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ import {
1717
type WatchDraft,
1818
type WatchKind,
1919
} from "@internal/dashboard-agent-contracts";
20-
import { useState } from "react";
20+
import { useId, useState } from "react";
2121
import { Button } from "~/components/primitives/Buttons";
22-
import { CheckboxWithLabel } from "~/components/primitives/Checkbox";
22+
import { Checkbox } from "~/components/primitives/Checkbox";
2323
import { Input } from "~/components/primitives/Input";
2424
import { AgentSpinner } from "~/components/primitives/Spinner";
2525
import { cn } from "~/utils/cn";
@@ -62,26 +62,65 @@ function ButtonSpinner() {
6262
return <AgentSpinner size={14} />;
6363
}
6464

65+
/** Controlled, unlike `CheckboxWithLabel`: the draft is the only thing that says what's on. */
66+
function Toggle({
67+
label,
68+
checked,
69+
disabled,
70+
onChange,
71+
}: {
72+
label: string;
73+
checked: boolean;
74+
disabled: boolean;
75+
onChange: (checked: boolean) => void;
76+
}) {
77+
const id = useId();
78+
return (
79+
<div className={cn("group flex w-fit items-start gap-x-2", disabled && "opacity-70")}>
80+
<Checkbox
81+
id={id}
82+
checked={checked}
83+
disabled={disabled}
84+
onChange={(event) => onChange(event.target.checked)}
85+
className="mt-1"
86+
/>
87+
<label
88+
htmlFor={id}
89+
className={cn(
90+
"mt-0.5 select-none text-sm text-text-bright",
91+
disabled ? "cursor-default" : "cursor-pointer"
92+
)}
93+
>
94+
{label}
95+
</label>
96+
</div>
97+
);
98+
}
99+
65100
/** One choice in an inline picker. */
66101
function Choice({
67102
selected,
103+
disabled,
68104
onSelect,
69105
children,
70106
}: {
71107
selected: boolean;
108+
disabled: boolean;
72109
onSelect: () => void;
73110
children: React.ReactNode;
74111
}) {
75112
return (
76113
<button
77114
type="button"
78115
aria-pressed={selected}
116+
disabled={disabled}
79117
onClick={onSelect}
80118
className={cn(
81119
"rounded-full border px-2 py-0.5 text-xs transition focus-custom",
82120
selected
83121
? "border-border-brightest bg-background-bright text-text-bright"
84-
: "border-border-bright text-text-dimmed hover:text-text-bright"
122+
: "border-border-bright text-text-dimmed hover:text-text-bright",
123+
disabled && "cursor-default opacity-70 hover:text-text-dimmed"
85124
)}
86125
>
87126
{children}
@@ -178,6 +217,7 @@ export function WatchCard({
178217
<Choice
179218
key={kind}
180219
selected={kind === spec.kind}
220+
disabled={pending}
181221
onSelect={() => {
182222
if (kind !== spec.kind) onChange(withVariant(draft, kind));
183223
}}
@@ -200,6 +240,7 @@ export function WatchCard({
200240
min={0}
201241
variant="small"
202242
className="w-28"
243+
disabled={pending}
203244
// A half-typed field must show empty, not "NaN"; `watchDraftError`
204245
// is what refuses to submit it.
205246
value={Number.isFinite(spec.threshold) ? String(spec.threshold) : ""}
@@ -218,6 +259,7 @@ export function WatchCard({
218259
min={1}
219260
variant="small"
220261
className="w-28"
262+
disabled={pending}
221263
value={Number.isFinite(spec.thresholdMinutes) ? String(spec.thresholdMinutes) : ""}
222264
onChange={(event) =>
223265
onChange(withAgeMinutes(draft, Number.parseInt(event.target.value, 10)))
@@ -233,6 +275,7 @@ export function WatchCard({
233275
<Choice
234276
key={hours}
235277
selected={spec.maxHours === hours}
278+
disabled={pending}
236279
onSelect={() => onChange(withWindow(draft, hours))}
237280
>
238281
{formatWatchWindow(hours)}
@@ -247,6 +290,7 @@ export function WatchCard({
247290
<Choice
248291
key={minutes}
249292
selected={spec.checkEveryMinutes === minutes}
293+
disabled={pending}
250294
onSelect={() => onChange(withCadence(draft, minutes))}
251295
>
252296
{formatWatchCadence(minutes)}
@@ -258,18 +302,18 @@ export function WatchCard({
258302
group, so "email instead of chat" is not expressible. */}
259303
<Field label="When there's an answer">
260304
<div className="flex flex-col gap-1.5">
261-
<CheckboxWithLabel
262-
variant="simple/small"
305+
<Toggle
263306
label="Investigate attention outcomes"
264-
defaultChecked={draft.followUp.investigateOnAttention}
307+
checked={draft.followUp.investigateOnAttention}
308+
disabled={pending}
265309
onChange={(checked) =>
266310
onChange(withFollowUp(draft, { investigateOnAttention: checked }))
267311
}
268312
/>
269-
<CheckboxWithLabel
270-
variant="simple/small"
313+
<Toggle
271314
label="Also notify me externally"
272-
defaultChecked={draft.followUp.notifyExternally}
315+
checked={draft.followUp.notifyExternally}
316+
disabled={pending}
273317
onChange={(checked) => onChange(withFollowUp(draft, { notifyExternally: checked }))}
274318
/>
275319
</div>

0 commit comments

Comments
 (0)