use toasty::stmt::Expr;
use topcoat::{
context::Cx,
icon::icon,
view::{BoxView, ViewExt, view},
};
use super::{
gate::enforce_tenant,
list::{declared_chrome, load_scoped_page, table_error_view, wire_table},
state::current,
};
use crate::{
form::RecordForm,
navigation::runtime_link,
policy::Ability,
resource::{Mounted, Resource, mounted},
table::{RETURN_PARAM, create_page_url},
topcoat_compat::async_page,
};
pub(crate) struct Child {
pub(crate) slug: String,
pub(crate) plural_label: String,
pub(crate) render: for<'a> fn(&'a Cx, BoundRelation) -> BoxView<'a>,
}
pub(crate) struct BoundRelation {
pub(crate) key: String,
pub(crate) label: String,
pub(crate) scope: Expr<bool>,
pub(crate) seed: (String, String),
pub(crate) page: String,
}
pub(crate) fn render_relations<'a, R: Resource>(
cx: &'a Cx,
resource: &Mounted<R>,
owner: &R::Model,
) -> Vec<BoxView<'a>> {
let Some(panel) = current(cx) else {
return Vec::new();
};
let page = topcoat::router::request::uri(cx).path().to_string();
resource
.relations
.iter()
.filter_map(|relation| {
let child = panel.children.get(&relation.child)?;
let (scope, value) = relation.bind(owner);
Some((child.render)(
cx,
BoundRelation {
key: child.slug.clone(),
label: relation
.label
.clone()
.unwrap_or_else(|| child.plural_label.clone()),
scope,
seed: (relation.foreign_key.clone(), value),
page: page.clone(),
},
))
})
.collect()
}
pub(crate) fn relation_table<C: Resource>(cx: &Cx, relation: BoundRelation) -> BoxView<'_> {
async_page(async move {
let Some(resource) = mounted::<C>(cx) else {
return Ok(().boxed());
};
if enforce_tenant(cx, &resource).is_err() || !resource.can(cx, Ability::ViewAny) {
return Ok(().boxed());
}
let BoundRelation {
key,
label,
scope,
seed,
page,
} = relation;
let table = wire_table(cx, &resource, declared_chrome(cx, &resource)).prefixed(&key);
let (signals, state) = table.browser_state(cx);
let table = table.returning_to(state.list_url(&page));
let create_url = (<C::Form as RecordForm>::HAS_FORM && resource.can(cx, Ability::Create))
.then(|| {
let query = form_urlencoded::Serializer::new(String::new())
.append_pair(&seed.0, &seed.1)
.append_pair(RETURN_PARAM, &state.list_url(&page))
.finish();
format!("{}?{query}", create_page_url(&resource.url))
});
let body = match load_scoped_page(cx, &resource, &table, &state, scope).await {
Ok(rows) => table.render_page(cx, rows, &state, &page, &signals).await?,
Err(error) => table_error_view(
cx,
&resource.slug,
&resource.plural_label,
&state,
&error,
&page,
),
};
Ok(relation_section(
cx,
key,
label,
&resource.label,
create_url,
body,
))
})
}
fn relation_section<'a>(
cx: &'a Cx,
key: String,
label: String,
child_label: &str,
create_url: Option<String>,
body: BoxView<'a>,
) -> BoxView<'a> {
let create_label = format!("New {child_label}");
view! {
cx =>
<section class="flex flex-col gap-3" data-relation=(key)>
<div class="flex items-center justify-between gap-4">
<h2 class="text-lg font-semibold tracking-tight text-foreground">
(label)
</h2>
if let Some(url) = create_url {
<a
(runtime_link(cx, &url))
class=(tablo_ui::button_variants(
tablo_ui::ButtonVariant::Outline,
tablo_ui::ButtonSize::Sm,
))
>
icon(data: tablo_ui::icons::PLUS)
(create_label)
</a>
}
</div>
(body)
</section>
}
.boxed()
}