Skip to content

feat(scheduled-tasks): pause/resume, mutation toasts, submit guards, empty state#5044

Merged
waleedlatif1 merged 11 commits into
stagingfrom
feat/scheduled-tasks-control
Jun 14, 2026
Merged

feat(scheduled-tasks): pause/resume, mutation toasts, submit guards, empty state#5044
waleedlatif1 merged 11 commits into
stagingfrom
feat/scheduled-tasks-control

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a control & trust layer to the scheduled-tasks module — the calendar was solid, but mutations failed silently and there was no way to pause a task or recover from a blank first-run.
  • Toasts on every job mutation (create / update / delete / exclude-occurrence / pause / resume) — failures surface instead of vanishing; the modal/dialog no longer closes a failed save into the void.
  • Submit guards — the create/edit modal now awaits persistence: it stays open until the task saves (closing only on success), and the submit button is disabled in-flight so Enter racing the click can't double-create.
  • Pause / Resume recurring tasks — one context-menu item that swaps Pause↔Resume, reusing the existing disable/reactivate endpoints. Paused occurrences render dimmed so a suspended task stays visible and resumable instead of disappearing.
  • First-run empty state — a centered prompt + CTA when the workspace has no scheduled tasks, replacing the blank grid.

Type of Change

  • New feature

Testing

  • 45 unit tests pass (incl. new cases: active vs. paused recurring expansion, paused one-time produces nothing)
  • typecheck clean (sole error is pre-existing tailwind.config.ts), biome clean, check:api-validation and check:react-query pass
  • Pause/Resume is gated to recurring tasks only — one-time tasks carry no cadence to resume (the reactivate endpoint requires a cron)

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…empty state

- Toasts on every job mutation (create/update/delete/exclude/pause/resume) so failures are never silent
- TaskModal stays open until the save persists; submit disabled in-flight to block double-submit
- Pause/Resume recurring tasks via the context menu; paused occurrences render dimmed so they stay resumable
- First-run empty state with a create CTA when the workspace has no tasks
@vercel

vercel Bot commented Jun 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 14, 2026 5:49pm

Request Review

@cursor

cursor Bot commented Jun 14, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches schedule persistence and expansion logic for paused recurring jobs; modal submit flow changes could affect edge cases around concurrent delete vs save, though guards were added.

Overview
Adds pause and resume for upcoming recurring tasks via the task context menu (existing disable/reactivate APIs), wired through useScheduledTasks and a new useResumeSchedule hook. Paused series still expand on the calendar with a disabled flag so pills render dimmed and stay discoverable; one-time paused jobs with no history stay hidden.

The create/edit modal now awaits async saves: it stays open on failure, blocks dismiss and double-submit while in flight, and shows in-progress labels on the primary action. Create/update mutations use mutateAsync so rejections propagate to the modal.

Schedule mutations surface success and error toasts (create, update, delete, exclude occurrence, pause, resume). General settings aligns Theme, Timezone, and Snap-to-grid dropdown triggers to a shared 240px width.

Reviewed by Cursor Bugbot for commit a7624ee. Configure here.

…its persistence

The edit TaskModal's onSubmit called updateTask without returning its promise,
so the await resolved immediately — the modal closed before the save finished,
failed edits didn't stay open, and a rejected mutateAsync went unhandled. Return
the promise so the await tracks the real mutation (matching the create path).
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit ac2b846. Configure here.

@greptile-apps

greptile-apps Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a control and observability layer to the scheduled-tasks module: mutation toasts on every job operation (create/update/delete/pause/resume/exclude), a submit guard that keeps the modal open on failure, pause/resume of recurring tasks via the context menu, and dimmed rendering for paused calendar occurrences.

  • Submit guard (task-modal.tsx): handleSubmit is now async, awaiting onSubmit, and uses a submittingRef to prevent double-submission. A stale-closure trick makes programmatic success-close pass through handleOpenChange while user dismisses during flight are blocked.
  • Pause / Resume (use-scheduled-tasks.ts, task-context-menu.tsx, schedule-events.ts): pauseTask/resumeTask reuse the existing disable/reactivate endpoints; paused recurring schedules still expand occurrences flagged disabled: true so they remain visible and resumable.
  • Toast layer (schedules.ts): onSuccess/onError toast callbacks added to all six mutation hooks; mutateAsync callers wrap the rejection with .catch(() => false), so the onError toast fires once.

Confidence Score: 5/5

The PR is safe to merge — all mutation paths now surface errors via toast and the modal correctly guards against premature close and double-submit.

