use std::sync::Arc;
use topcoat::{
Result,
context::Cx,
icon::icon,
router::{Body, error::forbidden, request::original_method},
view::{BoxView, SuspenseMode, ViewExt, attributes, internal::ThenView, suspense, view},
};
use super::gate::gate;
use crate::{
form::RecordForm,
policy::Ability,
resource::{Mounted, Resource},
table::{
RowActions, Table, TableAction, TableChrome, TablePage, TableState, WiredTable,
create_page_url,
},
topcoat_compat::async_page,
};
pub(crate) fn retry_url_for_error(
state: &TableState,
error: &topcoat::Error,
path: &str,
) -> String {
if crate::error::TabloError::is_cursor(error) {
state.without_cursor(path)
} else {
state.list_url(path)
}
}
pub(crate) fn declared_chrome<R: Resource>(cx: &Cx, resource: &Mounted<R>) -> TableChrome {
TableChrome {
delete: resource.can(cx, Ability::DeleteAny),
edit: <R::Form as RecordForm>::HAS_FORM,
view: resource.viewed(),
actions: true,
}
}
pub(crate) fn wire_table_actions<R: Resource>(
cx: &Cx,
resource: &Arc<Mounted<R>>,
) -> WiredTable<R::Model> {
wire_table(cx, resource, declared_chrome(cx, resource))
}
pub(crate) fn wire_table<R: Resource>(
cx: &Cx,
resource: &Arc<Mounted<R>>,
chrome: TableChrome,
) -> WiredTable<R::Model> {
let mut table = WiredTable::new(Arc::clone(&resource.table));
let (policy_cx, policy) = (cx.clone(), Arc::clone(resource));
table = table.row_actions(move |record| {
let view = policy.can(&policy_cx, Ability::View(record));
RowActions {
view,
edit: view && policy.can(&policy_cx, Ability::Update(record)),
delete: view && policy.can(&policy_cx, Ability::Delete(record)),
}
});
if chrome.delete {
table = table
.with_delete(resource.url.clone())
.with_bulk_delete(true);
}
if chrome.edit {
table = table.with_edit(resource.url.clone());
}
if chrome.view {
table = table.with_view(resource.url.clone());
}
if chrome.actions {
table = wire_custom_actions(cx, resource, table);
}
table
}
fn wire_custom_actions<R: Resource>(
cx: &Cx,
resource: &Arc<Mounted<R>>,
table: WiredTable<R::Model>,
) -> WiredTable<R::Model> {
if resource.actions.entries().is_empty() {
return table;
}
let wired = resource
.actions
.entries()
.iter()
.map(|action| {
let can_run = action.can_run;
let (policy_cx, policy) = (cx.clone(), Arc::clone(resource));
TableAction {
name: action.name,
label: (action.label)(cx),
row: action.row,
bulk: action.bulk,
confirm: action.confirm,
allowed: Arc::new(move |record: &R::Model| {
policy.can(&policy_cx, Ability::View(record)) && can_run(&policy_cx, record)
}),
}
})
.collect();
table.with_custom_actions(resource.url.clone(), wired)
}
pub(crate) fn table_error_view<'a>(
cx: &'a Cx,
slug: &str,
plural_label: &str,
state: &TableState,
error: &topcoat::Error,
path: &str,
) -> BoxView<'a> {
tracing::error!(resource = slug, error = %error, "table load failed");
let title = format!("Couldn't load {plural_label}");
let retry_url = retry_url_for_error(state, error, path);
view! {
cx =>
tablo_ui::error_state(
title: title,
detail: "Something went wrong while loading the records.",
action: Some(view! { cx => <a href=(retry_url)>"Retry"</a> }.boxed().into()),
attrs: attributes! { role="alert" }
)
}
.boxed()
}
fn list_header<'a>(
cx: &'a Cx,
plural_label: &str,
label: &str,
create_url: Option<String>,
) -> BoxView<'a> {
let title = plural_label.to_string();
let create_label = format!("Create {label}");
view! {
cx =>
tablo_ui::page_header(
tablo_ui::page_title((title))
if let Some(url) = create_url {
tablo_ui::page_actions(
<a
(crate::navigation::runtime_link(cx, &url))
class=(tablo_ui::button_variants(
tablo_ui::ButtonVariant::Primary,
tablo_ui::ButtonSize::Md,
))
>
icon(data: tablo_ui::icons::PLUS)
(create_label)
</a>
)
}
)
}
.boxed()
}
pub(crate) fn resource_list<R: Resource>(cx: &Cx, _body: Body) -> BoxView<'_> {
async_page(async move {
let resource = gate::<R>(cx)?;
if !resource.can(cx, Ability::ViewAny) {
return Err(forbidden().into());
}
crate::csrf::ensure_token(cx);
let table = wire_table_actions(cx, &resource);
let (signals, state) = table.browser_state(cx);
let back = state.without_cursor(&resource.url);
let table = if back == resource.url {
table
} else {
table.returning_to(back)
};
let skeleton = table.render_skeleton(cx, &state).await?;
let create_url = (<R::Form as RecordForm>::HAS_FORM && resource.can(cx, Ability::Create))
.then(|| create_page_url(&resource.url));
let header = list_header(cx, &resource.plural_label, &resource.label, create_url);
let mode = rerun_suspense_mode(cx);
let lazy_rows = ThenView::new(async move {
let list_path = &resource.url;
let rendered = async {
let page = load_table_page(cx, &resource, &table, &state).await?;
table
.render_page(cx, page, &state, list_path, &signals)
.await
};
match rendered.await {
Ok(view) => Ok(view),
Err(error) => Ok(table_error_view(
cx,
&resource.slug,
&resource.plural_label,
&state,
&error,
list_path,
)),
}
});
Ok(list_page(cx, header, skeleton, mode, lazy_rows.boxed()))
})
}
fn list_page<'a>(
cx: &'a Cx,
header: BoxView<'a>,
skeleton: BoxView<'a>,
mode: SuspenseMode,
rows: BoxView<'a>,
) -> BoxView<'a> {
view! {
cx =>
tablo_ui::page(
(header)
tablo_ui::page_content(suspense(fallback: skeleton, mode: mode, (rows)))
)
}
.boxed()
}
fn rerun_suspense_mode(cx: &Cx) -> SuspenseMode {
if original_method(cx) == http::Method::POST {
SuspenseMode::Wait
} else {
SuspenseMode::Stream
}
}
pub(crate) async fn load_table_page<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
table: &Table<R::Model>,
state: &TableState,
) -> Result<TablePage<R::Model>> {
TablePage::load(cx, table, resource.scoped_query(cx)?, state).await
}
pub(crate) async fn load_scoped_page<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
table: &Table<R::Model>,
state: &TableState,
scope: toasty::stmt::Expr<bool>,
) -> Result<TablePage<R::Model>> {
let query = resource.scoped_query(cx)?.filter(scope);
TablePage::load(cx, table, query, state).await
}
#[cfg(test)]
mod tests;