Fix recurring event series modification and UI issues
Backend fixes: - Fix "this event only" EXDATE handling - ensure proper timezone conversion for exception dates - Remove debug logging for cleaner production output Frontend fixes: - Add EXDATE timezone conversion in convert_utc_to_local function - Fix event duplication when viewing weeks across month boundaries with deduplication logic - Update CSS theme colors for context menus, recurrence options, and recurring edit modals These changes ensure RFC 5545 compliance for recurring event exceptions and improve the user experience across different themes and calendar views. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -465,13 +465,10 @@ pub async fn update_event_series(
|
||||
};
|
||||
|
||||
// Update the event on the CalDAV server using the original event's href
|
||||
println!("📤 Updating event on CalDAV server...");
|
||||
let event_href = existing_event
|
||||
.href
|
||||
.as_ref()
|
||||
.ok_or_else(|| ApiError::Internal("Event missing href for update".to_string()))?;
|
||||
println!("📤 Using event href: {}", event_href);
|
||||
println!("📤 Calendar path: {}", calendar_path);
|
||||
|
||||
match client
|
||||
.update_event(&calendar_path, &updated_event, event_href)
|
||||
@@ -1026,7 +1023,7 @@ async fn update_single_occurrence(
|
||||
|
||||
println!("✅ Created exception event successfully");
|
||||
|
||||
// Return the original series (now with EXDATE) - main handler will update it on CalDAV
|
||||
// Return the modified existing event with EXDATE for the main handler to update on CalDAV
|
||||
Ok((existing_event.clone(), 1)) // 1 occurrence modified (via exception)
|
||||
}
|
||||
|
||||
|
||||
@@ -181,6 +181,20 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate events that may appear in multiple month fetches
|
||||
// This happens when a recurring event spans across month boundaries
|
||||
all_events.sort_by(|a, b| {
|
||||
// Sort by UID first, then by start time
|
||||
match a.uid.cmp(&b.uid) {
|
||||
std::cmp::Ordering::Equal => a.dtstart.cmp(&b.dtstart),
|
||||
other => other,
|
||||
}
|
||||
});
|
||||
all_events.dedup_by(|a, b| {
|
||||
// Remove duplicates with same UID and start time
|
||||
a.uid == b.uid && a.dtstart == b.dtstart
|
||||
});
|
||||
|
||||
// Process the combined events
|
||||
match Ok(all_events) as Result<Vec<VEvent>, String>
|
||||
|
||||
@@ -317,6 +317,11 @@ impl CalendarService {
|
||||
event.last_modified = Some(modified_utc + chrono::Duration::minutes(-timezone_offset_minutes as i64));
|
||||
event.last_modified_tzid = None;
|
||||
}
|
||||
|
||||
// Convert EXDATE entries from UTC to local time
|
||||
event.exdate = event.exdate.into_iter()
|
||||
.map(|exdate| exdate + chrono::Duration::minutes(-timezone_offset_minutes as i64))
|
||||
.collect();
|
||||
}
|
||||
|
||||
event
|
||||
@@ -333,8 +338,6 @@ impl CalendarService {
|
||||
// Convert UTC events to local time for proper display
|
||||
let event = Self::convert_utc_to_local(event);
|
||||
if let Some(ref rrule) = event.rrule {
|
||||
|
||||
|
||||
// Generate occurrences for recurring events using VEvent
|
||||
let occurrences = Self::generate_occurrences(&event, rrule, start_range, end_range);
|
||||
expanded_events.extend(occurrences);
|
||||
@@ -437,25 +440,14 @@ impl CalendarService {
|
||||
|
||||
// Check if this occurrence is in the exception dates (EXDATE)
|
||||
let is_exception = base_event.exdate.iter().any(|exception_date| {
|
||||
// Compare dates ignoring sub-second precision
|
||||
let exception_naive = exception_date.and_utc();
|
||||
let occurrence_naive = occurrence_datetime.and_utc();
|
||||
// EXDATE from server is in local time, but stored as NaiveDateTime
|
||||
// We need to compare both as local time (naive datetimes) instead of UTC
|
||||
let exception_naive = *exception_date;
|
||||
let occurrence_naive = occurrence_datetime;
|
||||
|
||||
// Check if dates match (within a minute to handle minor time differences)
|
||||
let diff = occurrence_naive - exception_naive;
|
||||
let matches = diff.num_seconds().abs() < 60;
|
||||
|
||||
if matches {
|
||||
web_sys::console::log_1(
|
||||
&format!(
|
||||
"🚫 Excluding occurrence {} due to EXDATE {}",
|
||||
occurrence_naive, exception_naive
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
matches
|
||||
diff.num_seconds().abs() < 60
|
||||
});
|
||||
|
||||
if !is_exception {
|
||||
@@ -632,22 +624,11 @@ impl CalendarService {
|
||||
|
||||
// Check if this occurrence is in the exception dates (EXDATE)
|
||||
let is_exception = base_event.exdate.iter().any(|exception_date| {
|
||||
let exception_naive = exception_date.and_utc();
|
||||
let occurrence_naive = occurrence_datetime.and_utc();
|
||||
// Compare as local time (naive datetimes) instead of UTC
|
||||
let exception_naive = *exception_date;
|
||||
let occurrence_naive = occurrence_datetime;
|
||||
let diff = occurrence_naive - exception_naive;
|
||||
let matches = diff.num_seconds().abs() < 60;
|
||||
|
||||
if matches {
|
||||
web_sys::console::log_1(
|
||||
&format!(
|
||||
"🚫 Excluding occurrence {} due to EXDATE {}",
|
||||
occurrence_naive, exception_naive
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
matches
|
||||
diff.num_seconds().abs() < 60
|
||||
});
|
||||
|
||||
if !is_exception {
|
||||
|
||||
@@ -2586,7 +2586,7 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1.25rem;
|
||||
color: #374151;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 0.875rem;
|
||||
@@ -2607,8 +2607,8 @@ body {
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
color: #1f2937;
|
||||
background: var(--background-tertiary);
|
||||
color: var(--text-primary);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
@@ -2834,12 +2834,12 @@ body {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #333;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.recurring-option .option-description {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@@ -4487,8 +4487,8 @@ body {
|
||||
.recurrence-options {
|
||||
margin-top: 1.5rem;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user