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..13427ec1
--- /dev/null
+++ b/inc/questiontype.class.php
@@ -0,0 +1,369 @@
+.
+ * -------------------------------------------------------------------------
+ * @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;
+
+final 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 = self::getFieldsFromBlock($input['block_id']);
+ return isset($available_fields[$input['field_id']]);
+ }
+
+ #[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 = self::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 = self::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;
+ }
+
+ public static 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 (array_keys($blocks) as $block_id) {
+ $fields = (new self())->getFieldsFromBlock($block_id);
+ if ($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..4fea927e
--- /dev/null
+++ b/src/Controller/QuestionTypeAjaxController.php
@@ -0,0 +1,106 @@
+.
+ * -------------------------------------------------------------------------
+ * @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 available fields for the selected block
+ $available_fields = PluginFieldsQuestionType::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,
+ ]);
+ }
+}
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..c06d82ca
--- /dev/null
+++ b/templates/question_type_end_user.html.twig
@@ -0,0 +1,49 @@
+{#
+ # -------------------------------------------------------------------------
+ # 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
+ # -------------------------------------------------------------------------
+ #}
+
+{% 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