From a878926ce7168ee61bad2c356fe45518814a835d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Cailly?= <42278610+ccailly@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:43:06 +0200 Subject: [PATCH] feat(forms): Adding actors category and related sub question types --- ajax/getFormQuestionActorsDropdownValue.php | 62 ++++ js/common.js | 102 ++++++ js/form_editor_controller.js | 33 +- src/AbstractRightsDropdown.php | 16 +- src/Dropdown.php | 2 + src/Form/Dropdown/FormActorsDropdown.php | 163 ++++++++++ src/Form/Question.php | 32 +- .../AbstractQuestionTypeActors.php | 305 ++++++++++++++++++ .../AbstractQuestionTypeShortAnswer.php | 10 +- .../QuestionType/QuestionTypeAssignee.php | 63 ++++ .../QuestionType/QuestionTypeCategory.php | 6 + .../QuestionType/QuestionTypeDateTime.php | 12 +- .../QuestionType/QuestionTypeInterface.php | 11 +- .../QuestionType/QuestionTypeLongText.php | 10 +- .../QuestionType/QuestionTypeObserver.php | 62 ++++ .../QuestionType/QuestionTypeRequester.php | 62 ++++ src/Html.php | 130 +++----- src/User.php | 2 + tests/functional/Glpi/Form/AnswersSet.php | 22 ++ .../QuestionType/QuestionTypesManager.php | 10 + 20 files changed, 1001 insertions(+), 114 deletions(-) create mode 100644 ajax/getFormQuestionActorsDropdownValue.php create mode 100644 src/Form/Dropdown/FormActorsDropdown.php create mode 100644 src/Form/QuestionType/AbstractQuestionTypeActors.php create mode 100644 src/Form/QuestionType/QuestionTypeAssignee.php create mode 100644 src/Form/QuestionType/QuestionTypeObserver.php create mode 100644 src/Form/QuestionType/QuestionTypeRequester.php diff --git a/ajax/getFormQuestionActorsDropdownValue.php b/ajax/getFormQuestionActorsDropdownValue.php new file mode 100644 index 00000000000..91c5e7a5b5f --- /dev/null +++ b/ajax/getFormQuestionActorsDropdownValue.php @@ -0,0 +1,62 @@ +. + * + * --------------------------------------------------------------------- + */ + +use Glpi\Form\Dropdown\FormActorsDropdown; +use Glpi\Form\Question; +use Glpi\Form\QuestionType\QuestionTypeAssignee; +use Glpi\Form\QuestionType\QuestionTypeObserver; +use Glpi\Form\QuestionType\QuestionTypeRequester; + +include(__DIR__ . '/getAbstractRightDropdownValue.php'); + +Session::checkLoginUser(); + +if (Session::getCurrentInterface() !== 'central') { + $questions = (new Question())->find([ + 'type' => [ + QuestionTypeAssignee::class, + QuestionTypeObserver::class, + QuestionTypeRequester::class + ] + ]); + + // Check if the user can view at least one question + if (array_reduce($questions, fn($acc, $question) => $acc || $question->canViewItem(), false) === false) { + http_response_code(403); + exit(); + } +} + +show_rights_dropdown(FormActorsDropdown::class); diff --git a/js/common.js b/js/common.js index 5af92768d3f..6e2f077ecc7 100644 --- a/js/common.js +++ b/js/common.js @@ -42,6 +42,11 @@ var timeoutglobalvar; // api does not provide any method to get the current configuration var tinymce_editor_configs = {}; +// Store select2 configurations +// This is needed if a select2 need to be destroyed and recreated as select2 +// api does not provide any method to get the current configuration +var select2_configs = {}; + /** * modifier la propriete display d'un element * @@ -1760,3 +1765,100 @@ function getUUID() { if (typeof GlpiCommonAjaxController == "function") { new GlpiCommonAjaxController(); } + +function setupAjaxDropdown(config) { + // Field ID is used as a selector, so we need to escape special characters + // to avoid issues with jQuery. + const field_id = $.escapeSelector(config.field_id); + + const select2_el = $('#' + field_id).select2({ + width: config.width, + multiple: config.multiple, + placeholder: config.placeholder, + allowClear: config.allowclear, + minimumInputLength: 0, + quietMillis: 100, + dropdownAutoWidth: true, + dropdownParent: $('#' + field_id).closest('div.modal, div.dropdown-menu, body'), + minimumResultsForSearch: config.ajax_limit_count, + ajax: { + url: config.url, + dataType: 'json', + type: 'POST', + data: function (params) { + query = params; + var data = $.extend({}, config.params, { + searchText: params.term, + }); + + if (config.parent_id_field !== '') { + data.parent_id = document.getElementById(config.parent_id_field).value; + } + + data.page_limit = config.dropdown_max; // page size + data.page = params.page || 1; // page number + + return data; + }, + processResults: function (data, params) { + params.page = params.page || 1; + var more = (data.count >= config.dropdown_max); + + return { + results: data.results, + pagination: { + more: more + } + }; + } + }, + templateResult: config.templateResult, + templateSelection: config.templateSelection + }) + .bind('setValue', function (e, value) { + $.ajax(config.url, { + data: $.extend({}, config.params, { + _one_id: value, + }), + dataType: 'json', + type: 'POST', + }).done(function (data) { + + var iterate_options = function (options, value) { + var to_return = false; + $.each(options, function (index, option) { + if (Object.prototype.hasOwnProperty.call(option, 'id') && option.id == value) { + to_return = option; + return false; // act as break; + } + + if (Object.prototype.hasOwnProperty.call(option, 'children')) { + to_return = iterate_options(option.children, value); + } + }); + + return to_return; + }; + + var option = iterate_options(data.results, value); + if (option !== false) { + var newOption = new Option(option.text, option.id, true, true); + $('#' + field_id).append(newOption).trigger('change'); + } + }); + }); + + if (config.on_change !== '') { + $('#' + field_id).on('change', function () { eval(config.on_change); }); + } + + $('label[for=' + field_id + ']').on('click', function () { $('#' + field_id).select2('open'); }); + $('#' + field_id).on('select2:open', function (e) { + const search_input = document.querySelector(`.select2-search__field[aria-controls='select2-${e.target.id}-results']`); + if (search_input) { + search_input.focus(); + } + }); + + return select2_el; +} diff --git a/js/form_editor_controller.js b/js/form_editor_controller.js index 6c91c9c89e8..54b273ab7f0 100644 --- a/js/form_editor_controller.js +++ b/js/form_editor_controller.js @@ -31,7 +31,7 @@ * --------------------------------------------------------------------- */ -/* global _, tinymce_editor_configs, getUUID, getRealInputWidth, sortable, tinymce, glpi_toast_error, bootstrap */ +/* global _, tinymce_editor_configs, getUUID, getRealInputWidth, sortable, tinymce, glpi_toast_error, bootstrap, setupAjaxDropdown */ /** * Client code to handle users actions on the form_editor template @@ -430,7 +430,7 @@ class GlpiFormEditorController } // Format input name - const field = $(input).data("glpi-form-editor-original-name"); + let field = $(input).data("glpi-form-editor-original-name"); let base_input_index = ""; if (type === "section") { // The input is for the section itself @@ -450,9 +450,15 @@ class GlpiFormEditorController } // Update input name + let postfix = ""; + if (field.endsWith("[]")) { + field = field.slice(0, -2); + postfix = "[]"; + } + $(input).attr( "name", - base_input_index + `[${field}]` + base_input_index + `[${field}]${postfix}` ); }); } @@ -773,6 +779,9 @@ class GlpiFormEditorController // Keep track of rich text editors that will need to be initialized const tiny_mce_to_init = []; + // Keep track of select2 that will need to be initialized + const select2_to_init = []; + // Look for tiynmce editor to init copy.find("textarea").each(function() { // Get editor config for this field @@ -798,6 +807,21 @@ class GlpiFormEditorController window.tinymce_editor_configs[id] = config; }); + // Look for select2 to init + copy.find("select").each(function() { + const id = $(this).attr("id"); + const config = window.select2_configs[id]; + + // Check if a select2 isn't already initialized + // and if a configuration is available + if ( + $(this).hasClass("select2-hidden-accessible") === false + && config !== undefined + ) { + select2_to_init.push(config); + } + }); + // Insert the new question switch (action) { case "append": @@ -819,6 +843,9 @@ class GlpiFormEditorController // Init the editors tiny_mce_to_init.forEach((config) => tinyMCE.init(config)); + // Init the select2 + select2_to_init.forEach((config) => setupAjaxDropdown(config)); + // Init tooltips const tooltip_trigger_list = copy.find('[data-bs-toggle="tooltip"]'); [...tooltip_trigger_list].map( diff --git a/src/AbstractRightsDropdown.php b/src/AbstractRightsDropdown.php index b2abbf796ad..8cc3505fcf7 100644 --- a/src/AbstractRightsDropdown.php +++ b/src/AbstractRightsDropdown.php @@ -75,7 +75,7 @@ protected static function isTypeEnabled(string $type): bool * * @return string */ - public static function show(string $name, array $values): string + public static function show(string $name, array $values, array $params = []): string { // Flatten values $dropdown_values = []; @@ -92,12 +92,18 @@ public static function show(string $name, array $values): string $url = static::getAjaxUrl(); // Build params - $params = [ + $params = array_merge([ 'name' => $name . "[]", - 'values' => $dropdown_values, - 'valuesnames' => self::getValueNames($dropdown_values), 'multiple' => true, - ]; + ], $params); + + if ($params['multiple']) { + $params['values'] = $dropdown_values; + $params['valuesnames'] = self::getValueNames($dropdown_values); + } elseif (count($dropdown_values) > 0) { + $params['value'] = $dropdown_values[0]; + $params['valuename'] = self::getValueNames($dropdown_values)[0]; + } return Html::jsAjaxDropdown($params['name'], $field_id, $url, $params); } diff --git a/src/Dropdown.php b/src/Dropdown.php index 5d844cb9db0..5487ebe68cc 100644 --- a/src/Dropdown.php +++ b/src/Dropdown.php @@ -132,6 +132,7 @@ public static function show($itemtype, $options = []) $params['readonly'] = false; $params['parent_id_field'] = null; $params['multiple'] = false; + $params['init'] = true; if (is_array($options) && count($options)) { foreach ($options as $key => $val) { @@ -258,6 +259,7 @@ public static function show($itemtype, $options = []) 'order' => $params['order'] ?? null, 'parent_id_field' => $params['parent_id_field'], 'multiple' => $params['multiple'] ?? false, + 'init' => $params['init'] ?? true, ]; if ($params['multiple']) { diff --git a/src/Form/Dropdown/FormActorsDropdown.php b/src/Form/Dropdown/FormActorsDropdown.php new file mode 100644 index 00000000000..1c3ec01b895 --- /dev/null +++ b/src/Form/Dropdown/FormActorsDropdown.php @@ -0,0 +1,163 @@ +. + * + * --------------------------------------------------------------------- + */ + +namespace Glpi\Form\Dropdown; + +use AbstractRightsDropdown; +use Group; +use Override; +use Supplier; +use User; + +final class FormActorsDropdown extends AbstractRightsDropdown +{ + #[Override] + protected static function getAjaxUrl(): string + { + /** @var array $CFG_GLPI */ + global $CFG_GLPI; + + return $CFG_GLPI['root_doc'] . "/ajax/getFormQuestionActorsDropdownValue.php"; + } + + #[Override] + protected static function getTypes(): array + { + $allowed_types = [ + User::getType(), + Group::getType(), + Supplier::getType(), + ]; + + if (isset($_POST['allowed_types'])) { + $allowed_types = array_intersect($allowed_types, $_POST['allowed_types']); + } + + return $allowed_types; + } + + #[Override] + public static function show(string $name, array $values, array $params = []): string + { + $itemtype_name = fn($itemtype) => $itemtype::getTypeName(1); + $params['width'] = '100%'; + $params['templateSelection'] = <<'; + } else if (data.itemtype === 'Group') { + icon = ''; + } else if (data.itemtype === 'Supplier') { + icon = ''; + } + + return $('' + icon + text + ''); + } + JS; + $params['templateResult'] = $params['templateSelection']; + + return parent::show($name, $values, $params); + } + + #[Override] + protected static function getUsers(string $text): array + { + $users = User::getSqlSearchResult(false, "all", -1, 0, [], $text, 0, self::LIMIT); + $users_items = []; + foreach ($users as $user) { + $new_key = 'users_id-' . $user['id']; + $text = formatUserName($user["id"], $user["name"], $user["realname"], $user["firstname"]); + $users_items[$new_key] = [ + 'text' => $text, + 'title' => sprintf(__('%1$s - %2$s'), $text, $user['name']), + ]; + } + + return $users_items; + } + + #[Override] + public static function fetchValues(string $text = ""): array + { + $possible_rights = []; + + // Add users if enabled + if (self::isTypeEnabled(User::getType())) { + $possible_rights[User::getType()] = self::getUsers($text); + } + + // Add groups if enabled + if (self::isTypeEnabled(Group::getType())) { + $possible_rights[Group::getType()] = self::getGroups($text); + } + + // Add suppliers if enabled + if (self::isTypeEnabled(Supplier::getType())) { + $possible_rights[Supplier::getType()] = self::getSuppliers($text); + } + + $results = []; + foreach ($possible_rights as $itemtype => $ids) { + $new_group = []; + foreach ($ids as $id => $labels) { + $text = $labels['text'] ?? $labels; + $title = $labels['title'] ?? $text; + $new_group[] = [ + 'id' => $id, + 'itemtype' => $itemtype, + 'text' => $text, + 'title' => $title, + 'selection_text' => "$itemtype - $text", + ]; + } + $results[] = [ + 'itemtype' => $itemtype, + 'text' => $itemtype::getTypeName(1), + 'title' => $itemtype::getTypeName(1), + 'children' => $new_group, + ]; + } + + $ret = [ + 'results' => $results, + 'count' => count($results) + ]; + + return $ret; + } +} diff --git a/src/Form/Question.php b/src/Form/Question.php index abc0ca5085a..0be769ee6c1 100644 --- a/src/Form/Question.php +++ b/src/Form/Question.php @@ -136,15 +136,41 @@ public function prepareInputForUpdate($input) private function prepareInput(&$input) { $question_type = $this->getQuestionType(); + + // The question type can be null when the question is created + // We need to instantiate the question type to format and validate attributes + if ( + $question_type === null + && isset($input['type']) + && class_exists($input['type']) + ) { + $question_type = new $input['type'](); + } + if ($question_type) { - $is_extra_data_valid = $question_type->validateExtraDataInput($input['extra_data'] ?? null); + if (isset($input['default_value'])) { + $input['default_value'] = $question_type::formatDefaultValueForDB($input['default_value']); + } + + $extra_data = $input['extra_data'] ?? []; + if (is_string($extra_data)) { + if (empty($extra_data)) { + $extra_data = []; + } else { + // Decode extra data as JSON + $extra_data = json_decode($extra_data, true); + } + } + + $is_extra_data_valid = $question_type::validateExtraDataInput($extra_data); + if (!$is_extra_data_valid) { throw new \InvalidArgumentException("Invalid extra data for question"); } // Save extra data as JSON - if (isset($input['extra_data'])) { - $input['extra_data'] = json_encode($input['extra_data']); + if (!empty($extra_data)) { + $input['extra_data'] = json_encode($extra_data); } } } diff --git a/src/Form/QuestionType/AbstractQuestionTypeActors.php b/src/Form/QuestionType/AbstractQuestionTypeActors.php new file mode 100644 index 00000000000..f299d7ff337 --- /dev/null +++ b/src/Form/QuestionType/AbstractQuestionTypeActors.php @@ -0,0 +1,305 @@ +. + * + * --------------------------------------------------------------------- + */ + +namespace Glpi\Form\QuestionType; + +use Glpi\Application\View\TemplateRenderer; +use Glpi\Form\Question; +use Override; + +/** + * "Actors" questions represent an input field for actors (requesters, ...) + */ +abstract class AbstractQuestionTypeActors implements QuestionTypeInterface +{ + #[Override] + public function __construct() + { + } + + /** + * Retrieve the allowed actor types + * + * @return array + */ + abstract public function getAllowedActorTypes(): array; + + #[Override] + public static function formatDefaultValueForDB(mixed $value): ?string + { + if (is_array($value)) { + return implode(',', $value); + } + + return $value; + } + + #[Override] + public static function validateExtraDataInput(array $input): bool + { + $allowed_keys = [ + 'is_multiple_actors' + ]; + + return empty(array_diff(array_keys($input), $allowed_keys)) + && array_reduce($input, fn($carry, $value) => $carry && preg_match('/^[01]$/', $value), true); + } + + /** + * Check if the question allows multiple actors + * + * @param ?Question $question + * @return bool + */ + public function isMultipleActors(?Question $question): bool + { + if ($question === null) { + return false; + } + + return $question->getExtraDatas()['is_multiple_actors'] ?? false; + } + + /** + * Retrieve the default value + * + * @param ?Question $question + * @param bool $multiple + * @return int + */ + public function getDefaultValue(?Question $question, bool $multiple = false): array + { + // If the question is not set or the default value is empty, we return 0 (default option for dropdowns) + if ( + $question === null + || empty($question->fields['default_value']) + ) { + return []; + } + + $default_values = []; + $raw_default_values = explode(',', $question->fields['default_value']); + foreach ($raw_default_values as $raw_default_value) { + $entry = explode('-', $raw_default_value); + $default_values[$entry[0]][] = $entry[1]; + } + + if ($multiple) { + return $default_values; + } + + return [key($default_values) => current($default_values)]; + } + + #[Override] + public function renderAdministrationTemplate(?Question $question): string + { + $template = <<renderFromStringTemplate($template, [ + 'init' => $question != null, + 'values' => $this->getDefaultValue($question, $this->isMultipleActors($question)), + 'allowed_types' => $this->getAllowedActorTypes(), + 'is_multiple_actors' => $this->isMultipleActors($question) + ]); + } + + + #[Override] + public function renderAdministrationOptionsTemplate(?Question $question): string + { + $template = << + + + + +TWIG; + + $twig = TemplateRenderer::getInstance(); + return $twig->renderFromStringTemplate($template, [ + 'is_multiple_actors' => $this->isMultipleActors($question), + 'is_multiple_actors_label' => __('Allow multiple actors') + ]); + } + + #[Override] + public function renderAnswerTemplate($answer): string + { + $template = << + {% for itemtype, actors_id in actors %} + {% for actor_id in actors_id %} + {{ get_item_link(itemtype, actor_id) }} + {% endfor %} + {% endfor %} + +TWIG; + + $actors = []; + foreach ($answer as $actor) { + foreach ($this->getAllowedActorTypes() as $type) { + if (strpos($actor, $type::getForeignKeyField()) === 0) { + $actors[$type][] = (int)substr($actor, strlen($type::getForeignKeyField()) + 1); + break; + } + } + } + + $twig = TemplateRenderer::getInstance(); + return $twig->renderFromStringTemplate($template, [ + 'actors' => $actors + ]); + } + + #[Override] + public function renderEndUserTemplate(Question $question): string + { + $template = <<isMultipleActors($question); + $twig = TemplateRenderer::getInstance(); + return $twig->renderFromStringTemplate($template, [ + 'value' => $this->getDefaultValue($question, $is_multiple_actors), + 'question' => $question, + 'allowed_types' => $this->getAllowedActorTypes(), + 'is_multiple_actors' => $is_multiple_actors + ]); + + return ''; + } + + #[Override] + public function getCategory(): QuestionTypeCategory + { + return QuestionTypeCategory::ACTORS; + } +} diff --git a/src/Form/QuestionType/AbstractQuestionTypeShortAnswer.php b/src/Form/QuestionType/AbstractQuestionTypeShortAnswer.php index f140eee0747..4ed3864b667 100644 --- a/src/Form/QuestionType/AbstractQuestionTypeShortAnswer.php +++ b/src/Form/QuestionType/AbstractQuestionTypeShortAnswer.php @@ -57,9 +57,15 @@ public function __construct() abstract public function getInputType(): string; #[Override] - public function validateExtraDataInput(?array $input): bool + public static function formatDefaultValueForDB(mixed $value): ?string { - return $input === null; // No extra data for this question type + return $value; + } + + #[Override] + public static function validateExtraDataInput(array $input): bool + { + return empty($input); // No extra data for this question type } #[Override] diff --git a/src/Form/QuestionType/QuestionTypeAssignee.php b/src/Form/QuestionType/QuestionTypeAssignee.php new file mode 100644 index 00000000000..e45a79aca58 --- /dev/null +++ b/src/Form/QuestionType/QuestionTypeAssignee.php @@ -0,0 +1,63 @@ +. + * + * --------------------------------------------------------------------- + */ + +namespace Glpi\Form\QuestionType; + +use Group; +use Override; +use Session; +use Supplier; +use User; + +final class QuestionTypeAssignee extends AbstractQuestionTypeActors +{ + #[Override] + public function getName(): string + { + return _n('Assignee', 'Assignees', Session::getPluralNumber()); + } + + #[Override] + public function getWeight(): int + { + return 30; + } + + #[Override] + public function getAllowedActorTypes(): array + { + return [User::class, Group::class, Supplier::class]; + } +} diff --git a/src/Form/QuestionType/QuestionTypeCategory.php b/src/Form/QuestionType/QuestionTypeCategory.php index e8d0e56261d..1c19a7a7961 100644 --- a/src/Form/QuestionType/QuestionTypeCategory.php +++ b/src/Form/QuestionType/QuestionTypeCategory.php @@ -55,6 +55,11 @@ enum QuestionTypeCategory: string */ case DATE_AND_TIME = "date_and_time"; + /** + * Question that expect actors (users, groups, suppliers or anonymous users) + */ + case ACTORS = "actors"; + /** * Get category label * @return string @@ -65,6 +70,7 @@ public function getLabel(): string self::SHORT_ANSWER => __("Short answer"), self::LONG_ANSWER => __("Long answer"), self::DATE_AND_TIME => __("Date and time"), + self::ACTORS => __("Actors"), }; } } diff --git a/src/Form/QuestionType/QuestionTypeDateTime.php b/src/Form/QuestionType/QuestionTypeDateTime.php index 4076853ea7f..d6715210670 100644 --- a/src/Form/QuestionType/QuestionTypeDateTime.php +++ b/src/Form/QuestionType/QuestionTypeDateTime.php @@ -50,6 +50,12 @@ public function __construct() { } + #[Override] + public static function formatDefaultValueForDB(mixed $value): ?string + { + return $value; + } + #[Override] public function getCategory(): QuestionTypeCategory { @@ -169,7 +175,7 @@ public function isTimeEnabled(?Question $question): bool } #[Override] - public function validateExtraDataInput(?array $input): bool + public static function validateExtraDataInput(array $input): bool { $allowed_keys = [ 'is_default_value_current_time', @@ -177,10 +183,6 @@ public function validateExtraDataInput(?array $input): bool 'is_time_enabled' ]; - if ($input === null) { - return false; - } - return empty(array_diff(array_keys($input), $allowed_keys)) && array_reduce($input, fn($carry, $value) => $carry && preg_match('/^[01]$/', $value), true); } diff --git a/src/Form/QuestionType/QuestionTypeInterface.php b/src/Form/QuestionType/QuestionTypeInterface.php index 1f893897398..309e137b6a1 100644 --- a/src/Form/QuestionType/QuestionTypeInterface.php +++ b/src/Form/QuestionType/QuestionTypeInterface.php @@ -44,6 +44,15 @@ interface QuestionTypeInterface { public function __construct(); + /** + * Format the default value for the database. + * This method is called before saving the question. + * + * @param mixed $value The default value to format. + * @return string + */ + public static function formatDefaultValueForDB(mixed $value): ?string; + /** * Validate the input for extra data of the question. * This method is called before saving the question. @@ -52,7 +61,7 @@ public function __construct(); * * @return bool */ - public function validateExtraDataInput(?array $input): bool; + public static function validateExtraDataInput(array $input): bool; /** * Render the administration template for the given question. diff --git a/src/Form/QuestionType/QuestionTypeLongText.php b/src/Form/QuestionType/QuestionTypeLongText.php index c8ccead5f26..886d7a25524 100644 --- a/src/Form/QuestionType/QuestionTypeLongText.php +++ b/src/Form/QuestionType/QuestionTypeLongText.php @@ -50,9 +50,15 @@ public function __construct() } #[Override] - public function validateExtraDataInput(?array $input): bool + public static function formatDefaultValueForDB(mixed $value): ?string { - return $input === null; // No extra data for this question type + return $value; + } + + #[Override] + public static function validateExtraDataInput(array $input): bool + { + return empty($input); // No extra data for this question type } #[Override] diff --git a/src/Form/QuestionType/QuestionTypeObserver.php b/src/Form/QuestionType/QuestionTypeObserver.php new file mode 100644 index 00000000000..f4bbe2ab302 --- /dev/null +++ b/src/Form/QuestionType/QuestionTypeObserver.php @@ -0,0 +1,62 @@ +. + * + * --------------------------------------------------------------------- + */ + +namespace Glpi\Form\QuestionType; + +use Group; +use Override; +use Session; +use User; + +final class QuestionTypeObserver extends AbstractQuestionTypeActors +{ + #[Override] + public function getName(): string + { + return _n('Observer', 'Observers', Session::getPluralNumber()); + } + + #[Override] + public function getWeight(): int + { + return 20; + } + + #[Override] + public function getAllowedActorTypes(): array + { + return [User::class, Group::class]; + } +} diff --git a/src/Form/QuestionType/QuestionTypeRequester.php b/src/Form/QuestionType/QuestionTypeRequester.php new file mode 100644 index 00000000000..89face52af5 --- /dev/null +++ b/src/Form/QuestionType/QuestionTypeRequester.php @@ -0,0 +1,62 @@ +. + * + * --------------------------------------------------------------------- + */ + +namespace Glpi\Form\QuestionType; + +use Group; +use Override; +use Session; +use User; + +final class QuestionTypeRequester extends AbstractQuestionTypeActors +{ + #[Override] + public function getName(): string + { + return _n('Requester', 'Requesters', Session::getPluralNumber()); + } + + #[Override] + public function getWeight(): int + { + return 10; + } + + #[Override] + public function getAllowedActorTypes(): array + { + return [User::class, Group::class]; + } +} diff --git a/src/Html.php b/src/Html.php index ea68a9a3da3..1bba941fd01 100644 --- a/src/Html.php +++ b/src/Html.php @@ -4917,6 +4917,7 @@ public static function jsAjaxDropdown($name, $field_id, $url, $params = []) global $CFG_GLPI; $default_options = [ + 'init' => true, 'value' => 0, 'valuename' => Dropdown::EMPTY_VALUE, 'multiple' => false, @@ -4928,6 +4929,8 @@ public static function jsAjaxDropdown($name, $field_id, $url, $params = []) 'display_emptychoice' => false, 'specific_tags' => [], 'parent_id_field' => null, + 'templateResult' => 'templateResult', + 'templateSelection' => 'templateSelection', ]; $params = array_merge($default_options, $params); @@ -4937,6 +4940,8 @@ public static function jsAjaxDropdown($name, $field_id, $url, $params = []) $on_change = $params["on_change"]; $placeholder = $params['placeholder'] ?? ''; $multiple = $params['multiple']; + $templateResult = $params['templateResult']; + $templateSelection = $params['templateSelection']; unset($params["on_change"]); unset($params["width"]); @@ -4950,7 +4955,7 @@ public static function jsAjaxDropdown($name, $field_id, $url, $params = []) 'selected' => $value ]; - // manage multiple select (with multiple values) + // manage multiple select (with multiple values) if ($params['multiple']) { $values = array_combine($params['values'], $params['valuesnames']); $options['multiple'] = 'multiple'; @@ -4958,7 +4963,7 @@ public static function jsAjaxDropdown($name, $field_id, $url, $params = []) } else { $values = []; - // simple select (multiple = no) + // simple select (multiple = no) if ($value !== null) { $values = ["$value" => $valuename]; } @@ -4979,110 +4984,49 @@ public static function jsAjaxDropdown($name, $field_id, $url, $params = []) $output = ''; - $js = " - var params_$field_id = {"; + $js_params = ""; foreach ($params as $key => $val) { - // Specific boolean case + // Specific boolean case if (is_bool($val)) { - $js .= "$key: " . ($val ? 1 : 0) . ",\n"; + $js_params .= "$key: " . ($val ? 1 : 0) . ",\n"; } else { - $js .= "$key: " . json_encode($val) . ",\n"; + $js_params .= "$key: " . json_encode($val) . ",\n"; } } - $js .= "}; - const select2_el = $('#$field_id').select2({ - width: '$width', - multiple: '$multiple', - placeholder: " . json_encode($placeholder) . ", - allowClear: $allowclear, - minimumInputLength: 0, - quietMillis: 100, - dropdownAutoWidth: true, - dropdownParent: $('#$field_id').closest('div.modal, div.dropdown-menu, body'), - minimumResultsForSearch: " . $CFG_GLPI['ajax_limit_count'] . ", - ajax: { - url: '$url', - dataType: 'json', - type: 'POST', - data: function (params) { - query = params; - return $.extend({}, params_$field_id, { - searchText: params.term,"; - - if ($parent_id_field !== null) { - $js .= " - parent_id : document.getElementById('" . $parent_id_field . "').value,"; + // Some variables need to be json encoded + $on_change = json_encode($on_change); + if (!empty($placeholder)) { + $placeholder = json_encode($placeholder); } - $js .= " - page_limit: " . $CFG_GLPI['dropdown_max'] . ", // page size - page: params.page || 1, // page number - }); - }, - processResults: function (data, params) { - params.page = params.page || 1; - var more = (data.count >= " . $CFG_GLPI['dropdown_max'] . "); - - return { - results: data.results, - pagination: { - more: more - } - }; - } - }, - templateResult: templateResult, - templateSelection: templateSelection - }) - .bind('setValue', function(e, value) { - $.ajax('$url', { - data: $.extend({}, params_$field_id, { - _one_id: value, - }), - dataType: 'json', - type: 'POST', - }).done(function(data) { - - var iterate_options = function(options, value) { - var to_return = false; - $.each(options, function(index, option) { - if (option.hasOwnProperty('id') - && option.id == value) { - to_return = option; - return false; // act as break; - } - - if (option.hasOwnProperty('children')) { - to_return = iterate_options(option.children, value); - } - }); - return to_return; - }; + $js = << false, 'readonly' => false, 'multiple' => false, + 'init' => true ]; if (is_array($options) && count($options)) { @@ -4831,6 +4832,7 @@ public static function dropdown($options = []) $field_id = Html::cleanId("dropdown_" . $p['name'] . $p['rand']); $param = [ + 'init' => $p['init'], 'multiple' => $p['multiple'], 'width' => $p['width'], 'all' => $p['all'], diff --git a/tests/functional/Glpi/Form/AnswersSet.php b/tests/functional/Glpi/Form/AnswersSet.php index 236bdb0d0b4..36b383ad13f 100644 --- a/tests/functional/Glpi/Form/AnswersSet.php +++ b/tests/functional/Glpi/Form/AnswersSet.php @@ -39,6 +39,9 @@ use Computer; use DbTestCase; use Glpi\Form\AnswersHandler\AnswersHandler; +use Glpi\Form\QuestionType\QuestionTypeAssignee; +use Glpi\Form\QuestionType\QuestionTypeObserver; +use Glpi\Form\QuestionType\QuestionTypeRequester; use Glpi\Form\Destination\FormDestinationTicket; use Glpi\Form\QuestionType\QuestionTypeDateTime; use Glpi\Form\QuestionType\QuestionTypeEmail; @@ -49,8 +52,11 @@ use Glpi\Form\QuestionType\QuestionTypeTime; use Glpi\Tests\FormBuilder; use Glpi\Tests\FormTesterTrait; +use Group; use Impact; +use Supplier; use Ticket; +use User; class AnswersSet extends DbTestCase { @@ -246,6 +252,9 @@ public function testShowForm(): void 'is_date_enabled' => 1, 'is_time_enabled' => 1 ])) + ->addQuestion("Requester", QuestionTypeRequester::class) + ->addQuestion("Observer", QuestionTypeObserver::class) + ->addQuestion("Assignee", QuestionTypeAssignee::class) ); $answers_set = $answers_handler->saveAnswers($form, [ $this->getQuestionId($form, "Name") => "Pierre Paul Jacques", @@ -255,6 +264,19 @@ public function testShowForm(): void $this->getQuestionId($form, "Date") => "2021-01-01", $this->getQuestionId($form, "Time") => "12:00", $this->getQuestionId($form, "DateTime") => "2021-01-01 12:00:00", + $this->getQuestionId($form, "Requester") => [ + User::getForeignKeyField() . '-1', + Group::getForeignKeyField() . '-1' + ], + $this->getQuestionId($form, "Observer") => [ + User::getForeignKeyField() . '-1', + Group::getForeignKeyField() . '-1' + ], + $this->getQuestionId($form, "Assignee") => [ + User::getForeignKeyField() . '-1', + Group::getForeignKeyField() . '-1', + Supplier::getForeignKeyField() . '-1' + ], ], \Session::getLoginUserID()); // Ensure we used every possible questions types diff --git a/tests/functional/Glpi/Form/QuestionType/QuestionTypesManager.php b/tests/functional/Glpi/Form/QuestionType/QuestionTypesManager.php index 1e0fb2c6843..19b67da3079 100644 --- a/tests/functional/Glpi/Form/QuestionType/QuestionTypesManager.php +++ b/tests/functional/Glpi/Form/QuestionType/QuestionTypesManager.php @@ -80,6 +80,7 @@ public function testGetCategories(): void QuestionTypeCategory::SHORT_ANSWER, QuestionTypeCategory::LONG_ANSWER, QuestionTypeCategory::DATE_AND_TIME, + QuestionTypeCategory::ACTORS, ]; // Manual array comparison, `isEqualTo` doesn't seem to work properly @@ -119,6 +120,15 @@ protected function testGetTypesForCategoryProvider(): iterable new \Glpi\Form\QuestionType\QuestionTypeDateTime(), ] ]; + + yield [ + QuestionTypeCategory::ACTORS, + [ + new \Glpi\Form\QuestionType\QuestionTypeRequester(), + new \Glpi\Form\QuestionType\QuestionTypeObserver(), + new \Glpi\Form\QuestionType\QuestionTypeAssignee(), + ] + ]; } /**