Fix singleton to series conversion with complete RRULE parameter support
All checks were successful
Build and Push Docker Image / docker (push) Successful in 2m16s

- Add new context menu callback for singleton events to avoid series pipeline
- Implement complete RRULE construction with INTERVAL, COUNT, and UNTIL parameters
- Update frontend service methods to pass recurrence parameters correctly
- Add missing recurrence fields to backend UpdateEventRequest model
- Fix parameter ordering in frontend method calls
- Ensure singleton→series conversion uses single event pipeline initially

This resolves issues where converting singleton events to recurring series
would not respect recurrence interval (every N days), count (N occurrences),
or until date parameters.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-09-13 22:28:52 -04:00
parent a6092d13ce
commit ac1164fd81
8 changed files with 183 additions and 44 deletions

View File

@@ -723,8 +723,12 @@ pub fn App() -> Html {
crate::components::event_form::RecurrenceType::Monthly |
crate::components::event_form::RecurrenceType::Yearly);
web_sys::console::log_1(&format!("🐛 FRONTEND DEBUG: is_recurring={}, edit_scope={:?}, original_uid={:?}",
is_recurring, event_data_for_update.edit_scope, event_data_for_update.original_uid).into());
let update_result = if is_recurring && event_data_for_update.edit_scope.is_some() {
// Use series update endpoint for recurring events
// Only use series endpoint for existing recurring events being edited
// Singleton→series conversion should use regular update_event endpoint
let edit_action = event_data_for_update.edit_scope.unwrap();
let scope = match edit_action {
crate::components::EditAction::EditAll => "all_in_series".to_string(),
@@ -754,12 +758,13 @@ pub fn App() -> Html {
params.14, // reminder
params.15, // recurrence
params.16, // recurrence_days
params.17, // recurrence_interval
params.18, // recurrence_count
params.19, // recurrence_until
params.17, // calendar_path
params.20, // calendar_path
scope,
event_data_for_update.occurrence_date.map(|d| d.format("%Y-%m-%d").to_string()), // occurrence_date
params.20, // timezone
params.21, // timezone
)
.await
} else {
@@ -786,11 +791,11 @@ pub fn App() -> Html {
params.14, // reminder
params.15, // recurrence
params.16, // recurrence_days
params.17, // calendar_path
vec![], // exception_dates - empty for simple updates
None, // update_action - None for regular updates
None, // until_date - None for regular updates
params.20, // timezone
params.17, // recurrence_interval
params.18, // recurrence_count
params.19, // recurrence_until
params.20, // calendar_path
params.21, // timezone
)
.await
};
@@ -874,10 +879,11 @@ pub fn App() -> Html {
params.14, // reminder
params.15, // recurrence
params.16, // recurrence_days
params.17, // recurrence_interval
params.18, // recurrence_count
params.19, // recurrence_until
params.17, // calendar_path
params.20, // timezone
params.20, // calendar_path
params.21, // timezone
)
.await;
match create_result {
@@ -1042,12 +1048,13 @@ pub fn App() -> Html {
original_event.categories.join(","),
reminder_str.clone(),
recurrence_str.clone(),
vec![false; 7],
None,
None,
original_event.calendar_path.clone(),
scope.clone(),
occurrence_date,
vec![false; 7], // recurrence_days
1, // recurrence_interval - default for drag-and-drop
None, // recurrence_count
None, // recurrence_until
original_event.calendar_path.clone(), // calendar_path
scope.clone(), // update_scope
occurrence_date, // occurrence_date
{
// Get timezone offset
let date = js_sys::Date::new_0();
@@ -1055,7 +1062,7 @@ pub fn App() -> Html {
let hours = -(timezone_offset as i32) / 60; // Convert to hours, negate for proper sign
let minutes = (timezone_offset as i32).abs() % 60;
format!("{:+03}:{:02}", hours, minutes) // Format as +05:00 or -04:00
},
}, // timezone
)
.await,
)
@@ -1099,14 +1106,10 @@ pub fn App() -> Html {
reminder_str,
recurrence_str,
recurrence_days,
1, // recurrence_interval - default to 1 for drag-and-drop
None, // recurrence_count - preserve existing
None, // recurrence_until - preserve existing
original_event.calendar_path,
original_event.exdate.clone(),
if preserve_rrule {
Some("update_series".to_string())
} else {
Some("this_and_future".to_string())
},
until_date,
{
// Get timezone offset
let date = js_sys::Date::new_0();
@@ -1639,6 +1642,19 @@ pub fn App() -> Html {
}
}
})}
on_edit_singleton={Callback::from({
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();
let event_edit_scope = event_edit_scope.clone();
move |event: VEvent| {
// For singleton events, open edit modal WITHOUT setting edit_scope
event_context_menu_event.set(Some(event));
event_edit_scope.set(None); // Explicitly set to None for singleton edits
event_context_menu_open.set(false);
create_event_modal_open.set(true);
}
})}
on_view_details={Callback::from({
let event_context_menu_open = event_context_menu_open.clone();
let view_event_modal_open = view_event_modal_open.clone();