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:
@@ -50,6 +50,9 @@ pub struct CalendarEvent {
|
||||
/// Recurrence rule (RRULE)
|
||||
pub recurrence_rule: Option<String>,
|
||||
|
||||
/// Exception dates - dates to exclude from recurrence (EXDATE)
|
||||
pub exception_dates: Vec<DateTime<Utc>>,
|
||||
|
||||
/// All-day event flag
|
||||
pub all_day: bool,
|
||||
|
||||
@@ -361,6 +364,9 @@ impl CalDAVClient {
|
||||
let last_modified = properties.get("LAST-MODIFIED")
|
||||
.and_then(|s| self.parse_datetime(s, None).ok());
|
||||
|
||||
// Parse exception dates (EXDATE)
|
||||
let exception_dates = self.parse_exception_dates(&event);
|
||||
|
||||
Ok(CalendarEvent {
|
||||
uid,
|
||||
summary: properties.get("SUMMARY").cloned(),
|
||||
@@ -377,6 +383,7 @@ impl CalDAVClient {
|
||||
created,
|
||||
last_modified,
|
||||
recurrence_rule: properties.get("RRULE").cloned(),
|
||||
exception_dates,
|
||||
all_day,
|
||||
reminders: self.parse_alarms(&event)?,
|
||||
etag: None, // Set by caller
|
||||
@@ -591,6 +598,28 @@ impl CalDAVClient {
|
||||
Err(CalDAVError::ParseError(format!("Unable to parse datetime: {}", datetime_str)))
|
||||
}
|
||||
|
||||
/// Parse EXDATE properties from an iCal event
|
||||
fn parse_exception_dates(&self, event: &ical::parser::ical::component::IcalEvent) -> Vec<DateTime<Utc>> {
|
||||
let mut exception_dates = Vec::new();
|
||||
|
||||
// Look for EXDATE properties
|
||||
for property in &event.properties {
|
||||
if property.name.to_uppercase() == "EXDATE" {
|
||||
if let Some(value) = &property.value {
|
||||
// EXDATE can contain multiple comma-separated dates
|
||||
for date_str in value.split(',') {
|
||||
// Try to parse the date (the parse_datetime method will handle different formats)
|
||||
if let Ok(date) = self.parse_datetime(date_str.trim(), None) {
|
||||
exception_dates.push(date);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exception_dates
|
||||
}
|
||||
|
||||
/// Create a new calendar on the CalDAV server using MKCALENDAR
|
||||
pub async fn create_calendar(&self, name: &str, description: Option<&str>, color: Option<&str>) -> Result<(), CalDAVError> {
|
||||
// Sanitize calendar name for URL path
|
||||
@@ -758,6 +787,56 @@ impl CalDAVClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Update an existing event on the CalDAV server
|
||||
pub async fn update_event(&self, calendar_path: &str, event: &CalendarEvent, event_href: &str) -> Result<(), CalDAVError> {
|
||||
// Construct the full URL for the event
|
||||
let full_url = if event_href.starts_with("http") {
|
||||
event_href.to_string()
|
||||
} else if event_href.starts_with("/dav.php") {
|
||||
// Event href is already a full path, combine with base server URL (without /dav.php)
|
||||
let base_url = self.config.server_url.trim_end_matches('/').trim_end_matches("/dav.php");
|
||||
format!("{}{}", base_url, event_href)
|
||||
} else {
|
||||
// Event href is just a filename, combine with calendar path
|
||||
let clean_path = if calendar_path.starts_with("/dav.php") {
|
||||
calendar_path.trim_start_matches("/dav.php")
|
||||
} else {
|
||||
calendar_path
|
||||
};
|
||||
format!("{}/dav.php{}/{}", self.config.server_url.trim_end_matches('/'), clean_path, event_href)
|
||||
};
|
||||
|
||||
println!("📝 Updating event at: {}", full_url);
|
||||
|
||||
// Generate iCalendar data for the event
|
||||
let ical_data = self.generate_ical_event(event)?;
|
||||
|
||||
println!("📝 Updated iCal data: {}", ical_data);
|
||||
println!("📝 Event has {} exception dates", event.exception_dates.len());
|
||||
|
||||
let response = self.http_client
|
||||
.put(&full_url)
|
||||
.header("Authorization", format!("Basic {}", self.config.get_basic_auth()))
|
||||
.header("Content-Type", "text/calendar; charset=utf-8")
|
||||
.header("User-Agent", "calendar-app/0.1.0")
|
||||
.body(ical_data)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| CalDAVError::ParseError(e.to_string()))?;
|
||||
|
||||
println!("Event update response status: {}", response.status());
|
||||
|
||||
if response.status().is_success() || response.status().as_u16() == 201 || response.status().as_u16() == 204 {
|
||||
println!("✅ Event updated successfully");
|
||||
Ok(())
|
||||
} else {
|
||||
let status = response.status();
|
||||
let error_body = response.text().await.unwrap_or_default();
|
||||
println!("❌ Event update failed: {} - {}", status, error_body);
|
||||
Err(CalDAVError::ServerError(status.as_u16()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate iCalendar data for a CalendarEvent
|
||||
fn generate_ical_event(&self, event: &CalendarEvent) -> Result<String, CalDAVError> {
|
||||
let now = chrono::Utc::now();
|
||||
@@ -871,6 +950,15 @@ impl CalDAVClient {
|
||||
ical.push_str(&format!("RRULE:{}\r\n", rrule));
|
||||
}
|
||||
|
||||
// Exception dates (EXDATE)
|
||||
for exception_date in &event.exception_dates {
|
||||
if event.all_day {
|
||||
ical.push_str(&format!("EXDATE;VALUE=DATE:{}\r\n", format_date(exception_date)));
|
||||
} else {
|
||||
ical.push_str(&format!("EXDATE:{}\r\n", format_datetime(exception_date)));
|
||||
}
|
||||
}
|
||||
|
||||
ical.push_str("END:VEVENT\r\n");
|
||||
ical.push_str("END:VCALENDAR\r\n");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user