fix(frontend): use signal-based toast for async contexts

This commit is contained in:
spinline
2026-02-05 20:48:40 +03:00
parent 6c7379483e
commit 497b39e0ae
3 changed files with 51 additions and 32 deletions

View File

@@ -1,6 +1,7 @@
use leptos::*; use leptos::*;
use leptos::html::Dialog; use leptos::html::Dialog;
use crate::store::{toast_success, toast_error, toast_warning}; use crate::store::{show_toast_with_signal, TorrentStore};
use shared::NotificationLevel;
#[component] #[component]
@@ -8,6 +9,9 @@ pub fn AddTorrentModal(
#[prop(into)] #[prop(into)]
on_close: Callback<()>, on_close: Callback<()>,
) -> impl IntoView { ) -> impl IntoView {
let store = use_context::<TorrentStore>().expect("TorrentStore not provided");
let notifications = store.notifications;
let dialog_ref = create_node_ref::<Dialog>(); let dialog_ref = create_node_ref::<Dialog>();
let (uri, set_uri) = create_signal(String::new()); let (uri, set_uri) = create_signal(String::new());
let (is_loading, set_loading) = create_signal(false); let (is_loading, set_loading) = create_signal(false);
@@ -23,7 +27,7 @@ pub fn AddTorrentModal(
let handle_submit = move |_| { let handle_submit = move |_| {
let uri_val = uri.get(); let uri_val = uri.get();
if uri_val.is_empty() { if uri_val.is_empty() {
toast_warning("Lütfen bir Magnet URI veya URL girin"); show_toast_with_signal(notifications, NotificationLevel::Warning, "Lütfen bir Magnet URI veya URL girin");
set_error_msg.set(Some("Please enter a Magnet URI or URL".to_string())); set_error_msg.set(Some("Please enter a Magnet URI or URL".to_string()));
return; return;
} }
@@ -44,7 +48,7 @@ pub fn AddTorrentModal(
Ok(resp) => { Ok(resp) => {
if resp.ok() { if resp.ok() {
logging::log!("Torrent added successfully"); logging::log!("Torrent added successfully");
toast_success("Torrent eklendi"); show_toast_with_signal(notifications, NotificationLevel::Success, "Torrent eklendi");
set_loading.set(false); set_loading.set(false);
if let Some(dialog) = dialog_ref.get() { if let Some(dialog) = dialog_ref.get() {
dialog.close(); dialog.close();
@@ -54,14 +58,14 @@ pub fn AddTorrentModal(
let status = resp.status(); let status = resp.status();
let text = resp.text().await.unwrap_or_default(); let text = resp.text().await.unwrap_or_default();
logging::error!("Failed to add torrent: {} - {}", status, text); logging::error!("Failed to add torrent: {} - {}", status, text);
toast_error("Torrent eklenemedi"); show_toast_with_signal(notifications, NotificationLevel::Error, "Torrent eklenemedi");
set_error_msg.set(Some(format!("Error {}: {}", status, text))); set_error_msg.set(Some(format!("Error {}: {}", status, text)));
set_loading.set(false); set_loading.set(false);
} }
} }
Err(e) => { Err(e) => {
logging::error!("Network error: {}", e); logging::error!("Network error: {}", e);
toast_error("Bağlantı hatası"); show_toast_with_signal(notifications, NotificationLevel::Error, "Bağlantı hatası");
set_error_msg.set(Some(format!("Network Error: {}", e))); set_error_msg.set(Some(format!("Network Error: {}", e)));
set_loading.set(false); set_loading.set(false);
} }
@@ -69,7 +73,7 @@ pub fn AddTorrentModal(
} }
Err(e) => { Err(e) => {
logging::error!("Serialization error: {}", e); logging::error!("Serialization error: {}", e);
toast_error("İstek hatası"); show_toast_with_signal(notifications, NotificationLevel::Error, "İstek hatası");
set_error_msg.set(Some(format!("Request Error: {}", e))); set_error_msg.set(Some(format!("Request Error: {}", e)));
set_loading.set(false); set_loading.set(false);
} }

View File

@@ -1,7 +1,8 @@
use leptos::*; use leptos::*;
use wasm_bindgen::closure::Closure; use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use crate::store::{get_action_messages, toast_success, toast_error}; use crate::store::{get_action_messages, show_toast_with_signal};
use shared::NotificationLevel;
fn format_bytes(bytes: i64) -> String { fn format_bytes(bytes: i64) -> String {
const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"]; const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"];
@@ -186,6 +187,9 @@ pub fn TorrentTable() -> impl IntoView {
let (success_msg, error_msg) = get_action_messages(&action); let (success_msg, error_msg) = get_action_messages(&action);
let success_msg = success_msg.to_string(); let success_msg = success_msg.to_string();
let error_msg = error_msg.to_string(); let error_msg = error_msg.to_string();
// Capture notifications signal before async (use_context unavailable in spawn_local)
let notifications = store.notifications;
spawn_local(async move { spawn_local(async move {
let action_req = if action == "delete_with_data" { let action_req = if action == "delete_with_data" {
@@ -210,20 +214,20 @@ pub fn TorrentTable() -> impl IntoView {
resp.status(), resp.status(),
resp.status_text() resp.status_text()
); );
toast_error(error_msg); show_toast_with_signal(notifications, NotificationLevel::Error, error_msg);
} else { } else {
logging::log!("Action {} executed successfully", action); logging::log!("Action {} executed successfully", action);
toast_success(success_msg); show_toast_with_signal(notifications, NotificationLevel::Success, success_msg);
} }
} }
Err(e) => { Err(e) => {
logging::error!("Network error executing action: {}", e); logging::error!("Network error executing action: {}", e);
toast_error(format!("{}: Bağlantı hatası", error_msg)); show_toast_with_signal(notifications, NotificationLevel::Error, format!("{}: Bağlantı hatası", error_msg));
} }
}, },
Err(e) => { Err(e) => {
logging::error!("Failed to serialize request: {}", e); logging::error!("Failed to serialize request: {}", e);
toast_error(error_msg); show_toast_with_signal(notifications, NotificationLevel::Error, error_msg);
} }
} }
}); });

View File

@@ -13,46 +13,57 @@ pub struct NotificationItem {
// Toast Helper Functions (Clean Code: Single Responsibility) // Toast Helper Functions (Clean Code: Single Responsibility)
// ============================================================================ // ============================================================================
/// Shows a toast notification using a direct signal reference.
/// Use this version inside async blocks (spawn_local) where use_context is unavailable.
/// Auto-removes after 5 seconds.
pub fn show_toast_with_signal(
notifications: RwSignal<Vec<NotificationItem>>,
level: NotificationLevel,
message: impl Into<String>,
) {
let id = js_sys::Date::now() as u64;
let notification = SystemNotification {
level,
message: message.into(),
};
let item = NotificationItem { id, notification };
notifications.update(|list| list.push(item));
// Auto-remove after 5 seconds
let _ = set_timeout(
move || {
notifications.update(|list| list.retain(|i| i.id != id));
},
std::time::Duration::from_secs(5),
);
}
/// Shows a toast notification with the given level and message. /// Shows a toast notification with the given level and message.
/// Only works within reactive scope (components, effects). For async, use show_toast_with_signal.
/// Auto-removes after 5 seconds. /// Auto-removes after 5 seconds.
pub fn show_toast(level: NotificationLevel, message: impl Into<String>) { pub fn show_toast(level: NotificationLevel, message: impl Into<String>) {
if let Some(store) = use_context::<TorrentStore>() { if let Some(store) = use_context::<TorrentStore>() {
let id = js_sys::Date::now() as u64; show_toast_with_signal(store.notifications, level, message);
let notification = SystemNotification {
level,
message: message.into(),
};
let item = NotificationItem { id, notification };
store.notifications.update(|list| list.push(item));
// Auto-remove after 5 seconds
let notifications = store.notifications;
let _ = set_timeout(
move || {
notifications.update(|list| list.retain(|i| i.id != id));
},
std::time::Duration::from_secs(5),
);
} }
} }
/// Convenience function for success toasts /// Convenience function for success toasts (reactive scope only)
pub fn toast_success(message: impl Into<String>) { pub fn toast_success(message: impl Into<String>) {
show_toast(NotificationLevel::Success, message); show_toast(NotificationLevel::Success, message);
} }
/// Convenience function for error toasts /// Convenience function for error toasts (reactive scope only)
pub fn toast_error(message: impl Into<String>) { pub fn toast_error(message: impl Into<String>) {
show_toast(NotificationLevel::Error, message); show_toast(NotificationLevel::Error, message);
} }
/// Convenience function for info toasts /// Convenience function for info toasts (reactive scope only)
pub fn toast_info(message: impl Into<String>) { pub fn toast_info(message: impl Into<String>) {
show_toast(NotificationLevel::Info, message); show_toast(NotificationLevel::Info, message);
} }
/// Convenience function for warning toasts /// Convenience function for warning toasts (reactive scope only)
pub fn toast_warning(message: impl Into<String>) { pub fn toast_warning(message: impl Into<String>) {
show_toast(NotificationLevel::Warning, message); show_toast(NotificationLevel::Warning, message);
} }