N°8772 - dynamic form

This commit is contained in:
Benjamin Dalsass
2025-11-12 17:09:40 +01:00
parent 3d2485a004
commit 5cacfcb754
18 changed files with 320 additions and 296 deletions

View File

@@ -501,6 +501,7 @@ return array(
'Combodo\\iTop\\Forms\\Block\\FormType\\OqlFormType' => $baseDir . '/sources/Forms/Block/FormType/OqlFormType.php',
'Combodo\\iTop\\Forms\\Block\\IO\\AbstractFormIO' => $baseDir . '/sources/Forms/Block/IO/AbstractFormIO.php',
'Combodo\\iTop\\Forms\\Block\\IO\\Converter\\AbstractConverter' => $baseDir . '/sources/Forms/Block/IO/Converter/AbstractConverter.php',
'Combodo\\iTop\\Forms\\Block\\IO\\Converter\\ChoiceValueToLabelConverter' => $baseDir . '/sources/Forms/Block/IO/Converter/ChoiceValueToLabelConverter.php',
'Combodo\\iTop\\Forms\\Block\\IO\\Converter\\OqlToClassConverter' => $baseDir . '/sources/Forms/Block/IO/Converter/OqlToClassConverter.php',
'Combodo\\iTop\\Forms\\Block\\IO\\Converter\\StringToAttributeConverter' => $baseDir . '/sources/Forms/Block/IO/Converter/StringToAttributeConverter.php',
'Combodo\\iTop\\Forms\\Block\\IO\\Converter\\StringToBooleanConverter' => $baseDir . '/sources/Forms/Block/IO/Converter/StringToBooleanConverter.php',

View File

@@ -882,6 +882,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Forms\\Block\\FormType\\OqlFormType' => __DIR__ . '/../..' . '/sources/Forms/Block/FormType/OqlFormType.php',
'Combodo\\iTop\\Forms\\Block\\IO\\AbstractFormIO' => __DIR__ . '/../..' . '/sources/Forms/Block/IO/AbstractFormIO.php',
'Combodo\\iTop\\Forms\\Block\\IO\\Converter\\AbstractConverter' => __DIR__ . '/../..' . '/sources/Forms/Block/IO/Converter/AbstractConverter.php',
'Combodo\\iTop\\Forms\\Block\\IO\\Converter\\ChoiceValueToLabelConverter' => __DIR__ . '/../..' . '/sources/Forms/Block/IO/Converter/ChoiceValueToLabelConverter.php',
'Combodo\\iTop\\Forms\\Block\\IO\\Converter\\OqlToClassConverter' => __DIR__ . '/../..' . '/sources/Forms/Block/IO/Converter/OqlToClassConverter.php',
'Combodo\\iTop\\Forms\\Block\\IO\\Converter\\StringToAttributeConverter' => __DIR__ . '/../..' . '/sources/Forms/Block/IO/Converter/StringToAttributeConverter.php',
'Combodo\\iTop\\Forms\\Block\\IO\\Converter\\StringToBooleanConverter' => __DIR__ . '/../..' . '/sources/Forms/Block/IO/Converter/StringToBooleanConverter.php',

View File

@@ -7,18 +7,19 @@
namespace Combodo\iTop\Forms\Block;
use Combodo\iTop\Forms\Block\Base\FormBlock;
use Combodo\iTop\Forms\Block\IO\AbstractFormIO;
use Combodo\iTop\Forms\Block\IO\Converter\AbstractConverter;
use Combodo\iTop\Forms\Block\IO\Format\RawFormat;
use Combodo\iTop\Forms\Block\IO\FormInput;
use Combodo\iTop\Forms\Block\IO\FormOutput;
use Combodo\iTop\Forms\IFormBlock;
use Forms\BlockIO;
/**
* Abstract form block.
*
* A form block describe a form (complex or simple type).
* A complex form have sub blocks.
* It defines its inputs and outputs.
* Inputs / Outputs.
* Options.
*
*/
abstract class AbstractFormBlock implements IFormBlock
@@ -96,7 +97,12 @@ abstract class AbstractFormBlock implements IFormBlock
return $this->oParent;
}
public function HasParent(): bool
/**
* Return true if this block is root.
*
* @return bool
*/
public function IsRootBlock(): bool
{
return $this->oParent !== null;
}
@@ -111,28 +117,6 @@ abstract class AbstractFormBlock implements IFormBlock
return $this->sName;
}
public function GetIdentifier(): string
{
$sParentName = $this->GetParent()?->GetIdentifier();
if (is_null($sParentName)) {
return $this->GetName();
}
return $sParentName.'_'.$this->sName;
}
public function GetPath(): array
{
$aPath = [];
$oCurrent = $this;
do {
$aPath[] = $oCurrent->GetName();
$oCurrent = $oCurrent->getParent();
} while ($oCurrent->HasParent());
return array_reverse($aPath);
}
/**
* Return the form block options.
* Options will be passed to FormType for building.
@@ -170,7 +154,7 @@ abstract class AbstractFormBlock implements IFormBlock
* @param string $sName the input name
* @param string $sType the type of the input
*
* @return void
* @return AbstractFormBlock
*/
public function AddInput(string $sName, string $sType): AbstractFormBlock
{
@@ -180,6 +164,27 @@ abstract class AbstractFormBlock implements IFormBlock
return $this;
}
/**
* Add an input connected to another block.
*
* @param string $sName the input name
* @param string $sOutputBlockName
* @param string $sOutputName
*
* @return AbstractFormBlock
* @throws FormBlockException
*/
public function AddInputDependsOn(string $sName, string $sOutputBlockName, string $sOutputName): AbstractFormBlock
{
$oOutputBlock = $this->GetParent()->Get($sOutputBlockName);
$oBlockOutput = $oOutputBlock->GetOutput($sOutputName);
$this->AddInput($sName, $oBlockOutput->GetDataType());
$this->DependsOn($sName, $sOutputBlockName, $sOutputName);
return $this;
}
/**
* Get an input.
*
@@ -204,7 +209,7 @@ abstract class AbstractFormBlock implements IFormBlock
* @param string $sType
* @param AbstractConverter|null $oConverter
*
* @return void
* @return AbstractFormBlock
*/
public function AddOutput(string $sName, string $sType, AbstractConverter $oConverter = null): AbstractFormBlock
{
@@ -342,11 +347,11 @@ abstract class AbstractFormBlock implements IFormBlock
}
/**
* Get bindings on inputs.
* Get bound inputs bindings.
*
* @return array
*/
public function GetInputsBindings(): array
public function GetBoundInputsBindings(): array
{
$aBindings = [];
@@ -361,11 +366,11 @@ abstract class AbstractFormBlock implements IFormBlock
}
/**
* Get bindings on outputs.
* Get bound outputs bindings.
*
* @return array
*/
public function GetOutputBindings(): array
public function GetBoundOutputBindings(): array
{
$aBindings = [];
@@ -379,26 +384,6 @@ abstract class AbstractFormBlock implements IFormBlock
return $aBindings;
}
/**
* Inputs data ready.
*
* @param string|null $sType
*
* @return bool
*/
public function IsInputsDataReady(string $sType = null): bool
{
foreach ($this->aFormInputs as $oFormInput) {
if ($oFormInput->IsBound()) {
if (!$oFormInput->IsEventDataReady($sType)) {
return false;
}
}
}
return true;
}
/**
* The block has been added to its parent.
*
@@ -421,6 +406,26 @@ abstract class AbstractFormBlock implements IFormBlock
$this->bIsAddedToForm = $bIsAdded;
}
/**
* Inputs data ready.
*
* @param string|null $sType
*
* @return bool
*/
public function IsInputsDataReady(string $sType = null): bool
{
foreach ($this->aFormInputs as $oFormInput) {
if ($oFormInput->IsBound()) {
if (!$oFormInput->IsEventDataReady($sType)) {
return false;
}
}
}
return true;
}
/**
* Compute outputs values.
*
@@ -438,19 +443,9 @@ abstract class AbstractFormBlock implements IFormBlock
$oFormOutput->ComputeValue($sEventType, $oData);
}
}
/**
* Propagate inputs values.
*
* @return void
*/
public function PropagateInputsValues(): void
{
foreach ($this->aFormInputs as $oFormInput) {
$oFormInput->PropagateBindingsValues();
}
}
/**
* Initialize inputs.
@@ -471,7 +466,27 @@ abstract class AbstractFormBlock implements IFormBlock
$this->AddOutput(self::OUTPUT_VALUE, RawFormat::class);
}
public function InputHasChanged()
/**
* Called when a binding value has been transmitted.
*
* @param AbstractFormIO $oBlockIO
*
* @return void
*/
public function BindingReceivedEvent(AbstractFormIO $oBlockIO): void
{
if ($this->IsInputsDataReady()) {
$this->AllInputsReadyEvent();
}
}
/**
* Called when all inputs are ready.
*
* @return void
*/
public function AllInputsReadyEvent(): void
{
}
}

