diff --git a/backend/src/models.rs b/backend/src/models.rs index 532a92f..853a352 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -201,6 +201,7 @@ pub struct UpdateEventSeriesRequest { // Update scope control pub update_scope: String, // "this_only", "this_and_future", "all_in_series" pub occurrence_date: Option, // ISO date string for specific occurrence being updated + pub changed_fields: Option>, // List of field names that were changed (for optimization) } #[derive(Debug, Serialize)] diff --git a/frontend/src/app.rs b/frontend/src/app.rs index ae9c34d..95da19b 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -2,7 +2,7 @@ use yew::prelude::*; use yew_router::prelude::*; use gloo_storage::{LocalStorage, Storage}; use web_sys::MouseEvent; -use crate::components::{Sidebar, ViewMode, Theme, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType, DeleteAction}; +use crate::components::{Sidebar, ViewMode, Theme, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType, DeleteAction, EditAction}; use crate::services::{CalendarService, calendar_service::UserInfo}; use crate::models::ical::VEvent; use chrono::NaiveDate; @@ -54,6 +54,7 @@ pub fn App() -> Html { let calendar_context_menu_date = use_state(|| -> Option { None }); let create_event_modal_open = use_state(|| false); let selected_date_for_event = use_state(|| -> Option { None }); + let event_edit_scope = use_state(|| -> Option { None }); let _recurring_edit_modal_open = use_state(|| false); let _recurring_edit_event = use_state(|| -> Option { None }); let _recurring_edit_data = use_state(|| -> Option { None }); @@ -738,8 +739,10 @@ pub fn App() -> Html { let _event_context_menu_event = event_context_menu_event.clone(); let event_context_menu_open = event_context_menu_open.clone(); let create_event_modal_open = create_event_modal_open.clone(); - move |_| { - // Close the context menu and open the edit modal + let event_edit_scope = event_edit_scope.clone(); + move |edit_action: EditAction| { + // Set the edit scope and close the context menu + event_edit_scope.set(Some(edit_action)); event_context_menu_open.set(false); create_event_modal_open.set(true); } @@ -840,13 +843,16 @@ pub fn App() -> Html { is_open={*create_event_modal_open} selected_date={(*selected_date_for_event).clone()} event_to_edit={(*event_context_menu_event).clone()} + edit_scope={(*event_edit_scope).clone()} on_close={Callback::from({ let create_event_modal_open = create_event_modal_open.clone(); let event_context_menu_event = event_context_menu_event.clone(); + let event_edit_scope = event_edit_scope.clone(); move |_| { create_event_modal_open.set(false); - // Clear the event being edited + // Clear the event being edited and edit scope event_context_menu_event.set(None); + event_edit_scope.set(None); } })} on_create={on_event_create} @@ -854,10 +860,12 @@ pub fn App() -> Html { let auth_token = auth_token.clone(); let create_event_modal_open = create_event_modal_open.clone(); let event_context_menu_event = event_context_menu_event.clone(); + let event_edit_scope = event_edit_scope.clone(); move |(original_event, updated_data): (VEvent, EventCreationData)| { - web_sys::console::log_1(&format!("Updating event: {:?}", updated_data).into()); + web_sys::console::log_1(&format!("Updating event: {:?}, edit_scope: {:?}", updated_data, updated_data.edit_scope).into()); create_event_modal_open.set(false); event_context_menu_event.set(None); + event_edit_scope.set(None); if let Some(token) = (*auth_token).clone() { wasm_bindgen_futures::spawn_local(async move { @@ -988,8 +996,61 @@ pub fn App() -> Html { web_sys::window().unwrap().alert_with_message("Cannot move event - original event missing calendar path").unwrap(); } } else { - // Calendar hasn't changed - normal update - match calendar_service.update_event( + // Calendar hasn't changed - check if we should use series endpoint + let use_series_endpoint = updated_data.edit_scope.is_some() && original_event.rrule.is_some(); + + if use_series_endpoint { + // Use series endpoint for recurring event modal edits + let update_scope = match updated_data.edit_scope.as_ref().unwrap() { + EditAction::EditThis => "this_only", + EditAction::EditFuture => "this_and_future", + EditAction::EditAll => "all_in_series", + }; + + // For single occurrence edits, we need the occurrence date + let occurrence_date = if update_scope == "this_only" || update_scope == "this_and_future" { + // Use the original event's start date as the occurrence date + Some(original_event.dtstart.format("%Y-%m-%d").to_string()) + } else { + None + }; + + match calendar_service.update_series( + &token, + &password, + original_event.uid, + updated_data.title, + updated_data.description, + start_date, + start_time, + end_date, + end_time, + updated_data.location, + updated_data.all_day, + status_str, + class_str, + updated_data.priority, + updated_data.organizer, + updated_data.attendees, + updated_data.categories, + reminder_str, + recurrence_str, + updated_data.selected_calendar, + update_scope.to_string(), + occurrence_date, + ).await { + Ok(_) => { + web_sys::console::log_1(&"Series updated successfully".into()); + web_sys::window().unwrap().location().reload().unwrap(); + } + Err(err) => { + web_sys::console::error_1(&format!("Failed to update series: {}", err).into()); + web_sys::window().unwrap().alert_with_message(&format!("Failed to update series: {}", err)).unwrap(); + } + } + } else { + // Use regular event endpoint for non-recurring events or legacy updates + match calendar_service.update_event( &token, &password, original_event.uid, @@ -1025,6 +1086,7 @@ pub fn App() -> Html { web_sys::window().unwrap().alert_with_message(&format!("Failed to update event: {}", err)).unwrap(); } } + } } }); } diff --git a/frontend/src/components/create_event_modal.rs b/frontend/src/components/create_event_modal.rs index 3b04ac7..b676d2e 100644 --- a/frontend/src/components/create_event_modal.rs +++ b/frontend/src/components/create_event_modal.rs @@ -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, #[prop_or_default] pub initial_end_time: Option, + #[prop_or_default] + pub edit_scope: Option, } #[derive(Clone, PartialEq, Debug)] @@ -330,6 +333,10 @@ pub struct EventCreationData { pub monthly_by_day: Option, // For monthly: "1MO" = first Monday, "2TU" = second Tuesday, etc. pub monthly_by_monthday: Option, // For monthly: day of month (1-31) pub yearly_by_month: Vec, // For yearly: [Jan, Feb, Mar, ..., Dec] + + // Edit scope and tracking fields + pub edit_scope: Option, + pub changed_fields: Vec, // 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::() { 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::() { 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); } }) diff --git a/frontend/src/components/event_context_menu.rs b/frontend/src/components/event_context_menu.rs index ccb5ac4..161f91a 100644 --- a/frontend/src/components/event_context_menu.rs +++ b/frontend/src/components/event_context_menu.rs @@ -9,13 +9,20 @@ pub enum DeleteAction { DeleteSeries, } +#[derive(Clone, PartialEq, Debug)] +pub enum EditAction { + EditThis, + EditFuture, + EditAll, +} + #[derive(Properties, PartialEq)] pub struct EventContextMenuProps { pub is_open: bool, pub x: i32, pub y: i32, pub event: Option, - pub on_edit: Callback<()>, + pub on_edit: Callback, pub on_delete: Callback, pub on_close: Callback<()>, } @@ -38,11 +45,11 @@ pub fn event_context_menu(props: &EventContextMenuProps) -> Html { .map(|event| event.rrule.is_some()) .unwrap_or(false); - let on_edit_click = { + let create_edit_callback = |action: EditAction| { let on_edit = props.on_edit.clone(); let on_close = props.on_close.clone(); Callback::from(move |_: MouseEvent| { - on_edit.emit(()); + on_edit.emit(action.clone()); on_close.emit(()); }) }; @@ -62,9 +69,29 @@ pub fn event_context_menu(props: &EventContextMenuProps) -> Html { class="context-menu" style={style} > -
- {"Edit Event"} -
+ { + if is_recurring { + html! { + <> +
+ {"Edit This Event"} +
+
+ {"Edit This and Future Events"} +
+
+ {"Edit All Events in Series"} +
+ + } + } else { + html! { +
+ {"Edit Event"} +
+ } + } + } { if is_recurring { html! { diff --git a/frontend/src/components/mod.rs b/frontend/src/components/mod.rs index fdf1155..3e01c0d 100644 --- a/frontend/src/components/mod.rs +++ b/frontend/src/components/mod.rs @@ -22,7 +22,7 @@ pub use week_view::WeekView; pub use event_modal::EventModal; pub use create_calendar_modal::CreateCalendarModal; pub use context_menu::ContextMenu; -pub use event_context_menu::{EventContextMenu, DeleteAction}; +pub use event_context_menu::{EventContextMenu, DeleteAction, EditAction}; pub use calendar_context_menu::CalendarContextMenu; pub use create_event_modal::{CreateEventModal, EventCreationData, EventStatus, EventClass, ReminderType, RecurrenceType}; pub use sidebar::{Sidebar, ViewMode, Theme};