Fix single event deletion functionality with proper recurring vs non-recurring handling

This commit resolves multiple issues with event deletion:

Backend fixes:
- Fix CalDAV URL construction for DELETE requests (missing slash separator)
- Improve event lookup by href with exact matching and fallback to UID extraction
- Add support for both RFC3339 and simple YYYY-MM-DD date formats in occurrence parsing
- Implement proper logic to distinguish recurring vs non-recurring events in delete_this action
- For non-recurring events: delete entire event from CalDAV server
- For recurring events: add EXDATE to exclude specific occurrences
- Add comprehensive debug logging for troubleshooting deletion issues

Frontend fixes:
- Update callback signatures to support series endpoint parameters (7-parameter tuples)
- Add update_series method to CalendarService for series-specific operations
- Route single occurrence modifications through series endpoint with proper scoping
- Fix all component prop definitions to use new callback signature
- Update all emit calls to pass correct number of parameters

The deletion process now works correctly:
- Single events are completely removed from the calendar
- Recurring event occurrences are properly excluded via EXDATE
- Debug logging helps identify and resolve CalDAV communication issues

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-08-30 18:40:48 -04:00
parent a6aac42c78
commit ee1c6ee299
7 changed files with 267 additions and 93 deletions

View File

@@ -1015,7 +1015,7 @@ impl CalDAVClient {
} else {
calendar_path
};
format!("{}/dav.php{}{}", self.config.server_url.trim_end_matches('/'), clean_path, event_href)
format!("{}/dav.php{}/{}", self.config.server_url.trim_end_matches('/'), clean_path, event_href)
};
println!("Deleting event at: {}", full_url);

View File

