Make item name optional

This vastly changes how item names are displayed. To make this more
ergonomic, it adds some helper macros. This also aids in adhering to
DRY.
This commit is contained in:
Simon Bruder 2024-07-10 01:14:49 +02:00
parent 5147257e72
commit c4a73204aa
Signed by: simon
GPG key ID: 347FF8699CDA0776
10 changed files with 131 additions and 32 deletions

View file

@ -0,0 +1,13 @@
-- SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
ALTER TABLE items
DROP CONSTRAINT name_not_empty;
UPDATE items
SET name = ''
WHERE NAME IS NULL;
ALTER TABLE items
ALTER name SET NOT NULL;

View file

@ -0,0 +1,13 @@
-- SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
ALTER TABLE items
ALTER name DROP NOT NULL;
UPDATE items
SET name = NULL
WHERE name = '';
ALTER TABLE items
ADD CONSTRAINT name_not_empty CHECK(name <> '');

View file

@ -27,6 +27,7 @@ struct ItemDetails {
req: HttpRequest, req: HttpRequest,
item: Item, item: Item,
item_class: ItemClass, item_class: ItemClass,
item_classes: HashMap<Uuid, ItemClass>,
parents: Vec<Item>, parents: Vec<Item>,
children: Vec<Item>, children: Vec<Item>,
} }
@ -43,10 +44,13 @@ async fn show_item(
.await .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
let item_class = manage::item_class::get(&mut pool.get().await.unwrap(), item.class) let item_classes = manage::item_class::get_all_as_map(&mut pool.get().await.unwrap())
.await .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
// TODO: remove clone (should be possible without it)
let item_class = item_classes.get(&item.class).unwrap().clone();
let parents = manage::item::get_parents_details(&mut pool.get().await.unwrap(), item.id) let parents = manage::item::get_parents_details(&mut pool.get().await.unwrap(), item.id)
.await .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
@ -59,6 +63,7 @@ async fn show_item(
req, req,
item, item,
item_class, item_class,
item_classes,
parents, parents,
children, children,
}) })
@ -71,9 +76,13 @@ struct ItemList {
// Both a Vec and a HashMap are used to have both the natural order, // Both a Vec and a HashMap are used to have both the natural order,
// as well as arbitrary access capabilities. // as well as arbitrary access capabilities.
item_list: Vec<Item>, item_list: Vec<Item>,
#[allow(dead_code)] // remove once item_parents can be constructed in the template
items: HashMap<Uuid, Item>, items: HashMap<Uuid, Item>,
item_classes: HashMap<Uuid, ItemClass>, item_classes: HashMap<Uuid, ItemClass>,
#[allow(dead_code)] // remove once item_parents can be constructed in the template
item_tree: HashMap<Uuid, Vec<Uuid>>, item_tree: HashMap<Uuid, Vec<Uuid>>,
// to overcome askamas lack of support for closures
item_parents: HashMap<Uuid, Vec<Item>>,
} }
#[get("/items")] #[get("/items")]
@ -97,12 +106,29 @@ async fn list_items(
.await .await
.map_err(error::ErrorInternalServerError)?; .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()
.iter()
.map(|is| items.get(is).unwrap().clone())
.collect()
})
})
.collect();
Ok(ItemList { Ok(ItemList {
req, req,
item_list, item_list,
items, items,
item_classes, item_classes,
item_tree, item_tree,
item_parents,
}) })
} }
@ -134,6 +160,7 @@ async fn add_item_post(
struct ItemEditForm { struct ItemEditForm {
req: HttpRequest, req: HttpRequest,
item: Item, item: Item,
item_class: ItemClass,
} }
#[get("/item/{id}/edit")] #[get("/item/{id}/edit")]
@ -148,7 +175,15 @@ async fn edit_item(
.await .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
Ok(ItemEditForm { req, item }) let item_class = manage::item_class::get(&mut pool.get().await.unwrap(), item.class)
.await
.map_err(error::ErrorInternalServerError)?;
Ok(ItemEditForm {
req,
item,
item_class,
})
} }
#[post("/item/{id}/edit")] #[post("/item/{id}/edit")]

View file

