From 2a277d0a7067b4d758e3080d35410fcd69ced53a Mon Sep 17 00:00:00 2001 From: MyuTsu Date: Mon, 14 Sep 2026 13:52:45 +0200 Subject: [PATCH 1/2] feat(forms): support multiple-select dropdowns in Fields plugin questions --- inc/destinationfield.class.php | 12 +- inc/field.class.php | 2 +- inc/questiontype.class.php | 37 ++- .../question_type_administration.html.twig | 4 +- templates/question_type_end_user.html.twig | 7 +- tests/QuestionTypeTestCase.php | 27 ++ tests/Units/FieldDestinationFieldTest.php | 78 +++++- tests/Units/FieldQuestionTypeTest.php | 262 ++++++++++++++++-- 8 files changed, 389 insertions(+), 40 deletions(-) diff --git a/inc/destinationfield.class.php b/inc/destinationfield.class.php index bacd9722..e1819460 100644 --- a/inc/destinationfield.class.php +++ b/inc/destinationfield.class.php @@ -135,8 +135,16 @@ public function applyConfiguratedValueToInputUsingAnswers( $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')) { - $raw_id = (int) ($answer->getRawAnswer()['items_id'] ?? 0); - $input[$field_name] = ($raw_id > 0) ? $raw_id : null; + $ids = array_values(array_filter( + array_map(intval(...), PluginFieldsQuestionType::extractDropdownAnswerIds($answer->getRawAnswer())), + static fn($id) => $id > 0, + )); + + if ($field->fields['multiple']) { + $input[$field_name] = $ids; + } else { + $input[$field_name] = (int) (reset($ids) ?: 0); + } } else { $input[$field_name] = $value ?? $answer->getRawAnswer(); } diff --git a/inc/field.class.php b/inc/field.class.php index edbc3583..1dc7aace 100644 --- a/inc/field.class.php +++ b/inc/field.class.php @@ -1326,7 +1326,7 @@ public static function prepareHtmlFields( $value = array_values(array_filter( array_merge(...array_map( static fn($v) => is_array($v) ? array_values($v) : [$v], - $value, + array_values($value), )), is_scalar(...), )); diff --git a/inc/questiontype.class.php b/inc/questiontype.class.php index 0d83b1ce..72882f23 100644 --- a/inc/questiontype.class.php +++ b/inc/questiontype.class.php @@ -203,10 +203,7 @@ public function formatRawAnswer(mixed $answer, Question $question): string case 'date': return (string) $answer; case 'dropdown': - $answer = $answer['items_id']; - if (is_string($answer) || is_numeric($answer)) { - $answer = [$answer]; - } + $answer = self::extractDropdownAnswerIds($answer); $itemtype = PluginFieldsDropdown::getClassname($current_field->fields['name']); return implode(', ', array_map(fn($opt_id) => $itemtype::getById($opt_id)?->fields['name'] ?? '', $answer)); @@ -234,9 +231,7 @@ public function formatRawAnswer(mixed $answer, Question $question): string return ''; } - if (!is_array($answer)) { - $answer = [$answer]; - } + $answer = self::extractDropdownAnswerIds($answer); $names = []; foreach ($answer as $items_id) { @@ -252,6 +247,20 @@ public function formatRawAnswer(mixed $answer, Question $question): string return (string) $answer; } + /** + * Extract the selected item id(s) from a dropdown-type question's raw answer. + * + * @return array + */ + public static function extractDropdownAnswerIds(mixed $answer): array + { + if (is_array($answer) && array_key_exists('itemtype', $answer)) { + $answer = $answer['items_ids'] ?? $answer['items_id'] ?? []; + } + + return is_array($answer) ? $answer : [$answer]; + } + #[Override] public function beforeConversion(array $rawData): void {} @@ -315,12 +324,18 @@ public function getConditionHandlers( $itemtype = $dropdown_matches['class']; } + $is_multiple = (bool) $field->fields['multiple']; + $condition_handlers = array_merge( $condition_handlers, - [ - new ItemConditionHandler($itemtype), - new ItemAsTextConditionHandler($itemtype), - ], + $is_multiple + ? [ + new ItemConditionHandler($itemtype, true), + ] + : [ + new ItemConditionHandler($itemtype, false), + new ItemAsTextConditionHandler($itemtype), + ], ); } diff --git a/templates/question_type_administration.html.twig b/templates/question_type_administration.html.twig index 47ceaf16..6de9abee 100644 --- a/templates/question_type_administration.html.twig +++ b/templates/question_type_administration.html.twig @@ -33,10 +33,10 @@ {% set is_ajax_reload = is_ajax_reload|default(false) %} {% set is_dropdown = field.type starts with 'dropdown' %} -{% set name_suffix = is_dropdown ? '[items_id]' : '' %} +{% set name_suffix = is_dropdown ? '[items_ids]' : '' %} {% set field_for_html = field|merge({ - 'default_value': default_value.items_id ?? default_value ?? field.default_value, + 'default_value': is_dropdown ? (default_value.items_ids ?? default_value.items_id ?? default_value ?? field.default_value) : (default_value ?? field.default_value), 'mandatory': false }) %} diff --git a/templates/question_type_end_user.html.twig b/templates/question_type_end_user.html.twig index 4e583ba5..0891a18c 100644 --- a/templates/question_type_end_user.html.twig +++ b/templates/question_type_end_user.html.twig @@ -28,12 +28,13 @@ {% import 'components/form/fields_macros.html.twig' as fields %} +{% set is_dropdown = field.type starts with 'dropdown' %} + {% set field = field|merge({ - 'default_value': default_value.items_id ?? default_value ?? field.default_value + 'default_value': is_dropdown ? (default_value.items_ids ?? default_value.items_id ?? default_value ?? field.default_value) : (default_value ?? field.default_value) }) %} -{% set is_dropdown = field.type starts with 'dropdown' %} -{% set name_suffix = is_dropdown ? '[items_id]' : '' %} +{% set name_suffix = is_dropdown ? '[items_ids]' : '' %} {{ fields.hiddenField( question.getEndUserInputName() ~ '[itemtype]', diff --git a/tests/QuestionTypeTestCase.php b/tests/QuestionTypeTestCase.php index e3fd1c3b..075506b2 100644 --- a/tests/QuestionTypeTestCase.php +++ b/tests/QuestionTypeTestCase.php @@ -35,6 +35,7 @@ use Glpi\Tests\DbTestCase; use Glpi\Tests\FormTesterTrait; use Glpi\Tests\GLPITestCase; +use Location; use PluginFieldsContainer; use PluginFieldsField; use ReflectionClass; @@ -78,6 +79,32 @@ public function createFieldAndContainer(): void 'ranking' => 1, 'is_active' => 1, ]); + + $this->fields['dropdown_multiple'] = $this->createField([ + 'label' => 'Dropdown multiple', + 'type' => 'dropdown', + 'multiple' => 1, + PluginFieldsContainer::getForeignKeyField() => $this->block->getID(), + 'ranking' => 1, + 'is_active' => 1, + ]); + + $this->fields['dropdown_location'] = $this->createField([ + 'label' => 'Dropdown location', + 'type' => 'dropdown-' . Location::class, + PluginFieldsContainer::getForeignKeyField() => $this->block->getID(), + 'ranking' => 1, + 'is_active' => 1, + ]); + + $this->fields['dropdown_location_multiple'] = $this->createField([ + 'label' => 'Dropdown location multiple', + 'type' => 'dropdown-' . Location::class, + 'multiple' => 1, + PluginFieldsContainer::getForeignKeyField() => $this->block->getID(), + 'ranking' => 1, + 'is_active' => 1, + ]); } public function setUp(): void diff --git a/tests/Units/FieldDestinationFieldTest.php b/tests/Units/FieldDestinationFieldTest.php index 246bfa9b..24e37c0b 100644 --- a/tests/Units/FieldDestinationFieldTest.php +++ b/tests/Units/FieldDestinationFieldTest.php @@ -124,6 +124,15 @@ private function initFieldTest(): void 'is_active' => 1, 'is_readonly' => 0, ]); + $this->fields[] = $this->createField([ + 'label' => 'Location Field Multiple', + 'type' => 'dropdown-Location', + 'multiple' => 1, + PluginFieldsContainer::getForeignKeyField() => $this->blocks[Ticket::class]->getID(), + 'ranking' => 4, + 'is_active' => 1, + 'is_readonly' => 0, + ]); } public function setUp(): void @@ -230,26 +239,85 @@ public function testDestinationWithLocationAdditonalFields(): void 'entities_id' => $this->getTestRootEntity(true), ]); + $expected_field_values = [ + Ticket::class => [ + $this->fields[4]->fields['name'] => $location->getID(), + ], + ]; + + // The end user template submits dropdowns as an array containing the selected itemtype and items_ids. + $this->sendFormAndAssertITILObjectAdditionalFields( + form: $form, + config: new SimpleValueConfig(1), + answers: [ + "Location Field" => [ + 'itemtype' => Location::class, + 'items_ids' => $location->getID(), + ], + ], + expected_field_values: $expected_field_values, + ); + + // Answers submitted before the 'items_ids' rename are stored with a singular 'items_id' key $this->sendFormAndAssertITILObjectAdditionalFields( form: $form, config: new SimpleValueConfig(1), answers: [ - // The end user template submits dropdowns as an array - // containing the selected itemtype and items_id. "Location Field" => [ 'itemtype' => Location::class, 'items_id' => $location->getID(), ], ], + expected_field_values: $expected_field_values, + ); + + // delete location for another run + $location->delete($location->fields, true); + } + + public function testDestinationWithMultipleLocationAdditionalFields(): void + { + $this->login(); + $form = $this->createForm((new FormBuilder())->addQuestion( + "Location Field Multiple", + PluginFieldsQuestionType::class, + extra_data: json_encode([ + 'block_id' => $this->blocks[Ticket::class]->getID(), + 'field_id' => $this->fields[5]->getID(), + ]), + )); + + // Arrange: Create two locations to select + $location1 = $this->createItem(Location::class, [ + 'name' => 'Location Alpha', + 'entities_id' => $this->getTestRootEntity(true), + ]); + $location2 = $this->createItem(Location::class, [ + 'name' => 'Location Beta', + 'entities_id' => $this->getTestRootEntity(true), + ]); + + $this->sendFormAndAssertITILObjectAdditionalFields( + form: $form, + config: new SimpleValueConfig(1), + answers: [ + // This is the shape a real multi-select submission produces: + // items_ids as an array of the selected ids + "Location Field Multiple" => [ + 'itemtype' => Location::class, + 'items_ids' => [$location1->getID(), $location2->getID()], + ], + ], expected_field_values: [ Ticket::class => [ - $this->fields[4]->fields['name'] => $location->getID(), + // 'multiple' fields are stored as a JSON-encoded array of all selected ids + $this->fields[5]->fields['name'] => json_encode([$location1->getID(), $location2->getID()]), ], ], ); - // delete location for another run - $location->delete($location->fields, true); + $location1->delete($location1->fields, true); + $location2->delete($location2->fields, true); } #[Override] diff --git a/tests/Units/FieldQuestionTypeTest.php b/tests/Units/FieldQuestionTypeTest.php index 856fca89..8014c118 100644 --- a/tests/Units/FieldQuestionTypeTest.php +++ b/tests/Units/FieldQuestionTypeTest.php @@ -38,11 +38,13 @@ 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 LogicException; +use Location; use PluginFieldsContainer; use PluginFieldsDropdown; use PluginFieldsField; @@ -158,18 +160,13 @@ public function testFieldsQuestionHelpdeskRendering(): void $this->assertNotEmpty($crawler->filter('[data-glpi-form-renderer-fields-question-type-specific-container]')); } + public function testFieldsQuestionSubmitEmptyDropdown(): void { $this->login(); - /** @var CommonDBTM $dropdown_item */ - $dropdown_item = getItemForItemtype(PluginFieldsDropdown::getClassname($this->fields['dropdown']->fields['name'])); - $dropdown_ids = []; - for ($i = 1; $i <= 3; $i++) { - $dropdown_ids[] = $dropdown_item->add([ - 'name' => 'Option ' . $i, - ]); - } + $itemtype = PluginFieldsDropdown::getClassname($this->fields['dropdown']->fields['name']); + $this->createItemsWithNames($itemtype, ['Option 1', 'Option 2', 'Option 3']); // Arrange: create form with Field question $builder = new FormBuilder("My form"); @@ -183,11 +180,124 @@ public function testFieldsQuestionSubmitEmptyDropdown(): void // Act: submit form $this->sendFormAndGetCreatedTicket($form, [ "Dropdown field question" => [ - 'items_id' => '0', + 'itemtype' => $itemtype, + 'items_ids' => '0', ], ]); } + public function testFormatRawAnswerSupportsLegacyItemsIdKey(): void + { + $this->login(); + + $itemtype = PluginFieldsDropdown::getClassname($this->fields['dropdown']->fields['name']); + $item = $this->createItem($itemtype, ['name' => 'Legacy Option']); + + $builder = new FormBuilder("Legacy answer format form"); + $builder->addQuestion( + "Dropdown field question", + PluginFieldsQuestionType::class, + extra_data: json_encode($this->getFieldExtraDataConfig('dropdown')), + ); + $form = $this->createForm($builder); + + $question = new Question(); + $this->assertTrue($question->getFromDB($this->getQuestionId($form, "Dropdown field question"))); + + $question_type = new PluginFieldsQuestionType(); + + // Answers submitted before the 'items_ids' rename are stored with a singular 'items_id' key + $legacy_answer = ['itemtype' => $itemtype, 'items_id' => $item->getID()]; + $current_answer = ['itemtype' => $itemtype, 'items_ids' => $item->getID()]; + + $this->assertSame('Legacy Option', $question_type->formatRawAnswer($legacy_answer, $question)); + $this->assertSame( + $question_type->formatRawAnswer($current_answer, $question), + $question_type->formatRawAnswer($legacy_answer, $question), + ); + + // Answers submitted before dropdown values were wrapped with their itemtype (plugin < 1.24.0) + // are stored as a bare scalar id + $pre_wrapping_answer = (string) $item->getID(); + $this->assertSame('Legacy Option', $question_type->formatRawAnswer($pre_wrapping_answer, $question)); + } + + public function testFormatRawAnswerForNativeItemtypeDropdown(): void + { + $this->login(); + + $location = $this->createItem(Location::class, [ + 'name' => 'Native Dropdown Location', + 'entities_id' => $this->getTestRootEntity(true), + ]); + + $builder = new FormBuilder("Native dropdown form"); + $builder->addQuestion( + "Location dropdown question", + PluginFieldsQuestionType::class, + extra_data: json_encode($this->getFieldExtraDataConfig('dropdown_location')), + ); + $form = $this->createForm($builder); + + $question = new Question(); + $this->assertTrue($question->getFromDB($this->getQuestionId($form, "Location dropdown question"))); + + $question_type = new PluginFieldsQuestionType(); + + // Current format + $answer = ['itemtype' => Location::class, 'items_ids' => $location->getID()]; + $this->assertSame('Native Dropdown Location', $question_type->formatRawAnswer($answer, $question)); + + // Answers submitted before the 'items_ids' rename are stored with a singular 'items_id' key + $legacy_answer = ['itemtype' => Location::class, 'items_id' => $location->getID()]; + $this->assertSame('Native Dropdown Location', $question_type->formatRawAnswer($legacy_answer, $question)); + + // Answers submitted before dropdown values were wrapped with their itemtype (plugin < 1.24.0) + // are stored as a bare scalar id + $pre_wrapping_answer = (string) $location->getID(); + $this->assertSame('Native Dropdown Location', $question_type->formatRawAnswer($pre_wrapping_answer, $question)); + + $location->delete($location->fields, true); + } + + public function testFormatRawAnswerForNativeItemtypeMultipleDropdown(): void + { + $this->login(); + + $location1 = $this->createItem(Location::class, [ + 'name' => 'Location Alpha', + 'entities_id' => $this->getTestRootEntity(true), + ]); + $location2 = $this->createItem(Location::class, [ + 'name' => 'Location Beta', + 'entities_id' => $this->getTestRootEntity(true), + ]); + + $builder = new FormBuilder("Native multiple dropdown form"); + $builder->addQuestion( + "Location dropdown question", + PluginFieldsQuestionType::class, + extra_data: json_encode($this->getFieldExtraDataConfig('dropdown_location_multiple')), + ); + $form = $this->createForm($builder); + + $question = new Question(); + $this->assertTrue($question->getFromDB($this->getQuestionId($form, "Location dropdown question"))); + + $question_type = new PluginFieldsQuestionType(); + $answer = ['itemtype' => Location::class, 'items_ids' => [$location1->getID(), $location2->getID()]]; + + $this->assertSame('Location Alpha, Location Beta', $question_type->formatRawAnswer($answer, $question)); + + // Answers submitted before dropdown values were wrapped with their itemtype (plugin < 1.24.0) + // are stored as a flat array of ids, with no wrapper at all + $pre_wrapping_answer = [$location1->getID(), $location2->getID()]; + $this->assertSame('Location Alpha, Location Beta', $question_type->formatRawAnswer($pre_wrapping_answer, $question)); + + $location1->delete($location1->fields, true); + $location2->delete($location2->fields, true); + } + public function testFieldDeletionWhenUsedInForm(): void { $this->login(); @@ -305,8 +415,7 @@ public function testDropdownConditionHandlerContainsOperator(): void $this->login(); $itemtype = PluginFieldsDropdown::getClassname($this->fields['dropdown']->fields['name']); - $dropdown_item = getItemForItemtype($itemtype); - $item_id = $dropdown_item->add(['name' => 'Alpha Option']); + $item_id = $this->createItem($itemtype, ['name' => 'Alpha Option'])->getID(); $builder = new FormBuilder("Dropdown contains test form"); $builder->addQuestion( @@ -363,8 +472,7 @@ public function testDropdownConditionHandlerNotContainsOperator(): void $this->login(); $itemtype = PluginFieldsDropdown::getClassname($this->fields['dropdown']->fields['name']); - $dropdown_item = getItemForItemtype($itemtype); - $item_id = $dropdown_item->add(['name' => 'Beta Option']); + $item_id = $this->createItem($itemtype, ['name' => 'Beta Option'])->getID(); $builder = new FormBuilder("Dropdown not contains test form"); $builder->addQuestion( @@ -416,6 +524,91 @@ public function testDropdownConditionHandlerNotContainsOperator(): void $this->assertFalse($engine->computeVisibility()->isQuestionVisible($question_id2)); } + public function testGetConditionHandlersForMultipleDropdownFieldExcludesItemAsTextHandler(): void + { + $question_type = new PluginFieldsQuestionType(); + $config = $this->getFieldExtraDataConfig('dropdown_multiple'); + + $handlers = $question_type->getConditionHandlers($config); + $handler_classes = array_map(fn($h) => $h::class, $handlers); + + $this->assertContains(ItemConditionHandler::class, $handler_classes); + $this->assertNotContains(ItemAsTextConditionHandler::class, $handler_classes); + + /** @var ItemConditionHandler $item_handler */ + $item_handler = current(array_filter($handlers, fn($h) => $h instanceof ItemConditionHandler)); + $this->assertContains(ValueOperator::CONTAINS, $item_handler->getSupportedValueOperators()); + $this->assertContains(ValueOperator::NOT_CONTAINS, $item_handler->getSupportedValueOperators()); + } + + public function testMultipleDropdownConditionHandlerEqualsOperatorIsOrderIndependent(): void + { + $this->login(); + + [$form, $question_id, $dropdown_question_id, $itemtype, $item1_id, $item2_id] = $this->createMultipleDropdownConditionForm( + ValueOperator::EQUALS, + ); + + // Test: same selection (single item, matches condition value) → question is visible + $engine = new Engine($form, new EngineInput([$dropdown_question_id => ['itemtype' => $itemtype, 'items_ids' => [$item1_id]]])); + $this->assertTrue($engine->computeVisibility()->isQuestionVisible($question_id)); + + // Test: additional item selected → question is not visible + $engine = new Engine($form, new EngineInput([$dropdown_question_id => ['itemtype' => $itemtype, 'items_ids' => [$item1_id, $item2_id]]])); + $this->assertFalse($engine->computeVisibility()->isQuestionVisible($question_id)); + } + + public function testMultipleDropdownConditionHandlerNotEqualsOperatorIsOrderIndependent(): void + { + $this->login(); + + [$form, $question_id, $dropdown_question_id, $itemtype, $item1_id, $item2_id] = $this->createMultipleDropdownConditionForm( + ValueOperator::NOT_EQUALS, + ); + + // Test: same selection (single item, matches condition value) → question is not visible + $engine = new Engine($form, new EngineInput([$dropdown_question_id => ['itemtype' => $itemtype, 'items_ids' => [$item1_id]]])); + $this->assertFalse($engine->computeVisibility()->isQuestionVisible($question_id)); + + // Test: additional item selected → question is visible + $engine = new Engine($form, new EngineInput([$dropdown_question_id => ['itemtype' => $itemtype, 'items_ids' => [$item1_id, $item2_id]]])); + $this->assertTrue($engine->computeVisibility()->isQuestionVisible($question_id)); + } + + public function testMultipleDropdownConditionHandlerContainsOperator(): void + { + $this->login(); + + [$form, $question_id, $dropdown_question_id, $itemtype, $item1_id, $item2_id] = $this->createMultipleDropdownConditionForm( + ValueOperator::CONTAINS, + ); + + // Test: selection includes the required item among others → question is visible + $engine = new Engine($form, new EngineInput([$dropdown_question_id => ['itemtype' => $itemtype, 'items_ids' => [$item1_id, $item2_id]]])); + $this->assertTrue($engine->computeVisibility()->isQuestionVisible($question_id)); + + // Test: selection does not include the required item → question is not visible + $engine = new Engine($form, new EngineInput([$dropdown_question_id => ['itemtype' => $itemtype, 'items_ids' => [$item2_id]]])); + $this->assertFalse($engine->computeVisibility()->isQuestionVisible($question_id)); + } + + public function testMultipleDropdownConditionHandlerNotContainsOperator(): void + { + $this->login(); + + [$form, $question_id, $dropdown_question_id, $itemtype, $item1_id, $item2_id] = $this->createMultipleDropdownConditionForm( + ValueOperator::NOT_CONTAINS, + ); + + // Test: selection does not include the excluded item → question is visible + $engine = new Engine($form, new EngineInput([$dropdown_question_id => ['itemtype' => $itemtype, 'items_ids' => [$item2_id]]])); + $this->assertTrue($engine->computeVisibility()->isQuestionVisible($question_id)); + + // Test: selection includes the excluded item → question is not visible + $engine = new Engine($form, new EngineInput([$dropdown_question_id => ['itemtype' => $itemtype, 'items_ids' => [$item1_id, $item2_id]]])); + $this->assertFalse($engine->computeVisibility()->isQuestionVisible($question_id)); + } + private function getFieldExtraDataConfig(string $field_name): PluginFieldsQuestionTypeExtraDataConfig { if (!$this->block instanceof PluginFieldsContainer || !$this->fields[$field_name] instanceof PluginFieldsField) { @@ -432,9 +625,9 @@ private function getFieldExtraDataConfig(string $field_name): PluginFieldsQuesti private function createDropdownConditionForm(ValueOperator $operator): array { $itemtype = PluginFieldsDropdown::getClassname($this->fields['dropdown']->fields['name']); - $dropdown_item = getItemForItemtype($itemtype); - $item1_id = $dropdown_item->add(['name' => 'First Option']); - $item2_id = $dropdown_item->add(['name' => 'Second Option']); + [$item1, $item2] = $this->createItemsWithNames($itemtype, ['First Option', 'Second Option']); + $item1_id = $item1->getID(); + $item2_id = $item2->getID(); $condition_value = ['itemtype' => $itemtype, 'items_ids' => [$item1_id]]; @@ -461,4 +654,41 @@ private function createDropdownConditionForm(ValueOperator $operator): array return [$form, $question_id, $dropdown_question_id, $itemtype, $item1_id, $item2_id]; } + + /** + * Helper to create a form with a "multiple" dropdown question and a condition on it. + * Returns [form, question_id, dropdown_question_id, itemtype, item1_id, item2_id]. + */ + private function createMultipleDropdownConditionForm(ValueOperator $operator): array + { + $itemtype = PluginFieldsDropdown::getClassname($this->fields['dropdown_multiple']->fields['name']); + [$item1, $item2] = $this->createItemsWithNames($itemtype, ['First Option', 'Second Option']); + $item1_id = $item1->getID(); + $item2_id = $item2->getID(); + + $condition_value = ['itemtype' => $itemtype, 'items_ids' => [$item1_id]]; + + $builder = new FormBuilder("Multiple dropdown condition form"); + $builder->addQuestion( + "Dropdown question", + PluginFieldsQuestionType::class, + extra_data: json_encode($this->getFieldExtraDataConfig('dropdown_multiple')), + ); + $builder->addQuestion("Subject", QuestionTypeShortText::class); + $builder->setQuestionVisibility("Subject", VisibilityStrategy::VISIBLE_IF, [ + [ + 'logic_operator' => LogicOperator::AND, + 'item_name' => "Dropdown question", + 'item_type' => Type::QUESTION, + 'value_operator' => $operator, + 'value' => $condition_value, + ], + ]); + $form = $this->createForm($builder); + + $question_id = $this->getQuestionId($form, "Subject"); + $dropdown_question_id = $this->getQuestionId($form, "Dropdown question"); + + return [$form, $question_id, $dropdown_question_id, $itemtype, $item1_id, $item2_id]; + } } From 7eab904e2d9ec368a5e583aa5cf3ceec19dce218 Mon Sep 17 00:00:00 2001 From: MyuTsu Date: Mon, 14 Sep 2026 14:05:14 +0200 Subject: [PATCH 2/2] changelog.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b8b2c2e..c5cc3e1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - GLPI 12 compatibility +- Add multiple-select support for "Field" dropdown questions in forms ## [1.24.4] - 2026-08-06