tablo_core/
notification.rs1mod bell;
6pub(crate) mod stored;
7
8use tablo_ui::{
9 icons, toast, toast_close, toast_content, toast_description, toast_icon, toast_title,
10};
11use topcoat::{
12 context::Cx,
13 icon::icon,
14 runtime::{Signal, shard, signal},
15 view::{Attributes, BoxView, View, ViewExt, attributes, view},
16};
17
18pub use self::{
19 bell::{
20 clear_notifications, mark_all_notifications_read, mark_notification, notification_bell,
21 remove_notification,
22 },
23 stored::StoredNotification,
24};
25pub(crate) use self::{
26 bell::{prefetch as prefetch_bell, topbar_bell},
27 stored::Notifications,
28};
29pub use crate::flash::{set_notification, take_notification};
30pub(crate) use crate::outcome::{Notification, NotificationStatus};
31
32pub(crate) fn status_icon(status: &NotificationStatus) -> (topcoat::icon::IconData, &'static str) {
34 match status {
35 NotificationStatus::Success => (icons::CIRCLE_CHECK, "size-4"),
36 NotificationStatus::Error => (icons::OCTAGON_X, "size-4 text-destructive"),
37 NotificationStatus::Info => (icons::INFO, "size-4"),
38 NotificationStatus::Warning => (icons::TRIANGLE_ALERT, "size-4"),
39 }
40}
41
42pub async fn render_notification<'a>(
44 cx: &'a Cx,
45 notification: Notification,
46 attrs: Attributes,
47) -> topcoat::Result<BoxView<'a>> {
48 let Notification {
49 status,
50 title,
51 description,
52 actions,
53 } = notification;
54 let (icon_data, icon_class) = status_icon(&status);
55 let links: Vec<(String, String)> = actions
56 .iter()
57 .filter_map(|action| Some((action.label.clone(), action.safe_url()?.to_owned())))
58 .collect();
59 let status = status.as_str();
60 Ok(view! {
61 cx =>
62 toast(
63 attrs: attributes! { data-type=(status) (attrs) },
64 toast_icon(icon(data: icon_data, attrs: attributes! { class=(icon_class) }))
65 toast_content(
66 toast_title((title))
67 if let Some(description) = description {
68 toast_description((description))
69 }
70 if !links.is_empty() {
71 <div class="flex flex-wrap gap-3 pt-1">
72 for (label, url) in links {
73 <a
74 href=(url)
75 class="font-medium text-primary underline-offset-4 hover:underline"
76 >
77 (label)
78 </a>
79 }
80 </div>
81 }
82 )
83 toast_close(label: crate::t!(cx, "tablo-toast-close"))
84 )
85 }
86 .boxed())
87}
88
89#[derive(Clone)]
91pub struct LiveToast {
92 pub status: Signal<String>,
94 pub title: Signal<String>,
96 pub description: Signal<String>,
98 pub serial: Signal<u64>,
100}
101
102pub fn live_toast(cx: &Cx) -> LiveToast {
104 LiveToast {
105 status: signal(cx, String::new),
106 title: signal(cx, String::new),
107 description: signal(cx, String::new),
108 serial: signal(cx, || 0u64),
109 }
110}
111
112#[cfg(test)]
116pub(crate) const LIVE_TOASTER_PATH: &str = "/_topcoat/runtime/shards/tablo-live-toaster";
117
118#[shard("/_topcoat/runtime/shards/tablo-live-toaster")]
121pub async fn live_toaster(
122 cx: &Cx,
123 status: Signal<String>,
124 title: Signal<String>,
125 description: Signal<String>,
126 serial: Signal<u64>,
127) -> topcoat::Result<impl View> {
128 render_live_toaster(cx, &status, &title, &description, &serial).await
129}
130
131async fn render_live_toaster<'a>(
132 cx: &'a Cx,
133 status: &Signal<String>,
134 title: &Signal<String>,
135 description: &Signal<String>,
136 serial: &Signal<u64>,
137) -> topcoat::Result<BoxView<'a>> {
138 crate::auth::guard(cx)?;
141 let status = status.get();
142 let mount = serial.get();
143 if status.is_empty() {
144 return Ok(().boxed());
145 }
146 let title = title.get();
147 let description = description.get();
148 let notification = match status.as_str() {
149 "success" => Notification::success(title),
150 "warning" => Notification::warning(title),
151 "error" => Notification::error(title),
152 _ => Notification::info(title),
153 };
154 let notification = if description.is_empty() {
155 notification
156 } else {
157 notification.description(description)
158 };
159 let mount_id = format!("live-toast-{mount}");
160 render_notification(cx, notification, attributes! { cx => id=(mount_id) }).await
161}
162
163#[cfg(test)]
164mod tests;