## 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>
277 lines
10 KiB
Rust
277 lines
10 KiB
Rust
use axum::{
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
Json,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
// API request/response types
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CalDAVLoginRequest {
|
|
pub username: String,
|
|
pub password: String,
|
|
pub server_url: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct AuthResponse {
|
|
pub token: String,
|
|
pub username: String,
|
|
pub server_url: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct UserInfo {
|
|
pub username: String,
|
|
pub server_url: String,
|
|
pub calendars: Vec<CalendarInfo>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct CalendarInfo {
|
|
pub path: String,
|
|
pub display_name: String,
|
|
pub color: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateCalendarRequest {
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub color: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct CreateCalendarResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DeleteCalendarRequest {
|
|
pub path: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DeleteCalendarResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DeleteEventRequest {
|
|
pub calendar_path: String,
|
|
pub event_href: String,
|
|
pub delete_action: String, // "delete_this", "delete_following", or "delete_series"
|
|
pub occurrence_date: Option<String>, // ISO date string for the specific occurrence
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DeleteEventResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateEventRequest {
|
|
pub title: String,
|
|
pub description: String,
|
|
pub start_date: String, // YYYY-MM-DD format
|
|
pub start_time: String, // HH:MM format
|
|
pub end_date: String, // YYYY-MM-DD format
|
|
pub end_time: String, // HH:MM format
|
|
pub location: String,
|
|
pub all_day: bool,
|
|
pub status: String, // confirmed, tentative, cancelled
|
|
pub class: String, // public, private, confidential
|
|
pub priority: Option<u8>, // 0-9 priority level
|
|
pub organizer: String, // organizer email
|
|
pub attendees: String, // comma-separated attendee emails
|
|
pub categories: String, // comma-separated categories
|
|
pub reminder: String, // reminder type
|
|
pub recurrence: String, // recurrence type
|
|
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
|
pub calendar_path: Option<String>, // Optional - use first calendar if not specified
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct CreateEventResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
pub event_href: Option<String>, // The created event's href/filename
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdateEventRequest {
|
|
pub uid: String, // Event UID to identify which event to update
|
|
pub title: String,
|
|
pub description: String,
|
|
pub start_date: String, // YYYY-MM-DD format
|
|
pub start_time: String, // HH:MM format
|
|
pub end_date: String, // YYYY-MM-DD format
|
|
pub end_time: String, // HH:MM format
|
|
pub location: String,
|
|
pub all_day: bool,
|
|
pub status: String, // confirmed, tentative, cancelled
|
|
pub class: String, // public, private, confidential
|
|
pub priority: Option<u8>, // 0-9 priority level
|
|
pub organizer: String, // organizer email
|
|
pub attendees: String, // comma-separated attendee emails
|
|
pub categories: String, // comma-separated categories
|
|
pub reminder: String, // reminder type
|
|
pub recurrence: String, // recurrence type
|
|
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
|
pub calendar_path: Option<String>, // Optional - search all calendars if not specified
|
|
pub update_action: Option<String>, // "update_series" for recurring events
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub until_date: Option<String>, // ISO datetime string for RRULE UNTIL clause
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct UpdateEventResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
}
|
|
|
|
// ==================== EVENT SERIES MODELS ====================
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateEventSeriesRequest {
|
|
pub title: String,
|
|
pub description: String,
|
|
pub start_date: String, // YYYY-MM-DD format
|
|
pub start_time: String, // HH:MM format
|
|
pub end_date: String, // YYYY-MM-DD format
|
|
pub end_time: String, // HH:MM format
|
|
pub location: String,
|
|
pub all_day: bool,
|
|
pub status: String, // confirmed, tentative, cancelled
|
|
pub class: String, // public, private, confidential
|
|
pub priority: Option<u8>, // 0-9 priority level
|
|
pub organizer: String, // organizer email
|
|
pub attendees: String, // comma-separated attendee emails
|
|
pub categories: String, // comma-separated categories
|
|
pub reminder: String, // reminder type
|
|
|
|
// Series-specific fields
|
|
pub recurrence: String, // recurrence type (daily, weekly, monthly, yearly)
|
|
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
|
pub recurrence_interval: Option<u32>, // Every N days/weeks/months/years
|
|
pub recurrence_end_date: Option<String>, // When the series ends (YYYY-MM-DD)
|
|
pub recurrence_count: Option<u32>, // Number of occurrences
|
|
pub calendar_path: Option<String>, // Optional - search all calendars if not specified
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct CreateEventSeriesResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
pub series_uid: Option<String>, // The base UID for the series
|
|
pub occurrences_created: Option<u32>, // Number of individual events created
|
|
pub event_href: Option<String>, // The created series' href/filename
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdateEventSeriesRequest {
|
|
pub series_uid: String, // Series UID to identify which series to update
|
|
pub title: String,
|
|
pub description: String,
|
|
pub start_date: String, // YYYY-MM-DD format
|
|
pub start_time: String, // HH:MM format
|
|
pub end_date: String, // YYYY-MM-DD format
|
|
pub end_time: String, // HH:MM format
|
|
pub location: String,
|
|
pub all_day: bool,
|
|
pub status: String, // confirmed, tentative, cancelled
|
|
pub class: String, // public, private, confidential
|
|
pub priority: Option<u8>, // 0-9 priority level
|
|
pub organizer: String, // organizer email
|
|
pub attendees: String, // comma-separated attendee emails
|
|
pub categories: String, // comma-separated categories
|
|
pub reminder: String, // reminder type
|
|
|
|
// Series-specific fields
|
|
pub recurrence: String, // recurrence type (daily, weekly, monthly, yearly)
|
|
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
|
pub recurrence_interval: Option<u32>, // Every N days/weeks/months/years
|
|
pub recurrence_end_date: Option<String>, // When the series ends (YYYY-MM-DD)
|
|
pub recurrence_count: Option<u32>, // Number of occurrences
|
|
pub calendar_path: Option<String>, // Optional - search all calendars if not specified
|
|
|
|
// 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)]
|
|
pub struct UpdateEventSeriesResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
pub series_uid: Option<String>,
|
|
pub occurrences_affected: Option<u32>, // Number of events updated
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DeleteEventSeriesRequest {
|
|
pub series_uid: String, // Series UID to identify which series to delete
|
|
pub calendar_path: String,
|
|
pub event_href: String,
|
|
|
|
// Delete scope control
|
|
pub delete_scope: String, // "this_only", "this_and_future", "all_in_series"
|
|
pub occurrence_date: Option<String>, // ISO date string for specific occurrence being deleted
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DeleteEventSeriesResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
pub occurrences_affected: Option<u32>, // Number of events deleted
|
|
}
|
|
|
|
// Error handling
|
|
#[derive(Debug)]
|
|
pub enum ApiError {
|
|
Database(String),
|
|
NotFound(String),
|
|
Unauthorized(String),
|
|
BadRequest(String),
|
|
Conflict(String),
|
|
Internal(String),
|
|
}
|
|
|
|
impl IntoResponse for ApiError {
|
|
fn into_response(self) -> Response {
|
|
let (status, error_message) = match self {
|
|
ApiError::Database(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
|
|
ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
|
|
ApiError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg),
|
|
ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
|
|
ApiError::Conflict(msg) => (StatusCode::CONFLICT, msg),
|
|
ApiError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
|
|
};
|
|
|
|
let body = Json(serde_json::json!({
|
|
"error": error_message,
|
|
"status": status.as_u16()
|
|
}));
|
|
|
|
(status, body).into_response()
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for ApiError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
ApiError::Database(msg) => write!(f, "Database error: {}", msg),
|
|
ApiError::NotFound(msg) => write!(f, "Not found: {}", msg),
|
|
ApiError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
|
|
ApiError::BadRequest(msg) => write!(f, "Bad request: {}", msg),
|
|
ApiError::Conflict(msg) => write!(f, "Conflict: {}", msg),
|
|
ApiError::Internal(msg) => write!(f, "Internal error: {}", msg),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for ApiError {} |