Skip to content

Commit 2fce12f

Browse files
committed
improvement(scheduled-tasks): move recurrence into modal body as a section
Replace the footer RecurrenceControl (a row of chip dropdowns) with a RecurrenceSection rendered between the prompt body and footer: a "Recurring" Switch toggles one-time vs repeat, and — once on — frequency and end (never, on a date, after N runs) are labeled ChipModalField rows aligned to the modal header/footer gutter. Toggling Recurring off now preserves the recurrence shape (cadence, end, and a passed-through custom cron) and only sets frequency: 'once', so toggling back on restores a conversationally-authored custom schedule instead of silently rewriting it to daily. Also restore the prompt editor's native scale (text-[15px], -0.015em tracking) so the editor reads the same in the chat input and the task modal body.
1 parent e6587ca commit 2fce12f

4 files changed

Lines changed: 184 additions & 158 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,27 +47,25 @@ export interface PlusMenuHandle {
4747
/**
4848
* Box and typography shared by the textarea and its mirror overlay — both must
4949
* produce identical line wrapping so the overlay text sits exactly over the
50-
* (transparent) textarea text. The scale is the canonical chip text-field
51-
* scale ({@link ChipTextarea}: `text-sm`, default tracking), so the editor
52-
* reads identically in the chat input and inside chip modals — one size,
53-
* everywhere.
50+
* (transparent) textarea text. The scale is the chat input's native prompt
51+
* scale (`text-[15px]`, `-0.015em` tracking); the task modal's body inherits it
52+
* so the editor reads the same whether it's the chat input or inside the modal.
5453
*/
5554
const FIELD_MIRROR_CLASSES = cn(
56-
'm-0 box-border min-h-[20px] w-full break-words [overflow-wrap:anywhere] border-0 bg-transparent',
57-
'px-1 py-1 font-body text-sm leading-[20px]'
55+
'm-0 box-border min-h-[24px] w-full break-words [overflow-wrap:anywhere] border-0 bg-transparent',
56+
'px-1 py-1 font-body text-[15px] leading-[24px] tracking-[-0.015em]'
5857
)
5958

6059
/**
6160
* The textarea grows to its full content height (`h-auto`, no internal scroll);
6261
* the shared scroller clips and scrolls it. Its text is transparent so the
63-
* mirror overlay shows through; only the caret paints. The placeholder uses
64-
* the canonical `--text-muted`, matching every other chip text field.
62+
* mirror overlay shows through; only the caret paints.
6563
*/
6664
export const TEXTAREA_BASE_CLASSES = cn(
6765
FIELD_MIRROR_CLASSES,
6866
'block h-auto resize-none overflow-hidden',
6967
'text-transparent caret-[var(--text-primary)] outline-none',
70-
'placeholder:text-[var(--text-muted)]',
68+
'placeholder:font-[380] placeholder:text-[var(--text-subtle)]',
7169
'focus-visible:ring-0 focus-visible:ring-offset-0'
7270
)
7371

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

Lines changed: 0 additions & 131 deletions
This file was deleted.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
'use client'
2+
3+
import { format } from 'date-fns'
4+
import { ChipDatePicker, ChipModalField, Switch } from '@/components/emcn'
5+
import type { Recurrence } from '@/app/workspace/[workspaceId]/scheduled-tasks/utils/recurrence'
6+
7+
const WEEKDAY_PRESET = [1, 2, 3, 4, 5]
8+
/** Seed count when the user first chooses "ends after N runs". */
9+
const DEFAULT_END_AFTER_COUNT = 10
10+
/** Cadence a task falls back to when the user first flips on recurrence. */
11+
const DEFAULT_RECURRING_FREQUENCY = 'daily'
12+
13+
/** The frequency presets the dropdown authors, keyed by a synthetic option value. */
14+
type FrequencyOption = 'daily' | 'weekly' | 'weekdays' | 'monthly' | 'custom'
15+
16+
function isWeekdayPreset(weekdays: number[]): boolean {
17+
return (
18+
weekdays.length === WEEKDAY_PRESET.length && WEEKDAY_PRESET.every((d) => weekdays.includes(d))
19+
)
20+
}
21+
22+
/** Collapses a recurring recurrence into the single dropdown value that represents it. */
23+
function frequencyOptionFor(recurrence: Recurrence): FrequencyOption {
24+
if (recurrence.frequency === 'weekly')
25+
return isWeekdayPreset(recurrence.weekdays) ? 'weekdays' : 'weekly'
26+
if (recurrence.frequency === 'once') return DEFAULT_RECURRING_FREQUENCY
27+
return recurrence.frequency
28+
}
29+
30+
interface RecurrenceSectionProps {
31+
recurrence: Recurrence
32+
onChange: (recurrence: Recurrence) => void
33+
/** The launch day, so weekly/monthly labels name the weekday and day-of-month. */
34+
launchDate: string
35+
}
36+
37+
/**
38+
* The repeat + end controls for a scheduled task, rendered as a body section
39+
* below the prompt: a "Recurring" {@link Switch} that toggles a one-time launch
40+
* into a repeat, and — once on — the frequency preset and how it ends (never, on
41+
* a date, or after N runs).
42+
*
43+
* Composed as a sibling between the prompt body and footer; it owns its own
44+
* leading separator and mirrors {@link ChipModalBody}'s spacing
45+
* (`gap-4 px-2 pt-4 pb-4.5`) so every {@link ChipModalField} lands at the same
46+
* effective `px-4` as the modal header/footer — no changes to the `ChipModal`
47+
* primitives.
48+
*/
49+
export function RecurrenceSection({ recurrence, onChange, launchDate }: RecurrenceSectionProps) {
50+
const launch = new Date(`${launchDate}T00:00`)
51+
const isRecurring = recurrence.frequency !== 'once'
52+
53+
const frequencyOptions = [
54+
{ value: 'daily', label: 'Daily' },
55+
{ value: 'weekly', label: `Weekly on ${format(launch, 'EEE')}` },
56+
{ value: 'weekdays', label: 'Weekdays' },
57+
{ value: 'monthly', label: `Monthly on the ${format(launch, 'do')}` },
58+
...(recurrence.frequency === 'custom' ? [{ value: 'custom', label: 'Custom' }] : []),
59+
]
60+
61+
/**
62+
* Flips the one-time launch into a repeat and back. Toggling off keeps the
63+
* recurrence shape (cadence, end, and a passed-through `custom` cron) on the
64+
* object and only sets `frequency: 'once'` — the wire ignores everything but
65+
* `frequency` for a one-time task — so toggling back on restores `custom`
66+
* rather than silently rewriting a conversationally-authored cron to `daily`.
67+
*/
68+
const handleRecurringToggle = (checked: boolean) => {
69+
if (!checked) {
70+
onChange({ ...recurrence, frequency: 'once' })
71+
return
72+
}
73+
onChange({
74+
...recurrence,
75+
frequency: recurrence.cron ? 'custom' : DEFAULT_RECURRING_FREQUENCY,
76+
weekdays: [],
77+
})
78+
}
79+
80+
const handleFrequencyChange = (value: string) => {
81+
const option = value as FrequencyOption
82+
switch (option) {
83+
case 'daily':
84+
onChange({ ...recurrence, frequency: 'daily', weekdays: [] })
85+
return
86+
case 'weekly':
87+
onChange({ ...recurrence, frequency: 'weekly', weekdays: [launch.getDay()] })
88+
return
89+
case 'weekdays':
90+
onChange({ ...recurrence, frequency: 'weekly', weekdays: [...WEEKDAY_PRESET] })
91+
return
92+
case 'monthly':
93+
onChange({ ...recurrence, frequency: 'monthly', weekdays: [] })
94+
return
95+
case 'custom':
96+
onChange({ ...recurrence, frequency: 'custom' })
97+
}
98+
}
99+
100+
const handleEndChange = (value: string) => {
101+
if (value === 'never') onChange({ ...recurrence, end: { type: 'never' } })
102+
else if (value === 'on')
103+
onChange({ ...recurrence, end: { type: 'on', date: format(launch, 'yyyy-MM-dd') } })
104+
else {
105+
const count = recurrence.end.type === 'after' ? recurrence.end.count : DEFAULT_END_AFTER_COUNT
106+
onChange({ ...recurrence, end: { type: 'after', count } })
107+
}
108+
}
109+
110+
return (
111+
<div className='flex flex-col'>
112+
<div className='h-px bg-[var(--border)]' />
113+
<div className='flex flex-col gap-4 px-2 pt-4 pb-4.5'>
114+
<ChipModalField type='custom' title='Recurring'>
115+
<Switch checked={isRecurring} onCheckedChange={handleRecurringToggle} />
116+
</ChipModalField>
117+
118+
{isRecurring && (
119+
<>
120+
<ChipModalField
121+
type='dropdown'
122+
title='Frequency'
123+
value={frequencyOptionFor(recurrence)}
124+
options={frequencyOptions}
125+
onChange={handleFrequencyChange}
126+
/>
127+
128+
<ChipModalField
129+
type='dropdown'
130+
title='Ends'
131+
value={recurrence.end.type}
132+
options={[
133+
{ value: 'never', label: 'No end' },
134+
{ value: 'on', label: 'Ends on' },
135+
{ value: 'after', label: 'Ends after' },
136+
]}
137+
onChange={handleEndChange}
138+
/>
139+
140+
{recurrence.end.type === 'on' && (
141+
<ChipModalField type='custom' title='End date'>
142+
<ChipDatePicker
143+
value={recurrence.end.date}
144+
onChange={(date) => onChange({ ...recurrence, end: { type: 'on', date } })}
145+
fullWidth
146+
/>
147+
</ChipModalField>
148+
)}
149+
150+
{recurrence.end.type === 'after' && (
151+
<ChipModalField
152+
type='input'
153+
title='Number of runs'
154+
value={String(recurrence.end.count)}
155+
onChange={(value) => {
156+
const count = Math.max(1, Math.floor(Number(value) || 1))
157+
onChange({ ...recurrence, end: { type: 'after', count } })
158+
}}
159+
/>
160+
)}
161+
</>
162+
)}
163+
</div>
164+
</div>
165+
)
166+
}

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

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
PromptEditor,
1919
usePromptEditor,
2020
} from '@/app/workspace/[workspaceId]/home/components/user-input/components'
21-
import { RecurrenceControl } from '@/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/recurrence-control'
21+
import { RecurrenceSection } from '@/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/recurrence-section'
2222
import type { CalendarSlot } from '@/app/workspace/[workspaceId]/scheduled-tasks/hooks/use-calendar'
2323
import {
2424
DEFAULT_RECURRENCE,
@@ -117,10 +117,10 @@ interface TaskModalProps {
117117

118118
/**
119119
* The "schedule a task" modal, shared by create (blank, or pre-filled from a
120-
* duplicate) and edit (seeded from a task's schedule). The body is one prompt
121-
* surface — the chat input's editor, so `@` mentions resources and `/` invokes
122-
* skills exactly like talking to Sim — and the footer carries the recurrence,
123-
* launch date/time, and (edit only) Delete.
120+
* duplicate) and edit (seeded from a task's schedule). The body is the chat
121+
* input's editorso `@` mentions resources and `/` invokes skills exactly like
122+
* talking to Sim — followed by the recurrence section; the footer carries the
123+
* launch date/time and (edit only) Delete.
124124
*/
125125
export function TaskModal({
126126
open,
@@ -296,10 +296,11 @@ function TaskModalContent({
296296
}
297297

298298
/**
299-
* Footer secondary actions. Delete is disabled while `submitting` because it
300-
* bypasses the dismiss guard — it closes the modal via `closeTask`, not the
301-
* guarded `onOpenChange` — so without the lock an in-flight edit and a delete
302-
* could run against the same task at once.
299+
* Footer secondary actions — the launch date/time pickers and (edit only)
300+
* Delete. Delete is disabled while `submitting` because it bypasses the
301+
* dismiss guard — it closes the modal via `closeTask`, not the guarded
302+
* `onOpenChange` — so without the lock an in-flight edit and a delete could
303+
* run against the same task at once. Recurrence lives in the body, not here.
303304
*/
304305
const secondaryActions: ChipModalFooterSlotAction[] = [
305306
...(edit && onRequestDelete
@@ -312,15 +313,6 @@ function TaskModalContent({
312313
},
313314
]
314315
: []),
315-
{
316-
custom: (
317-
<RecurrenceControl
318-
recurrence={recurrence}
319-
onChange={setRecurrence}
320-
launchDate={launchDate}
321-
/>
322-
),
323-
},
324316
{ custom: <ChipDatePicker value={launchDate} onChange={editLaunchDate} flush /> },
325317
{ custom: <ChipTimePicker value={launchTime} onChange={editLaunchTime} flush /> },
326318
]
@@ -338,6 +330,7 @@ function TaskModalContent({
338330
onSubmit={handleSubmit}
339331
/>
340332
</ChipModalPromptBody>
333+
<RecurrenceSection recurrence={recurrence} onChange={setRecurrence} launchDate={launchDate} />
341334
<ChipModalFooter
342335
onCancel={close}
343336
cancelDisabled={submitting}

0 commit comments

Comments
 (0)