From 8592bd8e14c42429fa6ff9bb2ca795657bf6ade8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Cailly?= Date: Tue, 7 Jan 2025 12:08:35 +0100 Subject: [PATCH 1/2] feat(forms): Move the itemtype config field for QuestionTypeItem --- ajax/dropdownAllItems.php | 3 + js/modules/Forms/EditorController.js | 78 +++++++++++++++++ js/modules/Forms/QuestionItem.js | 24 ++++++ .../QuestionType/AbstractQuestionType.php | 24 ++++++ .../QuestionType/QuestionTypeInterface.php | 29 +++++++ .../Form/QuestionType/QuestionTypeItem.php | 83 +++++++++++-------- .../pages/admin/form/form_editor.html.twig | 12 +++ .../pages/admin/form/form_question.html.twig | 23 +++++ .../itilcategory.cy.js | 7 ++ .../e2e/form/question_types/item.cy.js | 6 ++ 10 files changed, 255 insertions(+), 34 deletions(-) diff --git a/ajax/dropdownAllItems.php b/ajax/dropdownAllItems.php index 5efe93425b4..3318a0c8532 100644 --- a/ajax/dropdownAllItems.php +++ b/ajax/dropdownAllItems.php @@ -98,6 +98,9 @@ if (isset($_POST['specific_tags_items_id_dropdown'])) { $p['specific_tags'] = $_POST['specific_tags_items_id_dropdown']; } + if (isset($_POST['aria_label'])) { + $p['aria_label'] = $_POST['aria_label']; + } $p['_idor_token'] = Session::getNewIDORToken($_POST["idtable"], $idor_params); echo Html::jsAjaxDropdown( diff --git a/js/modules/Forms/EditorController.js b/js/modules/Forms/EditorController.js index ffe038e61cd..233f2b845cb 100644 --- a/js/modules/Forms/EditorController.js +++ b/js/modules/Forms/EditorController.js @@ -68,6 +68,12 @@ export class GlpiFormEditorController */ #options; + /** + * Subtypes options for each question type + * @type {Object} + */ + #question_subtypes_options; + /** * Create a new GlpiFormEditorController instance for the given target. * The target must be a valid form. @@ -83,6 +89,7 @@ export class GlpiFormEditorController this.#defaultQuestionType = defaultQuestionType; this.#templates = templates; this.#options = {}; + this.#question_subtypes_options = {}; // Validate target if ($(this.#target).prop("tagName") != "FORM") { @@ -228,6 +235,16 @@ export class GlpiFormEditorController this.#options[type] = options; } + /** + * Register new subtypes options for the given question type. + * + * @param {string} type Question type + * @param {Object} options Subtypes options for the question type + */ + registerQuestionSubTypesOptions(type, options) { + this.#question_subtypes_options[type] = options; + } + /** * Handle backend response */ @@ -316,6 +333,13 @@ export class GlpiFormEditorController ); break; + case "change-question-sub-type": + this.#changeQuestionSubType( + target.closest("[data-glpi-form-editor-question]"), + target.val() + ); + break; + // Add a new section at the end of the form case "add-section": this.#addSection( @@ -1208,9 +1232,63 @@ export class GlpiFormEditorController extracted_default_value ); + // Update sub question types + if (this.#question_subtypes_options[type] !== undefined) { + const sub_types_select = question.find("[data-glpi-form-editor-question-sub-type-selector]"); + + // Show sub question type selector + sub_types_select.closest("div").removeClass("d-none"); + sub_types_select.attr('disabled', false); + + // Remove current sub types options + sub_types_select.find('optgroup, option').remove(); + + // Find sub types available for the new type + const new_sub_types = this.#question_subtypes_options[type].subtypes; + + // Copy the new sub types options into the dropdown + for (const category in new_sub_types) { + const optgroup = $(``); + for (const [sub_type, label] of Object.entries(new_sub_types[category])) { + const option = $(``); + optgroup.append(option); + } + sub_types_select.append(optgroup); + } + + // Set the default sub type + if (this.#question_subtypes_options[type].default_value) { + sub_types_select.val(this.#question_subtypes_options[type].default_value); + } + + // Update the field name and aria-label + sub_types_select.attr("name", this.#question_subtypes_options[type].field_name); + sub_types_select.attr("aria-label", this.#question_subtypes_options[type].field_aria_label); + + // Remove the "original-name" data attribute to avoid conflicts + sub_types_select.removeAttr("data-glpi-form-editor-original-name"); + + // Trigger sub type change + sub_types_select.trigger("change"); + } else { + // Hide sub question type selector + question.find("[data-glpi-form-editor-question-sub-type-selector]") + .attr('disabled', true) + .closest("div").addClass("d-none"); + } + $(document).trigger('glpi-form-editor-question-type-changed', [question, type]); } + /** + * Handle the change of the sub type of the given question. + * @param {jQuery} question Question to update + * @param {string} sub_type New sub type + */ + #changeQuestionSubType(question, sub_type) { + $(document).trigger('glpi-form-editor-question-sub-type-changed', [question, sub_type]); + } + /** * Add a new section at the end of the form. * @param {jQuery} target Current position in the form diff --git a/js/modules/Forms/QuestionItem.js b/js/modules/Forms/QuestionItem.js index 81f5a866fd5..7103309ec94 100644 --- a/js/modules/Forms/QuestionItem.js +++ b/js/modules/Forms/QuestionItem.js @@ -55,6 +55,30 @@ export class GlpiFormQuestionTypeItem { this.#updateItemsIdDropdownID(question_details); } }); + + $(document).on('glpi-form-editor-question-sub-type-changed', (event, question, sub_type) => { + if (question.find('[name="type"], [data-glpi-form-editor-original-name="type"]').val() !== this.#question_type) { + return; + } + + const select = question.find('[data-glpi-form-editor-question-type-specific] select'); + const container = select.parent(); + + // Add a flag to all children to mark them as to be removed + container.children().attr('data-to-remove', 'true'); + + // Load the new dropdown + container.load( + `${CFG_GLPI.root_doc}/ajax/dropdownAllItems.php`, + { + 'idtable' : sub_type, + 'width' : '100%', + 'name' : select.data('glpi-form-editor-original-name') || select.attr('name'), + 'aria_label': select.attr('aria-label'), + }, + () => container.find('[data-to-remove]').remove() + ); + }); } #updateItemsIdDropdownID(question_details) { diff --git a/src/Glpi/Form/QuestionType/AbstractQuestionType.php b/src/Glpi/Form/QuestionType/AbstractQuestionType.php index b06a7ddcd47..8e9a832bf13 100644 --- a/src/Glpi/Form/QuestionType/AbstractQuestionType.php +++ b/src/Glpi/Form/QuestionType/AbstractQuestionType.php @@ -169,4 +169,28 @@ public function getDefaultValueConfig(array $serialized_data): ?JsonFieldInterfa return $config_class::jsonDeserialize($serialized_data); } + + #[Override] + public function getSubTypes(): array + { + return []; + } + + #[Override] + public function getSubTypeFieldName(): string + { + return 'sub_type'; + } + + #[Override] + public function getSubTypeFieldAriaLabel(): string + { + return __('Question sub type'); + } + + #[Override] + public function getSubTypeDefaultValue(?Question $question): ?string + { + return ''; + } } diff --git a/src/Glpi/Form/QuestionType/QuestionTypeInterface.php b/src/Glpi/Form/QuestionType/QuestionTypeInterface.php index 70d3a05d871..c588d27d859 100644 --- a/src/Glpi/Form/QuestionType/QuestionTypeInterface.php +++ b/src/Glpi/Form/QuestionType/QuestionTypeInterface.php @@ -212,4 +212,33 @@ public function getDefaultValueConfigClass(): ?string; * @return ?JsonFieldInterface */ public function getDefaultValueConfig(array $serialized_data): ?JsonFieldInterface; + + /** + * Retrieve the allowed sub-types for the question type + * + * @return array + */ + public function getSubTypes(): array; + + /** + * Retrieve the sub-type field name for the question type + * + * @return string + */ + public function getSubTypeFieldName(): string; + + /** + * Retrieve the sub-type field label for the question type + * + * @return string + */ + public function getSubTypeFieldAriaLabel(): string; + + /** + * Retrieve the default value for the sub-type field + * + * @param Question|null $question The question to get the default value for + * @return null|string + */ + public function getSubTypeDefaultValue(?Question $question): ?string; } diff --git a/src/Glpi/Form/QuestionType/QuestionTypeItem.php b/src/Glpi/Form/QuestionType/QuestionTypeItem.php index 41433f3633b..0525234d6b9 100644 --- a/src/Glpi/Form/QuestionType/QuestionTypeItem.php +++ b/src/Glpi/Form/QuestionType/QuestionTypeItem.php @@ -37,9 +37,9 @@ use CartridgeItem; use ConsumableItem; +use Dropdown; use Glpi\Application\View\TemplateRenderer; use Glpi\Form\Question; -use Group; use Line; use Override; use PassiveDCEquipment; @@ -167,6 +167,30 @@ public function validateExtraDataInput(array $input): bool return true; } + #[Override] + public function getSubTypes(): array + { + return Dropdown::buildItemtypesDropdownOptions($this->getAllowedItemtypes()); + } + + #[Override] + public function getSubTypeFieldName(): string + { + return 'itemtype'; + } + + #[Override] + public function getSubTypeFieldAriaLabel(): string + { + return $this->itemtype_aria_label; + } + + #[Override] + public function getSubTypeDefaultValue(?Question $question): ?string + { + return $this->getDefaultValueItemtype($question); + } + #[Override] public function renderAdministrationTemplate(?Question $question): string { @@ -175,29 +199,21 @@ public function renderAdministrationTemplate(?Question $question): string {% set rand = random() %} - {{ fields.dropdownItemsFromItemtypes( + {{ fields.dropdownField( + default_itemtype|default(itemtypes|first|first), 'default_value', + default_items_id, '', { - 'init' : init, - 'itemtypes' : itemtypes, - 'no_label' : true, - 'display_emptychoice' : true, - 'default_itemtype' : default_itemtype, - 'default_items_id' : default_items_id, - 'itemtype_name' : 'itemtype', - 'items_id_name' : 'default_value', - 'width' : '100%', - 'container_css_class' : 'mt-2', - 'no_sort' : true, - 'aria_label' : itemtype_aria_label, - 'specific_tags_items_id_dropdown': { - 'aria-label': items_id_aria_label, - }, - 'add_data_attributes_itemtype_dropdown' : { - 'glpi-form-editor-specific-question-extra-data': '', - }, - 'mb' : '', + 'init' : init, + 'no_label' : true, + 'display_emptychoice': true, + 'width' : '100%', + 'container_css_class': 'mt-2', + 'mb' : '', + 'comments' : false, + 'addicon' : false, + 'aria_label' : aria_label, } ) }} @@ -212,14 +228,13 @@ public function renderAdministrationTemplate(?Question $question): string $twig = TemplateRenderer::getInstance(); return $twig->renderFromStringTemplate($template, [ - 'init' => $question != null, - 'question' => $question, - 'question_type' => $this::class, - 'default_itemtype' => $this->getDefaultValueItemtype($question) ?? '0', - 'default_items_id' => $this->getDefaultValueItemId($question), - 'itemtypes' => $this->getAllowedItemtypes(), - 'itemtype_aria_label' => $this->itemtype_aria_label, - 'items_id_aria_label' => $this->items_id_aria_label, + 'init' => $question != null, + 'question' => $question, + 'question_type' => $this::class, + 'default_itemtype' => $this->getDefaultValueItemtype($question), + 'default_items_id' => $this->getDefaultValueItemId($question), + 'itemtypes' => $this->getAllowedItemtypes(), + 'aria_label' => $this->items_id_aria_label, ]); } @@ -257,11 +272,11 @@ public function renderEndUserTemplate(Question $question): string $twig = TemplateRenderer::getInstance(); return $twig->renderFromStringTemplate($template, [ - 'question' => $question, - 'itemtype' => $this->getDefaultValueItemtype($question) ?? '0', - 'default_items_id' => $this->getDefaultValueItemId($question), - 'aria_label' => $this->items_id_aria_label, - 'items_id_aria_label' => $this->items_id_aria_label, + 'question' => $question, + 'itemtype' => $this->getDefaultValueItemtype($question) ?? '0', + 'default_items_id' => $this->getDefaultValueItemId($question), + 'aria_label' => $this->items_id_aria_label, + 'sub_types' => $this->getSubTypes(), ]); } diff --git a/templates/pages/admin/form/form_editor.html.twig b/templates/pages/admin/form/form_editor.html.twig index 1b65026232d..5f8e6c3c0be 100644 --- a/templates/pages/admin/form/form_editor.html.twig +++ b/templates/pages/admin/form/form_editor.html.twig @@ -389,6 +389,18 @@ '{{ get_class(question_type)|e('js') }}', {{ question_type.getFormEditorJsOptions()|raw }} ); + + {% if question_type.getSubTypes() is not empty %} + controller.registerQuestionSubTypesOptions( + '{{ get_class(question_type)|e('js') }}', + { + 'subtypes' : {{ question_type.getSubTypes()|json_encode|raw }}, + 'default_value' : '{{ question_type.getSubTypeDefaultValue(null) }}', + 'field_name' : '{{ question_type.getSubTypeFieldName() }}', + 'field_aria_label': '{{ question_type.getSubTypeFieldAriaLabel() }}', + } + ) + {% endif %} {% endfor %} $(container_selector).data('controller', controller); diff --git a/templates/pages/admin/form/form_question.html.twig b/templates/pages/admin/form/form_question.html.twig index 61237e65f74..f199bc40c2a 100644 --- a/templates/pages/admin/form/form_question.html.twig +++ b/templates/pages/admin/form/form_question.html.twig @@ -201,6 +201,29 @@ } ) }} + {% set sub_types = question_type.getSubTypes() %} + {{ fields.dropdownArrayField( + question_type.getSubTypeFieldName(), + question_type.getSubTypeDefaultValue(question), + question_type.getSubTypes(), + '', + { + 'init' : question is not null, + 'no_label' : true, + 'mb' : '', + 'field_class' : 'me-2' ~ (sub_types is empty ? ' d-none' : ''), + 'class' : 'form-select form-select-sm', + 'width' : 'auto', + 'disabled' : sub_types is empty, + 'aria_label' : question_type.getSubTypeFieldAriaLabel(), + 'add_data_attributes' : { + 'glpi-form-editor-on-change' : 'change-question-sub-type', + 'glpi-form-editor-question-sub-type-selector' : '', + 'glpi-form-editor-specific-question-extra-data': '' + } + } + ) }} + {# Render the specific question options #}
{{ question_type.renderAdministrationOptionsTemplate(question)|raw }} diff --git a/tests/cypress/e2e/form/destination_config_fields/itilcategory.cy.js b/tests/cypress/e2e/form/destination_config_fields/itilcategory.cy.js index b3d28d8eb63..d022c42e2d3 100644 --- a/tests/cypress/e2e/form/destination_config_fields/itilcategory.cy.js +++ b/tests/cypress/e2e/form/destination_config_fields/itilcategory.cy.js @@ -55,6 +55,13 @@ describe('ITILCategory configuration', () => { cy.getDropdownByLabelText("Select a dropdown type").selectDropdownValue('ITIL categories'); cy.get('@form_id').then((form_id) => { const itilcategory_name = `Test ITIL Category for the ITILCategory configuration suite - ${form_id}`; + + // Wait for the items_id dropdown to be loaded + cy.intercept('/ajax/dropdownAllItems.php').as('dropdownAllItems'); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(200); + cy.getDropdownByLabelText("Select a dropdown item").selectDropdownValue(`ยป${itilcategory_name}`); }); cy.findByRole('button', {'name': 'Save'}).click(); diff --git a/tests/cypress/e2e/form/question_types/item.cy.js b/tests/cypress/e2e/form/question_types/item.cy.js index a86d1b2de4d..79efcdfc0d4 100644 --- a/tests/cypress/e2e/form/question_types/item.cy.js +++ b/tests/cypress/e2e/form/question_types/item.cy.js @@ -70,6 +70,9 @@ describe('Item form question type', () => { // Select the ticket itemtype cy.findByRole("option", { name: "Tickets" }).should('exist').click(); + // Wait for the items_id dropdown to be loaded + cy.intercept('/ajax/dropdownAllItems.php').as('dropdownAllItems'); + // Click on the items_id dropdown cy.getDropdownByLabelText("Select an item").click(); @@ -105,6 +108,9 @@ describe('Item form question type', () => { // Select the ITIL category itemtype cy.findByRole("option", { name: "ITIL categories" }).should('exist').click(); + // Wait for the items_id dropdown to be loaded + cy.intercept('/ajax/dropdownAllItems.php').as('dropdownAllItems'); + // Click on the items_id dropdown cy.getDropdownByLabelText("Select a dropdown item").click(); From d1ff561b8166d186f579d1eec230d805bbf2d807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Anne?= Date: Tue, 14 Jan 2025 15:46:07 +0100 Subject: [PATCH 2/2] Update templates/pages/admin/form/form_editor.html.twig --- templates/pages/admin/form/form_editor.html.twig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/pages/admin/form/form_editor.html.twig b/templates/pages/admin/form/form_editor.html.twig index 5f8e6c3c0be..ddbf136c594 100644 --- a/templates/pages/admin/form/form_editor.html.twig +++ b/templates/pages/admin/form/form_editor.html.twig @@ -395,9 +395,9 @@ '{{ get_class(question_type)|e('js') }}', { 'subtypes' : {{ question_type.getSubTypes()|json_encode|raw }}, - 'default_value' : '{{ question_type.getSubTypeDefaultValue(null) }}', - 'field_name' : '{{ question_type.getSubTypeFieldName() }}', - 'field_aria_label': '{{ question_type.getSubTypeFieldAriaLabel() }}', + 'default_value' : '{{ question_type.getSubTypeDefaultValue(null)|e('js') }}', + 'field_name' : '{{ question_type.getSubTypeFieldName()|e('js') }}', + 'field_aria_label': '{{ question_type.getSubTypeFieldAriaLabel()|e('js') }}', } ) {% endif %}