diff --git a/css/backoffice/components/_form.scss b/css/backoffice/components/_form.scss index 63db77757..510a7a0ca 100644 --- a/css/backoffice/components/_form.scss +++ b/css/backoffice/components/_form.scss @@ -54,12 +54,3 @@ .ibo-field legend{ margin-top: 24px; } - -// to fix 3.3 -.ts-wrapper{ - border: 1px solid #aebecd!important; -} -.ts-dropdown{ - background-color: #fff; - border: 1px solid #aebecd!important; -} \ No newline at end of file diff --git a/js/forms/custom-elements/tom-select.js b/js/forms/custom-elements/choices.js similarity index 88% rename from js/forms/custom-elements/tom-select.js rename to js/forms/custom-elements/choices.js index 8b28c4fde..efced185d 100644 --- a/js/forms/custom-elements/tom-select.js +++ b/js/forms/custom-elements/choices.js @@ -1,6 +1,7 @@ -class TomSelectElement extends HTMLSelectElement { +class ChoicesElement extends HTMLSelectElement { plugins = []; connectedCallback() { + if (this.getAttribute('multiple')) { this.plugins.push('remove_button'); } @@ -30,4 +31,4 @@ class TomSelectElement extends HTMLSelectElement { } } -customElements.define('tom-select-element', TomSelectElement, {extends: 'select'}); +customElements.define('choices-element', ChoicesElement, {extends: 'select'}); diff --git a/js/forms/custom-elements/oql.js b/js/forms/custom-elements/oql.js new file mode 100644 index 000000000..3bfc1e5df --- /dev/null +++ b/js/forms/custom-elements/oql.js @@ -0,0 +1,70 @@ +class OqlElement extends HTMLTextAreaElement { + + // register the custom element + static{ + customElements.define('oql-element', OqlElement, {extends: 'textarea'}); + } + + // variables + #url = '../pages/ajax.render.php?route=oql.validate_query'; + #iconValid = 'fa-check-double'; + #iconNotValid = 'fa-exclamation-triangle'; + #debounceTimer = null; + #debounce = 300; + + /** connectedCallback **/ + connectedCallback() { + this.addEventListener('input', this.#onInput.bind(this)); + this.#callValidateQuery(); + } + + /** + * Call oql verification with debounce when input event is fired. + */ + #onInput() { + if (this.#debounceTimer) clearTimeout(this.#debounceTimer); + this.#debounceTimer = setTimeout(() => { + this.#callValidateQuery(true); + }, this.#debounce); + } + + /** + * Call the ajax to validate the query. + * + * @param fireChange flag to handle change event + */ + #callValidateQuery(fireChange = false) { + + fetch(this.#url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Combodo-Ajax': true + }, + body: JSON.stringify({ + query: this.value + }) + }) + .then(response => response.json()) + .then(response => { + // fire change event only if the query is valid + if (fireChange && response.is_valid){ + this.#fireChangeEvent(); + } + // update the icon color + const fieldEl = this.closest('.ibo-field'); + const marqueeEl = fieldEl.querySelector('[role="marquee"]'); + marqueeEl.style.color = response.is_valid ? 'green' : 'orange'; + marqueeEl.classList.toggle(this.#iconNotValid, !response.is_valid); + marqueeEl.classList.toggle(this.#iconValid, response.is_valid); + }); + } + + /** + * Fire a change event. + */ + #fireChangeEvent() { + const changeEvent = new Event('change', { bubbles: true, cancelable: true }); + this.dispatchEvent(changeEvent); + } +} diff --git a/js/forms/forms.js b/js/forms/forms.js index d7f75e31d..7e36cd763 100644 --- a/js/forms/forms.js +++ b/js/forms/forms.js @@ -3,12 +3,25 @@ * @license http://opensource.org/licenses/AGPL-3.0 */ +$aFormBlockDataTransmittedData = {}; + function triggerTurbo(el) { - let sFormName = el.form.getAttribute("name"); - el.form.querySelector(`[name="${sFormName}[_turbo_trigger]"]`).value = el.getAttribute('name'); - el.form.setAttribute('novalidate', true); - el.form.requestSubmit(); - console.log('Auto submitting form due to change in field ' + el.getAttribute('name')); + + const name = el.getAttribute('name'); + + if(isCheckbox(el) || $aFormBlockDataTransmittedData[name] !== el.value) { + let sFormName = el.form.getAttribute("name"); + el.form.querySelector(`[name="${sFormName}[_turbo_trigger]"]`).value = el.getAttribute('name'); + el.form.setAttribute('novalidate', true); + el.form.requestSubmit(); + $aFormBlockDataTransmittedData[name] = el.value; + } + +} + +function isCheckbox (element) { + return element instanceof HTMLInputElement + && element.getAttribute('type') === 'checkbox' } function addFormToCollection(e) { diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 9ca5f0f4d..eb3d8c873 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -345,6 +345,7 @@ return array( 'Combodo\\iTop\\Controller\\AjaxRenderController' => $baseDir . '/sources/Controller/AjaxRenderController.php', 'Combodo\\iTop\\Controller\\Base\\Layout\\ActivityPanelController' => $baseDir . '/sources/Controller/Base/Layout/ActivityPanelController.php', 'Combodo\\iTop\\Controller\\Base\\Layout\\ObjectController' => $baseDir . '/sources/Controller/Base/Layout/ObjectController.php', + 'Combodo\\iTop\\Controller\\Base\\Layout\\OqlController' => $baseDir . '/sources/Controller/Base/Layout/OqlController.php', 'Combodo\\iTop\\Controller\\Links\\LinkSetController' => $baseDir . '/sources/Controller/Links/LinkSetController.php', 'Combodo\\iTop\\Controller\\Newsroom\\iTopNewsroomController' => $baseDir . '/sources/Controller/Newsroom/iTopNewsroomController.php', 'Combodo\\iTop\\Controller\\Notifications\\ActionController' => $baseDir . '/sources/Controller/Notifications/ActionController.php', @@ -496,6 +497,7 @@ return array( 'Combodo\\iTop\\Forms\\Block\\Expression\\BooleanExpressionFormBlock' => $baseDir . '/sources/Forms/Block/Expression/BooleanExpressionFormBlock.php', 'Combodo\\iTop\\Forms\\Block\\Expression\\NumberExpressionFormBlock' => $baseDir . '/sources/Forms/Block/Expression/NumberExpressionFormBlock.php', 'Combodo\\iTop\\Forms\\Block\\FormBlockException' => $baseDir . '/sources/Forms/Block/FormBlockException.php', + 'Combodo\\iTop\\Forms\\Block\\FormBlockHelper' => $baseDir . '/sources/Forms/Block/FormBlockHelper.php', 'Combodo\\iTop\\Forms\\FormBuilder\\DependencyHandler' => $baseDir . '/sources/Forms/FormBuilder/DependencyHandler.php', 'Combodo\\iTop\\Forms\\FormBuilder\\DependencyMap' => $baseDir . '/sources/Forms/FormBuilder/DependencyMap.php', 'Combodo\\iTop\\Forms\\FormBuilder\\FormBuilder' => $baseDir . '/sources/Forms/FormBuilder/FormBuilder.php', @@ -3668,5 +3670,5 @@ return array( 'privUITransactionFile' => $baseDir . '/application/transaction.class.inc.php', 'privUITransactionSession' => $baseDir . '/application/transaction.class.inc.php', 'utils' => $baseDir . '/application/utils.inc.php', - '©' => $vendorDir . '/symfony/cache/Traits/ValueWrapper.php', + '�' => $vendorDir . '/symfony/cache/Traits/ValueWrapper.php', ); diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 77ce8085e..fdd414028 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -726,6 +726,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Controller\\AjaxRenderController' => __DIR__ . '/../..' . '/sources/Controller/AjaxRenderController.php', 'Combodo\\iTop\\Controller\\Base\\Layout\\ActivityPanelController' => __DIR__ . '/../..' . '/sources/Controller/Base/Layout/ActivityPanelController.php', 'Combodo\\iTop\\Controller\\Base\\Layout\\ObjectController' => __DIR__ . '/../..' . '/sources/Controller/Base/Layout/ObjectController.php', + 'Combodo\\iTop\\Controller\\Base\\Layout\\OqlController' => __DIR__ . '/../..' . '/sources/Controller/Base/Layout/OqlController.php', 'Combodo\\iTop\\Controller\\Links\\LinkSetController' => __DIR__ . '/../..' . '/sources/Controller/Links/LinkSetController.php', 'Combodo\\iTop\\Controller\\Newsroom\\iTopNewsroomController' => __DIR__ . '/../..' . '/sources/Controller/Newsroom/iTopNewsroomController.php', 'Combodo\\iTop\\Controller\\Notifications\\ActionController' => __DIR__ . '/../..' . '/sources/Controller/Notifications/ActionController.php', @@ -877,6 +878,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Forms\\Block\\Expression\\BooleanExpressionFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Expression/BooleanExpressionFormBlock.php', 'Combodo\\iTop\\Forms\\Block\\Expression\\NumberExpressionFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Expression/NumberExpressionFormBlock.php', 'Combodo\\iTop\\Forms\\Block\\FormBlockException' => __DIR__ . '/../..' . '/sources/Forms/Block/FormBlockException.php', + 'Combodo\\iTop\\Forms\\Block\\FormBlockHelper' => __DIR__ . '/../..' . '/sources/Forms/Block/FormBlockHelper.php', 'Combodo\\iTop\\Forms\\FormBuilder\\DependencyHandler' => __DIR__ . '/../..' . '/sources/Forms/FormBuilder/DependencyHandler.php', 'Combodo\\iTop\\Forms\\FormBuilder\\DependencyMap' => __DIR__ . '/../..' . '/sources/Forms/FormBuilder/DependencyMap.php', 'Combodo\\iTop\\Forms\\FormBuilder\\FormBuilder' => __DIR__ . '/../..' . '/sources/Forms/FormBuilder/FormBuilder.php', @@ -4049,7 +4051,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'privUITransactionFile' => __DIR__ . '/../..' . '/application/transaction.class.inc.php', 'privUITransactionSession' => __DIR__ . '/../..' . '/application/transaction.class.inc.php', 'utils' => __DIR__ . '/../..' . '/application/utils.inc.php', - '©' => __DIR__ . '/..' . '/symfony/cache/Traits/ValueWrapper.php', + '�' => __DIR__ . '/..' . '/symfony/cache/Traits/ValueWrapper.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/sources/Application/WebPage/iTopWebPage.php b/sources/Application/WebPage/iTopWebPage.php index eab0c6789..593c9fcc8 100644 --- a/sources/Application/WebPage/iTopWebPage.php +++ b/sources/Application/WebPage/iTopWebPage.php @@ -182,7 +182,8 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage $this->LinkScriptFromAppRoot('node_modules/selectize-plugin-a11y/selectize-plugin-a11y.js'); $this->LinkScriptFromAppRoot('js/jquery.multiselect.js'); $this->LinkScriptFromAppRoot('node_modules/tom-select/dist/js/tom-select.complete.min.js'); - $this->LinkScriptFromAppRoot('js/forms/custom-elements/tom-select.js'); + $this->LinkScriptFromAppRoot('js/forms/custom-elements/choices.js'); + $this->LinkScriptFromAppRoot('js/forms/custom-elements/oql.js'); // Used by inline image, CKEditor and other places $this->LinkScriptFromAppRoot('node_modules/magnific-popup/dist/jquery.magnific-popup.min.js'); diff --git a/sources/Controller/Base/Layout/OqlController.php b/sources/Controller/Base/Layout/OqlController.php new file mode 100644 index 000000000..95d0ec043 --- /dev/null +++ b/sources/Controller/Base/Layout/OqlController.php @@ -0,0 +1,45 @@ +SetOutputDataOnly(true); + + $data = json_decode(file_get_contents('php://input'), true); + $sOql = $data['query']; + + try{ + /** @var ModelReflection $oModelReflection */ + $oModelReflexion = new ModelReflectionRuntime(); + $oModelReflexion->GetQuery($sOql); + } + catch(Exception $e){ + + } + + $oPage->SetData([ + 'is_valid' => !isset($e), + ]); + + $oPage->output(); + } + +} diff --git a/sources/Forms/Block/AbstractFormBlock.php b/sources/Forms/Block/AbstractFormBlock.php index a1528d9a5..694f227dc 100644 --- a/sources/Forms/Block/AbstractFormBlock.php +++ b/sources/Forms/Block/AbstractFormBlock.php @@ -94,7 +94,7 @@ abstract class AbstractFormBlock implements IFormBlock */ public function IsRootBlock(): bool { - return $this->oParent !== null; + return $this->oParent === null; } /** diff --git a/sources/Forms/Block/AbstractTypeFormBlock.php b/sources/Forms/Block/AbstractTypeFormBlock.php index 0e80d3d8f..1f0cc0961 100644 --- a/sources/Forms/Block/AbstractTypeFormBlock.php +++ b/sources/Forms/Block/AbstractTypeFormBlock.php @@ -93,7 +93,8 @@ abstract class AbstractTypeFormBlock extends AbstractFormBlock parent::UpdateOptions($oOptionsRegister); if ($this->GetInput(self::INPUT_ENABLE)->IsBound()) { - $oOptionsRegister->SetOption('disabled', !$this->GetInputValue(self::INPUT_ENABLE)); + $test = $this->GetInputValue(self::INPUT_ENABLE)->IsTrue(); + $oOptionsRegister->SetOption('disabled', !$this->GetInputValue(self::INPUT_ENABLE)->IsTrue()); } } } diff --git a/sources/Forms/Block/Base/ChoiceFormBlock.php b/sources/Forms/Block/Base/ChoiceFormBlock.php index 047751651..bdca2a9af 100644 --- a/sources/Forms/Block/Base/ChoiceFormBlock.php +++ b/sources/Forms/Block/Base/ChoiceFormBlock.php @@ -21,7 +21,7 @@ class ChoiceFormBlock extends AbstractTypeFormBlock { // Outputs public const OUTPUT_LABEL = 'label'; - public const OUTPUT_CODE = 'code'; + public const OUTPUT_VALUE = 'value'; /** @inheritdoc */ public function GetFormType(): string @@ -34,6 +34,6 @@ class ChoiceFormBlock extends AbstractTypeFormBlock { parent::RegisterIO($oIORegister); $oIORegister->AddOutput(self::OUTPUT_LABEL, StringIOFormat::class, new ChoiceValueToLabelConverter($this)); - $oIORegister->AddOutput(self::OUTPUT_CODE, StringIOFormat::class); + $oIORegister->AddOutput(self::OUTPUT_VALUE, StringIOFormat::class); } } diff --git a/sources/Forms/Block/DataModel/AttributeValueChoiceFormBlock.php b/sources/Forms/Block/DataModel/AttributeValueChoiceFormBlock.php index e44863f7b..de9f531b6 100644 --- a/sources/Forms/Block/DataModel/AttributeValueChoiceFormBlock.php +++ b/sources/Forms/Block/DataModel/AttributeValueChoiceFormBlock.php @@ -27,9 +27,6 @@ class AttributeValueChoiceFormBlock extends ChoiceFormBlock public const INPUT_CLASS_NAME = 'class_name'; public const INPUT_ATTRIBUTE = 'attribute'; - // Outputs - public const OUTPUT_VALUE = 'value'; - /** @inheritdoc */ protected function RegisterOptions(OptionsRegister $oOptionsRegister): void { @@ -45,7 +42,6 @@ class AttributeValueChoiceFormBlock extends ChoiceFormBlock parent::RegisterIO($oIORegister); $oIORegister->AddInput(self::INPUT_CLASS_NAME, ClassIOFormat::class); $oIORegister->AddInput(self::INPUT_ATTRIBUTE, AttributeIOFormat::class); - $oIORegister->AddOutput(self::OUTPUT_VALUE, AttributeIOFormat::class); } /** @inheritdoc diff --git a/sources/Forms/Block/Expression/AbstractExpressionFormBlock.php b/sources/Forms/Block/Expression/AbstractExpressionFormBlock.php index e73196c2f..96beb8f9b 100644 --- a/sources/Forms/Block/Expression/AbstractExpressionFormBlock.php +++ b/sources/Forms/Block/Expression/AbstractExpressionFormBlock.php @@ -47,7 +47,7 @@ abstract class AbstractExpressionFormBlock extends AbstractFormBlock $aParamsToResolve = $oExpression->GetParameters(); $aResolvedParams = []; foreach ($aParamsToResolve as $sParamToResolve) { - $aResolvedParams[$sParamToResolve] = $this->GetInputValue($sParamToResolve); + $aResolvedParams[$sParamToResolve] = strval($this->GetInputValue($sParamToResolve)); } return $oExpression->Evaluate($aResolvedParams); } catch (\Exception $e) { diff --git a/sources/Forms/Block/FormBlockHelper.php b/sources/Forms/Block/FormBlockHelper.php new file mode 100644 index 000000000..e855aa95c --- /dev/null +++ b/sources/Forms/Block/FormBlockHelper.php @@ -0,0 +1,25 @@ +getParent())) { + return $oForm->getName(); + } + return self::GetFormId($oForm->getParent()).'_'.$oForm->getName(); + } +} \ No newline at end of file diff --git a/sources/Forms/IO/FormOutput.php b/sources/Forms/IO/FormOutput.php index 5eed07aba..fcffc4c8a 100644 --- a/sources/Forms/IO/FormOutput.php +++ b/sources/Forms/IO/FormOutput.php @@ -45,7 +45,7 @@ class FormOutput extends AbstractFormIO { if (is_null($this->oConverter)) { $sType = $this->GetDataType(); - return new $sType($oData); + return $oData !== null ? new $sType($oData) : null; } return $this->oConverter->Convert($oData); diff --git a/sources/Forms/Register/IORegister.php b/sources/Forms/Register/IORegister.php index 081b46992..736fb3baa 100644 --- a/sources/Forms/Register/IORegister.php +++ b/sources/Forms/Register/IORegister.php @@ -8,8 +8,12 @@ namespace Combodo\iTop\Forms\Register; use Combodo\iTop\Forms\Block\AbstractFormBlock; +use Combodo\iTop\Forms\Block\Base\FormBlock; use Combodo\iTop\Forms\Block\FormBlockException; +use Combodo\iTop\Forms\Block\FormBlockHelper; +use Combodo\iTop\Forms\FormType\FormTypeHelper; use Combodo\iTop\Forms\IO\Converter\AbstractConverter; +use Combodo\iTop\Forms\IO\FormBlockIOException; use Combodo\iTop\Forms\IO\FormInput; use Combodo\iTop\Forms\IO\FormOutput; @@ -25,7 +29,7 @@ class IORegister private array $aOutputs = []; /** - * @param \Combodo\iTop\Forms\Block\AbstractFormBlock $oFormBlock + * @param AbstractFormBlock $oFormBlock */ public function __construct(private readonly AbstractFormBlock $oFormBlock) { @@ -35,16 +39,18 @@ class IORegister * @param string $sName * @param string $sType * - * @return void - * @throws \Combodo\iTop\Forms\IO\FormBlockIOException + * @return $this + * @throws FormBlockIOException */ - public function AddInput(string $sName, string $sType): void + public function AddInput(string $sName, string $sType): self { $oFormInput = new FormInput($sName, $sType, $this->oFormBlock); if (array_key_exists($oFormInput->GetName(), $this->aInputs)) { throw new RegisterException('Input already exists '.json_encode($oFormInput->GetName()).' for '.json_encode($this->oFormBlock->GetName())); } $this->aInputs[$oFormInput->GetName()] = $oFormInput; + + return $this; } /** @@ -55,9 +61,9 @@ class IORegister * @param string $sOutputName * * @return $this - * @throws \Combodo\iTop\Forms\Block\FormBlockException - * @throws \Combodo\iTop\Forms\IO\FormBlockIOException - * @throws \Combodo\iTop\Forms\Register\RegisterException + * @throws FormBlockException + * @throws FormBlockIOException + * @throws RegisterException */ public function AddInputDependsOn(string $sName, string $sOutputBlockName, string $sOutputName): self { @@ -78,9 +84,9 @@ class IORegister * @param string $sOutputName the dependency output name * * @return $this - * @throws \Combodo\iTop\Forms\Block\FormBlockException - * @throws \Combodo\iTop\Forms\IO\FormBlockIOException - * @throws \Combodo\iTop\Forms\Register\RegisterException + * @throws FormBlockException + * @throws FormBlockIOException + * @throws RegisterException */ public function DependsOn(string $sInputName, string $sOutputBlockName, string $sOutputName): self { @@ -102,9 +108,9 @@ class IORegister * @param string $sParentOutputName parent output name * * @return $this - * @throws \Combodo\iTop\Forms\Block\FormBlockException - * @throws \Combodo\iTop\Forms\IO\FormBlockIOException - * @throws \Combodo\iTop\Forms\Register\RegisterException + * @throws FormBlockException + * @throws FormBlockIOException + * @throws RegisterException */ public function ImpactParent(string $sOutputName, string $sParentOutputName): self { @@ -119,7 +125,7 @@ class IORegister { $oFormOutput = new FormOutput($sName, $sType, $this->oFormBlock, $oConverter); if (array_key_exists($oFormOutput->GetName(), $this->aOutputs)) { - throw new RegisterException('Output already exists '.json_encode($oFormOutput->GetName()).' for '.json_encode($this->oFormBlock->GetName())); + throw new RegisterException('Output already exists '.json_encode($oFormOutput->GetName()).' for '.json_encode($this->oFormBlock->GetName()) . ' in block ' . FormBlockHelper::GetFormId($this->oFormBlock) . ' of class ' . get_class($this->oFormBlock)); } $this->aOutputs[$oFormOutput->GetName()] = $oFormOutput; } @@ -130,17 +136,29 @@ class IORegister * @param string $sName * * @return FormInput - * @throws \Combodo\iTop\Forms\Register\RegisterException + * @throws RegisterException */ public function GetInput(string $sName): FormInput { - if (!array_key_exists($sName, $this->aInputs)) { + if (!$this->HasInput($sName)) { throw new RegisterException('Missing input '.json_encode($sName).' for '.json_encode($this->oFormBlock->GetName())); } return $this->aInputs[$sName]; } + /** + * Test input existence. + * + * @param string $sName + * + * @return bool + */ + public function HasInput(string $sName): bool + { + return array_key_exists($sName, $this->aInputs); + } + /** * @return array */ @@ -189,7 +207,7 @@ class IORegister * @param string $sName output name * * @return FormOutput - * @throws \Combodo\iTop\Forms\Register\RegisterException + * @throws RegisterException */ public function GetOutput(string $sName): FormOutput { @@ -326,9 +344,9 @@ class IORegister * @param string $sParentInputName parent input name * * @return $this - * @throws \Combodo\iTop\Forms\Block\FormBlockException - * @throws \Combodo\iTop\Forms\IO\FormBlockIOException - * @throws \Combodo\iTop\Forms\Register\RegisterException + * @throws FormBlockException + * @throws FormBlockIOException + * @throws RegisterException */ public function DependsOnParent(string $sInputName, string $sParentInputName): self { diff --git a/templates/application/forms/itop_console_layout.html.twig b/templates/application/forms/itop_console_layout.html.twig index b5bd15535..f5f6e2b0e 100644 --- a/templates/application/forms/itop_console_layout.html.twig +++ b/templates/application/forms/itop_console_layout.html.twig @@ -23,7 +23,7 @@ {%- block choice_widget_collapsed -%} {% set attr = attr|merge({class: (attr.class|default('') ~ ' ibo-input')|trim}) %} - + {% if with_ai_button is defined and with_ai_button %} + + {% endif %} + +{%- endblock oql_form_widget -%} diff --git a/tests/php-unit-tests/phpunit.xml.dist b/tests/php-unit-tests/phpunit.xml.dist index 0129e2c3d..6b4515775 100644 --- a/tests/php-unit-tests/phpunit.xml.dist +++ b/tests/php-unit-tests/phpunit.xml.dist @@ -69,10 +69,10 @@ ../../core/apc-emulation.php ../../core/ormlinkset.class.inc.php ../../datamodels/2.x/itop-tickets/main.itop-tickets.php - ../../sources/Forms/Block/AbstractTypeFormBlock.php - ../../sources/Forms/Block/AbstractFormBlock.php - ../../sources/Forms/Block/Base/FormBlock.php - + + + + ../../sources/Forms/ diff --git a/tests/php-unit-tests/unitary-tests/sources/Forms/AbstractFormsTest.php b/tests/php-unit-tests/unitary-tests/sources/Forms/AbstractFormsTest.php index 32161ccab..624c60729 100644 --- a/tests/php-unit-tests/unitary-tests/sources/Forms/AbstractFormsTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/Forms/AbstractFormsTest.php @@ -12,7 +12,9 @@ use Combodo\iTop\Forms\Block\Base\FormBlock; use Combodo\iTop\Forms\IO\Format\StringIOFormat; use Combodo\iTop\Forms\IO\FormInput; use Combodo\iTop\Forms\IO\FormOutput; +use Combodo\iTop\Forms\Register\IORegister; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; +use ReflectionClass; /** * @copyright Copyright (C) 2010-2025 Combodo SARL @@ -35,15 +37,23 @@ abstract class AbstractFormsTest extends ItopDataTestCase return new FormOutput($sName.'_output', $sType, $oBlock); } - public function GivenFormBlock(string $sName, string $sBlockClass = FormBlock::class): AbstractFormBlock + public function GivenFormBlock(string $sName): FormBlock { - return new $sBlockClass($sName, []); + return new FormBlock($sName, []); } - public function GivenSubFormBlock(AbstractFormBlock $oParent, string $sName, string $ssBlockClass = FormBlock::class): AbstractFormBlock + public function GivenSubFormBlock(FormBlock $oParent, string $sName, string $ssBlockClass = FormBlock::class): AbstractFormBlock { $oParent->Add($sName, $ssBlockClass, []); return $oParent->Get($sName); } + + public function GetIORegister(AbstractFormBlock $oFormBlock): IORegister + { + $reflection = new ReflectionClass(AbstractFormBlock::class); + $reflection_property = $reflection->getProperty('oIORegister'); + $reflection_property->setAccessible(true); + return $reflection_property->getValue($oFormBlock); + } } diff --git a/tests/php-unit-tests/unitary-tests/sources/Forms/Block/BlockTest.php b/tests/php-unit-tests/unitary-tests/sources/Forms/Block/BlockTest.php index 3ed2c1075..ca6ce247c 100644 --- a/tests/php-unit-tests/unitary-tests/sources/Forms/Block/BlockTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/Forms/Block/BlockTest.php @@ -113,56 +113,14 @@ class BlockTest extends AbstractFormsTest $oForm->get('birthdate'); } - public function testAddingTwiceTheSameInputThrowsException(): void + public function testIsRootBlock(): void { - $oFormBlock = $this->GivenFormBlock('OneBlock') - ->AddInput('test_input', StringIOFormat::class); + /** @var FormBlock $oFormBlock */ + $oFormBlock = $this->GivenFormBlock('OneBlock'); - $this->expectException(RegisterException::class); - $oFormBlock->AddInput('test_input', StringIOFormat::class); - } + $oFormBlock->Add('subform', FormBlock::class); - public function testAddingTwiceTheSameOutputThrowsException(): void - { - $oFormBlock = $this->GivenFormBlock('OneBlock') - ->AddOutput('test_output', StringIOFormat::class); - - $this->expectException(RegisterException::class); - $oFormBlock->AddOutput('test_output', StringIOFormat::class); - } - - public function testDependingOnNonExistingInputThrowsException(): void - { - $oParentBlock = $this->GivenFormBlock('ParentBlock'); - - $oFormBlock = $this->GivenSubFormBlock($oParentBlock, 'OneBlock') - ->AddInput('test_input', StringIOFormat::class); - - $this->GivenSubFormBlock($oParentBlock, 'OtherBlock') - ->AddOutput('test_output', StringIOFormat::class); - - $this->expectException(RegisterException::class); - $oFormBlock->DependsOn('non_existing_input', 'OtherBlock', 'test_output'); - } - - public function testDependingOnNonExistingOutputThrowsException(): void - { - $oParentBlock = $this->GivenFormBlock('ParentBlock'); - $oFormBlock = $this->GivenSubFormBlock($oParentBlock, 'OneBlock') - ->AddInput('test_input', StringIOFormat::class); - $this->GivenSubFormBlock($oParentBlock, 'OtherBlock') - ->AddOutput('test_output', StringIOFormat::class); - - $this->expectException(RegisterException::class); - $oFormBlock->DependsOn('test_input', 'OtherBlock', 'non_existing_output'); - } - - public function testDependingOnNonExistingBlockThrowsException(): void - { - $oFormBlock = $this->GivenFormBlock('OneBlock') - ->AddOutput('test_output', StringIOFormat::class); - - $this->expectException(RegisterException::class); - $oFormBlock->DependsOn('test_input', 'UnknownBlock', 'test'); + $this->assertTrue($oFormBlock->IsRootBlock()); + $this->assertFalse($oFormBlock->Get('subform')->IsRootBlock()); } } diff --git a/tests/php-unit-tests/unitary-tests/sources/Forms/Register/IORegisterTest.php b/tests/php-unit-tests/unitary-tests/sources/Forms/Register/IORegisterTest.php new file mode 100644 index 000000000..841d3ac15 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/sources/Forms/Register/IORegisterTest.php @@ -0,0 +1,194 @@ +oFormBlock = $this->GivenFormBlock('OneBlock'); + $this->oIORegister = $this->GetIORegister($this->oFormBlock); + } + + public function testAddInput(): void + { + $this->oIORegister->AddInput('input', StringIOFormat::class); + + $this->assertTrue($this->oIORegister->HasInput('input')); + $this->assertNotNull($this->oIORegister->GetInput('input')); + } + + public function testGetInputs(): void + { + $iOriginInputCount = count($this->oIORegister->GetInputs()); + $this->oIORegister->AddInput('input_1', StringIOFormat::class); + $this->oIORegister->AddInput('input_2', BooleanIOFormat::class); + $this->oIORegister->AddInput('input_3', StringIOFormat::class); + + $this->assertCount(3 + $iOriginInputCount, $this->oIORegister->GetInputs()); + $this->assertArrayHasKey('input_1', $this->oIORegister->GetInputs()); + } + + public function testGetOutputs(): void + { + $this->oIORegister->AddOutput('output_1', StringIOFormat::class); + $this->oIORegister->AddOutput('output_2', BooleanIOFormat::class); + + $this->assertCount(2, $this->oIORegister->GetOutputs()); + $this->assertArrayHasKey('output_1', $this->oIORegister->GetOutputs()); + } + + public function testMissingInput(): void + { + $this->expectException(RegisterException::class); + $this->oIORegister->GetInput('missing_input'); + } + + public function testMissingOutput(): void + { + $this->expectException(RegisterException::class); + $this->oIORegister->GetOutput('missing_output'); + } + + public function testGetBoundInputs(): void + { + $this->GivenSubFormBlock($this->oFormBlock, 'SubFormA', TextFormBlock::class); + $this->GivenSubFormBlock($this->oFormBlock, 'SubFormB', CheckboxFormBlock::class); + $this->GivenSubFormBlock($this->oFormBlock, 'SubFormC', TextFormBlock::class); + + $oSubForm = $this->GivenSubFormBlock($this->oFormBlock, 'SubForm', TextFormBlock::class); + $oSubForm->AddInput('input_from_A', StringIOFormat::class); + $oSubForm->AddInput('input_from_B', BooleanIOFormat::class); + $oSubForm->AddInput('input_from_C', StringIOFormat::class); + $oSubForm->AddInput('unbound_input', StringIOFormat::class); + + $this->GetIORegister($oSubForm)->DependsOn('input_from_A', 'SubFormA', TextFormBlock::OUTPUT_TEXT); + $this->GetIORegister($oSubForm)->DependsOn('input_from_B', 'SubFormB', CheckboxFormBlock::OUTPUT_CHECKED); + $this->GetIORegister($oSubForm)->DependsOn('input_from_C', 'SubFormC', TextFormBlock::OUTPUT_TEXT); + + $aBoundInputs = $this->GetIORegister($oSubForm)->GetBoundInputs(); + $this->assertCount(3, $aBoundInputs); + } + + public function testGetBoundOutputs(): void + { + $this->oFormBlock->AddOutput('output', StringIOFormat::class); + + $oSubFormA = $this->GivenSubFormBlock($this->oFormBlock, 'SubFormA', TextFormBlock::class); + $oIORegisterA = $this->GetIORegister($oSubFormA); + + $oIORegisterA->ImpactParent(TextFormBlock::OUTPUT_TEXT, 'output'); + + $this->assertCount(1, $this->oIORegister->GetBoundOutputs()); + } + + public function testAddInputDependsOn(): void + { + $this->GivenSubFormBlock($this->oFormBlock, 'SubFormA', TextFormBlock::class); + $oSubFormB = $this->GivenSubFormBlock($this->oFormBlock, 'SubFormB', TextFormBlock::class); + $oIORegisterB = $this->GetIORegister($oSubFormB); + + $oIORegisterB->AddInputDependsOn('input', 'SubFormA', TextFormBlock::OUTPUT_TEXT); + + $this->assertNotNull($oIORegisterB->GetInput('input')); + } + + public function testImpactParent(): void + { + $this->oFormBlock->AddOutput('output', StringIOFormat::class); + + $oSubFormA = $this->GivenSubFormBlock($this->oFormBlock, 'SubFormA', TextFormBlock::class); + $oIORegisterA = $this->GetIORegister($oSubFormA); + + $oIORegisterA->ImpactParent(TextFormBlock::OUTPUT_TEXT, 'output'); + + $this->assertTrue($this->oFormBlock->GetOutput('output')->IsBound()); + } + + public function testAddingTwiceTheSameInputThrowsException(): void + { + $this->oIORegister->AddInput('test_input', StringIOFormat::class); + $this->expectException(RegisterException::class); + $this->oIORegister->AddInput('test_input', StringIOFormat::class); + } + + public function testAddingTwiceTheSameOutputThrowsException(): void + { + $this->oIORegister->AddOutput('test_output', StringIOFormat::class); + $this->expectException(RegisterException::class); + $this->oIORegister->AddOutput('test_output', StringIOFormat::class); + } + + public function testDependingOnNonExistingInputThrowsException(): void + { + $this->oIORegister->AddInput('test_input', StringIOFormat::class); + $this->oIORegister->AddOutput('test_output', StringIOFormat::class); + + $this->expectException(RegisterException::class); + + $this->oIORegister->DependsOn('non_existing_input', 'OtherBlock', 'test_output'); + } + + public function testDependingOnNonExistingOutputThrowsException(): void + { + $this->oIORegister->AddInput('test_input', StringIOFormat::class); + + $this->expectException(RegisterException::class); + $this->oIORegister->DependsOn('test_input', 'OtherBlock', 'non_existing_output'); + } + + public function testDependingOnNonExistingBlockThrowsException(): void + { + $this->oIORegister->AddInput('test_input', StringIOFormat::class); + $this->oIORegister->AddOutput('test_output', StringIOFormat::class); + + $this->expectException(RegisterException::class); + $this->oIORegister->DependsOn('test_input', 'UnknownBlock', 'test'); + } + + public function testHasDependenciesBlocks(): void + { + $this->GivenSubFormBlock($this->oFormBlock, 'SubFormA', TextFormBlock::class); + + $oSubForm = $this->GivenSubFormBlock($this->oFormBlock, 'SubForm', TextFormBlock::class); + $oSubForm->AddInput('input_from_A', StringIOFormat::class); + + $this->GetIORegister($oSubForm)->DependsOn('input_from_A', 'SubFormA', TextFormBlock::OUTPUT_TEXT); + $this->assertTrue($this->GetIORegister($oSubForm)->HasDependenciesBlocks()); + + $this->assertFalse($this->oIORegister->HasDependenciesBlocks()); + } + + public function testImpactBlocks(): void + { + $oSubFormA = $this->GivenSubFormBlock($this->oFormBlock, 'SubFormA', TextFormBlock::class); + + $oSubForm = $this->GivenSubFormBlock($this->oFormBlock, 'SubForm', TextFormBlock::class); + $oSubForm->AddInput('input_from_A', StringIOFormat::class); + + $this->GetIORegister($oSubForm)->DependsOn('input_from_A', 'SubFormA', TextFormBlock::OUTPUT_TEXT); + $this->assertFalse($this->GetIORegister($oSubForm)->ImpactDependentsBlocks()); + + $this->assertTrue($this->GetIORegister($oSubFormA)->ImpactDependentsBlocks()); + } +} diff --git a/tests/php-unit-tests/unitary-tests/sources/Forms/Register/OptionsRegisterTest.php b/tests/php-unit-tests/unitary-tests/sources/Forms/Register/OptionsRegisterTest.php index 1e044bae4..83350f93c 100644 --- a/tests/php-unit-tests/unitary-tests/sources/Forms/Register/OptionsRegisterTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/Forms/Register/OptionsRegisterTest.php @@ -30,6 +30,14 @@ class OptionsRegisterTest extends ItopDataTestCase $this->oOptionsRegister->SetOption('not valid option name', 'value'); } + public function testSetOptionTwice(): void + { + $this->oOptionsRegister->SetOption('valid_option_name', 'value'); + $this->oOptionsRegister->SetOption('valid_option_name', 'value2'); + + $this->assertEquals('value2', $this->oOptionsRegister->GetOption('valid_option_name')); + } + public function testSetNonTypeOption(): void { $this->oOptionsRegister->SetOption('not_a_type_option', 'value', false);