Skip to content

Commit a15566d

Browse files
d-csclaude
andcommitted
fix(webapp): control cancel-run Dialog state so submit isn't raced by Radix DialogClose
Previous attempt wrapped the form's submit button in <DialogClose asChild> so the dialog closed on click. That race-condition'd with Remix's <Form>: Radix's Slot-attached onClick triggered onOpenChange(false), the Dialog and its child Form unmounted mid-cycle, and the button's name=value pair (carrying `redirectUrl`) was dropped from the submitted FormData. The action then read `submission.value.redirectUrl` as undefined and the resulting redirect landed on `/env/dev` instead of the run-detail page. Switch to a ControlledCancelRunDialog at the call site that owns the Radix `open` state. The inner CancelRunDialog watches the navigation state transitions and signals the parent to close the dialog once the submission has captured its submitter cleanly. Submit-button name=value is preserved; redirect resolves to the run-detail page; modal still dismisses after submit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 75c6e2b commit a15566d

2 files changed

Lines changed: 74 additions & 31 deletions

File tree

  • apps/webapp/app

apps/webapp/app/components/runs/v3/CancelRunDialog.tsx

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NoSymbolIcon } from "@heroicons/react/24/solid";
22
import { DialogClose } from "@radix-ui/react-dialog";
33
import { Form, useNavigation } from "@remix-run/react";
4+
import { useEffect, useRef } from "react";
45
import { Button } from "~/components/primitives/Buttons";
56
import { DialogContent, DialogHeader } from "~/components/primitives/Dialog";
67
import { FormButtons } from "~/components/primitives/FormButtons";
@@ -10,14 +11,35 @@ import { SpinnerWhite } from "~/components/primitives/Spinner";
1011
type CancelRunDialogProps = {
1112
runFriendlyId: string;
1213
redirectPath: string;
14+
// Optional: when provided, close the dialog as soon as the cancel
15+
// action transitions to "loading" (the redirect is in flight). Lets
16+
// the caller control the open state without interfering with the
17+
// form's submit name=value pair the way `<DialogClose asChild>`
18+
// around the submit button does.
19+
onCancelSubmitted?: () => void;
1320
};
1421

15-
export function CancelRunDialog({ runFriendlyId, redirectPath }: CancelRunDialogProps) {
22+
export function CancelRunDialog({
23+
runFriendlyId,
24+
redirectPath,
25+
onCancelSubmitted,
26+
}: CancelRunDialogProps) {
1627
const navigation = useNavigation();
1728

1829
const formAction = `/resources/taskruns/${runFriendlyId}/cancel`;
1930
const isLoading = navigation.formAction === formAction;
2031

32+
const wasSubmitting = useRef(false);
33+
useEffect(() => {
34+
if (!onCancelSubmitted) return;
35+
if (navigation.state === "submitting" && navigation.formAction === formAction) {
36+
wasSubmitting.current = true;
37+
} else if (wasSubmitting.current && navigation.state !== "submitting") {
38+
wasSubmitting.current = false;
39+
onCancelSubmitted();
40+
}
41+
}, [navigation.state, navigation.formAction, formAction, onCancelSubmitted]);
42+
2143
return (
2244
<DialogContent key="cancel">
2345
<DialogHeader>Cancel this run?</DialogHeader>
@@ -28,19 +50,17 @@ export function CancelRunDialog({ runFriendlyId, redirectPath }: CancelRunDialog
2850
<FormButtons
2951
confirmButton={
3052
<Form action={`/resources/taskruns/${runFriendlyId}/cancel`} method="post">
31-
<DialogClose asChild>
32-
<Button
33-
type="submit"
34-
name="redirectUrl"
35-
value={redirectPath}
36-
variant="danger/medium"
37-
LeadingIcon={isLoading ? SpinnerWhite : NoSymbolIcon}
38-
disabled={isLoading}
39-
shortcut={{ modifiers: ["mod"], key: "enter" }}
40-
>
41-
{isLoading ? "Canceling..." : "Cancel run"}
42-
</Button>
43-
</DialogClose>
53+
<Button
54+
type="submit"
55+
name="redirectUrl"
56+
value={redirectPath}
57+
variant="danger/medium"
58+
LeadingIcon={isLoading ? SpinnerWhite : NoSymbolIcon}
59+
disabled={isLoading}
60+
shortcut={{ modifiers: ["mod"], key: "enter" }}
61+
>
62+
{isLoading ? "Canceling..." : "Cancel run"}
63+
</Button>
4464
</Form>
4565
}
4666
cancelButton={

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -480,23 +480,17 @@ export default function Page() {
480480
/>
481481
</Dialog>
482482
{run.isFinished ? null : (
483-
<Dialog key={`cancel-${run.friendlyId}`}>
484-
<DialogTrigger asChild>
485-
<Button variant="danger/small" LeadingIcon={StopCircleIcon} shortcut={{ key: "C" }}>
486-
Cancel run…
487-
</Button>
488-
</DialogTrigger>
489-
<CancelRunDialog
490-
runFriendlyId={run.friendlyId}
491-
redirectPath={v3RunSpanPath(
492-
organization,
493-
project,
494-
environment,
495-
{ friendlyId: run.friendlyId },
496-
{ spanId: run.spanId }
497-
)}
498-
/>
499-
</Dialog>
483+
<ControlledCancelRunDialog
484+
key={`cancel-${run.friendlyId}`}
485+
runFriendlyId={run.friendlyId}
486+
redirectPath={v3RunSpanPath(
487+
organization,
488+
project,
489+
environment,
490+
{ friendlyId: run.friendlyId },
491+
{ spanId: run.spanId }
492+
)}
493+
/>
500494
)}
501495
</PageAccessories>
502496
</NavBar>
@@ -660,6 +654,35 @@ function TraceView({
660654
);
661655
}
662656

657+
// Controlled wrapper around the cancel dialog. Owns the Radix open state
658+
// so the dialog closes itself once the cancel action transitions through
659+
// submission. We can't `<DialogClose asChild>`-wrap the submit button
660+
// because Radix's onClick handler swallows the button's name=value pair
661+
// that the form action depends on for `redirectUrl`.
662+
function ControlledCancelRunDialog({
663+
runFriendlyId,
664+
redirectPath,
665+
}: {
666+
runFriendlyId: string;
667+
redirectPath: string;
668+
}) {
669+
const [open, setOpen] = useState(false);
670+
return (
671+
<Dialog open={open} onOpenChange={setOpen}>
672+
<DialogTrigger asChild>
673+
<Button variant="danger/small" LeadingIcon={StopCircleIcon} shortcut={{ key: "C" }}>
674+
Cancel run…
675+
</Button>
676+
</DialogTrigger>
677+
<CancelRunDialog
678+
runFriendlyId={runFriendlyId}
679+
redirectPath={redirectPath}
680+
onCancelSubmitted={() => setOpen(false)}
681+
/>
682+
</Dialog>
683+
);
684+
}
685+
663686
function NoLogsView({ run, resizable }: Pick<LoaderData, "run" | "resizable">) {
664687
const plan = useCurrentPlan();
665688
const organization = useOrganization();

0 commit comments

Comments
 (0)