From ae4e583c2dd34b93ac74a3312f67120bca8b2926 Mon Sep 17 00:00:00 2001 From: Simon Bruder Date: Tue, 6 Aug 2024 14:10:05 +0200 Subject: [PATCH] Allow unlimited nesting of item classes --- ...120210_item_class_unlimited_depth.down.sql | 28 ++++++++ ...06120210_item_class_unlimited_depth.up.sql | 50 +++++++++++++ src/frontend/item_class/show.rs | 72 +++++++++---------- 3 files changed, 112 insertions(+), 38 deletions(-) create mode 100644 migrations/20240806120210_item_class_unlimited_depth.down.sql create mode 100644 migrations/20240806120210_item_class_unlimited_depth.up.sql diff --git a/migrations/20240806120210_item_class_unlimited_depth.down.sql b/migrations/20240806120210_item_class_unlimited_depth.down.sql new file mode 100644 index 0000000..8de0ca5 --- /dev/null +++ b/migrations/20240806120210_item_class_unlimited_depth.down.sql @@ -0,0 +1,28 @@ +-- SPDX-FileCopyrightText: 2024 Simon Bruder +-- +-- SPDX-License-Identifier: AGPL-3.0-or-later + +DROP VIEW item_class_tree; + +DROP TRIGGER prevent_item_class_cycle ON item_classes; +DROP FUNCTION check_item_class_cycle; + +CREATE FUNCTION check_item_class_recursion_depth() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.parent IS NULL THEN + RETURN NEW; + END IF; + + IF (SELECT parent FROM item_classes WHERE id = NEW.parent) IS NULL THEN + RETURN NEW; + END IF; + + RAISE EXCEPTION 'Item classes may only be nested one level deep'; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER prevent_item_class_recursion +BEFORE INSERT OR UPDATE ON item_classes +FOR EACH ROW +EXECUTE FUNCTION check_item_class_recursion_depth(); diff --git a/migrations/20240806120210_item_class_unlimited_depth.up.sql b/migrations/20240806120210_item_class_unlimited_depth.up.sql new file mode 100644 index 0000000..304abb9 --- /dev/null +++ b/migrations/20240806120210_item_class_unlimited_depth.up.sql @@ -0,0 +1,50 @@ +-- SPDX-FileCopyrightText: 2024 Simon Bruder +-- +-- SPDX-License-Identifier: AGPL-3.0-or-later + +DROP TRIGGER prevent_item_class_recursion ON item_classes; +DROP FUNCTION check_item_class_recursion_depth; + +CREATE FUNCTION check_item_class_cycle() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.parent IS NULL THEN + RETURN NEW; + END IF; + + IF NEW.id = NEW.parent THEN + RAISE EXCEPTION 'Cycle detected'; + END IF; + + IF (WITH RECURSIVE cte AS ( + SELECT id, parent + FROM item_classes + WHERE id = NEW.parent + + UNION ALL + + SELECT item_classes.id, item_classes.parent + FROM item_classes, cte + WHERE item_classes.id = cte.parent + ) + SELECT 1 + FROM cte + WHERE parent = NEW.id + LIMIT 1) THEN + RAISE EXCEPTION 'Cycle detected'; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER prevent_item_class_cycle +BEFORE INSERT OR UPDATE ON item_classes +FOR EACH ROW +EXECUTE FUNCTION check_item_class_cycle(); + +CREATE RECURSIVE VIEW item_class_tree (id, parents) AS ( + SELECT id, ARRAY[]::UUID[] AS parents FROM item_classes WHERE parent IS NULL + UNION ALL + SELECT item_classes.id, item_class_tree.parents || item_classes.parent FROM item_classes, item_class_tree WHERE item_classes.parent = item_class_tree.id +); diff --git a/src/frontend/item_class/show.rs b/src/frontend/item_class/show.rs index c3f92f0..4615158 100644 --- a/src/frontend/item_class/show.rs +++ b/src/frontend/item_class/show.rs @@ -44,49 +44,45 @@ async fn get( let mut title = item_class.name.clone(); title.push_str(" – Item Details"); - let mut page_actions = vec![ - (PageActionGroup::Button { - action: PageAction { - method: PageActionMethod::Get, - target: format!("/items/add?class={}", item_class.id), - name: "Add Item".to_string(), - }, - colour: Colour::Success, - }), - ]; - if item_class.parent.is_none() { - page_actions.push(PageActionGroup::Button { - action: PageAction { - method: PageActionMethod::Get, - target: format!("/item-classes/add?parent={}", item_class.id), - name: "Add Child".to_string(), - }, - colour: Colour::Primary, - }); - } - page_actions.push(PageActionGroup::Button { - action: PageAction { - method: PageActionMethod::Get, - target: format!("/item-class/{}/edit", item_class.id), - name: "Edit".to_string(), - }, - colour: Colour::Warning, - }); - page_actions.push(PageActionGroup::Button { - action: PageAction { - method: PageActionMethod::Post, - target: format!("/item-class/{}/delete", item_class.id), - name: "Delete".to_string(), - }, - colour: Colour::Danger, - }); - Ok(templates::base( TemplateConfig { path: &format!("/item-class/{}", item_class.id), title: Some(&title), page_title: Some(Box::new(item_class.name.clone())), - page_actions, + page_actions: vec![ + PageActionGroup::Button { + action: PageAction { + method: PageActionMethod::Get, + target: format!("/items/add?class={}", item_class.id), + name: "Add Item".to_string(), + }, + colour: Colour::Success, + }, + PageActionGroup::Button { + action: PageAction { + method: PageActionMethod::Get, + target: format!("/item-classes/add?parent={}", item_class.id), + name: "Add Child".to_string(), + }, + colour: Colour::Primary, + }, + PageActionGroup::Button { + action: PageAction { + method: PageActionMethod::Get, + target: format!("/item-class/{}/edit", item_class.id), + name: "Edit".to_string(), + }, + colour: Colour::Warning, + }, + PageActionGroup::Button { + action: PageAction { + method: PageActionMethod::Post, + target: format!("/item-class/{}/delete", item_class.id), + name: "Delete".to_string(), + }, + colour: Colour::Danger, + }, + ], user: Some(user), ..Default::default() },