Merge remote-tracking branch 'origin/feature/8772_form_dependencies_manager' into feature/8772_form_dependencies_manager

This commit is contained in:
jf-cbd
2025-12-02 16:50:14 +01:00
13 changed files with 164 additions and 18 deletions

View File

@@ -14,6 +14,8 @@ function triggerTurbo(el) {
el.form.querySelector(`[name="${sFormName}[_turbo_trigger]"]`).value = el.getAttribute('name');
el.form.setAttribute('novalidate', true);
el.form.requestSubmit();
el.form.querySelector(`[name="${sFormName}[_turbo_trigger]"]`).value = null;
el.form.setAttribute('novalidate', false);
$aFormBlockDataTransmittedData[name] = el.value;
}

View File

@@ -534,6 +534,8 @@ return array(
'Combodo\\iTop\\Forms\\Register\\OptionsRegister' => $baseDir . '/sources/Forms/Register/OptionsRegister.php',
'Combodo\\iTop\\Forms\\Register\\RegisterException' => $baseDir . '/sources/Forms/Register/RegisterException.php',
'Combodo\\iTop\\Forms\\Twig\\Extension\\FormCompatibilityExtension' => $baseDir . '/sources/Forms/Twig/Extension/FormCompatibilityExtension.php',
'Combodo\\iTop\\Forms\\Validator\\AttributeExist' => $baseDir . '/sources/Forms/Validator/AttributeExist.php',
'Combodo\\iTop\\Forms\\Validator\\AttributeExistValidator' => $baseDir . '/sources/Forms/Validator/AttributeExistValidator.php',
'Combodo\\iTop\\PhpParser\\Evaluation\\PhpExpressionEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php',
'Combodo\\iTop\\Renderer\\BlockRenderer' => $baseDir . '/sources/Renderer/BlockRenderer.php',
'Combodo\\iTop\\Renderer\\Bootstrap\\BsFieldRendererMappings' => $baseDir . '/sources/Renderer/Bootstrap/BsFieldRendererMappings.php',
@@ -3343,7 +3345,6 @@ return array(
'Symfony\\Component\\Validator\\Mapping\\PropertyMetadataInterface' => $vendorDir . '/symfony/validator/Mapping/PropertyMetadataInterface.php',
'Symfony\\Component\\Validator\\Mapping\\TraversalStrategy' => $vendorDir . '/symfony/validator/Mapping/TraversalStrategy.php',
'Symfony\\Component\\Validator\\ObjectInitializerInterface' => $vendorDir . '/symfony/validator/ObjectInitializerInterface.php',
'Symfony\\Component\\Validator\\Test\\ConstraintValidatorTestCase' => $vendorDir . '/symfony/validator/Test/ConstraintValidatorTestCase.php',
'Symfony\\Component\\Validator\\Util\\PropertyPath' => $vendorDir . '/symfony/validator/Util/PropertyPath.php',
'Symfony\\Component\\Validator\\Validation' => $vendorDir . '/symfony/validator/Validation.php',
'Symfony\\Component\\Validator\\ValidatorBuilder' => $vendorDir . '/symfony/validator/ValidatorBuilder.php',
@@ -3893,5 +3894,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',
'<EFBFBD>' => $vendorDir . '/symfony/cache/Traits/ValueWrapper.php',
);

View File

@@ -920,6 +920,8 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Forms\\Register\\OptionsRegister' => __DIR__ . '/../..' . '/sources/Forms/Register/OptionsRegister.php',
'Combodo\\iTop\\Forms\\Register\\RegisterException' => __DIR__ . '/../..' . '/sources/Forms/Register/RegisterException.php',
'Combodo\\iTop\\Forms\\Twig\\Extension\\FormCompatibilityExtension' => __DIR__ . '/../..' . '/sources/Forms/Twig/Extension/FormCompatibilityExtension.php',
'Combodo\\iTop\\Forms\\Validator\\AttributeExist' => __DIR__ . '/../..' . '/sources/Forms/Validator/AttributeExist.php',
'Combodo\\iTop\\Forms\\Validator\\AttributeExistValidator' => __DIR__ . '/../..' . '/sources/Forms/Validator/AttributeExistValidator.php',
'Combodo\\iTop\\PhpParser\\Evaluation\\PhpExpressionEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php',
'Combodo\\iTop\\Renderer\\BlockRenderer' => __DIR__ . '/../..' . '/sources/Renderer/BlockRenderer.php',
'Combodo\\iTop\\Renderer\\Bootstrap\\BsFieldRendererMappings' => __DIR__ . '/../..' . '/sources/Renderer/Bootstrap/BsFieldRendererMappings.php',
@@ -3729,7 +3731,6 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Symfony\\Component\\Validator\\Mapping\\PropertyMetadataInterface' => __DIR__ . '/..' . '/symfony/validator/Mapping/PropertyMetadataInterface.php',
'Symfony\\Component\\Validator\\Mapping\\TraversalStrategy' => __DIR__ . '/..' . '/symfony/validator/Mapping/TraversalStrategy.php',
'Symfony\\Component\\Validator\\ObjectInitializerInterface' => __DIR__ . '/..' . '/symfony/validator/ObjectInitializerInterface.php',
'Symfony\\Component\\Validator\\Test\\ConstraintValidatorTestCase' => __DIR__ . '/..' . '/symfony/validator/Test/ConstraintValidatorTestCase.php',
'Symfony\\Component\\Validator\\Util\\PropertyPath' => __DIR__ . '/..' . '/symfony/validator/Util/PropertyPath.php',
'Symfony\\Component\\Validator\\Validation' => __DIR__ . '/..' . '/symfony/validator/Validation.php',
'Symfony\\Component\\Validator\\ValidatorBuilder' => __DIR__ . '/..' . '/symfony/validator/ValidatorBuilder.php',
@@ -4279,7 +4280,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',
'<EFBFBD>' => __DIR__ . '/..' . '/symfony/cache/Traits/ValueWrapper.php',
);
public static function getInitializer(ClassLoader $loader)

