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
|
this_original_start = event.original_start or event.start_datetime
|
||||||
|
|
||||||
if parent_id is not None:
|
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(
|
await db.execute(
|
||||||
delete(CalendarEvent).where(
|
delete(CalendarEvent).where(
|
||||||
CalendarEvent.parent_event_id == parent_id,
|
CalendarEvent.parent_event_id == parent_id,
|
||||||
@ -318,17 +325,33 @@ async def update_event(
|
|||||||
event.is_recurring = True
|
event.is_recurring = True
|
||||||
event.original_start = None
|
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:
|
if event.recurrence_rule:
|
||||||
await db.flush()
|
await db.flush()
|
||||||
children = generate_occurrences(event)
|
children = generate_occurrences(event)
|
||||||
for child in children:
|
for child in children:
|
||||||
db.add(child)
|
db.add(child)
|
||||||
else:
|
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():
|
for key, value in update_data.items():
|
||||||
setattr(event, key, value)
|
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()
|
await db.commit()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -46,6 +46,7 @@ export default function CalendarPage() {
|
|||||||
const [scopeDialogOpen, setScopeDialogOpen] = useState(false);
|
const [scopeDialogOpen, setScopeDialogOpen] = useState(false);
|
||||||
const [scopeAction, setScopeAction] = useState<ScopeAction>('edit');
|
const [scopeAction, setScopeAction] = useState<ScopeAction>('edit');
|
||||||
const [scopeEvent, setScopeEvent] = useState<CalendarEvent | null>(null);
|
const [scopeEvent, setScopeEvent] = useState<CalendarEvent | null>(null);
|
||||||
|
const [activeEditScope, setActiveEditScope] = useState<'this' | 'this_and_future' | null>(null);
|
||||||
|
|
||||||
const { data: calendars = [] } = useCalendars();
|
const { data: calendars = [] } = useCalendars();
|
||||||
|
|
||||||
@ -177,8 +178,8 @@ export default function CalendarPage() {
|
|||||||
const handleScopeChoice = (scope: 'this' | 'this_and_future') => {
|
const handleScopeChoice = (scope: 'this' | 'this_and_future') => {
|
||||||
if (!scopeEvent) return;
|
if (!scopeEvent) return;
|
||||||
if (scopeAction === 'edit') {
|
if (scopeAction === 'edit') {
|
||||||
// For edits, open form — the form will send scope on save
|
setEditingEvent(scopeEvent);
|
||||||
setEditingEvent({ ...scopeEvent, _editScope: scope } as any);
|
setActiveEditScope(scope);
|
||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
setScopeDialogOpen(false);
|
setScopeDialogOpen(false);
|
||||||
} else if (scopeAction === 'delete') {
|
} else if (scopeAction === 'delete') {
|
||||||
@ -235,6 +236,7 @@ export default function CalendarPage() {
|
|||||||
calendarRef.current?.getApi().unselect();
|
calendarRef.current?.getApi().unselect();
|
||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
setEditingEvent(null);
|
setEditingEvent(null);
|
||||||
|
setActiveEditScope(null);
|
||||||
setSelectedStart(null);
|
setSelectedStart(null);
|
||||||
setSelectedEnd(null);
|
setSelectedEnd(null);
|
||||||
setSelectedAllDay(false);
|
setSelectedAllDay(false);
|
||||||
@ -323,6 +325,7 @@ export default function CalendarPage() {
|
|||||||
initialStart={selectedStart}
|
initialStart={selectedStart}
|
||||||
initialEnd={selectedEnd}
|
initialEnd={selectedEnd}
|
||||||
initialAllDay={selectedAllDay}
|
initialAllDay={selectedAllDay}
|
||||||
|
editScope={activeEditScope}
|
||||||
onClose={handleCloseForm}
|
onClose={handleCloseForm}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ interface EventFormProps {
|
|||||||
initialStart?: string | null;
|
initialStart?: string | null;
|
||||||
initialEnd?: string | null;
|
initialEnd?: string | null;
|
||||||
initialAllDay?: boolean;
|
initialAllDay?: boolean;
|
||||||
|
editScope?: 'this' | 'this_and_future' | null;
|
||||||
onClose: () => void;
|
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 queryClient = useQueryClient();
|
||||||
const { data: calendars = [] } = useCalendars();
|
const { data: calendars = [] } = useCalendars();
|
||||||
const isAllDay = event?.all_day ?? initialAllDay ?? false;
|
const isAllDay = event?.all_day ?? initialAllDay ?? false;
|
||||||
@ -125,7 +126,8 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
|
|||||||
case 'every_n_days':
|
case 'every_n_days':
|
||||||
return { type: 'every_n_days', interval: recurrenceInterval };
|
return { type: 'every_n_days', interval: recurrenceInterval };
|
||||||
case 'weekly':
|
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':
|
case 'monthly_nth_weekday':
|
||||||
return { type: 'monthly_nth_weekday', week: recurrenceWeek, weekday: recurrenceWeekday };
|
return { type: 'monthly_nth_weekday', week: recurrenceWeek, weekday: recurrenceWeekday };
|
||||||
case 'monthly_date':
|
case 'monthly_date':
|
||||||
@ -145,7 +147,7 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
|
|||||||
endDt = adjustAllDayEndForSave(endDt);
|
endDt = adjustAllDayEndForSave(endDt);
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload: Record<string, any> = {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
start_datetime: data.start_datetime,
|
start_datetime: data.start_datetime,
|
||||||
@ -158,6 +160,9 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
|
if (editScope) {
|
||||||
|
payload.edit_scope = editScope;
|
||||||
|
}
|
||||||
const response = await api.put(`/events/${event.id}`, payload);
|
const response = await api.put(`/events/${event.id}`, payload);
|
||||||
return response.data;
|
return response.data;
|
||||||
} else {
|
} else {
|
||||||
@ -179,7 +184,8 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
|
|||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
await api.delete(`/events/${event?.id}`);
|
const params = editScope ? `?scope=${editScope}` : '';
|
||||||
|
await api.delete(`/events/${event?.id}${params}`);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['calendar-events'] });
|
queryClient.invalidateQueries({ queryKey: ['calendar-events'] });
|
||||||
@ -346,18 +352,9 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{recurrenceType === 'weekly' && (
|
{recurrenceType === 'weekly' && (
|
||||||
<div className="space-y-2">
|
<p className="text-xs text-muted-foreground">
|
||||||
<Label htmlFor="weekday">Day of week</Label>
|
Repeats every week on the same day as the start date.
|
||||||
<Select
|
</p>
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{recurrenceType === 'monthly_nth_weekday' && (
|
{recurrenceType === 'monthly_nth_weekday' && (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user