The submit guard uses a well-reasoned stale-closure pattern that correctly allows programmatic success-close to pass through while blocking all user-initiated dismissals during flight. The mutateAsync to onError to catch chain produces exactly one toast on failure and leaves the modal open with the draft intact. The pause/resume feature is correctly gated to recurring pending tasks only, and paused occurrences continue to expand in the calendar with a disabled flag. The schedule-events logic change is covered by three new unit tests.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/task-modal.tsx Submit guard added: async handleSubmit, submittingRef double-submit lock, stale-closure pass-through for programmatic close, setSubmitting cleared before close. Logic is sound.
apps/sim/hooks/queries/schedules.ts Added useResumeSchedule hook; onSuccess/onError toasts added to all six mutation hooks. Invalidation patterns are consistent with the existing hooks.
apps/sim/app/workspace/[workspaceId]/scheduled-tasks/hooks/use-scheduled-tasks.ts createTask/updateTask switched to mutateAsync for promise-based submit guard; pauseTask/resumeTask added using fire-and-forget mutate (correct for context-menu actions).
apps/sim/app/workspace/[workspaceId]/scheduled-tasks/utils/schedule-events.ts Added disabled field to ScheduledTask; paused recurring schedules now expand occurrences flagged disabled:true instead of being skipped entirely. Logic is correct.
apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-context-menu/task-context-menu.tsx Pause/Resume menu items added behind canPauseResume gate (recurring + pending only). Toggle between Pause/Resume driven by task.disabled. Correct.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant TM as TaskModal
    participant HS as handleSubmit
    participant HK as useScheduledTasks
    participant SQ as schedules.ts
    participant API as API

    U->>TM: Click Schedule or Save
    TM->>HS: handleSubmit()
    HS->>HK: createTask or updateTask
    HK->>SQ: mutateAsync
    SQ->>API: HTTP request
    alt Success
        API-->>SQ: 200 OK
        SQ->>SQ: onSuccess toast.success
        SQ-->>HS: resolves
        HS->>TM: close via stale closure
        TM-->>U: Modal closes
    else Failure
        API-->>SQ: Error
        SQ->>SQ: onError toast.error
        SQ-->>HS: catch returns false
        HS-->>TM: stays open
        TM-->>U: Modal open with toast
    end
Loading

Reviews (10): Last reviewed commit: "fix(scheduled-tasks): gate submit on a s..." | Re-trigger Greptile

Comment thread apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx Outdated
@greptile-apps

greptile-apps Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a control layer to the scheduled-tasks module: toast feedback on every job mutation, an async submit guard in the create/edit modal (stays open until the task persists, closes only on success), Pause/Resume for recurring tasks via the existing disable/reactivate endpoints, and a first-run empty state in place of the blank calendar grid.

  • Submit guard: handleSubmit is now async, uses mutateAsync for create and update, and gates the submit button with a submitting flag — but setSubmitting(false) is only called in the catch branch; the success path relies solely on unmounting via close().
  • Pause/Resume: paused recurring schedules are still expanded as disabled: true occurrences (rendered dimmed) so they stay visible and resumable; the context menu toggles Pause↔Resume based on task.disabled, gated to recurring === true.
  • Toasts: onSuccess/onError toast callbacks are added uniformly across all six mutation hooks; useResumeSchedule is a new hook that wraps the existing reactivate endpoint with workspace-scoped cache invalidation.

Confidence Score: 4/5

The change is well-scoped and the core Pause/Resume and toast wiring is correct; the only rough edge is the missing setSubmitting(false) on the success path in the modal.

The submit guard is the riskiest new surface: setSubmitting(false) is never called before close() on the success path. In the current wiring this is harmless because the Radix portal unmounts the content on close, discarding the state. If onOpenChange ever fails to actually close the dialog (e.g., a future consumer prevents it), the submit button is permanently stuck in the "Scheduling…" disabled state with no recovery path. The Pause/Resume expansion logic and the disabled flag propagation through schedule-events.ts are correct and well-tested. Toast wiring is consistent across all six mutation hooks.