@ -11,12 +11,12 @@ use uuid::Uuid;
use crate::schema::*; use crate::schema::*;
#[derive(Debug, Queryable, Selectable, Insertable, Serialize)] #[derive(Clone, Debug, Queryable, Selectable, Insertable, Serialize)]
#[diesel(table_name = items)] #[diesel(table_name = items)]
#[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Item { pub struct Item {
pub id: Uuid, pub id: Uuid,
pub name: String, pub name: Option<String>,
pub parent: Option<Uuid>, pub parent: Option<Uuid>,
pub class: Uuid, pub class: Uuid,
} }
@ -24,14 +24,16 @@ pub struct Item {
#[derive(Debug, Insertable, Deserialize, AsChangeset)] #[derive(Debug, Insertable, Deserialize, AsChangeset)]
#[diesel(table_name = items)] #[diesel(table_name = items)]
#[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(check_for_backend(diesel::pg::Pg))]
#[diesel(treat_none_as_null = true)]
pub struct NewItem { pub struct NewItem {
pub name: String, #[serde(default)]
pub name: Option<String>,
#[serde(default)] #[serde(default)]
pub parent: Option<Uuid>, pub parent: Option<Uuid>,
pub class: Uuid, pub class: Uuid,
} }
#[derive(Debug, DbEnum, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, DbEnum, PartialEq, Deserialize, Serialize)]
#[ExistingTypePath = "sql_types::ItemClassType"] #[ExistingTypePath = "sql_types::ItemClassType"]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum ItemClassType { pub enum ItemClassType {
@ -54,7 +56,7 @@ impl fmt::Display for ItemClassType {
} }
} }
#[derive(Debug, Queryable, Selectable, Insertable, Serialize)] #[derive(Clone, Debug, Queryable, Selectable, Insertable, Serialize)]
#[diesel(table_name = item_classes)] #[diesel(table_name = item_classes)]
#[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(check_for_backend(diesel::pg::Pg))]
pub struct ItemClass { pub struct ItemClass {

View file

@ -11,7 +11,7 @@ pub mod sql_types {
diesel::table! { diesel::table! {
items (id) { items (id) {
id -> Uuid, id -> Uuid,
name -> Varchar, name -> Nullable<Varchar>,
parent -> Nullable<Uuid>, parent -> Nullable<Uuid>,
class -> Uuid, class -> Uuid,
} }

View file

@ -10,7 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<form method="POST"> <form method="POST">
<div class="mb-3"> <div class="mb-3">
<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 %}> <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>
<div class="mb-3"> <div class="mb-3">
<label for="class" class="form-label">Class</label> <label for="class" class="form-label">Class</label>

View file

@ -4,8 +4,9 @@ SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
SPDX-License-Identifier: AGPL-3.0-or-later SPDX-License-Identifier: AGPL-3.0-or-later
#} #}
{%- import "macros.html" as macros -%}
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{% block page_title %}{{ item.name }}{% endblock %} Item Details {{ branding }}{% endblock %} {% block title %}{% block page_title %}{% call macros::item_name(item, item_class, false) %}{% endblock %} Item Details {{ branding }}{% endblock %}
{% block page_actions %} {% block page_actions %}
<a class="btn btn-warning" href="/item/{{ item.id }}/edit">Edit</a> <a class="btn btn-warning" href="/item/{{ item.id }}/edit">Edit</a>
{% endblock %} {% endblock %}
@ -23,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
Name Name
</th> </th>
<td> <td>
{{ item.name }} {% call macros::item_name_terse(item, true) %}
</td> </td>
</tr> </tr>
<tr> <tr>
@ -39,12 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
Parents Parents
</th> </th>
<td> <td>
<ol class="breadcrumb mb-0"> {%- call macros::parents_breadcrumb(item, parents, item_classes, full=true) %}
{%- for parent in parents %}
<li class="breadcrumb-item"><a href="/item/{{ parent.id }}">{{ parent.name }}</a></li>
{%- endfor %}
<li class="breadcrumb-item active">{{ item.name }}</li>
</ol>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -55,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<ul> <ul>
{% for child in children %} {% for child in children %}
<li><a href="/item/{{ child.id }}">{{ child.name }}</a></li> <li><a href="/item/{{ child.id }}">{% call macros::item_name(child, item_classes.get(child.class).unwrap(), true) %}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}

View file

@ -4,8 +4,9 @@ SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
SPDX-License-Identifier: AGPL-3.0-or-later SPDX-License-Identifier: AGPL-3.0-or-later
#} #}
{%- import "macros.html" as macros -%}
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{% block page_title %}{{ item.name }}{% endblock %} Edit Item {{ branding }}{% endblock %} {% block title %}{% block page_title %}{% call macros::item_name(item, item_class, false) %}{% endblock %} Edit Item {{ branding }}{% endblock %}
{% block main %} {% block main %}
<form method="POST"> <form method="POST">
<div class="mb-3"> <div class="mb-3">
@ -14,7 +15,12 @@ SPDX-License-Identifier: AGPL-3.0-or-later
</div> </div>
<div class="mb-3"> <div class="mb-3">
<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 }}"> <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>
<div class="mb-3"> <div class="mb-3">
<label for="class" class="form-label">Class</label> <label for="class" class="form-label">Class</label>

View file

@ -4,6 +4,7 @@ SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
SPDX-License-Identifier: AGPL-3.0-or-later SPDX-License-Identifier: AGPL-3.0-or-later
#} #}
{%- import "macros.html" as macros -%}
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{% block page_title %}Item List{% endblock %} {{ branding }}{% endblock %} {% block title %}{% block page_title %}Item List{% endblock %} {{ branding }}{% endblock %}
{% block page_actions %} {% block page_actions %}
@ -21,21 +22,13 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<tbody> <tbody>
{% for item in item_list -%} {% for item in item_list -%}
{% let class = item_classes.get(item.class).unwrap() %} {% let class = item_classes.get(item.class).unwrap() %}
{# inlining this breaks? #}
{%- let parents = item_parents.get(item.id).unwrap() %}
<tr> <tr>
<td><a href="/item/{{ item.id }}">{{ item.name }}</a></td> <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><a href="/item-class/{{ class.id }}">{{ class.name }}</a></td>
<td> <td>
<ol class="breadcrumb mb-0"> {%- call macros::parents_breadcrumb(item, parents, item_classes, full=false) %}
{%- let parents = item_tree.get(item.id).unwrap() -%}
{%- if parents.len() > 3 %}
<li class="breadcrumb-item"></li>
{%- endif %}
{%- for parent in parents.iter().rev().take(3).rev() %}
{%- let parent = items.get(parent).unwrap() %}
<li class="breadcrumb-item"><a href="/item/{{ parent.id }}">{{ parent.name }}</a></li>
{%- endfor %}
<li class="breadcrumb-item active">{{ item.name }}</li>
</ol>
</td> </td>
</tr> </tr>
{% endfor -%} {% endfor -%}

36
templates/macros.html Normal file
View file

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