View File

@@ -11,6 +11,7 @@ use Combodo\iTop\Forms\Block\AbstractFormBlock;
use Combodo\iTop\Forms\Block\FormBlockException;
use Combodo\iTop\Forms\IO\Format\BooleanIOFormat;
use Combodo\iTop\Forms\Register\IORegister;
use Exception;
use Expression;
use Symfony\Component\Form\FormEvents;
@@ -52,9 +53,13 @@ abstract class AbstractExpressionFormBlock extends AbstractFormBlock
foreach ($aParamsToResolve as $sParamToResolve) {
$aResolvedParams[$sParamToResolve] = strval($this->GetInputValue($sParamToResolve));
}
$aFieldsToResolve = $oExpression->ListRequiredFields();
foreach ($aFieldsToResolve as $sFieldToResolve) {
$aResolvedParams[$sFieldToResolve] = strval($this->GetInputValue($sFieldToResolve));
}
return $oExpression->Evaluate($aResolvedParams);
} catch (\Exception $e) {
throw new FormBlockException('Compute expression '.json_encode($sExpression).' block issue', 0, $e);
} catch (Exception $e) {
throw new FormBlockException('Compute expression '.json_encode($sExpression).' block issue: '.$e->getMessage(), 0, $e);
}
}

View File

@@ -95,9 +95,12 @@ class FormBuilder implements FormBuilderInterface, IteratorAggregate
$oFormBlock->oDependencyMap = $this->oDependencyHandler->GetMap();
}
if (is_null($oFormBlock->GetParent())) {
if ($oFormBlock->IsRootBlock()) {
// Insert a hidden type to save the place
$this->builder->add('_turbo_trigger', HiddenType::class, ['prevent_form_build' => true]);
$this->builder->add('_turbo_trigger', HiddenType::class, [
'prevent_form_build' => true,
'mapped' => false,
]);
}
}

View File