task-modal.tsx — the handleSubmit async flow; use-scheduled-tasks.ts — the updateTask path no longer synchronously clears selectedTask (now delegated to the modal's close() callback).

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/task-modal.tsx Added async submit guard with submitting state — setSubmitting(false) is only called in the catch branch, not on the success path before close(); also accepts `void
apps/sim/app/workspace/[workspaceId]/scheduled-tasks/hooks/use-scheduled-tasks.ts Switched createTask/updateTask to mutateAsync for modal-close coordination; added pauseTask/resumeTask and hasTasks derived flag; removed the synchronous setSelectedTask clear from updateTask (now delegated to the modal's close()).
apps/sim/hooks/queries/schedules.ts Added useResumeSchedule mutation, toast feedback to all mutation hooks (create/update/delete/exclude/disable/resume), and wired getErrorMessage for error toasts.
apps/sim/app/workspace/[workspaceId]/scheduled-tasks/utils/schedule-events.ts Added disabled field to ScheduledTask; paused (status === 'disabled') recurring schedules now expand their future occurrences with disabled: true so they stay visible and resumable on the calendar.
apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx Wired isEmpty/onCreate props to the calendar, added handlePauseContextTask/handleResumeContextTask callbacks, and threaded pauseTask/resumeTask into the context menu.
apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-context-menu/task-context-menu.tsx Added Pause/Resume menu items gated to isUpcoming && task.recurring === true; toggles correctly based on task.disabled.
apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/schedule-calendar.tsx Added CalendarEmptyState component and isEmpty/onCreate props; skips the scroll effect when empty.
apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/calendar-event-chip/calendar-event-chip.tsx Applies opacity-45 to chips when event.task.disabled is true, visually indicating paused recurring occurrences.
apps/sim/app/workspace/[workspaceId]/scheduled-tasks/utils/schedule-events.test.ts Added disabled: false to makeTask default; new tests cover active vs. paused recurring expansion and paused one-time producing nothing.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant TM as TaskModal
    participant UST as useScheduledTasks
    participant SQ as schedules.ts (React Query)
    participant API as API

    U->>TM: clicks Schedule / Save
    TM->>TM: setSubmitting(true)
    TM->>UST: onSubmit(draft) → createTask / updateTask
    UST->>SQ: mutateAsync(body)
    SQ->>API: POST /schedules or PATCH /schedules/:id
    API-->>SQ: 200 OK
    SQ->>SQ: onSuccess → toast.success(...)
    SQ-->>UST: Promise resolves
    UST-->>TM: Promise resolves
    TM->>TM: close() → onOpenChange(false)
    Note right of TM: setSubmitting(false) never called on success path

    alt API error
        API-->>SQ: 4xx / 5xx
        SQ->>SQ: onError → toast.error(...)
        SQ-->>UST: Promise rejects
        UST-->>TM: throws
        TM->>TM: catch → setSubmitting(false)
        Note right of TM: Modal stays open with draft intact
    end

    U->>TM: right-clicks task chip
    TM->>UST: pauseTask(scheduleId) / resumeTask(scheduleId)
    UST->>SQ: disableSchedule.mutate / resumeSchedule.mutate
    UST->>UST: setSelectedTask(null) immediately
    SQ->>API: PATCH /schedules/:id (disable/reactivate)
    API-->>SQ: 200 OK
    SQ->>SQ: onSuccess → toast.success(...)
    SQ->>SQ: onSettled → invalidateQueries(list + details)
Loading

Reviews (2): Last reviewed commit: "refactor(scheduled-tasks): explicit retu..." | Re-trigger Greptile

…guard

- Seed the created job into the workspace-list cache on success so the new task
  renders instantly and the first-run empty state never flashes between the
  success toast and the list refetch; onSettled still reconciles authoritatively.
- Reset the modal's submitting flag in a finally so the submit button can never
  stick disabled if the modal is kept open.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/hooks/queries/schedules.ts Outdated
Remove the empty-state prompt and its supporting hasTasks flag; with the empty
state gone, also revert the create optimistic-insert (its only purpose was
avoiding the empty-state flash) back to plain toast + list invalidation.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Cancel, the header X, Escape, and overlay click all route through one guarded
onOpenChange that no-ops while submitting, and the footer Cancel is disabled
(cancelDisabled) — so an in-progress create/edit can't be abandoned mid-save and
lose its draft. submitting moves up to TaskModal so the guard can read it.
Theme/Snap-to-grid (ChipSelect) hugged their content (~90px) while Timezone
(ChipCombobox) was pinned to 260px, so the three read as a ragged column. Give
all three a shared 240px trigger via fullWidth + a common width wrapper so they
align as one column; menus match their triggers. No behavioral change — timezone
keeps search, options and handlers are untouched.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 79d40f7. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Delete bypasses the modal's dismiss guard (it closes via closeTask, not
onOpenChange), so a click mid-save could run a delete against the same task as
the in-flight update. Disable it while submitting, matching Cancel.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit c5d659d. Configure here.

Move the Delete-lock rationale to a TSDoc block on secondaryActions, and
restructure handleSubmit to a boolean persist result so the failure path is
self-evident — removing the inline comments and the empty catch block.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

… 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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit a7624ee. Configure here.

@waleedlatif1 waleedlatif1 merged commit 7a33508 into staging Jun 14, 2026
15 checks passed
@waleedlatif1 waleedlatif1 deleted the feat/scheduled-tasks-control branch June 14, 2026 18:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant