From 413a02cdaa5c366b5c1c05b09c4463faaf5428a4 Mon Sep 17 00:00:00 2001 From: Simon Bruder Date: Tue, 6 Aug 2024 16:39:02 +0200 Subject: [PATCH] Show item classes as tree --- ...33eb6b9639a356fe7ec543691e385ffef51e7.json | 32 ++++++++ src/database/item_classes/list.rs | 81 +++++++++++++++++++ src/database/item_classes/mod.rs | 1 + src/frontend/item_class/list.rs | 26 +----- src/frontend/templates/render.rs | 23 +++++- 5 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 .sqlx/query-689a5177bdbc4ac788579a47fe033eb6b9639a356fe7ec543691e385ffef51e7.json diff --git a/.sqlx/query-689a5177bdbc4ac788579a47fe033eb6b9639a356fe7ec543691e385ffef51e7.json b/.sqlx/query-689a5177bdbc4ac788579a47fe033eb6b9639a356fe7ec543691e385ffef51e7.json new file mode 100644 index 0000000..f282ec7 --- /dev/null +++ b/.sqlx/query-689a5177bdbc4ac788579a47fe033eb6b9639a356fe7ec543691e385ffef51e7.json @@ -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" +} diff --git a/src/database/item_classes/list.rs b/src/database/item_classes/list.rs index 57d2bff..c7c6c69 100644 --- a/src/database/item_classes/list.rs +++ b/src/database/item_classes/list.rs @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +use std::collections::HashMap; + use sqlx::query; use uuid::Uuid; @@ -13,6 +15,12 @@ pub struct ItemClassListEntry { pub parent: Option, } +pub struct ItemClassTreeElement { + pub id: Uuid, + pub name: String, + pub children: Vec, +} + impl ItemClassRepository { pub async fn list(&self) -> sqlx::Result> { query!( @@ -33,4 +41,77 @@ impl ItemClassRepository { .fetch_all(&self.pool) .await } + + pub async fn tree(&self) -> sqlx::Result> { + let mut mappings: HashMap = 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(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::>(); + item_classes.sort_by(|this, other| this.name.cmp(&other.name)); + Ok(item_classes) + } } diff --git a/src/database/item_classes/mod.rs b/src/database/item_classes/mod.rs index c6e95ff..c374b19 100644 --- a/src/database/item_classes/mod.rs +++ b/src/database/item_classes/mod.rs @@ -14,6 +14,7 @@ use uuid::Uuid; pub use add::{ItemClassAddForm, ItemClassAddFormPrefilled}; pub use edit::ItemClassEditForm; +pub use list::ItemClassTreeElement; #[derive(Clone)] pub struct ItemClassRepository { diff --git a/src/frontend/item_class/list.rs b/src/frontend/item_class/list.rs index 9708cfa..dbc6310 100644 --- a/src/frontend/item_class/list.rs +++ b/src/frontend/item_class/list.rs @@ -6,7 +6,6 @@ use actix_identity::Identity; use actix_web::{error, get, web, Responder}; use maud::html; -use crate::database::item_classes::ItemClassPreview; use crate::database::ItemClassRepository; use crate::frontend::templates::{ self, @@ -24,7 +23,7 @@ async fn get( user: Identity, ) -> actix_web::Result { let item_classes = item_class_repo - .list() + .tree() .await .map_err(error::ErrorInternalServerError)?; @@ -47,26 +46,9 @@ async fn get( ..Default::default() }, html! { - table .table { - thead { - tr { - th { "Name" } - th { "Parents" } - } - } - tbody { - @for item_class in item_classes { - tr { - td { (ItemClassPreview::new(item_class.id, item_class.name)) } - td { - @if let Some(parent) = item_class.parent { - (parent) - } @else { - "-" - } - } - } - } + ul { + @for item_class in item_classes { + (item_class) } } }, diff --git a/src/frontend/templates/render.rs b/src/frontend/templates/render.rs index 9e10039..731caeb 100644 --- a/src/frontend/templates/render.rs +++ b/src/frontend/templates/render.rs @@ -7,7 +7,11 @@ use std::fmt::{self, Display}; use maud::{html, Markup, Render}; 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 { 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) + } + } + } + } + } + } +}