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;