Show item classes as tree

This commit is contained in:
Simon Bruder 2024-08-06 16:39:02 +02:00
parent ae4e583c2d
commit 615f073995
Signed by: simon
GPG key ID: 347FF8699CDA0776
5 changed files with 140 additions and 23 deletions

View file

@ -0,0 +1,32 @@
{
"db_name": "PostgreSQL",
"query": "WITH RECURSIVE item_class_children AS (\n SELECT\n item_classes.id,\n array_remove(array_agg(children.id), NULL) AS \"children\"\n FROM item_classes\n LEFT JOIN item_classes AS \"children\"\n ON item_classes.id = children.parent\n GROUP BY item_classes.id\n ),\n cte AS (\n SELECT\n item_classes.id,\n item_classes.name,\n item_class_children.children,\n 0 AS \"reverse_level\"\n FROM item_classes\n JOIN item_class_children\n ON item_classes.id = item_class_children.id\n WHERE item_class_children.children = '{}'\n\n UNION\n\n SELECT\n item_classes.id,\n item_classes.name,\n item_class_children.children,\n cte.reverse_level + 1\n FROM item_classes\n JOIN item_class_children\n ON item_classes.id = item_class_children.id\n JOIN cte\n ON cte.id = ANY (item_class_children.children)\n )\n SELECT\n id AS \"id!\",\n name AS \"name!\",\n children AS \"children!\"\n FROM cte\n GROUP BY id, name, children\n ORDER BY max(reverse_level)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name!",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "children!",
"type_info": "UuidArray"
}
],
"parameters": {
"Left": []
},
"nullable": [
null,
null,
null
]
},
"hash": "689a5177bdbc4ac788579a47fe033eb6b9639a356fe7ec543691e385ffef51e7"
}

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 sqlx::query; use sqlx::query;
use uuid::Uuid; use uuid::Uuid;
@ -13,6 +15,12 @@ pub struct ItemClassListEntry {
pub parent: Option<ItemClassPreview>, pub parent: Option<ItemClassPreview>,
} }
pub struct ItemClassTreeElement {
pub id: Uuid,
pub name: String,
pub children: Vec<ItemClassTreeElement>,
}
impl ItemClassRepository { impl ItemClassRepository {
pub async fn list(&self) -> sqlx::Result<Vec<ItemClassListEntry>> { pub async fn list(&self) -> sqlx::Result<Vec<ItemClassListEntry>> {
query!( query!(
@ -33,4 +41,77 @@ impl ItemClassRepository {
.fetch_all(&self.pool) .fetch_all(&self.pool)
.await .await
} }
pub async fn tree(&self) -> sqlx::Result<Vec<ItemClassTreeElement>> {
let mut mappings: HashMap<Uuid, ItemClassTreeElement> = HashMap::new();
for row in query!(
r##"WITH RECURSIVE item_class_children AS (
SELECT
item_classes.id,
array_remove(array_agg(children.id), NULL) AS "children"
FROM item_classes
LEFT JOIN item_classes AS "children"
ON item_classes.id = children.parent
GROUP BY item_classes.id
),
cte AS (
SELECT
item_classes.id,
item_classes.name,
item_class_children.children,
0 AS "reverse_level"
FROM item_classes
JOIN item_class_children
ON item_classes.id = item_class_children.id
WHERE item_class_children.children = '{}'
UNION
SELECT
item_classes.id,
item_classes.name,
item_class_children.children,
cte.reverse_level + 1
FROM item_classes
JOIN item_class_children
ON item_classes.id = item_class_children.id
JOIN cte
ON cte.id = ANY (item_class_children.children)
)
SELECT
id AS "id!",
name AS "name!",
children AS "children!"
FROM cte
GROUP BY id, name, children
ORDER BY max(reverse_level)"##
)
.fetch_all(&self.pool)
.await?
{
let mut children = if row.children.is_empty() {
Vec::new()
} else {
row.children
.iter()
.map(|id| mappings.remove(dbg!(id)).unwrap())
.collect()
};
children.sort_by(|this, other| this.name.cmp(&other.name));
mappings.insert(
row.id,
ItemClassTreeElement {
id: row.id,
name: row.name,
children,
},
);
}
let mut item_classes = mappings.into_values().collect::<Vec<_>>();
item_classes.sort_by(|this, other| this.name.cmp(&other.name));
Ok(item_classes)
}
} }

View file

@ -14,6 +14,7 @@ use uuid::Uuid;
pub use add::{ItemClassAddForm, ItemClassAddFormPrefilled}; pub use add::{ItemClassAddForm, ItemClassAddFormPrefilled};
pub use edit::ItemClassEditForm; pub use edit::ItemClassEditForm;
pub use list::ItemClassTreeElement;
#[derive(Clone)] #[derive(Clone)]
pub struct ItemClassRepository { pub struct ItemClassRepository {

View file

@ -6,7 +6,6 @@ use actix_identity::Identity;
use actix_web::{error, get, web, Responder}; use actix_web::{error, get, web, Responder};
use maud::html; use maud::html;
use crate::database::item_classes::ItemClassPreview;
use crate::database::ItemClassRepository; use crate::database::ItemClassRepository;
use crate::frontend::templates::{ use crate::frontend::templates::{
self, self,
@ -24,7 +23,7 @@ async fn get(
user: Identity, user: Identity,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let item_classes = item_class_repo let item_classes = item_class_repo
.list() .tree()
.await .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
@ -47,26 +46,9 @@ async fn get(
..Default::default() ..Default::default()
}, },
html! { html! {
table .table { ul {
thead {
tr {
th { "Name" }
th { "Parents" }
}
}
tbody {
@for item_class in item_classes { @for item_class in item_classes {
tr { (item_class)
td { (ItemClassPreview::new(item_class.id, item_class.name)) }
td {
@if let Some(parent) = item_class.parent {
(parent)
} @else {
"-"
}
}
}
}
} }
} }
}, },

View file

@ -7,7 +7,11 @@ use std::fmt::{self, Display};
use maud::{html, Markup, Render}; use maud::{html, Markup, Render};
use crate::database::items::ItemPreview; use crate::database::items::ItemPreview;
use crate::database::{item_classes::ItemClassPreview, item_events::ItemEvent, items::ItemName}; use crate::database::{
item_classes::{ItemClassPreview, ItemClassTreeElement},
item_events::ItemEvent,
items::ItemName,
};
impl Render for ItemClassPreview { impl Render for ItemClassPreview {
fn render(&self) -> Markup { fn render(&self) -> Markup {
@ -68,3 +72,20 @@ impl Render for ItemPreview {
} }
} }
} }
impl Render for ItemClassTreeElement {
fn render(&self) -> Markup {
html! {
li {
(ItemClassPreview::new(self.id, self.name.clone()))
@if !self.children.is_empty() {
ul {
@for child in &self.children {
(child)
}
}
}
}
}
}
}