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 toast(
62 attrs: attributes! { data-type=(status) (attrs) },
63 toast_icon(icon(data: icon_data, attrs: attributes! { class=(icon_class) }))
64 toast_content(
65 toast_title((title))
66 if let Some(description) = description {
67 toast_description((description))
68 }
69 if !links.is_empty() {
70 <div class="flex flex-wrap gap-3 pt-1">
71 for (label, url) in links {
72 <a
73 href=(url)
74 class="font-medium text-primary underline-offset-4 hover:underline"
75 >
76 (label)
77 </a>
78 }
79 </div>
80 }
81 )
82 toast_close(label: crate::t!(cx, "tablo-toast-close"))
83 )
84 }
85 .boxed())
86}
87
88#[derive(Clone)]
90pub struct LiveToast {
91 pub status: Signal<String>,
93 pub title: Signal<String>,
95 pub description: Signal<String>,
97 pub serial: Signal<u64>,
99}
100
101pub fn live_toast(cx: &Cx) -> LiveToast {
103 LiveToast {
104 status: signal(cx, String::new),
105 title: signal(cx, String::new),
106 description: signal(cx, String::new),
107 serial: signal(cx, || 0u64),
108 }
109}
110
111#[cfg(test)]
115pub(crate) const LIVE_TOASTER_PATH: &str = "/_topcoat/runtime/shards/tablo-live-toaster";
116
117#[shard("/_topcoat/runtime/shards/tablo-live-toaster")]
120pub async fn live_toaster(
121 cx: &Cx,
122 status: Signal<String>,
123 title: Signal<String>,
124 description: Signal<String>,
125 serial: Signal<u64>,
126) -> topcoat::Result<impl View> {
127 render_live_toaster(cx, &status, &title, &description, &serial).await
128}
129
130async fn render_live_toaster<'a>(
131 cx: &'a Cx,
132 status: &Signal<String>,
133 title: &Signal<String>,
134 description: &Signal<String>,
135 serial: &Signal<u64>,
136) -> topcoat::Result<BoxView<'a>> {
137 crate::auth::guard(cx)?;
140 let status = status.get();
141 let mount = serial.get();
142 if status.is_empty() {
143 return Ok(().boxed());
144 }
145 let title = title.get();
146 let description = description.get();
147 let notification = match status.as_str() {
148 "success" => Notification::success(title),
149 "warning" => Notification::warning(title),
150 "error" => Notification::error(title),
151 _ => Notification::info(title),
152 };
153 let notification = if description.is_empty() {
154 notification
155 } else {
156 notification.description(description)
157 };
158 let mount_id = format!("live-toast-{mount}");
159 render_notification(cx, notification, attributes! { id=(mount_id) }).await
160}
161
162#[cfg(test)]
163mod tests;