Implement comprehensive event series editing via modal

## Frontend Changes:
- Add EditAction enum (EditThis, EditFuture, EditAll) to event context menu
- Update context menu to show 3 edit options for recurring events
- Enhance EventCreationData with edit_scope and changed_fields tracking
- Update app component to handle EditAction types and pass to modal
- Add field change tracking infrastructure to CreateEventModal

## Backend Changes:
- Add changed_fields parameter to UpdateEventSeriesRequest for optimization
- Existing series endpoint already supports the three update types:
  - "this_only" - creates exception with EXDATE
  - "this_and_future" - creates new series with UNTIL on original
  - "all_in_series" - updates existing series in-place

## Implementation Details:
- Event context menu shows single edit option for non-recurring events
- Recurring events get three options: "Edit This Event", "Edit This and Future Events", "Edit All Events in Series"
- Modal tracks which fields user actually changed for efficient updates
- Backend series endpoint already has the logic for all three update scenarios
- Full RFC 5545 compliance with proper EXDATE and UNTIL handling

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-08-31 18:49:56 -04:00
parent ee181cf6cb
commit 5a12c0e0d0
5 changed files with 91 additions and 16 deletions

View File

@@ -4,6 +4,7 @@ use wasm_bindgen::JsCast;
use chrono::{NaiveDate, NaiveTime, Local, TimeZone, Utc, Datelike};
use crate::services::calendar_service::CalendarInfo;
use crate::models::ical::VEvent;
use crate::components::EditAction;
#[derive(Properties, PartialEq)]
pub struct CreateEventModalProps {
@@ -18,6 +19,8 @@ pub struct CreateEventModalProps {
pub initial_start_time: Option<NaiveTime>,
#[prop_or_default]
pub initial_end_time: Option<NaiveTime>,
#[prop_or_default]
pub edit_scope: Option<EditAction>,
}
#[derive(Clone, PartialEq, Debug)]
@@ -330,6 +333,10 @@ pub struct EventCreationData {
pub monthly_by_day: Option<String>, // For monthly: "1MO" = first Monday, "2TU" = second Tuesday, etc.
pub monthly_by_monthday: Option<u8>, // For monthly: day of month (1-31)
pub yearly_by_month: Vec<bool>, // For yearly: [Jan, Feb, Mar, ..., Dec]
// Edit scope and tracking fields
pub edit_scope: Option<EditAction>,
pub changed_fields: Vec<String>, // List of field names that were changed
}
impl Default for EventCreationData {
@@ -365,6 +372,10 @@ impl Default for EventCreationData {
monthly_by_day: None,
monthly_by_monthday: None,
yearly_by_month: vec![false; 12], // [Jan, Feb, ..., Dec] - all false by default
// Edit scope and tracking defaults
edit_scope: None,
changed_fields: vec![],
}
}
}
@@ -566,6 +577,10 @@ impl EventCreationData {
} else {
vec![false; 12]
},
// Edit scope and tracking defaults (will be set later if needed)
edit_scope: None,
changed_fields: vec![],
}
}
@@ -593,9 +608,9 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html {
let active_tab = use_state(|| ModalTab::default());
// Initialize with selected date or event data if provided
use_effect_with((props.selected_date, props.event_to_edit.clone(), props.is_open, props.available_calendars.clone(), props.initial_start_time, props.initial_end_time), {
use_effect_with((props.selected_date, props.event_to_edit.clone(), props.is_open, props.available_calendars.clone(), props.initial_start_time, props.initial_end_time, props.edit_scope.clone()), {
let event_data = event_data.clone();
move |(selected_date, event_to_edit, is_open, available_calendars, initial_start_time, initial_end_time)| {
move |(selected_date, event_to_edit, is_open, available_calendars, initial_start_time, initial_end_time, edit_scope)| {
if *is_open {
let mut data = if let Some(event) = event_to_edit {
// Pre-populate with event data for editing
@@ -625,6 +640,11 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html {
data.selected_calendar = Some(available_calendars[0].path.clone());
}
// Set edit scope if provided
if let Some(scope) = edit_scope {
data.edit_scope = Some(scope.clone());
}
event_data.set(data);
}
|| ()
@@ -644,12 +664,25 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html {
})
};
// Helper function to track field changes
let _track_field_change = |data: &mut EventCreationData, field_name: &str| {
if !data.changed_fields.contains(&field_name.to_string()) {
data.changed_fields.push(field_name.to_string());
}
};
let on_title_input = {
let event_data = event_data.clone();
Callback::from(move |e: InputEvent| {
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
let mut data = (*event_data).clone();
data.title = input.value();
let new_value = input.value();
if data.title != new_value {
data.title = new_value;
if !data.changed_fields.contains(&"title".to_string()) {
data.changed_fields.push("title".to_string());
}
}
event_data.set(data);
}
})
@@ -661,7 +694,13 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html {
if let Some(select) = e.target_dyn_into::<HtmlSelectElement>() {
let mut data = (*event_data).clone();
let value = select.value();
data.selected_calendar = if value.is_empty() { None } else { Some(value) };
let new_calendar = if value.is_empty() { None } else { Some(value) };
if data.selected_calendar != new_calendar {
data.selected_calendar = new_calendar;
if !data.changed_fields.contains(&"selected_calendar".to_string()) {
data.changed_fields.push("selected_calendar".to_string());
}
}
event_data.set(data);
}
})