diff --git a/public/css/advancedforms.css b/public/css/advancedforms.scss similarity index 72% rename from public/css/advancedforms.css rename to public/css/advancedforms.scss index 866d883..321d688 100644 --- a/public/css/advancedforms.css +++ b/public/css/advancedforms.scss @@ -31,5 +31,28 @@ /* Hide ldap preview on active questions */ [data-glpi-form-editor-active-question] [data-ldap-question-preview] { - display: none !important; + display: none !important; +} + +[data-glpi-itildestination-field-slm-computed-strategy-config] { + >div { + &:first-child { + width: 30%; + + .select2-selection { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + } + } + + &:last-child { + margin-left: calc(-1 * var(--tblr-border-width)); + width: calc(70% + 1 * var(--tblr-border-width)); + + .select2-selection { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; + } + } + } } diff --git a/setup.php b/setup.php index c6dea5e..8ce55fc 100644 --- a/setup.php +++ b/setup.php @@ -58,7 +58,7 @@ function plugin_init_advancedforms(): void } $hook_manager = new HookManager('advancedforms'); - $hook_manager->registerCSSFile('css/advancedforms.css'); + $hook_manager->registerCSSFile('css/advancedforms.scss'); $hook_manager->registerJavascriptFile('js/advancedforms.js'); InitManager::getInstance()->init(); diff --git a/src/Model/Destination/Strategies/ComputedDateFromFormSubmitDateSLMStrategy.php b/src/Model/Destination/Strategies/ComputedDateFromFormSubmitDateSLMStrategy.php new file mode 100644 index 0000000..1bb8147 --- /dev/null +++ b/src/Model/Destination/Strategies/ComputedDateFromFormSubmitDateSLMStrategy.php @@ -0,0 +1,183 @@ +getExtraDataValue(self::EXTRA_KEY_TIME_OFFSET); + $time_definition = $config->getExtraDataValue(self::EXTRA_KEY_TIME_DEFINITION); + $creation_date = $answers_set->fields['date_creation']; + + if ( + !is_numeric($time_offset) + || !is_string($time_definition) + || !array_key_exists($time_definition, LevelAgreement::getDefinitionTimeValues()) + || !is_string($creation_date) + ) { + return $input; + } + + // Get the form submission date + $submit_date = new DateTime($creation_date); + + // Apply the offset based on the time definition + $interval_spec = match ($time_definition) { + 'minute' => 'PT' . abs((int) $time_offset) . 'M', // Minutes + 'hour' => 'PT' . abs((int) $time_offset) . 'H', // Hours + 'day' => 'P' . abs((int) $time_offset) . 'D', // Days + default => 'P' . abs((int) $time_offset) . 'M', // Months + }; + + $interval = new DateInterval($interval_spec); + if ((int) $time_offset < 0) { + $submit_date->sub($interval); + } else { + $submit_date->add($interval); + } + + // Apply the computed date to the input array + $slm = $field->getSLM(); + $field_names = $slm::getFieldNames($field->getType()); + if (!empty($field_names) && is_string($field_names[0])) { + $input[$field_names[0]] = $submit_date->format('Y-m-d H:i:s'); + } + + return $input; + } + + #[Override] + public function renderExtraConfigFields( + Form $form, + SLMField $field, + SLMFieldConfig $config, + string $input_name, + array $display_options, + ): string { + $twig = TemplateRenderer::getInstance(); + + return $twig->render('@advancedforms/editor/destinations/strategies/computed_date_from_form_submit_date_slm_strategy.html.twig', [ + 'strategy_key' => self::KEY, + 'options' => $display_options, + 'extra_time_offset_field' => [ + 'aria_label' => __('Enter time offset', 'advancedforms'), + 'value' => $config->getExtraDataValue(self::EXTRA_KEY_TIME_OFFSET), + 'input_name' => $input_name . "[" . SLMFieldConfig::EXTRA_DATA . "][" . self::EXTRA_KEY_TIME_OFFSET . "]", + 'min' => -30, + 'max' => 30, + ], + 'extra_time_definition_field' => [ + 'aria_label' => __('Select time definition', 'advancedforms'), + 'value' => $config->getExtraDataValue(self::EXTRA_KEY_TIME_DEFINITION), + 'input_name' => $input_name . "[" . SLMFieldConfig::EXTRA_DATA . "][" . self::EXTRA_KEY_TIME_DEFINITION . "]", + 'possible_values' => LevelAgreement::getDefinitionTimeValues(), + ], + ]); + } + + #[Override] + public function getExtraConfigKeys(): array + { + return [self::EXTRA_KEY_TIME_OFFSET, self::EXTRA_KEY_TIME_DEFINITION]; + } + + #[Override] + public function getWeight(): int + { + return 60; + } +} diff --git a/src/Model/Destination/Strategies/ComputedFromSpecificDateAnswerSLMStrategy.php b/src/Model/Destination/Strategies/ComputedFromSpecificDateAnswerSLMStrategy.php new file mode 100644 index 0000000..de31425 --- /dev/null +++ b/src/Model/Destination/Strategies/ComputedFromSpecificDateAnswerSLMStrategy.php @@ -0,0 +1,228 @@ +getExtraDataValue(self::EXTRA_KEY_QUESTION_ID); + $time_offset = $config->getExtraDataValue(self::EXTRA_KEY_TIME_OFFSET); + $time_definition = $config->getExtraDataValue(self::EXTRA_KEY_TIME_DEFINITION); + + // Validate configuration values + if (!is_numeric($question_id) || !is_numeric($time_offset) || !is_string($time_definition)) { + return $input; + } + + // Get and validate the answer from the answers set + $answer = $answers_set->getAnswerByQuestionId((int) $question_id); + if (!$answer instanceof Answer) { + return $input; + } + + // Get and validate the raw value from the answer + $string_date = $answer->getRawAnswer(); + if (empty($string_date) || !is_string($string_date)) { + return $input; + } + + // Create DateTime object from the answer's raw value + try { + $date = new DateTime($string_date); + } catch (DateMalformedStringException) { + return $input; + } + + // Apply the offset based on the time definition + $interval_spec = match ($time_definition) { + 'minute' => 'PT' . abs((int) $time_offset) . 'M', // Minutes + 'hour' => 'PT' . abs((int) $time_offset) . 'H', // Hours + 'day' => 'P' . abs((int) $time_offset) . 'D', // Days + default => 'P' . abs((int) $time_offset) . 'M', // Months + }; + + $interval = new DateInterval($interval_spec); + if ((int) $time_offset < 0) { + $date->sub($interval); + } else { + $date->add($interval); + } + + // Apply the computed date to the input array + $slm = $field->getSLM(); + $field_names = $slm::getFieldNames($field->getType()); + if (!empty($field_names) && is_string($field_names[0])) { + $input[$field_names[0]] = $date->format('Y-m-d H:i:s'); + } + + return $input; + } + + #[Override] + public function renderExtraConfigFields( + Form $form, + SLMField $field, + SLMFieldConfig $config, + string $input_name, + array $display_options, + ): string { + $twig = TemplateRenderer::getInstance(); + + return $twig->render('@advancedforms/editor/destinations/strategies/computed_date_from_specific_date_answer_slm_strategy.html.twig', [ + 'strategy_key' => self::KEY, + 'options' => $display_options, + 'extra_question_id_field' => [ + 'empty_label' => __("Select a question..."), + 'value' => $config->getExtraDataValue(self::EXTRA_KEY_QUESTION_ID), + 'input_name' => $input_name . "[" . SLMFieldConfig::EXTRA_DATA . "][" . self::EXTRA_KEY_QUESTION_ID . "]", + 'possible_values' => $this->getDateQuestionsForDropdown($form), + ], + 'extra_time_offset_field' => [ + 'aria_label' => __('Enter time offset', 'advancedforms'), + 'value' => $config->getExtraDataValue(self::EXTRA_KEY_TIME_OFFSET), + 'input_name' => $input_name . "[" . SLMFieldConfig::EXTRA_DATA . "][" . self::EXTRA_KEY_TIME_OFFSET . "]", + 'min' => -30, + 'max' => 30, + ], + 'extra_time_definition_field' => [ + 'aria_label' => __('Select time definition', 'advancedforms'), + 'value' => $config->getExtraDataValue(self::EXTRA_KEY_TIME_DEFINITION), + 'input_name' => $input_name . "[" . SLMFieldConfig::EXTRA_DATA . "][" . self::EXTRA_KEY_TIME_DEFINITION . "]", + 'possible_values' => LevelAgreement::getDefinitionTimeValues(), + ], + ]); + } + + #[Override] + public function getExtraConfigKeys(): array + { + return [self::EXTRA_KEY_QUESTION_ID, self::EXTRA_KEY_TIME_OFFSET, self::EXTRA_KEY_TIME_DEFINITION]; + } + + #[Override] + public function getWeight(): int + { + return 70; + } + + /** + * Get date questions available in the form for the dropdown. + * + * @return array + */ + private function getDateQuestionsForDropdown(Form $form): array + { + $values = []; + $questions = $form->getQuestionsByType(QuestionTypeDateTime::class); + + foreach ($questions as $question) { + // Ensure the date part is enabled + if (!(new QuestionTypeDateTime())->isDateEnabled($question)) { + continue; + } + + $values[$question->getId()] = $question->getName(); + } + + return $values; + } +} diff --git a/src/Model/Destination/Strategies/SpecificDateAnswerSLMStrategy.php b/src/Model/Destination/Strategies/SpecificDateAnswerSLMStrategy.php new file mode 100644 index 0000000..2f127e2 --- /dev/null +++ b/src/Model/Destination/Strategies/SpecificDateAnswerSLMStrategy.php @@ -0,0 +1,181 @@ +getExtraDataValue(self::EXTRA_KEY_QUESTION_ID); + if (!is_numeric($question_id)) { + return $input; + } + + // Get and validate the answer from the answers set + $answer = $answers_set->getAnswerByQuestionId((int) $question_id); + if (!$answer instanceof Answer) { + return $input; + } + + // Get and validate the raw value from the answer + $raw_value = $answer->getRawAnswer(); + if (empty($raw_value) || !is_string($raw_value)) { + return $input; + } + + // Apply the date to the input array + $slm = $field->getSLM(); + $field_names = $slm::getFieldNames($field->getType()); + if (!empty($field_names) && is_string($field_names[0])) { + $input[$field_names[0]] = $raw_value; + } + + return $input; + } + + #[Override] + public function renderExtraConfigFields( + Form $form, + SLMField $field, + SLMFieldConfig $config, + string $input_name, + array $display_options, + ): string { + $twig = TemplateRenderer::getInstance(); + + return $twig->render('@advancedforms/editor/destinations/strategies/specific_date_answer_slm_strategy.html.twig', [ + 'strategy_key' => self::KEY, + 'options' => $display_options, + 'extra_field' => [ + 'empty_label' => __("Select a question..."), + 'value' => $config->getExtraDataValue(self::EXTRA_KEY_QUESTION_ID), + 'input_name' => $input_name . "[" . SLMFieldConfig::EXTRA_DATA . "][" . self::EXTRA_KEY_QUESTION_ID . "]", + 'possible_values' => $this->getDateQuestionsForDropdown($form), + ], + ]); + } + + #[Override] + public function getExtraConfigKeys(): array + { + return [self::EXTRA_KEY_QUESTION_ID]; + } + + #[Override] + public function getWeight(): int + { + return 50; + } + + /** + * Get date questions available in the form for the dropdown. + * + * @return array + */ + private function getDateQuestionsForDropdown(Form $form): array + { + $values = []; + $questions = $form->getQuestionsByType(QuestionTypeDateTime::class); + + foreach ($questions as $question) { + // Ensure the date part is enabled + if (!(new QuestionTypeDateTime())->isDateEnabled($question)) { + continue; + } + + $values[$question->getId()] = $question->getName(); + } + + return $values; + } +} diff --git a/src/Service/ConfigManager.php b/src/Service/ConfigManager.php index 7ce6be8..b2cdf91 100644 --- a/src/Service/ConfigManager.php +++ b/src/Service/ConfigManager.php @@ -35,9 +35,13 @@ use Config; use Glpi\Application\View\TemplateRenderer; +use Glpi\Form\Destination\CommonITILField\SLMFieldStrategyInterface; use Glpi\Form\QuestionType\QuestionTypeInterface; use Glpi\Toolbox\SingletonTrait; use GlpiPlugin\Advancedforms\Model\Config\ConfigurableItemInterface; +use GlpiPlugin\Advancedforms\Model\Destination\Strategies\ComputedDateFromFormSubmitDateSLMStrategy; +use GlpiPlugin\Advancedforms\Model\Destination\Strategies\ComputedFromSpecificDateAnswerSLMStrategy; +use GlpiPlugin\Advancedforms\Model\Destination\Strategies\SpecificDateAnswerSLMStrategy; use GlpiPlugin\Advancedforms\Model\QuestionType\HiddenQuestion; use GlpiPlugin\Advancedforms\Model\QuestionType\HostnameQuestion; use GlpiPlugin\Advancedforms\Model\QuestionType\IpAddressQuestion; @@ -51,8 +55,9 @@ public function renderConfigForm(): string { $twig = TemplateRenderer::getInstance(); return $twig->render('@advancedforms/config_form.html.twig', [ - 'config_manager' => $this, - 'question_types' => $this->getConfigurableQuestionTypes(), + 'config_manager' => $this, + 'question_types' => $this->getConfigurableQuestionTypes(), + 'slm_strategies' => $this->getConfigurableSLMStrategies(), ]); } @@ -67,6 +72,16 @@ public function getConfigurableQuestionTypes(): array ]; } + /** @return array */ + public function getConfigurableSLMStrategies(): array + { + return [ + new SpecificDateAnswerSLMStrategy(), + new ComputedDateFromFormSubmitDateSLMStrategy(), + new ComputedFromSpecificDateAnswerSLMStrategy(), + ]; + } + public function isConfigurableItemEnabled( ConfigurableItemInterface $item, ): bool { @@ -91,6 +106,15 @@ public function getEnabledQuestionsTypes(): array ); } + /** @return array */ + public function getEnabledSLMStrategies(): array + { + return array_filter( + $this->getConfigurableSLMStrategies(), + fn(ConfigurableItemInterface $s): bool => $this->isConfigurableItemEnabled($s), + ); + } + public function hasAtLeastOneQuestionTypeEnabled(): bool { return $this->getEnabledQuestionsTypes() !== []; diff --git a/src/Service/InitManager.php b/src/Service/InitManager.php index 45699c6..c9f70e9 100644 --- a/src/Service/InitManager.php +++ b/src/Service/InitManager.php @@ -34,6 +34,7 @@ namespace GlpiPlugin\Advancedforms\Service; use Config; +use Glpi\Form\Destination\FormDestinationManager; use Glpi\Form\Migration\TypesConversionMapper; use Glpi\Form\QuestionType\QuestionTypesManager; use Glpi\Plugin\Hooks; @@ -51,6 +52,7 @@ public function init(): void { $this->registerConfiguration(); $this->registerPluginTypes(); + $this->registerPluginDestinationStrategies(); } private function registerConfiguration(): void @@ -96,4 +98,14 @@ private function registerPluginTypes(): void } } } + + private function registerPluginDestinationStrategies(): void + { + $config_manager = ConfigManager::getInstance(); + $destination_manager = FormDestinationManager::getInstance(); + + foreach ($config_manager->getEnabledSLMStrategies() as $strategy) { + $destination_manager->registerPluginSLMFieldStrategy($strategy); + } + } } diff --git a/templates/config_form.html.twig b/templates/config_form.html.twig index 33c9796..a93e2c5 100644 --- a/templates/config_form.html.twig +++ b/templates/config_form.html.twig @@ -43,6 +43,10 @@ action="{{ 'Config'|itemtype_form_path }}" data-track-changes="true" > +

