Add item classes

This commit is contained in:
Simon Bruder 2024-07-07 13:48:31 +02:00
parent ae50056252
commit 775bc6ba9e
Signed by: simon
GPG key ID: 347FF8699CDA0776
22 changed files with 679 additions and 7 deletions

21
Cargo.lock generated
View file

@ -595,6 +595,18 @@ dependencies = [
"uuid", "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]] [[package]]
name = "diesel_derives" name = "diesel_derives"
version = "2.2.1" version = "2.2.1"
@ -646,7 +658,7 @@ checksum = "0892a17df262a24294c382f0d5997571006e7a4348b4327557c4ff1cd4a8bccc"
dependencies = [ dependencies = [
"darling", "darling",
"either", "either",
"heck", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
@ -803,6 +815,12 @@ version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@ -915,6 +933,7 @@ dependencies = [
"askama", "askama",
"askama_actix", "askama_actix",
"diesel", "diesel",
"diesel-derive-enum",
"diesel_migrations", "diesel_migrations",
"env_logger", "env_logger",
"log", "log",

View file

@ -15,6 +15,7 @@ actix-web = "4.8.0"
askama = { version = "0.12.1", features = ["with-actix-web"] } askama = { version = "0.12.1", features = ["with-actix-web"] }
askama_actix = "0.14.0" askama_actix = "0.14.0"
diesel = { version = "2.2.1", features = ["postgres", "r2d2", "uuid"] } 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"] } 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"

View file

@ -0,0 +1,12 @@
-- SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
--
-- 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;

View file

@ -0,0 +1,47 @@
-- SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- generic is a category of products (like CR2032 3V battery)
-- or a product where details do not matter (like USB-A to USB-Micro-B cable 1m)
-- 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();

83
src/api/v1/item_class.rs Normal file
View file

@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
//
// 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<DbPool>,
new_item_class: web::Json<NewItemClass>,
) -> actix_web::Result<impl Responder> {
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<DbPool>) -> actix_web::Result<impl Responder> {
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<DbPool>, path: web::Path<Uuid>) -> actix_web::Result<impl Responder> {
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<DbPool>,
path: web::Path<Uuid>,
) -> actix_web::Result<impl Responder> {
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<DbPool>,
path: web::Path<Uuid>,
new_item_class: web::Json<NewItemClass>,
) -> actix_web::Result<impl Responder> {
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))
}

View file

@ -3,9 +3,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
mod item; mod item;
mod item_class;
use actix_web::web; use actix_web::web;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.configure(item::config); cfg.configure(item::config).configure(item_class::config);
} }

View file

@ -2,6 +2,8 @@
// //
// 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, HttpRequest, Responder}; use actix_web::{error, get, post, web, HttpRequest, Responder};
use askama_actix::Template; use askama_actix::Template;
use uuid::Uuid; use uuid::Uuid;
@ -24,6 +26,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
struct ItemDetails { struct ItemDetails {
req: HttpRequest, req: HttpRequest,
item: Item, item: Item,
item_class: ItemClass,
} }
#[get("/item/{id}")] #[get("/item/{id}")]
@ -34,11 +37,22 @@ async fn show_item(
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let id = path.into_inner(); 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? .await?
.map_err(error::ErrorInternalServerError)?; .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)] #[derive(Template)]
@ -46,6 +60,7 @@ async fn show_item(
struct ItemList { struct ItemList {
req: HttpRequest, req: HttpRequest,
items: Vec<Item>, items: Vec<Item>,
item_classes: HashMap<Uuid, ItemClass>,
} }
#[get("/items")] #[get("/items")]
@ -53,11 +68,22 @@ async fn list_items(
req: HttpRequest, req: HttpRequest,
pool: web::Data<DbPool>, pool: web::Data<DbPool>,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
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? .await?
.map_err(error::ErrorInternalServerError)?; .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)] #[derive(Template)]

140
src/frontend/item_class.rs Normal file
View file

@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
//
// 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<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> {
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<ItemClass>,
}
#[get("/item-classes")]
async fn list_item_classes(
req: HttpRequest,
pool: web::Data<DbPool>,
) -> actix_web::Result<impl Responder> {
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<NewItemClass>,
}
#[get("/item-classes/add")]
async fn add_item_class(req: HttpRequest) -> actix_web::Result<impl Responder> {
Ok(ItemClassAddForm { req, data: None })
}
#[post("/item-classes/add")]
async fn add_item_class_post(
data: web::Form<NewItemClass>,
pool: web::Data<DbPool>,
) -> actix_web::Result<impl Responder> {
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<DbPool>,
path: web::Path<Uuid>,
) -> actix_web::Result<impl Responder> {
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<DbPool>,
path: web::Path<Uuid>,
data: web::Form<NewItemClass>,
) -> actix_web::Result<impl Responder> {
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())
}

View file

@ -3,12 +3,15 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
mod item; mod item;
mod item_class;
use actix_web::{get, web, HttpRequest, Responder}; use actix_web::{get, web, HttpRequest, Responder};
use askama_actix::Template; use askama_actix::Template;
pub fn config(cfg: &mut web::ServiceConfig) { 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)] #[derive(Template)]

