Merge pull request 'Small bugfixes on the external calendar handling' (#19) from bugfix/external_cal_misc into main
All checks were successful
Build and Push Docker Image / docker (push) Successful in 4m3s
All checks were successful
Build and Push Docker Image / docker (push) Successful in 4m3s
Reviewed-on: #19
This commit is contained in:
@@ -78,17 +78,75 @@ pub async fn fetch_external_calendar_events(
|
|||||||
|
|
||||||
// If not fetched from cache, get from external URL
|
// If not fetched from cache, get from external URL
|
||||||
if !fetched_from_cache {
|
if !fetched_from_cache {
|
||||||
let client = Client::new();
|
// Log the URL being fetched for debugging
|
||||||
let response = client
|
println!("🌍 Fetching calendar URL: {}", calendar.url);
|
||||||
.get(&calendar.url)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| ApiError::Internal(format!("Failed to fetch calendar: {}", e)))?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
let user_agents = vec![
|
||||||
return Err(ApiError::Internal(format!("Calendar server returned: {}", response.status())));
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (compatible; Runway Calendar/1.0)",
|
||||||
|
"Outlook-iOS/709.2226530.prod.iphone (3.24.1)"
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut response = None;
|
||||||
|
let mut last_error = None;
|
||||||
|
|
||||||
|
// Try different user agents
|
||||||
|
for (i, ua) in user_agents.iter().enumerate() {
|
||||||
|
println!("🔄 Attempt {} with User-Agent: {}", i + 1, ua);
|
||||||
|
|
||||||
|
let client = Client::builder()
|
||||||
|
.redirect(reqwest::redirect::Policy::limited(10))
|
||||||
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
|
.user_agent(*ua)
|
||||||
|
.build()
|
||||||
|
.map_err(|e| ApiError::Internal(format!("Failed to create HTTP client: {}", e)))?;
|
||||||
|
|
||||||
|
let result = client
|
||||||
|
.get(&calendar.url)
|
||||||
|
.header("Accept", "text/calendar,application/calendar+xml,text/plain,*/*")
|
||||||
|
.header("Accept-Charset", "utf-8")
|
||||||
|
.header("Cache-Control", "no-cache")
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(resp) => {
|
||||||
|
let status = resp.status();
|
||||||
|
println!("📡 Response status: {}", status);
|
||||||
|
if status.is_success() {
|
||||||
|
response = Some(resp);
|
||||||
|
break;
|
||||||
|
} else if status == 400 {
|
||||||
|
// Check if this is an Outlook auth error
|
||||||
|
let error_body = resp.text().await.unwrap_or_default();
|
||||||
|
if error_body.contains("OwaPage") || error_body.contains("Outlook") {
|
||||||
|
println!("🚫 Outlook authentication error detected, trying next approach...");
|
||||||
|
last_error = Some(format!("Outlook auth error: {}", error_body.chars().take(100).collect::<String>()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
last_error = Some(format!("Bad Request: {}", error_body.chars().take(100).collect::<String>()));
|
||||||
|
} else {
|
||||||
|
last_error = Some(format!("HTTP {}", status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("❌ Request failed: {}", e);
|
||||||
|
last_error = Some(format!("Request error: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let response = response.ok_or_else(|| {
|
||||||
|
ApiError::Internal(format!(
|
||||||
|
"Failed to fetch calendar after trying {} different approaches. Last error: {}",
|
||||||
|
user_agents.len(),
|
||||||
|
last_error.unwrap_or("Unknown error".to_string())
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Response is guaranteed to be successful here since we checked in the loop
|
||||||
|
println!("✅ Successfully fetched calendar data");
|
||||||
|
|
||||||
ics_content = response
|
ics_content = response
|
||||||
.text()
|
.text()
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -567,19 +567,60 @@ pub fn App() -> Html {
|
|||||||
|
|
||||||
let on_color_change = {
|
let on_color_change = {
|
||||||
let user_info = user_info.clone();
|
let user_info = user_info.clone();
|
||||||
|
let external_calendars = external_calendars.clone();
|
||||||
let color_picker_open = color_picker_open.clone();
|
let color_picker_open = color_picker_open.clone();
|
||||||
Callback::from(move |(calendar_path, color): (String, String)| {
|
Callback::from(move |(calendar_path, color): (String, String)| {
|
||||||
if let Some(mut info) = (*user_info).clone() {
|
if calendar_path.starts_with("external_") {
|
||||||
for calendar in &mut info.calendars {
|
// Handle external calendar color change
|
||||||
if calendar.path == calendar_path {
|
if let Ok(id_str) = calendar_path.strip_prefix("external_").unwrap_or("").parse::<i32>() {
|
||||||
calendar.color = color.clone();
|
let external_calendars = external_calendars.clone();
|
||||||
break;
|
let color = color.clone();
|
||||||
}
|
|
||||||
}
|
|
||||||
user_info.set(Some(info.clone()));
|
|
||||||
|
|
||||||
if let Ok(json) = serde_json::to_string(&info) {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
let _ = LocalStorage::set("calendar_colors", json);
|
// Find the external calendar to get its current details
|
||||||
|
if let Some(cal) = (*external_calendars).iter().find(|c| c.id == id_str) {
|
||||||
|
match CalendarService::update_external_calendar(
|
||||||
|
id_str,
|
||||||
|
&cal.name,
|
||||||
|
&cal.url,
|
||||||
|
&color,
|
||||||
|
cal.is_visible,
|
||||||
|
).await {
|
||||||
|
Ok(_) => {
|
||||||
|
// Update the local state
|
||||||
|
let mut updated_calendars = (*external_calendars).clone();
|
||||||
|
for calendar in &mut updated_calendars {
|
||||||
|
if calendar.id == id_str {
|
||||||
|
calendar.color = color.clone();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
external_calendars.set(updated_calendars);
|
||||||
|
|
||||||
|
// No need to refresh events - they will automatically pick up the new color
|
||||||
|
// from the calendar when rendered since they use the same calendar_path matching
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
web_sys::console::error_1(&format!("Failed to update external calendar color: {}", e).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle CalDAV calendar color change (existing logic)
|
||||||
|
if let Some(mut info) = (*user_info).clone() {
|
||||||
|
for calendar in &mut info.calendars {
|
||||||
|
if calendar.path == calendar_path {
|
||||||
|
calendar.color = color.clone();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user_info.set(Some(info.clone()));
|
||||||
|
|
||||||
|
if let Ok(json) = serde_json::to_string(&info) {
|
||||||
|
let _ = LocalStorage::set("calendar_colors", json);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
color_picker_open.set(None);
|
color_picker_open.set(None);
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
|||||||
let is_creating = use_state(|| false);
|
let is_creating = use_state(|| false);
|
||||||
|
|
||||||
// External Calendar state
|
// External Calendar state
|
||||||
let external_name_ref = use_node_ref();
|
let external_name = use_state(|| String::new());
|
||||||
let external_url_ref = use_node_ref();
|
let external_url = use_state(|| String::new());
|
||||||
let external_selected_color = use_state(|| Some("#4285f4".to_string()));
|
let external_selected_color = use_state(|| Some("#4285f4".to_string()));
|
||||||
let external_is_loading = use_state(|| false);
|
let external_is_loading = use_state(|| false);
|
||||||
let external_error_message = use_state(|| None::<String>);
|
let external_error_message = use_state(|| None::<String>);
|
||||||
@@ -43,6 +43,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
|||||||
let selected_color = selected_color.clone();
|
let selected_color = selected_color.clone();
|
||||||
let create_error_message = create_error_message.clone();
|
let create_error_message = create_error_message.clone();
|
||||||
let is_creating = is_creating.clone();
|
let is_creating = is_creating.clone();
|
||||||
|
let external_name = external_name.clone();
|
||||||
|
let external_url = external_url.clone();
|
||||||
let external_is_loading = external_is_loading.clone();
|
let external_is_loading = external_is_loading.clone();
|
||||||
let external_error_message = external_error_message.clone();
|
let external_error_message = external_error_message.clone();
|
||||||
let external_selected_color = external_selected_color.clone();
|
let external_selected_color = external_selected_color.clone();
|
||||||
@@ -56,6 +58,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
|||||||
selected_color.set(None);
|
selected_color.set(None);
|
||||||
create_error_message.set(None);
|
create_error_message.set(None);
|
||||||
is_creating.set(false);
|
is_creating.set(false);
|
||||||
|
external_name.set(String::new());
|
||||||
|
external_url.set(String::new());
|
||||||
external_is_loading.set(false);
|
external_is_loading.set(false);
|
||||||
external_error_message.set(None);
|
external_error_message.set(None);
|
||||||
external_selected_color.set(Some("#4285f4".to_string()));
|
external_selected_color.set(Some("#4285f4".to_string()));
|
||||||
@@ -146,8 +150,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
|||||||
|
|
||||||
// External Calendar handlers
|
// External Calendar handlers
|
||||||
let on_external_submit = {
|
let on_external_submit = {
|
||||||
let external_name_ref = external_name_ref.clone();
|
let external_name = external_name.clone();
|
||||||
let external_url_ref = external_url_ref.clone();
|
let external_url = external_url.clone();
|
||||||
let external_selected_color = external_selected_color.clone();
|
let external_selected_color = external_selected_color.clone();
|
||||||
let external_is_loading = external_is_loading.clone();
|
let external_is_loading = external_is_loading.clone();
|
||||||
let external_error_message = external_error_message.clone();
|
let external_error_message = external_error_message.clone();
|
||||||
@@ -157,24 +161,28 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
|||||||
Callback::from(move |e: SubmitEvent| {
|
Callback::from(move |e: SubmitEvent| {
|
||||||
e.prevent_default();
|
e.prevent_default();
|
||||||
|
|
||||||
let name = external_name_ref
|
let name = (*external_name).trim().to_string();
|
||||||
.cast::<HtmlInputElement>()
|
let url = (*external_url).trim().to_string();
|
||||||
.map(|input| input.value())
|
|
||||||
.unwrap_or_default()
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let url = external_url_ref
|
|
||||||
.cast::<HtmlInputElement>()
|
|
||||||
.map(|input| input.value())
|
|
||||||
.unwrap_or_default()
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let color = (*external_selected_color).clone().unwrap_or_else(|| "#4285f4".to_string());
|
let color = (*external_selected_color).clone().unwrap_or_else(|| "#4285f4".to_string());
|
||||||
|
|
||||||
if name.is_empty() || url.is_empty() {
|
// Debug logging to understand the issue
|
||||||
external_error_message.set(Some("Name and URL are required".to_string()));
|
web_sys::console::log_1(&format!("External calendar form submission - Name: '{}', URL: '{}'", name, url).into());
|
||||||
|
|
||||||
|
if name.is_empty() {
|
||||||
|
external_error_message.set(Some("Calendar name is required".to_string()));
|
||||||
|
web_sys::console::log_1(&"Validation failed: empty name".into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if url.is_empty() {
|
||||||
|
external_error_message.set(Some("Calendar URL is required".to_string()));
|
||||||
|
web_sys::console::log_1(&"Validation failed: empty URL".into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic URL validation
|
||||||
|
if !url.starts_with("http://") && !url.starts_with("https://") {
|
||||||
|
external_error_message.set(Some("Please enter a valid HTTP or HTTPS URL".to_string()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +212,25 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// External input change handlers
|
||||||
|
let on_external_name_change = {
|
||||||
|
let external_name = external_name.clone();
|
||||||
|
Callback::from(move |e: Event| {
|
||||||
|
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
|
||||||
|
external_name.set(input.value());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_external_url_change = {
|
||||||
|
let external_url = external_url.clone();
|
||||||
|
Callback::from(move |e: Event| {
|
||||||
|
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
|
||||||
|
external_url.set(input.value());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
if !props.is_open {
|
if !props.is_open {
|
||||||
return html! {};
|
return html! {};
|
||||||
}
|
}
|
||||||
@@ -333,7 +360,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="external-name"
|
id="external-name"
|
||||||
ref={external_name_ref.clone()}
|
value={(*external_name).clone()}
|
||||||
|
onchange={on_external_name_change}
|
||||||
placeholder="Enter calendar name"
|
placeholder="Enter calendar name"
|
||||||
disabled={*external_is_loading}
|
disabled={*external_is_loading}
|
||||||
/>
|
/>
|
||||||
@@ -344,7 +372,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
|||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
id="external-url"
|
id="external-url"
|
||||||
ref={external_url_ref.clone()}
|
value={(*external_url).clone()}
|
||||||
|
onchange={on_external_url_change}
|
||||||
placeholder="https://example.com/calendar.ics"
|
placeholder="https://example.com/calendar.ics"
|
||||||
disabled={*external_is_loading}
|
disabled={*external_is_loading}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -256,7 +256,11 @@ pub fn sidebar(props: &SidebarProps) -> Html {
|
|||||||
html! {
|
html! {
|
||||||
<li class="external-calendar-item" style="position: relative;">
|
<li class="external-calendar-item" style="position: relative;">
|
||||||
<div
|
<div
|
||||||
class="external-calendar-info"
|
class={if props.color_picker_open.as_ref() == Some(&format!("external_{}", cal.id)) {
|
||||||
|
"external-calendar-info color-picker-active"
|
||||||
|
} else {
|
||||||
|
"external-calendar-info"
|
||||||
|
}}
|
||||||
oncontextmenu={{
|
oncontextmenu={{
|
||||||
let on_context_menu = on_external_calendar_context_menu.clone();
|
let on_context_menu = on_external_calendar_context_menu.clone();
|
||||||
let cal_id = cal.id;
|
let cal_id = cal.id;
|
||||||
@@ -273,7 +277,48 @@ pub fn sidebar(props: &SidebarProps) -> Html {
|
|||||||
<span
|
<span
|
||||||
class="external-calendar-color"
|
class="external-calendar-color"
|
||||||
style={format!("background-color: {}", cal.color)}
|
style={format!("background-color: {}", cal.color)}
|
||||||
/>
|
onclick={{
|
||||||
|
let on_color_picker_toggle = props.on_color_picker_toggle.clone();
|
||||||
|
let external_id = format!("external_{}", cal.id);
|
||||||
|
Callback::from(move |e: MouseEvent| {
|
||||||
|
e.stop_propagation();
|
||||||
|
on_color_picker_toggle.emit(external_id.clone());
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
if props.color_picker_open.as_ref() == Some(&format!("external_{}", cal.id)) {
|
||||||
|
html! {
|
||||||
|
<div class="color-picker-dropdown">
|
||||||
|
{
|
||||||
|
props.available_colors.iter().map(|color| {
|
||||||
|
let color_str = color.clone();
|
||||||
|
let external_id = format!("external_{}", cal.id);
|
||||||
|
let on_color_change = props.on_color_change.clone();
|
||||||
|
|
||||||
|
let on_color_select = Callback::from(move |_: MouseEvent| {
|
||||||
|
on_color_change.emit((external_id.clone(), color_str.clone()));
|
||||||
|
});
|
||||||
|
|
||||||
|
let is_selected = cal.color == *color;
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div
|
||||||
|
key={color.clone()}
|
||||||
|
class={if is_selected { "color-option selected" } else { "color-option" }}
|
||||||
|
style={format!("background-color: {}", color)}
|
||||||
|
onclick={on_color_select}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}).collect::<Html>()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</span>
|
||||||
<span class="external-calendar-name">{&cal.name}</span>
|
<span class="external-calendar-name">{&cal.name}</span>
|
||||||
<div class="external-calendar-actions">
|
<div class="external-calendar-actions">
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ body {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
z-index: 1000;
|
z-index: 999999;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@@ -3677,7 +3677,7 @@ body {
|
|||||||
border: 1px solid var(--glass-bg);
|
border: 1px solid var(--glass-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.external-calendar-info:hover {
|
.external-calendar-info:hover:not(.color-picker-active) {
|
||||||
background: var(--glass-bg);
|
background: var(--glass-bg);
|
||||||
border-color: var(--glass-bg-light);
|
border-color: var(--glass-bg-light);
|
||||||
transform: translateX(2px);
|
transform: translateX(2px);
|
||||||
|
|||||||
Reference in New Issue
Block a user