From 5b47bc73d0cf272efae16c15a31edfabacde9af9 Mon Sep 17 00:00:00 2001 From: Richard Loureiro Date: Wed, 29 Apr 2026 16:00:45 -0300 Subject: [PATCH 1/3] feat: add multi-select dropdown support for Forms questions Fields plugin dropdowns with multiple=1 now work correctly in GLPI Forms. Adds "Allow multiple options" toggle to the form editor when using the "Campo" question type, properly handles array answers in the save pipeline, and applies multi-select values correctly when creating tickets. --- inc/destinationfield.class.php | 22 ++++-- inc/questiontype.class.php | 91 ++++++++++++++++++++++- inc/questiontypeextradataconfig.class.php | 10 +++ 3 files changed, 115 insertions(+), 8 deletions(-) diff --git a/inc/destinationfield.class.php b/inc/destinationfield.class.php index 831f2081..ceda36b7 100644 --- a/inc/destinationfield.class.php +++ b/inc/destinationfield.class.php @@ -131,13 +131,25 @@ public function applyConfiguratedValueToInputUsingAnswers( $field_name = $field->fields['name']; } + $raw_answer = $answer->getRawAnswer(); + $is_multiple = (bool) $field->fields['multiple']; + if (!$is_multiple && $question->getQuestionType() instanceof PluginFieldsQuestionType) { + $extra = json_decode($question->fields['extra_data'] ?? '{}', true) ?? []; + $is_multiple = (bool) ($extra[PluginFieldsQuestionTypeExtraDataConfig::IS_MULTIPLE] ?? false); + } + 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 ($field->fields['type'] == 'dropdown') { - $input[$field_name] = $answer->getRawAnswer()['items_id']; + $input[sprintf('itemtype_%s', $field_name)] = $raw_answer['itemtype']; + $input[sprintf('items_id_%s', $field_name)] = $raw_answer['items_id']; + } elseif (str_starts_with((string) $field->fields['type'], 'dropdown')) { + $items_id = $raw_answer['items_id'] ?? $raw_answer; + if ($is_multiple && is_array($items_id)) { + $input[$field_name] = json_encode(array_values(array_map('intval', $items_id))); + } else { + $input[$field_name] = $items_id; + } } else { - $input[$field_name] = $value ?? $answer->getRawAnswer(); + $input[$field_name] = $value ?? $raw_answer; } } } diff --git a/inc/questiontype.class.php b/inc/questiontype.class.php index e40963d0..2c5410a7 100644 --- a/inc/questiontype.class.php +++ b/inc/questiontype.class.php @@ -32,6 +32,7 @@ use Glpi\DBAL\JsonFieldInterface; use Glpi\Form\Condition\ConditionHandler\ItemAsTextConditionHandler; use Glpi\Form\Condition\ConditionHandler\ItemConditionHandler; +use Glpi\Form\Condition\ConditionHandler\MultipleChoiceFromValuesConditionHandler; use Glpi\Form\Form; use Glpi\Form\Migration\FormQuestionDataConverterInterface; use Glpi\Form\Question; @@ -85,6 +86,43 @@ public function formatDefaultValueForDB(mixed $value): string return json_encode($value); } + #[Override] + public function prepareEndUserAnswer(Question $question, mixed $answer): mixed + { + if ($this->isMultipleForQuestion($question)) { + if (isset($answer['items_id']) && is_array($answer['items_id'])) { + $answer['items_id'] = array_values(array_map('intval', $answer['items_id'])); + } + } + + return $answer; + } + + #[Override] + public function renderAdministrationOptionsTemplate(?Question $question): string + { + $is_multiple = $this->isMultipleForQuestion($question); + + $template = << + + +TWIG; + + $twig = TemplateRenderer::getInstance(); + return $twig->renderFromStringTemplate($template, [ + 'is_multiple' => $is_multiple, + 'label' => __('Allow multiple options'), + ]); + } + #[Override] public function validateExtraDataInput(array $input): bool { @@ -134,6 +172,9 @@ public function renderAdministrationTemplate(?Question $question): string $default_value = json_decode($question->fields['default_value'], true); } + $field_data = $current_field->fields; + $field_data['multiple'] = $this->isMultipleForQuestion($question) ? 1 : 0; + $twig = TemplateRenderer::getInstance(); return $twig->render('@fields/question_type_administration.html.twig', [ 'question' => $question, @@ -141,7 +182,7 @@ public function renderAdministrationTemplate(?Question $question): string 'selected_field_id' => $current_field_id, 'available_fields' => $available_fields, 'item' => new Form(), - 'field' => $current_field->fields, + 'field' => $field_data, ]); } @@ -181,10 +222,13 @@ public function renderEndUserTemplate(Question $question): string } } + $field_data = $current_field->fields; + $field_data['multiple'] = $this->isMultipleForQuestion($question) ? 1 : 0; + $twig = TemplateRenderer::getInstance(); return $twig->render('@fields/question_type_end_user.html.twig', [ 'question' => $question, - 'field' => $current_field->fields, + 'field' => $field_data, 'default_value' => $default_value, 'item' => new Form(), 'itemtype' => $itemtype, @@ -312,7 +356,6 @@ public function getConditionHandlers( return parent::getConditionHandlers($question_config); } - // If the question is configured with a dropdown field, we add condition handlers to handle item and item as text conditions on the dropdown options $field = PluginFieldsField::getById($question_config->getFieldId()); if ($field && str_starts_with((string) $field->fields['type'], 'dropdown')) { if ($field->fields['type'] == 'dropdown') { @@ -330,11 +373,28 @@ public function getConditionHandlers( new ItemAsTextConditionHandler($itemtype), ], ); + + if ($field->fields['multiple'] || $question_config->isMultiple()) { + $condition_handlers[] = new MultipleChoiceFromValuesConditionHandler( + $this->getDropdownValuesForCondition($itemtype) + ); + } } return $condition_handlers; } + private function getDropdownValuesForCondition(string $itemtype): array + { + $values = []; + $item = new $itemtype(); + $rows = $item->find([], 'name'); + foreach ($rows as $row) { + $values[(string) $row['id']] = $row['name']; + } + return $values; + } + /** * Retrieve the default value block from the question's extra data * @@ -375,6 +435,31 @@ public function getDefaultValueFieldId(?Question $question): ?int return $config->getFieldId(); } + private function getFieldForQuestion(Question $question): ?PluginFieldsField + { + $field_id = $this->getDefaultValueFieldId($question); + if ($field_id === null) { + return null; + } + return PluginFieldsField::getById($field_id) ?: null; + } + + private function isMultipleForQuestion(?Question $question): bool + { + if (!$question instanceof Question) { + return false; + } + + /** @var ?PluginFieldsQuestionTypeExtraDataConfig $config */ + $config = $this->getExtraDataConfig(json_decode($question->fields['extra_data'], true) ?? []); + if ($config !== null && $config->isMultiple()) { + return true; + } + + $field = $this->getFieldForQuestion($question); + return $field !== null && (bool) $field->fields['multiple']; + } + private function getAvailableBlocks(): array { $field_container = new PluginFieldsContainer(); diff --git a/inc/questiontypeextradataconfig.class.php b/inc/questiontypeextradataconfig.class.php index ce07e6aa..65faf7fc 100644 --- a/inc/questiontypeextradataconfig.class.php +++ b/inc/questiontypeextradataconfig.class.php @@ -37,9 +37,12 @@ class PluginFieldsQuestionTypeExtraDataConfig implements JsonFieldInterface public const FIELD_ID = "field_id"; + public const IS_MULTIPLE = "is_multiple"; + public function __construct( private readonly ?int $block_id = null, private readonly ?int $field_id = null, + private readonly bool $is_multiple = false, ) {} #[Override] @@ -48,6 +51,7 @@ public static function jsonDeserialize(array $data): self return new self( block_id: $data[self::BLOCK_ID] ?? null, field_id: $data[self::FIELD_ID] ?? null, + is_multiple: (bool) ($data[self::IS_MULTIPLE] ?? false), ); } @@ -57,6 +61,7 @@ public function jsonSerialize(): array return [ self::BLOCK_ID => $this->block_id, self::FIELD_ID => $this->field_id, + self::IS_MULTIPLE => $this->is_multiple, ]; } @@ -69,4 +74,9 @@ public function getFieldId(): ?int { return $this->field_id; } + + public function isMultiple(): bool + { + return $this->is_multiple; + } } From 8983830ea7ed102ea0af4ae27851df9ff8fb611f Mon Sep 17 00:00:00 2001 From: Richard Loureiro Date: Wed, 29 Apr 2026 17:42:25 -0300 Subject: [PATCH 2/3] fix: allow tab containers in Forms and fix strlen crash on multi-select - Include 'tab' type containers in getAvailableBlocks() so fields from tab containers can be used in Forms questions (not only 'dom' type) - Fix strlen(): array given error in fields.html.twig by passing the decoded array value via the 'values' option instead of the value parameter when rendering multi-select dropdowns (both custom and GLPI core dropdown types) --- inc/questiontype.class.php | 2 +- templates/fields.html.twig | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/inc/questiontype.class.php b/inc/questiontype.class.php index 2c5410a7..549e1e97 100644 --- a/inc/questiontype.class.php +++ b/inc/questiontype.class.php @@ -476,7 +476,7 @@ private function getAvailableBlocks(): array $result = $field_container->find([ 'is_active' => 1, - 'type' => 'dom', + 'type' => ['dom', 'tab'], 'OR' => [ ['itemtypes' => ['LIKE', '%\"Ticket\"%']], ['itemtypes' => ['LIKE', '%\"Change\"%']], diff --git a/templates/fields.html.twig b/templates/fields.html.twig index 44a3c5a4..7f525dd7 100644 --- a/templates/fields.html.twig +++ b/templates/fields.html.twig @@ -142,6 +142,10 @@ {% set dropdown_options = {'entity': item.getEntityID()} %} {% if field['multiple'] %} {% set dropdown_options = dropdown_options|merge({'multiple': true}) %} + {% if value is iterable %} + {% set dropdown_options = dropdown_options|merge({'values': value}) %} + {% set value = '' %} + {% endif %} {% endif %} {% if item.isRecursive() %} {% set dropdown_options = dropdown_options|merge({'entity_sons': true}) %} @@ -168,6 +172,10 @@ {% endif %} {% if field['multiple'] %} {% set dropdown_options = dropdown_options|merge({'multiple': true}) %} + {% if value is iterable %} + {% set dropdown_options = dropdown_options|merge({'values': value}) %} + {% set value = '' %} + {% endif %} {% endif %} {{ macros.dropdownField(field['dropdown_class'], input_name, value, label, field_options|merge(dropdown_options|default({}))) }} From 5be418a78fffd43c6f76660e759954ac75f7115f Mon Sep 17 00:00:00 2001 From: Richard Loureiro Date: Fri, 1 May 2026 13:30:29 -0300 Subject: [PATCH 3/3] style: fix php-cs-fixer and Rector CI violations - Add trailing comma in MultipleChoiceFromValuesConditionHandler arg - Collapse nested if into single condition (Rector) - Use first-class callable intval(...) instead of string 'intval' (Rector) - Add blank line before return in getDropdownValuesForCondition (Rector) --- inc/destinationfield.class.php | 2 +- inc/questiontype.class.php | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/inc/destinationfield.class.php b/inc/destinationfield.class.php index ceda36b7..8b881823 100644 --- a/inc/destinationfield.class.php +++ b/inc/destinationfield.class.php @@ -144,7 +144,7 @@ public function applyConfiguratedValueToInputUsingAnswers( } elseif (str_starts_with((string) $field->fields['type'], 'dropdown')) { $items_id = $raw_answer['items_id'] ?? $raw_answer; if ($is_multiple && is_array($items_id)) { - $input[$field_name] = json_encode(array_values(array_map('intval', $items_id))); + $input[$field_name] = json_encode(array_values(array_map(intval(...), $items_id))); } else { $input[$field_name] = $items_id; } diff --git a/inc/questiontype.class.php b/inc/questiontype.class.php index 549e1e97..39c4c410 100644 --- a/inc/questiontype.class.php +++ b/inc/questiontype.class.php @@ -89,10 +89,8 @@ public function formatDefaultValueForDB(mixed $value): string #[Override] public function prepareEndUserAnswer(Question $question, mixed $answer): mixed { - if ($this->isMultipleForQuestion($question)) { - if (isset($answer['items_id']) && is_array($answer['items_id'])) { - $answer['items_id'] = array_values(array_map('intval', $answer['items_id'])); - } + if ($this->isMultipleForQuestion($question) && (isset($answer['items_id']) && is_array($answer['items_id']))) { + $answer['items_id'] = array_values(array_map(intval(...), $answer['items_id'])); } return $answer; @@ -376,7 +374,7 @@ public function getConditionHandlers( if ($field->fields['multiple'] || $question_config->isMultiple()) { $condition_handlers[] = new MultipleChoiceFromValuesConditionHandler( - $this->getDropdownValuesForCondition($itemtype) + $this->getDropdownValuesForCondition($itemtype), ); } } @@ -392,6 +390,7 @@ private function getDropdownValuesForCondition(string $itemtype): array foreach ($rows as $row) { $values[(string) $row['id']] = $row['name']; } + return $values; }