From 099eba43f9b1d5d793063b20d9e3c31919b87226 Mon Sep 17 00:00:00 2001 From: alisher372 Date: Sat, 1 Aug 2026 14:03:15 +0500 Subject: [PATCH] feat(forms): implement embeddable rendering for Fields question type ## Summary Implement the new embeddable question type interface for the Fields question type. This allows the Fields question type to be rendered inside composite question types while keeping all rendering logic inside the Fields plugin. ## What is implemented - implementation of the new embeddable interface; - embedded administration rendering; - embedded end-user rendering; - automatic block detection; - selection of the concrete field only; - reuse of the existing Fields rendering logic; - full compatibility with existing standalone Fields questions. ## Motivation The Fields plugin already contains all rendering logic required to display its controls. Instead of duplicating this logic in other plugins, it is now exposed through the common interface introduced in GLPI. This keeps responsibilities separated: - Fields knows how to render Fields controls; - composite question types only request rendering. ## Backward compatibility No existing behaviour is changed. Standalone Fields questions continue to work exactly as before. ## Related Depends on: - GLPI Core PR: Used by: - Advanced Forms PR: --- inc/questiontype.class.php | 118 ++++++++++++++++-- ...ion_type_embedded_administration.html.twig | 25 ++++ templates/question_type_end_user.html.twig | 13 +- 3 files changed, 139 insertions(+), 17 deletions(-) create mode 100644 templates/question_type_embedded_administration.html.twig diff --git a/inc/questiontype.class.php b/inc/questiontype.class.php index a38daf1b..b5232f91 100644 --- a/inc/questiontype.class.php +++ b/inc/questiontype.class.php @@ -37,11 +37,14 @@ use Glpi\Form\Question; use Glpi\Form\QuestionType\AbstractQuestionType; use Glpi\Form\QuestionType\QuestionTypeCategoryInterface; +use Glpi\Form\QuestionType\QuestionTypeEmbeddableInterface; use function Safe\json_decode; use function Safe\json_encode; -final class PluginFieldsQuestionType extends AbstractQuestionType implements FormQuestionDataConverterInterface +final class PluginFieldsQuestionType extends AbstractQuestionType implements + FormQuestionDataConverterInterface, + QuestionTypeEmbeddableInterface { #[Override] public function getCategory(): QuestionTypeCategoryInterface @@ -112,6 +115,48 @@ public function validateExtraDataInput(array $input): bool #[Override] public function renderAdministrationTemplate(?Question $question): string { + return $this->renderAdministrationTemplateWithInputNames( + $question, + field_id_input_name: 'field_id', + default_value_input_name: 'default_value', + embedded: false, + ); + } + + /** Render the complete Fields configuration inside another question type. */ + public function renderEmbeddedAdministrationTemplate( + ?Question $question, + string $extra_data_input_prefix, + ): string { + $block_id = $this->getDefaultValueBlockId($question); + $available_blocks = $this->getAvailableBlocks(); + if ($block_id === null) { + $block_id = current(array_keys($available_blocks)); + } + + $available_fields = self::getFieldsFromBlock($block_id); + $current_field_id = $this->getDefaultValueFieldId($question); + if ($current_field_id === null || !isset($available_fields[$current_field_id])) { + $current_field_id = current(array_keys($available_fields)); + } + + return TemplateRenderer::getInstance()->render('@fields/question_type_embedded_administration.html.twig', [ + 'block_input_name' => $extra_data_input_prefix . '[block_id]', + 'field_input_name' => $extra_data_input_prefix . '[field_id]', + 'selected_block_id' => $block_id, + 'selected_field_id' => $current_field_id, + 'available_blocks' => $available_blocks, + 'available_fields' => $available_fields, + ]); + } + + private function renderAdministrationTemplateWithInputNames( + + ?Question $question, + string $field_id_input_name, + string $default_value_input_name, + bool $embedded, + ): string { // Get the block_id from the question's extra data or use the first available block $block_id = $this->getDefaultValueBlockId($question); if ($block_id === null) { @@ -127,6 +172,9 @@ public function renderAdministrationTemplate(?Question $question): string } $current_field = PluginFieldsField::getById($current_field_id); + if (!$current_field) { + return ''; + } // Compute default value for the field $default_value = null; @@ -136,33 +184,75 @@ public function renderAdministrationTemplate(?Question $question): string $twig = TemplateRenderer::getInstance(); return $twig->render('@fields/question_type_administration.html.twig', [ - 'question' => $question, - 'default_value' => $default_value, - 'selected_field_id' => $current_field_id, - 'available_fields' => $available_fields, - 'item' => new Form(), - 'field' => $current_field->fields, + 'question' => $question, + 'default_value' => $default_value, + 'selected_field_id' => $current_field_id, + 'available_fields' => $available_fields, + 'item' => new Form(), + 'field' => $current_field->fields, + 'field_id_input_name' => $field_id_input_name, + 'default_value_input_name' => $default_value_input_name, + 'embedded' => $embedded, ]); } #[Override] public function renderEndUserTemplate(Question $question): string { - // Get the block_id from the question's extra data or use the first available block + return $this->renderEndUserTemplateWithInputName( + $question, + $question->getEndUserInputName(), + false, + ); + } + + /** Render the end-user control with an input name supplied by an embedding question type. */ + public function renderEmbeddedEndUserTemplate(Question $question, string $input_name): string + { + return $this->renderEndUserTemplateWithInputName($question, $input_name, true); + } + + private function renderEndUserTemplateWithInputName( + Question $question, + string $input_name, + bool $embedded, + ): string + { + // Resolve a valid block. Embedded table questions may contain an empty, + // stale or no longer accessible block_id. + $available_blocks = $this->getAvailableBlocks(); + if ($available_blocks === []) { + return ''; + } + $block_id = $this->getDefaultValueBlockId($question); - if ($block_id === null) { - $block_id = current(array_keys($this->getAvailableBlocks())); + if ( + $block_id === null + || !array_key_exists($block_id, $available_blocks) + ) { + $block_id = array_key_first($available_blocks); } $available_fields = self::getFieldsFromBlock($block_id); + if ($available_fields === []) { + return ''; + } - // Retrieve current field + // Resolve a valid field for the selected block. This also handles + // deleted fields and configurations created before automatic block + // selection was added. $current_field_id = $this->getDefaultValueFieldId($question); - if ($current_field_id === null) { - $current_field_id = current(array_keys($available_fields)); + if ( + $current_field_id === null + || !array_key_exists($current_field_id, $available_fields) + ) { + $current_field_id = array_key_first($available_fields); } $current_field = PluginFieldsField::getById($current_field_id); + if (!$current_field instanceof PluginFieldsField) { + return ''; + } // Compute default value for the field $default_value = null; @@ -188,6 +278,8 @@ public function renderEndUserTemplate(Question $question): string 'default_value' => $default_value, 'item' => new Form(), 'itemtype' => $itemtype, + 'input_name' => $input_name, + 'embedded' => $embedded, ]); } diff --git a/templates/question_type_embedded_administration.html.twig b/templates/question_type_embedded_administration.html.twig new file mode 100644 index 00000000..99c5ef9e --- /dev/null +++ b/templates/question_type_embedded_administration.html.twig @@ -0,0 +1,25 @@ +{% import 'components/form/fields_macros.html.twig' as fields %} + +
+ {{ fields.hiddenField( + block_input_name, + selected_block_id + ) }} + + {{ fields.dropdownArrayField( + field_input_name, + selected_field_id, + available_fields, + '', + { + 'no_label': true, + 'field_class': 'flex-grow-1', + 'class': 'form-select form-select-sm w-100', + 'mb': '', + 'init': true, + 'add_data_attributes': { + 'af-embedded-question-config-control': '' + } + } + ) }} +
diff --git a/templates/question_type_end_user.html.twig b/templates/question_type_end_user.html.twig index 4e583ba5..05da2d68 100644 --- a/templates/question_type_end_user.html.twig +++ b/templates/question_type_end_user.html.twig @@ -28,6 +28,8 @@ {% import 'components/form/fields_macros.html.twig' as fields %} +{% set input_name = input_name|default(question.getEndUserInputName()) %} + {% set field = field|merge({ 'default_value': default_value.items_id ?? default_value ?? field.default_value }) %} @@ -36,7 +38,7 @@ {% set name_suffix = is_dropdown ? '[items_id]' : '' %} {{ fields.hiddenField( - question.getEndUserInputName() ~ '[itemtype]', + input_name ~ '[itemtype]', itemtype ) }} @@ -48,11 +50,14 @@ true, false, { - 'input_name' : question.getEndUserInputName() ~ name_suffix, - 'full_width' : not (field.type starts with 'dropdown' or field.type == 'glpi_item'), + 'input_name' : input_name ~ name_suffix, + 'full_width' : embedded|default(false) + or not (field.type starts with 'dropdown' or field.type == 'glpi_item'), 'no_label' : true, 'mb' : '', - 'add_field_class': 'glpi-fields-plugin-question-type-glpi-item-field', + 'add_field_class': embedded|default(false) + ? 'glpi-fields-plugin-question-type-glpi-item-field w-100' + : 'glpi-fields-plugin-question-type-glpi-item-field', 'comments' : false } ])|raw }}