Skip to content

Commit 5b47bc7

Browse files
committed
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.
1 parent b97257b commit 5b47bc7

3 files changed

Lines changed: 115 additions & 8 deletions

File tree

‎inc/destinationfield.class.php‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,25 @@ public function applyConfiguratedValueToInputUsingAnswers(
131131
$field_name = $field->fields['name'];
132132
}
133133

134+
$raw_answer = $answer->getRawAnswer();
135+
$is_multiple = (bool) $field->fields['multiple'];
136+
if (!$is_multiple && $question->getQuestionType() instanceof PluginFieldsQuestionType) {
137+
$extra = json_decode($question->fields['extra_data'] ?? '{}', true) ?? [];
138+
$is_multiple = (bool) ($extra[PluginFieldsQuestionTypeExtraDataConfig::IS_MULTIPLE] ?? false);
139+
}
140+
134141
if ($field->fields['type'] == 'glpi_item') {
135-
$input[sprintf('itemtype_%s', $field_name)] = $answer->getRawAnswer()['itemtype'];
136-
$input[sprintf('items_id_%s', $field_name)] = $answer->getRawAnswer()['items_id'];
137-
} elseif ($field->fields['type'] == 'dropdown') {
138-
$input[$field_name] = $answer->getRawAnswer()['items_id'];
142+
$input[sprintf('itemtype_%s', $field_name)] = $raw_answer['itemtype'];
143+
$input[sprintf('items_id_%s', $field_name)] = $raw_answer['items_id'];
144+
} elseif (str_starts_with((string) $field->fields['type'], 'dropdown')) {
145+
$items_id = $raw_answer['items_id'] ?? $raw_answer;
146+
if ($is_multiple && is_array($items_id)) {
147+
$input[$field_name] = json_encode(array_values(array_map('intval', $items_id)));
148+
} else {
149+
$input[$field_name] = $items_id;
150+
}
139151
} else {
140-
$input[$field_name] = $value ?? $answer->getRawAnswer();
152+
$input[$field_name] = $value ?? $raw_answer;
141153
}
142154
}
143155
}

‎inc/questiontype.class.php‎

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use Glpi\DBAL\JsonFieldInterface;
3333
use Glpi\Form\Condition\ConditionHandler\ItemAsTextConditionHandler;
3434
use Glpi\Form\Condition\ConditionHandler\ItemConditionHandler;
35+
use Glpi\Form\Condition\ConditionHandler\MultipleChoiceFromValuesConditionHandler;
3536
use Glpi\Form\Form;
3637
use Glpi\Form\Migration\FormQuestionDataConverterInterface;
3738
use Glpi\Form\Question;
@@ -85,6 +86,43 @@ public function formatDefaultValueForDB(mixed $value): string
8586
return json_encode($value);
8687
}
8788

89+
#[Override]
90+
public function prepareEndUserAnswer(Question $question, mixed $answer): mixed
91+
{
92+
if ($this->isMultipleForQuestion($question)) {
93+
if (isset($answer['items_id']) && is_array($answer['items_id'])) {
94+
$answer['items_id'] = array_values(array_map('intval', $answer['items_id']));
95+
}
96+
}
97+
98+
return $answer;
99+
}
100+
101+
#[Override]
102+
public function renderAdministrationOptionsTemplate(?Question $question): string
103+
{
104+
$is_multiple = $this->isMultipleForQuestion($question);
105+
106+
$template = <<<TWIG
107+
<div class="d-flex gap-2">
108+
<label class="form-check form-switch mb-0">
109+
<input type="hidden" name="is_multiple" value="0"
110+
data-glpi-form-editor-specific-question-extra-data>
111+
<input class="form-check-input" type="checkbox" name="is_multiple"
112+
value="1" {{ is_multiple ? 'checked' : '' }}
113+
data-glpi-form-editor-specific-question-extra-data>
114+
<span class="form-check-label">{{ label }}</span>
115+
</label>
116+
</div>
117+
TWIG;
118+
119+
$twig = TemplateRenderer::getInstance();
120+
return $twig->renderFromStringTemplate($template, [
121+
'is_multiple' => $is_multiple,
122+
'label' => __('Allow multiple options'),
123+
]);
124+
}
125+
88126
#[Override]
89127
public function validateExtraDataInput(array $input): bool
90128
{
@@ -134,14 +172,17 @@ public function renderAdministrationTemplate(?Question $question): string
134172
$default_value = json_decode($question->fields['default_value'], true);
135173
}
136174

175+
$field_data = $current_field->fields;
176+
$field_data['multiple'] = $this->isMultipleForQuestion($question) ? 1 : 0;
177+
137178
$twig = TemplateRenderer::getInstance();
138179
return $twig->render('@fields/question_type_administration.html.twig', [
139180
'question' => $question,
140181
'default_value' => $default_value,
141182
'selected_field_id' => $current_field_id,
142183
'available_fields' => $available_fields,
143184
'item' => new Form(),
144-
'field' => $current_field->fields,
185+
'field' => $field_data,
145186
]);
146187
}
147188