View File

@@ -35,12 +35,11 @@ abstract class AbstractTypeFormBlock extends AbstractFormBlock
* @param string|null $sEventType
*
* @return bool
* @throws \Combodo\iTop\Forms\Block\FormBlockException
* @throws FormBlockException
*/
public function IsVisible(string $sEventType = null): bool
{
$oInput = $this->GetInput(self::INPUT_VISIBLE);
if(!$oInput->IsBound()){
return true;
}

View File

@@ -8,6 +8,8 @@ namespace Combodo\iTop\Forms\Block\Base;
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
use Combodo\iTop\Forms\Block\FormType\ChoiceFormType;
use Combodo\iTop\Forms\Block\IO\Converter\ChoiceValueToLabelConverter;
use Combodo\iTop\Forms\Block\IO\Format\RawFormat;
/**
* Form block for choices.
@@ -15,10 +17,18 @@ use Combodo\iTop\Forms\Block\FormType\ChoiceFormType;
*/
class ChoiceFormBlock extends AbstractTypeFormBlock
{
// Outputs
public const OUTPUT_LABEL = 'label';
/** @inheritdoc */
public function GetFormType(): string
{
return ChoiceFormType::class;
}
public function InitOutputs(): void
{
parent::InitOutputs();
$this->AddOutput(self::OUTPUT_LABEL, RawFormat::class, new ChoiceValueToLabelConverter($this));
}
}

View File

@@ -11,6 +11,7 @@ use Combodo\iTop\Forms\Block\FormBlockException;
use Combodo\iTop\Forms\Block\IO\Converter\StringToAttributeConverter;
use Combodo\iTop\Forms\Block\IO\Format\AttributeIOFormat;
use Combodo\iTop\Forms\Block\IO\Format\ClassIOFormat;
use Combodo\iTop\Forms\Block\IO\Format\RawFormat;
use CoreException;
use MetaModel;
@@ -52,10 +53,17 @@ class AttributeChoiceFormBlock extends ChoiceFormBlock
/** @inheritdoc */
public function UpdateDynamicOptions(string $sEventType = null): void
{
$oValue = $this->GetInput(self::INPUT_CLASS_NAME)->GetValue($sEventType);
$oClass = $this->GetInput(self::INPUT_CLASS_NAME)->GetValue($sEventType);
$aAttributeCodes = MetaModel::GetAttributesList($oValue);
$this->aDynamicOptions['choices'] = array_combine($aAttributeCodes, $aAttributeCodes);
$aAttributeCodes = MetaModel::GetAttributesList($oClass);
$aAttributes = [];
foreach ($aAttributeCodes as $sAttributeCode){
$oAttribute = MetaModel::GetAttributeDef(strval($oClass), $sAttributeCode);
$aAttributes[$oAttribute->GetLabel()] = $sAttributeCode;
}
$this->aDynamicOptions['choices'] = $aAttributes;
}
}

