Fix recurrence edit/delete scope and simplify weekly UI

- Weekly recurrence no longer requires manual weekday selection;
  auto-derives from event start date
- EventForm now receives and forwards editScope prop to API
  (edit_scope in PUT body, scope query param in DELETE)
- CalendarPage passes scope through proper prop instead of _editScope hack
- Backend this_and_future: inherits parent's recurrence_rule when child
  has none, properly regenerates children after edit
- Backend: parent-level edits now delete+regenerate all children

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-22 01:08:00 +08:00
parent 232bdd3ef2
commit f826d05c60
3 changed files with 43 additions and 20 deletions

View File

@ -304,6 +304,13 @@ async def update_event(
this_original_start = event.original_start or event.start_datetime
if parent_id is not None:
# Fetch the parent's recurrence_rule before deleting siblings
parent_result = await db.execute(
select(CalendarEvent).where(CalendarEvent.id == parent_id)
)
parent_event = parent_result.scalar_one_or_none()
parent_rule = parent_event.recurrence_rule if parent_event else None
await db.execute(
delete(CalendarEvent).where(
CalendarEvent.parent_event_id == parent_id,
@ -318,17 +325,33 @@ async def update_event(
event.is_recurring = True
event.original_start = None
# If a new recurrence_rule was provided, regenerate children from this point
# Inherit parent's recurrence_rule if none was provided in update
if not event.recurrence_rule and parent_rule:
event.recurrence_rule = parent_rule
# Regenerate children from this point
if event.recurrence_rule:
await db.flush()
children = generate_occurrences(event)
for child in children:
db.add(child)
else:
# Not part of a series — plain update
# This IS a parent — update it and regenerate all children
for key, value in update_data.items():
setattr(event, key, value)
# Delete all existing children and regenerate
if event.recurrence_rule:
await db.execute(
delete(CalendarEvent).where(
CalendarEvent.parent_event_id == event.id
)
)
await db.flush()
children = generate_occurrences(event)
for child in children:
db.add(child)
await db.commit()
else:

View File

@ -46,6 +46,7 @@ export default function CalendarPage() {
const [scopeDialogOpen, setScopeDialogOpen] = useState(false);
const [scopeAction, setScopeAction] = useState<ScopeAction>('edit');
const [scopeEvent, setScopeEvent] = useState<CalendarEvent | null>(null);
const [activeEditScope, setActiveEditScope] = useState<'this' | 'this_and_future' | null>(null);
const { data: calendars = [] } = useCalendars();
@ -177,8 +178,8 @@ export default function CalendarPage() {
const handleScopeChoice = (scope: 'this' | 'this_and_future') => {
if (!scopeEvent) return;
if (scopeAction === 'edit') {
// For edits, open form — the form will send scope on save
setEditingEvent({ ...scopeEvent, _editScope: scope } as any);
setEditingEvent(scopeEvent);
setActiveEditScope(scope);
setShowForm(true);
setScopeDialogOpen(false);
} else if (scopeAction === 'delete') {
@ -235,6 +236,7 @@ export default function CalendarPage() {
calendarRef.current?.getApi().unselect();
setShowForm(false);
setEditingEvent(null);
setActiveEditScope(null);
setSelectedStart(null);
setSelectedEnd(null);
setSelectedAllDay(false);
@ -323,6 +325,7 @@ export default function CalendarPage() {
initialStart={selectedStart}
initialEnd={selectedEnd}
initialAllDay={selectedAllDay}
editScope={activeEditScope}
onClose={handleCloseForm}
/>
)}

View File

@ -25,6 +25,7 @@ interface EventFormProps {
initialStart?: string | null;
initialEnd?: string | null;
initialAllDay?: boolean;
editScope?: 'this' | 'this_and_future' | null;
onClose: () => void;
}
@ -74,7 +75,7 @@ function parseRecurrenceRule(raw?: string): RecurrenceRule | null {
}
}
export default function EventForm({ event, initialStart, initialEnd, initialAllDay, onClose }: EventFormProps) {
export default function EventForm({ event, initialStart, initialEnd, initialAllDay, editScope, onClose }: EventFormProps) {
const queryClient = useQueryClient();
const { data: calendars = [] } = useCalendars();
const isAllDay = event?.all_day ?? initialAllDay ?? false;
@ -125,7 +126,8 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
case 'every_n_days':
return { type: 'every_n_days', interval: recurrenceInterval };
case 'weekly':
return { type: 'weekly', weekday: recurrenceWeekday };
// No weekday needed — backend derives it from the event's start date
return { type: 'weekly' };
case 'monthly_nth_weekday':
return { type: 'monthly_nth_weekday', week: recurrenceWeek, weekday: recurrenceWeekday };
case 'monthly_date':
@ -145,7 +147,7 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
endDt = adjustAllDayEndForSave(endDt);
}
const payload = {
const payload: Record<string, any> = {
title: data.title,
description: data.description,
start_datetime: data.start_datetime,
@ -158,6 +160,9 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
};
if (event) {
if (editScope) {
payload.edit_scope = editScope;
}
const response = await api.put(`/events/${event.id}`, payload);
return response.data;
} else {
@ -179,7 +184,8 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
const deleteMutation = useMutation({
mutationFn: async () => {
await api.delete(`/events/${event?.id}`);
const params = editScope ? `?scope=${editScope}` : '';
await api.delete(`/events/${event?.id}${params}`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['calendar-events'] });
@ -346,18 +352,9 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
)}
{recurrenceType === 'weekly' && (
<div className="space-y-2">
<Label htmlFor="weekday">Day of week</Label>
<Select
id="weekday"
value={recurrenceWeekday.toString()}
onChange={(e) => setRecurrenceWeekday(parseInt(e.target.value))}
>
{WEEKDAYS.map((name, i) => (
<option key={i} value={i}>{name}</option>
))}
</Select>
</div>
<p className="text-xs text-muted-foreground">
Repeats every week on the same day as the start date.
</p>
)}
{recurrenceType === 'monthly_nth_weekday' && (