Implement event deletion with right-click context menu
- Add EventContextMenu component with delete option - Create DELETE /api/calendar/events/delete endpoint - Implement CalDAV event deletion in backend - Add proper URL construction for CalDAV event hrefs - Integrate context menu with calendar event right-clicks - Auto-refresh UI after successful event deletion 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -694,6 +694,47 @@ impl CalDAVClient {
|
|||||||
Err(CalDAVError::ServerError(status.as_u16()))
|
Err(CalDAVError::ServerError(status.as_u16()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete an event from a CalDAV calendar
|
||||||
|
pub async fn delete_event(&self, calendar_path: &str, event_href: &str) -> Result<(), CalDAVError> {
|
||||||
|
// Construct the full URL for the event
|
||||||
|
let full_url = if event_href.starts_with("http") {
|
||||||
|
event_href.to_string()
|
||||||
|
} else if event_href.starts_with("/dav.php") {
|
||||||
|
// Event href is already a full path, combine with base server URL (without /dav.php)
|
||||||
|
let base_url = self.config.server_url.trim_end_matches('/').trim_end_matches("/dav.php");
|
||||||
|
format!("{}{}", base_url, event_href)
|
||||||
|
} else {
|
||||||
|
// Event href is just a filename, combine with calendar path
|
||||||
|
let clean_path = if calendar_path.starts_with("/dav.php") {
|
||||||
|
calendar_path.trim_start_matches("/dav.php")
|
||||||
|
} else {
|
||||||
|
calendar_path
|
||||||
|
};
|
||||||
|
format!("{}/dav.php{}{}", self.config.server_url.trim_end_matches('/'), clean_path, event_href)
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Deleting event at: {}", full_url);
|
||||||
|
|
||||||
|
let response = self.http_client
|
||||||
|
.delete(&full_url)
|
||||||
|
.header("Authorization", format!("Basic {}", self.config.get_basic_auth()))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| CalDAVError::ParseError(e.to_string()))?;
|
||||||
|
|
||||||
|
println!("Event deletion response status: {}", response.status());
|
||||||
|
|
||||||
|
if response.status().is_success() || response.status().as_u16() == 204 {
|
||||||
|
println!("✅ Event deleted successfully at {}", event_href);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
let status = response.status();
|
||||||
|
let error_body = response.text().await.unwrap_or_default();
|
||||||
|
println!("❌ Event deletion failed: {} - {}", status, error_body);
|
||||||
|
Err(CalDAVError::ServerError(status.as_u16()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper struct for extracting calendar data from XML responses
|
/// Helper struct for extracting calendar data from XML responses
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use serde::Deserialize;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use chrono::Datelike;
|
use chrono::Datelike;
|
||||||
|
|
||||||
use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest, DeleteCalendarResponse}};
|
use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest, DeleteCalendarResponse, DeleteEventRequest, DeleteEventResponse}};
|
||||||
use crate::calendar::{CalDAVClient, CalendarEvent};
|
use crate::calendar::{CalDAVClient, CalendarEvent};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -323,4 +323,38 @@ pub async fn delete_calendar(
|
|||||||
success: true,
|
success: true,
|
||||||
message: "Calendar deleted successfully".to_string(),
|
message: "Calendar deleted successfully".to_string(),
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_event(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Json(request): Json<DeleteEventRequest>,
|
||||||
|
) -> Result<Json<DeleteEventResponse>, ApiError> {
|
||||||
|
println!("🗑️ Delete event request received: calendar_path='{}', event_href='{}'", request.calendar_path, request.event_href);
|
||||||
|
|
||||||
|
// Extract and verify token
|
||||||
|
let token = extract_bearer_token(&headers)?;
|
||||||
|
let password = extract_password_header(&headers)?;
|
||||||
|
|
||||||
|
// Validate request
|
||||||
|
if request.calendar_path.trim().is_empty() {
|
||||||
|
return Err(ApiError::BadRequest("Calendar path is required".to_string()));
|
||||||
|
}
|
||||||
|
if request.event_href.trim().is_empty() {
|
||||||
|
return Err(ApiError::BadRequest("Event href is required".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create CalDAV config from token and password
|
||||||
|
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
|
||||||
|
let client = CalDAVClient::new(config);
|
||||||
|
|
||||||
|
// Delete the event
|
||||||
|
client.delete_event(&request.calendar_path, &request.event_href)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?;
|
||||||
|
|
||||||
|
Ok(Json(DeleteEventResponse {
|
||||||
|
success: true,
|
||||||
|
message: "Event deleted successfully".to_string(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,7 @@ pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.route("/api/calendar/create", post(handlers::create_calendar))
|
.route("/api/calendar/create", post(handlers::create_calendar))
|
||||||
.route("/api/calendar/delete", post(handlers::delete_calendar))
|
.route("/api/calendar/delete", post(handlers::delete_calendar))
|
||||||
.route("/api/calendar/events", get(handlers::get_calendar_events))
|
.route("/api/calendar/events", get(handlers::get_calendar_events))
|
||||||
|
.route("/api/calendar/events/delete", post(handlers::delete_event))
|
||||||
.route("/api/calendar/events/:uid", get(handlers::refresh_event))
|
.route("/api/calendar/events/:uid", get(handlers::refresh_event))
|
||||||
.layer(
|
.layer(
|
||||||
CorsLayer::new()
|
CorsLayer::new()
|
||||||
|
|||||||
@@ -58,6 +58,18 @@ pub struct DeleteCalendarResponse {
|
|||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct DeleteEventRequest {
|
||||||
|
pub calendar_path: String,
|
||||||
|
pub event_href: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct DeleteEventResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
// Error handling
|
// Error handling
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ApiError {
|
pub enum ApiError {
|
||||||
|
|||||||
75
src/app.rs
75
src/app.rs
@@ -2,8 +2,8 @@ use yew::prelude::*;
|
|||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
use gloo_storage::{LocalStorage, Storage};
|
use gloo_storage::{LocalStorage, Storage};
|
||||||
use web_sys::MouseEvent;
|
use web_sys::MouseEvent;
|
||||||
use crate::components::{Sidebar, CreateCalendarModal, ContextMenu, RouteHandler};
|
use crate::components::{Sidebar, CreateCalendarModal, ContextMenu, EventContextMenu, RouteHandler};
|
||||||
use crate::services::{CalendarService, calendar_service::UserInfo};
|
use crate::services::{CalendarService, calendar_service::{UserInfo, CalendarEvent}};
|
||||||
|
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
@@ -18,6 +18,9 @@ pub fn App() -> Html {
|
|||||||
let context_menu_open = use_state(|| false);
|
let context_menu_open = use_state(|| false);
|
||||||
let context_menu_pos = use_state(|| (0i32, 0i32));
|
let context_menu_pos = use_state(|| (0i32, 0i32));
|
||||||
let context_menu_calendar_path = use_state(|| -> Option<String> { None });
|
let context_menu_calendar_path = use_state(|| -> Option<String> { None });
|
||||||
|
let event_context_menu_open = use_state(|| false);
|
||||||
|
let event_context_menu_pos = use_state(|| (0i32, 0i32));
|
||||||
|
let event_context_menu_event = use_state(|| -> Option<CalendarEvent> { None });
|
||||||
|
|
||||||
let available_colors = [
|
let available_colors = [
|
||||||
"#3B82F6", "#10B981", "#F59E0B", "#EF4444",
|
"#3B82F6", "#10B981", "#F59E0B", "#EF4444",
|
||||||
@@ -99,9 +102,11 @@ pub fn App() -> Html {
|
|||||||
let on_outside_click = {
|
let on_outside_click = {
|
||||||
let color_picker_open = color_picker_open.clone();
|
let color_picker_open = color_picker_open.clone();
|
||||||
let context_menu_open = context_menu_open.clone();
|
let context_menu_open = context_menu_open.clone();
|
||||||
|
let event_context_menu_open = event_context_menu_open.clone();
|
||||||
Callback::from(move |_: MouseEvent| {
|
Callback::from(move |_: MouseEvent| {
|
||||||
color_picker_open.set(None);
|
color_picker_open.set(None);
|
||||||
context_menu_open.set(false);
|
context_menu_open.set(false);
|
||||||
|
event_context_menu_open.set(false);
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -148,6 +153,17 @@ pub fn App() -> Html {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let on_event_context_menu = {
|
||||||
|
let event_context_menu_open = event_context_menu_open.clone();
|
||||||
|
let event_context_menu_pos = event_context_menu_pos.clone();
|
||||||
|
let event_context_menu_event = event_context_menu_event.clone();
|
||||||
|
Callback::from(move |(event, calendar_event): (MouseEvent, CalendarEvent)| {
|
||||||
|
event_context_menu_open.set(true);
|
||||||
|
event_context_menu_pos.set((event.client_x(), event.client_y()));
|
||||||
|
event_context_menu_event.set(Some(calendar_event));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
let refresh_calendars = {
|
let refresh_calendars = {
|
||||||
let auth_token = auth_token.clone();
|
let auth_token = auth_token.clone();
|
||||||
let user_info = user_info.clone();
|
let user_info = user_info.clone();
|
||||||
@@ -217,6 +233,7 @@ pub fn App() -> Html {
|
|||||||
auth_token={(*auth_token).clone()}
|
auth_token={(*auth_token).clone()}
|
||||||
user_info={(*user_info).clone()}
|
user_info={(*user_info).clone()}
|
||||||
on_login={on_login.clone()}
|
on_login={on_login.clone()}
|
||||||
|
on_event_context_menu={Some(on_event_context_menu.clone())}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
@@ -228,6 +245,7 @@ pub fn App() -> Html {
|
|||||||
auth_token={(*auth_token).clone()}
|
auth_token={(*auth_token).clone()}
|
||||||
user_info={(*user_info).clone()}
|
user_info={(*user_info).clone()}
|
||||||
on_login={on_login.clone()}
|
on_login={on_login.clone()}
|
||||||
|
on_event_context_menu={Some(on_event_context_menu.clone())}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -323,6 +341,59 @@ pub fn App() -> Html {
|
|||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<EventContextMenu
|
||||||
|
is_open={*event_context_menu_open}
|
||||||
|
x={event_context_menu_pos.0}
|
||||||
|
y={event_context_menu_pos.1}
|
||||||
|
on_close={Callback::from({
|
||||||
|
let event_context_menu_open = event_context_menu_open.clone();
|
||||||
|
move |_| event_context_menu_open.set(false)
|
||||||
|
})}
|
||||||
|
on_delete={Callback::from({
|
||||||
|
let auth_token = auth_token.clone();
|
||||||
|
let event_context_menu_event = event_context_menu_event.clone();
|
||||||
|
let event_context_menu_open = event_context_menu_open.clone();
|
||||||
|
let refresh_calendars = refresh_calendars.clone();
|
||||||
|
move |_: MouseEvent| {
|
||||||
|
if let (Some(token), Some(event)) = ((*auth_token).clone(), (*event_context_menu_event).clone()) {
|
||||||
|
let refresh_calendars = refresh_calendars.clone();
|
||||||
|
let event_context_menu_open = event_context_menu_open.clone();
|
||||||
|
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
let calendar_service = CalendarService::new();
|
||||||
|
|
||||||
|
let password = if let Ok(credentials_str) = LocalStorage::get::<String>("caldav_credentials") {
|
||||||
|
if let Ok(credentials) = serde_json::from_str::<serde_json::Value>(&credentials_str) {
|
||||||
|
credentials["password"].as_str().unwrap_or("").to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let (Some(calendar_path), Some(event_href)) = (&event.calendar_path, &event.href) {
|
||||||
|
match calendar_service.delete_event(&token, &password, calendar_path.clone(), event_href.clone()).await {
|
||||||
|
Ok(_) => {
|
||||||
|
web_sys::console::log_1(&"Event deleted successfully!".into());
|
||||||
|
// Close the context menu
|
||||||
|
event_context_menu_open.set(false);
|
||||||
|
// Force a page reload to refresh the calendar events
|
||||||
|
web_sys::window().unwrap().location().reload().unwrap();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
web_sys::console::log_1(&format!("Failed to delete event: {}", err).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
web_sys::console::log_1(&"Missing calendar_path or href - cannot delete event".into());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ pub struct CalendarProps {
|
|||||||
pub refreshing_event_uid: Option<String>,
|
pub refreshing_event_uid: Option<String>,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub user_info: Option<UserInfo>,
|
pub user_info: Option<UserInfo>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
@@ -131,6 +133,18 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
|||||||
on_event_click.emit(event_clone.clone());
|
on_event_click.emit(event_clone.clone());
|
||||||
selected_event_clone.set(Some(event_clone.clone()));
|
selected_event_clone.set(Some(event_clone.clone()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let event_context_menu = {
|
||||||
|
let event_clone = event.clone();
|
||||||
|
let on_event_context_menu = props.on_event_context_menu.clone();
|
||||||
|
Callback::from(move |e: MouseEvent| {
|
||||||
|
e.prevent_default();
|
||||||
|
e.stop_propagation();
|
||||||
|
if let Some(callback) = &on_event_context_menu {
|
||||||
|
callback.emit((e, event_clone.clone()));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
let title = event.get_title();
|
let title = event.get_title();
|
||||||
let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid);
|
let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid);
|
||||||
@@ -140,6 +154,7 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
|||||||
<div class={class_name}
|
<div class={class_name}
|
||||||
title={title.clone()}
|
title={title.clone()}
|
||||||
onclick={event_click}
|
onclick={event_click}
|
||||||
|
oncontextmenu={event_context_menu}
|
||||||
style={format!("background-color: {}", event_color)}>
|
style={format!("background-color: {}", event_color)}>
|
||||||
{
|
{
|
||||||
if is_refreshing {
|
if is_refreshing {
|
||||||
|
|||||||
47
src/components/event_context_menu.rs
Normal file
47
src/components/event_context_menu.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use yew::prelude::*;
|
||||||
|
use web_sys::MouseEvent;
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct EventContextMenuProps {
|
||||||
|
pub is_open: bool,
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
pub on_delete: Callback<MouseEvent>,
|
||||||
|
pub on_close: Callback<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(EventContextMenu)]
|
||||||
|
pub fn event_context_menu(props: &EventContextMenuProps) -> Html {
|
||||||
|
let menu_ref = use_node_ref();
|
||||||
|
|
||||||
|
if !props.is_open {
|
||||||
|
return html! {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let style = format!(
|
||||||
|
"position: fixed; left: {}px; top: {}px; z-index: 1001;",
|
||||||
|
props.x, props.y
|
||||||
|
);
|
||||||
|
|
||||||
|
let on_delete_click = {
|
||||||
|
let on_delete = props.on_delete.clone();
|
||||||
|
let on_close = props.on_close.clone();
|
||||||
|
Callback::from(move |e: MouseEvent| {
|
||||||
|
on_delete.emit(e);
|
||||||
|
on_close.emit(());
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div
|
||||||
|
ref={menu_ref}
|
||||||
|
class="context-menu"
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<div class="context-menu-item context-menu-delete" onclick={on_delete_click}>
|
||||||
|
<span class="context-menu-icon">{"🗑️"}</span>
|
||||||
|
{"Delete Event"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ pub mod calendar;
|
|||||||
pub mod event_modal;
|
pub mod event_modal;
|
||||||
pub mod create_calendar_modal;
|
pub mod create_calendar_modal;
|
||||||
pub mod context_menu;
|
pub mod context_menu;
|
||||||
|
pub mod event_context_menu;
|
||||||
pub mod sidebar;
|
pub mod sidebar;
|
||||||
pub mod calendar_list_item;
|
pub mod calendar_list_item;
|
||||||
pub mod route_handler;
|
pub mod route_handler;
|
||||||
@@ -12,6 +13,7 @@ pub use calendar::Calendar;
|
|||||||
pub use event_modal::EventModal;
|
pub use event_modal::EventModal;
|
||||||
pub use create_calendar_modal::CreateCalendarModal;
|
pub use create_calendar_modal::CreateCalendarModal;
|
||||||
pub use context_menu::ContextMenu;
|
pub use context_menu::ContextMenu;
|
||||||
|
pub use event_context_menu::EventContextMenu;
|
||||||
pub use sidebar::Sidebar;
|
pub use sidebar::Sidebar;
|
||||||
pub use calendar_list_item::CalendarListItem;
|
pub use calendar_list_item::CalendarListItem;
|
||||||
pub use route_handler::RouteHandler;
|
pub use route_handler::RouteHandler;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
use crate::components::Login;
|
use crate::components::Login;
|
||||||
use crate::services::calendar_service::UserInfo;
|
use crate::services::calendar_service::{UserInfo, CalendarEvent};
|
||||||
|
|
||||||
#[derive(Clone, Routable, PartialEq)]
|
#[derive(Clone, Routable, PartialEq)]
|
||||||
pub enum Route {
|
pub enum Route {
|
||||||
@@ -18,6 +18,8 @@ pub struct RouteHandlerProps {
|
|||||||
pub auth_token: Option<String>,
|
pub auth_token: Option<String>,
|
||||||
pub user_info: Option<UserInfo>,
|
pub user_info: Option<UserInfo>,
|
||||||
pub on_login: Callback<String>,
|
pub on_login: Callback<String>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component(RouteHandler)]
|
#[function_component(RouteHandler)]
|
||||||
@@ -25,12 +27,14 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html {
|
|||||||
let auth_token = props.auth_token.clone();
|
let auth_token = props.auth_token.clone();
|
||||||
let user_info = props.user_info.clone();
|
let user_info = props.user_info.clone();
|
||||||
let on_login = props.on_login.clone();
|
let on_login = props.on_login.clone();
|
||||||
|
let on_event_context_menu = props.on_event_context_menu.clone();
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<Switch<Route> render={move |route| {
|
<Switch<Route> render={move |route| {
|
||||||
let auth_token = auth_token.clone();
|
let auth_token = auth_token.clone();
|
||||||
let user_info = user_info.clone();
|
let user_info = user_info.clone();
|
||||||
let on_login = on_login.clone();
|
let on_login = on_login.clone();
|
||||||
|
let on_event_context_menu = on_event_context_menu.clone();
|
||||||
|
|
||||||
match route {
|
match route {
|
||||||
Route::Home => {
|
Route::Home => {
|
||||||
@@ -49,7 +53,12 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html {
|
|||||||
}
|
}
|
||||||
Route::Calendar => {
|
Route::Calendar => {
|
||||||
if auth_token.is_some() {
|
if auth_token.is_some() {
|
||||||
html! { <CalendarView user_info={user_info} /> }
|
html! {
|
||||||
|
<CalendarView
|
||||||
|
user_info={user_info}
|
||||||
|
on_event_context_menu={on_event_context_menu}
|
||||||
|
/>
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
html! { <Redirect<Route> to={Route::Login}/> }
|
html! { <Redirect<Route> to={Route::Login}/> }
|
||||||
}
|
}
|
||||||
@@ -62,10 +71,12 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html {
|
|||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct CalendarViewProps {
|
pub struct CalendarViewProps {
|
||||||
pub user_info: Option<UserInfo>,
|
pub user_info: Option<UserInfo>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
use gloo_storage::{LocalStorage, Storage};
|
use gloo_storage::{LocalStorage, Storage};
|
||||||
use crate::services::{CalendarService, CalendarEvent};
|
use crate::services::CalendarService;
|
||||||
use crate::components::Calendar;
|
use crate::components::Calendar;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use chrono::{Local, NaiveDate, Datelike};
|
use chrono::{Local, NaiveDate, Datelike};
|
||||||
@@ -79,6 +90,7 @@ pub fn calendar_view(props: &CalendarViewProps) -> Html {
|
|||||||
|
|
||||||
let auth_token: Option<String> = LocalStorage::get("auth_token").ok();
|
let auth_token: Option<String> = LocalStorage::get("auth_token").ok();
|
||||||
|
|
||||||
|
|
||||||
let today = Local::now().date_naive();
|
let today = Local::now().date_naive();
|
||||||
let current_year = today.year();
|
let current_year = today.year();
|
||||||
let current_month = today.month();
|
let current_month = today.month();
|
||||||
@@ -212,12 +224,24 @@ pub fn calendar_view(props: &CalendarViewProps) -> Html {
|
|||||||
html! {
|
html! {
|
||||||
<div class="calendar-error">
|
<div class="calendar-error">
|
||||||
<p>{format!("Error: {}", err)}</p>
|
<p>{format!("Error: {}", err)}</p>
|
||||||
<Calendar events={HashMap::new()} on_event_click={dummy_callback} refreshing_event_uid={(*refreshing_event).clone()} user_info={props.user_info.clone()} />
|
<Calendar
|
||||||
|
events={HashMap::new()}
|
||||||
|
on_event_click={dummy_callback}
|
||||||
|
refreshing_event_uid={(*refreshing_event).clone()}
|
||||||
|
user_info={props.user_info.clone()}
|
||||||
|
on_event_context_menu={props.on_event_context_menu.clone()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
html! {
|
html! {
|
||||||
<Calendar events={(*events).clone()} on_event_click={on_event_click} refreshing_event_uid={(*refreshing_event).clone()} user_info={props.user_info.clone()} />
|
<Calendar
|
||||||
|
events={(*events).clone()}
|
||||||
|
on_event_click={on_event_click}
|
||||||
|
refreshing_event_uid={(*refreshing_event).clone()}
|
||||||
|
user_info={props.user_info.clone()}
|
||||||
|
on_event_context_menu={props.on_event_context_menu.clone()}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -528,6 +528,64 @@ impl CalendarService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete an event from the CalDAV server
|
||||||
|
pub async fn delete_event(
|
||||||
|
&self,
|
||||||
|
token: &str,
|
||||||
|
password: &str,
|
||||||
|
calendar_path: String,
|
||||||
|
event_href: String
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let window = web_sys::window().ok_or("No global window exists")?;
|
||||||
|
|
||||||
|
let opts = RequestInit::new();
|
||||||
|
opts.set_method("POST");
|
||||||
|
opts.set_mode(RequestMode::Cors);
|
||||||
|
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"calendar_path": calendar_path,
|
||||||
|
"event_href": event_href
|
||||||
|
});
|
||||||
|
|
||||||
|
let body_string = serde_json::to_string(&body)
|
||||||
|
.map_err(|e| format!("JSON serialization failed: {}", e))?;
|
||||||
|
|
||||||
|
let url = format!("{}/calendar/events/delete", self.base_url);
|
||||||
|
opts.set_body(&body_string.into());
|
||||||
|
let request = Request::new_with_str_and_init(&url, &opts)
|
||||||
|
.map_err(|e| format!("Request creation failed: {:?}", e))?;
|
||||||
|
|
||||||
|
request.headers().set("Authorization", &format!("Bearer {}", token))
|
||||||
|
.map_err(|e| format!("Authorization header setting failed: {:?}", e))?;
|
||||||
|
|
||||||
|
request.headers().set("X-CalDAV-Password", password)
|
||||||
|
.map_err(|e| format!("Password header setting failed: {:?}", e))?;
|
||||||
|
|
||||||
|
request.headers().set("Content-Type", "application/json")
|
||||||
|
.map_err(|e| format!("Content-Type header setting failed: {:?}", e))?;
|
||||||
|
|
||||||
|
let resp_value = JsFuture::from(window.fetch_with_request(&request))
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Network request failed: {:?}", e))?;
|
||||||
|
|
||||||
|
let resp: Response = resp_value.dyn_into()
|
||||||
|
.map_err(|e| format!("Response cast failed: {:?}", e))?;
|
||||||
|
|
||||||
|
let text = JsFuture::from(resp.text()
|
||||||
|
.map_err(|e| format!("Text extraction failed: {:?}", e))?)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Text promise failed: {:?}", e))?;
|
||||||
|
|
||||||
|
let text_string = text.as_string()
|
||||||
|
.ok_or("Response text is not a string")?;
|
||||||
|
|
||||||
|
if resp.ok() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("Request failed with status {}: {}", resp.status(), text_string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete a calendar from the CalDAV server
|
/// Delete a calendar from the CalDAV server
|
||||||
pub async fn delete_calendar(
|
pub async fn delete_calendar(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
Reference in New Issue
Block a user