View File

@@ -13,66 +13,47 @@ use Combodo\iTop\Forms\FormsException;
use IssueLog;
use Symfony\Component\Form\FormEvents;
/**
*
*/
class ExpressionFormBlock extends AbstractFormBlock
{
public const EXPRESSION_PATTERN = "/\[\[(?<input>[^\]]+)]]/";
// Outputs
const OUTPUT_RESULT = "result";
const OUTPUT_RESULT_INVERT = "result_invert";
public function InitBlockOptions(array &$aUserOptions): void
{
parent::InitBlockOptions($aUserOptions);
}
/** @inheritdoc */
public function InitOutputs(): void
{
parent::InitOutputs();
$this->AddOutput(self::OUTPUT_RESULT, BooleanIOFormat::class);
// $this->AddOutput(self::OUTPUT_RAW, RawFormat::class);
$this->AddOutput(self::OUTPUT_RESULT_INVERT, BooleanIOFormat::class);
}
// public function InputHasChanged()
// {
// if (!$this->IsInputsDataReady()) {
// return;
// }
// $sExpression = $this->GetOptions()['expression'];
// $sValue = preg_replace_callback(
// "/\[\[(?<input>[^\]]+)]]/",
// function(array $aMatches): string {
// return $this->GetInput($aMatches['input'])->GetValue();
// },
// $sExpression);
//
// foreach ($this->GetInputs() as $oFormInput) {
// IssueLog::Info($oFormInput->GetName().' = '.$oFormInput->GetValue());
// }
// IssueLog::Info("Result of [$sExpression] is [$sValue]");
//
// $result = '';
// eval('$result = '.$sValue.';');
// IssueLog::Info("Result of [$sExpression] is eval to [$result]");
//
// $this->GetOutput(self::OUTPUT_RESULT)->SetValue(FormEvents::POST_SUBMIT, new BooleanIOFormat($result));
// $this->GetOutput(self::OUTPUT_VALUE)->SetValue(FormEvents::POST_SUBMIT, new RawFormat($result));
// }
public function InputHasChanged()
/** @inheritdoc */
public function AllInputsReadyEvent(): void
{
if (!$this->IsInputsDataReady()) {
return;
}
$sExpression = $this->GetOptions()['expression'];
$this->Compute($sExpression, FormEvents::POST_SET_DATA);
$this->Compute($sExpression, FormEvents::POST_SUBMIT);
$this->ComputeExpression(FormEvents::POST_SET_DATA);
$this->ComputeExpression(FormEvents::POST_SUBMIT);
}
public function Compute(string $sExpression, string $sEventType): void
/**
* Compute the expression and set the output values.
*
* @param string $sEventType
*
* @return void
*/
public function ComputeExpression(string $sEventType): void
{
try{
$sExpression = $this->GetOptions()['expression'];
$sValue = preg_replace_callback(
"/\[\[(?<input>[^\]]+)]]/",
self::EXPRESSION_PATTERN,
function(array $aMatches) use ($sEventType): ?string {
$oInput = $this->GetInput($aMatches['input']);
if(!$oInput->HasEventValue($sEventType)){
@@ -86,6 +67,7 @@ class ExpressionFormBlock extends AbstractFormBlock
eval('$result = '.$sValue.';');
$this->GetOutput(self::OUTPUT_RESULT)->SetValue($sEventType, new BooleanIOFormat($result));
$this->GetOutput(self::OUTPUT_RESULT_INVERT)->SetValue($sEventType, new BooleanIOFormat(!$result));
$this->GetOutput(self::OUTPUT_VALUE)->SetValue($sEventType, new RawFormat($result));
}
catch(\Exception $e){

View File

@@ -33,13 +33,16 @@ class ChoiceFormType extends AbstractType
{
parent::configureOptions($resolver);
// options to control the inline display of choices
$resolver->setDefault('inline_display', true);
}
/** @inheritdoc */
public function buildView(FormView $view, FormInterface $form, array $options): void
{
parent::buildView($view, $form, $options);
// pass options to the view
$view->vars['inline_display'] = $options['inline_display'];
}
@@ -47,46 +50,51 @@ class ChoiceFormType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// on preset data
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) use ($options) {
if ($options['multiple'] === false && $options['required'] === true) {
if ($event->getData() === null) {
$FirstElement = array_shift($options['choices']);
if ($FirstElement !== null) {
$event->setData($FirstElement);
}
}
}
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $oEvent) use ($options) {
$this->InitializeValue($oEvent, $options);
});
// on pre submit
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event) use ($options) {
if($options['multiple'] === false && $options['required'] === true) {
if ($event->getData() === null) {
$FirstElement = array_shift($options['choices']);
if($FirstElement !== null){
$event->setData($FirstElement);
}
}
}
});
// on pre submit
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event) use ($options){
// on pre submit (prior)
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $oEvent) use ($options){
// reset value if not in available choices
if (!empty($event->getData()) && !$this->CheckValue($event->getData(), $options)) {
$event->getForm()->addError(new FormError("The value has been reset because it is not part of the available choices anymore."));
$event->setData(null);
if (!empty($oEvent->getData()) && !$this->CheckValue($oEvent->getData(), $options)) {
$oEvent->getForm()->addError(new FormError("The value has been reset because it is not part of the available choices anymore."));
$oEvent->setData(null);
}
}, 1);
}, 1); // priority 1 to be executed before the default validation (priority 0)
// on pre submit
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $oEvent) use ($options) {
$this->InitializeValue($oEvent, $options);
});
}
/**
* Initialize the value of the choice field.
*
* @param PreSetDataEvent|PreSubmitEvent $oEvent
* @param array $options
*
* @return void
*/
private function InitializeValue(PreSetDataEvent|PreSubmitEvent $oEvent, array $options): void
{
if ($options['multiple'] === false && $options['required'] === true) {
if ($oEvent->getData() === null) {
$oFirstElement = array_shift($options['choices']);
if ($oFirstElement !== null) {
$oEvent->setData($oFirstElement);
}
}
}
}
/**
* Check if the value(s) are part of the available choices.
*
* @param $oValue
* @param $options
*

View File

@@ -1,4 +1,8 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Forms\Block\FormType;
@@ -8,14 +12,18 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
/**
*
*/
class FormType extends AbstractType
{
/** @inheritdoc */
public function getParent(): string
{
return \Symfony\Component\Form\Extension\Core\Type\FormType::class;
}
/** @inheritdoc */
public function buildView(FormView $view, FormInterface $form, array $options)
{
parent::buildView($view, $form, $options);

View File

@@ -1,10 +1,17 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Forms\Block\FormType;
use Combodo\iTop\Forms\Block\Base\FormBlock;
use Symfony\Component\Form\FormInterface;
/**
*
*/
class FormTypeHelper
{
@@ -51,7 +58,7 @@ class FormTypeHelper
$aBlocksToRedraw[$sBlockTurboTriggerId] = $oFormTurboTrigger->createView();
// Add impacted blocks
$aImpacted = $oMap->GetImpacted($oBlockTurboTrigger->GetName());
$aImpacted = $oMap->GetBlocksImpactedBy($oBlockTurboTrigger->GetName());
foreach ($aImpacted as $oImpactedBlock) {
$sName = $sParentName.'_'.$oImpactedBlock->GetName();
if($oParent->has($oImpactedBlock->GetName())){

View File

@@ -0,0 +1,35 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Forms\Block\IO\Converter;
use Combodo\iTop\Forms\Block\Base\ChoiceFormBlock;
use Combodo\iTop\Forms\Block\IO\Format\RawFormat;
/**
*
*/
class ChoiceValueToLabelConverter extends AbstractConverter
{
private ChoiceFormBlock $oChoiceBlock;
public function __construct(ChoiceFormBlock $oChoiceBlock)
{
$this->oChoiceBlock = $oChoiceBlock;
}
/** @inheritdoc */
public function Convert(mixed $oData): ?RawFormat
{
if(is_null($oData) || is_array($oData)){
return null;
}
$aOptions = array_flip($this->oChoiceBlock->GetOptionsMergedWithDynamic()['choices']);
return new RawFormat($aOptions[$oData]);
}
}

View File

@@ -22,7 +22,6 @@ class FormBinding
{
$this->oDestinationIO = $oDestinationIO;
$this->oSourceIO = $oSourceIO;
}
/**
@@ -33,6 +32,6 @@ class FormBinding
public function PropagateValues(): void
{
$this->oDestinationIO->SetValues($this->oSourceIO->GetValues());
$this->oDestinationIO->GetOwnerBlock()->InputHasChanged();
$this->oDestinationIO->GetOwnerBlock()->BindingReceivedEvent($this->oDestinationIO);
}
}

View File

@@ -4,16 +4,16 @@ namespace Combodo\iTop\Forms\Block\IO\Format;
class RawFormat
{
public string $oValue;
public string $sValue;
public function __construct(string $oValue)
public function __construct(string $sValue)
{
$this->oValue = $oValue;
$this->sValue = $sValue;
// validation du format sinon exception
}
public function __toString(): string
{
return strval($this->oValue);
return strval($this->sValue);
}
}

View File

@@ -8,6 +8,7 @@ namespace Combodo\iTop\Forms\FormBuilder;
use Combodo\iTop\Forms\Block\AbstractFormBlock;
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
use Combodo\iTop\Forms\Block\Base\FormBlock;
use Exception;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
@@ -29,27 +30,24 @@ class DependencyHandler
/** @var array events */
private array $aEvents = [];
private readonly string $sName;
private readonly AbstractFormBlock $oFormBlock;
private readonly FormBuilder $oFormBuilder;
private readonly array $aSubBlocks;
private readonly FormBlock $oFormBlock;
private readonly array $aDependentBlocks;
/**
* Constructor.
*
* @param string $sName The name
* @param AbstractFormBlock $oFormBlock The attached form block
* @param FormBuilder $oFormBuilder The form builder
* @param array $aSubBlocks Sub blocks
* @param FormBlock $oFormBlock The block attached to the builder
* @param array $aDependentBlocks Dependants blocks
*/
public function __construct(string $sName, AbstractFormBlock $oFormBlock, FormBuilder $oFormBuilder, array $aSubBlocks, array $aDependentBlocks)
public function __construct(FormBuilder $oFormBuilder, FormBlock $oFormBlock, array $aDependentBlocks)
{
$this->sName = $oFormBuilder->getName();
$this->aDependentBlocks = $aDependentBlocks;
$this->aSubBlocks = $aSubBlocks;
$this->oFormBuilder = $oFormBuilder;
$this->oFormBlock = $oFormBlock;
$this->sName = $sName;
// dependencies map
$this->oDependenciesMap = new DependencyMap($aDependentBlocks);
@@ -63,16 +61,6 @@ class DependencyHandler
self::$aDependencyHandlers[] = $this;
}
/**
* Return the form block.
*
* @return AbstractFormBlock
*/
public function GetFormBlock(): AbstractFormBlock
{
return $this->oFormBlock;
}
/**
* Add form ready listener.
*
@@ -86,13 +74,8 @@ class DependencyHandler
// Initialize the dependencies listeners once the form is built
$this->oFormBuilder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
/** Iterate throw listened blocks */
foreach ($this->oDependenciesMap->GetInitialBoundOutputBlockNames() as $sOutputBlockName) {
// inner binding
if ($sOutputBlockName === $this->oFormBlock->getName()) {
continue;
}
/** Iterate throw blocks impacting other but without dependencies */
foreach ($this->oDependenciesMap->GetImpactingBlocksWithoutDependencies() as $sOutputBlockName) {
// Add event
$this->AddEvent('form.listen', $sOutputBlockName);
@@ -123,7 +106,7 @@ class DependencyHandler
$oForm = $oEvent->getForm();
// Get the form block
$oFormBlock = $this->aSubBlocks[$oForm->getName()];
$oFormBlock = $this->oFormBlock->Get($oForm->getName());
// Compute the block outputs with the data
try{
@@ -151,11 +134,9 @@ class DependencyHandler
{
$aImpactedBlocks = $this->aDependentBlocks;
if($sOutputBlock !== null){
$aImpactedBlocks = $this->oDependenciesMap->GetBlocksDependingOn($sOutputBlock);
}
if($aImpactedBlocks === null){
return;
$aImpactedBlocks = $this->oDependenciesMap->GetBlocksImpactedBy($sOutputBlock, function(AbstractFormBlock $oBlock) use ($sEventType) {
return $oBlock instanceof AbstractTypeFormBlock;
});
}
/** Iterate throw dependencies... @var AbstractFormBlock $oDependentBlock */
@@ -172,7 +153,7 @@ class DependencyHandler
$aOptions = $oDependentBlock->GetOptionsMergedWithDynamic($sEventType);
// Add the listener callback to the dependent field if it is also a dependency for another field
if ($this->oDependenciesMap->IsTheBlockInDependencies($oDependentBlock->getName())) {
if ($this->oDependenciesMap->HasBlocksImpactedBy($oDependentBlock->getName())) {
// Pass the listener call back to be registered by the dependency form builder
$aOptions = array_merge($aOptions, [

View File

@@ -7,7 +7,6 @@
namespace Combodo\iTop\Forms\FormBuilder;
use Combodo\iTop\Forms\Block\AbstractFormBlock;
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
use Combodo\iTop\Forms\Block\Expression\ExpressionFormBlock;
use Combodo\iTop\Forms\Block\FormBlock;
use Combodo\iTop\Forms\Block\IO\FormBinding;
@@ -20,8 +19,8 @@ use Combodo\iTop\Forms\Block\IO\FormOutput;
*/
class DependencyMap
{
/** @var array array of blocks with dependencies group by dependence */
private array $aBlocksWithDependenciesGroupByDependence = [];
/** @var array array of blocks impacted by dependence */
private array $aBlocksImpactedBy = [];
/** @var array array of binding */
private array $aBindings = [];
@@ -57,13 +56,13 @@ class DependencyMap
foreach ($this->aBlocksWithDependencies as $oDependentBlock) {
/** Iterate throw the block inputs bindings... @var FormBinding $oBinding**/
foreach ($oDependentBlock->GetInputsBindings() as $oBinding) {
foreach ($oDependentBlock->GetBoundInputsBindings() as $oBinding) {
// OUT > IN
if($oBinding->oSourceIO instanceof FormOutput
&& $oBinding->oDestinationIO instanceof FormInput){
$this->AddBindingToMap($this->aBindingsOutputToInput, $oBinding);
$this->AddToBlockWithDependenciesMap($oBinding->oSourceIO->GetOwnerBlock()->GetName(), $oDependentBlock);
$this->AddToBlockImpactedBy($oBinding->oSourceIO->GetOwnerBlock()->GetName(), $oDependentBlock);
}
// IN > IN
@@ -75,7 +74,7 @@ class DependencyMap
}
/** Iterate throw the block inputs connections... @var FormBinding $oBinding**/
foreach ($oDependentBlock->GetOutputBindings() as $oBinding) {
foreach ($oDependentBlock->GetBoundOutputBindings() as $oBinding) {
// OUT > OUT
if($oBinding->oSourceIO instanceof FormOutput
@@ -88,45 +87,6 @@ class DependencyMap
}
/**
* @param string $sDependenceBlockName
* @param AbstractFormBlock $oBlockWithDependencies
*
* @return void
*/
private function AddToBlockWithDependenciesMap(string $sDependenceBlockName, AbstractFormBlock $oBlockWithDependencies): void
{
// Initialize array for this dependence
if(!array_key_exists($sDependenceBlockName, $this->aBlocksWithDependenciesGroupByDependence)){
$this->aBlocksWithDependenciesGroupByDependence[$sDependenceBlockName] = [];
}
// Add the block
$this->aBlocksWithDependenciesGroupByDependence[$sDependenceBlockName][$oBlockWithDependencies->GetName()] = $oBlockWithDependencies;
// TODO
if($oBlockWithDependencies instanceof ExpressionFormBlock){
foreach($oBlockWithDependencies->GetOutputs() as $oOutput){
foreach($oOutput->GetBindings() as $oBinding){
$this->AddToBlockWithDependenciesMap($sDependenceBlockName, $oBinding->oDestinationIO->GetOwnerBlock());
}
}
}
}
/**
* @param string $sBlockName
*
* @return array|null
*/
public function GetBlocksDependingOn(string $sBlockName): ?array
{
if(!array_key_exists($sBlockName,$this->aBlocksWithDependenciesGroupByDependence)){
return null;
}
return $this->aBlocksWithDependenciesGroupByDependence[$sBlockName] ?? null;
}
/**
* Add a binding to a map.
*
@@ -154,10 +114,36 @@ class DependencyMap
$this->aBindings[] = $oBinding;
}
/**
* @param string $sDependsOnName
* @param AbstractFormBlock $oImpactedBlock
*
* @return void
*/
private function AddToBlockImpactedBy(string $sDependsOnName, AbstractFormBlock $oImpactedBlock): void
{
// Initialize array for this dependence
if(!array_key_exists($sDependsOnName, $this->aBlocksImpactedBy)){
$this->aBlocksImpactedBy[$sDependsOnName] = [];
}
// Add the block
$this->aBlocksImpactedBy[$sDependsOnName][$oImpactedBlock->GetName()] = $oImpactedBlock;
// TODO
if($oImpactedBlock instanceof ExpressionFormBlock){
foreach($oImpactedBlock->GetOutputs() as $oOutput){
foreach($oOutput->GetBindings() as $oBinding){
$this->AddToBlockImpactedBy($sDependsOnName, $oBinding->oDestinationIO->GetOwnerBlock());
}
}
}
}
/**
* @return array
*/
public function GetInitialBoundOutputBlockNames(): array
public function GetImpactingBlocksWithoutDependencies(): array
{
$aResult = [];
@@ -173,79 +159,78 @@ class DependencyMap
return $aResult;
}
public function GetImpacted(string $sBlockName): array
/**
* Get block impacted by a given block.
* The blocks can be filtered using a callable.
*
* @param string $sBlockName
* @param callable|null $oFilter
*
* @return array|null
*/
public function GetBlocksImpactedBy(string $sBlockName, callable $oFilter = null): ?array
{
$aImpacted = [];
if (array_key_exists($sBlockName, $this->aBindingsOutputToInput)) {
foreach ($this->aBindingsOutputToInput[$sBlockName] as $aBindings) {
foreach ($aBindings as $oBinding) {
$oDestBlock = $oBinding->oDestinationIO->GetOwnerBlock();
if ($oDestBlock instanceof AbstractTypeFormBlock) {
$aImpacted[] = $oDestBlock;
} else {
$aImpacted = array_merge($aImpacted, $this->GetImpacted($oDestBlock->GetName()));
}
}
}
if(!array_key_exists($sBlockName, $this->aBlocksImpactedBy)){
return null;
}
$aBlocks = $this->aBlocksImpactedBy[$sBlockName];
// Filtering
if($oFilter !== null){
$aBlocks = array_filter($aBlocks, $oFilter);
}
return $aImpacted;
return $aBlocks;
}
/**
* Check if a block impacts other blocks.
*
* @param string $sBlockName
*
* @return bool
*/
public function IsBlockHasOutputs(string $sBlockName): bool
public function HasBlocksImpactedBy(string $sBlockName): bool
{
return array_key_exists($sBlockName, $this->aBindingsOutputToInput);
return $this->GetBlocksImpactedBy($sBlockName) !== null;
}
/**
* @param string $sBlockName
* Get bindings OUT > IN.
*
* @return array
*/
public function GetOutputsForBlock(string $sBlockName): array
{
return array_keys($this->aBindingsOutputToInput[$sBlockName]);
}
public function GetOutputsDependenciesForBlock(string $sOutputBlockName): array
{
return $this->aBindingsOutputToInput[$sOutputBlockName];
}
public function IsTheBlockInDependencies(string $sBlockName): bool
{
// foreach ($this->aDependentBlocks as $oDependentBlock)
// {
// if($oDependentBlock->getName() === $sBlockName) {
// return true;
// }
// }
//
// return false;
return $this->GetBlocksDependingOn($sBlockName) !== null;
}
public function GetOutputToInputs(): array
{
return $this->aBindingsOutputToInput;
}
/**
* Get bindings IN > IN.
*
* @return array
*/
public function GetInputToInputs(): array
{
return $this->aBindingsInputToInput;
}
/**
* Get bindings OUT > OUT.
*
* @return array
*/
public function GetOutputToOutputs(): array
{
return $this->aBindingsOutputToOutputs;
}
public function GetBindings()
/**
* Get all bindings.
*
* @return array
*/
public function GetAllBindings()
{
return $this->aBindings;
}

View File

@@ -32,8 +32,7 @@ class FormBuilder implements FormBuilderInterface, IteratorAggregate
/** @var AbstractFormBlock */
private AbstractFormBlock $oFormBlock;
/** @var array sub blocks */
private array $aChildren = [];
/** @var FormBuilderInterface */
private readonly FormBuilderInterface $builder;
/**
@@ -63,7 +62,7 @@ class FormBuilder implements FormBuilderInterface, IteratorAggregate
*/
private function BuildForm(FormBlock $oFormBlock): void
{
// Hidden (ignore)
// Prevent form build option
$aOptions = $this->builder->getOptions();
if (array_key_exists('prevent_form_build', $aOptions) && $aOptions['prevent_form_build']) {
return;
@@ -73,11 +72,8 @@ class FormBuilder implements FormBuilderInterface, IteratorAggregate
/** Iterate throw the form sub blocks... @var FormBlock $oSubFormBlock */
foreach ($oFormBlock->GetChildren() as $sBlockName => $oChildBlock) {
// Add to the children
$this->aChildren[$sBlockName] = $oChildBlock;
// Handle block
$bHasDependency = $this->HandleSubBlock($oChildBlock);
// Handle child block
$bHasDependency = $this->HandleChildBlock($oChildBlock);
// Add to the array of blocks with dependencies
if ($bHasDependency) {
@@ -88,7 +84,7 @@ class FormBuilder implements FormBuilderInterface, IteratorAggregate
// Create a dependency handler if needed
if (count($aBlocksWithDependencies) > 0) {
$this->oDependencyHandler = new DependencyHandler($this->builder->getName(), $oFormBlock, $this, $this->aChildren, $aBlocksWithDependencies);
$this->oDependencyHandler = new DependencyHandler($this, $oFormBlock, $aBlocksWithDependencies);
$oFormBlock->oDependencyMap = $this->oDependencyHandler->GetMap();
}
@@ -105,7 +101,7 @@ class FormBuilder implements FormBuilderInterface, IteratorAggregate
*
* @return bool
*/
private function HandleSubBlock(AbstractFormBlock $oSubFormBlock): bool
private function HandleChildBlock(AbstractFormBlock $oSubFormBlock): bool
{
// Has dependencies blocks
@@ -122,18 +118,6 @@ class FormBuilder implements FormBuilderInterface, IteratorAggregate
return true;
}
/**
* Get a child block.
*
* @param string $sName
*
* @return AbstractFormBlock|null
*/
public function GetChild(string $sName): ?AbstractFormBlock
{
return $this->aChildren[$sName] ?? null;
}
/**
* Return the dependency handler attached to this builder.
*

View File

@@ -51,8 +51,9 @@
<div id="turbo_{{ block.id }}" class="ibo-field ibo-content-block ibo-block ibo-field-small">
{% if block.added == 1 %}
{{ form_row(form[block.name]) }}
{% else %}
<div style="background-color: #ecd9eb;border-radius: 6px;padding: 2px 5px;">Reserved place for <b>{{ block.name }}</b></div>
{# {% else %}#}
{# {% else %}#}
{# <div style="background-color: #ecd9eb;border-radius: 6px;padding: 2px 5px;">Reserved place for <b>{{ block.name }}</b></div>#}
{% endif %}
</div>
{% endfor %}

View File

@@ -5,8 +5,8 @@
{% UITurboUpdate Standard { sTarget: "turbo_" ~ sBlockIdentifier} %}
{% if oBlockToRedraw is not null %}
{{ form_row(oBlockToRedraw) }}
{% else %}
<div style="background-color: #ecd9eb;border-radius: 6px;padding: 2px 5px;">Reserved place for <b>{{ sBlockIdentifier }}</b></div>
{# {% else %}#}
{# <div style="background-color: #ecd9eb;border-radius: 6px;padding: 2px 5px;">Reserved place for <b>{{ sBlockIdentifier }}</b></div>#}
{% endif %}
{% EndUITurboUpdate %}
{% endfor %}