Skip to main content

tablo_core/
page.rs

1//! [`Page`] — a panel page that is not a resource: a dashboard, a report, a
2//! settings screen, a media library.
3
4use std::future::Future;
5
6use topcoat::{Result, context::Cx, view::View};
7
8use crate::{
9    naming::{kebab_case, sentence_case, type_stem},
10    navigation::NavigationItem,
11};
12
13/// A panel page that is not a resource, mounted at `{prefix}/{slug}` with a
14/// sidebar entry; the panel owns the route and entry while the page owns its
15/// markup.
16///
17/// ```text
18/// struct ReportsPage;
19///
20/// impl Page for ReportsPage {
21///     async fn render(cx: &Cx) -> Result<impl View> {
22///         Ok(view! { cx => tablo_ui::page(tablo_ui::page_title("Reports")) })
23///     }
24/// }
25///
26/// Panel::new("admin").page::<ReportsPage>() // GET /admin/reports
27/// ```
28///
29/// A page serves one `GET` with a sidebar entry; a form it renders posts to an
30/// app-declared route the auth gate covers.
31pub trait Page: Sized + Send + Sync + 'static {
32    /// The URL segment under the panel prefix. Default: the type name without
33    /// a `Page` suffix, kebab-cased (`MediaLibraryPage` → `media-library`).
34    ///
35    /// [`Panel::home`](crate::Panel::home) mounts the page at the prefix and
36    /// does not read it.
37    fn slug() -> String {
38        kebab_case(type_stem::<Self>("Page"))
39    }
40
41    /// The sidebar label. Default: the type name without a `Page` suffix, in
42    /// sentence case (`MediaLibraryPage` → `Media library`).
43    fn navigation_label() -> String {
44        sentence_case(type_stem::<Self>("Page"))
45    }
46
47    /// The sidebar entry; override to set the `order` or the icon.
48    fn navigation() -> NavigationItem {
49        NavigationItem::for_page::<Self>()
50    }
51
52    /// Render the page body. The panel checks for a resolved user before
53    /// calling it.
54    fn render(cx: &Cx) -> impl Future<Output = Result<impl View>> + Send;
55}
56
57impl NavigationItem {
58    /// The default sidebar entry for the [`Page`] `P`.
59    pub fn for_page<P: Page>() -> Self {
60        Self {
61            label: P::navigation_label(),
62            ..Self::default()
63        }
64    }
65}
66
67#[cfg(test)]
68mod tests;