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:
parent
5147257e72
commit
c4a73204aa
|
@ -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;
|
13
migrations/2024-07-09-133113_item_make_name_optional/up.sql
Normal file
13
migrations/2024-07-09-133113_item_make_name_optional/up.sql
Normal 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 <> '');
|
|
@ -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 askama’s 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")]
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
36
templates/macros.html
Normal 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 %}
|
Loading…
Reference in a new issue