-
Notifications
You must be signed in to change notification settings - Fork 3.7k
improvement(scheduled-tasks): move recurrence into modal body as a section #5054
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
waleedlatif1
merged 4 commits into
staging
from
improvement/scheduled-task-recurrence-section
Jun 15, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
424a1e3
improvement(scheduled-tasks): move recurrence into modal body as a se…
emir-karabeg ede498c
fix(scheduled-tasks): clear custom cron when switching frequency away…
emir-karabeg 857c39d
fix(scheduled-tasks): restore the prior cadence when re-enabling recu…
emir-karabeg cf57926
improvement(scheduled-tasks): compose canonical modal separator, tidy…
waleedlatif1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
131 changes: 0 additions & 131 deletions
131
.../app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/recurrence-control.tsx
This file was deleted.
Oops, something went wrong.
185 changes: 185 additions & 0 deletions
185
.../app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/recurrence-section.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| 'use client' | ||
|
|
||
| import { useRef } from 'react' | ||
| import { format } from 'date-fns' | ||
| import { ChipDatePicker, ChipModalField, ChipModalSeparator, Switch } from '@/components/emcn' | ||
| import type { | ||
| Recurrence, | ||
| RecurrenceFrequency, | ||
| } from '@/app/workspace/[workspaceId]/scheduled-tasks/utils/recurrence' | ||
|
|
||
| const WEEKDAY_PRESET = [1, 2, 3, 4, 5] | ||
| /** Seed count when the user first chooses "ends after N runs". */ | ||
| const DEFAULT_END_AFTER_COUNT = 10 | ||
| /** Cadence a task falls back to when the user first flips on recurrence. */ | ||
| const DEFAULT_RECURRING_FREQUENCY = 'daily' | ||
|
|
||
| /** The frequency presets the dropdown authors, keyed by a synthetic option value. */ | ||
| type FrequencyOption = 'daily' | 'weekly' | 'weekdays' | 'monthly' | 'custom' | ||
|
|
||
| function isWeekdayPreset(weekdays: number[]): boolean { | ||
| return ( | ||
| weekdays.length === WEEKDAY_PRESET.length && WEEKDAY_PRESET.every((d) => weekdays.includes(d)) | ||
| ) | ||
| } | ||
|
|
||
| /** Collapses a recurring recurrence into the single dropdown value that represents it. */ | ||
| function frequencyOptionFor(recurrence: Recurrence): FrequencyOption { | ||
| if (recurrence.frequency === 'weekly') | ||
| return isWeekdayPreset(recurrence.weekdays) ? 'weekdays' : 'weekly' | ||
| // Exhaustiveness fallback: callers gate on `isRecurring`, so `once` never | ||
| // reaches here at runtime, but the dropdown can't represent it — mapping it to | ||
| // a recurring default keeps the return type `FrequencyOption` without a cast. | ||
| if (recurrence.frequency === 'once') return DEFAULT_RECURRING_FREQUENCY | ||
| return recurrence.frequency | ||
| } | ||
|
|
||
| interface RecurrenceSectionProps { | ||
| recurrence: Recurrence | ||
| onChange: (recurrence: Recurrence) => void | ||
| /** The launch day, so weekly/monthly labels name the weekday and day-of-month. */ | ||
| launchDate: string | ||
| } | ||
|
|
||
| /** | ||
| * The repeat + end controls for a scheduled task, rendered as a body section | ||
| * below the prompt: a "Recurring" {@link Switch} that toggles a one-time launch | ||
| * into a repeat, and — once on — the frequency preset and how it ends (never, on | ||
| * a date, or after N runs). | ||
| * | ||
| * Composed as a sibling between the prompt body and footer; it owns its own | ||
| * leading separator and mirrors {@link ChipModalBody}'s spacing | ||
| * (`gap-4 px-2 pt-4 pb-4.5`) so every {@link ChipModalField} lands at the same | ||
| * effective `px-4` as the modal header/footer — no changes to the `ChipModal` | ||
| * primitives. | ||
| */ | ||
| export function RecurrenceSection({ recurrence, onChange, launchDate }: RecurrenceSectionProps) { | ||
| /** | ||
| * The cadence to reinstate when recurrence is toggled back on. Toggling off | ||
| * collapses `frequency` to `once`, dropping which preset was active, so the | ||
| * last recurring cadence is cached here and restored — a paused "Weekly on | ||
| * Mon" returns as weekly, not silently reset to daily. Written during render | ||
| * (an idempotent cache), so it is current before the toggle handler reads it. | ||
| */ | ||
| const lastRecurringFrequency = useRef<RecurrenceFrequency>(DEFAULT_RECURRING_FREQUENCY) | ||
| if (recurrence.frequency !== 'once') lastRecurringFrequency.current = recurrence.frequency | ||
|
|
||
| const launch = new Date(`${launchDate}T00:00`) | ||
| const isRecurring = recurrence.frequency !== 'once' | ||
|
|
||
| const frequencyOptions = [ | ||
| { value: 'daily', label: 'Daily' }, | ||
| { value: 'weekly', label: `Weekly on ${format(launch, 'EEE')}` }, | ||
| { value: 'weekdays', label: 'Weekdays' }, | ||
| { value: 'monthly', label: `Monthly on the ${format(launch, 'do')}` }, | ||
| ...(recurrence.frequency === 'custom' ? [{ value: 'custom', label: 'Custom' }] : []), | ||
| ] | ||
|
|
||
| /** | ||
| * Flips the one-time launch into a repeat and back. Toggling off keeps the | ||
| * recurrence shape (weekdays, end, and a passed-through `custom` cron) on the | ||
| * object and only collapses `frequency` to `once`; toggling back on reinstates | ||
| * the remembered cadence, so neither a weekly preset nor a conversationally | ||
| * authored custom cron is silently rewritten to daily. | ||
| */ | ||
| const handleRecurringToggle = (checked: boolean) => { | ||
| onChange({ ...recurrence, frequency: checked ? lastRecurringFrequency.current : 'once' }) | ||
| } | ||
|
emir-karabeg marked this conversation as resolved.
emir-karabeg marked this conversation as resolved.
|
||
|
|
||
| const handleFrequencyChange = (value: string) => { | ||
| const option = value as FrequencyOption | ||
| switch (option) { | ||
| case 'daily': | ||
| onChange({ ...recurrence, frequency: 'daily', weekdays: [], cron: undefined }) | ||
| return | ||
| case 'weekly': | ||
| onChange({ | ||
| ...recurrence, | ||
| frequency: 'weekly', | ||
| weekdays: [launch.getDay()], | ||
| cron: undefined, | ||
| }) | ||
| return | ||
| case 'weekdays': | ||
| onChange({ | ||
| ...recurrence, | ||
| frequency: 'weekly', | ||
| weekdays: [...WEEKDAY_PRESET], | ||
| cron: undefined, | ||
| }) | ||
| return | ||
| case 'monthly': | ||
| onChange({ ...recurrence, frequency: 'monthly', weekdays: [], cron: undefined }) | ||
| return | ||
| case 'custom': | ||
| onChange({ ...recurrence, frequency: 'custom' }) | ||
| } | ||
| } | ||
|
|
||
| const handleEndChange = (value: string) => { | ||
| if (value === 'never') onChange({ ...recurrence, end: { type: 'never' } }) | ||
| else if (value === 'on') | ||
| onChange({ ...recurrence, end: { type: 'on', date: format(launch, 'yyyy-MM-dd') } }) | ||
| else { | ||
| const count = recurrence.end.type === 'after' ? recurrence.end.count : DEFAULT_END_AFTER_COUNT | ||
| onChange({ ...recurrence, end: { type: 'after', count } }) | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <div className='flex flex-col'> | ||
| <ChipModalSeparator /> | ||
| <div className='flex flex-col gap-4 px-2 pt-4 pb-4.5'> | ||
| <ChipModalField type='custom' title='Recurring'> | ||
| <Switch checked={isRecurring} onCheckedChange={handleRecurringToggle} /> | ||
| </ChipModalField> | ||
|
|
||
| {isRecurring && ( | ||
| <> | ||
| <ChipModalField | ||
| type='dropdown' | ||
| title='Frequency' | ||
| value={frequencyOptionFor(recurrence)} | ||
| options={frequencyOptions} | ||
| onChange={handleFrequencyChange} | ||
| /> | ||
|
|
||
| <ChipModalField | ||
| type='dropdown' | ||
| title='Ends' | ||
| value={recurrence.end.type} | ||
| options={[ | ||
| { value: 'never', label: 'No end' }, | ||
| { value: 'on', label: 'Ends on' }, | ||
| { value: 'after', label: 'Ends after' }, | ||
| ]} | ||
| onChange={handleEndChange} | ||
| /> | ||
|
|
||
| {recurrence.end.type === 'on' && ( | ||
| <ChipModalField type='custom' title='End date'> | ||
| <ChipDatePicker | ||
| value={recurrence.end.date} | ||
| onChange={(date) => onChange({ ...recurrence, end: { type: 'on', date } })} | ||
| fullWidth | ||
| /> | ||
| </ChipModalField> | ||
| )} | ||
|
|
||
| {recurrence.end.type === 'after' && ( | ||
| <ChipModalField | ||
| type='input' | ||
| title='Number of runs' | ||
| value={String(recurrence.end.count)} | ||
| onChange={(value) => { | ||
| const count = Math.max(1, Math.floor(Number(value) || 1)) | ||
| onChange({ ...recurrence, end: { type: 'after', count } }) | ||
| }} | ||
| /> | ||
| )} | ||
| </> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.