From 775bc6ba9e916ec6f5c1cb87b78cc95324c79cf0 Mon Sep 17 00:00:00 2001 From: Simon Bruder Date: Sun, 7 Jul 2024 13:48:31 +0200 Subject: [PATCH] Add item classes --- Cargo.lock | 21 ++- Cargo.toml | 1 + .../down.sql | 12 ++ .../up.sql | 47 ++++++ src/api/v1/item_class.rs | 83 +++++++++++ src/api/v1/mod.rs | 3 +- src/frontend/item.rs | 34 ++++- src/frontend/item_class.rs | 140 ++++++++++++++++++ src/frontend/mod.rs | 5 +- src/manage/item_class.rs | 61 ++++++++ src/manage/mod.rs | 1 + src/models.rs | 49 ++++++ src/schema.rs | 23 +++ templates/base.html | 3 + templates/item_add.html | 4 + templates/item_class_add.html | 50 +++++++ templates/item_class_details.html | 51 +++++++ templates/item_class_edit.html | 52 +++++++ templates/item_class_list.html | 31 ++++ templates/item_details.html | 8 + templates/item_edit.html | 4 + templates/item_list.html | 3 + 22 files changed, 679 insertions(+), 7 deletions(-) create mode 100644 migrations/2024-07-05-104056_create_item_class/down.sql create mode 100644 migrations/2024-07-05-104056_create_item_class/up.sql create mode 100644 src/api/v1/item_class.rs create mode 100644 src/frontend/item_class.rs create mode 100644 src/manage/item_class.rs create mode 100644 templates/item_class_add.html create mode 100644 templates/item_class_details.html create mode 100644 templates/item_class_edit.html create mode 100644 templates/item_class_list.html diff --git a/Cargo.lock b/Cargo.lock index 03de86b..a518402 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -595,6 +595,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "diesel-derive-enum" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81c5131a2895ef64741dad1d483f358c2a229a3a2d1b256778cdc5e146db64d4" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "diesel_derives" version = "2.2.1" @@ -646,7 +658,7 @@ checksum = "0892a17df262a24294c382f0d5997571006e7a4348b4327557c4ff1cd4a8bccc" dependencies = [ "darling", "either", - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn", @@ -803,6 +815,12 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -915,6 +933,7 @@ dependencies = [ "askama", "askama_actix", "diesel", + "diesel-derive-enum", "diesel_migrations", "env_logger", "log", diff --git a/Cargo.toml b/Cargo.toml index 7f6ac1d..7e07b2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ actix-web = "4.8.0" askama = { version = "0.12.1", features = ["with-actix-web"] } askama_actix = "0.14.0" diesel = { version = "2.2.1", features = ["postgres", "r2d2", "uuid"] } +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" diff --git a/migrations/2024-07-05-104056_create_item_class/down.sql b/migrations/2024-07-05-104056_create_item_class/down.sql new file mode 100644 index 0000000..816113d --- /dev/null +++ b/migrations/2024-07-05-104056_create_item_class/down.sql @@ -0,0 +1,12 @@ +-- SPDX-FileCopyrightText: 2024 Simon Bruder +-- +-- SPDX-License-Identifier: AGPL-3.0-or-later + +ALTER TABLE items + DROP class; + +DROP TABLE item_classes; + +DROP TYPE item_class_type; + +DROP FUNCTION check_item_class_parent; diff --git a/migrations/2024-07-05-104056_create_item_class/up.sql b/migrations/2024-07-05-104056_create_item_class/up.sql new file mode 100644 index 0000000..5fcd26c --- /dev/null +++ b/migrations/2024-07-05-104056_create_item_class/up.sql @@ -0,0 +1,47 @@ +-- SPDX-FileCopyrightText: 2024 Simon Bruder +-- +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- generic is a category of products (like CR2032 3 V battery) +-- or a product where details do not matter (like USB-A to USB-Micro-B cable 1 m) +-- specific is a concrete model of product (like Panasonic DL2032) +CREATE TYPE item_class_type AS ENUM ('generic', 'specific'); + +CREATE TABLE item_classes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR NOT NULL, + type item_class_type NOT NULL DEFAULT 'generic', + parent UUID REFERENCES item_classes(id), + CONSTRAINT parent_only_for_specific CHECK (type = 'generic' AND parent IS NULL OR type = 'specific' AND parent IS NOT NULL) +); + +INSERT INTO item_classes (id, name) values ('afd0e9bd-c4df-425e-8af4-6b5b0326a4ae', 'Default Item Class'); + +ALTER TABLE items + ADD class UUID NOT NULL REFERENCES item_classes(id) DEFAULT 'afd0e9bd-c4df-425e-8af4-6b5b0326a4ae'; +ALTER TABLE items + ALTER class DROP DEFAULT; + +DELETE FROM item_classes + WHERE (id = 'afd0e9bd-c4df-425e-8af4-6b5b0326a4ae' + AND NOT EXISTS (SELECT 1 FROM items WHERE class = 'afd0e9bd-c4df-425e-8af4-6b5b0326a4ae' LIMIT 1)); + +CREATE FUNCTION check_item_class_parent() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.parent IS NULL THEN + RETURN NEW; + END IF; + + IF (SELECT type FROM item_classes WHERE id = NEW.parent) = 'generic' THEN + RETURN NEW; + END IF; + + RAISE EXCEPTION 'Specific item classes may only have a generic parent (to avoid recursion)'; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER prevent_item_class_recursion +BEFORE INSERT OR UPDATE ON item_classes +FOR EACH ROW +EXECUTE FUNCTION check_item_class_parent(); diff --git a/src/api/v1/item_class.rs b/src/api/v1/item_class.rs new file mode 100644 index 0000000..4756efd --- /dev/null +++ b/src/api/v1/item_class.rs @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2024 Simon Bruder +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use actix_web::{error, get, post, put, web, HttpResponse, Responder}; +use uuid::Uuid; + +use crate::manage; +use crate::models::*; +use crate::DbPool; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(add) + .service(list) + .service(show) + .service(items) + .service(update); +} + +#[put("/item-class")] +async fn add( + pool: web::Data, + new_item_class: web::Json, +) -> actix_web::Result { + let item_class = web::block(move || { + manage::item_class::add(&mut pool.get().unwrap(), new_item_class.into_inner()) + }) + .await? + .map_err(error::ErrorInternalServerError)?; + + Ok(HttpResponse::Ok().json(item_class)) +} + +#[get("/item-class")] +async fn list(pool: web::Data) -> actix_web::Result { + let item_classes = web::block(move || manage::item_class::get_all(&mut pool.get().unwrap())) + .await? + .map_err(error::ErrorInternalServerError)?; + + Ok(HttpResponse::Ok().json(item_classes)) +} + +#[get("/item-class/{id}")] +async fn show(pool: web::Data, path: web::Path) -> actix_web::Result { + let id = path.into_inner(); + + let item_class = web::block(move || manage::item_class::get(&mut pool.get().unwrap(), id)) + .await? + .map_err(error::ErrorInternalServerError)?; + + Ok(HttpResponse::Ok().json(item_class)) +} + +#[get("/item-class/{id}/items")] +async fn items( + pool: web::Data, + path: web::Path, +) -> actix_web::Result { + let id = path.into_inner(); + + let items = web::block(move || manage::item_class::items(&mut pool.get().unwrap(), id)) + .await? + .map_err(error::ErrorInternalServerError)?; + + Ok(HttpResponse::Ok().json(items)) +} + +#[post("/item-class/{id}")] +async fn update( + pool: web::Data, + path: web::Path, + new_item_class: web::Json, +) -> actix_web::Result { + let id = path.into_inner(); + + let item_class = web::block(move || { + manage::item_class::update(&mut pool.get().unwrap(), id, new_item_class.into_inner()) + }) + .await? + .map_err(error::ErrorInternalServerError)?; + + Ok(HttpResponse::Ok().json(item_class)) +} diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 1793340..09e9d9a 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -3,9 +3,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later mod item; +mod item_class; use actix_web::web; pub fn config(cfg: &mut web::ServiceConfig) { - cfg.configure(item::config); + cfg.configure(item::config).configure(item_class::config); } diff --git a/src/frontend/item.rs b/src/frontend/item.rs index f40ee55..4085beb 100644 --- a/src/frontend/item.rs +++ b/src/frontend/item.rs @@ -2,6 +2,8 @@ // // 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 uuid::Uuid; @@ -24,6 +26,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { struct ItemDetails { req: HttpRequest, item: Item, + item_class: ItemClass, } #[get("/item/{id}")] @@ -34,11 +37,22 @@ async fn show_item( ) -> actix_web::Result { let id = path.into_inner(); - let item = web::block(move || manage::item::get(&mut pool.get().unwrap(), id)) + let mut conn = pool.get().unwrap(); + + let item = web::block(move || manage::item::get(&mut conn, id)) .await? .map_err(error::ErrorInternalServerError)?; - Ok(ItemDetails { req, item }) + let item_class = + web::block(move || manage::item_class::get(&mut pool.get().unwrap(), item.class)) + .await? + .map_err(error::ErrorInternalServerError)?; + + Ok(ItemDetails { + req, + item, + item_class, + }) } #[derive(Template)] @@ -46,6 +60,7 @@ async fn show_item( struct ItemList { req: HttpRequest, items: Vec, + item_classes: HashMap, } #[get("/items")] @@ -53,11 +68,22 @@ async fn list_items( req: HttpRequest, pool: web::Data, ) -> actix_web::Result { - let items = web::block(move || manage::item::get_all(&mut pool.get().unwrap())) + let mut conn = pool.get().unwrap(); + + let items = web::block(move || manage::item::get_all(&mut conn)) .await? .map_err(error::ErrorInternalServerError)?; - Ok(ItemList { req, items }) + let item_classes = + web::block(move || manage::item_class::get_all_as_map(&mut pool.get().unwrap())) + .await? + .map_err(error::ErrorInternalServerError)?; + + Ok(ItemList { + req, + items, + item_classes, + }) } #[derive(Template)] diff --git a/src/frontend/item_class.rs b/src/frontend/item_class.rs new file mode 100644 index 0000000..c78e8c6 --- /dev/null +++ b/src/frontend/item_class.rs @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: 2024 Simon Bruder +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use actix_web::{error, get, post, web, HttpRequest, Responder}; +use askama_actix::Template; +use uuid::Uuid; + +use crate::manage; +use crate::models::*; +use crate::DbPool; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(show_item_class) + .service(list_item_classes) + .service(add_item_class) + .service(add_item_class_post) + .service(edit_item_class) + .service(edit_item_class_post); +} + +#[derive(Template)] +#[template(path = "item_class_details.html")] +struct ItemClassDetails { + req: HttpRequest, + item_class: ItemClass, + parent: Option, +} + +#[get("/item-class/{id}")] +async fn show_item_class( + req: HttpRequest, + pool: web::Data, + path: web::Path, +) -> actix_web::Result { + let id = path.into_inner(); + + // FIXME hack + let pool_ = pool.clone(); + + let item_class = web::block(move || manage::item_class::get(&mut pool.get().unwrap(), id)) + .await? + .map_err(error::ErrorInternalServerError)?; + + let parent = web::block(move || { + item_class.parent.map_or(Ok(None), |id| { + manage::item_class::get(&mut pool_.get().unwrap(), id).map(Some) + }) + }) + .await? + .map_err(error::ErrorInternalServerError)?; + + Ok(ItemClassDetails { + req, + item_class, + parent, + }) +} + +#[derive(Template)] +#[template(path = "item_class_list.html")] +struct ItemClassList { + req: HttpRequest, + item_classes: Vec, +} + +#[get("/item-classes")] +async fn list_item_classes( + req: HttpRequest, + pool: web::Data, +) -> actix_web::Result { + let item_classes = web::block(move || manage::item_class::get_all(&mut pool.get().unwrap())) + .await? + .map_err(error::ErrorInternalServerError)?; + + Ok(ItemClassList { req, item_classes }) +} + +#[derive(Template)] +#[template(path = "item_class_add.html")] +struct ItemClassAddForm { + req: HttpRequest, + data: Option, +} + +#[get("/item-classes/add")] +async fn add_item_class(req: HttpRequest) -> actix_web::Result { + Ok(ItemClassAddForm { req, data: None }) +} + +#[post("/item-classes/add")] +async fn add_item_class_post( + data: web::Form, + pool: web::Data, +) -> actix_web::Result { + let item = + web::block(move || manage::item_class::add(&mut pool.get().unwrap(), data.into_inner())) + .await? + .map_err(error::ErrorInternalServerError)?; + 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, + path: web::Path, +) -> actix_web::Result { + let id = path.into_inner(); + + let item_class = web::block(move || manage::item_class::get(&mut pool.get().unwrap(), id)) + .await? + .map_err(error::ErrorInternalServerError)?; + + Ok(ItemClassEditForm { req, item_class }) +} + +#[post("/item-class/{id}/edit")] +async fn edit_item_class_post( + pool: web::Data, + path: web::Path, + data: web::Form, +) -> actix_web::Result { + let id = path.into_inner(); + + let item_class = web::block(move || { + manage::item_class::update(&mut pool.get().unwrap(), id, data.into_inner()) + }) + .await? + .map_err(error::ErrorInternalServerError)?; + + Ok(web::Redirect::to("/item-class/".to_owned() + &item_class.id.to_string()).see_other()) +} diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs index 1698b41..0594e79 100644 --- a/src/frontend/mod.rs +++ b/src/frontend/mod.rs @@ -3,12 +3,15 @@ // SPDX-License-Identifier: AGPL-3.0-or-later mod item; +mod item_class; use actix_web::{get, web, HttpRequest, Responder}; use askama_actix::Template; pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service(index).configure(item::config); + cfg.service(index) + .configure(item::config) + .configure(item_class::config); } #[derive(Template)] diff --git a/src/manage/item_class.rs b/src/manage/item_class.rs new file mode 100644 index 0000000..d2950fb --- /dev/null +++ b/src/manage/item_class.rs @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2024 Simon Bruder +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::collections::HashMap; + +use diesel::pg::PgConnection; +use diesel::prelude::*; +use uuid::Uuid; + +use crate::{models::*, schema}; + +pub fn add( + conn: &mut PgConnection, + new_item_class: NewItemClass, +) -> Result { + diesel::insert_into(schema::item_classes::table) + .values(new_item_class) + .returning(ItemClass::as_returning()) + .get_result(conn) +} + +pub fn get(conn: &mut PgConnection, id: Uuid) -> Result { + schema::item_classes::table + .filter(schema::item_classes::id.eq(id)) + .select(ItemClass::as_select()) + .first(conn) +} + +pub fn get_all(conn: &mut PgConnection) -> Result, diesel::result::Error> { + schema::item_classes::table + .select(ItemClass::as_select()) + .load(conn) +} + +pub fn get_all_as_map( + conn: &mut PgConnection, +) -> Result, diesel::result::Error> { + Ok(get_all(conn)? + .into_iter() + .map(|ic| (ic.id, ic)) + .collect()) +} + +pub fn update( + conn: &mut PgConnection, + id: Uuid, + modified_item_class: NewItemClass, +) -> Result { + diesel::update(schema::item_classes::table.filter(schema::item_classes::id.eq(id))) + .set(modified_item_class) + .returning(ItemClass::as_returning()) + .get_result(conn) +} + +pub fn items(conn: &mut PgConnection, id: Uuid) -> Result, diesel::result::Error> { + schema::items::table + .filter(schema::items::class.eq(id)) + .select(Item::as_select()) + .load(conn) +} diff --git a/src/manage/mod.rs b/src/manage/mod.rs index 4ee270c..e5adb01 100644 --- a/src/manage/mod.rs +++ b/src/manage/mod.rs @@ -3,3 +3,4 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pub mod item; +pub mod item_class; diff --git a/src/models.rs b/src/models.rs index 26cdf84..7949e88 100644 --- a/src/models.rs +++ b/src/models.rs @@ -2,7 +2,10 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +use std::fmt; + use diesel::prelude::*; +use diesel_derive_enum::DbEnum; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -15,6 +18,7 @@ pub struct Item { pub id: Uuid, pub name: String, pub parent: Option, + pub class: Uuid, } #[derive(Debug, Insertable, Deserialize, AsChangeset)] @@ -24,4 +28,49 @@ pub struct NewItem { pub name: String, #[serde(default)] pub parent: Option, + pub class: Uuid, +} + +#[derive(Debug, DbEnum, PartialEq, Deserialize, Serialize)] +#[ExistingTypePath = "sql_types::ItemClassType"] +#[serde(rename_all = "snake_case")] +pub enum ItemClassType { + Generic, + Specific, +} + +impl ItemClassType { + pub const VARIANTS: [ItemClassType; 2] = [Self::Generic, Self::Specific]; +} + +impl fmt::Display for ItemClassType { + // FIXME: currently, the HTML form relies on this matching the serde serializsation. + // This should not be the case. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Generic => write!(f, "generic"), + Self::Specific => write!(f, "specific"), + } + } +} + +#[derive(Debug, Queryable, Selectable, Insertable, Serialize)] +#[diesel(table_name = item_classes)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct ItemClass { + pub id: Uuid, + pub name: String, + pub r#type: ItemClassType, + #[serde(skip_serializing_if = "Option::is_none")] + pub parent: Option, +} + +#[derive(Debug, Insertable, Deserialize, AsChangeset)] +#[diesel(table_name = item_classes)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct NewItemClass { + pub name: String, + pub r#type: ItemClassType, + #[serde(default)] + pub parent: Option, } diff --git a/src/schema.rs b/src/schema.rs index 2db38b5..799cfa4 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -2,11 +2,18 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +pub mod sql_types { + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "item_class_type"))] + pub struct ItemClassType; +} + diesel::table! { items (id) { id -> Uuid, name -> Varchar, parent -> Nullable, + class -> Uuid, } } @@ -16,3 +23,19 @@ diesel::table! { parents -> Array, } } + +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::ItemClassType; + + item_classes (id) { + id -> Uuid, + name -> Varchar, + r#type -> ItemClassType, + parent -> Nullable, + } +} + +diesel::joinable!(items -> item_classes (class)); + +diesel::allow_tables_to_appear_in_same_query!(item_classes, items); diff --git a/templates/base.html b/templates/base.html index ba58d23..bc21533 100644 --- a/templates/base.html +++ b/templates/base.html @@ -31,6 +31,9 @@ SPDX-License-Identifier: AGPL-3.0-or-later + diff --git a/templates/item_add.html b/templates/item_add.html index ec01bca..c31cb52 100644 --- a/templates/item_add.html +++ b/templates/item_add.html @@ -12,6 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-or-later +
+ + +
diff --git a/templates/item_class_add.html b/templates/item_class_add.html new file mode 100644 index 0000000..944f3f7 --- /dev/null +++ b/templates/item_class_add.html @@ -0,0 +1,50 @@ +{# +SPDX-FileCopyrightText: 2024 Simon Bruder + +SPDX-License-Identifier: AGPL-3.0-or-later +#} + +{% extends "base.html" %} +{% block title %}{% block page_title %}Add Item Class{% endblock %} – {{ branding }}{% endblock %} +{% block main %} +
+
+ + +
+
+ + +
+
+ + +
+ +
+{% endblock %} +{% block extra_scripts %} + +{% endblock %} diff --git a/templates/item_class_details.html b/templates/item_class_details.html new file mode 100644 index 0000000..200802b --- /dev/null +++ b/templates/item_class_details.html @@ -0,0 +1,51 @@ +{# +SPDX-FileCopyrightText: 2024 Simon Bruder + +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 %} +Edit +{% endblock %} +{% block main %} + + + + + + + + + + + + + + + {% if let Some(parent) = parent -%} + + + + + {%- endif %} + +
+ UUID + + {{ item_class.id }} +
+ Name + + {{ item_class.name }} +
+ Type + + {{ item_class.type }} +
+ Parent + + {{ parent.name }} ({{ parent.id }}) +
+{% endblock %} diff --git a/templates/item_class_edit.html b/templates/item_class_edit.html new file mode 100644 index 0000000..610e5c3 --- /dev/null +++ b/templates/item_class_edit.html @@ -0,0 +1,52 @@ +{# +SPDX-FileCopyrightText: 2024 Simon Bruder + +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 %} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +{% endblock %} diff --git a/templates/item_class_list.html b/templates/item_class_list.html new file mode 100644 index 0000000..b2ee6aa --- /dev/null +++ b/templates/item_class_list.html @@ -0,0 +1,31 @@ +{# +SPDX-FileCopyrightText: 2024 Simon Bruder + +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 %} +Add +{% endblock %} +{% block main %} + + + + + + + + + + {% for item_class in item_classes -%} + + + + + + {% endfor -%} + +
UUIDNameType
{{ item_class.id }}{{ item_class.name }}{{ item_class.type }}
+{% endblock %} diff --git a/templates/item_details.html b/templates/item_details.html index 8f4a28d..294fc6d 100644 --- a/templates/item_details.html +++ b/templates/item_details.html @@ -28,6 +28,14 @@ SPDX-License-Identifier: AGPL-3.0-or-later {{ item.name }} + + + Class + + + {{ item_class.name }} + + Parent diff --git a/templates/item_edit.html b/templates/item_edit.html index b28744f..5c1efdc 100644 --- a/templates/item_edit.html +++ b/templates/item_edit.html @@ -16,6 +16,10 @@ SPDX-License-Identifier: AGPL-3.0-or-later
+
+ + +
diff --git a/templates/item_list.html b/templates/item_list.html index b6e32e5..e586a58 100644 --- a/templates/item_list.html +++ b/templates/item_list.html @@ -15,13 +15,16 @@ SPDX-License-Identifier: AGPL-3.0-or-later UUID Name + Class {% for item in items -%} + {% let class = item_classes.get(item.class).unwrap() %} {{ item.id }} {{ item.name }} + {{ class.name }} {% endfor -%}