From d9151adc157a7cc774f94d82a9700952cd5a640c Mon Sep 17 00:00:00 2001 From: MyuTsu Date: Mon, 29 Jun 2026 16:08:59 +0200 Subject: [PATCH 1/3] fix(TreeCascadeDropdownQuestion): fix incorrect options displayed in custom dropdown --- CHANGELOG.md | 1 + .../TreeDropdownChildrenController.php | 6 +++ .../TreeCascadeDropdownQuestion.php | 22 ++++++++ .../TreeCascadeDropdownQuestionTest.php | 53 +++++++++++++++++++ 4 files changed, 82 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b974d4..c539a13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Fixed the `Tree cascade Dropdown` field so that the subtree depth limit is enforced when loading children via AJAX +- Fixed the `Tree cascade Dropdown` question to only show items from the configured custom dropdown instead of all custom dropdowns ## [1.1.1] - 2026-05-27 diff --git a/src/Controller/TreeDropdownChildrenController.php b/src/Controller/TreeDropdownChildrenController.php index 4b1a665..0a6ef3b 100644 --- a/src/Controller/TreeDropdownChildrenController.php +++ b/src/Controller/TreeDropdownChildrenController.php @@ -101,6 +101,12 @@ public function __invoke(Request $request): Response $where = array_merge($where, $condition_param); } + /** @var array $system_criteria */ + $system_criteria = $itemtype::getSystemSQLCriteria(); + if ($system_criteria !== []) { + $where = array_merge($where, $system_criteria); + } + if ($item_check instanceof CommonTreeDropdown && $item_check->isField('is_deleted')) { $where['is_deleted'] = 0; } diff --git a/src/Model/QuestionType/TreeCascadeDropdownQuestion.php b/src/Model/QuestionType/TreeCascadeDropdownQuestion.php index f55f41a..2d6c9c2 100644 --- a/src/Model/QuestionType/TreeCascadeDropdownQuestion.php +++ b/src/Model/QuestionType/TreeCascadeDropdownQuestion.php @@ -137,18 +137,23 @@ public function renderEndUserTemplate(Question $question): string } } + /** @var array $system_criteria */ + $system_criteria = $itemtype::getSystemSQLCriteria(); + $ancestor_chain = $this->buildAncestorChain( $itemtype, $default_items_id, $restriction_where, $root_items_id, $selectable_tree_root, + $system_criteria, ); $first_level_items = $this->getFirstLevelItems( $itemtype, $restriction_where, $root_items_id, + $system_criteria, ); $twig = TemplateRenderer::getInstance(); @@ -178,6 +183,7 @@ public function renderEndUserTemplate(Question $question): string /** * @param class-string $itemtype * @param array $extra_conditions + * @param array $system_criteria * @return array}> */ private function buildAncestorChain( @@ -186,6 +192,7 @@ private function buildAncestorChain( array $extra_conditions = [], int $root_items_id = 0, bool $selectable_tree_root = false, + array $system_criteria = [], ): array { if ($items_id <= 0) { return []; @@ -268,6 +275,10 @@ private function buildAncestorChain( $base_where['is_deleted'] = 0; } + if ($system_criteria !== []) { + $base_where = array_merge($base_where, $system_criteria); + } + foreach ($chain as $index => &$node) { $raw_where = [ $foreign_key => $index === 0 ? max($root_items_id, 0) : $node['parent_id'], @@ -276,6 +287,10 @@ private function buildAncestorChain( $raw_where['is_deleted'] = 0; } + if ($system_criteria !== []) { + $raw_where = array_merge($raw_where, $system_criteria); + } + /** @var array $typed_base_where */ $typed_base_where = $base_where; $node['siblings'] = $this->getValidItemsForLevel($table, $typed_base_where, $raw_where); @@ -287,12 +302,14 @@ private function buildAncestorChain( /** * @param class-string $itemtype * @param array $extra_conditions + * @param array $system_criteria * @return array */ private function getFirstLevelItems( string $itemtype, array $extra_conditions = [], int $root_items_id = 0, + array $system_criteria = [], ): array { $table = $itemtype::getTable(); $foreign_key = $itemtype::getForeignKeyField(); @@ -327,6 +344,11 @@ private function getFirstLevelItems( $raw_where['is_deleted'] = 0; } + if ($system_criteria !== []) { + $base_where = array_merge($base_where, $system_criteria); + $raw_where = array_merge($raw_where, $system_criteria); + } + /** @var array $typed_base_where */ $typed_base_where = $base_where; return $this->getValidItemsForLevel($table, $typed_base_where, $raw_where); diff --git a/tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php b/tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php index 6ae4b8f..ee6f057 100644 --- a/tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php +++ b/tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php @@ -607,6 +607,59 @@ public function testParentExceedingDepthLimitIsNotSelectableAndHasNoChildren(): $this->assertStringNotContainsString('L4', $response->getContent()); } + /** + * Verify that when a question is configured with the "Test1" custom dropdown, + * only items from that dropdown appear in the rendered form. Items from another + * custom dropdown ("Test2"), which shares the same database table, must not appear. + */ + public function testHelpdeskRenderingOnlyShowsItemsFromConfiguredCustomDropdown(): void + { + $this->login(); + $item = $this->getTestedQuestionType(); + $this->enableConfigurableItem($item); + + $entity_id = Session::getActiveEntity(); + + $test1_definition = $this->initDropdownDefinition('Test1'); + $test2_definition = $this->initDropdownDefinition('Test2'); + + $test1_class = $test1_definition->getDropdownClassName(); + $test2_class = $test2_definition->getDropdownClassName(); + + \Dropdown::resetItemtypesStaticCache(); + + $this->createItem($test1_class, [ + 'name' => 'Item from Test1', + 'entities_id' => $entity_id, + ]); + $this->createItem($test2_class, [ + 'name' => 'Item from Test2', + 'entities_id' => $entity_id, + ]); + + $extra_data = json_encode(new QuestionTypeItemDropdownExtraDataConfig( + itemtype: $test1_class, + )); + + $builder = new FormBuilder("Custom Dropdown Form"); + $builder->addQuestion( + "TreeCascadeDropdown Question", + TreeCascadeDropdownQuestion::class, + '', + $extra_data, + ); + $form = $this->createForm($builder); + + $html = $this->renderHelpdeskForm($form); + + $all_option_texts = $html->filter('.af-tree-cascade-select option')->each( + fn(Crawler $node) => $node->text(), + ); + + $this->assertContains('Item from Test1', $all_option_texts); + $this->assertNotContains('Item from Test2', $all_option_texts); + } + private function renderHelpdeskForm(\Glpi\Form\Form $form): Crawler { $this->login(); From ff66f33d56d8d160d7c4b391fe7fdc8601d9092f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Langlois=20Ga=C3=ABtan?= <64356364+MyvTsv@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:26:44 +0200 Subject: [PATCH 2/3] Update CHANGELOG.md Co-authored-by: Romain B. <8530352+Rom1-B@users.noreply.github.com> --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c539a13..8deab59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +<<<<<<< HEAD - Fixed the `Tree cascade Dropdown` field so that the subtree depth limit is enforced when loading children via AJAX - Fixed the `Tree cascade Dropdown` question to only show items from the configured custom dropdown instead of all custom dropdowns +======= +- Fixed `Tree cascade Dropdown` question showing items from all custom dropdowns instead of only items from the configured one +>>>>>>> 5e3a05c (Update CHANGELOG.md) ## [1.1.1] - 2026-05-27 From 358242849cb78eafe496e346508f11abc072fcba Mon Sep 17 00:00:00 2001 From: MyuTsu Date: Tue, 30 Jun 2026 16:39:18 +0200 Subject: [PATCH 3/3] add tests --- .../TreeCascadeDropdownQuestionTest.php | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php b/tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php index ee6f057..295236c 100644 --- a/tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php +++ b/tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php @@ -660,6 +660,145 @@ public function testHelpdeskRenderingOnlyShowsItemsFromConfiguredCustomDropdown( $this->assertNotContains('Item from Test2', $all_option_texts); } + /** + * Verify that when a default value is set on a TreeCascadeDropdown question + */ + public function testHelpdeskRenderingWithCustomDropdownAndDefaultValue(): void + { + $this->login(); + $item = $this->getTestedQuestionType(); + $this->enableConfigurableItem($item); + + $entity_id = Session::getActiveEntity(); + + $test1_definition = $this->initDropdownDefinition('Test1'); + $test2_definition = $this->initDropdownDefinition('Test2'); + + $test1_class = $test1_definition->getDropdownClassName(); + $test2_class = $test2_definition->getDropdownClassName(); + + \Dropdown::resetItemtypesStaticCache(); + + $foreign_key = $test1_class::getForeignKeyField(); + + $parent = $this->createItem($test1_class, [ + 'name' => 'Parent Custom Item', + 'entities_id' => $entity_id, + ]); + $child = $this->createItem($test1_class, [ + 'name' => 'Child Custom Item', + $foreign_key => $parent->getID(), + 'entities_id' => $entity_id, + ]); + $this->createItem($test2_class, [ + 'name' => 'Other Dropdown Item', + 'entities_id' => $entity_id, + ]); + + $extra_data = json_encode(new QuestionTypeItemDropdownExtraDataConfig( + itemtype: $test1_class, + )); + + $builder = new FormBuilder("Custom Dropdown Default Value Form"); + $builder->addQuestion( + "My custom dropdown", + TreeCascadeDropdownQuestion::class, + $child->getID(), + $extra_data, + ); + $form = $this->createForm($builder); + + $html = $this->renderHelpdeskForm($form); + + $hidden_input = $html->filter('input[type="hidden"][value="' . $child->getID() . '"]'); + $this->assertNotEmpty($hidden_input); + + $selects = $html->filter('.af-tree-cascade-select'); + + $options = $selects->eq(0)->filter('option')->each( + fn(Crawler $node) => $node->text(), + ); + $this->assertContains('Parent Custom Item', $options); + $this->assertNotContains('Other Dropdown Item', $options); + + $selected_option = $selects->eq(1)->filter('option[selected]'); + $this->assertNotEmpty($selected_option); + $this->assertEquals('Child Custom Item', $selected_option->text()); + } + + /** + * Verify that when a subtree root is configured for a custom dropdown + */ + public function testHelpdeskRenderingWithCustomDropdownAndSubtreeRoot(): void + { + $this->login(); + $item = $this->getTestedQuestionType(); + $this->enableConfigurableItem($item); + + $entity_id = Session::getActiveEntity(); + + $test1_definition = $this->initDropdownDefinition('Test1'); + $test2_definition = $this->initDropdownDefinition('Test2'); + + $test1_class = $test1_definition->getDropdownClassName(); + $test2_class = $test2_definition->getDropdownClassName(); + + \Dropdown::resetItemtypesStaticCache(); + + $foreign_key = $test1_class::getForeignKeyField(); + + [$root, $root_2] = $this->createItems($test1_class, [ + [ + 'name' => 'Root', + 'entities_id' => $entity_id, + ], + [ + 'name' => 'Root 2', + 'entities_id' => $entity_id, + ], + ]); + $this->createItem($test1_class, [ + 'name' => 'Subtree Child', + $foreign_key => $root->getID(), + 'entities_id' => $entity_id, + ]); + $this->createItem($test1_class, [ + 'name' => 'Subtree Child 2', + $foreign_key => $root_2->getID(), + 'entities_id' => $entity_id, + ]); + $this->createItem($test2_class, [ + 'name' => 'Test 2 Option', + 'entities_id' => $entity_id, + ]); + + $extra_data = json_encode(new QuestionTypeItemDropdownExtraDataConfig( + itemtype: $test1_class, + root_items_id: $root->getID(), + )); + + $builder = new FormBuilder("Custom Dropdown Subtree Root Form"); + $builder->addQuestion( + "My custom dropdown", + TreeCascadeDropdownQuestion::class, + '', + $extra_data, + ); + $form = $this->createForm($builder); + + $html = $this->renderHelpdeskForm($form); + + $all_option_texts = $html->filter('.af-tree-cascade-select option')->each( + fn(Crawler $node) => $node->text(), + ); + + $this->assertContains('Subtree Child', $all_option_texts); + $this->assertNotContains('Subtree Child 2', $all_option_texts); + $this->assertNotContains('Root', $all_option_texts); + $this->assertNotContains('Root 2', $all_option_texts); + $this->assertNotContains('Test 2 Option', $all_option_texts); + } + private function renderHelpdeskForm(\Glpi\Form\Form $form): Crawler { $this->login();