diff --git a/phpunit/functional/Glpi/Form/Migration/FormMigrationTest.php b/phpunit/functional/Glpi/Form/Migration/FormMigrationTest.php index d1c6e807555..ae0498811a5 100644 --- a/phpunit/functional/Glpi/Form/Migration/FormMigrationTest.php +++ b/phpunit/functional/Glpi/Form/Migration/FormMigrationTest.php @@ -43,9 +43,15 @@ use Glpi\Form\AccessControl\FormAccessControlManager; use Glpi\Form\Category; use Glpi\Form\Comment; +use Glpi\Form\Condition\ConditionData; +use Glpi\Form\Condition\CreationStrategy; +use Glpi\Form\Condition\LogicOperator; +use Glpi\Form\Condition\ValueOperator; +use Glpi\Form\Condition\VisibilityStrategy; use Glpi\Form\Destination\CommonITILField\ContentField; use Glpi\Form\Destination\CommonITILField\SimpleValueConfig; use Glpi\Form\Destination\CommonITILField\TitleField; +use Glpi\Form\Destination\FormDestination; use Glpi\Form\Destination\FormDestinationTicket; use Glpi\Form\Form; use Glpi\Form\FormTranslation; @@ -71,6 +77,7 @@ use Glpi\Form\QuestionType\QuestionTypeRequestType; use Glpi\Form\QuestionType\QuestionTypeSelectableExtraDataConfig; use Glpi\Form\QuestionType\QuestionTypeShortText; +use Glpi\Form\QuestionType\QuestionTypesManager; use Glpi\Form\QuestionType\QuestionTypeUrgency; use Glpi\Form\Section; use Glpi\Message\MessageType; @@ -1157,4 +1164,628 @@ public function testFormMigrationTagConversion(string $rawContent, string $expec $content_config->getValue() ); } + + public static function provideFormMigrationConditionsForQuestions(): iterable + { + yield 'QuestionTypeShortText - Always visible' => [ + 'text', + 1, + [], + VisibilityStrategy::ALWAYS_VISIBLE + ]; + + yield 'QuestionTypeShortText - Hidden if condition' => [ + 'text', + 3, + [ + [ + 'show_condition' => 1, + 'show_value' => 'Test', + 'show_logic' => 1 + ] + ], + VisibilityStrategy::HIDDEN_IF, + [ + [ + 'value_operator' => ValueOperator::EQUALS, + 'value' => 'Test', + 'logic_operator' => LogicOperator::AND + ] + ] + ]; + + yield 'QuestionTypeShortText - Visible if condition' => [ + 'text', + 2, + [ + [ + 'show_condition' => 1, + 'show_value' => 'Test', + 'show_logic' => 1 + ] + ], + VisibilityStrategy::VISIBLE_IF, + [ + [ + 'value_operator' => ValueOperator::EQUALS, + 'value' => 'Test', + 'logic_operator' => LogicOperator::AND + ] + ] + ]; + + $value_operators = [ + 1 => ValueOperator::EQUALS, + 2 => ValueOperator::NOT_EQUALS, + 3 => ValueOperator::LESS_THAN, + 4 => ValueOperator::GREATER_THAN, + 5 => ValueOperator::LESS_THAN_OR_EQUALS, + 6 => ValueOperator::GREATER_THAN_OR_EQUALS, + 7 => ValueOperator::VISIBLE, + 8 => ValueOperator::NOT_VISIBLE, + 9 => ValueOperator::MATCH_REGEX, + ]; + foreach ($value_operators as $key => $value_operator) { + yield 'QuestionTypeShortText - Visible if condition with value operator ' . $value_operator->getLabel() => [ + 'text', + 2, + [ + [ + 'show_condition' => $key, + 'show_value' => 'Test', + 'show_logic' => 1 + ] + ], + VisibilityStrategy::VISIBLE_IF, + [ + [ + 'value_operator' => $value_operator, + 'value' => 'Test', + 'logic_operator' => LogicOperator::AND + ] + ] + ]; + } + + yield 'QuestionTypeShortText - Visible if multiple conditions' => [ + 'text', + 2, + [ + [ + 'show_condition' => 1, + 'show_value' => 'Test', + 'show_logic' => 1 + ], + [ + 'show_condition' => 2, + 'show_value' => 'Test2', + 'show_logic' => 2 + ] + ], + VisibilityStrategy::VISIBLE_IF, + [ + [ + 'value_operator' => ValueOperator::EQUALS, + 'value' => 'Test', + 'logic_operator' => LogicOperator::AND + ], + [ + 'value_operator' => ValueOperator::NOT_EQUALS, + 'value' => 'Test2', + 'logic_operator' => LogicOperator::OR + ] + ] + ]; + + yield 'QuestionTypeRadio - Visible if' => [ + 'field_type' => 'radios', + 'show_rule' => 2, + 'conditions' => [ + [ + 'show_condition' => 1, + 'show_value' => 'Second option', + 'show_logic' => 1 + ] + ], + 'expected_visibility_strategy' => VisibilityStrategy::VISIBLE_IF, + 'expected_conditions' => [ + [ + 'value_operator' => ValueOperator::EQUALS, + 'value' => 1, + 'logic_operator' => LogicOperator::AND + ] + ], + 'values' => '["First option","Second option"]' + ]; + + yield 'QuestionTypeCheckbox - Visible if' => [ + 'field_type' => 'radios', + 'show_rule' => 2, + 'conditions' => [ + [ + 'show_condition' => 1, + 'show_value' => 'Second option', + 'show_logic' => 1 + ], + [ + 'show_condition' => 1, + 'show_value' => 'Third option', + 'show_logic' => 2 + ] + ], + 'expected_visibility_strategy' => VisibilityStrategy::VISIBLE_IF, + 'expected_conditions' => [ + [ + 'value_operator' => ValueOperator::EQUALS, + 'value' => 1, + 'logic_operator' => LogicOperator::AND + ], + [ + 'value_operator' => ValueOperator::EQUALS, + 'value' => 2, + 'logic_operator' => LogicOperator::OR + ] + ], + 'values' => '["First option","Second option","Third option"]' + ]; + + yield 'QuestionTypeDropdown - Visible if' => [ + 'field_type' => 'select', + 'show_rule' => 2, + 'conditions' => [ + [ + 'show_condition' => 1, + 'show_value' => 'Second option', + 'show_logic' => 1 + ], + [ + 'show_condition' => 1, + 'show_value' => 'Third option', + 'show_logic' => 2 + ] + ], + 'expected_visibility_strategy' => VisibilityStrategy::VISIBLE_IF, + 'expected_conditions' => [ + [ + 'value_operator' => ValueOperator::EQUALS, + 'value' => 1, + 'logic_operator' => LogicOperator::AND + ], + [ + 'value_operator' => ValueOperator::EQUALS, + 'value' => 2, + 'logic_operator' => LogicOperator::OR + ] + ], + 'values' => '["First option","Second option","Third option"]' + ]; + } + + #[DataProvider('provideFormMigrationConditionsForQuestions')] + public function testFormMigrationConditionsForQuestions( + string $field_type, + int $show_rule, + array $conditions, + VisibilityStrategy $expected_visibility_strategy, + array $expected_conditions = [], + ?string $values = null + ): void { + /** + * @var \DBmysql $DB + */ + global $DB; + + // Create a form + $this->assertTrue($DB->insert( + 'glpi_plugin_formcreator_forms', + [ + 'name' => 'Test form migration condition for questions', + ] + )); + $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' => 'Test form migration condition for questions - Target question', + 'plugin_formcreator_sections_id' => $section_id, + 'fieldtype' => $field_type, + 'values' => $values, + 'row' => 0, + 'col' => 0, + ] + )); + $target_question_id = $DB->insertId(); + + // Insert another question to apply conditions on + $this->assertTrue($DB->insert( + 'glpi_plugin_formcreator_questions', + [ + 'name' => 'Test form migration condition for questions - Condition question', + 'plugin_formcreator_sections_id' => $section_id, + 'row' => 0, + 'col' => 1, + 'show_rule' => $show_rule, + ] + )); + $condition_question_id = $DB->insertId(); + + // Insert condition if needed + if (!empty($conditions)) { + foreach ($conditions as $condition) { + $this->assertTrue($DB->insert( + 'glpi_plugin_formcreator_conditions', + [ + 'itemtype' => 'PluginFormcreatorQuestion', + 'items_id' => $condition_question_id, + 'plugin_formcreator_questions_id' => $target_question_id, + 'show_condition' => $condition['show_condition'], + 'show_value' => $condition['show_value'], + 'show_logic' => $condition['show_logic'], + ] + )); + } + } + + // Process migration + $migration = new FormMigration($DB, FormAccessControlManager::getInstance()); + $this->setPrivateProperty($migration, 'result', new PluginMigrationResult()); + $this->assertTrue($this->callPrivateMethod($migration, 'processMigration')); + + // Verify that the condition has been migrated correctly + /** @var Form $form */ + $form = getItemByTypeName(Form::class, 'Test form migration condition for questions'); + $this->assertCount(1, $form->getSections()); + $this->assertCount(2, $form->getQuestions()); + + /** @var Question $condition_question */ + $condition_question = getItemByTypeName(Question::class, 'Test form migration condition for questions - Condition question'); + $this->assertNotFalse($condition_question); + $this->assertEquals($expected_visibility_strategy, $condition_question->getConfiguredVisibilityStrategy()); + $this->assertEquals( + $expected_conditions, + array_map( + fn (ConditionData $condition) => [ + 'value_operator' => $condition->getValueOperator(), + 'value' => $condition->getValue(), + 'logic_operator' => $condition->getLogicOperator(), + ], + $condition_question->getConfiguredConditionsData() + ) + ); + } + + public function testFormMigrationConditionsForSections(): void + { + /** + * @var \DBmysql $DB + */ + global $DB; + + // Create a form + $this->assertTrue($DB->insert( + 'glpi_plugin_formcreator_forms', + [ + 'name' => 'Test form migration condition for sections', + ] + )); + $form_id = $DB->insertId(); + + // Insert a section for the form + $this->assertTrue($DB->insert( + 'glpi_plugin_formcreator_sections', + [ + 'name' => 'Test form migration condition for sections - Target section', + 'plugin_formcreator_forms_id' => $form_id + ] + )); + $target_section_id = $DB->insertId(); + + // Insert another section to apply conditions on + $this->assertTrue($DB->insert( + 'glpi_plugin_formcreator_sections', + [ + 'name' => 'Test form migration condition for sections - Condition section', + 'plugin_formcreator_forms_id' => $form_id, + 'show_rule' => 3, + ] + )); + $condition_section_id = $DB->insertId(); + + // Insert a question for the form + $this->assertTrue($DB->insert( + 'glpi_plugin_formcreator_questions', + [ + 'name' => 'Test form migration condition for sections - Target question', + 'plugin_formcreator_sections_id' => $target_section_id, + ] + )); + $target_question_id = $DB->insertId(); + + // Insert condition + $this->assertTrue($DB->insert( + 'glpi_plugin_formcreator_conditions', + [ + 'itemtype' => 'PluginFormcreatorSection', + 'items_id' => $condition_section_id, + 'plugin_formcreator_questions_id' => $target_question_id, + 'show_condition' => 1, + 'show_value' => 'Test', + 'show_logic' => 1, + ] + )); + + // Process migration + $migration = new FormMigration($DB, FormAccessControlManager::getInstance()); + $this->setPrivateProperty($migration, 'result', new PluginMigrationResult()); + $this->assertTrue($this->callPrivateMethod($migration, 'processMigration')); + + // Verify that the condition has been migrated correctly + /** @var Form $form */ + $form = getItemByTypeName(Form::class, 'Test form migration condition for sections'); + $this->assertCount(2, $form->getSections()); + + /** @var Section $condition_section */ + $condition_section = getItemByTypeName(Section::class, 'Test form migration condition for sections - Condition section'); + $this->assertNotFalse($condition_section); + $this->assertEquals(VisibilityStrategy::HIDDEN_IF, $condition_section->getConfiguredVisibilityStrategy()); + $this->assertEquals( + [ + [ + 'value_operator' => ValueOperator::EQUALS, + 'value' => 'Test', + 'logic_operator' => LogicOperator::AND, + ] + ], + array_map( + fn (ConditionData $condition) => [ + 'value_operator' => $condition->getValueOperator(), + 'value' => $condition->getValue(), + 'logic_operator' => $condition->getLogicOperator(), + ], + $condition_section->getConfiguredConditionsData() + ) + ); + } + + public function testFormMigrationConditionsForComments(): void + { + /** + * @var \DBmysql $DB + */ + global $DB; + + // Create a form + $this->assertTrue($DB->insert( + 'glpi_plugin_formcreator_forms', + [ + 'name' => 'Test form migration condition for questions', + ] + )); + $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' => 'Test form migration condition for comments - Target question', + 'plugin_formcreator_sections_id' => $section_id, + ] + )); + $target_question_id = $DB->insertId(); + + // Insert a comment to apply conditions on + $this->assertTrue($DB->insert( + 'glpi_plugin_formcreator_questions', + [ + 'name' => 'Test form migration condition for comments - Condition comment', + 'plugin_formcreator_sections_id' => $section_id, + 'fieldtype' => 'description', + 'show_rule' => 3, + ] + )); + $condition_comment_id = $DB->insertId(); + + // Insert condition + $this->assertTrue($DB->insert( + 'glpi_plugin_formcreator_conditions', + [ + 'itemtype' => 'PluginFormcreatorQuestion', + 'items_id' => $condition_comment_id, + 'plugin_formcreator_questions_id' => $target_question_id, + 'show_condition' => 1, + 'show_value' => 'Test', + 'show_logic' => 1, + ] + )); + + // Process migration + $migration = new FormMigration($DB, FormAccessControlManager::getInstance()); + $this->setPrivateProperty($migration, 'result', new PluginMigrationResult()); + $this->assertTrue($this->callPrivateMethod($migration, 'processMigration')); + + // Verify that the condition has been migrated correctly + /** @var Form $form */ + $form = getItemByTypeName(Form::class, 'Test form migration condition for questions'); + $this->assertCount(1, $form->getSections()); + $this->assertCount(1, $form->getQuestions()); + $this->assertCount(1, $form->getFormComments()); + + /** @var FormComment $condition_comment */ + $condition_comment = getItemByTypeName(Comment::class, 'Test form migration condition for comments - Condition comment'); + $this->assertNotFalse($condition_comment); + $this->assertEquals(VisibilityStrategy::HIDDEN_IF, $condition_comment->getConfiguredVisibilityStrategy()); + $this->assertEquals( + [ + [ + 'value_operator' => ValueOperator::EQUALS, + 'value' => 'Test', + 'logic_operator' => LogicOperator::AND, + ] + ], + array_map( + fn (ConditionData $condition) => [ + 'value_operator' => $condition->getValueOperator(), + 'value' => $condition->getValue(), + 'logic_operator' => $condition->getLogicOperator(), + ], + $condition_comment->getConfiguredConditionsData() + ) + ); + } + + public static function provideFormMigrationConditionsForDestinations(): iterable + { + $creation_strategies = [ + 1 => CreationStrategy::ALWAYS_CREATED, + 2 => CreationStrategy::CREATED_IF, + 3 => CreationStrategy::CREATED_UNLESS, + ]; + + $types = [ + 'glpi_plugin_formcreator_targettickets' => 'PluginFormcreatorTargetTicket', + 'glpi_plugin_formcreator_targetproblems' => 'PluginFormcreatorTargetProblem', + 'glpi_plugin_formcreator_targetchanges' => 'PluginFormcreatorTargetChange', + ]; + + foreach ($types as $table => $itemtype) { + foreach ($creation_strategies as $key => $strategy) { + if ($strategy === CreationStrategy::ALWAYS_CREATED) { + $expected_conditions = []; + } else { + $expected_conditions = [ + [ + 'value_operator' => ValueOperator::EQUALS, + 'value' => 'Test', + 'logic_operator' => LogicOperator::AND, + ] + ]; + } + + yield 'Destination ' . $itemtype . ' - ' . $strategy->getLabel() => [ + 'legacy_itemtype' => $itemtype, + 'legacy_table' => $table, + 'legacy_strategy' => $key, + 'expected_creation_strategy' => $strategy, + 'expected_conditions' => $expected_conditions + ]; + } + } + } + + #[DataProvider('provideFormMigrationConditionsForDestinations')] + public function testFormMigrationConditionsForDestinations( + string $legacy_itemtype, + string $legacy_table, + int $legacy_strategy, + CreationStrategy $expected_creation_strategy, + array $expected_conditions + ): void { + /** + * @var \DBmysql $DB + */ + global $DB; + + // Create a form + $this->assertTrue($DB->insert( + 'glpi_plugin_formcreator_forms', + [ + 'name' => 'Test form migration condition for destinations', + ] + )); + $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' => 'Test form migration condition for destinations', + 'plugin_formcreator_sections_id' => $section_id, + ] + )); + $target_question_id = $DB->insertId(); + + // Insert a target for the form + $this->assertTrue($DB->insert( + $legacy_table, + [ + 'name' => 'Test form migration condition for destinations', + 'content' => 'Test', + 'plugin_formcreator_forms_id' => $form_id, + 'show_rule' => $legacy_strategy + ] + )); + $destination_id = $DB->insertId(); + + // Insert condition + $this->assertTrue($DB->insert( + 'glpi_plugin_formcreator_conditions', + [ + 'itemtype' => $legacy_itemtype, + 'items_id' => $destination_id, + 'plugin_formcreator_questions_id' => $target_question_id, + 'show_condition' => 1, + 'show_value' => 'Test', + 'show_logic' => 1, + ] + )); + + // Process migration + $migration = new FormMigration($DB, FormAccessControlManager::getInstance()); + $this->setPrivateProperty($migration, 'result', new PluginMigrationResult()); + $this->assertTrue($this->callPrivateMethod($migration, 'processMigration')); + + // Verify that the condition has been migrated correctly + /** @var Form $form */ + $form = getItemByTypeName(Form::class, 'Test form migration condition for destinations'); + $this->assertCount(1, $form->getSections()); + $this->assertCount(1, $form->getQuestions()); + $this->assertCount(1, $form->getDestinations()); + + /** @var FormDestination $destination */ + $destination = getItemByTypeName(FormDestination::class, 'Test form migration condition for destinations'); + $this->assertNotFalse($destination); + $this->assertEquals($expected_creation_strategy, $destination->getConfiguredCreationStrategy()); + $this->assertEquals( + $expected_conditions, + array_map( + fn (ConditionData $condition) => [ + 'value_operator' => $condition->getValueOperator(), + 'value' => $condition->getValue(), + 'logic_operator' => $condition->getLogicOperator(), + ], + $destination->getConfiguredConditionsData() + ) + ); + } } diff --git a/src/Glpi/Form/Condition/ConditionHandler/SingleChoiceFromValuesConditionHandler.php b/src/Glpi/Form/Condition/ConditionHandler/SingleChoiceFromValuesConditionHandler.php index f0107fec09e..38c808d226c 100644 --- a/src/Glpi/Form/Condition/ConditionHandler/SingleChoiceFromValuesConditionHandler.php +++ b/src/Glpi/Form/Condition/ConditionHandler/SingleChoiceFromValuesConditionHandler.php @@ -35,9 +35,12 @@ namespace Glpi\Form\Condition\ConditionHandler; use Glpi\Form\Condition\ValueOperator; +use Glpi\Form\Migration\ConditionHandlerDataConverterInterface; use Override; -final class SingleChoiceFromValuesConditionHandler implements ConditionHandlerInterface +final class SingleChoiceFromValuesConditionHandler implements + ConditionHandlerInterface, + ConditionHandlerDataConverterInterface { public function __construct( private array $values, @@ -83,4 +86,10 @@ public function applyValueOperator( default => false, }; } + + #[Override] + public function convertConditionValue(string $value): int + { + return array_search($value, $this->values, true) ?: 0; + } } diff --git a/src/Glpi/Form/Condition/FormData.php b/src/Glpi/Form/Condition/FormData.php index f03742f730c..927fb7ab42f 100644 --- a/src/Glpi/Form/Condition/FormData.php +++ b/src/Glpi/Form/Condition/FormData.php @@ -81,9 +81,10 @@ public static function createFromForm(Form $form): self foreach ($form->getQuestions() as $question) { $questions_data[] = [ - 'uuid' => $question->fields['uuid'], - 'name' => $question->fields['name'], - 'type' => $question->getQuestionType(), + 'uuid' => $question->fields['uuid'], + 'name' => $question->fields['name'], + 'type' => $question->getQuestionType(), + 'extra_data' => json_decode($question->fields['extra_data'] ?? '{}', true), ]; } diff --git a/src/Glpi/Form/Migration/ConditionHandlerDataConverterInterface.php b/src/Glpi/Form/Migration/ConditionHandlerDataConverterInterface.php new file mode 100644 index 00000000000..02f426f8754 --- /dev/null +++ b/src/Glpi/Form/Migration/ConditionHandlerDataConverterInterface.php @@ -0,0 +1,46 @@ +. + * + * --------------------------------------------------------------------- + */ + +namespace Glpi\Form\Migration; + +interface ConditionHandlerDataConverterInterface +{ + /** + * Convert conditions value + * + * @param string $value + * @return mixed + */ + public function convertConditionValue(string $value): mixed; +} diff --git a/src/Glpi/Form/Migration/FormMigration.php b/src/Glpi/Form/Migration/FormMigration.php index 9734876734d..8025996a8ae 100644 --- a/src/Glpi/Form/Migration/FormMigration.php +++ b/src/Glpi/Form/Migration/FormMigration.php @@ -44,14 +44,21 @@ use Glpi\Form\AccessControl\ControlType\DirectAccessConfig; use Glpi\Form\AccessControl\FormAccessControl; use Glpi\Form\AccessControl\FormAccessControlManager; +use Glpi\Form\BlockInterface; use Glpi\Form\Category; use Glpi\Form\Comment; +use Glpi\Form\Condition\ConditionHandler\ConditionHandlerInterface; +use Glpi\Form\Condition\CreationStrategy; +use Glpi\Form\Condition\LogicOperator; +use Glpi\Form\Condition\ValueOperator; +use Glpi\Form\Condition\VisibilityStrategy; use Glpi\Form\Destination\AbstractConfigField; use Glpi\Form\Destination\AbstractCommonITILFormDestination; use Glpi\Form\Destination\CommonITILField\ContentField; use Glpi\Form\Destination\CommonITILField\ITILActorField; use Glpi\Form\Destination\FormDestination; use Glpi\Form\Destination\FormDestinationChange; +use Glpi\Form\Destination\FormDestinationInterface; use Glpi\Form\Destination\FormDestinationProblem; use Glpi\Form\Destination\FormDestinationTicket; use Glpi\Form\Form; @@ -160,6 +167,75 @@ private function getStrategyForAccessTypes(): array ]; } + /** + * Get the visibility strategy from the legacy value + * + * @param int $visibility The legacy visibility value + * @return VisibilityStrategy The corresponding visibility strategy + */ + private function getVisibilityStrategyFromLegacy(int $visibility): VisibilityStrategy + { + return match ($visibility) { + 1 => VisibilityStrategy::ALWAYS_VISIBLE, + 2 => VisibilityStrategy::VISIBLE_IF, + 3 => VisibilityStrategy::HIDDEN_IF, + default => throw new LogicException("Invalid visibility value: {$visibility}") + }; + } + + /** + * Get the creation strategy from the legacy value + * + * @param int $creation The legacy creation value + * @return CreationStrategy The corresponding creation strategy + */ + private function getCreationStrategyFromLegacy(int $creation): CreationStrategy + { + return match ($creation) { + 1 => CreationStrategy::ALWAYS_CREATED, + 2 => CreationStrategy::CREATED_IF, + 3 => CreationStrategy::CREATED_UNLESS, + default => throw new LogicException("Invalid creation value: {$creation}") + }; + } + + /** + * Get the value operator from the legacy value + * + * @param int $value_operator The legacy value operator + * @return ValueOperator|null The corresponding value operator or null if not found + */ + private function getValueOperatorFromLegacy(int $value_operator): ?ValueOperator + { + return match ($value_operator) { + 1 => ValueOperator::EQUALS, + 2 => ValueOperator::NOT_EQUALS, + 3 => ValueOperator::LESS_THAN, + 4 => ValueOperator::GREATER_THAN, + 5 => ValueOperator::LESS_THAN_OR_EQUALS, + 6 => ValueOperator::GREATER_THAN_OR_EQUALS, + 7 => ValueOperator::VISIBLE, + 8 => ValueOperator::NOT_VISIBLE, + 9 => ValueOperator::MATCH_REGEX, + default => null + }; + } + + /** + * Get the logic operator from the legacy value + * + * @param int $logic_operator The legacy logic operator + * @return LogicOperator The corresponding logic operator + */ + private function getLogicOperatorFromLegacy(int $logic_operator): LogicOperator + { + return match ($logic_operator) { + 1 => LogicOperator::AND, + 2 => LogicOperator::OR, + default => throw new LogicException("Invalid logic operator value: {$logic_operator}") + }; + } + /** * Create the appropriate strategy configuration based on form access rights * @@ -214,6 +290,9 @@ protected function validatePrerequisites(): bool ], 'glpi_plugin_formcreator_forms_languages' => [ 'plugin_formcreator_forms_id', 'name' + ], + 'glpi_plugin_formcreator_conditions' => [ + 'itemtype', 'items_id', 'plugin_formcreator_questions_id', 'show_condition', 'show_value', 'show_logic', 'order' ] ]; @@ -232,7 +311,8 @@ protected function processMigration(): bool 'targets_ticket' => $this->countRecords('glpi_plugin_formcreator_targettickets'), 'targets_problem' => $this->countRecords('glpi_plugin_formcreator_targetproblems'), 'targets_change' => $this->countRecords('glpi_plugin_formcreator_targetchanges'), - 'translations' => $this->countRecords('glpi_plugin_formcreator_forms_languages') + 'translations' => $this->countRecords('glpi_plugin_formcreator_forms_languages'), + 'conditions' => $this->countRecords('glpi_plugin_formcreator_conditions'), ]; // Set total progress steps @@ -251,6 +331,7 @@ protected function processMigration(): bool $this->processMigrationOfAccessControls(); $this->processMigrationOfFormTargets(); $this->processMigrationOfTranslations(); + $this->processMigrationOfConditions(); $this->progress_indicator?->setProgressBarMessage(''); $this->progress_indicator?->finish(); @@ -881,8 +962,14 @@ private function processMigrationOfDestination( ] ); + $source_itemtype = match ($targetTable) { + 'glpi_plugin_formcreator_targettickets' => 'PluginFormcreatorTargetTicket', + 'glpi_plugin_formcreator_targetproblems' => 'PluginFormcreatorTargetProblem', + 'glpi_plugin_formcreator_targetchanges' => 'PluginFormcreatorTargetChange', + default => throw new LogicException("Unknown target table {$targetTable}") + }; $this->mapItem( - 'PluginFormcreatorTarget' . basename($targetTable), + $source_itemtype, $raw_target['id'], FormDestination::class, $destination->getID() @@ -920,8 +1007,14 @@ private function processMigrationOfITILActorsFields( ]); foreach ($raw_targets_actors as $raw_target_actor) { + $source_itemtype = match ($targetTable) { + 'glpi_plugin_formcreator_targettickets' => 'PluginFormcreatorTargetTicket', + 'glpi_plugin_formcreator_targetproblems' => 'PluginFormcreatorTargetProblem', + 'glpi_plugin_formcreator_targetchanges' => 'PluginFormcreatorTargetChange', + default => throw new LogicException("Unknown target table {$targetTable}") + }; $target_id = $this->getMappedItemTarget( - 'PluginFormcreatorTarget' . basename($targetTable), + $source_itemtype, $raw_target_actor['items_id'] )['items_id'] ?? 0; @@ -1046,6 +1139,218 @@ private function processMigrationOfTranslations(): void } } + private function processMigrationOfConditions(): void + { + $this->progress_indicator?->setProgressBarMessage(__('Importing conditions...')); + + // Retrieve data from glpi_plugin_formcreator_conditions table + $raw_conditions = $this->db->request([ + 'SELECT' => [ + 'glpi_plugin_formcreator_conditions.itemtype', + 'glpi_plugin_formcreator_conditions.items_id', + 'glpi_plugin_formcreator_conditions.plugin_formcreator_questions_id', + 'glpi_plugin_formcreator_conditions.show_condition', + 'glpi_plugin_formcreator_conditions.show_value', + 'glpi_plugin_formcreator_conditions.show_logic', + 'glpi_plugin_formcreator_conditions.order', + new QueryExpression( + 'COALESCE(glpi_plugin_formcreator_questions.show_rule, glpi_plugin_formcreator_sections.show_rule, glpi_plugin_formcreator_targettickets.show_rule, glpi_plugin_formcreator_targetchanges.show_rule, glpi_plugin_formcreator_targetproblems.show_rule)', + 'show_rule' + ), + ], + 'FROM' => 'glpi_plugin_formcreator_conditions', + 'JOIN' => [ + 'glpi_plugin_formcreator_questions' => [ + 'ON' => [ + 'glpi_plugin_formcreator_conditions' => 'items_id', + 'glpi_plugin_formcreator_questions' => 'id', + ['AND' => ['glpi_plugin_formcreator_conditions.itemtype' => 'PluginFormcreatorQuestion']] + ] + ], + 'glpi_plugin_formcreator_sections' => [ + 'ON' => [ + 'glpi_plugin_formcreator_conditions' => 'items_id', + 'glpi_plugin_formcreator_sections' => 'id', + ['AND' => ['glpi_plugin_formcreator_conditions.itemtype' => 'PluginFormcreatorSection']] + ] + ], + 'glpi_plugin_formcreator_targettickets' => [ + 'ON' => [ + 'glpi_plugin_formcreator_conditions' => 'items_id', + 'glpi_plugin_formcreator_targettickets' => 'id', + ['AND' => ['glpi_plugin_formcreator_conditions.itemtype' => 'PluginFormcreatorTargetTicket']] + ] + ], + 'glpi_plugin_formcreator_targetchanges' => [ + 'ON' => [ + 'glpi_plugin_formcreator_conditions' => 'items_id', + 'glpi_plugin_formcreator_targetchanges' => 'id', + ['AND' => ['glpi_plugin_formcreator_conditions.itemtype' => 'PluginFormcreatorTargetChange']] + ] + ], + 'glpi_plugin_formcreator_targetproblems' => [ + 'ON' => [ + 'glpi_plugin_formcreator_conditions' => 'items_id', + 'glpi_plugin_formcreator_targetproblems' => 'id', + ['AND' => ['glpi_plugin_formcreator_conditions.itemtype' => 'PluginFormcreatorTargetProblem']] + ] + ] + ], + 'HAVING' => [ + 'NOT' => [ + 'show_rule' => 'NULL' + ], + ], + 'ORDER' => [ + 'glpi_plugin_formcreator_conditions.order' + ] + ]); + + $conditions = []; + foreach ($raw_conditions as $raw_condition) { + $target_item = $this->getMappedItemTarget( + $raw_condition['itemtype'], + $raw_condition['items_id'] + ); + $question_id = $this->getMappedItemTarget( + 'PluginFormcreatorQuestion', + $raw_condition['plugin_formcreator_questions_id'] + )['items_id'] ?? 0; + + if ($target_item === null) { + $this->result->addMessage( + MessageType::Error, + sprintf( + 'Condition for itemtype "%s" and items_id "%s" not found. It will not be migrated.', + $raw_condition['itemtype'], + $raw_condition['items_id'] + ) + ); + continue; + } elseif ($question_id === 0) { + $this->result->addMessage( + MessageType::Error, + sprintf( + 'Question with id "%s" for itemtype "%s" and items_id "%s" not found. It will not be migrated.', + $raw_condition['plugin_formcreator_questions_id'], + $raw_condition['itemtype'], + $raw_condition['items_id'] + ) + ); + } + + $question = Question::getById($question_id); + if ($question === false) { + continue; + } + + $value = $raw_condition['show_value']; + if (isset($question->fields['extra_data'])) { + $config = $question->getQuestionType()->getExtraDataConfig( + json_decode($question->fields['extra_data'], true) + ); + $condition_handlers = $question->getQuestionType()->getConditionHandlers($config); + $condition_handler = null; + + // Get the value operator before trying to find a compatible handler + $value_operator = $this->getValueOperatorFromLegacy($raw_condition['show_condition']); + + if ($value_operator !== null) { + $condition_handler = current(array_filter( + $condition_handlers, + function (ConditionHandlerInterface $handler) use ($value_operator) { + if (!($handler instanceof ConditionHandlerDataConverterInterface)) { + return false; + } + + return in_array( + $value_operator, + $handler->getSupportedValueOperators() + ); + } + )); + } + + if ($condition_handler) { + /** @var ConditionHandlerDataConverterInterface $condition_handler */ + $value = $condition_handler->convertConditionValue($value); + } + } + + if (is_a($target_item['itemtype'], FormDestination::class, true)) { + $creation_strategy = $this->getCreationStrategyFromLegacy($raw_condition['show_rule']); + if ($creation_strategy !== CreationStrategy::ALWAYS_CREATED) { + $conditions[$target_item['itemtype']][$target_item['items_id']]['creation_strategy'] = $creation_strategy->value; + $conditions[$target_item['itemtype']][$target_item['items_id']]['conditions'][] = [ + 'item' => sprintf('question-%s', $question->getUUID()), + 'value' => $value, + 'item_type' => 'question', + 'item_uuid' => $question->getUUID(), + 'value_operator' => $this->getValueOperatorFromLegacy($raw_condition['show_condition']), + 'logic_operator' => $this->getLogicOperatorFromLegacy($raw_condition['show_logic']), + ]; + } + } else { + $visibility_strategy = $this->getVisibilityStrategyFromLegacy($raw_condition['show_rule']); + if ($visibility_strategy !== VisibilityStrategy::ALWAYS_VISIBLE) { + $conditions[$target_item['itemtype']][$target_item['items_id']]['visibility_strategy'] = $visibility_strategy->value; + $conditions[$target_item['itemtype']][$target_item['items_id']]['conditions'][] = [ + 'item' => sprintf('question-%s', $question->getUUID()), + 'value' => $value, + 'item_type' => 'question', + 'item_uuid' => $question->getUUID(), + 'value_operator' => $this->getValueOperatorFromLegacy($raw_condition['show_condition']), + 'logic_operator' => $this->getLogicOperatorFromLegacy($raw_condition['show_logic']), + ]; + } + } + + $this->progress_indicator?->advance(); + } + + // Process all collected conditions at once + foreach ($conditions as $itemtype => $items) { + foreach ($items as $item_id => $data) { + $input = []; + + if (isset($data['visibility_strategy'])) { + $input = [ + 'id' => $item_id, + 'visibility_strategy' => $data['visibility_strategy'], + '_conditions' => $data['conditions'], + ]; + } elseif (isset($data['creation_strategy'])) { + $input = [ + 'id' => $item_id, + 'creation_strategy' => $data['creation_strategy'], + '_conditions' => $data['conditions'], + ]; + } + + /** + * If the target item is a block (Question or Comment), we must explicitly provide the horizontal_rank. + * A frontend constraint resets horizontal_rank to NULL during update if it's not included in the input. + */ + if (is_a($itemtype, BlockInterface::class, true)) { + $item = new $itemtype(); + $item->getFromDB($item_id); + + if ($item->fields['horizontal_rank'] !== null) { + $input['horizontal_rank'] = $item->fields['horizontal_rank']; + } + } + + $this->importItem( + $itemtype, + $input, + [ + 'id' => $item_id + ] + ); + } + } + } + /** * Get translations from a formcreator translation file * diff --git a/templates/pages/admin/form/destination_visibility_conditions_configuration.html.twig b/templates/pages/admin/form/destination_visibility_conditions_configuration.html.twig index 5e41aab919c..08a1e3bcba7 100644 --- a/templates/pages/admin/form/destination_visibility_conditions_configuration.html.twig +++ b/templates/pages/admin/form/destination_visibility_conditions_configuration.html.twig @@ -68,6 +68,7 @@ uuid: '{{ question_data.getUuid()|escape('js') }}', name: '{{ question_data.getName()|escape('js') }}', type: '{{ get_class(question_data.getType())|escape('js') }}', + extra_data: {{ question_data.getExtraData()|json_encode|raw }}, }); {% endfor %} diff --git a/tests/glpi-formcreator-migration-data.sql b/tests/glpi-formcreator-migration-data.sql index a621b24a87c..732601045b4 100644 --- a/tests/glpi-formcreator-migration-data.sql +++ b/tests/glpi-formcreator-migration-data.sql @@ -508,4 +508,24 @@ LOCK TABLES `glpi_plugin_formcreator_forms_languages` WRITE; INSERT INTO `glpi_plugin_formcreator_forms_languages` VALUES (2,19,'fr_FR','','18b24cbd-91f5039d-67e3dc2daab018.88018762'); UNLOCK TABLES; +-- +-- 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; + -- Dump completed on 2025-01-21 11:41:32