Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 10 additions & 2 deletions inc/destinationfield.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
2 changes: 1 addition & 1 deletion inc/field.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(...),
));
Expand Down
37 changes: 26 additions & 11 deletions inc/questiontype.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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) {
Expand All @@ -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<int, mixed>
*/
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 {}

Expand Down Expand Up @@ -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),
],
);
}

Expand Down
4 changes: 2 additions & 2 deletions templates/question_type_administration.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -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
}) %}

Expand Down
7 changes: 4 additions & 3 deletions templates/question_type_end_user.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -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]',
Expand Down
27 changes: 27 additions & 0 deletions tests/QuestionTypeTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use Glpi\Tests\DbTestCase;
use Glpi\Tests\FormTesterTrait;
use Glpi\Tests\GLPITestCase;
use Location;
use PluginFieldsContainer;
use PluginFieldsField;
use ReflectionClass;
Expand Down Expand Up @@ -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
Expand Down
78 changes: 73 additions & 5 deletions tests/Units/FieldDestinationFieldTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
Loading