Implement complete recurring event deletion with EXDATE and RRULE UNTIL support

Frontend Changes:
- Add DeleteAction enum with DeleteThis, DeleteFollowing, DeleteSeries options
- Update EventContextMenu to show different delete options for recurring events
- Add exception_dates field to CalendarEvent struct
- Fix occurrence generation to respect EXDATE exclusions
- Add comprehensive RRULE parsing with UNTIL date support
- Fix UNTIL date parsing to handle backend format (YYYYMMDDTHHMMSSZ)
- Enhanced debugging for RRULE processing and occurrence generation

Backend Changes:
- Add exception_dates field to CalendarEvent struct with EXDATE parsing/generation
- Implement update_event method for CalDAV client
- Add fetch_event_by_href helper function
- Update DeleteEventRequest model with delete_action and occurrence_date fields
- Implement proper delete_this logic with EXDATE addition
- Implement delete_following logic with RRULE UNTIL modification
- Add comprehensive logging for delete operations

CalDAV Integration:
- Proper EXDATE generation in iCal format for excluded occurrences
- RRULE modification with UNTIL clause for partial series deletion
- Event updating via CalDAV PUT operations
- Full iCal RFC 5545 compliance for recurring event modifications

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-08-29 09:18:35 -04:00
parent e1578ed11c
commit 1b57adab98
7 changed files with 440 additions and 40 deletions

View File

@@ -2,7 +2,7 @@ use yew::prelude::*;
use yew_router::prelude::*;
use gloo_storage::{LocalStorage, Storage};
use web_sys::MouseEvent;
use crate::components::{Sidebar, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType};
use crate::components::{Sidebar, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType, DeleteAction};
use crate::services::{CalendarService, calendar_service::{UserInfo, CalendarEvent}};
use chrono::NaiveDate;
@@ -475,6 +475,7 @@ pub fn App() -> Html {
is_open={*event_context_menu_open}
x={event_context_menu_pos.0}
y={event_context_menu_pos.1}
event={(*event_context_menu_event).clone()}
on_close={Callback::from({
let event_context_menu_open = event_context_menu_open.clone();
move |_| event_context_menu_open.set(false)
@@ -484,11 +485,18 @@ pub fn App() -> Html {
let event_context_menu_event = event_context_menu_event.clone();
let event_context_menu_open = event_context_menu_open.clone();
let refresh_calendars = refresh_calendars.clone();
move |_: MouseEvent| {
move |delete_action: DeleteAction| {
if let (Some(token), Some(event)) = ((*auth_token).clone(), (*event_context_menu_event).clone()) {
let _refresh_calendars = refresh_calendars.clone();
let event_context_menu_open = event_context_menu_open.clone();
// Log the delete action for now - we'll implement different behaviors later
match delete_action {
DeleteAction::DeleteThis => web_sys::console::log_1(&"Delete this event".into()),
DeleteAction::DeleteFollowing => web_sys::console::log_1(&"Delete following events".into()),
DeleteAction::DeleteSeries => web_sys::console::log_1(&"Delete entire series".into()),
}
wasm_bindgen_futures::spawn_local(async move {
let calendar_service = CalendarService::new();
@@ -503,9 +511,37 @@ pub fn App() -> Html {
};
if let (Some(calendar_path), Some(event_href)) = (&event.calendar_path, &event.href) {
match calendar_service.delete_event(&token, &password, calendar_path.clone(), event_href.clone()).await {
Ok(_) => {
web_sys::console::log_1(&"Event deleted successfully!".into());
// Convert delete action to string and get occurrence date
let action_str = match delete_action {
DeleteAction::DeleteThis => "delete_this".to_string(),
DeleteAction::DeleteFollowing => "delete_following".to_string(),
DeleteAction::DeleteSeries => "delete_series".to_string(),
};
// Get the occurrence date from the clicked event
let occurrence_date = Some(event.start.date_naive().format("%Y-%m-%d").to_string());
web_sys::console::log_1(&format!("🔄 Delete action: {}", action_str).into());
web_sys::console::log_1(&format!("🔄 Event UID: {}", event.uid).into());
web_sys::console::log_1(&format!("🔄 Event start: {}", event.start).into());
web_sys::console::log_1(&format!("🔄 Occurrence date: {:?}", occurrence_date).into());
match calendar_service.delete_event(
&token,
&password,
calendar_path.clone(),
event_href.clone(),
action_str,
occurrence_date
).await {
Ok(message) => {
web_sys::console::log_1(&format!("Delete response: {}", message).into());
// Show the message to the user to explain what actually happened
if message.contains("Warning") {
web_sys::window().unwrap().alert_with_message(&message).unwrap();
}
// Close the context menu
event_context_menu_open.set(false);
// Force a page reload to refresh the calendar events
@@ -513,6 +549,7 @@ pub fn App() -> Html {
}
Err(err) => {
web_sys::console::log_1(&format!("Failed to delete event: {}", err).into());
web_sys::window().unwrap().alert_with_message(&format!("Failed to delete event: {}", err)).unwrap();
}
}
} else {