mod bell;
pub(crate) mod stored;
use tablo_ui::{
icons, toast, toast_close, toast_content, toast_description, toast_icon, toast_title,
};
use topcoat::{
context::Cx,
icon::icon,
runtime::{Signal, shard, signal},
view::{Attributes, BoxView, View, ViewExt, attributes, view},
};
pub use self::{
bell::{
clear_notifications, mark_all_notifications_read, mark_notification, notification_bell,
remove_notification,
},
stored::StoredNotification,
};
pub(crate) use self::{
bell::{prefetch as prefetch_bell, topbar_bell},
stored::Notifications,
};
pub use crate::flash::{set_notification, take_notification};
pub(crate) use crate::outcome::{Notification, NotificationStatus};
pub(crate) fn status_icon(status: &NotificationStatus) -> (topcoat::icon::IconData, &'static str) {
match status {
NotificationStatus::Success => (icons::CIRCLE_CHECK, "size-4"),
NotificationStatus::Error => (icons::OCTAGON_X, "size-4 text-destructive"),
NotificationStatus::Info => (icons::INFO, "size-4"),
NotificationStatus::Warning => (icons::TRIANGLE_ALERT, "size-4"),
}
}
pub async fn render_notification<'a>(
cx: &'a Cx,
notification: Notification,
attrs: Attributes,
) -> topcoat::Result<BoxView<'a>> {
let Notification {
status,
title,
description,
actions,
} = notification;
let (icon_data, icon_class) = status_icon(&status);
let links: Vec<(String, String)> = actions
.iter()
.filter_map(|action| Some((action.label.clone(), action.safe_url()?.to_owned())))
.collect();
let status = status.as_str();
Ok(view! {
cx =>
toast(
attrs: attributes! { data-type=(status) (attrs) },
toast_icon(icon(data: icon_data, attrs: attributes! { class=(icon_class) }))
toast_content(
toast_title((title))
if let Some(description) = description {
toast_description((description))
}
if !links.is_empty() {
<div class="flex flex-wrap gap-3 pt-1">
for (label, url) in links {
<a
href=(url)
class="font-medium text-primary underline-offset-4 hover:underline"
>
(label)
</a>
}
</div>
}
)
toast_close(label: crate::t!(cx, "tablo-toast-close"))
)
}
.boxed())
}
#[derive(Clone)]
pub struct LiveToast {
pub status: Signal<String>,
pub title: Signal<String>,
pub description: Signal<String>,
pub serial: Signal<u64>,
}
pub fn live_toast(cx: &Cx) -> LiveToast {
LiveToast {
status: signal(cx, String::new),
title: signal(cx, String::new),
description: signal(cx, String::new),
serial: signal(cx, || 0u64),
}
}
#[cfg(test)]
pub(crate) const LIVE_TOASTER_PATH: &str = "/_topcoat/runtime/shards/tablo-live-toaster";
#[shard("/_topcoat/runtime/shards/tablo-live-toaster")]
pub async fn live_toaster(
cx: &Cx,
status: Signal<String>,
title: Signal<String>,
description: Signal<String>,
serial: Signal<u64>,
) -> topcoat::Result<impl View> {
render_live_toaster(cx, &status, &title, &description, &serial).await
}
async fn render_live_toaster<'a>(
cx: &'a Cx,
status: &Signal<String>,
title: &Signal<String>,
description: &Signal<String>,
serial: &Signal<u64>,
) -> topcoat::Result<BoxView<'a>> {
crate::auth::guard(cx)?;
let status = status.get();
let mount = serial.get();
if status.is_empty() {
return Ok(().boxed());
}
let title = title.get();
let description = description.get();
let notification = match status.as_str() {
"success" => Notification::success(title),
"warning" => Notification::warning(title),
"error" => Notification::error(title),
_ => Notification::info(title),
};
let notification = if description.is_empty() {
notification
} else {
notification.description(description)
};
let mount_id = format!("live-toast-{mount}");
render_notification(cx, notification, attributes! { cx => id=(mount_id) }).await
}
#[cfg(test)]
mod tests;