@@ -106,14 +106,34 @@ async fn fetch_event_by_href(client: &CalDAVClient, calendar_path: &str, event_h
// For now, we'll fetch all events and find the matching one by href (inefficient but functional)
let events = client.fetch_events(calendar_path).await?;
// Try to match by UID extracted from href
let uid_from_href = event_href.trim_end_matches(".ics");
println!("🔍 fetch_event_by_href: looking for href='{}'", event_href);
println!("🔍 Available events with hrefs: {:?}", events.iter().map(|e| (&e.uid, &e.href)).collect::<Vec<_>>());
// First try to match by exact href
for event in &events {
if let Some(stored_href) = &event.href {
if stored_href == event_href {
println!("✅ Found matching event by exact href: {}", event.uid);
return Ok(Some(event.clone()));
}
}
}
// Fallback: try to match by UID extracted from href filename
let filename = event_href.split('/').last().unwrap_or(event_href);
let uid_from_href = filename.trim_end_matches(".ics");
println!("🔍 Fallback: trying UID match. filename='{}', uid='{}'", filename, uid_from_href);
for event in events {
if event.uid == uid_from_href {
println!("✅ Found matching event by UID: {}", event.uid);
return Ok(Some(event));
}
}
println!("❌ No matching event found for href: {}", event_href);
Ok(None)
}
@@ -132,34 +152,56 @@ pub async fn delete_event(
// Handle different delete actions for recurring events
match request.delete_action.as_str() {
"delete_this" => {
// For single occurrence deletion, we need to:
// 1. Fetch the recurring event
// 2. Add an EXDATE for this occurrence
// 3. Update the event
if let Some(mut event) = fetch_event_by_href(&client, &request.calendar_path, &request.event_href).await
if let Some(event) = fetch_event_by_href(&client, &request.calendar_path, &request.event_href).await
.map_err(|e| ApiError::Internal(format!("Failed to fetch event: {}", e)))? {
if let Some(occurrence_date) = &request.occurrence_date {
// Parse the occurrence date and add it to EXDATE
if let Ok(date) = chrono::DateTime::parse_from_rfc3339(occurrence_date) {
let exception_utc = date.with_timezone(&chrono::Utc);
event.exdate.push(exception_utc);
// Check if this is a recurring event
if event.rrule.is_some() && !event.rrule.as_ref().unwrap().is_empty() {
// Recurring event - add EXDATE for this occurrence
if let Some(occurrence_date) = &request.occurrence_date {
let exception_utc = if let Ok(date) = chrono::DateTime::parse_from_rfc3339(occurrence_date) {
// RFC3339 format (with time and timezone)
date.with_timezone(&chrono::Utc)
} else if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d") {
// Simple date format (YYYY-MM-DD)
naive_date.and_hms_opt(0, 0, 0).unwrap().and_utc()
} else {
return Err(ApiError::BadRequest(format!("Invalid occurrence date format: {}. Expected RFC3339 or YYYY-MM-DD", occurrence_date)));
};
let mut updated_event = event;
updated_event.exdate.push(exception_utc);
println!("🔄 Adding EXDATE {} to recurring event {}", exception_utc.format("%Y%m%dT%H%M%SZ"), updated_event.uid);
// Update the event with the new EXDATE
client.update_event(&request.calendar_path, &event, &request.event_href)
client.update_event(&request.calendar_path, &updated_event, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to update event with EXDATE: {}", e)))?;
println!("✅ Successfully updated recurring event with EXDATE");
Ok(Json(DeleteEventResponse {
success: true,
message: "Single occurrence deleted successfully".to_string(),
}))
} else {
Err(ApiError::BadRequest("Invalid occurrence date format".to_string()))
Err(ApiError::BadRequest("Occurrence date is required for single occurrence deletion of recurring events".to_string()))
}
} else {
Err(ApiError::BadRequest("Occurrence date is required for single occurrence deletion".to_string()))
// Non-recurring event - delete the entire event
println!("🗑️ Deleting non-recurring event: {}", event.uid);
client.delete_event(&request.calendar_path, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?;
println!("✅ Successfully deleted non-recurring event");
Ok(Json(DeleteEventResponse {
success: true,
message: "Event deleted successfully".to_string(),
}))
}
} else {
Err(ApiError::NotFound("Event not found".to_string()))
@@ -175,41 +217,45 @@ pub async fn delete_event(
.map_err(|e| ApiError::Internal(format!("Failed to fetch event: {}", e)))? {
if let Some(occurrence_date) = &request.occurrence_date {
if let Ok(date) = chrono::DateTime::parse_from_rfc3339(occurrence_date) {
let until_date = date.with_timezone(&chrono::Utc);
// Modify the RRULE to add an UNTIL clause
if let Some(rrule) = &event.rrule {
// Remove existing UNTIL if present and add new one
let parts: Vec<&str> = rrule.split(';').filter(|part| {
!part.starts_with("UNTIL=") && !part.starts_with("COUNT=")
}).collect();
let new_rrule = format!("{};UNTIL={}", parts.join(";"), until_date.format("%Y%m%dT%H%M%SZ"));
event.rrule = Some(new_rrule);
// Update the event with the modified RRULE
client.update_event(&request.calendar_path, &event, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to update event with modified RRULE: {}", e)))?;
Ok(Json(DeleteEventResponse {
success: true,
message: "This and following occurrences deleted successfully".to_string(),
}))
} else {
// No RRULE, just delete the single event
client.delete_event(&request.calendar_path, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?;
Ok(Json(DeleteEventResponse {
success: true,
message: "Event deleted successfully".to_string(),
}))
}
let until_date = if let Ok(date) = chrono::DateTime::parse_from_rfc3339(occurrence_date) {
// RFC3339 format (with time and timezone)
date.with_timezone(&chrono::Utc)
} else if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d") {
// Simple date format (YYYY-MM-DD)
naive_date.and_hms_opt(0, 0, 0).unwrap().and_utc()
} else {
Err(ApiError::BadRequest("Invalid occurrence date format".to_string()))
return Err(ApiError::BadRequest(format!("Invalid occurrence date format: {}. Expected RFC3339 or YYYY-MM-DD", occurrence_date)));
};
// Modify the RRULE to add an UNTIL clause
if let Some(rrule) = &event.rrule {
// Remove existing UNTIL if present and add new one
let parts: Vec<&str> = rrule.split(';').filter(|part| {
!part.starts_with("UNTIL=") && !part.starts_with("COUNT=")
}).collect();
let new_rrule = format!("{};UNTIL={}", parts.join(";"), until_date.format("%Y%m%dT%H%M%SZ"));
event.rrule = Some(new_rrule);
// Update the event with the modified RRULE
client.update_event(&request.calendar_path, &event, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to update event with modified RRULE: {}", e)))?;
Ok(Json(DeleteEventResponse {
success: true,
message: "This and following occurrences deleted successfully".to_string(),
}))
} else {
// No RRULE, just delete the single event
client.delete_event(&request.calendar_path, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?;
Ok(Json(DeleteEventResponse {
success: true,
message: "Event deleted successfully".to_string(),
}))
}
} else {
Err(ApiError::BadRequest("Occurrence date is required for following deletion".to_string()))