use std::{any::TypeId, sync::Arc};
use topcoat::{
context::Cx,
router::{
Body, RouteFuture,
error::{forbidden, not_found},
path_param_segment,
},
view::{BoxView, ViewExt, view},
};
use super::actions::{input_options, run_header};
use crate::{
FormInput, FormValue, FormValues, HeaderAction, Page, page::MountedPage,
topcoat_compat::async_page,
};
pub(crate) fn mounted_page<P: Page>(cx: &Cx) -> Option<&Arc<MountedPage>> {
mounted(cx, TypeId::of::<P>())
}
fn mounted(cx: &Cx, page: TypeId) -> Option<&Arc<MountedPage>> {
super::state::current(cx)?.mounted_pages.get(&page)
}
pub(super) fn mounted_admitted(cx: &Cx, page: TypeId) -> Option<&Arc<MountedPage>> {
mounted(cx, page).filter(|page| crate::auth::guard(cx).is_ok() && page.admits(cx))
}
pub(super) fn declared_body(cx: &Cx, page: TypeId) -> BoxView<'_> {
let Some(mounted) = mounted_admitted(cx, page) else {
return ().boxed();
};
let title = mounted.title.localize(cx);
let description = mounted
.description
.as_ref()
.map(|description| description.localize(cx));
let buttons = header_buttons(cx, page, mounted);
let filters = filter_bar(cx, page, mounted);
let grid = super::widgets::page_grid(cx, mounted);
view! {
tablo_ui::page(
tablo_ui::page_header(
tablo_ui::page_title((title))
if let Some(description) = description {
tablo_ui::page_description((description))
}
tablo_ui::page_actions((buttons))
)
if let Some(filters) = filters {
(filters)
}
tablo_ui::page_content((grid))
)
}
.boxed()
}
fn filter_bar<'a>(cx: &'a Cx, page: TypeId, mounted: &MountedPage) -> Option<BoxView<'a>> {
let url = super::state::current(cx)?.urls.get(&page)?;
let held = crate::page::filters::read(&mounted.filters, &crate::table::request_query(cx));
super::page_filters::bar(cx, &mounted.filters, &held, url)
}
pub fn page_filter_bar<P: Page>(cx: &Cx) -> BoxView<'_> {
let page = TypeId::of::<P>();
mounted_admitted(cx, page)
.and_then(|mounted| filter_bar(cx, page, mounted))
.unwrap_or_else(|| ().boxed())
}
pub(crate) fn current_page(cx: &Cx) -> Option<&Arc<MountedPage>> {
current_entry(cx).and_then(|(page, _)| mounted_admitted(cx, page))
}
fn current_entry(cx: &Cx) -> Option<(TypeId, &str)> {
let panel = super::state::current(cx)?;
let path = topcoat::router::request::uri(cx)
.path()
.trim_end_matches('/');
panel
.urls
.iter()
.find(|(page, url)| {
url.trim_end_matches('/') == path && panel.mounted_pages.contains_key(*page)
})
.map(|(page, url)| (*page, url.as_str()))
}
pub(super) fn current_header_bar(cx: &Cx) -> super::bar::ActionBar {
current_entry(cx)
.and_then(|(page, url)| {
let mounted = mounted_admitted(cx, page)?;
Some(super::header_bar(
cx,
url,
&mounted.header_actions,
|_| true,
&[],
))
})
.unwrap_or_default()
}
fn header_buttons<'a>(cx: &'a Cx, page: TypeId, mounted: &MountedPage) -> BoxView<'a> {
let Some(url) = super::state::current(cx).and_then(|panel| panel.urls.get(&page)) else {
return ().boxed();
};
super::header_bar(cx, url, &mounted.header_actions, |_| true, &[]).render(cx)
}
pub fn header_action_buttons<P: Page>(cx: &Cx) -> BoxView<'_> {
let page = TypeId::of::<P>();
match mounted_admitted(cx, page) {
Some(mounted) => header_buttons(cx, page, mounted),
None => ().boxed(),
}
}
pub fn header_action_button<'a, P: Page, A: HeaderAction>(
cx: &'a Cx,
input: &A::Input,
) -> BoxView<'a> {
let mut values = FormValues::new();
input.write(&mut values);
preset_button(cx, TypeId::of::<P>(), A::NAME, values)
}
fn preset_button<'a>(
cx: &'a Cx,
page: TypeId,
name: &'static str,
values: FormValues,
) -> BoxView<'a> {
let mut pairs = Vec::new();
for (key, value) in values {
match value {
FormValue::Text(text) => pairs.push((key, text)),
FormValue::List(items) => {
pairs.extend(items.into_iter().map(|item| (key.clone(), item)))
}
FormValue::Rows(_) => {
tracing::error!(
action = name,
key,
"a preset input cannot post a repeater's rows"
);
return ().boxed();
}
}
}
let Some(mounted) = mounted_admitted(cx, page) else {
return ().boxed();
};
let (Some(action), Some(url)) = (
mounted.header_actions.find(name),
super::state::current(cx).and_then(|panel| panel.urls.get(&page)),
) else {
return ().boxed();
};
super::bar::preset_button(cx, url, action, pairs).render(cx)
}
fn admitted<P: Page>(cx: &Cx) -> topcoat::Result<&Arc<MountedPage>> {
crate::auth::guard(cx)?;
let page = mounted_page::<P>(cx).ok_or_else(not_found)?;
if !page.admits(cx) {
return Err(forbidden().into());
}
Ok(page)
}
pub(super) fn page_handler<P: Page>(cx: &Cx, _body: Body) -> BoxView<'_> {
async_page(async move {
admitted::<P>(cx)?;
P::render(cx).await
})
}
pub(super) fn page_action<P: Page>(cx: &Cx, body: Body) -> BoxView<'_> {
async_page(async move {
let page = admitted::<P>(cx)?;
let Some(action) = page
.header_actions
.find(path_param_segment(cx, "action"))
.copied()
else {
return Err(not_found().into());
};
if !(action.can_run)(cx) {
return Err(forbidden().into());
}
let Some(home) = crate::panel::url::page::<P>(cx) else {
return Err(not_found().into());
};
run_header(cx, body, action, &home, None).await
})
}
pub(super) fn page_action_options<P: Page>(cx: &Cx, _body: Body) -> RouteFuture<'_> {
Box::pin(async move {
let page = admitted::<P>(cx)?;
let Some(action) = page.header_actions.find(path_param_segment(cx, "action")) else {
return Err(not_found().into());
};
if !(action.can_run)(cx) {
return Err(forbidden().into());
}
input_options(cx, action.input).await
})
}
#[cfg(test)]
mod tests;