Fix this_only concurrent request cancellation issue

Resolves the same "Failed to fetch" cancellation issue that was occurring
in the "modify single event in a series" flow by eliminating concurrent
HTTP requests from the frontend.

## Problem:
The RecurringEditAction::ThisEvent handler was making two concurrent requests:
1. UPDATE request via update_callback.emit()
2. CREATE request via create_callback.emit()

This caused the same race condition and HTTP cancellation (~700-900ms) that
we previously fixed in the "this_and_future" flow.

## Solution:
- **Remove concurrent CREATE request** from frontend
- **Use single UPDATE request** with "this_only" scope
- **Backend handles both operations** atomically:
  1. Add EXDATE to original series (exclude occurrence)
  2. Create exception event with RECURRENCE-ID (user modifications)

## Implementation:
- Frontend sends single request with occurrence_date and dragged times
- Backend update_single_occurrence() already handled both operations
- Added comprehensive RFC 5545 documentation for single occurrence modification
- Cleaned up unused imports and variables

## Benefits:
- No more HTTP request cancellation for single event modifications
- Proper RFC 5545 EXDATE + RECURRENCE-ID exception handling
- Atomic operations ensure data consistency
- Matches the pattern used in this_and_future fix

The "modify single event" drag operations now work reliably without
network errors, completing the fix for all recurring event modification flows.

🤖 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 20:29:02 -04:00
parent 9536158f58
commit b9e8778f8f

View File

@@ -4,7 +4,7 @@ use std::collections::HashMap;
use web_sys::MouseEvent;
use crate::services::calendar_service::UserInfo;
use crate::models::ical::VEvent;
use crate::components::{RecurringEditModal, RecurringEditAction, EventCreationData, EventStatus, EventClass, ReminderType, RecurrenceType};
use crate::components::{RecurringEditModal, RecurringEditAction, EventCreationData};
#[derive(Properties, PartialEq)]
pub struct WeekViewProps {
@@ -106,57 +106,49 @@ pub fn week_view(props: &WeekViewProps) -> Html {
let pending_recurring_edit = pending_recurring_edit.clone();
let on_event_update = props.on_event_update.clone();
let _on_create_event = props.on_create_event.clone();
let on_create_event_request = props.on_create_event_request.clone();
let events = props.events.clone();
Callback::from(move |action: RecurringEditAction| {
if let Some(edit) = (*pending_recurring_edit).clone() {
match action {
RecurringEditAction::ThisEvent => {
// Use the series endpoint with "this_only" scope for RFC 5545 compliant single occurrence modification
web_sys::console::log_1(&format!("🎯 Single occurrence modification: calling series update with this_only scope for event '{}'",
edit.event.summary.as_deref().unwrap_or("Untitled")
).into());
// TODO: Need to call calendar service directly with update_scope "this_only" and occurrence_date
// For now, fall back to the old method but with better logging
// RFC 5545 Compliant Single Occurrence Modification: "This Event Only"
//
// When a user chooses to modify "this event only" for a recurring series,
// we implement an exception-based modification that:
//
// 1. **Add EXDATE to Original Series**: The original series is updated with
// an EXDATE entry to exclude this specific occurrence from generation
// 2. **Create Exception Event**: A new standalone event is created with
// RECURRENCE-ID pointing to the original occurrence, containing the modifications
//
// Example: User drags Aug 22 occurrence of "Daily 9AM meeting" to 2PM:
// - Original series: "Daily 9AM meeting" + EXDATE for Aug 22 (continues as normal except Aug 22)
// - Exception event: "Daily 2PM meeting" with RECURRENCE-ID=Aug22 (only affects Aug 22)
//
// This approach ensures:
// - All other occurrences remain unchanged (past and future)
// - Modified occurrence displays user's changes
// - RFC 5545 compliance through EXDATE and RECURRENCE-ID
// - CalDAV compatibility with standard calendar applications
//
// The backend handles both operations atomically within a single API call.
if let Some(update_callback) = &on_event_update {
// This currently goes to regular update endpoint, but we need it to go to series endpoint
// with update_scope: "this_only" and occurrence_date: edit.event.dtstart.format("%Y-%m-%d")
let updated_event = edit.event.clone();
// Extract occurrence date for backend processing
let occurrence_date = edit.event.dtstart.format("%Y-%m-%d").to_string();
web_sys::console::log_1(&format!("⚠️ Using regular update callback - this should be changed to use series endpoint with this_only scope").into());
update_callback.emit((updated_event, edit.new_start, edit.new_end, true, None, Some("this_only".to_string()), Some(edit.event.dtstart.format("%Y-%m-%d").to_string()))); // preserve_rrule = true, update_scope = this_only
}
// Note: The proper fix requires calling calendar_service.update_event_with_scope() directly
// with update_scope: "this_only" and occurrence_date
if let Some(create_callback) = &on_create_event_request {
// Convert to EventCreationData for single event
let event_data = EventCreationData {
title: edit.event.summary.clone().unwrap_or_default(),
description: edit.event.description.clone().unwrap_or_default(),
start_date: edit.new_start.date(),
start_time: edit.new_start.time(),
end_date: edit.new_end.date(),
end_time: edit.new_end.time(),
location: edit.event.location.clone().unwrap_or_default(),
all_day: edit.event.all_day,
status: EventStatus::Confirmed,
class: EventClass::Public,
priority: edit.event.priority,
organizer: edit.event.organizer.as_ref().map(|o| o.cal_address.clone()).unwrap_or_default(),
attendees: edit.event.attendees.iter().map(|a| a.cal_address.clone()).collect::<Vec<_>>().join(","),
categories: edit.event.categories.join(","),
reminder: ReminderType::None,
recurrence: RecurrenceType::None, // Single event, no recurrence
recurrence_days: vec![false; 7],
selected_calendar: edit.event.calendar_path.clone(),
};
// Create the single event
create_callback.emit(event_data);
// Send single request to backend with "this_only" scope
// Backend will atomically:
// 1. Add EXDATE to original series (excludes this occurrence)
// 2. Create exception event with RECURRENCE-ID and user's modifications
update_callback.emit((
edit.event.clone(), // Original event (series to modify)
edit.new_start, // Dragged start time for exception
edit.new_end, // Dragged end time for exception
true, // preserve_rrule = true
None, // No until_date for this_only
Some("this_only".to_string()), // Update scope
Some(occurrence_date) // Date of occurrence being modified
));
}
},
RecurringEditAction::FutureEvents => {