From a4eb9920deb2c46a894bfccf06e081bfa01b3fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Cailly?= Date: Thu, 23 Oct 2025 13:52:46 +0200 Subject: [PATCH 1/4] feat(forms): add Fields question type and category implementation --- .gitignore | 7 +- CHANGELOG.md | 5 +- Makefile | 1 + inc/field.class.php | 22 + inc/questiontype.class.php | 373 ++++++++++++++ .../questiontypecategory.class.php | 46 +- inc/questiontypeextradataconfig.class.php | 71 +++ phpunit.xml | 7 + public/css/fields.scss | 91 ++++ public/js/modules/QuestionField.js | 191 +++++++ setup.php | 22 +- src/Controller/QuestionTypeAjaxController.php | 136 +++++ templates/fields.html.twig | 85 ++-- .../question_type_administration.html.twig | 101 ++++ templates/question_type_end_user.html.twig | 51 ++ tests/FieldTestCase.php | 64 +++ tests/QuestionTypeTestCase.php | 127 +++++ .../Units/FieldQuestionTypeMigrationTest.php | 133 +++++ tests/Units/FieldQuestionTypeTest.php | 158 ++++++ tests/bootstrap.php | 38 ++ tests/fixtures/formcreator.sql | 469 ++++++++++++++++++ 21 files changed, 2139 insertions(+), 59 deletions(-) create mode 100644 Makefile create mode 100644 inc/questiontype.class.php rename public/css/fields.css => inc/questiontypecategory.class.php (73%) create mode 100644 inc/questiontypeextradataconfig.class.php create mode 100644 phpunit.xml create mode 100644 public/css/fields.scss create mode 100644 public/js/modules/QuestionField.js create mode 100644 src/Controller/QuestionTypeAjaxController.php create mode 100644 templates/question_type_administration.html.twig create mode 100644 templates/question_type_end_user.html.twig create mode 100644 tests/FieldTestCase.php create mode 100644 tests/QuestionTypeTestCase.php create mode 100644 tests/Units/FieldQuestionTypeMigrationTest.php create mode 100644 tests/Units/FieldQuestionTypeTest.php create mode 100644 tests/bootstrap.php create mode 100644 tests/fixtures/formcreator.sql diff --git a/.gitignore b/.gitignore index 283db0af..ddede569 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ -dist/ -vendor/ +/dist/ +/node_modules/ +/vendor/ .gh_token +.phpunit.result.cache +tests/files/ *.min.* diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ce3b530..bc5777b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). - ## [UNRELEASED] +### Added + +- Implement `Field` question type for new GLPI forms + ## [1.22.2] - 2025-10-24 ### Fixed diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ef1bed5a --- /dev/null +++ b/Makefile @@ -0,0 +1 @@ +include ../../PluginsMakefile.mk diff --git a/inc/field.class.php b/inc/field.class.php index 659f2743..efaa97a6 100644 --- a/inc/field.class.php +++ b/inc/field.class.php @@ -30,6 +30,7 @@ use Glpi\Features\Clonable; use Glpi\DBAL\QueryExpression; use Glpi\Application\View\TemplateRenderer; +use Glpi\Form\Question; class PluginFieldsField extends CommonDBChild { @@ -350,6 +351,27 @@ public function pre_deleteItem() */ global $DB; + // Check if the field is used in a form question + $question = new Question(); + $found = $question->find([ + 'type' => PluginFieldsQuestionType::class, + $this->fields['id'] => new QueryExpression(sprintf( + "JSON_VALUE(%s, '$.field_id')", + DBmysql::quoteName('extra_data'), + )), + ]); + if (!empty($found)) { + $question->getFromDB(current($found)['id']); + Session::addMessageAfterRedirect( + msg: $question->formatSessionMessageAfterAction(sprintf( + __('The field "%s" cannot be deleted because it is used in a form question', 'fields'), + $this->fields['label'], + )), + message_type: ERROR, + ); + return false; + } + //retrieve search option ID to clean DiplayPreferences $container_obj = new PluginFieldsContainer(); $container_obj->getFromDB($this->fields['plugin_fields_containers_id']); diff --git a/inc/questiontype.class.php b/inc/questiontype.class.php new file mode 100644 index 00000000..14967311 --- /dev/null +++ b/inc/questiontype.class.php @@ -0,0 +1,373 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +use Glpi\Application\View\TemplateRenderer; +use Glpi\Form\Form; +use Glpi\Form\Migration\FormQuestionDataConverterInterface; +use Glpi\Form\Question; +use Glpi\Form\QuestionType\AbstractQuestionType; +use Glpi\Form\QuestionType\QuestionTypeCategoryInterface; + +use function Safe\json_decode; +use function Safe\json_encode; + +class PluginFieldsQuestionType extends AbstractQuestionType implements FormQuestionDataConverterInterface +{ + #[Override] + public function getCategory(): QuestionTypeCategoryInterface + { + return new PluginFieldsQuestionTypeCategory(); + } + + #[Override] + public function getExtraDataConfigClass(): ?string + { + return PluginFieldsQuestionTypeExtraDataConfig::class; + } + + #[Override] + public function getSubTypes(): array + { + return $this->getAvailableBlocks(); + } + + #[Override] + public function getSubTypeFieldName(): string + { + return 'block_id'; + } + + #[Override] + public function getSubTypeFieldAriaLabel(): string + { + return __('Select a block'); + } + + #[Override] + public function getSubTypeDefaultValue(?Question $question): ?string + { + return (string) $this->getDefaultValueBlockId($question); + } + + #[Override] + public function formatDefaultValueForDB(mixed $value): ?string + { + return json_encode($value); + } + + #[Override] + public function validateExtraDataInput(array $input): bool + { + // Check if the block_id is set and is numeric + if (!isset($input['block_id']) || !is_numeric($input['block_id'])) { + return false; + } + + // Check if the block_id exists + $available_blocks = $this->getAvailableBlocks(); + if (!isset($available_blocks[$input['block_id']])) { + return false; + } + + // Check if the field_id is set and is numeric + if (!isset($input['field_id']) || !is_numeric($input['field_id'])) { + return false; + } + + // Check if the field_id exists in the selected block + $available_fields = $this->getFieldsFromBlock($input['block_id']); + if (!isset($available_fields[$input['field_id']])) { + return false; + } + + return true; + } + + #[Override] + public function renderAdministrationTemplate(?Question $question): 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) { + $block_id = current(array_keys($this->getAvailableBlocks())); + } + $available_fields = $this->getFieldsFromBlock($block_id); + + // Retrieve current field + $current_field_id = $this->getDefaultValueFieldId($question); + if ($current_field_id === null) { + $current_field_id = current(array_keys($available_fields)); + } + + $current_field = PluginFieldsField::getById($current_field_id); + + // Compute default value for the field + $default_value = null; + if ($question !== null && !empty($question->fields['default_value'])) { + $default_value = json_decode($question->fields['default_value'], true); + } + + $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, + ]); + } + + #[Override] + public function renderEndUserTemplate(Question $question): 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) { + $block_id = current(array_keys($this->getAvailableBlocks())); + } + $available_fields = $this->getFieldsFromBlock($block_id); + + // Retrieve current field + $current_field_id = $this->getDefaultValueFieldId($question); + if ($current_field_id === null) { + $current_field_id = current(array_keys($available_fields)); + } + + $current_field = PluginFieldsField::getById($current_field_id); + + // Compute default value for the field + $default_value = null; + if (!empty($question->fields['default_value'])) { + $default_value = json_decode($question->fields['default_value'], true); + } + + $twig = TemplateRenderer::getInstance(); + return $twig->render('@fields/question_type_end_user.html.twig', [ + 'question' => $question, + 'field' => $current_field->fields, + 'default_value' => $default_value, + 'item' => new Form(), + ]); + } + + #[Override] + public function formatRawAnswer(mixed $answer, Question $question): string + { + $current_field_id = $this->getDefaultValueFieldId($question); + if ($current_field_id === null) { + throw new LogicException('No field configured for this question'); + } + + $current_field = PluginFieldsField::getById((int) $current_field_id); + + switch ($current_field->fields['type']) { + case 'header': + case 'text': + case 'textarea': + case 'richtext': + case 'number': + case 'url': + case 'date': + return (string) $answer; + case 'dropdown': + if (is_string($answer)) { + $answer = [$answer]; + } + + $itemtype = PluginFieldsDropdown::getClassname($current_field->fields['name']); + return implode(', ', array_map(fn($opt_id) => $itemtype::getById($opt_id)->fields['name'], $answer)); + case 'yesno': + return $answer ? __('Yes') : __('No'); + case 'datetime': + return (new DateTime($answer))->format('Y-m-d H:i'); + case 'glpi_item': + $item = $answer['itemtype']::getById($answer['items_id']); + if (!$item) { + return ''; + } + + return $item->fields['name']; + } + + if (str_starts_with($current_field->fields['type'], 'dropdown-')) { + $itemtype = substr($current_field->fields['type'], strlen('dropdown-')); + if (!getItemForItemtype($itemtype)) { + return ''; + } + + if (is_string($answer)) { + $answer = [$answer]; + } + return implode(', ', array_map(fn($items_id) => $itemtype::getById($items_id)->fields['name'], $answer)); + } + + return (string) $answer; + } + + #[Override] + public function beforeConversion(array $rawData): void {} + + #[Override] + public function convertDefaultValue(array $rawData): null + { + return null; + } + + #[Override] + public function convertExtraData(array $rawData): ?array + { + $values = json_decode($rawData['values'], true); + if (!isset($values['dropdown_fields_field']) || !isset($values['blocks_field'])) { + return null; + } + + $block = new PluginFieldsContainer(); + if (!$block->getFromDB($values['blocks_field'])) { + return null; + } + + $field = new PluginFieldsField(); + if (!$field->getFromDBByCrit(['name' => $values['dropdown_fields_field']])) { + return null; + } + + return [ + 'block_id' => $block->getID(), + 'field_id' => $field->getID(), + ]; + } + + #[Override] + public function getTargetQuestionType(array $rawData): string + { + return self::class; + } + + /** + * Retrieve the default value block from the question's extra data + * + * @param Question|null $question The question to retrieve the default value from + * @return ?int + */ + public function getDefaultValueBlockId(?Question $question): ?int + { + if ($question === null) { + return null; + } + + /** @var ?PluginFieldsQuestionTypeExtraDataConfig $config */ + $config = $this->getExtraDataConfig(json_decode($question->fields['extra_data'], true) ?? []); + if ($config === null) { + return null; + } + + return $config->getBlockId(); + } + + /** + * Retrieve the default value field from the question's extra data + * + * @param Question|null $question The question to retrieve the default value from + * @return ?int + */ + public function getDefaultValueFieldId(?Question $question): ?int + { + if ($question === null) { + return null; + } + + /** @var ?PluginFieldsQuestionTypeExtraDataConfig $config */ + $config = $this->getExtraDataConfig(json_decode($question->fields['extra_data'], true) ?? []); + if ($config === null) { + return null; + } + + return $config->getFieldId(); + } + + private function getAvailableBlocks(?Form $form = null): array + { + $field_container = new PluginFieldsContainer(); + $available_blocks = []; + $result = $field_container->find([ + 'is_active' => 1, + 'type' => 'dom', + 'OR' => [ + ['itemtypes' => ['LIKE', '%\"Ticket\"%']], + ['itemtypes' => ['LIKE', '%\"Change\"%']], + ['itemtypes' => ['LIKE', '%\"Problem\"%']], + ], + ] + getEntitiesRestrictCriteria(PluginFieldsContainer::getTable(), '', '', true), 'name'); + foreach ($result as $id => $data) { + $available_blocks[$id] = $data['label']; + } + return $available_blocks; + } + + private function getFieldsFromBlock(?int $block_id): array + { + $fields = []; + $field_container = PluginFieldsContainer::getById($block_id); + if ($field_container) { + $field = new PluginFieldsField(); + $result = $field->find([ + 'is_active' => 1, + 'plugin_fields_containers_id' => $block_id, + 'NOT' => [ + ['type' => 'header'], // Exclude headers + ], + ]); + foreach ($result as $id => $data) { + $fields[$id] = $data['label']; + } + } + + return $fields; + } + + /** + * Check if there is at least one available field in the available blocks + * + * @return bool + */ + public static function hasAvailableFields(): bool + { + $blocks = (new self())->getAvailableBlocks(); + foreach ($blocks as $block_id => $block_label) { + $fields = (new self())->getFieldsFromBlock($block_id); + if (!empty($fields)) { + return true; + } + } + + return false; + } +} diff --git a/public/css/fields.css b/inc/questiontypecategory.class.php similarity index 73% rename from public/css/fields.css rename to inc/questiontypecategory.class.php index c6a46935..f9d38b14 100644 --- a/public/css/fields.css +++ b/inc/questiontypecategory.class.php @@ -1,4 +1,6 @@ -/*! +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +use Glpi\DBAL\JsonFieldInterface; + +class PluginFieldsQuestionTypeExtraDataConfig implements JsonFieldInterface +{ + // Unique reference to hardcoded name used for serialization + public const BLOCK_ID = "block_id"; + public const FIELD_ID = "field_id"; + + public function __construct( + private ?int $block_id = null, + private ?int $field_id = null, + ) {} + + #[Override] + public static function jsonDeserialize(array $data): self + { + return new self( + block_id: $data[self::BLOCK_ID] ?? null, + field_id: $data[self::FIELD_ID] ?? null, + ); + } + + #[Override] + public function jsonSerialize(): array + { + return [ + self::BLOCK_ID => $this->block_id, + self::FIELD_ID => $this->field_id, + ]; + } + + public function getBlockId(): ?int + { + return $this->block_id; + } + + public function getFieldId(): ?int + { + return $this->field_id; + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..ff4a9299 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,7 @@ + + + + tests + + + \ No newline at end of file diff --git a/public/css/fields.scss b/public/css/fields.scss new file mode 100644 index 00000000..15c09b06 --- /dev/null +++ b/public/css/fields.scss @@ -0,0 +1,91 @@ +/*! + * ------------------------------------------------------------------------- + * Fields plugin for GLPI + * ------------------------------------------------------------------------- + * + * LICENSE + * + * This file is part of Fields. + * + * Fields is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Fields is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Fields. If not, see . + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +//config +ul.fields_config li { + cursor:pointer; + float:left; + width:20%; + -moz-border-radius: 5px; + -webkit-border-radius:5px; + border-radius: 5px; + margin:0 1% 10px 1%; + padding:5px 1px; +} +ul.fields_config li:hover { + background-color:#E8E8E8; +} +ul.fields_config li p { + padding-top: 8px; +} +ul.fields_config li img { + float: left; + margin-right: 2px; +} +div.fields_clear { + clear: both; + width:100%; +} + +[data-glpi-form-editor-question-type-specific] .glpi-fields-plugin-question-type-glpi-item-field .select2-selection:first-of-type:not(:only-child) { + border-radius: 0; +} + +[data-glpi-form-editor-question-type-specific]:has(.glpi-fields-plugin-question-type-glpi-item-field), +[data-glpi-form-renderer-fields-question-type-specific-container]:has(.glpi-fields-plugin-question-type-glpi-item-field) { + &:has(> div > span select[data-select2-id]) { + > div:first-of-type .select2-selection { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + + > div:not(:first-of-type) { + .glpi-fields-plugin-question-type-glpi-item-field { + width: 100%; + } + + .select2-selection { + margin-top: calc(-1 * var(--tblr-border-width)); + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + .dropdown_tooltip { + margin-top: calc(-1 * var(--tblr-border-width)); + + &:last-of-type { + border-bottom-right-radius: var(--tblr-border-radius); + } + } + } + + > div:has([data-glpi-form-editor-question-type-fields-field-id-selector]) { + display: none; + } + } +} diff --git a/public/js/modules/QuestionField.js b/public/js/modules/QuestionField.js new file mode 100644 index 00000000..02be9901 --- /dev/null +++ b/public/js/modules/QuestionField.js @@ -0,0 +1,191 @@ +/** + * ------------------------------------------------------------------------- + * Fields plugin for GLPI + * ------------------------------------------------------------------------- + * + * LICENSE + * + * This file is part of Fields. + * + * Fields is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Fields is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Fields. If not, see . + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +export class GlpiPluginFieldsQuestionTypeField { + /** + * Move the field dropdown to the dropdown group + * @param {jQuery} question - The question element + */ + moveFieldDropdownToGroup(question) { + const fieldDropdown = question.find('[data-glpi-form-editor-question-type-specific]') + .find('select[data-glpi-form-editor-question-type-fields-field-id-selector]').parent(); + const dropdownGroup = question.find('.question-type-dropdown-group'); + + // Remove from current parent if already in group + dropdownGroup.find('select[data-glpi-form-editor-question-type-fields-field-id-selector]').parent().remove(); + + // Append to the dropdown group + dropdownGroup.append(fieldDropdown); + } + + /** + * Remove the field dropdown if it's in the dropdown group + * @param {jQuery} question - The question element + */ + removeFieldDropdownFromGroup(question) { + const fieldDropdown = question.find('select[data-glpi-form-editor-question-type-fields-field-id-selector]'); + + if (fieldDropdown.parent().closest('.question-type-dropdown-group').length === 1) { + fieldDropdown.parent().remove(); + } + } + + /** + * Lock the mandatory input for the question + * @param {jQuery} question - The question element + * @param {boolean} isMandatory - Whether the question is mandatory + */ + lockMandatoryInput(question, isMandatory) { + question.find('[name="is_mandatory"][type="checkbox"], [data-glpi-form-editor-original-name="is_mandatory"][type="checkbox"]') + .prop('disabled', true) + .prop('checked', isMandatory); + question.find('[name="is_mandatory"][type="hidden"], [data-glpi-form-editor-original-name="is_mandatory"][type="hidden"]') + .val(isMandatory ? '1' : '0'); + } + + /** + * Unlock the mandatory input for the question + * @param {jQuery} question - The question element + */ + unlockMandatoryInput(question) { + question.find('[name="is_mandatory"][type="checkbox"], [data-glpi-form-editor-original-name="is_mandatory"][type="checkbox"]') + .prop('disabled', false); + question.find('[name="is_mandatory"][type="hidden"], [data-glpi-form-editor-original-name="is_mandatory"][type="hidden"]') + .val('0'); + } + + /** + * Reload question content via AJAX + * @param {jQuery} question - The question element + * @param {number} blockId - The block ID + * @param {number} fieldId - The field ID + */ + async reloadQuestionContent(question, blockId, fieldId) { + // Get the current default value if it exists + const defaultValueInput = question.find('[name="default_value"], [data-glpi-form-editor-original-name="default_value"]'); + let defaultValue = null; + + if (defaultValueInput.length > 0) { + if (defaultValueInput.is('select[multiple]')) { + defaultValue = defaultValueInput.val() || []; + } else { + defaultValue = defaultValueInput.val(); + } + } + + // Find the container for the question type specific content + const specificContainer = question.find('[data-glpi-form-editor-question-type-specific]'); + + // Set loading state + const editorController = question.closest('form[data-glpi-form-editor-container]').data('controller'); + editorController.setQuestionTypeSpecificLoadingState(question, true); + + // Make AJAX request to get updated content + specificContainer.load(`${CFG_GLPI.root_doc}/plugins/fields/GetFieldQuestionContent`, { + block_id: blockId, + field_id: fieldId, + default_value: defaultValue + }, () => { + // Move the field dropdown back to the group + this.moveFieldDropdownToGroup(question); + + // Remove loading state + editorController.setQuestionTypeSpecificLoadingState(question, false); + + // Mark form as having unsaved changes + if (window.setHasUnsavedChanges) { + window.setHasUnsavedChanges(true); + } + }); + } + + /** + * Initialize event handlers for question type changes + */ + initEventHandlers() { + // Handle question type changes + $(document).on('glpi-form-editor-question-type-changed', (event, question, type) => { + if (type !== 'PluginFieldsQuestionType') { + this.removeFieldDropdownFromGroup(question); + this.unlockMandatoryInput(question); + } else { + this.moveFieldDropdownToGroup(question); + } + }); + + // Handle block_id (sub-type) changes + $(document).on('glpi-form-editor-question-sub-type-changed', async (event, question, subType) => { + // Check if this is a PluginFieldsQuestionType question + const questionType = question.find('[name="type"], [data-glpi-form-editor-original-name="type"]').val(); + if (questionType !== 'PluginFieldsQuestionType') { + return; + } + + // Get the field_id selector + const fieldDropdown = question.find('select[data-glpi-form-editor-question-type-fields-field-id-selector]'); + const fieldId = fieldDropdown.val(); + + // Reload the question content with the new block_id and current field_id + if (subType && fieldId) { + await this.reloadQuestionContent(question, subType, fieldId); + } + }); + + // Handle field_id changes + $(document).on('change', 'select[data-glpi-form-editor-question-type-fields-field-id-selector]', async (event) => { + const fieldDropdown = $(event.target); + const question = fieldDropdown.closest('[data-glpi-form-editor-question]'); + + // Check if this is a PluginFieldsQuestionType question + const questionType = question.find('[name="type"], [data-glpi-form-editor-original-name="type"]').val(); + if (questionType !== 'PluginFieldsQuestionType') { + return; + } + + // Get the block_id (sub-type) + const blockDropdown = question.find('[data-glpi-form-editor-question-sub-type-selector]'); + const blockId = blockDropdown.val(); + const fieldId = fieldDropdown.val(); + + // Reload the question content with the current block_id and new field_id + if (blockId && fieldId) { + await this.reloadQuestionContent(question, blockId, fieldId); + } + }); + } + + /** + * Initialize an existing question + * @param {string} rand - The random identifier + */ + initExistingQuestion(rand) { + const fieldDropdown = $(`select[data-glpi-form-editor-question-type-fields-field-id-selector="${rand}"]`); + const question = fieldDropdown.closest('[data-glpi-form-editor-question]'); + this.moveFieldDropdownToGroup(question); + } +} diff --git a/setup.php b/setup.php index 9dfc4c5c..88004591 100644 --- a/setup.php +++ b/setup.php @@ -34,7 +34,7 @@ define('PLUGIN_FIELDS_VERSION', '1.22.2'); // Minimal GLPI version, inclusive -define('PLUGIN_FIELDS_MIN_GLPI', '11.0.0'); +define('PLUGIN_FIELDS_MIN_GLPI', '11.0.2'); // Maximum GLPI version, exclusive define('PLUGIN_FIELDS_MAX_GLPI', '11.0.99'); @@ -66,6 +66,8 @@ mkdir(PLUGINFIELDS_FRONT_PATH); } +use Glpi\Form\Migration\TypesConversionMapper; +use Glpi\Form\QuestionType\QuestionTypesManager; use Symfony\Component\Yaml\Yaml; /** @@ -139,7 +141,7 @@ function plugin_init_fields() if (!$debug && file_exists(__DIR__ . '/public/css/fields.min.css')) { $PLUGIN_HOOKS['add_css']['fields'][] = 'css/fields.min.css'; } else { - $PLUGIN_HOOKS['add_css']['fields'][] = 'css/fields.css'; + $PLUGIN_HOOKS['add_css']['fields'][] = 'css/fields.scss'; } // Add/delete profiles to automaticaly to container @@ -193,6 +195,9 @@ function plugin_init_fields() 'PluginFieldsField', 'showForTab', ]; + + // Register fields question type + plugin_fields_register_plugin_types(); } } @@ -389,3 +394,16 @@ function plugin_fields_exportBlockAsYaml($container_id = null) return false; } + +function plugin_fields_register_plugin_types(): void +{ + $types = QuestionTypesManager::getInstance(); + $type_mapper = TypesConversionMapper::getInstance(); + + // Register question category, type and converter if valid fields are defined + if (PluginFieldsQuestionType::hasAvailableFields()) { + $types->registerPluginCategory(new PluginFieldsQuestionTypeCategory()); + $types->registerPluginQuestionType(new PluginFieldsQuestionType()); + $type_mapper->registerPluginQuestionTypeConverter('fields', new PluginFieldsQuestionType()); + } +} diff --git a/src/Controller/QuestionTypeAjaxController.php b/src/Controller/QuestionTypeAjaxController.php new file mode 100644 index 00000000..6e2fe619 --- /dev/null +++ b/src/Controller/QuestionTypeAjaxController.php @@ -0,0 +1,136 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +namespace GlpiPlugin\Fields\Controller; + +use Glpi\Controller\AbstractController; +use Glpi\Form\Form; +use PluginFieldsContainer; +use PluginFieldsField; +use PluginFieldsQuestionType; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +final class QuestionTypeAjaxController extends AbstractController +{ + #[Route( + path: 'GetFieldQuestionContent', + name: 'get_field_question_content_ajax', + )] + public function __invoke(Request $request): Response + { + // Get the block_id and field_id from the request + $block_id = $request->request->get('block_id'); + $field_id = $request->request->get('field_id'); + $default_value = $request->request->get('default_value'); + + // Validate the block_id + if (!$block_id || !is_numeric($block_id)) { + return new Response('Invalid block_id', Response::HTTP_BAD_REQUEST); + } + + // Get the question type instance + $question_type = new PluginFieldsQuestionType(); + + // Get available fields for the selected block + $available_fields = $this->getFieldsFromBlock((int) $block_id); + + // If field_id is not provided or invalid, use the first available field + $current_field_id = null; + if ($field_id && is_numeric($field_id) && isset($available_fields[$field_id])) { + $current_field_id = (int) $field_id; + } else { + $current_field_id = !empty($available_fields) ? (int) current(array_keys($available_fields)) : null; + } + + if ($current_field_id === null) { + return new Response('No fields available for this block', Response::HTTP_BAD_REQUEST); + } + + // Get the container and field details + $current_container = PluginFieldsContainer::getById((int) $block_id); + $current_field = PluginFieldsField::getById($current_field_id); + + if (!$current_container || !$current_field || empty($current_field->fields)) { + return new Response('Invalid container or field', Response::HTTP_BAD_REQUEST); + } + + // Process default value if provided + if ($default_value !== null && !empty($default_value)) { + // If the field is multiple, convert the default value to an array + if ($current_field->fields['multiple']) { + if (!is_array($default_value)) { + $default_value = explode(',', $default_value); + } + } + } else { + $default_value = null; + } + + return $this->render('@fields/question_type_administration.html.twig', [ + 'question' => null, + 'default_value' => $default_value, + 'selected_field_id' => $current_field_id, + 'available_fields' => $available_fields, + 'item' => new Form(), + 'container' => $current_container, + 'field' => $current_field->fields, + 'is_ajax_reload' => true, + ]); + } + + /** + * Get fields from a block + * + * @param int|null $block_id + * @return array + */ + private function getFieldsFromBlock(?int $block_id): array + { + $fields = []; + $field_container = PluginFieldsContainer::getById($block_id); + if ($field_container) { + $field = new PluginFieldsField(); + $result = $field->find([ + 'is_active' => 1, + 'plugin_fields_containers_id' => $block_id, + 'NOT' => [ + ['type' => 'header'], // Exclude headers + ], + ]); + foreach ($result as $id => $data) { + $fields[$id] = $data['label']; + } + } + + return $fields; + } +} diff --git a/templates/fields.html.twig b/templates/fields.html.twig index 4f2270b2..b7d6dc4f 100644 --- a/templates/fields.html.twig +++ b/templates/fields.html.twig @@ -31,6 +31,10 @@ {% set already_wrapped = item is instanceof('CommonITILObject') and container.fields['type'] == 'dom' %} {% set dropdown_item = item is instanceof('CommonDropdown') and container.fields['type'] == 'dom' %} +{% if item is instanceof('Glpi\\Form\\Form') %} + {% set already_wrapped = true %} +{% endif %} + {% if not already_wrapped and not dropdown_item%} {% set class = item.isNewItem() ? 'col-xxl-12' : 'col-xxl-9' %} @@ -52,53 +56,54 @@ {% set field_options = field_options|merge({ 'readonly': readonly or not canedit, 'required': field['mandatory'], - 'full_width': already_wrapped + 'full_width': field_options.full_width ?? already_wrapped, }) %} + {% set input_name = field_options.input_name ?? name %} + {% if type == 'header' %} {{ macros.largeTitle(label) }} {% elseif type == 'text' %} - {{ macros.textField(name, value, label, field_options) }} + {{ macros.textField(input_name, value, label, field_options) }} {% elseif type == 'number' %} - {{ macros.numberField(name, value, label, field_options|merge({step: 'any', min: ''})) }} + {{ macros.numberField(input_name, value, label, field_options|merge({step: 'any', min: ''})) }} {% elseif type == 'url' %} {% set ext_link %} {% if value|length %} - + {{ __('show', 'fields') }} {% endif %} {% endset %} - {{ macros.textField(name, value, label, field_options|merge({ + {{ macros.textField(input_name, value, label, field_options|merge({ 'type': 'url', 'add_field_html': ext_link })) }} {% elseif type == 'textarea' %} - {{ macros.textareaField(name, value, label, field_options) }} + {{ macros.textareaField(input_name, value, label, field_options) }} {% elseif type == 'richtext' %} - {{ macros.textareaField(name, value, label, field_options|merge({ + {{ macros.textareaField(input_name, value, label, field_options|merge({ 'enable_richtext': true, 'field_class': 'col-12', 'label_class': '', 'input_class': '', 'align_label_right': false, - 'mb': 'm-2' })) }} {% elseif type == 'yesno' %} - {{ macros.dropdownYesNo(name, value, label, field_options) }} + {{ macros.dropdownYesNo(input_name, value, label, field_options) }} {% elseif type == 'date' %} - {{ macros.dateField(name, value, label, field_options) }} + {{ macros.dateField(input_name, value, label, field_options) }} {% elseif type == 'datetime' %} - {{ macros.datetimeField(name, value, label, field_options) }} + {{ macros.datetimeField(input_name, value, label, field_options) }} {% elseif type == 'dropdown' %} {% set dropdown_options = {'entity': item.getEntityID()} %} @@ -113,8 +118,10 @@ {% else %} {% set dropdown_itemtype = call("PluginFieldsDropdown::getClassname", [name]) %} {% endif %} - {% set name_fk = call("getForeignKeyFieldForItemType", [dropdown_itemtype]) %} - {{ macros.dropdownField(dropdown_itemtype, name_fk, value, label, field_options|merge(dropdown_options|default({}))) }} + {% if input_name == name %} + {% set name_fk = call("getForeignKeyFieldForItemType", [dropdown_itemtype]) %} + {% endif %} + {{ macros.dropdownField(dropdown_itemtype, name_fk ?? input_name, value, label, field_options|merge(dropdown_options|default({}))) }} {% elseif type matches '/^dropdown-.+/i' %} {% set dropdown_options = {'entity': item.getEntityID()} %} @@ -129,33 +136,49 @@ {% if field['multiple'] %} {% set dropdown_options = dropdown_options|merge({'multiple': true}) %} {% endif %} - {{ macros.dropdownField(field['dropdown_class'], name, value, label, field_options|merge(dropdown_options|default({}))) }} + {{ macros.dropdownField(field['dropdown_class'], input_name, value, label, field_options|merge(dropdown_options|default({}))) }} {% elseif type == 'glpi_item' %} {% if not massiveaction %} - {% set itemtype_prefix = 'itemtype_' %} - {% set items_id_prefix = 'items_id_' %} + + {% if item is instanceof('Glpi\\Form\\Form') %} + {% set itemtype_input_name = input_name ~ '[itemtype]' %} + {% set items_id_input_name = input_name ~ '[items_id]' %} + {% else %} + {% set itemtype_input_name = 'itemtype_' ~ name %} + {% set items_id_input_name = 'items_id_' ~ name %} + {% endif %} {% if container.fields['type'] == 'tab' %} {# start new row for glpi object #}
{% endif %} - {{ macros.dropdownArrayField(itemtype_prefix ~ name, value.itemtype|default(''), field['allowed_values'], label, field_options|merge({ + {{ macros.dropdownArrayField(itemtype_input_name, value.itemtype|default(''), field['allowed_values'], label, field_options|merge({ 'rand': rand, 'display_emptychoice': true, })) }} -
+ + {% set items_id_container_class = ['col-12'] %} + {% if item is instanceof('Glpi\\Form\\Form') %} + {% set items_id_container_class = items_id_container_class|merge(['col-sm-6']) %} + {% else %} + {% set items_id_container_class = items_id_container_class|merge(['form-field row mb-2']) %} + {% if container.fields['type'] == 'tab' %} + {% set items_id_container_class = items_id_container_class|merge(['col-sm-6']) %} + {% endif %} + {% endif %} +
{% do call('Ajax::updateItemOnSelectEvent', [ - 'dropdown_' ~ itemtype_prefix ~ name ~ rand, + 'dropdown_' ~ itemtype_input_name ~ rand, 'results_items_id' ~ (rand), config('root_doc') ~ '/ajax/dropdownAllItems.php', { 'idtable' : '__VALUE__', - 'name' : items_id_prefix ~ name, + 'name' : items_id_input_name, 'entity_restrict' : item.getEntityID(), - 'dom_name' : items_id_prefix ~ name, + 'dom_name' : items_id_input_name, 'display_emptychoice' : false, 'action' : 'get_items_from_itemtype', 'dom_rand' : rand, @@ -163,14 +186,16 @@ } ]) %} - {# fake label for DOM disposition #} - + {% if item is not instanceof('Glpi\\Form\\Form') %} + {# fake label for DOM disposition #} + -
- +
+ {% endif %} + {% if value.itemtype|default('') != '' %} - {{ macros.dropdownField(value.itemtype, items_id_prefix ~ name, value.items_id|default(''), ' ', field_options|merge({ + {{ macros.dropdownField(value.itemtype, items_id_input_name, value.items_id|default(''), ' ', field_options|merge({ 'entity': value.itemtype|default('') == 'User' ? -1 : item.getEntityID(), 'rand': rand, 'right': 'all', @@ -179,8 +204,10 @@ 'no_label': true })) }} {% endif %} - -
+
+ {% if item is not instanceof('Glpi\\Form\\Form') %} +
+ {% endif %}
{% endif %} {% endif %} diff --git a/templates/question_type_administration.html.twig b/templates/question_type_administration.html.twig new file mode 100644 index 00000000..538e491d --- /dev/null +++ b/templates/question_type_administration.html.twig @@ -0,0 +1,101 @@ +{# + # ------------------------------------------------------------------------- + # Fields plugin for GLPI + # ------------------------------------------------------------------------- + # + # LICENSE + # + # This file is part of Fields. + # + # Fields is free software; you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation; either version 2 of the License, or + # (at your option) any later version. + # + # Fields is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with Fields. If not, see . + # ------------------------------------------------------------------------- + # @copyright Copyright (C) 2013-2023 by Fields plugin team. + # @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + # @link https://github.com/pluginsGLPI/fields + # ------------------------------------------------------------------------- + #} + +{% import 'components/form/fields_macros.html.twig' as fields %} + +{% set rand = random() %} + +{% set is_ajax_reload = is_ajax_reload|default(false) %} + +{% set field = field|merge({ + 'default_value': default_value ?? field.default_value +}) %} + +{{ call('PluginFieldsField::prepareHtmlFields', [ + [field], + item, + true, + true, + false, + { + 'input_name' : 'default_value', + 'full_width' : not (field.type starts with 'dropdown' or field.type == 'glpi_item'), + 'no_label' : true, + 'mb' : '', + 'init' : is_ajax_reload or (question is not null), + 'add_field_class': 'glpi-fields-plugin-question-type-glpi-item-field', + 'comments' : false + } +])|raw }} + +{{ fields.dropdownArrayField( + 'field_id', + selected_field_id, + available_fields, + '', + { + 'no_label' : true, + 'field_class' : '', + 'class' : 'form-select form-select-sm', + 'mb' : '', + 'init' : is_ajax_reload or (question is not null), + 'add_data_attributes': { + 'glpi-form-editor-specific-question-extra-data' : '', + 'glpi-form-editor-question-type-fields-field-id-selector': rand + } + } +) }} + +{% if not is_ajax_reload %} + +{% else %} + +{% endif %} diff --git a/templates/question_type_end_user.html.twig b/templates/question_type_end_user.html.twig new file mode 100644 index 00000000..39be6586 --- /dev/null +++ b/templates/question_type_end_user.html.twig @@ -0,0 +1,51 @@ +{# + # ------------------------------------------------------------------------- + # Fields plugin for GLPI + # ------------------------------------------------------------------------- + # + # LICENSE + # + # This file is part of Fields. + # + # Fields is free software; you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation; either version 2 of the License, or + # (at your option) any later version. + # + # Fields is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with Fields. If not, see . + # ------------------------------------------------------------------------- + # @copyright Copyright (C) 2013-2023 by Fields plugin team. + # @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + # @link https://github.com/pluginsGLPI/fields + # ------------------------------------------------------------------------- + #} + +{% import 'components/form/fields_macros.html.twig' as fields %} + +{% set field = field|merge({ + 'default_value': default_value ?? field.default_value +}) %} + +
+ {{ call('PluginFieldsField::prepareHtmlFields', [ + [field], + item, + true, + true, + false, + { + 'input_name' : question.getEndUserInputName(), + 'full_width' : 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', + 'comments' : false + } + ])|raw }} +
diff --git a/tests/FieldTestCase.php b/tests/FieldTestCase.php new file mode 100644 index 00000000..5322a600 --- /dev/null +++ b/tests/FieldTestCase.php @@ -0,0 +1,64 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +namespace GlpiPlugin\Field\Tests; + +use DBmysql; +use DbTestCase; +use PluginFieldsContainer; + +abstract class FieldTestCase extends DbTestCase +{ + private static array $createdContainers = []; + + public function tearDown(): void + { + // Clean created containers + array_map( + fn(PluginFieldsContainer $container) => $container->delete($container->fields, true), + self::$createdContainers, + ); + self::$createdContainers = []; + + /** @var DBmysql $DB */ + global $DB; + $DB->clearSchemaCache(); + + parent::tearDown(); + } + + public function createFieldContainer(array $inputs): PluginFieldsContainer + { + $container = $this->createItem(PluginFieldsContainer::class, $inputs, ['itemtypes']); + self::$createdContainers[] = $container; + + return $container; + } +} diff --git a/tests/QuestionTypeTestCase.php b/tests/QuestionTypeTestCase.php new file mode 100644 index 00000000..5cb76d20 --- /dev/null +++ b/tests/QuestionTypeTestCase.php @@ -0,0 +1,127 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +namespace GlpiPlugin\Field\Tests; + +use Glpi\Controller\Form\RendererController; +use Glpi\Form\Form; +use Glpi\Form\Migration\TypesConversionMapper; +use Glpi\Form\QuestionType\QuestionTypesManager; +use Glpi\Tests\FormTesterTrait; +use PluginFieldsContainer; +use PluginFieldsField; +use ReflectionClass; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\HttpFoundation\Request; +use Ticket; + +abstract class QuestionTypeTestCase extends FieldTestCase +{ + use FormTesterTrait; + + protected ?PluginFieldsContainer $block = null; + protected ?PluginFieldsField $field = null; + + public function createFieldAndContainer(): void + { + // Arrange: create block and field + $this->block = $this->createFieldContainer([ + 'label' => 'Tickets Fields', + 'itemtypes' => [Ticket::class], + 'type' => 'dom', + 'is_active' => 1, + 'entities_id' => $this->getTestRootEntity(true), + ]); + + $this->field = $this->createItem(PluginFieldsField::class, [ + 'label' => 'GLPI Item', + 'type' => 'glpi_item', + PluginFieldsContainer::getForeignKeyField() => $this->block->getID(), + 'ranking' => 2, + 'is_active' => 1, + ]); + + // Register plugin question types + plugin_fields_register_plugin_types(); + } + + public function setUp(): void + { + parent::setUp(); + + // Delete form related single instances + $this->deleteSingletonInstance([ + QuestionTypesManager::class, + TypesConversionMapper::class, + ]); + + // Login + $this->login(); + } + + protected function renderFormEditor(Form $form): Crawler + { + $this->login(); + ob_start(); + (new Form())->showForm($form->getId()); + return new Crawler(ob_get_clean()); + } + + protected function renderHelpdeskForm(Form $form): Crawler + { + $this->login(); + $controller = new RendererController(); + $response = $controller->__invoke( + Request::create( + '', + 'GET', + [ + 'id' => $form->getID(), + ], + ), + ); + return new Crawler($response->getContent()); + } + + private function deleteSingletonInstance(array $classes) + { + foreach ($classes as $class) { + $reflection_class = new ReflectionClass($class); + if ($reflection_class->hasProperty('instance')) { + $reflection_property = $reflection_class->getProperty('instance'); + $reflection_property->setValue(null, null); + } + if ($reflection_class->hasProperty('_instances')) { + $reflection_property = $reflection_class->getProperty('_instances'); + $reflection_property->setValue(null, []); + } + } + } +} diff --git a/tests/Units/FieldQuestionTypeMigrationTest.php b/tests/Units/FieldQuestionTypeMigrationTest.php new file mode 100644 index 00000000..3732b0e0 --- /dev/null +++ b/tests/Units/FieldQuestionTypeMigrationTest.php @@ -0,0 +1,133 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +namespace GlpiPlugin\Field\Tests\Units; + +use Glpi\Form\AccessControl\FormAccessControlManager; +use Glpi\Form\Migration\FormMigration; +use Glpi\Form\Question; +use Glpi\Migration\PluginMigrationResult; +use GlpiPlugin\Field\Tests\QuestionTypeTestCase; +use PluginFieldsQuestionType; + +final class FieldQuestionTypeMigrationTest extends QuestionTypeTestCase +{ + public static function setUpBeforeClass(): void + { + global $DB; + + parent::setUpBeforeClass(); + + $tables = $DB->listTables('glpi\_plugin\_formcreator\_%'); + foreach ($tables as $table) { + $DB->dropTable($table['TABLE_NAME']); + } + + $queries = $DB->getQueriesFromFile(sprintf( + '%s/plugins/fields/tests/fixtures/formcreator.sql', + GLPI_ROOT, + )); + foreach ($queries as $query) { + $DB->doQuery($query); + } + } + + public static function tearDownAfterClass(): void + { + global $DB; + + $tables = $DB->listTables('glpi\_plugin\_formcreator\_%'); + foreach ($tables as $table) { + $DB->dropTable($table['TABLE_NAME']); + } + + parent::tearDownAfterClass(); + } + + public function testFieldsQuestionIsMigrated(): void + { + global $DB; + + $question_name = 'GLPI item fields question'; + + // Arrange: create block and field + $this->createFieldAndContainer(); + + // Create a form + $this->assertTrue($DB->insert( + 'glpi_plugin_formcreator_forms', + [ + 'name' => $question_name, + ], + )); + $form_id = $DB->insertId(); + + // Insert a section for the form + $this->assertTrue($DB->insert( + 'glpi_plugin_formcreator_sections', + [ + 'plugin_formcreator_forms_id' => $form_id, + ], + )); + + $section_id = $DB->insertId(); + + // Insert a question for the form + $this->assertTrue($DB->insert( + 'glpi_plugin_formcreator_questions', + [ + 'name' => $question_name, + 'plugin_formcreator_sections_id' => $section_id, + 'fieldtype' => 'fields', + 'row' => 0, + 'col' => 0, + 'values' => json_encode([ + 'dropdown_fields_field' => $this->field->fields['name'], + 'blocks_field' => $this->block->getID(), + ]), + ], + )); + + // Process migration + $migration = new FormMigration($DB, FormAccessControlManager::getInstance()); + $this->setPrivateProperty($migration, 'result', new PluginMigrationResult()); + $this->assertTrue($this->callPrivateMethod($migration, 'processMigration')); + + // Verify that the question has been migrated correctly + /** @var Question $question */ + $question = getItemByTypeName(Question::class, $question_name); + $question_type = $question->getQuestionType(); + $this->assertInstanceOf(PluginFieldsQuestionType::class, $question_type); + + // Delete created items + $form = $question->getForm(); + $form->delete($form->fields, true); + } +} diff --git a/tests/Units/FieldQuestionTypeTest.php b/tests/Units/FieldQuestionTypeTest.php new file mode 100644 index 00000000..20f1fc26 --- /dev/null +++ b/tests/Units/FieldQuestionTypeTest.php @@ -0,0 +1,158 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +namespace GlpiPlugin\Field\Tests\Units; + +use Glpi\Form\QuestionType\QuestionTypesManager; +use Glpi\Tests\FormBuilder; +use GlpiPlugin\Field\Tests\QuestionTypeTestCase; +use PluginFieldsQuestionType; +use PluginFieldsQuestionTypeCategory; +use PluginFieldsQuestionTypeExtraDataConfig; + +use function Safe\json_encode; + +final class FieldQuestionTypeTest extends QuestionTypeTestCase +{ + public function testFieldsQuestionCategoryIsAvailableWhenValidFieldExists(): void + { + // Arrange: create block and field + $this->createFieldAndContainer(); + + // Act: get enabled question type categories + $manager = QuestionTypesManager::getInstance(); + $categories = $manager->getCategories(); + + // Assert: check that Field question type category is registered + $this->assertContains( + PluginFieldsQuestionTypeCategory::class, + array_map(fn($category) => get_class($category), $categories), + ); + } + + public function testFieldsQuestionCategoryIsNotAvailableWhenNoValidFieldExists(): void + { + // Act: get enabled question type categories + $manager = QuestionTypesManager::getInstance(); + $categories = $manager->getCategories(); + + // Assert: check that Field question type category isn't registered + $this->assertNotContains( + PluginFieldsQuestionTypeCategory::class, + array_map(fn($category) => get_class($category), $categories), + ); + } + + public function testFieldsQuestionIsAvailableWhenValidFieldExists(): void + { + // Arrange: create block and field + $this->createFieldAndContainer(); + + // Act: get enabled question types + $manager = QuestionTypesManager::getInstance(); + $types = $manager->getQuestionTypes(); + + // Assert: check that Field question type is registered + $this->assertContains( + PluginFieldsQuestionType::class, + array_map(fn($type) => get_class($type), $types), + ); + } + + public function testFieldsQuestionIsNotAvailableWhenNoValidFieldExists(): void + { + // Act: get enabled question types + $manager = QuestionTypesManager::getInstance(); + $types = $manager->getQuestionTypes(); + + // Assert: check that Field question type isn't registered + $this->assertNotContains( + PluginFieldsQuestionType::class, + array_map(fn($type) => get_class($type), $types), + ); + } + + public function testFieldsQuestionEditorRendering(): void + { + // Arrange: create field and container + $this->createFieldAndContainer(); + + // Arrange: create form with Field question + $builder = new FormBuilder("My form"); + $builder->addQuestion( + "My question", + PluginFieldsQuestionType::class, + extra_data: json_encode($this->getFieldExtraDataConfig()), + ); + $form = $this->createForm($builder); + + // Act: render form editor + $crawler = $this->renderFormEditor($form); + + // Assert: item was rendered + $this->assertNotEmpty($crawler->filter('.form-editor-container [data-glpi-form-editor-question] .glpi-fields-plugin-question-type-glpi-item-field')); + + // Cleanup + $form->delete($form->fields, true); + } + + public function testFieldsQuestionHelpdeskRendering(): void + { + // Arrange: create field and container + $this->createFieldAndContainer(); + + // Arrange: create form with Field question + $builder = new FormBuilder("My form"); + $builder->addQuestion( + "My question", + PluginFieldsQuestionType::class, + extra_data: json_encode($this->getFieldExtraDataConfig()), + ); + $form = $this->createForm($builder); + + // Act: render helpdesk form + $crawler = $this->renderHelpdeskForm($form); + + // Assert: item was rendered + $this->assertNotEmpty($crawler->filter('[data-glpi-form-renderer-fields-question-type-specific-container]')); + + // Cleanup + $form->delete($form->fields, true); + } + + private function getFieldExtraDataConfig(): PluginFieldsQuestionTypeExtraDataConfig + { + if ($this->block === null || $this->field === null) { + throw new \LogicException("Field and container must be created before getting extra data config"); + } + + return new PluginFieldsQuestionTypeExtraDataConfig($this->block->getID(), $this->field->getID()); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..41ae4189 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,38 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +require __DIR__ . '/../../../tests/bootstrap.php'; + +if (!Plugin::isPluginActive("fields")) { + throw new RuntimeException("Plugin fields is not active in the test database"); +} + +require_once __DIR__ . '/FieldTestCase.php'; +require_once __DIR__ . '/QuestionTypeTestCase.php'; diff --git a/tests/fixtures/formcreator.sql b/tests/fixtures/formcreator.sql new file mode 100644 index 00000000..2c4ef61f --- /dev/null +++ b/tests/fixtures/formcreator.sql @@ -0,0 +1,469 @@ +-- +-- ------------------------------------------------------------------------- +-- Fields plugin for GLPI +-- ------------------------------------------------------------------------- +-- +-- LICENSE +-- +-- This file is part of Fields. +-- +-- Fields is free software; you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation; either version 2 of the License, or +-- (at your option) any later version. +-- +-- Fields is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with Fields. If not, see . +-- ------------------------------------------------------------------------- +-- @copyright Copyright (C) 2013-2023 by Fields plugin team. +-- @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html +-- @link https://github.com/pluginsGLPI/fields +-- ------------------------------------------------------------------------- +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_categories`; +CREATE TABLE `glpi_plugin_formcreator_categories` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `comment` mediumtext COLLATE utf8mb4_unicode_ci, + `completename` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `plugin_formcreator_categories_id` int unsigned NOT NULL DEFAULT '0', + `level` int NOT NULL DEFAULT '1', + `sons_cache` longtext COLLATE utf8mb4_unicode_ci, + `ancestors_cache` longtext COLLATE utf8mb4_unicode_ci, + `knowbaseitemcategories_id` int unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `name` (`name`), + KEY `knowbaseitemcategories_id` (`knowbaseitemcategories_id`), + KEY `plugin_formcreator_categories_id` (`plugin_formcreator_categories_id`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `glpi_plugin_formcreator_questions` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_questions`; +CREATE TABLE `glpi_plugin_formcreator_questions` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `plugin_formcreator_sections_id` int unsigned NOT NULL DEFAULT '0', + `fieldtype` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'text', + `required` tinyint(1) NOT NULL DEFAULT '0', + `show_empty` tinyint(1) NOT NULL DEFAULT '0', + `default_values` mediumtext COLLATE utf8mb4_unicode_ci, + `itemtype` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'itemtype used for glpi objects and dropdown question types', + `values` mediumtext COLLATE utf8mb4_unicode_ci, + `description` mediumtext COLLATE utf8mb4_unicode_ci, + `row` int NOT NULL DEFAULT '0', + `col` int NOT NULL DEFAULT '0', + `width` int NOT NULL DEFAULT '0', + `show_rule` int NOT NULL DEFAULT '1', + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `plugin_formcreator_sections_id` (`plugin_formcreator_sections_id`), + FULLTEXT KEY `Search` (`name`,`description`) +) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `glpi_plugin_formcreator_sections` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_sections`; +CREATE TABLE `glpi_plugin_formcreator_sections` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `plugin_formcreator_forms_id` int unsigned NOT NULL DEFAULT '0', + `order` int NOT NULL DEFAULT '0', + `show_rule` int NOT NULL DEFAULT '1', + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `plugin_formcreator_forms_id` (`plugin_formcreator_forms_id`) +) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `glpi_plugin_formcreator_forms` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_forms`; +CREATE TABLE `glpi_plugin_formcreator_forms` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `entities_id` int unsigned NOT NULL DEFAULT '0', + `is_recursive` tinyint(1) NOT NULL DEFAULT '0', + `icon` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `icon_color` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `background_color` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `access_rights` tinyint(1) NOT NULL DEFAULT '1', + `description` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `content` longtext COLLATE utf8mb4_unicode_ci, + `plugin_formcreator_categories_id` int unsigned NOT NULL DEFAULT '0', + `is_active` tinyint(1) NOT NULL DEFAULT '0', + `language` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `helpdesk_home` tinyint(1) NOT NULL DEFAULT '0', + `is_deleted` tinyint(1) NOT NULL DEFAULT '0', + `validation_required` tinyint(1) NOT NULL DEFAULT '0', + `usage_count` int NOT NULL DEFAULT '0', + `is_default` tinyint(1) NOT NULL DEFAULT '0', + `is_captcha_enabled` tinyint(1) NOT NULL DEFAULT '0', + `show_rule` int NOT NULL DEFAULT '1' COMMENT 'Conditions setting to show the submit button', + `formanswer_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `is_visible` tinyint NOT NULL DEFAULT '1', + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `entities_id` (`entities_id`), + KEY `plugin_formcreator_categories_id` (`plugin_formcreator_categories_id`), + FULLTEXT KEY `Search` (`name`,`description`) +) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `glpi_plugin_formcreator_targettickets` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_targettickets`; +CREATE TABLE `glpi_plugin_formcreator_targettickets` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `plugin_formcreator_forms_id` int unsigned NOT NULL DEFAULT '0', + `target_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `source_rule` int NOT NULL DEFAULT '0', + `source_question` int NOT NULL DEFAULT '0', + `type_rule` int NOT NULL DEFAULT '0', + `type_question` int unsigned NOT NULL DEFAULT '0', + `tickettemplates_id` int unsigned NOT NULL DEFAULT '0', + `content` longtext COLLATE utf8mb4_unicode_ci, + `due_date_rule` int NOT NULL DEFAULT '1', + `due_date_question` int unsigned NOT NULL DEFAULT '0', + `due_date_value` tinyint DEFAULT NULL, + `due_date_period` int NOT NULL DEFAULT '0', + `urgency_rule` int NOT NULL DEFAULT '1', + `urgency_question` int unsigned NOT NULL DEFAULT '0', + `validation_followup` tinyint(1) NOT NULL DEFAULT '1', + `destination_entity` int NOT NULL DEFAULT '1', + `destination_entity_value` int unsigned NOT NULL DEFAULT '0', + `tag_type` int NOT NULL DEFAULT '1', + `tag_questions` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `tag_specifics` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `category_rule` int NOT NULL DEFAULT '1', + `category_question` int unsigned NOT NULL DEFAULT '0', + `associate_rule` int NOT NULL DEFAULT '1', + `associate_question` int unsigned NOT NULL DEFAULT '0', + `location_rule` int NOT NULL DEFAULT '1', + `location_question` int unsigned NOT NULL DEFAULT '0', + `commonitil_validation_rule` int NOT NULL DEFAULT '1', + `commonitil_validation_question` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `show_rule` int NOT NULL DEFAULT '1', + `sla_rule` int NOT NULL DEFAULT '1', + `sla_question_tto` int unsigned NOT NULL DEFAULT '0', + `sla_question_ttr` int unsigned NOT NULL DEFAULT '0', + `ola_rule` int NOT NULL DEFAULT '1', + `ola_question_tto` int unsigned NOT NULL DEFAULT '0', + `ola_question_ttr` int unsigned NOT NULL DEFAULT '0', + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `tickettemplates_id` (`tickettemplates_id`) +) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `glpi_plugin_formcreator_targets_actors` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_targets_actors`; +CREATE TABLE `glpi_plugin_formcreator_targets_actors` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `itemtype` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `items_id` int unsigned NOT NULL DEFAULT '0', + `actor_role` int NOT NULL DEFAULT '1', + `actor_type` int NOT NULL DEFAULT '1', + `actor_value` int unsigned NOT NULL DEFAULT '0', + `use_notification` tinyint(1) NOT NULL DEFAULT '1', + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `item` (`itemtype`,`items_id`) +) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `glpi_plugin_formcreator_targetchanges` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_targetchanges`; +CREATE TABLE `glpi_plugin_formcreator_targetchanges` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `plugin_formcreator_forms_id` int unsigned NOT NULL DEFAULT '0', + `target_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `changetemplates_id` int unsigned NOT NULL DEFAULT '0', + `content` longtext COLLATE utf8mb4_unicode_ci, + `impactcontent` longtext COLLATE utf8mb4_unicode_ci, + `controlistcontent` longtext COLLATE utf8mb4_unicode_ci, + `rolloutplancontent` longtext COLLATE utf8mb4_unicode_ci, + `backoutplancontent` longtext COLLATE utf8mb4_unicode_ci, + `checklistcontent` longtext COLLATE utf8mb4_unicode_ci, + `due_date_rule` int NOT NULL DEFAULT '1', + `due_date_question` int unsigned NOT NULL DEFAULT '0', + `due_date_value` tinyint DEFAULT NULL, + `due_date_period` int NOT NULL DEFAULT '0', + `urgency_rule` int NOT NULL DEFAULT '1', + `urgency_question` int unsigned NOT NULL DEFAULT '0', + `validation_followup` tinyint(1) NOT NULL DEFAULT '1', + `destination_entity` int NOT NULL DEFAULT '1', + `destination_entity_value` int unsigned NOT NULL DEFAULT '0', + `tag_type` int NOT NULL DEFAULT '1', + `tag_questions` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `tag_specifics` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `category_rule` int NOT NULL DEFAULT '1', + `category_question` int unsigned NOT NULL DEFAULT '0', + `commonitil_validation_rule` int NOT NULL DEFAULT '1', + `commonitil_validation_question` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `show_rule` int NOT NULL DEFAULT '1', + `sla_rule` int NOT NULL DEFAULT '1', + `sla_question_tto` int unsigned NOT NULL DEFAULT '0', + `sla_question_ttr` int unsigned NOT NULL DEFAULT '0', + `ola_rule` int NOT NULL DEFAULT '1', + `ola_question_tto` int unsigned NOT NULL DEFAULT '0', + `ola_question_ttr` int unsigned NOT NULL DEFAULT '0', + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `glpi_plugin_formcreator_targetproblems` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_targetproblems`; +CREATE TABLE `glpi_plugin_formcreator_targetproblems` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `plugin_formcreator_forms_id` int unsigned NOT NULL DEFAULT '0', + `target_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `problemtemplates_id` int unsigned NOT NULL DEFAULT '0', + `content` longtext COLLATE utf8mb4_unicode_ci, + `impactcontent` longtext COLLATE utf8mb4_unicode_ci, + `causecontent` longtext COLLATE utf8mb4_unicode_ci, + `symptomcontent` longtext COLLATE utf8mb4_unicode_ci, + `urgency_rule` int NOT NULL DEFAULT '1', + `urgency_question` int unsigned NOT NULL DEFAULT '0', + `destination_entity` int NOT NULL DEFAULT '1', + `destination_entity_value` int unsigned NOT NULL DEFAULT '0', + `tag_type` int NOT NULL DEFAULT '1', + `tag_questions` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `tag_specifics` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `category_rule` int NOT NULL DEFAULT '1', + `category_question` int unsigned NOT NULL DEFAULT '0', + `show_rule` int NOT NULL DEFAULT '1', + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `problemtemplates_id` (`problemtemplates_id`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `glpi_plugin_formcreator_forms_users` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_forms_users`; +CREATE TABLE `glpi_plugin_formcreator_forms_users` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `plugin_formcreator_forms_id` int unsigned NOT NULL, + `users_id` int unsigned NOT NULL, + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `unicity` (`plugin_formcreator_forms_id`,`users_id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `glpi_plugin_formcreator_forms_groups` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_forms_groups`; +CREATE TABLE `glpi_plugin_formcreator_forms_groups` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `plugin_formcreator_forms_id` int unsigned NOT NULL, + `groups_id` int unsigned NOT NULL, + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `unicity` (`plugin_formcreator_forms_id`,`groups_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Dumping data for table `glpi_plugin_formcreator_forms_groups` +-- + +LOCK TABLES `glpi_plugin_formcreator_forms_groups` WRITE; +UNLOCK TABLES; + +-- +-- Table structure for table `glpi_plugin_formcreator_forms_profiles` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_forms_profiles`; +CREATE TABLE `glpi_plugin_formcreator_forms_profiles` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `plugin_formcreator_forms_id` int unsigned NOT NULL DEFAULT '0', + `profiles_id` int unsigned NOT NULL DEFAULT '0', + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `unicity` (`plugin_formcreator_forms_id`,`profiles_id`) +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `glpi_plugin_formcreator_items_targettickets` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_items_targettickets`; +CREATE TABLE `glpi_plugin_formcreator_items_targettickets` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `plugin_formcreator_targettickets_id` int unsigned NOT NULL DEFAULT '0', + `link` int NOT NULL DEFAULT '0', + `itemtype` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `items_id` int unsigned NOT NULL DEFAULT '0', + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `plugin_formcreator_targettickets_id` (`plugin_formcreator_targettickets_id`), + KEY `item` (`itemtype`,`items_id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Dumping data for table `glpi_plugin_formcreator_items_targettickets` +-- + +LOCK TABLES `glpi_plugin_formcreator_items_targettickets` WRITE; +UNLOCK TABLES; + +-- +-- Table structure for table `glpi_plugin_formcreator_forms_profiles` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_forms_profiles`; +CREATE TABLE `glpi_plugin_formcreator_forms_profiles` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `plugin_formcreator_forms_id` int unsigned NOT NULL DEFAULT '0', + `profiles_id` int unsigned NOT NULL DEFAULT '0', + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `unicity` (`plugin_formcreator_forms_id`,`profiles_id`) +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `glpi_plugin_formcreator_forms_groups` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_forms_groups`; +CREATE TABLE `glpi_plugin_formcreator_forms_groups` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `plugin_formcreator_forms_id` int unsigned NOT NULL, + `groups_id` int unsigned NOT NULL, + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `unicity` (`plugin_formcreator_forms_id`,`groups_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Dumping data for table `glpi_plugin_formcreator_forms_groups` +-- + +LOCK TABLES `glpi_plugin_formcreator_forms_groups` WRITE; +UNLOCK TABLES; + +-- +-- Table structure for table `glpi_plugin_formcreator_forms_users` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_forms_users`; +CREATE TABLE `glpi_plugin_formcreator_forms_users` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `plugin_formcreator_forms_id` int unsigned NOT NULL, + `users_id` int unsigned NOT NULL, + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `unicity` (`plugin_formcreator_forms_id`,`users_id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `glpi_plugin_formcreator_forms_languages` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_forms_languages`; +CREATE TABLE `glpi_plugin_formcreator_forms_languages` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `plugin_formcreator_forms_id` int unsigned NOT NULL DEFAULT '0', + `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `comment` text COLLATE utf8mb4_unicode_ci, + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `glpi_plugin_formcreator_conditions` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_conditions`; +CREATE TABLE `glpi_plugin_formcreator_conditions` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `itemtype` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'itemtype of the item affected by the condition', + `items_id` int unsigned NOT NULL DEFAULT '0' COMMENT 'item ID of the item affected by the condition', + `plugin_formcreator_questions_id` int unsigned NOT NULL DEFAULT '0' COMMENT 'question to test for the condition', + `show_condition` int NOT NULL DEFAULT '0', + `show_value` mediumtext COLLATE utf8mb4_unicode_ci, + `show_logic` int NOT NULL DEFAULT '1', + `order` int NOT NULL DEFAULT '1', + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `plugin_formcreator_questions_id` (`plugin_formcreator_questions_id`), + KEY `item` (`itemtype`,`items_id`) +) ENGINE=InnoDB AUTO_INCREMENT=825 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `glpi_plugin_formcreator_questionranges` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_questionranges`; +CREATE TABLE `glpi_plugin_formcreator_questionranges` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `plugin_formcreator_questions_id` int unsigned NOT NULL DEFAULT '0', + `range_min` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `range_max` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `fieldname` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `plugin_formcreator_questions_id` (`plugin_formcreator_questions_id`) +) ENGINE=InnoDB AUTO_INCREMENT=304 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- +-- Table structure for table `glpi_plugin_formcreator_questionregexes` +-- + +DROP TABLE IF EXISTS `glpi_plugin_formcreator_questionregexes`; +CREATE TABLE `glpi_plugin_formcreator_questionregexes` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `plugin_formcreator_questions_id` int unsigned NOT NULL DEFAULT '0', + `regex` mediumtext COLLATE utf8mb4_unicode_ci, + `fieldname` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `plugin_formcreator_questions_id` (`plugin_formcreator_questions_id`) +) ENGINE=InnoDB AUTO_INCREMENT=297 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `glpi_plugin_formcreator_entityconfigs` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `entities_id` int(10) unsigned NOT NULL DEFAULT 0, + `replace_helpdesk` int(11) NOT NULL DEFAULT -2, + `default_form_list_mode` int(11) NOT NULL DEFAULT -2, + `sort_order` int(11) NOT NULL DEFAULT -2, + `is_kb_separated` int(11) NOT NULL DEFAULT -2, + `is_search_visible` int(11) NOT NULL DEFAULT -2, + `is_dashboard_visible` int(11) NOT NULL DEFAULT -2, + `is_header_visible` int(11) NOT NULL DEFAULT -2, + `is_search_issue_visible` int(11) NOT NULL DEFAULT -2, + `tile_design` int(11) NOT NULL DEFAULT -2, + `home_page` int(11) NOT NULL DEFAULT -2, + `is_category_visible` int(11) NOT NULL DEFAULT -2, + `is_folded_menu` int(11) NOT NULL DEFAULT -2, + `header` text DEFAULT NULL, + `service_catalog_home` int(11) NOT NULL DEFAULT -2, + PRIMARY KEY (`id`), + UNIQUE KEY `unicity` (`entities_id`) +) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC From b940ff17c0ad0053f75926958dea2070111234c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Cailly?= Date: Mon, 27 Oct 2025 14:44:06 +0100 Subject: [PATCH 2/4] fix: Rector lint --- inc/questiontype.class.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/inc/questiontype.class.php b/inc/questiontype.class.php index 14967311..c3cbc474 100644 --- a/inc/questiontype.class.php +++ b/inc/questiontype.class.php @@ -103,11 +103,7 @@ public function validateExtraDataInput(array $input): bool // Check if the field_id exists in the selected block $available_fields = $this->getFieldsFromBlock($input['block_id']); - if (!isset($available_fields[$input['field_id']])) { - return false; - } - - return true; + return isset($available_fields[$input['field_id']]); } #[Override] @@ -361,9 +357,9 @@ private function getFieldsFromBlock(?int $block_id): array public static function hasAvailableFields(): bool { $blocks = (new self())->getAvailableBlocks(); - foreach ($blocks as $block_id => $block_label) { + foreach (array_keys($blocks) as $block_id) { $fields = (new self())->getFieldsFromBlock($block_id); - if (!empty($fields)) { + if ($fields !== []) { return true; } } From fae9c9b95defba8a4df7f62490e7c48b0b0634c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Cailly?= Date: Mon, 27 Oct 2025 14:45:08 +0100 Subject: [PATCH 3/4] fix: Twig lint --- templates/question_type_end_user.html.twig | 2 -- 1 file changed, 2 deletions(-) diff --git a/templates/question_type_end_user.html.twig b/templates/question_type_end_user.html.twig index 39be6586..c06d82ca 100644 --- a/templates/question_type_end_user.html.twig +++ b/templates/question_type_end_user.html.twig @@ -26,8 +26,6 @@ # ------------------------------------------------------------------------- #} -{% import 'components/form/fields_macros.html.twig' as fields %} - {% set field = field|merge({ 'default_value': default_value ?? field.default_value }) %} From 000f7631df46f9c4e72cafd386d693c34b9d2edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Cailly?= Date: Mon, 27 Oct 2025 14:56:11 +0100 Subject: [PATCH 4/4] fix: PHPStan + remove code duplication --- inc/questiontype.class.php | 16 +++++----- src/Controller/QuestionTypeAjaxController.php | 32 +------------------ 2 files changed, 9 insertions(+), 39 deletions(-) diff --git a/inc/questiontype.class.php b/inc/questiontype.class.php index c3cbc474..13427ec1 100644 --- a/inc/questiontype.class.php +++ b/inc/questiontype.class.php @@ -38,7 +38,7 @@ use function Safe\json_decode; use function Safe\json_encode; -class PluginFieldsQuestionType extends AbstractQuestionType implements FormQuestionDataConverterInterface +final class PluginFieldsQuestionType extends AbstractQuestionType implements FormQuestionDataConverterInterface { #[Override] public function getCategory(): QuestionTypeCategoryInterface @@ -47,7 +47,7 @@ public function getCategory(): QuestionTypeCategoryInterface } #[Override] - public function getExtraDataConfigClass(): ?string + public function getExtraDataConfigClass(): string { return PluginFieldsQuestionTypeExtraDataConfig::class; } @@ -71,13 +71,13 @@ public function getSubTypeFieldAriaLabel(): string } #[Override] - public function getSubTypeDefaultValue(?Question $question): ?string + public function getSubTypeDefaultValue(?Question $question): string { return (string) $this->getDefaultValueBlockId($question); } #[Override] - public function formatDefaultValueForDB(mixed $value): ?string + public function formatDefaultValueForDB(mixed $value): string { return json_encode($value); } @@ -102,7 +102,7 @@ public function validateExtraDataInput(array $input): bool } // Check if the field_id exists in the selected block - $available_fields = $this->getFieldsFromBlock($input['block_id']); + $available_fields = self::getFieldsFromBlock($input['block_id']); return isset($available_fields[$input['field_id']]); } @@ -114,7 +114,7 @@ public function renderAdministrationTemplate(?Question $question): string if ($block_id === null) { $block_id = current(array_keys($this->getAvailableBlocks())); } - $available_fields = $this->getFieldsFromBlock($block_id); + $available_fields = self::getFieldsFromBlock($block_id); // Retrieve current field $current_field_id = $this->getDefaultValueFieldId($question); @@ -149,7 +149,7 @@ public function renderEndUserTemplate(Question $question): string if ($block_id === null) { $block_id = current(array_keys($this->getAvailableBlocks())); } - $available_fields = $this->getFieldsFromBlock($block_id); + $available_fields = self::getFieldsFromBlock($block_id); // Retrieve current field $current_field_id = $this->getDefaultValueFieldId($question); @@ -328,7 +328,7 @@ private function getAvailableBlocks(?Form $form = null): array return $available_blocks; } - private function getFieldsFromBlock(?int $block_id): array + public static function getFieldsFromBlock(?int $block_id): array { $fields = []; $field_container = PluginFieldsContainer::getById($block_id); diff --git a/src/Controller/QuestionTypeAjaxController.php b/src/Controller/QuestionTypeAjaxController.php index 6e2fe619..4fea927e 100644 --- a/src/Controller/QuestionTypeAjaxController.php +++ b/src/Controller/QuestionTypeAjaxController.php @@ -57,11 +57,8 @@ public function __invoke(Request $request): Response return new Response('Invalid block_id', Response::HTTP_BAD_REQUEST); } - // Get the question type instance - $question_type = new PluginFieldsQuestionType(); - // Get available fields for the selected block - $available_fields = $this->getFieldsFromBlock((int) $block_id); + $available_fields = PluginFieldsQuestionType::getFieldsFromBlock((int) $block_id); // If field_id is not provided or invalid, use the first available field $current_field_id = null; @@ -106,31 +103,4 @@ public function __invoke(Request $request): Response 'is_ajax_reload' => true, ]); } - - /** - * Get fields from a block - * - * @param int|null $block_id - * @return array - */ - private function getFieldsFromBlock(?int $block_id): array - { - $fields = []; - $field_container = PluginFieldsContainer::getById($block_id); - if ($field_container) { - $field = new PluginFieldsField(); - $result = $field->find([ - 'is_active' => 1, - 'plugin_fields_containers_id' => $block_id, - 'NOT' => [ - ['type' => 'header'], // Exclude headers - ], - ]); - foreach ($result as $id => $data) { - $fields[$id] = $data['label']; - } - } - - return $fields; - } }