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:
parent
232bdd3ef2
commit
f826d05c60
@ -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:
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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' && (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user