@@ -181,10 +222,13 @@ public function renderEndUserTemplate(Question $question): string
181222
}
182223
}
183224

225+
$field_data = $current_field->fields;
226+
$field_data['multiple'] = $this->isMultipleForQuestion($question) ? 1 : 0;
227+
184228
$twig = TemplateRenderer::getInstance();
185229
return $twig->render('@fields/question_type_end_user.html.twig', [
186230
'question' => $question,
187-
'field' => $current_field->fields,
231+
'field' => $field_data,
188232
'default_value' => $default_value,
189233
'item' => new Form(),
190234
'itemtype' => $itemtype,
@@ -312,7 +356,6 @@ public function getConditionHandlers(
312356
return parent::getConditionHandlers($question_config);
313357
}
314358

315-
// 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
316359
$field = PluginFieldsField::getById($question_config->getFieldId());
317360
if ($field && str_starts_with((string) $field->fields['type'], 'dropdown')) {
318361
if ($field->fields['type'] == 'dropdown') {
@@ -330,11 +373,28 @@ public function getConditionHandlers(
330373
new ItemAsTextConditionHandler($itemtype),
331374
],
332375
);
376+
377+
if ($field->fields['multiple'] || $question_config->isMultiple()) {
378+
$condition_handlers[] = new MultipleChoiceFromValuesConditionHandler(
379+
$this->getDropdownValuesForCondition($itemtype)
380+
);
381+
}
333382
}
334383

335384
return $condition_handlers;
336385
}
337386

387+
private function getDropdownValuesForCondition(string $itemtype): array
388+
{
389+
$values = [];
390+
$item = new $itemtype();
391+
$rows = $item->find([], 'name');
392+
foreach ($rows as $row) {
393+
$values[(string) $row['id']] = $row['name'];
394+
}
395+
return $values;
396+
}
397+
338398
/**
339399
* Retrieve the default value block from the question's extra data
340400
*
@@ -375,6 +435,31 @@ public function getDefaultValueFieldId(?Question $question): ?int
375435
return $config->getFieldId();
376436
}
377437

438+
private function getFieldForQuestion(Question $question): ?PluginFieldsField
439+
{
440+
$field_id = $this->getDefaultValueFieldId($question);
441+
if ($field_id === null) {
442+
return null;
443+
}
444+
return PluginFieldsField::getById($field_id) ?: null;
445+
}
446+
447+
private function isMultipleForQuestion(?Question $question): bool
448+
{
449+
if (!$question instanceof Question) {
450+
return false;
451+
}
452+
453+
/** @var ?PluginFieldsQuestionTypeExtraDataConfig $config */
454+
$config = $this->getExtraDataConfig(json_decode($question->fields['extra_data'], true) ?? []);
455+
if ($config !== null && $config->isMultiple()) {
456+
return true;
457+
}
458+
459+
$field = $this->getFieldForQuestion($question);
460+
return $field !== null && (bool) $field->fields['multiple'];
461+
}
462+
378463
private function getAvailableBlocks(): array
379464
{
380465
$field_container = new PluginFieldsContainer();

‎inc/questiontypeextradataconfig.class.php‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,12 @@ class PluginFieldsQuestionTypeExtraDataConfig implements JsonFieldInterface
3737

3838
public const FIELD_ID = "field_id";
3939

40+
public const IS_MULTIPLE = "is_multiple";
41+
4042
public function __construct(
4143
private readonly ?int $block_id = null,
4244
private readonly ?int $field_id = null,
45+
private readonly bool $is_multiple = false,
4346
) {}
4447

4548
#[Override]
@@ -48,6 +51,7 @@ public static function jsonDeserialize(array $data): self
4851
return new self(
4952
block_id: $data[self::BLOCK_ID] ?? null,
5053
field_id: $data[self::FIELD_ID] ?? null,
54+
is_multiple: (bool) ($data[self::IS_MULTIPLE] ?? false),
5155
);
5256
}
5357

@@ -57,6 +61,7 @@ public function jsonSerialize(): array
5761
return [
5862
self::BLOCK_ID => $this->block_id,
5963
self::FIELD_ID => $this->field_id,
64+
self::IS_MULTIPLE => $this->is_multiple,
6065
];
6166
}
6267

@@ -69,4 +74,9 @@ public function getFieldId(): ?int
6974
{
7075
return $this->field_id;
7176
}
77+
78+
public function isMultiple(): bool
79+
{
80+
return $this->is_multiple;
81+
}
7282
}

0 commit comments

Comments
 (0)