+ + {{ __("Question types", "advancedforms") }} +

{% for question_type in question_types %} {{ include('@advancedforms/configurable_item.html.twig', { @@ -52,6 +56,19 @@ {% endfor %}
+

+ + {{ __("SLA/OLA strategies", "advancedforms") }} +

+
+ {% for slm_strategy in slm_strategies %} + {{ include('@advancedforms/configurable_item.html.twig', { + config_manager: config_manager, + item: slm_strategy, + }, with_context: false) }} + {% endfor %} +
+ diff --git a/templates/editor/destinations/strategies/computed_date_from_form_submit_date_slm_strategy.html.twig b/templates/editor/destinations/strategies/computed_date_from_form_submit_date_slm_strategy.html.twig new file mode 100644 index 0000000..d9d3167 --- /dev/null +++ b/templates/editor/destinations/strategies/computed_date_from_form_submit_date_slm_strategy.html.twig @@ -0,0 +1,67 @@ +{# + # ------------------------------------------------------------------------- + # advancedforms plugin for GLPI + # ------------------------------------------------------------------------- + # + # MIT License + # + # Permission is hereby granted, free of charge, to any person obtaining a copy + # of this software and associated documentation files (the "Software"), to deal + # in the Software without restriction, including without limitation the rights + # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + # copies of the Software, and to permit persons to whom the Software is + # furnished to do so, subject to the following conditions: + # + # The above copyright notice and this permission notice shall be included in all + # copies or substantial portions of the Software. + # + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + # SOFTWARE. + # ------------------------------------------------------------------------- + # @copyright Copyright (C) 2025 by the advancedforms plugin team. + # @copyright 2015-2025 Teclib' and contributors. + # @licence https://www.gnu.org/licenses/gpl-3.0.html + # @license MIT https://opensource.org/licenses/mit-license.php + # @link https://github.com/pluginsGLPI/advancedforms + # ------------------------------------------------------------------------- + #} + +{% import 'components/form/fields_macros.html.twig' as fields %} + +{# We need to redefine the random to avoid an ID conflict #} +{% set options = options|merge({rand: random()}) %} + +
+
+ {{ fields.dropdownNumberField( + extra_time_offset_field.input_name, + extra_time_offset_field.value, + "", + options|merge({ + field_class: '', + mb: '', + no_label: true, + min: extra_time_offset_field.min, + max: extra_time_offset_field.max, + aria_label: extra_time_offset_field.aria_label, + }) + ) }} + {{ fields.dropdownArrayField( + extra_time_definition_field.input_name, + extra_time_definition_field.value, + extra_time_definition_field.possible_values, + "", + options|merge({ + field_class: '', + mb: '', + no_label: true, + aria_label: extra_time_definition_field.aria_label, + }) + ) }} +
+
diff --git a/templates/editor/destinations/strategies/computed_date_from_specific_date_answer_slm_strategy.html.twig b/templates/editor/destinations/strategies/computed_date_from_specific_date_answer_slm_strategy.html.twig new file mode 100644 index 0000000..6afd73f --- /dev/null +++ b/templates/editor/destinations/strategies/computed_date_from_specific_date_answer_slm_strategy.html.twig @@ -0,0 +1,81 @@ +{# + # ------------------------------------------------------------------------- + # advancedforms plugin for GLPI + # ------------------------------------------------------------------------- + # + # MIT License + # + # Permission is hereby granted, free of charge, to any person obtaining a copy + # of this software and associated documentation files (the "Software"), to deal + # in the Software without restriction, including without limitation the rights + # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + # copies of the Software, and to permit persons to whom the Software is + # furnished to do so, subject to the following conditions: + # + # The above copyright notice and this permission notice shall be included in all + # copies or substantial portions of the Software. + # + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + # SOFTWARE. + # ------------------------------------------------------------------------- + # @copyright Copyright (C) 2025 by the advancedforms plugin team. + # @copyright 2015-2025 Teclib' and contributors. + # @licence https://www.gnu.org/licenses/gpl-3.0.html + # @license MIT https://opensource.org/licenses/mit-license.php + # @link https://github.com/pluginsGLPI/advancedforms + # ------------------------------------------------------------------------- + #} + +{% import 'components/form/fields_macros.html.twig' as fields %} + +{# We need to redefine the random to avoid an ID conflict #} +{% set options = options|merge({rand: random()}) %} + +
+ {{ fields.dropdownArrayField( + extra_question_id_field.input_name, + extra_question_id_field.value, + extra_question_id_field.possible_values, + "", + options|merge({ + field_class: '', + mb: '', + no_label: true, + display_emptychoice: true, + emptylabel: extra_question_id_field.empty_label, + aria_label: extra_question_id_field.empty_label, + }) + ) }} +
+ {{ fields.dropdownNumberField( + extra_time_offset_field.input_name, + extra_time_offset_field.value, + "", + options|merge({ + field_class: '', + mb: '', + no_label: true, + min: extra_time_offset_field.min, + max: extra_time_offset_field.max, + aria_label: extra_time_offset_field.aria_label, + }) + ) }} + {{ fields.dropdownArrayField( + extra_time_definition_field.input_name, + extra_time_definition_field.value, + extra_time_definition_field.possible_values, + "", + options|merge({ + field_class: '', + mb: '', + no_label: true, + aria_label: extra_time_definition_field.aria_label, + }) + ) }} +
+
diff --git a/templates/editor/destinations/strategies/specific_date_answer_slm_strategy.html.twig b/templates/editor/destinations/strategies/specific_date_answer_slm_strategy.html.twig new file mode 100644 index 0000000..01cd077 --- /dev/null +++ b/templates/editor/destinations/strategies/specific_date_answer_slm_strategy.html.twig @@ -0,0 +1,51 @@ +{# + # ------------------------------------------------------------------------- + # advancedforms plugin for GLPI + # ------------------------------------------------------------------------- + # + # MIT License + # + # Permission is hereby granted, free of charge, to any person obtaining a copy + # of this software and associated documentation files (the "Software"), to deal + # in the Software without restriction, including without limitation the rights + # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + # copies of the Software, and to permit persons to whom the Software is + # furnished to do so, subject to the following conditions: + # + # The above copyright notice and this permission notice shall be included in all + # copies or substantial portions of the Software. + # + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + # SOFTWARE. + # ------------------------------------------------------------------------- + # @copyright Copyright (C) 2025 by the advancedforms plugin team. + # @copyright 2015-2025 Teclib' and contributors. + # @licence https://www.gnu.org/licenses/gpl-3.0.html + # @license MIT https://opensource.org/licenses/mit-license.php + # @link https://github.com/pluginsGLPI/advancedforms + # ------------------------------------------------------------------------- + #} + +{% import 'components/form/fields_macros.html.twig' as fields %} + +
+ {{ fields.dropdownArrayField( + extra_field.input_name, + extra_field.value, + extra_field.possible_values, + "", + options|merge({ + field_class: '', + mb: '', + no_label: true, + display_emptychoice: true, + emptylabel: extra_field.empty_label, + aria_label: extra_field.empty_label, + }) + ) }} +
diff --git a/tests/AdvancedFormsTestCase.php b/tests/AdvancedFormsTestCase.php index 8e1bbb2..11d31b6 100644 --- a/tests/AdvancedFormsTestCase.php +++ b/tests/AdvancedFormsTestCase.php @@ -35,6 +35,8 @@ use AuthLDAP; use Config; +use Glpi\Form\Destination\CommonITILField\SLMFieldStrategyInterface; +use Glpi\Form\Destination\FormDestinationManager; use Glpi\Form\Form; use Glpi\Form\Migration\TypesConversionMapper; use Glpi\Form\QuestionType\QuestionTypeInterface; @@ -61,6 +63,13 @@ final public static function provideQuestionTypes(): array return array_map(fn($c): array => [$c], $types); } + /** @return array */ + final public static function provideSLMDestinationStrategies(): array + { + $strategies = ConfigManager::getInstance()->getConfigurableSLMStrategies(); + return array_map(fn($c): array => [$c], $strategies); + } + public function setUp(): void { parent::setUp(); @@ -69,6 +78,7 @@ public function setUp(): void $this->deleteSingletonInstance([ QuestionTypesManager::class, TypesConversionMapper::class, + FormDestinationManager::class, ]); } @@ -130,7 +140,7 @@ protected function setupAuthLdap(): AuthLDAP ], ['rootdn_passwd']); } - protected function createFormWithLdapQuestion(AuthLdap $ldap): Form + protected function createFormWithLdapQuestion(AuthLDAP $ldap): Form { $builder = new FormBuilder("My form"); $builder->addQuestion( diff --git a/tests/Model/Destination/Strategies/AbstractSLMStrategyTestCase.php b/tests/Model/Destination/Strategies/AbstractSLMStrategyTestCase.php new file mode 100644 index 0000000..bf88dda --- /dev/null +++ b/tests/Model/Destination/Strategies/AbstractSLMStrategyTestCase.php @@ -0,0 +1,182 @@ + ['slm_field' => new SLATTOField(), 'slm_field_config_class' => SLATTOFieldConfig::class]; + yield 'SLATTRField' => ['slm_field' => new SLATTRField(), 'slm_field_config_class' => SLATTRFieldConfig::class]; + yield 'OLATTOField' => ['slm_field' => new OLATTOField(), 'slm_field_config_class' => OLATTOFieldConfig::class]; + yield 'OLATTRField' => ['slm_field' => new OLATTRField(), 'slm_field_config_class' => OLATTRFieldConfig::class]; + } + + #[DataProvider('provideSLMConfigFields')] + public function testSLMDestinationStrategyIsAvailableInStrategiesDropdownWhenEnabled( + SLMField $slm_field, + string $slm_field_config_class, + ): void { + $this->enableConfigurableItem($this->getTestedSLMStrategy()); + $form = $this->createAndGetFormWithTicketDestination(); + + $html = $this->renderFormDestination($form); + + $options = $html + ->filter(sprintf( + 'select[name="config[%s][%s][]"]', + $slm_field->getKey(), + SLMFieldConfig::getStrategiesInputName(), + )) + ->eq(0) + ->filter('option') + ->each(fn(Crawler $node) => $node->text()) + ; + $this->assertContains($this->getTestedSLMStrategy()->getLabel($slm_field), $options); + } + + #[DataProvider('provideSLMConfigFields')] + public function testSLMDestinationStrategyIsAvailableInStrategiesDropdownWhenDisabled( + SLMField $slm_field, + string $slm_field_config_class, + ): void { + $this->disableConfigurableItem($this->getTestedSLMStrategy()); + $form = $this->createAndGetFormWithTicketDestination(); + + $html = $this->renderFormDestination($form); + + $options = $html + ->filter(sprintf( + 'select[name="config[%s][%s][]"]', + $slm_field->getKey(), + SLMFieldConfig::getStrategiesInputName(), + )) + ->eq(0) + ->filter('option') + ->each(fn(Crawler $node) => $node->text()) + ; + $this->assertNotContains($this->getTestedSLMStrategy()->getLabel($slm_field), $options); + } + + protected function checkSLMFieldConfiguration( + SLMField $slm_field, + Form $form, + array $formatted_answers, + SLMFieldConfig $config, + string $expected_time, + ): Ticket { + $_SESSION['glpi_currenttime'] = '2025-12-29 10:00:00'; + + $destinations = $form->getDestinations(); + $this->assertCount(1, $destinations); + $destination = current($destinations); + $this->updateItem( + $destination::getType(), + $destination->getId(), + ['config' => [$slm_field::getKey() => $config->jsonSerialize()]], + ["config"], + ); + + $answers_handler = AnswersHandler::getInstance(); + $answers = $answers_handler->saveAnswers( + $form, + $formatted_answers, + getItemByTypeName(\User::class, TU_USER, true), + ); + + $created_items = $answers->getCreatedItems(); + $this->assertCount(1, $created_items); + $ticket = current($created_items); + + $time_field_name = $slm_field->getSLM()::getFieldNames($slm_field->getType())[0]; + + $this->assertEquals($expected_time, $ticket->fields[$time_field_name]); + + return $ticket; + } + + protected function createAndGetFormWithTicketDestination(): Form + { + $builder = new FormBuilder(); + $builder->addQuestion( + name: "Date question", + type: QuestionTypeDateTime::class, + default_value: '2025-12-31 15:30:00', + extra_data: json_encode( + new QuestionTypeDateTimeExtraDataConfig( + is_date_enabled: true, + is_time_enabled: true, + ), + ), + ); + return $this->createForm($builder); + } + + protected function renderFormDestination(Form $form): Crawler + { + $this->login(); + $controller = new GetDestinationFormController(); + return new Crawler($controller->__invoke( + Request::create(''), + $form->getID(), + current($form->getDestinations())->getID(), + )->getContent()); + } +} diff --git a/tests/Model/Destination/Strategies/ComputedFromSpecificDateAnswerSLMStrategyTest.php b/tests/Model/Destination/Strategies/ComputedFromSpecificDateAnswerSLMStrategyTest.php new file mode 100644 index 0000000..63e935e --- /dev/null +++ b/tests/Model/Destination/Strategies/ComputedFromSpecificDateAnswerSLMStrategyTest.php @@ -0,0 +1,141 @@ +enableConfigurableItem($this->getTestedSLMStrategy()); + + $this->login('normal'); + $form = $this->createAndGetFormWithTicketDestination(); + $question_id = current($form->getQuestionsByType(QuestionTypeDateTime::class))->getID(); + + $this->checkSLMFieldConfiguration( + slm_field: $slm_field, + form: $form, + formatted_answers: [$question_id => '2025-12-31 15:30:00'], + config: new $slm_field_config_class( + strategy: $this->getTestedSLMStrategy()->getKey(), + extra_data: [ + ComputedFromSpecificDateAnswerSLMStrategy::EXTRA_KEY_QUESTION_ID => $question_id, + ComputedFromSpecificDateAnswerSLMStrategy::EXTRA_KEY_TIME_OFFSET => '2', + ComputedFromSpecificDateAnswerSLMStrategy::EXTRA_KEY_TIME_DEFINITION => 'day', + ], + ), + expected_time: '2026-01-02 15:30:00', + ); + } + + #[DataProvider('provideSLMConfigFields')] + public function testOnlyDateTimeQuestionWithAtLeastDateEnabledAreProposed( + SLMField $slm_field, + string $slm_field_config_class, + ): void { + // Arrange: enable the strategy + $this->enableConfigurableItem($this->getTestedSLMStrategy()); + + // Arrange: create a form with various DateTime questions + $builder = new FormBuilder(); + $builder->addQuestion( + name: 'DateTime question with both date and time enabled', + type: QuestionTypeDateTime::class, + extra_data: json_encode(new QuestionTypeDateTimeExtraDataConfig( + is_date_enabled: true, + is_time_enabled: true, + )), + )->addQuestion( + name: 'DateTime question with only date enabled', + type: QuestionTypeDateTime::class, + extra_data: json_encode(new QuestionTypeDateTimeExtraDataConfig( + is_date_enabled: true, + is_time_enabled: false, + )), + )->addQuestion( + name: 'DateTime question with neither date nor time enabled', + type: QuestionTypeDateTime::class, + extra_data: json_encode(new QuestionTypeDateTimeExtraDataConfig( + is_date_enabled: false, + is_time_enabled: false, + )), + )->addQuestion( + name: 'DateTime question with only time enabled', + type: QuestionTypeDateTime::class, + extra_data: json_encode(new QuestionTypeDateTimeExtraDataConfig( + is_date_enabled: false, + is_time_enabled: true, + )), + ); + $form = $this->createForm($builder); + + // Act: render the form destination configuration + $html = $this->renderFormDestination($form); + + $questions = $html + ->filter(sprintf( + 'select[name="config[%s][%s][%s]"]', + $slm_field->getKey(), + SLMFieldConfig::EXTRA_DATA, + ComputedFromSpecificDateAnswerSLMStrategy::EXTRA_KEY_QUESTION_ID, + )) + ->eq(0) + ->filter('option') + ->each(fn(Crawler $node) => $node->text()) + ; + $this->assertContains('DateTime question with both date and time enabled', $questions); + $this->assertContains('DateTime question with only date enabled', $questions); + $this->assertNotContains('DateTime question with neither date nor time enabled', $questions); + $this->assertNotContains('DateTime question with only time enabled', $questions); + } +} diff --git a/tests/Model/Destination/Strategies/SpecificDateAnswerSLMStrategyTest.php b/tests/Model/Destination/Strategies/SpecificDateAnswerSLMStrategyTest.php new file mode 100644 index 0000000..d1023bf --- /dev/null +++ b/tests/Model/Destination/Strategies/SpecificDateAnswerSLMStrategyTest.php @@ -0,0 +1,139 @@ +enableConfigurableItem($this->getTestedSLMStrategy()); + + $this->login('normal'); + $form = $this->createAndGetFormWithTicketDestination(); + $question_id = current($form->getQuestionsByType(QuestionTypeDateTime::class))->getID(); + + $this->checkSLMFieldConfiguration( + slm_field: $slm_field, + form: $form, + formatted_answers: [$question_id => '2025-12-31 15:30:00'], + config: new $slm_field_config_class( + strategy: $this->getTestedSLMStrategy()->getKey(), + extra_data: [ + SpecificDateAnswerSLMStrategy::EXTRA_KEY_QUESTION_ID => $question_id, + ], + ), + expected_time: '2025-12-31 15:30:00', + ); + } + + #[DataProvider('provideSLMConfigFields')] + public function testOnlyDateTimeQuestionWithAtLeastDateEnabledAreProposed( + SLMField $slm_field, + string $slm_field_config_class, + ): void { + // Arrange: enable the strategy + $this->enableConfigurableItem($this->getTestedSLMStrategy()); + + // Arrange: create a form with various DateTime questions + $builder = new FormBuilder(); + $builder->addQuestion( + name: 'DateTime question with both date and time enabled', + type: QuestionTypeDateTime::class, + extra_data: json_encode(new QuestionTypeDateTimeExtraDataConfig( + is_date_enabled: true, + is_time_enabled: true, + )), + )->addQuestion( + name: 'DateTime question with only date enabled', + type: QuestionTypeDateTime::class, + extra_data: json_encode(new QuestionTypeDateTimeExtraDataConfig( + is_date_enabled: true, + is_time_enabled: false, + )), + )->addQuestion( + name: 'DateTime question with neither date nor time enabled', + type: QuestionTypeDateTime::class, + extra_data: json_encode(new QuestionTypeDateTimeExtraDataConfig( + is_date_enabled: false, + is_time_enabled: false, + )), + )->addQuestion( + name: 'DateTime question with only time enabled', + type: QuestionTypeDateTime::class, + extra_data: json_encode(new QuestionTypeDateTimeExtraDataConfig( + is_date_enabled: false, + is_time_enabled: true, + )), + ); + $form = $this->createForm($builder); + + // Act: render the form destination configuration + $html = $this->renderFormDestination($form); + + $questions = $html + ->filter(sprintf( + 'select[name="config[%s][%s][%s]"]', + $slm_field->getKey(), + SLMFieldConfig::EXTRA_DATA, + SpecificDateAnswerSLMStrategy::EXTRA_KEY_QUESTION_ID, + )) + ->eq(0) + ->filter('option') + ->each(fn(Crawler $node) => $node->text()) + ; + $this->assertContains('DateTime question with both date and time enabled', $questions); + $this->assertContains('DateTime question with only date enabled', $questions); + $this->assertNotContains('DateTime question with neither date nor time enabled', $questions); + $this->assertNotContains('DateTime question with only time enabled', $questions); + } +} diff --git a/tests/Service/ConfigManagerTest.php b/tests/Service/ConfigManagerTest.php index 453e8b4..3d8730a 100644 --- a/tests/Service/ConfigManagerTest.php +++ b/tests/Service/ConfigManagerTest.php @@ -44,6 +44,7 @@ final class ConfigManagerTest extends AdvancedFormsTestCase { #[DataProvider('provideQuestionTypes')] + #[DataProvider('provideSLMDestinationStrategies')] public function testQuestionTypeConfigFormWhenEnabled( ConfigurableItemInterface $item, ): void { @@ -66,6 +67,7 @@ public function testQuestionTypeConfigFormWhenEnabled( } #[DataProvider('provideQuestionTypes')] + #[DataProvider('provideSLMDestinationStrategies')] public function testQuestionTypeConfigFormWhenDisabled( ConfigurableItemInterface $item, ): void { @@ -87,6 +89,7 @@ public function testQuestionTypeConfigFormWhenDisabled( } #[DataProvider('provideQuestionTypes')] + #[DataProvider('provideSLMDestinationStrategies')] public function testQuestionTypeConfigValueWhenEnabled( ConfigurableItemInterface $item, ): void { @@ -102,6 +105,7 @@ public function testQuestionTypeConfigValueWhenEnabled( } #[DataProvider('provideQuestionTypes')] + #[DataProvider('provideSLMDestinationStrategies')] public function testQuestionTypeConfigValueWhenDisabled( ConfigurableItemInterface $item, ): void { diff --git a/tests/Service/InitManagerTest.php b/tests/Service/InitManagerTest.php index a6d44c4..8e6f7fb 100644 --- a/tests/Service/InitManagerTest.php +++ b/tests/Service/InitManagerTest.php @@ -33,6 +33,7 @@ namespace GlpiPlugin\Advancedforms\Tests\Service; +use Glpi\Form\Destination\FormDestinationManager; use Glpi\Form\Migration\TypesConversionMapper; use Glpi\Form\QuestionType\QuestionTypesManager; use GlpiPlugin\Advancedforms\Model\Config\ConfigurableItemInterface; @@ -153,4 +154,42 @@ public function testQuestionTypeHiddenIsNotMappedInConverterWhenDisabled(): void // Assert: the ip address question type should only be found after enabling $this->assertNull($mapped_types['hidden']); } + + #[DataProvider('provideSLMDestinationStrategies')] + public function testSLMDestinationStrategieIsAvailableWhenEnabled( + ConfigurableItemInterface $item, + ): void { + // Arrange: enable slm destination strategy + $this->enableConfigurableItem($item); + + // Act: get available strategies + $manager = FormDestinationManager::getInstance(); + $strategies = $manager->getSLMFieldStrategies(); + + // Assert: the strategy should only be found after enabling + $classes = array_map( + fn($strategy) => $strategy::class, + $strategies, + ); + $this->assertContains($item::class, $classes); + } + + #[DataProvider('provideSLMDestinationStrategies')] + public function testSLMDestinationStrategieIsNotAvailableWhenDisabled( + ConfigurableItemInterface $item, + ): void { + // Arrange: disable slm destination strategy + $this->disableConfigurableItem($item); + + // Act: get available strategies + $manager = FormDestinationManager::getInstance(); + $types = $manager->getSLMFieldStrategies(); + + // Assert: the strategy should only be found after enabling + $classes = array_map( + fn($strategy) => $strategy::class, + $types, + ); + $this->assertNotContains($item::class, $classes); + } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 38d5ab0..b2d4103 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -43,3 +43,4 @@ require __DIR__ . "/Front/FrontTestCase.php"; require __DIR__ . "/Model/Mapper/MapperTestCase.php"; require __DIR__ . "/Model/QuestionType/QuestionTypeTestCase.php"; +require __DIR__ . "/Model/Destination/Strategies/AbstractSLMStrategyTestCase.php";