Show item classes as tree
This commit is contained in:
parent
ae4e583c2d
commit
bb309f5cd9
|
@ -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 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 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",
|
||||||
|
"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": "3eecdd98e2f76979a79883a8946e05977c45058536561e5752269d5933e271da"
|
||||||
|
}
|
|
@ -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,73 @@ 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
|
||||||
|
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
|
||||||
|
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"##
|
||||||
|
)
|
||||||
|
.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::<Vec<_>>();
|
||||||
|
item_classes.sort_by(|this, other| this.name.cmp(&other.name));
|
||||||
|
Ok(item_classes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
@for item_class in item_classes {
|
||||||
tr {
|
(item_class)
|
||||||
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 {
|
|
||||||
"-"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue