Switch to maud for templating
This commit is contained in:
parent
11be165f6d
commit
4a2dc721d4
121
Cargo.lock
generated
121
Cargo.lock
generated
|
@ -347,60 +347,6 @@ version = "0.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
|
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "askama"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
|
|
||||||
dependencies = [
|
|
||||||
"askama_derive",
|
|
||||||
"askama_escape",
|
|
||||||
"humansize",
|
|
||||||
"num-traits",
|
|
||||||
"percent-encoding",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "askama_actix"
|
|
||||||
version = "0.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e4b0dd17cfe203b00ba3853a89fba459ecf24c759b738b244133330607c78e55"
|
|
||||||
dependencies = [
|
|
||||||
"actix-web",
|
|
||||||
"askama",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "askama_derive"
|
|
||||||
version = "0.12.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
|
|
||||||
dependencies = [
|
|
||||||
"askama_parser",
|
|
||||||
"basic-toml",
|
|
||||||
"mime",
|
|
||||||
"mime_guess",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"serde",
|
|
||||||
"syn 2.0.68",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "askama_escape"
|
|
||||||
version = "0.10.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "askama_parser"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
|
|
||||||
dependencies = [
|
|
||||||
"nom",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.80"
|
version = "0.1.80"
|
||||||
|
@ -451,15 +397,6 @@ version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "basic-toml"
|
|
||||||
version = "0.1.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
|
@ -1082,15 +1019,6 @@ version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "humansize"
|
|
||||||
version = "2.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
|
|
||||||
dependencies = [
|
|
||||||
"libm",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "humantime"
|
name = "humantime"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
|
@ -1180,8 +1108,6 @@ version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-files",
|
"actix-files",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"askama",
|
|
||||||
"askama_actix",
|
|
||||||
"barcoders",
|
"barcoders",
|
||||||
"datamatrix",
|
"datamatrix",
|
||||||
"diesel",
|
"diesel",
|
||||||
|
@ -1190,6 +1116,7 @@ dependencies = [
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"log",
|
"log",
|
||||||
|
"maud",
|
||||||
"mime",
|
"mime",
|
||||||
"printpdf",
|
"printpdf",
|
||||||
"rust-fontconfig",
|
"rust-fontconfig",
|
||||||
|
@ -1204,12 +1131,6 @@ version = "0.2.155"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libm"
|
|
||||||
version = "0.2.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linked-hash-map"
|
name = "linked-hash-map"
|
||||||
version = "0.5.6"
|
version = "0.5.6"
|
||||||
|
@ -1266,6 +1187,30 @@ dependencies = [
|
||||||
"weezl",
|
"weezl",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "maud"
|
||||||
|
version = "0.26.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df518b75016b4289cdddffa1b01f2122f4a49802c93191f3133f6dc2472ebcaa"
|
||||||
|
dependencies = [
|
||||||
|
"actix-web",
|
||||||
|
"futures-util",
|
||||||
|
"itoa",
|
||||||
|
"maud_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "maud_macros"
|
||||||
|
version = "0.26.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa453238ec218da0af6b11fc5978d3b5c3a45ed97b722391a2a11f3306274e18"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.68",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md-5"
|
name = "md-5"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
|
@ -1325,12 +1270,6 @@ dependencies = [
|
||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minimal-lexical"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
|
@ -1362,16 +1301,6 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nom"
|
|
||||||
version = "7.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
"minimal-lexical",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
@ -12,8 +12,6 @@ license = "AGPL-3.0-or-later"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-files = "0.6.6"
|
actix-files = "0.6.6"
|
||||||
actix-web = "4.8.0"
|
actix-web = "4.8.0"
|
||||||
askama = { version = "0.12.1", features = ["with-actix-web"] }
|
|
||||||
askama_actix = "0.14.0"
|
|
||||||
barcoders = { version = "2.0.0", default-features = false, features = ["std"] }
|
barcoders = { version = "2.0.0", default-features = false, features = ["std"] }
|
||||||
datamatrix = "0.3.1"
|
datamatrix = "0.3.1"
|
||||||
diesel = { version = "2.2.1", features = ["uuid"] }
|
diesel = { version = "2.2.1", features = ["uuid"] }
|
||||||
|
@ -22,6 +20,7 @@ diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
|
||||||
diesel_migrations = { version = "2.2.0", features = ["postgres"] }
|
diesel_migrations = { version = "2.2.0", features = ["postgres"] }
|
||||||
env_logger = "0.11.3"
|
env_logger = "0.11.3"
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
|
maud = { version = "0.26.0", features = ["actix-web"] }
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
printpdf = "0.7.0"
|
printpdf = "0.7.0"
|
||||||
rust-fontconfig = "0.1.7"
|
rust-fontconfig = "0.1.7"
|
||||||
|
|
|
@ -4,10 +4,11 @@
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use actix_web::{error, get, post, web, HttpRequest, Responder};
|
use actix_web::{error, get, post, web, Responder};
|
||||||
use askama_actix::Template;
|
use maud::html;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::templates::{self, forms, TemplateConfig};
|
||||||
use crate::manage;
|
use crate::manage;
|
||||||
use crate::models::*;
|
use crate::models::*;
|
||||||
use crate::DbPool;
|
use crate::DbPool;
|
||||||
|
@ -21,20 +22,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
.service(edit_item_post);
|
.service(edit_item_post);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "item_details.html")]
|
|
||||||
struct ItemDetails {
|
|
||||||
req: HttpRequest,
|
|
||||||
item: Item,
|
|
||||||
item_class: ItemClass,
|
|
||||||
item_classes: HashMap<Uuid, ItemClass>,
|
|
||||||
parents: Vec<Item>,
|
|
||||||
children: Vec<Item>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/item/{id}")]
|
#[get("/item/{id}")]
|
||||||
async fn show_item(
|
async fn show_item(
|
||||||
req: HttpRequest,
|
|
||||||
pool: web::Data<DbPool>,
|
pool: web::Data<DbPool>,
|
||||||
path: web::Path<Uuid>,
|
path: web::Path<Uuid>,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
@ -48,9 +37,6 @@ async fn show_item(
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
// TODO: remove clone (should be possible without it)
|
|
||||||
let item_class = item_classes.get(&item.class).unwrap().clone();
|
|
||||||
|
|
||||||
let parents = manage::item::get_parents_details(&mut pool.get().await.unwrap(), item.id)
|
let parents = manage::item::get_parents_details(&mut pool.get().await.unwrap(), item.id)
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
@ -59,37 +45,62 @@ async fn show_item(
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
Ok(ItemDetails {
|
let item_class = item_classes.get(&item.class).unwrap();
|
||||||
req,
|
|
||||||
item,
|
|
||||||
item_class,
|
|
||||||
item_classes,
|
|
||||||
parents,
|
|
||||||
children,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Template)]
|
let item_name = templates::helpers::ItemName::new(&item, item_class);
|
||||||
#[template(path = "item_list.html")]
|
let mut title = item_name.to_string();
|
||||||
struct ItemList {
|
title.push_str(" – Item Details");
|
||||||
req: HttpRequest,
|
|
||||||
// Both a Vec and a HashMap are used to have both the natural order,
|
Ok(templates::base(
|
||||||
// as well as arbitrary access capabilities.
|
TemplateConfig {
|
||||||
item_list: Vec<Item>,
|
path: &format!("/item/{}", item.id),
|
||||||
#[allow(dead_code)] // remove once item_parents can be constructed in the template
|
title: Some(&title),
|
||||||
items: HashMap<Uuid, Item>,
|
page_title: Some(Box::new(item_name.clone())),
|
||||||
item_classes: HashMap<Uuid, ItemClass>,
|
page_actions: vec![
|
||||||
#[allow(dead_code)] // remove once item_parents can be constructed in the template
|
(templates::helpers::PageAction {
|
||||||
item_tree: HashMap<Uuid, Vec<Uuid>>,
|
href: &format!("/item/{}/edit", item.id),
|
||||||
// to overcome askama’s lack of support for closures
|
name: "Edit",
|
||||||
item_parents: HashMap<Uuid, Vec<Item>>,
|
}),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
table .table {
|
||||||
|
tr {
|
||||||
|
th { "UUID" }
|
||||||
|
td { (item.id) }
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
th { "Name" }
|
||||||
|
td { (item_name.clone().terse()) }
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
th { "Class" }
|
||||||
|
td { a href={ "/item-class/" (item.class) } { (item_class.name) } }
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
th { "Parents" }
|
||||||
|
td { (templates::helpers::parents_breadcrumb(&item, &item_class, &parents, &item_classes, true)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if !children.is_empty() {
|
||||||
|
h3 .mt-4 { "Direct Children" }
|
||||||
|
|
||||||
|
ul {
|
||||||
|
@for child in children {
|
||||||
|
li {
|
||||||
|
a href={ "/item/" (child.id) } { (templates::helpers::ItemName::new(&child, &item_classes.get(&child.class).unwrap())) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/items")]
|
#[get("/items")]
|
||||||
async fn list_items(
|
async fn list_items(pool: web::Data<DbPool>) -> actix_web::Result<impl Responder> {
|
||||||
req: HttpRequest,
|
|
||||||
pool: web::Data<DbPool>,
|
|
||||||
) -> actix_web::Result<impl Responder> {
|
|
||||||
let item_list = manage::item::get_all(&mut pool.get().await.unwrap())
|
let item_list = manage::item::get_all(&mut pool.get().await.unwrap())
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
@ -107,41 +118,95 @@ async fn list_items(
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
// TODO: remove clone (should be possible without it)
|
// TODO: remove clone (should be possible without it)
|
||||||
let item_parents = items
|
let item_parents: HashMap<Uuid, Vec<Item>> = item_tree
|
||||||
.clone()
|
.iter()
|
||||||
.into_iter()
|
.map(|(id, parent_ids)| {
|
||||||
.map(|(id, item)| {
|
(
|
||||||
(id, {
|
*id,
|
||||||
item_tree
|
parent_ids
|
||||||
.get(&item.id)
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|is| items.get(is).unwrap().clone())
|
.map(|parent_id| items.get(parent_id).unwrap().clone())
|
||||||
.collect()
|
.collect(),
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(ItemList {
|
Ok(templates::base(
|
||||||
req,
|
TemplateConfig {
|
||||||
item_list,
|
path: "/items",
|
||||||
items,
|
title: Some("Item List"),
|
||||||
item_classes,
|
page_title: Some(Box::new("Item List")),
|
||||||
item_tree,
|
page_actions: vec![
|
||||||
item_parents,
|
(templates::helpers::PageAction {
|
||||||
})
|
href: "/items/add",
|
||||||
}
|
name: "Add",
|
||||||
|
}),
|
||||||
#[derive(Template)]
|
],
|
||||||
#[template(path = "item_add.html")]
|
..Default::default()
|
||||||
struct ItemAddForm {
|
},
|
||||||
req: HttpRequest,
|
html! {
|
||||||
data: Option<NewItem>,
|
table .table {
|
||||||
|
thead {
|
||||||
|
tr {
|
||||||
|
th { "Name" }
|
||||||
|
th { "Class" }
|
||||||
|
th { "Parents" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
@for item in item_list {
|
||||||
|
@let class = item_classes.get(&item.class).unwrap();
|
||||||
|
tr {
|
||||||
|
td {
|
||||||
|
a href={ "/item/" (item.id) } {
|
||||||
|
(templates::helpers::ItemName::new(&item, class).terse())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td { a href={ "/item-class/" (class.id) } { (class.name) } }
|
||||||
|
td { (templates::helpers::parents_breadcrumb(&item, &class, item_parents.get(&item.id).unwrap(), &item_classes, false)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/items/add")]
|
#[get("/items/add")]
|
||||||
async fn add_item(req: HttpRequest) -> actix_web::Result<impl Responder> {
|
async fn add_item() -> actix_web::Result<impl Responder> {
|
||||||
Ok(ItemAddForm { req, data: None })
|
Ok(templates::base(
|
||||||
|
TemplateConfig {
|
||||||
|
path: "/items/add",
|
||||||
|
title: Some("Add Item"),
|
||||||
|
page_title: Some(Box::new("Add Item")),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
form method="POST" {
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "name",
|
||||||
|
title: "Name",
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "class",
|
||||||
|
title: "Class",
|
||||||
|
required: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "parent",
|
||||||
|
title: "Parent",
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
|
||||||
|
button .btn.btn-primary type="submit" { "Add" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/items/add")]
|
#[post("/items/add")]
|
||||||
|
@ -155,17 +220,8 @@ async fn add_item_post(
|
||||||
Ok(web::Redirect::to("/item/".to_owned() + &item.id.to_string()).see_other())
|
Ok(web::Redirect::to("/item/".to_owned() + &item.id.to_string()).see_other())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "item_edit.html")]
|
|
||||||
struct ItemEditForm {
|
|
||||||
req: HttpRequest,
|
|
||||||
item: Item,
|
|
||||||
item_class: ItemClass,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/item/{id}/edit")]
|
#[get("/item/{id}/edit")]
|
||||||
async fn edit_item(
|
async fn edit_item(
|
||||||
req: HttpRequest,
|
|
||||||
pool: web::Data<DbPool>,
|
pool: web::Data<DbPool>,
|
||||||
path: web::Path<Uuid>,
|
path: web::Path<Uuid>,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
@ -179,11 +235,56 @@ async fn edit_item(
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
Ok(ItemEditForm {
|
let item_name = templates::helpers::ItemName::new(&item, &item_class);
|
||||||
req,
|
let mut title = item_name.to_string();
|
||||||
item,
|
title.push_str(" – Edit Item");
|
||||||
item_class,
|
|
||||||
})
|
Ok(templates::base(
|
||||||
|
TemplateConfig {
|
||||||
|
path: &format!("/item/{}/edit", item.id),
|
||||||
|
title: Some(&title),
|
||||||
|
page_title: Some(Box::new(item_name.clone())),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
form method="POST" {
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "id",
|
||||||
|
title: "UUID",
|
||||||
|
required: true,
|
||||||
|
disabled: true,
|
||||||
|
value: Some(item.id.to_string().as_str()),
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "name",
|
||||||
|
title: "Name",
|
||||||
|
value: item.name.as_deref(),
|
||||||
|
disabled: item.name.is_none(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "class",
|
||||||
|
title: "Class",
|
||||||
|
required: true,
|
||||||
|
value: Some(item.class.to_string().as_str()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "parent",
|
||||||
|
title: "Parent",
|
||||||
|
value: item.parent.map(|id| id.to_string()).as_deref(),
|
||||||
|
disabled: item.parent.is_none(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
|
||||||
|
button .btn.btn-primary type="submit" { "Edit" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/item/{id}/edit")]
|
#[post("/item/{id}/edit")]
|
||||||
|
|
|
@ -2,16 +2,35 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use actix_web::{error, get, post, web, Responder};
|
||||||
|
use maud::html;
|
||||||
use actix_web::{error, get, post, web, HttpRequest, Responder};
|
|
||||||
use askama_actix::Template;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::templates::{self, forms, TemplateConfig};
|
||||||
use crate::manage;
|
use crate::manage;
|
||||||
use crate::models::*;
|
use crate::models::*;
|
||||||
use crate::DbPool;
|
use crate::DbPool;
|
||||||
|
|
||||||
|
const FORM_ENSURE_PARENT: templates::helpers::Js = templates::helpers::Js::Inline(
|
||||||
|
r#"
|
||||||
|
(() => {
|
||||||
|
document.getElementById("type").addEventListener("change", e => {
|
||||||
|
let parentInput = document.getElementById("parent")
|
||||||
|
switch (e.target.value) {
|
||||||
|
case "generic":
|
||||||
|
parentInput.disabled = true
|
||||||
|
parentInput.value = ""
|
||||||
|
break
|
||||||
|
case "specific":
|
||||||
|
parentInput.disabled = false
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.error("invalid type!")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})()"#,
|
||||||
|
);
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(show_item_class)
|
cfg.service(show_item_class)
|
||||||
.service(list_item_classes)
|
.service(list_item_classes)
|
||||||
|
@ -21,17 +40,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
.service(edit_item_class_post);
|
.service(edit_item_class_post);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "item_class_details.html")]
|
|
||||||
struct ItemClassDetails {
|
|
||||||
req: HttpRequest,
|
|
||||||
item_class: ItemClass,
|
|
||||||
parent: Option<ItemClass>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/item-class/{id}")]
|
#[get("/item-class/{id}")]
|
||||||
async fn show_item_class(
|
async fn show_item_class(
|
||||||
req: HttpRequest,
|
|
||||||
pool: web::Data<DbPool>,
|
pool: web::Data<DbPool>,
|
||||||
path: web::Path<Uuid>,
|
path: web::Path<Uuid>,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
@ -50,42 +60,131 @@ async fn show_item_class(
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(ItemClassDetails {
|
let mut title = item_class.name.clone();
|
||||||
req,
|
title.push_str(" – Item Details");
|
||||||
item_class,
|
|
||||||
parent,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Template)]
|
Ok(templates::base(
|
||||||
#[template(path = "item_class_list.html")]
|
TemplateConfig {
|
||||||
struct ItemClassList {
|
path: &format!("/item-class/{}", item_class.id),
|
||||||
req: HttpRequest,
|
title: Some(&title),
|
||||||
item_classes: HashMap<Uuid, ItemClass>,
|
page_title: Some(Box::new(item_class.name.clone())),
|
||||||
|
page_actions: vec![
|
||||||
|
(templates::helpers::PageAction {
|
||||||
|
href: &format!("/item-class/{}/edit", item_class.id),
|
||||||
|
name: "Edit",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
table .table {
|
||||||
|
tr {
|
||||||
|
th { "UUID" }
|
||||||
|
td { (item_class.id) }
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
th { "Name" }
|
||||||
|
td { (item_class.name) }
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
th { "Type" }
|
||||||
|
td { (item_class.r#type) }
|
||||||
|
}
|
||||||
|
@if let Some(parent) = parent {
|
||||||
|
tr {
|
||||||
|
th { "Parent" }
|
||||||
|
td { a href={ "/item-class/" (parent.id) } { (parent.name) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/item-classes")]
|
#[get("/item-classes")]
|
||||||
async fn list_item_classes(
|
async fn list_item_classes(pool: web::Data<DbPool>) -> actix_web::Result<impl Responder> {
|
||||||
req: HttpRequest,
|
|
||||||
pool: web::Data<DbPool>,
|
|
||||||
) -> actix_web::Result<impl Responder> {
|
|
||||||
let item_classes = manage::item_class::get_all_as_map(&mut pool.get().await.unwrap())
|
let item_classes = manage::item_class::get_all_as_map(&mut pool.get().await.unwrap())
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
Ok(ItemClassList { req, item_classes })
|
Ok(templates::base(
|
||||||
}
|
TemplateConfig {
|
||||||
|
path: "/item-classes",
|
||||||
#[derive(Template)]
|
title: Some("Item Class List"),
|
||||||
#[template(path = "item_class_add.html")]
|
page_title: Some(Box::new("Item Class List")),
|
||||||
struct ItemClassAddForm {
|
page_actions: vec![
|
||||||
req: HttpRequest,
|
(templates::helpers::PageAction {
|
||||||
data: Option<NewItemClass>,
|
href: "/item-classes/add",
|
||||||
|
name: "Add",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
table .table {
|
||||||
|
thead {
|
||||||
|
tr {
|
||||||
|
th { "Name" }
|
||||||
|
th { "Parents" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
@for item_class in item_classes.values() {
|
||||||
|
tr {
|
||||||
|
td { a href={ "/item-class/" (item_class.id) } { (item_class.name) } }
|
||||||
|
td {
|
||||||
|
@if let Some(parent) = item_class.parent {
|
||||||
|
@let parent = item_classes.get(&parent).unwrap();
|
||||||
|
a href={ "/item-class/" (parent.id) } { (parent.name) }
|
||||||
|
} @else {
|
||||||
|
"-"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/item-classes/add")]
|
#[get("/item-classes/add")]
|
||||||
async fn add_item_class(req: HttpRequest) -> actix_web::Result<impl Responder> {
|
async fn add_item_class() -> actix_web::Result<impl Responder> {
|
||||||
Ok(ItemClassAddForm { req, data: None })
|
Ok(templates::base(
|
||||||
|
TemplateConfig {
|
||||||
|
path: "/items-classes/add",
|
||||||
|
title: Some("Add Item Class"),
|
||||||
|
page_title: Some(Box::new("Add Item Class")),
|
||||||
|
extra_js: vec![FORM_ENSURE_PARENT],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
form method="POST" {
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "name",
|
||||||
|
title: "Name",
|
||||||
|
required: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
// TODO: drop type in favour of determining it on whether parent is set
|
||||||
|
.mb-3 {
|
||||||
|
label .form-label for="type" { "Type" }
|
||||||
|
select .form-select #type name="type" required {
|
||||||
|
@for variant in ItemClassType::VARIANTS {
|
||||||
|
option { (variant) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mb-3 {
|
||||||
|
label .form-label for="parent" { "Parent" }
|
||||||
|
input .form-control #parent type="text" name="parent" disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
button .btn.btn-primary type="submit" { "Add" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/item-classes/add")]
|
#[post("/item-classes/add")]
|
||||||
|
@ -99,16 +198,8 @@ async fn add_item_class_post(
|
||||||
Ok(web::Redirect::to("/item-class/".to_owned() + &item.id.to_string()).see_other())
|
Ok(web::Redirect::to("/item-class/".to_owned() + &item.id.to_string()).see_other())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "item_class_edit.html")]
|
|
||||||
struct ItemClassEditForm {
|
|
||||||
req: HttpRequest,
|
|
||||||
item_class: ItemClass,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/item-class/{id}/edit")]
|
#[get("/item-class/{id}/edit")]
|
||||||
async fn edit_item_class(
|
async fn edit_item_class(
|
||||||
req: HttpRequest,
|
|
||||||
pool: web::Data<DbPool>,
|
pool: web::Data<DbPool>,
|
||||||
path: web::Path<Uuid>,
|
path: web::Path<Uuid>,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
@ -118,7 +209,53 @@ async fn edit_item_class(
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
Ok(ItemClassEditForm { req, item_class })
|
let mut title = item_class.name.clone();
|
||||||
|
title.push_str(" – Item Details");
|
||||||
|
|
||||||
|
Ok(templates::base(
|
||||||
|
TemplateConfig {
|
||||||
|
path: &format!("/items-class/{}/add", id),
|
||||||
|
title: Some(&title),
|
||||||
|
page_title: Some(Box::new(item_class.name.clone())),
|
||||||
|
extra_js: vec![FORM_ENSURE_PARENT],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
form method="POST" {
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "id",
|
||||||
|
title: "UUID",
|
||||||
|
disabled: true,
|
||||||
|
required: true,
|
||||||
|
value: Some(item_class.id.to_string().as_str()),
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "name",
|
||||||
|
title: "Name",
|
||||||
|
required: true,
|
||||||
|
value: Some(&item_class.name),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
// TODO: drop type in favour of determining it on whether parent is set
|
||||||
|
.mb-3 {
|
||||||
|
label .form-label for="type" { "Type" }
|
||||||
|
select .form-select #type name="type" required {
|
||||||
|
@for variant in ItemClassType::VARIANTS {
|
||||||
|
option selected[variant == item_class.r#type] { (variant) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mb-3 {
|
||||||
|
label .form-label for="parent" { "Parent" }
|
||||||
|
input .form-control #parent type="text" name="parent" disabled[item_class.parent.is_none()] value=[item_class.parent];
|
||||||
|
}
|
||||||
|
|
||||||
|
button .btn.btn-primary type="submit" { "Edit" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/item-class/{id}/edit")]
|
#[post("/item-class/{id}/edit")]
|
||||||
|
|
|
@ -4,9 +4,10 @@
|
||||||
|
|
||||||
mod item;
|
mod item;
|
||||||
mod item_class;
|
mod item_class;
|
||||||
|
mod templates;
|
||||||
|
|
||||||
use actix_web::{get, web, HttpRequest, Responder};
|
use actix_web::{get, web, Responder};
|
||||||
use askama_actix::Template;
|
use maud::html;
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(index)
|
cfg.service(index)
|
||||||
|
@ -14,13 +15,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
.configure(item_class::config);
|
.configure(item_class::config);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "base.html")]
|
|
||||||
struct Home {
|
|
||||||
req: HttpRequest,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn index(req: HttpRequest) -> impl Responder {
|
async fn index() -> impl Responder {
|
||||||
Home { req }
|
templates::base(templates::TemplateConfig::default(), html! {})
|
||||||
}
|
}
|
||||||
|
|
70
src/frontend/templates/forms.rs
Normal file
70
src/frontend/templates/forms.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use maud::{html, Markup, Render};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum InputType {
|
||||||
|
Text,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for InputType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Text => write!(f, "text"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InputGroup<'a> {
|
||||||
|
pub r#type: InputType,
|
||||||
|
pub name: &'a str,
|
||||||
|
pub title: &'a str,
|
||||||
|
pub required: bool,
|
||||||
|
pub disabled: bool,
|
||||||
|
pub value: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InputGroup<'_> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
r#type: InputType::Text,
|
||||||
|
name: "placeholder",
|
||||||
|
title: "Placeholder",
|
||||||
|
required: false,
|
||||||
|
disabled: false,
|
||||||
|
value: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputGroup<'_> {
|
||||||
|
fn main_input(&self) -> Markup {
|
||||||
|
html! {
|
||||||
|
input .form-control #(self.name) name={ (self.name) } type={ (self.r#type) } required[self.required] disabled[self.disabled] value=[self.value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for InputGroup<'_> {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
html! {
|
||||||
|
.mb-3 {
|
||||||
|
label .form-label for={ (self.name) } { (self.title) }
|
||||||
|
@if self.required {
|
||||||
|
(self.main_input())
|
||||||
|
} @else {
|
||||||
|
.input-group {
|
||||||
|
.input-group-text {
|
||||||
|
input .form-check-input.mt-0.input-toggle type="checkbox" checked[!self.disabled];
|
||||||
|
}
|
||||||
|
(self.main_input())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
136
src/frontend/templates/helpers.rs
Normal file
136
src/frontend/templates/helpers.rs
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
|
use crate::models::*;
|
||||||
|
use maud::{html, Markup, PreEscaped, Render};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub enum Css<'a> {
|
||||||
|
File(&'a str),
|
||||||
|
#[allow(dead_code)]
|
||||||
|
Inline(&'a str),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Css<'_> {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
html! {
|
||||||
|
@match self {
|
||||||
|
Self::File(path) => {
|
||||||
|
link rel="stylesheet" href=(path);
|
||||||
|
},
|
||||||
|
Self::Inline(content) => {
|
||||||
|
style { (PreEscaped(content)) }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Js<'a> {
|
||||||
|
File(&'a str),
|
||||||
|
Inline(&'a str),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Js<'_> {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
html! {
|
||||||
|
@match self {
|
||||||
|
Self::File(path) => {
|
||||||
|
script src=(path) { }
|
||||||
|
},
|
||||||
|
Self::Inline(content) => {
|
||||||
|
script { (PreEscaped(content)) }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ItemName {
|
||||||
|
Item(String),
|
||||||
|
Class(String),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemName {
|
||||||
|
pub fn new(item: &Item, class: &ItemClass) -> Self {
|
||||||
|
if let Some(ref name) = item.name {
|
||||||
|
Self::Item(name.to_string())
|
||||||
|
} else {
|
||||||
|
Self::Class(class.name.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemName {
|
||||||
|
pub fn terse(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Item(_) => self,
|
||||||
|
Self::Class(_) | Self::None => Self::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ItemName {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Item(name) => write!(f, "{name}"),
|
||||||
|
Self::Class(name) => write!(f, "*{name}*"),
|
||||||
|
Self::None => write!(f, "[no name]"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ItemName {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
html! {
|
||||||
|
@match self {
|
||||||
|
Self::Item(name) => { (name) },
|
||||||
|
Self::Class(name) => { em { (name) } },
|
||||||
|
Self::None => { em { "[no name]" } },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PageAction<'a> {
|
||||||
|
pub href: &'a str,
|
||||||
|
pub name: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for PageAction<'_> {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
html! {
|
||||||
|
a .btn.btn-primary href=(self.href) { (self.name) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parents_breadcrumb(
|
||||||
|
item: &Item,
|
||||||
|
item_class: &ItemClass,
|
||||||
|
parents: &[Item],
|
||||||
|
parents_item_classes: &HashMap<Uuid, ItemClass>,
|
||||||
|
full: bool,
|
||||||
|
) -> Markup {
|
||||||
|
const LIMIT: usize = 3;
|
||||||
|
|
||||||
|
html! {
|
||||||
|
ol .breadcrumb .mb-0 {
|
||||||
|
@if !full && parents.len() > LIMIT {
|
||||||
|
li .breadcrumb-item { "…" }
|
||||||
|
}
|
||||||
|
@for parent in parents.iter().rev().take(LIMIT).rev() {
|
||||||
|
li .breadcrumb-item {
|
||||||
|
a href={ "/item/" (parent.id) } { (ItemName::new(parent, parents_item_classes.get(&parent.class).unwrap()) )}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
li .breadcrumb-item.active { (ItemName::new(item, item_class)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
128
src/frontend/templates/mod.rs
Normal file
128
src/frontend/templates/mod.rs
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
pub mod forms;
|
||||||
|
pub mod helpers;
|
||||||
|
|
||||||
|
use maud::{html, Markup, Render, DOCTYPE};
|
||||||
|
|
||||||
|
use helpers::*;
|
||||||
|
|
||||||
|
const BRANDING: &str = "li7y";
|
||||||
|
|
||||||
|
const NAVBAR_ITEMS: &[(&str, &str)] = &[
|
||||||
|
("/", "Home"),
|
||||||
|
("/items", "Items"),
|
||||||
|
("/item-classes", "Item Classes"),
|
||||||
|
];
|
||||||
|
|
||||||
|
fn navbar(path: &str) -> Markup {
|
||||||
|
html! {
|
||||||
|
nav .navbar.navbar-expand-lg.bg-body-secondary {
|
||||||
|
div .container {
|
||||||
|
a .navbar-brand href="/" { (BRANDING) }
|
||||||
|
|
||||||
|
button .navbar-toggler type="button" data-bs-toggle="collapse" data-bs-target="#navbar-expander" {
|
||||||
|
span .navbar-toggler-icon { }
|
||||||
|
}
|
||||||
|
|
||||||
|
div .collapse.navbar-collapse #navbar-expander {
|
||||||
|
ul .navbar-nav.me-auto.mb-2.mb-md-0 {
|
||||||
|
@for (target, name) in NAVBAR_ITEMS {
|
||||||
|
li .nav-item { a .nav-link .active[path == *target] href=(target) { (name) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn footer() -> Markup {
|
||||||
|
html! {
|
||||||
|
footer .mt-auto.py-3.bg-body-tertiary.text-body-tertiary {
|
||||||
|
div .container {
|
||||||
|
p .mb-0 { "li7y is free software, released under the terms of the AGPL v3" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TemplateConfig<'a> {
|
||||||
|
pub path: &'a str,
|
||||||
|
pub title: Option<&'a str>,
|
||||||
|
pub page_title: Option<Box<dyn Render>>,
|
||||||
|
pub page_actions: Vec<PageAction<'a>>,
|
||||||
|
pub extra_css: Vec<Css<'a>>,
|
||||||
|
pub extra_js: Vec<Js<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TemplateConfig<'_> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
path: "/",
|
||||||
|
title: None,
|
||||||
|
page_title: None,
|
||||||
|
page_actions: Vec::new(),
|
||||||
|
extra_css: Vec::new(),
|
||||||
|
extra_js: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn base(config: TemplateConfig, content: Markup) -> Markup {
|
||||||
|
html! {
|
||||||
|
(DOCTYPE)
|
||||||
|
html .h-100 {
|
||||||
|
head {
|
||||||
|
meta charset="utf-8";
|
||||||
|
title {
|
||||||
|
@if let Some(ref title) = config.title {
|
||||||
|
(title) " – "
|
||||||
|
}
|
||||||
|
(BRANDING)
|
||||||
|
}
|
||||||
|
|
||||||
|
meta name="viewport" content="width=device-width, initial-scale=1";
|
||||||
|
|
||||||
|
(Css::File("/static/vendor/bootstrap.min.css"))
|
||||||
|
@for css in config.extra_css {
|
||||||
|
(css)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body .d-flex.flex-column.h-100 {
|
||||||
|
(navbar(config.path))
|
||||||
|
|
||||||
|
main .container.my-4 {
|
||||||
|
div .d-flex.justify-content-between.mb-3 {
|
||||||
|
div {
|
||||||
|
@if let Some(ref page_title) = config.page_title {
|
||||||
|
h2 {
|
||||||
|
(page_title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
@for page_action in config.page_actions {
|
||||||
|
(page_action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
(footer())
|
||||||
|
|
||||||
|
(Js::File("/static/vendor/bootstrap.bundle.min.js"))
|
||||||
|
(Js::File("/static/app.js"))
|
||||||
|
// TODO this is not the best way, but it works for now
|
||||||
|
@for js in config.extra_js {
|
||||||
|
(js)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,35 +0,0 @@
|
||||||
{#
|
|
||||||
SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
#}
|
|
||||||
|
|
||||||
{% extends "base.html" %}
|
|
||||||
{% block title %}{% block page_title %}Add Item{% endblock %} – {{ branding }}{% endblock %}
|
|
||||||
{% block main %}
|
|
||||||
<form method="POST">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="name" class="form-label">Name</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-text">
|
|
||||||
<input type="checkbox" class="form-check-input mt-0 input-toggle"{% if let Some(data) = data %}{% if data.name.is_some() %} checked{% endif %}{% endif %}>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="form-control" id="name" name="name"{% if let Some(data) = data %}{% if let Some(name) = data.name %} value="{{ name }}"{% else %} disabled{% endif %}{% else %}disabled{% endif %}>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="class" class="form-label">Class</label>
|
|
||||||
<input type="text" class="form-control" id="class" name="class" required{% if let Some(data) = data %} value="{{ data.class }}"{% endif %}>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="parent" class="form-label">Parent</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-text">
|
|
||||||
<input type="checkbox" class="form-check-input mt-0 input-toggle"{% if let Some (data) = data %}{% if data.parent.is_some() %} checked{% endif %}{% endif %}>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="form-control" id="parent" name="parent"{% if let Some(data) = data %}{% if let Some(parent) = data.parent %} value="{{ parent }}"{% else %} disabled{% endif %}{% else %}disabled{% endif %}>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Add</button>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
|
@ -1,50 +0,0 @@
|
||||||
{#
|
|
||||||
SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
#}
|
|
||||||
|
|
||||||
{% extends "base.html" %}
|
|
||||||
{% block title %}{% block page_title %}Add Item Class{% endblock %} – {{ branding }}{% endblock %}
|
|
||||||
{% block main %}
|
|
||||||
<form method="POST">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="name" class="form-label">Name</label>
|
|
||||||
<input type="text" class="form-control" id="name" name="name" required{% if let Some(data) = data %} value="{{ data.name }}"{% endif %}>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="type" class="form-label">Type</label>
|
|
||||||
<select class="form-select" id="type" name="type" required{% if let Some(data) = data %} value="{{ data.type }}"{% endif %}>
|
|
||||||
{% for variant in ItemClassType::VARIANTS %}
|
|
||||||
<option>{{ variant }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="parent" class="form-label">Parent</label>
|
|
||||||
<input type="text" class="form-control" id="parent" name="parent" disabled{% if let Some(data) = data %}{% if let Some(parent) = data.parent %} value="{{ parent }}"{% endif %}{% endif %}>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Add</button>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
{% block extra_scripts %}
|
|
||||||
<script>
|
|
||||||
(() => {
|
|
||||||
document.getElementById("type").addEventListener("change", e => {
|
|
||||||
console.log(e)
|
|
||||||
let parentInput = document.getElementById("parent")
|
|
||||||
switch (e.target.value) {
|
|
||||||
case "generic":
|
|
||||||
parentInput.disabled = true
|
|
||||||
parentInput.value = ""
|
|
||||||
break
|
|
||||||
case "specific":
|
|
||||||
parentInput.disabled = false
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
console.error("invalid type!")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})()
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
|
@ -1,49 +0,0 @@
|
||||||
{#
|
|
||||||
SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
#}
|
|
||||||
|
|
||||||
{% extends "base.html" %}
|
|
||||||
{% block title %}{% block page_title %}{{ item_class.name }}{% endblock %} – Item Class Details – {{ branding }}{% endblock %}
|
|
||||||
{% block page_actions %}
|
|
||||||
<a class="btn btn-warning" href="/item-class/{{ item_class.id }}/edit">Edit</a>
|
|
||||||
{% endblock %}
|
|
||||||
{% block main %}
|
|
||||||
<table class="table">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
UUID
|
|
||||||
</th>
|
|
||||||
<td>{{ item_class.id }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
Name
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
{{ item_class.name }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
Type
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
{{ item_class.type }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% if let Some(parent) = parent -%}
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
Parent
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
<a href="/item-class/{{ parent.id }}">{{ parent.name }}</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{%- endif %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endblock %}
|
|
|
@ -1,29 +0,0 @@
|
||||||
{#
|
|
||||||
SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
#}
|
|
||||||
|
|
||||||
{% extends "base.html" %}
|
|
||||||
{% block title %}{% block page_title %}Item Class List{% endblock %} – {{ branding }}{% endblock %}
|
|
||||||
{% block page_actions %}
|
|
||||||
<a class="btn btn-primary" href="/item-classes/add">Add</a>
|
|
||||||
{% endblock %}
|
|
||||||
{% block main %}
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Parent</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for item_class in item_classes.values() -%}
|
|
||||||
<tr>
|
|
||||||
<td><a href="/item-class/{{ item_class.id }}">{{ item_class.name }}</a></td>
|
|
||||||
<td>{% if let Some(parent) = item_class.parent %}<a href="/item-class/{{ parent }}">{{ item_classes.get(parent).unwrap().name }}</a>{% else %}-{% endif %}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor -%}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endblock %}
|
|
|
@ -1,58 +0,0 @@
|
||||||
{#
|
|
||||||
SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
#}
|
|
||||||
|
|
||||||
{%- import "macros.html" as macros -%}
|
|
||||||
{% extends "base.html" %}
|
|
||||||
{% block title %}{% block page_title %}{% call macros::item_name(item, item_class, false) %}{% endblock %} – Item Details – {{ branding }}{% endblock %}
|
|
||||||
{% block page_actions %}
|
|
||||||
<a class="btn btn-warning" href="/item/{{ item.id }}/edit">Edit</a>
|
|
||||||
{% endblock %}
|
|
||||||
{% block main %}
|
|
||||||
<table class="table">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
UUID
|
|
||||||
</th>
|
|
||||||
<td>{{ item.id }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
Name
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
{% call macros::item_name_terse(item, true) %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
Class
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
<a href="/item-class/{{ item.class }}">{{ item_class.name }}</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
Parents
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
{%- call macros::parents_breadcrumb(item, parents, item_classes, full=true) %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{% if children.len() != 0 %}
|
|
||||||
<h3 class="mt-4">Direct Children</h3>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{% for child in children %}
|
|
||||||
<li><a href="/item/{{ child.id }}">{% call macros::item_name(child, item_classes.get(child.class).unwrap(), true) %}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
|
@ -1,40 +0,0 @@
|
||||||
{#
|
|
||||||
SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
#}
|
|
||||||
|
|
||||||
{%- import "macros.html" as macros -%}
|
|
||||||
{% extends "base.html" %}
|
|
||||||
{% block title %}{% block page_title %}{% call macros::item_name(item, item_class, false) %}{% endblock %} – Edit Item – {{ branding }}{% endblock %}
|
|
||||||
{% block main %}
|
|
||||||
<form method="POST">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="uuid" class="form-label">UUID</label>
|
|
||||||
<input type="text" class="form-control" id="uuid" disabled required value="{{ item.id }}">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="name" class="form-label">Name</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-text">
|
|
||||||
<input type="checkbox" class="form-check-input mt-0 input-toggle"{% if item.name.is_some() %} checked{% endif %}>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="form-control" id="name" name="name"{% if let Some(name) = item.name %} value="{{ name }}"{% else %} disabled{% endif %}>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="class" class="form-label">Class</label>
|
|
||||||
<input type="text" class="form-control" id="class" name="class" required value="{{ item.class }}">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="parent" class="form-label">Parent</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-text">
|
|
||||||
<input type="checkbox" class="form-check-input mt-0 input-toggle"{% if item.parent.is_some() %} checked{% endif %}>
|
|
||||||
</div>
|
|
||||||
<input type="text" class="form-control" id="parent" name="parent"{% if let Some(parent) = item.parent %} value="{{ parent }}"{% else %} disabled{% endif %}>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Edit</button>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
|
@ -1,37 +0,0 @@
|
||||||
{#
|
|
||||||
SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
#}
|
|
||||||
|
|
||||||
{%- import "macros.html" as macros -%}
|
|
||||||
{% extends "base.html" %}
|
|
||||||
{% block title %}{% block page_title %}Item List{% endblock %} – {{ branding }}{% endblock %}
|
|
||||||
{% block page_actions %}
|
|
||||||
<a class="btn btn-primary" href="/items/add">Add</a>
|
|
||||||
{% endblock %}
|
|
||||||
{% block main %}
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Class</th>
|
|
||||||
<th>Parents</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for item in item_list -%}
|
|
||||||
{% let class = item_classes.get(item.class).unwrap() %}
|
|
||||||
{# inlining this breaks? #}
|
|
||||||
{%- let parents = item_parents.get(item.id).unwrap() %}
|
|
||||||
<tr>
|
|
||||||
<td><a href="/item/{{ item.id }}">{% call macros::item_name_terse(item, true) %}</a></td>
|
|
||||||
<td><a href="/item-class/{{ class.id }}">{{ class.name }}</a></td>
|
|
||||||
<td>
|
|
||||||
{%- call macros::parents_breadcrumb(item, parents, item_classes, full=false) %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor -%}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endblock %}
|
|
|
@ -1,36 +0,0 @@
|
||||||
{#
|
|
||||||
SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
#}
|
|
||||||
|
|
||||||
{% macro emphasize(text, html) %}
|
|
||||||
{%- if html %}<em>{{ text }}</em>{% else %}*{{ text }}*{% endif %}
|
|
||||||
{%- endmacro %}
|
|
||||||
|
|
||||||
{% macro item_name_generic(name, fallback, html) %}
|
|
||||||
{%- if let Some(name) = name %}{{ name }}{% else %}{% call emphasize(fallback, html) %}{% endif %}
|
|
||||||
{%- endmacro %}
|
|
||||||
|
|
||||||
{% macro item_name(item, item_class, html) %}
|
|
||||||
{%- call item_name_generic(item.name, item_class.name, html) %}
|
|
||||||
{%- endmacro %}
|
|
||||||
|
|
||||||
{% macro item_name_terse(item, html) %}
|
|
||||||
{%- call item_name_generic(item.name, "[no name]", html) %}
|
|
||||||
{%- endmacro %}
|
|
||||||
|
|
||||||
{% macro parents_breadcrumb(item, parents, parents_item_classes, full) %}
|
|
||||||
{%- let limit = 3 %}
|
|
||||||
{%- let limited = parents.len() > limit && !full %}
|
|
||||||
{%- let parents = parents.iter().rev().take(limit.into()).rev() %}
|
|
||||||
<ol class="breadcrumb mb-0">
|
|
||||||
{%- if limited %}
|
|
||||||
<li class="breadcrumb-item">…</li>
|
|
||||||
{%- endif %}
|
|
||||||
{%- for parent in parents %}
|
|
||||||
<li class="breadcrumb-item"><a href="/item/{{ parent.id }}">{% call item_name(parent, parents_item_classes.get(parent.class).unwrap(), true) %}</a></li>
|
|
||||||
{%- endfor %}
|
|
||||||
<li class="breadcrumb-item active">{% call item_name(item, item_classes.get(item.class).unwrap(), true) %}</li>
|
|
||||||
</ol>
|
|
||||||
{%- endmacro %}
|
|
Loading…
Reference in a new issue