Switch to maud for templating
This commit is contained in:
parent
11be165f6d
commit
4f7d1808d4
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"
|
||||
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]]
|
||||
name = "async-trait"
|
||||
version = "0.1.80"
|
||||
|
@ -451,15 +397,6 @@ version = "0.22.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
|
@ -1082,15 +1019,6 @@ version = "1.0.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "humansize"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
|
@ -1180,8 +1108,6 @@ version = "0.0.0"
|
|||
dependencies = [
|
||||
"actix-files",
|
||||
"actix-web",
|
||||
"askama",
|
||||
"askama_actix",
|
||||
"barcoders",
|
||||
"datamatrix",
|
||||
"diesel",
|
||||
|
@ -1190,6 +1116,7 @@ dependencies = [
|
|||
"diesel_migrations",
|
||||
"env_logger",
|
||||
"log",
|
||||
"maud",
|
||||
"mime",
|
||||
"printpdf",
|
||||
"rust-fontconfig",
|
||||
|
@ -1204,12 +1131,6 @@ version = "0.2.155"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
|
@ -1266,6 +1187,30 @@ dependencies = [
|
|||
"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]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
|
@ -1325,12 +1270,6 @@ dependencies = [
|
|||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.4"
|
||||
|
@ -1362,16 +1301,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
|
|
|
@ -12,8 +12,6 @@ license = "AGPL-3.0-or-later"
|
|||
[dependencies]
|
||||
actix-files = "0.6.6"
|
||||
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"] }
|
||||
datamatrix = "0.3.1"
|
||||
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"] }
|
||||
env_logger = "0.11.3"
|
||||
log = "0.4.21"
|
||||
maud = { version = "0.26.0", features = ["actix-web"] }
|
||||
mime = "0.3.17"
|
||||
printpdf = "0.7.0"
|
||||
rust-fontconfig = "0.1.7"
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix_web::{error, get, post, web, HttpRequest, Responder};
|
||||
use askama_actix::Template;
|
||||
use actix_web::{error, get, post, web, Responder};
|
||||
use maud::html;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::templates::{self, forms, TemplateConfig};
|
||||
use crate::manage;
|
||||
use crate::models::*;
|
||||
use crate::DbPool;
|
||||
|
@ -21,20 +22,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||
.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}")]
|
||||
async fn show_item(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<DbPool>,
|
||||
path: web::Path<Uuid>,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
|
@ -48,9 +37,6 @@ async fn show_item(
|
|||
.await
|
||||
.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)
|
||||
.await
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
@ -59,37 +45,62 @@ async fn show_item(
|
|||
.await
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
||||
Ok(ItemDetails {
|
||||
req,
|
||||
item,
|
||||
item_class,
|
||||
item_classes,
|
||||
parents,
|
||||
children,
|
||||
})
|
||||
let item_class = item_classes.get(&item.class).unwrap();
|
||||
|
||||
let item_name = templates::helpers::ItemName::new(&item, item_class);
|
||||
let mut title = item_name.to_string();
|
||||
title.push_str(" – Item Details");
|
||||
|
||||
Ok(templates::base(
|
||||
TemplateConfig {
|
||||
path: &format!("/item/{}", item.id),
|
||||
title: Some(&title),
|
||||
page_title: Some(Box::new(item_name.clone())),
|
||||
page_actions: vec![
|
||||
(templates::helpers::PageAction {
|
||||
href: &format!("/item/{}/edit", item.id),
|
||||
name: "Edit",
|
||||
}),
|
||||
],
|
||||
..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)) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "item_list.html")]
|
||||
struct ItemList {
|
||||
req: HttpRequest,
|
||||
// Both a Vec and a HashMap are used to have both the natural order,
|
||||
// as well as arbitrary access capabilities.
|
||||
item_list: Vec<Item>,
|
||||
#[allow(dead_code)] // remove once item_parents can be constructed in the template
|
||||
items: HashMap<Uuid, Item>,
|
||||
item_classes: HashMap<Uuid, ItemClass>,
|
||||
#[allow(dead_code)] // remove once item_parents can be constructed in the template
|
||||
item_tree: HashMap<Uuid, Vec<Uuid>>,
|
||||
// to overcome askama’s lack of support for closures
|
||||
item_parents: HashMap<Uuid, Vec<Item>>,
|
||||
@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")]
|
||||
async fn list_items(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<DbPool>,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
async fn list_items(pool: web::Data<DbPool>) -> actix_web::Result<impl Responder> {
|
||||
let item_list = manage::item::get_all(&mut pool.get().await.unwrap())
|
||||
.await
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
@ -107,41 +118,95 @@ async fn list_items(
|
|||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
||||
// TODO: remove clone (should be possible without it)
|
||||
let item_parents = items
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(id, item)| {
|
||||
(id, {
|
||||
item_tree
|
||||
.get(&item.id)
|
||||
.unwrap()
|
||||
let item_parents: HashMap<Uuid, Vec<Item>> = item_tree
|
||||
.iter()
|
||||
.map(|is| items.get(is).unwrap().clone())
|
||||
.collect()
|
||||
})
|
||||
.map(|(id, parent_ids)| {
|
||||
(
|
||||
*id,
|
||||
parent_ids
|
||||
.iter()
|
||||
.map(|parent_id| items.get(parent_id).unwrap().clone())
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(ItemList {
|
||||
req,
|
||||
item_list,
|
||||
items,
|
||||
item_classes,
|
||||
item_tree,
|
||||
item_parents,
|
||||
})
|
||||
Ok(templates::base(
|
||||
TemplateConfig {
|
||||
path: "/items",
|
||||
title: Some("Item List"),
|
||||
page_title: Some(Box::new("Item List")),
|
||||
page_actions: vec![
|
||||
(templates::helpers::PageAction {
|
||||
href: "/items/add",
|
||||
name: "Add",
|
||||
}),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
html! {
|
||||
table .table {
|
||||
thead {
|
||||
tr {
|
||||
th { "Name" }
|
||||
th { "Class" }
|
||||
th { "Parents" }
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "item_add.html")]
|
||||
struct ItemAddForm {
|
||||
req: HttpRequest,
|
||||
data: Option<NewItem>,
|
||||
}
|
||||
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")]
|
||||
async fn add_item(req: HttpRequest) -> actix_web::Result<impl Responder> {
|
||||
Ok(ItemAddForm { req, data: None })
|
||||
async fn add_item() -> actix_web::Result<impl Responder> {
|
||||
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")]
|
||||
|
@ -155,17 +220,8 @@ async fn add_item_post(
|
|||
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")]
|
||||
async fn edit_item(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<DbPool>,
|
||||
path: web::Path<Uuid>,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
|
@ -179,11 +235,56 @@ async fn edit_item(
|
|||
.await
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
||||
Ok(ItemEditForm {
|
||||
req,
|
||||
item,
|
||||
item_class,
|
||||
let item_name = templates::helpers::ItemName::new(&item, &item_class);
|
||||
let mut title = item_name.to_string();
|
||||
title.push_str(" – Edit Item");
|
||||
|
||||
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")]
|
||||
|
|
|
@ -2,16 +2,35 @@
|
|||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix_web::{error, get, post, web, HttpRequest, Responder};
|
||||
use askama_actix::Template;
|
||||
use actix_web::{error, get, post, web, Responder};
|
||||
use maud::html;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::templates::{self, forms, TemplateConfig};
|
||||
use crate::manage;
|
||||
use crate::models::*;
|
||||
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) {
|
||||
cfg.service(show_item_class)
|
||||
.service(list_item_classes)
|
||||
|
@ -21,17 +40,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||
.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}")]
|
||||
async fn show_item_class(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<DbPool>,
|
||||
path: web::Path<Uuid>,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
|
@ -50,42 +60,131 @@ async fn show_item_class(
|
|||
None => None,
|
||||
};
|
||||
|
||||
Ok(ItemClassDetails {
|
||||
req,
|
||||
item_class,
|
||||
parent,
|
||||
})
|
||||
}
|
||||
let mut title = item_class.name.clone();
|
||||
title.push_str(" – Item Details");
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "item_class_list.html")]
|
||||
struct ItemClassList {
|
||||
req: HttpRequest,
|
||||
item_classes: HashMap<Uuid, ItemClass>,
|
||||
Ok(templates::base(
|
||||
TemplateConfig {
|
||||
path: &format!("/item-class/{}", item_class.id),
|
||||
title: Some(&title),
|
||||
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")]
|
||||
async fn list_item_classes(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<DbPool>,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
async fn list_item_classes(pool: web::Data<DbPool>) -> actix_web::Result<impl Responder> {
|
||||
let item_classes = manage::item_class::get_all_as_map(&mut pool.get().await.unwrap())
|
||||
.await
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
||||
Ok(ItemClassList { req, item_classes })
|
||||
Ok(templates::base(
|
||||
TemplateConfig {
|
||||
path: "/item-classes",
|
||||
title: Some("Item Class List"),
|
||||
page_title: Some(Box::new("Item Class List")),
|
||||
page_actions: vec![
|
||||
(templates::helpers::PageAction {
|
||||
href: "/item-classes/add",
|
||||
name: "Add",
|
||||
}),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
html! {
|
||||
table .table {
|
||||
thead {
|
||||
tr {
|
||||
th { "Name" }
|
||||
th { "Parents" }
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "item_class_add.html")]
|
||||
struct ItemClassAddForm {
|
||||
req: HttpRequest,
|
||||
data: Option<NewItemClass>,
|
||||
}
|
||||
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")]
|
||||
async fn add_item_class(req: HttpRequest) -> actix_web::Result<impl Responder> {
|
||||
Ok(ItemClassAddForm { req, data: None })
|
||||
async fn add_item_class() -> actix_web::Result<impl Responder> {
|
||||
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")]
|
||||
|
@ -99,16 +198,8 @@ async fn add_item_class_post(
|
|||
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")]
|
||||
async fn edit_item_class(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<DbPool>,
|
||||
path: web::Path<Uuid>,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
|
@ -118,7 +209,53 @@ async fn edit_item_class(
|
|||
.await
|
||||
.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")]
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
|
||||
mod item;
|
||||
mod item_class;
|
||||
mod templates;
|
||||
|
||||
use actix_web::{get, web, HttpRequest, Responder};
|
||||
use askama_actix::Template;
|
||||
use actix_web::{get, web, Responder};
|
||||
use maud::html;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(index)
|
||||
|
@ -14,13 +15,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||
.configure(item_class::config);
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "base.html")]
|
||||
struct Home {
|
||||
req: HttpRequest,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn index(req: HttpRequest) -> impl Responder {
|
||||
Home { req }
|
||||
async fn index() -> impl Responder {
|
||||
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,65 +0,0 @@
|
|||
{#
|
||||
SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
#}
|
||||
|
||||
{% let branding = "li7y" -%}
|
||||
<!DOCTYPE html>
|
||||
<html class="h-100">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{% block title %}{{ branding }}{% endblock %}</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="stylesheet" href="/static/vendor/bootstrap.min.css">
|
||||
</head>
|
||||
<body class="d-flex flex-column h-100">
|
||||
<nav class="navbar navbar-expand-lg bg-body-secondary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">{{ branding }}</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-expander">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbar-expander">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-md-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if req.path() == "/" %} active{% endif %}" href="/">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if req.path() == "/items" %} active{% endif %}" href="/items">Items</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if req.path() == "/item-classes" %} active{% endif %}" href="/item-classes">Item Classes</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container my-4">
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<div>
|
||||
<h2>{% block page_title %}{% endblock %}</h2>
|
||||
</div>
|
||||
<div>
|
||||
{% block page_actions %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% block main %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="mt-auto py-3 bg-body-tertiary text-body-tertiary">
|
||||
<div class="container">
|
||||
<p class="mb-0">li7y is free software, released under the terms of the AGPL v3</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/static/vendor/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/app.js"></script>
|
||||
{# TODO this is not the best way, but it works for now #}
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -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,52 +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 %} – Edit Item Class – {{ 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_class.id }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required value="{{ item_class.name }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="type" class="form-label">Type</label>
|
||||
<select class="form-select" id="type" name="type" required value="{{ item_class.type }}">
|
||||
{% 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"{% if let Some(parent) = item_class.parent %} value="{{ parent }}"{% else %} disabled{% endif %}>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Edit</button>
|
||||
</form>
|
||||
<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,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