Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +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

Expand Down
6 changes: 6 additions & 0 deletions src/Controller/TreeDropdownChildrenController.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ public function __invoke(Request $request): Response
$where = array_merge($where, $condition_param);
}

/** @var array<string, mixed> $system_criteria */
Comment thread
MyvTsv marked this conversation as resolved.
$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;
}
Expand Down
22 changes: 22 additions & 0 deletions src/Model/QuestionType/TreeCascadeDropdownQuestion.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,18 +137,23 @@ public function renderEndUserTemplate(Question $question): string
}
}

/** @var array<string, mixed> $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();
Expand Down Expand Up @@ -178,6 +183,7 @@ public function renderEndUserTemplate(Question $question): string
/**
* @param class-string<CommonTreeDropdown> $itemtype
* @param array<string, mixed> $extra_conditions
* @param array<string, mixed> $system_criteria
* @return array<int, array{id: int, parent_id: int, level: int, siblings: array<int, array{id: int, name: string}>}>
*/
private function buildAncestorChain(
Expand All @@ -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 [];
Expand Down Expand Up @@ -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'],
Expand All @@ -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<string, mixed> $typed_base_where */
$typed_base_where = $base_where;
$node['siblings'] = $this->getValidItemsForLevel($table, $typed_base_where, $raw_where);
Expand All @@ -287,12 +302,14 @@ private function buildAncestorChain(
/**
* @param class-string<CommonTreeDropdown> $itemtype
* @param array<string, mixed> $extra_conditions
* @param array<string, mixed> $system_criteria
* @return array<int, array{id: int, name: string}>
*/
private function getFirstLevelItems(
string $itemtype,
array $extra_conditions = [],
int $root_items_id = 0,
array $system_criteria = [],
): array {
$table = $itemtype::getTable();
$foreign_key = $itemtype::getForeignKeyField();
Expand Down Expand Up @@ -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<string, mixed> $typed_base_where */
$typed_base_where = $base_where;
return $this->getValidItemsForLevel($table, $typed_base_where, $raw_where);
Expand Down
192 changes: 192 additions & 0 deletions tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,198 @@ 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(),
);

Comment thread
MyvTsv marked this conversation as resolved.
$this->assertContains('Item from Test1', $all_option_texts);
$this->assertNotContains('Item from Test2', $all_option_texts);
}

/**
* Verify that when a default value is set on a TreeCascadeDropdown question
*/
Comment thread
MyvTsv marked this conversation as resolved.
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
*/
Comment thread
MyvTsv marked this conversation as resolved.
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();
Expand Down