use std::collections::{HashMap, HashSet};
use topcoat::{
Result,
context::Cx,
icon::icon,
router::{Body, error::forbidden},
view::{BoxView, ViewExt, attributes, view},
};
use super::super::{
gate::{gate, return_target},
relations::render_relations,
};
use crate::{
form::FieldErrors,
policy::Ability,
resource::{Mounted, Resource},
topcoat_compat::async_page,
};
pub(super) struct FormChrome<'a> {
title: String,
submit_label: &'static str,
public_url: Option<String>,
relations: Vec<BoxView<'a>>,
}
impl<'a> FormChrome<'a> {
pub(super) fn create<R: Resource>(resource: &Mounted<R>) -> Self {
Self {
title: format!("Create {}", resource.label),
submit_label: "Create",
public_url: None,
relations: Vec::new(),
}
}
pub(super) fn edit<R: Resource>(cx: &'a Cx, resource: &Mounted<R>, record: &R::Model) -> Self {
Self {
title: format!("Edit {}", resource.label),
submit_label: "Save",
public_url: R::public_url(cx, record),
relations: render_relations(cx, resource, record, false),
}
}
}
pub(super) async fn render_form_page<'a, R: Resource>(
cx: &'a Cx,
resource: &Mounted<R>,
chrome: FormChrome<'a>,
values: &HashMap<String, String>,
errors: &FieldErrors,
carried: &HashSet<String>,
) -> Result<BoxView<'a>> {
let FormChrome {
title,
submit_label,
public_url,
relations,
} = chrome;
let schema = &resource.form;
let form_html = schema
.render(cx, crate::schema::Source::form(values, errors))
.await?;
let path = topcoat::router::request::uri(cx).path();
let return_to = return_target(cx);
let action = match &return_to {
Some(target) => crate::table::with_return(path, target),
None => path.to_string(),
};
let cancel = return_to.unwrap_or_else(|| resource.url.clone());
let enctype: Option<String> = schema
.fields()
.any(|field| field.is_file())
.then(|| "multipart/form-data".to_string());
let csrf = crate::csrf::current_token(cx);
let mut carried_fields: Vec<BoxView<'a>> = Vec::new();
let mut carried_names: Vec<&String> = carried.iter().collect();
carried_names.sort();
for name in carried_names {
let Some(path) = values.get(name) else {
continue;
};
let control = format!("keep_{name}");
let path = path.clone();
carried_fields
.push(view! { cx => <input type="hidden" name=(control) value=(path)> }.boxed());
}
Ok(view! {
cx =>
tablo_ui::page(
tablo_ui::page_header(
tablo_ui::page_title((title.clone()))
if let Some(public) = public_url {
tablo_ui::page_actions(
<a
href=(public)
class=(tablo_ui::button_variants(
tablo_ui::ButtonVariant::Outline,
tablo_ui::ButtonSize::Md,
))
>
icon(data: tablo_ui::icons::EXTERNAL_LINK)
"View public post"
</a>
)
}
)
tablo_ui::page_content(
<form
method="post"
action=(action)
enctype=(enctype)
class="flex flex-col gap-4"
>
(crate::csrf::field(cx, &csrf))
for carried in carried_fields {
(carried)
}
(form_html)
<div class="flex items-center gap-2">
tablo_ui::button(
variant: tablo_ui::ButtonVariant::Primary,
attrs: attributes! { type="submit" },
(submit_label)
)
<a
(crate::navigation::runtime_link(cx, &cancel))
class=(tablo_ui::button_variants(
tablo_ui::ButtonVariant::Outline,
tablo_ui::ButtonSize::Md,
))
>
"Cancel"
</a>
</div>
</form>
for relation in relations {
(relation)
}
)
)
}
.boxed())
}
/// Renders the create page, seeding only relationship controls from query parameters.
pub(crate) fn resource_create<R: Resource>(cx: &Cx, _body: Body) -> BoxView<'_> {
async_page(async move {
let resource = gate::<R>(cx)?;
if !resource.can(cx, Ability::Create) {
return Err(forbidden().into());
}
crate::csrf::ensure_token(cx);
let html = render_form_page(
cx,
&resource,
FormChrome::create(&resource),
&seeded_values(cx, &resource),
&FieldErrors::new(),
&HashSet::new(),
)
.await?;
Ok(html)
})
}
/// Collects relationship-control query parameters for the create form, first occurrence wins.
fn seeded_values<R: Resource>(cx: &Cx, resource: &Mounted<R>) -> HashMap<String, String> {
let schema = &resource.form;
let seedable: Vec<&str> = schema
.fields()
.filter(|field| {
field
.as_choice()
.is_some_and(|choice| choice.is_relationship())
})
.map(|field| field.name())
.collect();
let query = topcoat::router::request::uri(cx)
.query()
.unwrap_or_default();
let mut values = HashMap::new();
for (name, value) in form_urlencoded::parse(query.as_bytes()) {
if seedable.contains(&name.as_ref()) {
values
.entry(name.into_owned())
.or_insert_with(|| value.into_owned());
}
}
values
}
#[cfg(test)]
mod tests;