61
src/manage/item_class.rs Normal file
View file

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
//
// 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<ItemClass, diesel::result::Error> {
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<ItemClass, diesel::result::Error> {
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<Vec<ItemClass>, diesel::result::Error> {
schema::item_classes::table
.select(ItemClass::as_select())
.load(conn)
}
pub fn get_all_as_map(
conn: &mut PgConnection,
) -> Result<HashMap<Uuid, ItemClass>, 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<ItemClass, diesel::result::Error> {
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<Vec<Item>, diesel::result::Error> {
schema::items::table
.filter(schema::items::class.eq(id))
.select(Item::as_select())
.load(conn)
}

View file

@ -3,3 +3,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
pub mod item; pub mod item;
pub mod item_class;

View file

@ -2,7 +2,10 @@
// //
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
use std::fmt;
use diesel::prelude::*; use diesel::prelude::*;
use diesel_derive_enum::DbEnum;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
@ -15,6 +18,7 @@ pub struct Item {
pub id: Uuid, pub id: Uuid,
pub name: String, pub name: String,
pub parent: Option<Uuid>, pub parent: Option<Uuid>,
pub class: Uuid,
} }
#[derive(Debug, Insertable, Deserialize, AsChangeset)] #[derive(Debug, Insertable, Deserialize, AsChangeset)]
@ -24,4 +28,49 @@ pub struct NewItem {
pub name: String, pub name: String,
#[serde(default)] #[serde(default)]
pub parent: Option<Uuid>, pub parent: Option<Uuid>,
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<Uuid>,
}
#[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<Uuid>,
} }

View file

@ -2,11 +2,18 @@
// //
// SPDX-License-Identifier: AGPL-3.0-or-later // 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! { diesel::table! {
items (id) { items (id) {
id -> Uuid, id -> Uuid,
name -> Varchar, name -> Varchar,
parent -> Nullable<Uuid>, parent -> Nullable<Uuid>,
class -> Uuid,
} }
} }
@ -16,3 +23,19 @@ diesel::table! {
parents -> Array<Uuid>, parents -> Array<Uuid>,
} }
} }
diesel::table! {
use diesel::sql_types::*;
use super::sql_types::ItemClassType;
item_classes (id) {
id -> Uuid,
name -> Varchar,
r#type -> ItemClassType,
parent -> Nullable<Uuid>,
}
}
diesel::joinable!(items -> item_classes (class));
diesel::allow_tables_to_appear_in_same_query!(item_classes, items);

View file

@ -31,6 +31,9 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{% if req.path() == "/items" %} active{% endif %}" href="/items">Items</a> <a class="nav-link{% if req.path() == "/items" %} active{% endif %}" href="/items">Items</a>
</li> </li>
<li class="nav-item">
<a class="nav-link{% if req.path() == "/item-classes" %} active{% endif %}" href="/item-classes">Item Classes</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -12,6 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<label for="name" class="form-label">Name</label> <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 %}> <input type="text" class="form-control" id="name" name="name" required{% if let Some(data) = data %} value="{{ data.name }}"{% 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"> <div class="mb-3">
<label for="parent" class="form-label">Parent</label> <label for="parent" class="form-label">Parent</label>
<div class="input-group"> <div class="input-group">

View file

@ -0,0 +1,50 @@
{#
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 %}

View file

@ -0,0 +1,51 @@
{#
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 }} ({{ parent.id }})</a>
</td>
</tr>
{%- endif %}
</tbody>
</table>
{% endblock %}

View file

@ -0,0 +1,52 @@
{#
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 %}

View file

@ -0,0 +1,31 @@
{#
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>UUID</th>
<th>Name</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{% for item_class in item_classes -%}
<tr>
<td><a href="/item-class/{{ item_class.id }}">{{ item_class.id }}</a></td>
<td>{{ item_class.name }}</td>
<td>{{ item_class.type }}</td>
</tr>
{% endfor -%}
</tbody>
</table>
{% endblock %}

View file

@ -28,6 +28,14 @@ SPDX-License-Identifier: AGPL-3.0-or-later
{{ item.name }} {{ item.name }}
</td> </td>
</tr> </tr>
<tr>
<th>
Class
</th>
<td>
<a href="/item-class/{{ item.class }}">{{ item_class.name }}</a>
</td>
</tr>
<tr> <tr>
<th> <th>
Parent Parent

View file

@ -16,6 +16,10 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<label for="name" class="form-label">Name</label> <label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" required value="{{ item.name }}"> <input type="text" class="form-control" id="name" name="name" required value="{{ item.name }}">
</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"> <div class="mb-3">
<label for="parent" class="form-label">Parent</label> <label for="parent" class="form-label">Parent</label>
<div class="input-group"> <div class="input-group">

View file

@ -15,13 +15,16 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<tr> <tr>
<th>UUID</th> <th>UUID</th>
<th>Name</th> <th>Name</th>
<th>Class</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for item in items -%} {% for item in items -%}
{% let class = item_classes.get(item.class).unwrap() %}
<tr> <tr>
<td><a href="/item/{{ item.id }}">{{ item.id }}</a></td> <td><a href="/item/{{ item.id }}">{{ item.id }}</a></td>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td><a href="/item-class/{{ class.id }}">{{ class.name }}</a></td>
</tr> </tr>
{% endfor -%} {% endfor -%}
</tbody> </tbody>