From 47fde98c4562d21ec085528cfdd7faa2da9dfeb7 Mon Sep 17 00:00:00 2001 From: Julien Durand Date: Tue, 29 Sep 2026 11:51:25 +0200 Subject: [PATCH 1/4] fix(46462): Mapping answers from dropdown with multiple answers to destination --- inc/destinationfield.class.php | 7 ++ inc/questiontype.class.php | 4 ++ tests/Units/FieldDestinationFieldTest.php | 85 ++++++++++++++++++++++- tests/Units/FieldQuestionTypeTest.php | 54 ++++++++++++++ 4 files changed, 149 insertions(+), 1 deletion(-) diff --git a/inc/destinationfield.class.php b/inc/destinationfield.class.php index b55be89c..bf90eea4 100644 --- a/inc/destinationfield.class.php +++ b/inc/destinationfield.class.php @@ -134,6 +134,13 @@ public function applyConfiguratedValueToInputUsingAnswers( if ($field->fields['type'] == 'glpi_item') { $input[sprintf('itemtype_%s', $field_name)] = $answer->getRawAnswer()['itemtype']; $input[sprintf('items_id_%s', $field_name)] = $answer->getRawAnswer()['items_id']; + } elseif (str_starts_with((string) $field->fields['type'], 'dropdown') && $field->fields['multiple']) { + // Multiple dropdowns are submitted as an array of ids + $raw_ids = (array) ($answer->getRawAnswer()['items_id'] ?? []); + $input[$field_name] = array_values(array_filter( + array_map(intval(...), $raw_ids), + fn(int $id) => $id > 0, + )); } elseif (str_starts_with((string) $field->fields['type'], 'dropdown')) { $raw_id = (int) ($answer->getRawAnswer()['items_id'] ?? 0); $input[$field_name] = ($raw_id > 0) ? $raw_id : null; diff --git a/inc/questiontype.class.php b/inc/questiontype.class.php index a38daf1b..919d1d87 100644 --- a/inc/questiontype.class.php +++ b/inc/questiontype.class.php @@ -242,6 +242,10 @@ public function formatRawAnswer(mixed $answer, Question $question): string return ''; } + if (is_array($answer) && array_key_exists('items_id', $answer)) { + $answer = $answer['items_id']; + } + if (!is_array($answer)) { $answer = [$answer]; } diff --git a/tests/Units/FieldDestinationFieldTest.php b/tests/Units/FieldDestinationFieldTest.php index 7a6f5e89..604d0d52 100644 --- a/tests/Units/FieldDestinationFieldTest.php +++ b/tests/Units/FieldDestinationFieldTest.php @@ -46,6 +46,7 @@ use Override; use PluginFieldsContainer; use PluginFieldsDestinationField; +use PluginFieldsDropdown; use PluginFieldsQuestionType; use Problem; use Ticket; @@ -120,12 +121,32 @@ private function initFieldTest(): void ]); $this->fields[] = $this->createField([ 'label' => 'Location Field', - 'type' => 'dropdown-Location', + 'type' => 'dropdown-' . Location::class, PluginFieldsContainer::getForeignKeyField() => $this->blocks[Ticket::class]->getID(), 'ranking' => 3, 'is_active' => 1, 'is_readonly' => 0, ]); + $this->fields[] = $this->createField([ + 'label' => 'Multiple dropdown', + 'type' => 'dropdown', + 'multiple' => 1, + 'default_value' => [], + PluginFieldsContainer::getForeignKeyField() => $this->blocks[Ticket::class]->getID(), + 'ranking' => 4, + 'is_active' => 1, + 'is_readonly' => 0, + ], ['default_value']); + $this->fields[] = $this->createField([ + 'label' => 'Multiple locations', + 'type' => 'dropdown-' . Location::class, + 'multiple' => 1, + 'default_value' => [], + PluginFieldsContainer::getForeignKeyField() => $this->blocks[Ticket::class]->getID(), + 'ranking' => 5, + 'is_active' => 1, + 'is_readonly' => 0, + ], ['default_value']); } public function setUp(): void @@ -254,6 +275,68 @@ public function testDestinationWithLocationAdditonalFields(): void $location->delete($location->fields, true); } + public function testDestinationWithMultipleDropdownAdditionalFields(): void + { + $this->login(); + + $dropdown_field = $this->fields[5]; + $locations_field = $this->fields[6]; + + $form = $this->createForm( + (new FormBuilder()) + ->addQuestion("Multiple dropdown", PluginFieldsQuestionType::class, extra_data: json_encode([ + 'block_id' => $this->blocks[Ticket::class]->getID(), + 'field_id' => $dropdown_field->getID(), + ])) + ->addQuestion("Multiple locations", PluginFieldsQuestionType::class, extra_data: json_encode([ + 'block_id' => $this->blocks[Ticket::class]->getID(), + 'field_id' => $locations_field->getID(), + ])), + ); + + $dropdown_class = PluginFieldsDropdown::getClassname($dropdown_field->fields['name']); + $value1 = $this->createItem($dropdown_class, ['name' => 'Value 1', 'entities_id' => 0]); + $value2 = $this->createItem($dropdown_class, ['name' => 'Value 2', 'entities_id' => 0]); + + $location1 = $this->createItem(Location::class, [ + 'name' => 'Location 1', + 'entities_id' => $this->getTestRootEntity(true), + ]); + $location2 = $this->createItem(Location::class, [ + 'name' => 'Location 2', + 'entities_id' => $this->getTestRootEntity(true), + ]); + + try { + $this->sendFormAndAssertITILObjectAdditionalFields( + form: $form, + config: new SimpleValueConfig(1), + answers: [ + // Multiple dropdowns are submitted as an array of ids + "Multiple dropdown" => [ + 'itemtype' => $dropdown_class, + 'items_id' => [(string) $value1->getID(), (string) $value2->getID()], + ], + "Multiple locations" => [ + 'itemtype' => Location::class, + 'items_id' => [(string) $location1->getID(), (string) $location2->getID()], + ], + ], + expected_field_values: [ + Ticket::class => [ + 'plugin_fields_' . $dropdown_field->fields['name'] . 'dropdowns_id' => json_encode([$value1->getID(), $value2->getID()]), + $locations_field->fields['name'] => json_encode([$location1->getID(), $location2->getID()]), + ], + ], + ); + } finally { + // delete created items for another run + foreach ([$value1, $value2, $location1, $location2] as $item) { + $item->delete($item->fields, true); + } + } + } + #[Override] public static function provideConvertFieldConfigFromFormCreator(): iterable { diff --git a/tests/Units/FieldQuestionTypeTest.php b/tests/Units/FieldQuestionTypeTest.php index afccf7f6..08a4c6c5 100644 --- a/tests/Units/FieldQuestionTypeTest.php +++ b/tests/Units/FieldQuestionTypeTest.php @@ -38,10 +38,12 @@ use Glpi\Form\Condition\Type; use Glpi\Form\Condition\ValueOperator; use Glpi\Form\Condition\VisibilityStrategy; +use Glpi\Form\Question; use Glpi\Form\QuestionType\QuestionTypeShortText; use Glpi\Form\QuestionType\QuestionTypesManager; use Glpi\Tests\FormBuilder; use GlpiPlugin\Field\Tests\QuestionTypeTestCase; +use Location; use LogicException; use PluginFieldsContainer; use PluginFieldsDropdown; @@ -188,6 +190,58 @@ public function testFieldsQuestionSubmitEmptyDropdown(): void ]); } + public function testFormatRawAnswerForMultipleGlpiItemDropdown(): void + { + $this->login(); + + // Arrange: create a multiple "dropdown-Location" field + $this->fields['locations'] = $this->createField([ + 'label' => 'Locations', + 'type' => 'dropdown-' . Location::class, + 'multiple' => 1, + 'default_value' => [], + PluginFieldsContainer::getForeignKeyField() => $this->block->getID(), + 'ranking' => 2, + 'is_active' => 1, + ], ['default_value']); + + $location1 = $this->createItem(Location::class, [ + 'name' => __FUNCTION__ . ' 1', + 'entities_id' => $this->getTestRootEntity(true), + ]); + $location2 = $this->createItem(Location::class, [ + 'name' => __FUNCTION__ . ' 2', + 'entities_id' => $this->getTestRootEntity(true), + ]); + + $builder = new FormBuilder("My form"); + $builder->addQuestion( + "Locations question", + PluginFieldsQuestionType::class, + extra_data: json_encode($this->getFieldExtraDataConfig('locations')), + ); + $form = $this->createForm($builder); + $question = Question::getById($this->getQuestionId($form, "Locations question")); + + try { + // Act: format the answer as submitted by the end user template + $formatted = (new PluginFieldsQuestionType())->formatRawAnswer([ + 'itemtype' => Location::class, + 'items_id' => [(string) $location1->getID(), (string) $location2->getID()], + ], $question); + + // Assert: both location names are displayed + $this->assertEquals( + $location1->fields['name'] . ', ' . $location2->fields['name'], + $formatted, + ); + } finally { + // delete locations for another run + $location1->delete($location1->fields, true); + $location2->delete($location2->fields, true); + } + } + public function testFieldDeletionWhenUsedInForm(): void { $this->login(); From 87efa2957cc3a4d4719471dccba0b9fb5794b2db Mon Sep 17 00:00:00 2001 From: Julien Durand Date: Tue, 29 Sep 2026 12:28:17 +0200 Subject: [PATCH 2/4] chore(46462): changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87d50f5f..360f515b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +- Fix value mapping between a multiple dropdown field and it's destination in a form - Fix massive action update on CustomAssets - Fix mandatory fields on a Tab block not being enforced when updating an item. - Fix administrators losing access to a block's configuration after setting a profile to "no access" on that block. From 855fe2f0879d5431a8ef6fdc0370623b5655d649 Mon Sep 17 00:00:00 2001 From: Julien DURAND Date: Thu, 1 Oct 2026 08:55:30 +0200 Subject: [PATCH 3/4] typo in changelog Co-authored-by: Romain B. <8530352+Rom1-B@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 286075d2..75e4ef00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed -- Fix value mapping between a multiple dropdown field and it's destination in a form +- Fix value mapping between a multiple dropdown field and its destination in a form - Fix display width for rich text fields - Fix massive action update on CustomAssets - Fix mandatory fields on a Tab block not being enforced when updating an item. From 44d198a8f574a85ec72cc29cf87f69941de7bf05 Mon Sep 17 00:00:00 2001 From: Julien DURAND Date: Thu, 1 Oct 2026 08:56:59 +0200 Subject: [PATCH 4/4] fix: reset value at the beginning of every loops to avoid reusing last $value value Co-authored-by: Romain B. <8530352+Rom1-B@users.noreply.github.com> --- inc/destinationfield.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/inc/destinationfield.class.php b/inc/destinationfield.class.php index bf90eea4..00214e10 100644 --- a/inc/destinationfield.class.php +++ b/inc/destinationfield.class.php @@ -93,6 +93,7 @@ public function applyConfiguratedValueToInputUsingAnswers( ]); foreach ($answers as $answer) { + $value = null; $question = Question::getById($answer->getQuestionId()); $block_id = PluginFieldsContainer::findContainer($this->itil_destination->getTarget()::class, 'dom'); if (!$block_id) {