@@ -38,7 +38,9 @@ final class Forms
public static function createFormFactoryBuilder(): FormFactoryBuilderInterface
{
// Set up the Validator component
$validator = Validation::createValidator();
$validator = Validation::createValidatorBuilder()
->enableAttributeMapping()->getValidator();
return (new FormFactoryBuilder())
->addExtension(new HttpFoundationExtension())
->addExtension(new ValidatorExtension($validator))

View File

@@ -19,4 +19,10 @@ use Throwable;
*/
class FormsException extends Exception
{
public function __construct(string $sMessage = '', int $iCode = 0, ?Throwable $oPrevious = null, array $aContext = [])
{
parent::__construct($sMessage, $iCode, $oPrevious);
IssueLog::Exception(get_class($this).' occurs: '.$sMessage, $this, null, $aContext);
}
}

View File

@@ -86,7 +86,7 @@ class AbstractFormIO
public function SetName(string $sName): self
{
// Check name validity
if (preg_match('/(?<name>\w+)/', $sName, $aMatches)) {
if (preg_match('/^(?<name>((\w+\.\w+)|\w+))$/', $sName, $aMatches)) {
$sParsedName = $aMatches['name'];
if ($sParsedName !== $sName) {
$sName = json_encode($sName);

View File

@@ -36,8 +36,6 @@ class OqlToClassConverter extends AbstractConverter
$oModelReflection = DIService::GetInstance()->GetService('ModelReflection');
try {
$oQuery = $oModelReflection->GetQuery($oData);
} catch (\OQLParserException $e) {
throw new FormBlockIOException($e->GetIssue(), $e->getCode(), $e);
} catch (Exception $e) {
throw new FormBlockIOException($e->getMessage(), $e->getCode(), $e);
}

View File

@@ -0,0 +1,68 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Forms\Validator;
use Attribute;
use InvalidArgumentException;
use Symfony\Component\Validator\Constraint;
/**
* Attribute exist constraint.
*
* @package Combodo\iTop\Forms\Validator
* @since 3.3.0
*/
#[Attribute]
class AttributeExist extends Constraint
{
/** @var string Violation message */
public string $sMessage = 'The attribute "{{ attribute }}" doesn\'t exist in class "{{ class }}" from OQL "{{ oql }}".';
/** @var string|mixed OQL expression property path */
public string $sOqlPropertyPath;
/** @var string|null Attribute list filter */
public ?string $sFilter;
/**
* Constructor.
*
* @param string|null $sOqlPropertyPath
* @param string|null $sFilter
* @param array $aOptions
* @param array|null $aGroups
* @param mixed|null $oPayload
*/
public function __construct(string $sOqlPropertyPath = null, string $sFilter = null, array $aOptions = [], ?array $aGroups = null, mixed $oPayload = null)
{
if ($sOqlPropertyPath === null) {
throw new InvalidArgumentException('The argument "sOqlPropertyPath" must be set.');
}
// Merge argument into options array
$aOptions = array_merge([
'sOqlPropertyPath' => $sOqlPropertyPath,
], $aOptions);
parent::__construct($aOptions, $aGroups, $oPayload);
// Retrieve options
$this->sFilter = $sFilter;
$this->sOqlPropertyPath = $aOptions['sOqlPropertyPath'];
}
public function getDefaultOption(): string
{
return 'sOqlPropertyPath';
}
public function getRequiredOptions(): array
{
return ['sOqlPropertyPath'];
}
}

View File

@@ -0,0 +1,57 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Forms\Validator;
use Combodo\iTop\Forms\IO\Converter\OqlToClassConverter;
use Combodo\iTop\Service\DependencyInjection\DIService;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Attribute exist validator.
*
* @package Combodo\iTop\Forms\Validator
* @since 3.3.0
*/
class AttributeExistValidator extends ConstraintValidator
{
private ?PropertyAccessorInterface $propertyAccessor;
public function __construct()
{
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
}
/**
* @inheritDoc
*/
public function validate(mixed $value, Constraint $constraint): void
{
$sOql = $this->propertyAccessor->getValue($this->context->getObject(), $constraint->sOqlPropertyPath);
$oOqlToClassConverter = new OqlToClassConverter();
$sClass = strval($oOqlToClassConverter->Convert($sOql));
$sClass = "UserRequest";
/** List attributes @var ModelReflection $oModelReflection */
$oModelReflection = DIService::GetInstance()->GetService('ModelReflection');
$aAttributeCodes = array_keys($oModelReflection->ListAttributes($sClass));
if (!in_array($value, $aAttributeCodes, true)) {
$this->context->buildViolation($constraint->sMessage)
->setParameter('{{ attribute }}', $value)
->setParameter('{{ class }}', $sClass)
->setParameter('{{ oql }}', $sOql)
->addViolation();
}
}
}

View File

@@ -25,16 +25,16 @@ abstract class AbstractFormsTest extends ItopDataTestCase
{
public function GivenInput(string $sName, string $sType = StringIOFormat::class): FormInput
{
$oBlock = $this->GivenFormBlock($sName.'_block');
$oBlock = $this->GivenFormBlock($sName);
return new FormInput($sName.'_input', $sType, $oBlock);
return new FormInput($sName, $sType, $oBlock);
}
public function GivenOutput(string $sName, string $sType = StringIOFormat::class): FormOutput
{
$oBlock = $this->GivenFormBlock($sName.'_block');
$oBlock = $this->GivenFormBlock($sName);
return new FormOutput($sName.'_output', $sType, $oBlock);
return new FormOutput($sName, $sType, $oBlock);
}
public function GivenFormBlock(string $sName): FormBlock

View File

@@ -67,7 +67,7 @@ class AbstractFormIOTest extends AbstractFormsTest
* @return void
* @throws \Combodo\iTop\Forms\IO\FormBlockIOException
*/
public function testNameFormatSupportsOnlyLettersUnderscoreAndNumbers(string $sName, bool $bGenerateException = true)
public function testNameFormatSupportsOnlyLettersUnderscoreAndNumbersAndDot(string $sName, bool $bGenerateException = true)
{
if ($bGenerateException) {
@@ -75,7 +75,7 @@ class AbstractFormIOTest extends AbstractFormsTest
}
$oInput = $this->GivenInput($sName);
if (!$bGenerateException) {
$this->assertEquals($sName.'_input', $oInput->GetName());
$this->assertEquals($sName, $oInput->GetName());
}
}
@@ -88,12 +88,15 @@ class AbstractFormIOTest extends AbstractFormsTest
'Percent not supported' => ['name%'],
'Accent not supported' => ['namé'],
'emoji not supported' => ['🎄🎄🎄🎄🎄'],
'.name not supported' => ['.name'],
'name. not supported' => ['name.'],
// Corrects
'Numbers OK' => ['name123', false],
'Starting with number OK' => ['123name123', false],
'Underscore OK' => ['The_test_name', false],
'Camel OK' => ['TheTestName', false],
'name.subname OK' => ['name.subname', false],
];
}