use std::collections::HashMap;
use axum::extract::{Path, State};
use minijinja::context;
use umbral::web::{HeaderMap, IntoResponse, Redirect, Response, StatusCode};
use umbral::orm::DynQuerySet;
use crate::AdminState;
use crate::auth::require_staff;
use crate::discovery::{find_model, pk_column, user_theme};
use crate::engine::render;
use crate::error::AdminError;
use crate::handlers::sheet::edit_sheet_handler;
use crate::rows::{fetch_rows_filtered, insert_row_in_tx, update_row_in_tx};
use crate::util::{
apply_write_error_to_fields, is_htmx, parse_unique_violation_column, sanitise_form_error,
};
use crate::view::{
form_fields_for, form_m2m_fields_for, model_for_template, sidebar_apps, validate_form,
};
pub(crate) async fn body_to_pairs(
content_type: &str,
body: axum::body::Bytes,
) -> Result<Vec<(String, String)>, AdminError> {
if umbral::web::is_multipart(content_type) {
umbral::web::parse_and_store_multipart(content_type, body)
.await
.map_err(|e| AdminError::BadInput(e.to_string()))
} else {
let s = String::from_utf8(body.to_vec())
.map_err(|_| AdminError::BadInput("request body was not valid UTF-8".to_string()))?;
serde_urlencoded::from_str(&s).map_err(|e| AdminError::BadInput(e.to_string()))
}
}
pub(crate) async fn apply_m2m_selections_pub(
parent: &umbral::migrate::ModelMeta,
parent_pk_str: &str,
multi_form: &[(String, String)],
) -> Result<(), sqlx::Error> {
apply_m2m_selections(parent, parent_pk_str, multi_form).await
}
async fn apply_m2m_selections(
parent: &umbral::migrate::ModelMeta,
parent_pk_str: &str,
multi_form: &[(String, String)],
) -> Result<(), sqlx::Error> {
if parent.m2m_relations.is_empty() {
return Ok(());
}
let Some(parent_pk_col) = parent.fields.iter().find(|c| c.primary_key) else {
return Ok(());
};
let parent_value = match umbral::orm::write::json_to_sea_value(
parent_pk_col.ty,
&serde_json::Value::String(parent_pk_str.to_string()),
false,
&parent_pk_col.name,
None,
) {
Ok(v) => v,
Err(_) => return Ok(()),
};
for rel in &parent.m2m_relations {
let field_key = format!("m2m_{}", rel.field_name);
let raw_child_values: Vec<&str> = multi_form
.iter()
.filter_map(|(k, v)| {
if k == &field_key {
Some(v.as_str())
} else {
None
}
})
.filter(|v| !v.is_empty())
.collect();
let Some(target) = umbral::migrate::registered_models()
.into_iter()
.find(|m| m.table == rel.target_table)
else {
continue;
};
let Some(child_pk_col) = target.fields.iter().find(|c| c.primary_key) else {
continue;
};
let mut child_values = Vec::with_capacity(raw_child_values.len());
for raw in raw_child_values {
match umbral::orm::write::json_to_sea_value(
child_pk_col.ty,
&serde_json::Value::String(raw.to_string()),
false,
&child_pk_col.name,
None,
) {
Ok(v) => child_values.push(v),
Err(_) => continue,
}
}
let junction_table = format!("{}_{}", parent.table, rel.field_name);
umbral::orm::set_junction_dynamic(
&junction_table,
parent_value.clone(),
child_values,
Some(parent.name.as_str()),
)
.await?;
}
Ok(())
}
pub(crate) async fn detail(
State(state): State<AdminState>,
headers: HeaderMap,
Path((table, id)): Path<(String, String)>,
) -> Response {
let path = format!("{}/{table}/{id}", crate::branding::current().base_path);
let user = match require_staff(&headers, &path).await {
Ok(u) => u,
Err(r) => return r,
};
let Some((plugin_name, model)) = find_model(&table) else {
return AdminError::NotFound(format!("no model with table `{table}`")).into_response();
};
if let Err(r) =
crate::permcheck::require(&user, &plugin_name, &table, crate::permcheck::Action::View).await
{
return r;
}
let perms = crate::permcheck::AdminPerms::load(&user, &plugin_name, &table).await;
let Some(pk) = pk_column(&model) else {
return AdminError::Render(format!("model `{table}` has no primary key")).into_response();
};
let all_cols: Vec<String> = model.fields.iter().map(|f| f.name.clone()).collect();
let rows = match fetch_rows_filtered(&model, Some((&pk.name, &id)), &all_cols).await {
Ok(r) => r,
Err(e) => return e.into_response(),
};
let Some(row) = rows.into_iter().next() else {
return AdminError::NotFound(format!("no row with {} = {}", pk.name, id)).into_response();
};
let apps = sidebar_apps(&state, &user).await;
let breadcrumbs = vec![
serde_json::json!({ "label": model.name.clone(), "url": format!("{}/{table}/", crate::branding::current().base_path) }),
serde_json::json!({ "label": format!("#{id}"), "url": format!("{}/{table}/{id}", crate::branding::current().base_path) }),
];
let initial_theme = user_theme(&user).await;
match render(
"admin/detail.html",
context!(
user => user.username.clone(),
model => model_for_template(&model),
row => row,
pk => pk.name.clone(),
apps => apps,
active_table => table,
breadcrumbs => breadcrumbs,
initial_theme => initial_theme,
perms => perms,
),
) {
Ok(html) => html.into_response(),
Err(e) => e.into_response(),
}
}
pub(crate) async fn new_form(
State(state): State<AdminState>,
headers: HeaderMap,
Path(table): Path<String>,
) -> Response {
let path = format!("{}/{table}/new", crate::branding::current().base_path);
let user = match require_staff(&headers, &path).await {
Ok(u) => u,
Err(r) => return r,
};
let Some((plugin_name, model)) = find_model(&table) else {
return AdminError::NotFound(format!("no model with table `{table}`")).into_response();
};
if let Err(r) =
crate::permcheck::require(&user, &plugin_name, &table, crate::permcheck::Action::Add).await
{
return r;
}
let perms = crate::permcheck::AdminPerms::load(&user, &plugin_name, &table).await;
let cfg = state.config_for(&table);
let fields = form_fields_for(&model, None, cfg);
let m2m_fields = form_m2m_fields_for(&model, None).await;
let inlines = match crate::inlines::build_inline_views(&model, None, cfg).await {
Ok(v) => v,
Err(e) => return e.into_response(),
};
let apps = sidebar_apps(&state, &user).await;
let breadcrumbs = vec![
serde_json::json!({ "label": model.name.clone(), "url": format!("{}/{table}/", crate::branding::current().base_path) }),
serde_json::json!({ "label": "Add", "url": format!("{}/{table}/new", crate::branding::current().base_path) }),
];
let initial_theme = user_theme(&user).await;
match render(
"admin/form.html",
context!(
user => user.username.clone(),
model => model_for_template(&model),
fields => fields,
m2m_fields => m2m_fields,
inlines => inlines,
verb => "Create",
action => format!("{}/{}/new", crate::branding::current().base_path, model.table),
error => "",
apps => apps,
active_table => table,
breadcrumbs => breadcrumbs,
initial_theme => initial_theme,
perms => perms,
),
) {
Ok(html) => html.into_response(),
Err(e) => e.into_response(),
}
}
pub(crate) async fn create(
State(state): State<AdminState>,
headers: HeaderMap,
Path(table): Path<String>,
body: axum::body::Bytes,
) -> Response {
let path = format!("{}/{table}/new", crate::branding::current().base_path);
let user = match require_staff(&headers, &path).await {
Ok(u) => u,
Err(r) => return r,
};
let Some((plugin_name, model)) = find_model(&table) else {
return AdminError::NotFound(format!("no model with table `{table}`")).into_response();
};
if let Err(r) =
crate::permcheck::require(&user, &plugin_name, &table, crate::permcheck::Action::Add).await
{
return r;
}
let content_type = headers
.get(umbral::web::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let pairs = match body_to_pairs(content_type, body).await {
Ok(p) => p,
Err(e) => return e.into_response(),
};
let form: HashMap<String, String> = pairs.iter().cloned().collect();
let multi_form: Vec<(String, String)> = pairs;
let cfg = state.config_for(&table);
let field_errors = validate_form(&model, &form, cfg);
if !field_errors.is_empty() {
let mut fields = form_fields_for(&model, Some(&form), cfg);
for f in &mut fields {
if let Some(m) = field_errors.get(&f.name) {
f.error = m.clone();
}
}
let m2m_fields = form_m2m_fields_for(&model, None).await;
let inlines = crate::inlines::build_inline_views_from_submitted(&model, cfg, &multi_form);
let apps = sidebar_apps(&state, &user).await;
let breadcrumbs = vec![
serde_json::json!({ "label": model.name.clone(), "url": format!("{}/{table}/", crate::branding::current().base_path) }),
serde_json::json!({ "label": "Add", "url": format!("{}/{table}/new", crate::branding::current().base_path) }),
];
let initial_theme = user_theme(&user).await;
return match render(
"admin/form.html",
context!(
user => user.username.clone(),
model => model_for_template(&model),
fields => fields,
m2m_fields => m2m_fields,
inlines => inlines,
verb => "Create",
action => format!("{}/{}/new", crate::branding::current().base_path, model.table),
error => "",
apps => apps,
active_table => table,
breadcrumbs => breadcrumbs,
initial_theme => initial_theme,
),
) {
Ok(html) => (StatusCode::BAD_REQUEST, html).into_response(),
Err(e2) => e2.into_response(),
};
}
let saved =
save_parent_and_inlines(&model, None, &form, cfg, &multi_form, user.is_superuser).await;
match saved {
Ok(new_pk) => {
if let Err(e) = apply_m2m_selections(&model, &new_pk, &multi_form).await {
return AdminError::Sqlx(e).into_response();
}
crate::models::log(
user.id,
"create",
&table,
None,
&format!("created {} (via form)", model.name),
)
.await;
Redirect::to(&format!(
"{}/{}/",
crate::branding::current().base_path,
model.table
))
.into_response()
}
Err(e) => {
let mut fields = form_fields_for(&model, Some(&form), cfg);
let banner_error = match &e {
AdminError::Write(we) => {
sanitise_form_error(&e); apply_write_error_to_fields(we, &mut fields)
}
AdminError::Sqlx(sqlx_err) => {
let msg = sqlx_err.to_string();
if let Some(col) = parse_unique_violation_column(&msg) {
if let Some(f) = fields.iter_mut().find(|f| f.name == col) {
f.error = format!("A record with this `{col}` already exists.");
String::new()
} else {
sanitise_form_error(&e)
}
} else {
sanitise_form_error(&e)
}
}
AdminError::BadInput(msg) => msg.clone(),
_ => sanitise_form_error(&e),
};
let m2m_fields = form_m2m_fields_for(&model, None).await;
let inlines =
crate::inlines::build_inline_views_from_submitted(&model, cfg, &multi_form);
let apps = sidebar_apps(&state, &user).await;
let breadcrumbs = vec![
serde_json::json!({ "label": model.name.clone(), "url": format!("{}/{table}/", crate::branding::current().base_path) }),
serde_json::json!({ "label": "Add", "url": format!("{}/{table}/new", crate::branding::current().base_path) }),
];
let initial_theme = user_theme(&user).await;
match render(
"admin/form.html",
context!(
user => user.username.clone(),
model => model_for_template(&model),
fields => fields,
m2m_fields => m2m_fields,
inlines => inlines,
verb => "Create",
action => format!("{}/{}/new", crate::branding::current().base_path, model.table),
error => banner_error,
apps => apps,
active_table => table,
breadcrumbs => breadcrumbs,
initial_theme => initial_theme,
),
) {
Ok(html) => (StatusCode::BAD_REQUEST, html).into_response(),
Err(e2) => e2.into_response(),
}
}
}
}
pub(crate) async fn create_parent_and_inlines_pub(
model: &umbral::migrate::ModelMeta,
form: &HashMap<String, String>,
cfg: Option<&crate::config::AdminConfig>,
multi_form: &[(String, String)],
authorize_privileged: bool,
) -> Result<String, AdminError> {
save_parent_and_inlines(model, None, form, cfg, multi_form, authorize_privileged).await
}
async fn save_parent_and_inlines(
model: &umbral::migrate::ModelMeta,
existing_pk: Option<(&umbral::migrate::Column, &str)>,
form: &HashMap<String, String>,
cfg: Option<&crate::config::AdminConfig>,
multi_form: &[(String, String)],
authorize_privileged: bool,
) -> Result<String, AdminError> {
let mut tx = umbral::db::begin().await.map_err(AdminError::Sqlx)?;
let parent_pk = match existing_pk {
Some((pk, pk_value)) => {
update_row_in_tx(
&mut tx,
model,
pk,
pk_value,
form,
cfg,
authorize_privileged,
)
.await?;
pk_value.to_string()
}
None => insert_row_in_tx(&mut tx, model, form, cfg, authorize_privileged).await?,
};
crate::inlines::save_inlines_in_tx(&mut tx, model, &parent_pk, cfg, multi_form).await?;
tx.commit().await.map_err(AdminError::Sqlx)?;
Ok(parent_pk)
}
pub(crate) async fn edit_form(
State(state): State<AdminState>,
headers: HeaderMap,
Path((table, id)): Path<(String, String)>,
) -> Response {
let path = format!("{}/{table}/{id}/edit", crate::branding::current().base_path);
let user = match require_staff(&headers, &path).await {
Ok(u) => u,
Err(r) => return r,
};
let Some((plugin_name, model)) = find_model(&table) else {
return AdminError::NotFound(format!("no model with table `{table}`")).into_response();
};
if let Err(r) = crate::permcheck::require(
&user,
&plugin_name,
&table,
crate::permcheck::Action::Change,
)
.await
{
return r;
}
let perms = crate::permcheck::AdminPerms::load(&user, &plugin_name, &table).await;
let Some(pk) = pk_column(&model) else {
return AdminError::Render(format!("model `{table}` has no primary key")).into_response();
};
let all_cols: Vec<String> = model.fields.iter().map(|f| f.name.clone()).collect();
let rows = match fetch_rows_filtered(&model, Some((&pk.name, &id)), &all_cols).await {
Ok(r) => r,
Err(e) => return e.into_response(),
};
let Some(row) = rows.into_iter().next() else {
return AdminError::NotFound(format!("no row with {} = {}", pk.name, id)).into_response();
};
let row_strings: HashMap<String, String> =
row.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
let cfg = state.config_for(&table);
let fields = form_fields_for(&model, Some(&row_strings), cfg);
let m2m_fields = form_m2m_fields_for(&model, Some(&id)).await;
let inlines = match crate::inlines::build_inline_views(&model, Some(&id), cfg).await {
Ok(v) => v,
Err(e) => return e.into_response(),
};
let apps = sidebar_apps(&state, &user).await;
let breadcrumbs = vec![
serde_json::json!({ "label": model.name.clone(), "url": format!("{}/{table}/", crate::branding::current().base_path) }),
serde_json::json!({ "label": format!("#{id}"), "url": format!("{}/{table}/{id}", crate::branding::current().base_path) }),
serde_json::json!({ "label": "Edit", "url": format!("{}/{table}/{id}/edit", crate::branding::current().base_path) }),
];
let initial_theme = user_theme(&user).await;
match render(
"admin/form.html",
context!(
user => user.username.clone(),
model => model_for_template(&model),
fields => fields,
m2m_fields => m2m_fields,
inlines => inlines,
verb => "Edit",
action => format!("{}/{}/{}/edit", crate::branding::current().base_path, model.table, id),
row => row,
pk => pk.name.clone(),
error => "",
apps => apps,
active_table => table,
breadcrumbs => breadcrumbs,
initial_theme => initial_theme,
perms => perms,
),
) {
Ok(html) => html.into_response(),
Err(e) => e.into_response(),
}
}
pub(crate) async fn update(
State(state): State<AdminState>,
headers: HeaderMap,
Path((table, id)): Path<(String, String)>,
body: axum::body::Bytes,
) -> Response {
let path = format!("{}/{table}/{id}/edit", crate::branding::current().base_path);
let user = match require_staff(&headers, &path).await {
Ok(u) => u,
Err(r) => return r,
};
let Some((plugin_name, model)) = find_model(&table) else {
return AdminError::NotFound(format!("no model with table `{table}`")).into_response();
};
if let Err(r) = crate::permcheck::require(
&user,
&plugin_name,
&table,
crate::permcheck::Action::Change,
)
.await
{
return r;
}
let Some(pk) = pk_column(&model) else {
return AdminError::Render(format!("model `{table}` has no primary key")).into_response();
};
let content_type = headers
.get(umbral::web::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let pairs = match body_to_pairs(content_type, body).await {
Ok(p) => p,
Err(e) => return e.into_response(),
};
let form: HashMap<String, String> = pairs.iter().cloned().collect();
let multi_form: Vec<(String, String)> = pairs;
let cfg = state.config_for(&table);
let field_errors = validate_form(&model, &form, cfg);
if !field_errors.is_empty() {
let mut fields = form_fields_for(&model, Some(&form), cfg);
for f in &mut fields {
if let Some(m) = field_errors.get(&f.name) {
f.error = m.clone();
}
}
let m2m_fields = form_m2m_fields_for(&model, Some(&id)).await;
let inlines = crate::inlines::build_inline_views_from_submitted(&model, cfg, &multi_form);
if is_htmx(&headers) {
let password_field = cfg.and_then(|c| c.password_field.as_deref()).unwrap_or("");
return match render(
"admin/sheet_edit.html",
context!(
model => model_for_template(&model),
instance_id => id,
fields => fields,
m2m_fields => m2m_fields,
inlines => inlines,
error => "",
password_field => password_field,
),
) {
Ok(html) => (StatusCode::BAD_REQUEST, html).into_response(),
Err(e2) => e2.into_response(),
};
}
let apps = sidebar_apps(&state, &user).await;
let breadcrumbs = vec![
serde_json::json!({ "label": model.name.clone(), "url": format!("{}/{table}/", crate::branding::current().base_path) }),
serde_json::json!({ "label": format!("#{id}"), "url": format!("{}/{table}/{id}", crate::branding::current().base_path) }),
serde_json::json!({ "label": "Edit", "url": format!("{}/{table}/{id}/edit", crate::branding::current().base_path) }),
];
let initial_theme = user_theme(&user).await;
return match render(
"admin/form.html",
context!(
user => user.username.clone(),
model => model_for_template(&model),
fields => fields,
m2m_fields => m2m_fields,
inlines => inlines,
verb => "Edit",
action => format!("{}/{}/{}/edit", crate::branding::current().base_path, model.table, id),
error => "",
apps => apps,
active_table => table,
breadcrumbs => breadcrumbs,
initial_theme => initial_theme,
),
) {
Ok(html) => (StatusCode::BAD_REQUEST, html).into_response(),
Err(e2) => e2.into_response(),
};
}
let saved = save_parent_and_inlines(
&model,
Some((pk, &id)),
&form,
cfg,
&multi_form,
user.is_superuser,
)
.await;
match saved {
Ok(_) => {
if let Err(e) = apply_m2m_selections(&model, &id, &multi_form).await {
return AdminError::Sqlx(e).into_response();
}
let object_id = Some(id.clone());
crate::models::log(
user.id,
"update",
&table,
object_id,
&format!("updated {} #{}", model.name, id),
)
.await;
if is_htmx(&headers) {
if form.contains_key("_save_continue") {
let mut resp =
edit_sheet_handler(State(state), headers, Path((table, id))).await;
let trigger = serde_json::json!({
"refreshTable": {},
"showToast": {
"message": format!("{} saved", model.name),
"level": "success"
},
});
if let Ok(hv) = trigger.to_string().parse() {
resp.headers_mut().insert("HX-Trigger", hv);
}
return resp;
}
let trigger = serde_json::json!({
"closeSheet": {},
"refreshTable": {},
"showToast": {
"message": format!("{} updated", model.name),
"level": "success"
},
});
let mut resp = axum::response::Response::builder()
.status(StatusCode::OK)
.header("HX-Trigger", trigger.to_string())
.body(axum::body::Body::empty())
.unwrap();
resp.headers_mut()
.insert("Content-Type", "text/html; charset=utf-8".parse().unwrap());
return resp;
}
Redirect::to(&format!(
"{}/{}/{}",
crate::branding::current().base_path,
model.table,
id
))
.into_response()
}
Err(e) => {
let mut fields = form_fields_for(&model, Some(&form), cfg);
let banner_error = match &e {
AdminError::Write(we) => {
sanitise_form_error(&e); apply_write_error_to_fields(we, &mut fields)
}
AdminError::Sqlx(sqlx_err) => {
let msg = sqlx_err.to_string();
if let Some(col) = parse_unique_violation_column(&msg) {
if let Some(f) = fields.iter_mut().find(|f| f.name == col) {
f.error = format!("A record with this `{col}` already exists.");
String::new()
} else {
sanitise_form_error(&e)
}
} else {
sanitise_form_error(&e)
}
}
AdminError::BadInput(msg) => msg.clone(),
_ => sanitise_form_error(&e),
};
let m2m_fields = form_m2m_fields_for(&model, Some(&id)).await;
let inlines =
crate::inlines::build_inline_views_from_submitted(&model, cfg, &multi_form);
if is_htmx(&headers) {
let password_field = cfg.and_then(|c| c.password_field.as_deref()).unwrap_or("");
return match render(
"admin/sheet_edit.html",
context!(
model => model_for_template(&model),
instance_id => id,
fields => fields,
m2m_fields => m2m_fields,
inlines => inlines,
error => banner_error,
password_field => password_field,
),
) {
Ok(html) => (StatusCode::BAD_REQUEST, html).into_response(),
Err(e2) => e2.into_response(),
};
}
let apps = sidebar_apps(&state, &user).await;
let breadcrumbs = vec![
serde_json::json!({ "label": model.name.clone(), "url": format!("{}/{table}/", crate::branding::current().base_path) }),
serde_json::json!({ "label": format!("#{id}"), "url": format!("{}/{table}/{id}", crate::branding::current().base_path) }),
serde_json::json!({ "label": "Edit", "url": format!("{}/{table}/{id}/edit", crate::branding::current().base_path) }),
];
let initial_theme = user_theme(&user).await;
match render(
"admin/form.html",
context!(
user => user.username.clone(),
model => model_for_template(&model),
fields => fields,
m2m_fields => m2m_fields,
inlines => inlines,
verb => "Edit",
action => format!("{}/{}/{}/edit", crate::branding::current().base_path, model.table, id),
error => banner_error,
apps => apps,
active_table => table,
breadcrumbs => breadcrumbs,
initial_theme => initial_theme,
),
) {
Ok(html) => (StatusCode::BAD_REQUEST, html).into_response(),
Err(e2) => e2.into_response(),
}
}
}
}
pub(crate) async fn delete(
State(_state): State<AdminState>,
headers: HeaderMap,
Path((table, id)): Path<(String, String)>,
) -> Response {
let path = format!(
"{}/{table}/{id}/delete",
crate::branding::current().base_path
);
let who = match require_staff(&headers, &path).await {
Ok(u) => u,
Err(r) => return r,
};
let Some((plugin_name, model)) = find_model(&table) else {
return AdminError::NotFound(format!("no model with table `{table}`")).into_response();
};
if let Err(r) =
crate::permcheck::require(&who, &plugin_name, &table, crate::permcheck::Action::Delete)
.await
{
return r;
}
let Some(pk) = pk_column(&model) else {
return AdminError::Render(format!("model `{table}` has no primary key")).into_response();
};
match DynQuerySet::for_meta(&model)
.filter_eq_string(&pk.name, &id)
.delete()
.await
{
Ok(_) => {
let object_id = Some(id.clone());
crate::models::log(
who.id,
"delete",
&table,
object_id,
&format!("deleted {} #{}", model.name, id),
)
.await;
Redirect::to(&format!(
"{}/{}/",
crate::branding::current().base_path,
model.table
))
.into_response()
}
Err(e) => AdminError::from(e).into_response(),
}
}
pub(crate) async fn htmx_delete(
State(_state): State<AdminState>,
headers: HeaderMap,
Path((table, id)): Path<(String, String)>,
) -> Response {
let path = format!("{}/{table}/{id}", crate::branding::current().base_path);
let who = match require_staff(&headers, &path).await {
Ok(u) => u,
Err(r) => return r,
};
let Some((plugin_name, model)) = find_model(&table) else {
return AdminError::NotFound(format!("no model with table `{table}`")).into_response();
};
if let Err(r) =
crate::permcheck::require(&who, &plugin_name, &table, crate::permcheck::Action::Delete)
.await
{
return r;
}
let Some(pk) = pk_column(&model) else {
return AdminError::Render(format!("model `{table}` has no primary key")).into_response();
};
match DynQuerySet::for_meta(&model)
.filter_eq_string(&pk.name, &id)
.delete()
.await
{
Ok(_) => {
let object_id = Some(id.clone());
crate::models::log(
who.id,
"delete",
&table,
object_id,
&format!("deleted {} #{}", model.name, id),
)
.await;
let trigger = serde_json::json!({
"closeSheet": {},
"refreshTable": {},
"showToast": {
"message": format!("{} #{} deleted", model.name, id),
"level": "success"
},
});
let mut resp = axum::response::Response::builder()
.status(StatusCode::OK)
.header("HX-Trigger", trigger.to_string())
.body(axum::body::Body::empty())
.unwrap_or_else(|_| {
Redirect::to(&format!(
"{}/{}/",
crate::branding::current().base_path,
model.table
))
.into_response()
});
resp.headers_mut()
.insert("Content-Type", "text/html; charset=utf-8".parse().unwrap());
resp
}
Err(e) => AdminError::from(e).into_response(),
}
}