Skip to content

Commit a7624ee

Browse files
committed
fix(scheduled-tasks): gate submit on a synchronous ref to fully block double-submit
The submitting state flag only reflects after a re-render, so two same-tick invocations (Enter racing the click) could both pass a state-based guard and fire two mutations. A submittingRef flips synchronously, so the second invocation is rejected before it can submit again; the state still drives the button/cancel UI.
1 parent a14cdbf commit a7624ee

1 file changed

Lines changed: 18 additions & 7 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal

apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/task-modal.tsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,14 @@ function TaskModalContent({
224224
() => source?.recurrence ?? DEFAULT_RECURRENCE
225225
)
226226
const launchEditedRef = useRef(false)
227+
/**
228+
* Synchronous mirror of `submitting` that gates {@link handleSubmit}. The
229+
* `submitting` state only reflects after a re-render, so two invocations in the
230+
* same tick (Enter racing the click) could both pass a state-based guard; the
231+
* ref flips immediately, so the second is rejected before it can fire a second
232+
* mutation.
233+
*/
234+
const submittingRef = useRef(false)
227235

228236
/**
229237
* Re-seed a blank create's default launch when the effective zone resolves
@@ -258,15 +266,17 @@ function TaskModalContent({
258266
const promptText = editor.value.trim()
259267

260268
/**
261-
* Submits the draft and waits for it to persist. The `submitting` guard blocks
262-
* a double-submit (Enter racing the click). The modal closes only when the save
263-
* resolves; a rejection leaves it open so the draft survives — the mutation hook
264-
* already surfaces the error via toast, so it is swallowed here rather than
265-
* duplicated. `submitting` is always cleared, so the button can never stick
266-
* disabled while the modal stays open.
269+
* Submits the draft and waits for it to persist. The synchronous
270+
* {@link submittingRef} guard blocks a double-submit (Enter racing the click).
271+
* The modal closes only when the save resolves; a rejection leaves it open so
272+
* the draft survives — the mutation hook already surfaces the error via toast,
273+
* so it is swallowed here rather than duplicated. Both the ref and the
274+
* `submitting` state are always cleared, so the button can never stick disabled
275+
* while the modal stays open.
267276
*/
268277
const handleSubmit = async () => {
269-
if (!promptText || isPastLaunch || submitting) return
278+
if (!promptText || isPastLaunch || submittingRef.current) return
279+
submittingRef.current = true
270280
setSubmitting(true)
271281
const persisted = await Promise.resolve(
272282
onSubmit({
@@ -280,6 +290,7 @@ function TaskModalContent({
280290
)
281291
.then(() => true)
282292
.catch(() => false)
293+
submittingRef.current = false
283294
setSubmitting(false)
284295
if (persisted) close()
285296
}

0 commit comments

Comments
 (0)