Compare commits
	
		
			3 Commits
		
	
	
		
			74d636117d
			...
			feature/mo
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 51d5552156 | ||
|   | 5a12c0e0d0 | ||
|   | ee181cf6cb | 
							
								
								
									
										34
									
								
								.gitea/workflows/docker.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								.gitea/workflows/docker.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| name: Build and Push Docker Image | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|  | ||||
| jobs: | ||||
|   docker: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout code | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|  | ||||
|       - name: Login to Docker Registry | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           registry: ${{ secrets.DOCKER_REGISTRY }} | ||||
|           username: ${{ secrets.DOCKER_USERNAME }} | ||||
|           password: ${{ secrets.DOCKER_PASSWORD }} | ||||
|  | ||||
|       - name: Build and push | ||||
|         uses: docker/build-push-action@v5 | ||||
|         with: | ||||
|           context: . | ||||
|           push: true | ||||
|           tags: | | ||||
|             ${{ secrets.DOCKER_REGISTRY }}/calendar:latest | ||||
|             ${{ secrets.DOCKER_REGISTRY }}/calendar:${{ github.sha }} | ||||
|           cache-from: type=gha | ||||
|           cache-to: type=gha,mode=max | ||||
| @@ -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<String>, // ISO date string for specific occurrence being updated | ||||
|     pub changed_fields: Option<Vec<String>>, // List of field names that were changed (for optimization) | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize)] | ||||
|   | ||||
| @@ -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<NaiveDate> { None }); | ||||
|     let create_event_modal_open = use_state(|| false); | ||||
|     let selected_date_for_event = use_state(|| -> Option<NaiveDate> { None }); | ||||
|     let event_edit_scope = use_state(|| -> Option<EditAction> { None }); | ||||
|     let _recurring_edit_modal_open = use_state(|| false); | ||||
|     let _recurring_edit_event = use_state(|| -> Option<VEvent> { None }); | ||||
|     let _recurring_edit_data = use_state(|| -> Option<EventCreationData> { 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(); | ||||
|                                             } | ||||
|                                         } | ||||
|                                         } | ||||
|                                     } | ||||
|                                 }); | ||||
|                             } | ||||
|   | ||||
| @@ -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); | ||||
|             } | ||||
|         }) | ||||
|   | ||||
| @@ -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<VEvent>, | ||||
|     pub on_edit: Callback<()>, | ||||
|     pub on_edit: Callback<EditAction>, | ||||
|     pub on_delete: Callback<DeleteAction>, | ||||
|     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} | ||||
|         > | ||||
|             <div class="context-menu-item" onclick={on_edit_click}> | ||||
|                 {"Edit Event"} | ||||
|             </div> | ||||
|             { | ||||
|                 if is_recurring { | ||||
|                     html! { | ||||
|                         <> | ||||
|                             <div class="context-menu-item" onclick={create_edit_callback(EditAction::EditThis)}> | ||||
|                                 {"Edit This Event"} | ||||
|                             </div> | ||||
|                             <div class="context-menu-item" onclick={create_edit_callback(EditAction::EditFuture)}> | ||||
|                                 {"Edit This and Future Events"} | ||||
|                             </div> | ||||
|                             <div class="context-menu-item" onclick={create_edit_callback(EditAction::EditAll)}> | ||||
|                                 {"Edit All Events in Series"} | ||||
|                             </div> | ||||
|                         </> | ||||
|                     } | ||||
|                 } else { | ||||
|                     html! { | ||||
|                         <div class="context-menu-item" onclick={create_edit_callback(EditAction::EditThis)}> | ||||
|                             {"Edit Event"} | ||||
|                         </div> | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             { | ||||
|                 if is_recurring { | ||||
|                     html! { | ||||
|   | ||||
| @@ -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}; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user