diff --git a/css/backoffice/components/_form.scss b/css/backoffice/components/_form.scss index d80fdd96a..0282865ff 100644 --- a/css/backoffice/components/_form.scss +++ b/css/backoffice/components/_form.scss @@ -6,4 +6,44 @@ .ibo-prop-header { @extend %ibo-font-size-150; padding-bottom: 14px; -} \ No newline at end of file +} + +.help-text{ + padding: 1px 5px; + background-color: #d7e3f8; + border: 1px solid #c6e7f5; + border-radius: 5px; + margin: 5px 0; + font-size: 0.9em; +} + +.form-error ul{ + padding: 1px 5px; + background-color: #f8d7da; + border: 1px solid #f5c6cb; + border-radius: 5px; + margin: 5px 0; + font-size: 0.9em; +} + +.subform{ + background-color: #efefef; + border-radius: 5px; + padding: 10px; +} + +.form-buttons{ + margin: 20px 0; +} + +#form select{ + padding: 0; + overflow-y: auto; +} + +#form select option{ + height: 30px; + display: flex; + align-items: center; +} + diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index c338b2bcc..5a6923f58 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -473,6 +473,16 @@ return array( 'Combodo\\iTop\\Form\\Validator\\MultipleChoicesValidator' => $baseDir . '/sources/Form/Validator/MultipleChoicesValidator.php', 'Combodo\\iTop\\Form\\Validator\\NotEmptyExtKeyValidator' => $baseDir . '/sources/Form/Validator/NotEmptyExtKeyValidator.php', 'Combodo\\iTop\\Form\\Validator\\SelectObjectValidator' => $baseDir . '/sources/Form/Validator/SelectObjectValidator.php', + 'Combodo\\iTop\\Forms\\Dependency\\DependencyDescription' => $baseDir . '/sources/Forms/Dependency/DependencyDescription.php', + 'Combodo\\iTop\\Forms\\Dependency\\DependencyHandler' => $baseDir . '/sources/Forms/Dependency/DependencyHandler.php', + 'Combodo\\iTop\\Forms\\FormBuilder\\FormBuilder' => $baseDir . '/sources/Forms/FormBuilder/FormBuilder.php', + 'Combodo\\iTop\\Forms\\FormBuilder\\FormTypeExtension' => $baseDir . '/sources/Forms/FormBuilder/FormTypeExtension.php', + 'Combodo\\iTop\\Forms\\FormBuilder\\ResolvedFormType' => $baseDir . '/sources/Forms/FormBuilder/ResolvedFormType.php', + 'Combodo\\iTop\\Forms\\FormBuilder\\ResolvedFormTypeFactory' => $baseDir . '/sources/Forms/FormBuilder/ResolvedFormTypeFactory.php', + 'Combodo\\iTop\\Forms\\FormType\\AttributeChoiceType' => $baseDir . '/sources/Forms/FormType/AttributeChoiceType.php', + 'Combodo\\iTop\\Forms\\FormType\\AttributeValueChoiceType' => $baseDir . '/sources/Forms/FormType/AttributeValueChoiceType.php', + 'Combodo\\iTop\\Forms\\FormType\\OqlType' => $baseDir . '/sources/Forms/FormType/OqlType.php', + 'Combodo\\iTop\\Forms\\Forms' => $baseDir . '/sources/Forms/Forms.php', 'Combodo\\iTop\\Forms\\Twig\\Extension\\FormCompatibilityExtension' => $baseDir . '/sources/Forms/Twig/Extension/FormCompatibilityExtension.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\PhpExpressionEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php', 'Combodo\\iTop\\Renderer\\BlockRenderer' => $baseDir . '/sources/Renderer/BlockRenderer.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index f237f3d1e..2bd29fa91 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -854,6 +854,16 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Form\\Validator\\MultipleChoicesValidator' => __DIR__ . '/../..' . '/sources/Form/Validator/MultipleChoicesValidator.php', 'Combodo\\iTop\\Form\\Validator\\NotEmptyExtKeyValidator' => __DIR__ . '/../..' . '/sources/Form/Validator/NotEmptyExtKeyValidator.php', 'Combodo\\iTop\\Form\\Validator\\SelectObjectValidator' => __DIR__ . '/../..' . '/sources/Form/Validator/SelectObjectValidator.php', + 'Combodo\\iTop\\Forms\\Dependency\\DependencyDescription' => __DIR__ . '/../..' . '/sources/Forms/Dependency/DependencyDescription.php', + 'Combodo\\iTop\\Forms\\Dependency\\DependencyHandler' => __DIR__ . '/../..' . '/sources/Forms/Dependency/DependencyHandler.php', + 'Combodo\\iTop\\Forms\\FormBuilder\\FormBuilder' => __DIR__ . '/../..' . '/sources/Forms/FormBuilder/FormBuilder.php', + 'Combodo\\iTop\\Forms\\FormBuilder\\FormTypeExtension' => __DIR__ . '/../..' . '/sources/Forms/FormBuilder/FormTypeExtension.php', + 'Combodo\\iTop\\Forms\\FormBuilder\\ResolvedFormType' => __DIR__ . '/../..' . '/sources/Forms/FormBuilder/ResolvedFormType.php', + 'Combodo\\iTop\\Forms\\FormBuilder\\ResolvedFormTypeFactory' => __DIR__ . '/../..' . '/sources/Forms/FormBuilder/ResolvedFormTypeFactory.php', + 'Combodo\\iTop\\Forms\\FormType\\AttributeChoiceType' => __DIR__ . '/../..' . '/sources/Forms/FormType/AttributeChoiceType.php', + 'Combodo\\iTop\\Forms\\FormType\\AttributeValueChoiceType' => __DIR__ . '/../..' . '/sources/Forms/FormType/AttributeValueChoiceType.php', + 'Combodo\\iTop\\Forms\\FormType\\OqlType' => __DIR__ . '/../..' . '/sources/Forms/FormType/OqlType.php', + 'Combodo\\iTop\\Forms\\Forms' => __DIR__ . '/../..' . '/sources/Forms/Forms.php', 'Combodo\\iTop\\Forms\\Twig\\Extension\\FormCompatibilityExtension' => __DIR__ . '/../..' . '/sources/Forms/Twig/Extension/FormCompatibilityExtension.php', 'Combodo\\iTop\\PhpParser\\Evaluation\\PhpExpressionEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php', 'Combodo\\iTop\\Renderer\\BlockRenderer' => __DIR__ . '/../..' . '/sources/Renderer/BlockRenderer.php', diff --git a/sources/Application/TwigBase/Controller/Controller.php b/sources/Application/TwigBase/Controller/Controller.php index 48b6770c3..5e7b71008 100644 --- a/sources/Application/TwigBase/Controller/Controller.php +++ b/sources/Application/TwigBase/Controller/Controller.php @@ -27,6 +27,7 @@ use Combodo\iTop\Application\WebPage\iTopWebPage; use Combodo\iTop\Application\WebPage\WebPage; use Combodo\iTop\Controller\AbstractController; use Combodo\iTop\Service\InterfaceDiscovery\InterfaceDiscovery; +use Combodo\iTop\Forms\Forms; use Dict; use Exception; use ExecutionKPI; @@ -45,7 +46,6 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormFactoryBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormRenderer; -use Symfony\Component\Form\Forms; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Csrf\CsrfTokenManager; use Twig\Error\SyntaxError; diff --git a/sources/Forms/Dependency/DependencyDescription.php b/sources/Forms/Dependency/DependencyDescription.php new file mode 100644 index 000000000..ac10635d0 --- /dev/null +++ b/sources/Forms/Dependency/DependencyDescription.php @@ -0,0 +1,107 @@ +isAdded; + } + + public function SetAdded(bool $bAdded): void + { + $this->isAdded = $bAdded; + } + + public function IsDataReady(string $sEventType): bool + { + $aData = ($sEventType === FormEvents::POST_SET_DATA) ? $this->aPostSetData : $this->aPostSubmitData; + + foreach (array_keys($this->aDependencies) as $sInput){ + if(!array_key_exists($sInput, $aData) || $aData[$sInput] == null){ + return false; + } + } + + return true; + } + + public function IsPostSetDataReady(): bool + { + foreach ($this->aDependencies as $sData => $sValue) { + if (!array_key_exists($sData, $this->aPostSetData)) { + return false; + } + } + return true; + } + + public function IsPostSubmitDataReady(): bool + { + foreach ($this->aDependencies as $sData => $sValue) { + if (!array_key_exists($sData, $this->aPostSubmitData)) { + return false; + } + } + return true; + } + + public function SetData(string $sEventType, string $sData, mixed $oValue): void + { + if($oValue === null) return; + + if($sEventType === FormEvents::POST_SET_DATA){ + $this->aPostSetData[$sData] = $oValue; + } + else{ + $this->aPostSubmitData[$sData] = $oValue; + } + } + + public function GetData(string $sEventType): array + { + if($sEventType === FormEvents::POST_SET_DATA){ + return $this->aPostSetData; + } + else{ + return $this->aPostSubmitData; + } + } + + public function SetPostSetData(string $sInput, mixed $oData): void + { + $this->aPostSetData[$sInput] = $oData; + } + + public function SetPostSubmitData(string $sInput, mixed $oData): void + { + $this->aPostSubmitData[$sInput] = $oData; + } + + public function IsReady(string $sEventType): bool + { + $aData = ($sEventType === FormEvents::POST_SET_DATA) ? $this->aPostSetData : $this->aPostSubmitData; + + foreach (array_keys($this->aDependencies) as $sInput){ + if(!array_key_exists($sInput, $aData)){ + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/sources/Forms/Dependency/DependencyHandler.php b/sources/Forms/Dependency/DependencyHandler.php new file mode 100644 index 000000000..4264430e0 --- /dev/null +++ b/sources/Forms/Dependency/DependencyHandler.php @@ -0,0 +1,173 @@ +addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { + $oForm = $event->getForm(); + $this->InitializeDependenciesMap($oForm); + }); + } + + /** + *Initialize the dependencies map and register listeners on the dependencies inputs. + * + * @param FormInterface $oForm + * + * @return void + */ + private function InitializeDependenciesMap(FormInterface $oForm): void + { + + /** iterate throw dependencies descriptions... @var DependencyDescription $oDependencyDescription */ + foreach ($this->aDependenciesDescription as $oDependencyDescription) { + + /** iterate throw dependencies items... */ + foreach ($oDependencyDescription->aDependencies as $sInput => $aData) { + + // get the dependency field name + $sDependency = $aData['source']; + + // get the field input name + $sOutput = array_key_exists('output', $aData) ? $aData['output'] : null; + + // add the dependency to the map + if(!array_key_exists($sDependency, $this->aDependenciesMap)){ + $this->aDependenciesMap[$sDependency] = []; + } + $this->aDependenciesMap[$sDependency][] = ['description' => $oDependencyDescription, 'input' => $sInput, 'output' => $sOutput]; + } + } + + /** iterate throw dependencies... */ + foreach (array_keys($this->aDependenciesMap) as $sDependency){ + + // Listen the dependency + $this->builder->get($sDependency)->addEventListener(FormEvents::POST_SET_DATA, $this->GetEventListeningCallback()); + $this->builder->get($sDependency)->addEventListener(FormEvents::POST_SUBMIT, $this->GetEventListeningCallback()); + + } + + } + + /** + * Add a dependency description. + * + * @param DependencyDescription $oDependencyDescription + * + * @return void + */ + public function AddDependencyDescription(DependencyDescription $oDependencyDescription): void + { + $this->aDependenciesDescription[] = $oDependencyDescription; + } + + /** + * The event handling callback. + * + * @return callable + */ + private function GetEventListeningCallback(): callable + { + return function (FormEvent $event){ + + // Get the event type + $sEventType = $this->GetEventType($event); + + // Get the form + $oForm = $event->getForm(); + + /** Iterate throw dependencies map... */ + foreach ($this->aDependenciesMap[$event->getForm()->getName()] as $aData){ + + // Get the map data + $oDependencyDescription = $aData['description']; + $sInput = $aData['input']; + $sOutput = $aData['output']; + + // Compute output value + $oValue = $event->getData(); + if(array_key_exists('outputs', $event->getForm()->getConfig()->getOptions())){ + $aOutputs = $event->getForm()->getConfig()->getOptions()['outputs']; + if(array_key_exists($sOutput, $aOutputs)){ + $oValue = $aOutputs[$sOutput]($oValue); + } + } + + // Store the input value + $oDependencyDescription->SetData($sEventType, $sInput, $oValue); + + // When dependencies met, add the dependent field if not already done + if(!$oDependencyDescription->IsAdded() && $oDependencyDescription->IsDataReady($sEventType)) { + + // Get the dependent field options + $aOptions = $oDependencyDescription->options; + + // Add the listener callback to the dependent field if it is also a dependency for another field + if(is_string($oDependencyDescription->child) && array_key_exists($oDependencyDescription->child, $this->aDependenciesMap)) { + $aOptions = array_merge($aOptions, [ + 'listener_callback' => $this->GetEventListeningCallback(), + ]); + } + + // Add the dependent field to the form + $oForm->getParent()->add($oDependencyDescription->child, $oDependencyDescription->type, array_merge($aOptions, $oDependencyDescription->type::GetOptionsFromInputs($oDependencyDescription->GetData($sEventType)))); + + // Mark the dependency as added + $oDependencyDescription->SetAdded(true); + } + } + + }; + + } + + /** + * Get the event type. + * + * @param FormEvent $event + * + * @return string + * @throws Exception + */ + private function GetEventType(FormEvent $event): string + { + if($event instanceof PostSetDataEvent) { + return FormEvents::POST_SET_DATA; + } + else if($event instanceof PostSubmitEvent) { + return FormEvents::POST_SUBMIT; + } + + throw new Exception(sprintf("Unknown event type %s", get_class($event))); + } + + + + + + +} \ No newline at end of file diff --git a/sources/Forms/FormBuilder/FormBuilder.php b/sources/Forms/FormBuilder/FormBuilder.php new file mode 100644 index 000000000..ca235cdf7 --- /dev/null +++ b/sources/Forms/FormBuilder/FormBuilder.php @@ -0,0 +1,429 @@ +dependencyHandler === null){ + \IssueLog::Error('create dependency handler ' . $this->builder->getName()); + $this->dependencyHandler = new DependencyHandler($this->builder); + } + + $this->dependencyHandler->AddDependencyDescription($oDependencyDescription); + } + + public function add(string|FormBuilderInterface $child, ?string $type = null, array $options = []): static + { + if(!empty($options['bindings'])) { + $this->builder->add($child, HiddenType::class); + $this->AddDependency(new DependencyDescription($options['bindings'], $child, $type, $options)); + } + else{ + $this->builder->add($child, $type, $options); + } + + return $this; + } + + public function addExpression(string $name, string $expression): static + { + $options['bindings'] = [$expression]; + return $this->add($name, null, $options); + } + + // pure decoration of FormBuilderInterface + + public function getIterator(): Traversable + { + return $this->builder->getIterator(); + } + + public function count(): int + { + return $this->builder->count(); + } + public function create(string $name, ?string $type = null, array $options = []): FormBuilderInterface + { + return $this->builder->create($name, $type, $options); + } + + public function get(string $name): FormBuilderInterface + { + return $this->builder->get($name); + } + + public function remove(string $name): static + { + $this->builder->remove($name); + return $this; + } + + public function has(string $name): bool + { + return $this->builder->has($name); + } + + public function all(): array + { + return $this->builder->all(); + } + + public function getForm(): FormInterface + { + return $this->builder->getForm(); + } + + public function addEventListener(string $eventName, callable $listener, int $priority = 0): static + { + $this->builder->addEventListener($eventName, $listener, $priority); + return $this; + } + + public function addEventSubscriber(EventSubscriberInterface $subscriber): static + { + $this->builder->addEventSubscriber($subscriber); + return $this; + } + + public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false): static + { + $this->builder->addViewTransformer($viewTransformer, $forcePrepend); + return $this; + } + + public function resetViewTransformers(): static + { + $this->builder->resetViewTransformers(); + return $this; + } + + public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false): static + { + $this->builder->addModelTransformer($modelTransformer, $forceAppend); + return $this; + } + + public function resetModelTransformers(): static + { + $this->builder->resetModelTransformers(); + return $this; + } + + public function setAttribute(string $name, mixed $value): static + { + $this->builder->setAttribute($name, $value); + return $this; + } + + public function setAttributes(array $attributes): static + { + $this->builder->setAttributes($attributes); + return $this; + } + + public function setDataMapper(?DataMapperInterface $dataMapper): static + { + $this->builder->setDataMapper($dataMapper); + return $this; + } + + public function setDisabled(bool $disabled): static + { + $this->builder->setDisabled($disabled); + return $this; + } + + public function setEmptyData(mixed $emptyData): static + { + $this->builder->setEmptyData($emptyData); + return $this; + } + + public function setErrorBubbling(bool $errorBubbling): static + { + $this->builder->setErrorBubbling($errorBubbling); + return $this; + } + + public function setRequired(bool $required): static + { + $this->builder->setRequired($required); + return $this; + } + + public function setPropertyPath(PropertyPathInterface|string|null $propertyPath): static + { + $this->builder->setPropertyPath($propertyPath); + return $this; + } + + public function setMapped(bool $mapped): static + { + $this->builder->setMapped($mapped); + return $this; + } + + public function setByReference(bool $byReference): static + { + $this->builder->setByReference($byReference); + return $this; + } + + public function setInheritData(bool $inheritData): static + { + $this->builder->setInheritData($inheritData); + return $this; + } + + public function setCompound(bool $compound): static + { + $this->builder->setCompound($compound); + return $this; + } + + public function setType(ResolvedFormTypeInterface $type): static + { + $this->builder->setType($type); + return $this; + } + + public function setData(mixed $data): static + { + $this->builder->setData($data); + return $this; + } + + public function setDataLocked(bool $locked): static + { + $this->builder->setDataLocked($locked); + return $this; + } + + public function setFormFactory(FormFactoryInterface $formFactory) + { + $this->builder->setFormFactory($formFactory); + } + + public function setAction(string $action): static + { + $this->builder->setAction($action); + return $this; + } + + public function setMethod(string $method): static + { + $this->builder->setMethod($method); + return $this; + } + + public function setRequestHandler(RequestHandlerInterface $requestHandler): static + { + $this->builder->setRequestHandler($requestHandler); + return $this; + } + + public function setAutoInitialize(bool $initialize): static + { + $this->builder->setAutoInitialize($initialize); + return $this; + } + + public function getFormConfig(): FormConfigInterface + { + return $this->builder->getFormConfig(); + } + + public function setIsEmptyCallback(?callable $isEmptyCallback): static + { + $this->builder->setIsEmptyCallback($isEmptyCallback); + return $this; + } + + public function getEventDispatcher(): EventDispatcherInterface + { + return $this->builder->getEventDispatcher(); + } + + public function getName(): string + { + return $this->builder->getName(); + } + + public function getPropertyPath(): ?PropertyPathInterface + { + return $this->builder->getPropertyPath(); + } + + public function getMapped(): bool + { + return $this->builder->getMapped(); + } + + public function getByReference(): bool + { + return $this->builder->getByReference(); + } + + public function getInheritData(): bool + { + return $this->builder->getInheritData(); + } + + public function getCompound(): bool + { + return $this->builder->getCompound(); + } + + public function getType(): ResolvedFormTypeInterface + { + return $this->builder->getType(); + } + + public function getViewTransformers(): array + { + return $this->builder->getViewTransformers(); + } + + public function getModelTransformers(): array + { + return $this->builder->getModelTransformers(); + } + + public function getDataMapper(): ?DataMapperInterface + { + return $this->builder->getDataMapper(); + } + + public function getRequired(): bool + { + return $this->builder->getRequired(); + } + + public function getDisabled(): bool + { + return $this->builder->getDisabled(); + } + + public function getErrorBubbling(): bool + { + return $this->builder->getErrorBubbling(); + } + + public function getEmptyData(): mixed + { + return $this->builder->getEmptyData(); + } + + public function getAttributes(): array + { + return $this->builder->getAttributes(); + } + + public function hasAttribute(string $name): bool + { + return $this->builder->hasAttribute($name); + } + + public function getAttribute(string $name, mixed $default = null): mixed + { + return $this->builder->getAttribute($name, $default); + } + + public function getData(): mixed + { + return $this->builder->getData(); + } + + public function getDataClass(): ?string + { + return $this->builder->getDataClass(); + } + + public function getDataLocked(): bool + { + return $this->builder->getDataLocked(); + } + + public function getFormFactory(): FormFactoryInterface + { + return $this->builder->getFormFactory(); + } + + public function getAction(): string + { + return $this->builder->getAction(); + } + + public function getMethod(): string + { + return $this->builder->getMethod(); + } + + public function getRequestHandler(): RequestHandlerInterface + { + return $this->builder->getRequestHandler(); + } + + public function getAutoInitialize(): bool + { + return $this->builder->getAutoInitialize(); + } + + public function getOptions(): array + { + return $this->builder->getOptions(); + } + + public function hasOption(string $name): bool + { + return $this->builder->hasOption($name); + } + + public function getOption(string $name, mixed $default = null): mixed + { + return $this->builder->getOption($name, $default); + } + + public function getIsEmptyCallback(): ?callable + { + return $this->builder->getIsEmptyCallback(); + } +} \ No newline at end of file diff --git a/sources/Forms/FormBuilder/FormTypeExtension.php b/sources/Forms/FormBuilder/FormTypeExtension.php new file mode 100644 index 000000000..06298196b --- /dev/null +++ b/sources/Forms/FormBuilder/FormTypeExtension.php @@ -0,0 +1,39 @@ +setDefined([ + 'inputs', + 'outputs', + 'bindings', + 'listener_callback' + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + if(array_key_exists('listener_callback', $options)) { + $builder->addEventListener(FormEvents::POST_SET_DATA, $options['listener_callback']); + $builder->addEventListener(FormEvents::POST_SUBMIT, $options['listener_callback']); + } + + } +} \ No newline at end of file diff --git a/sources/Forms/FormBuilder/ResolvedFormType.php b/sources/Forms/FormBuilder/ResolvedFormType.php new file mode 100644 index 000000000..65503730c --- /dev/null +++ b/sources/Forms/FormBuilder/ResolvedFormType.php @@ -0,0 +1,21 @@ +setDefault('inputs', [ + 'object_class' => 'string' + ]); + + $resolver->setDefault('outputs', [ + 'attribute' => function($oData) { + return $oData; + } + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + // on pre submit + $builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event) 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); + } + + }, 1); + } + + private function CheckValue($oValue, $options): bool + { + + if(!in_array($oValue, $options['choices'])){ + return false; + } + + + return true; + } + + public static function GetOptionsFromInputs(array $inputs): array + { + $aAttributeCodes = \MetaModel::GetAttributesList($inputs['object_class']); + + return [ + 'choices' => array_combine($aAttributeCodes, $aAttributeCodes) + ]; + } + +} \ No newline at end of file diff --git a/sources/Forms/FormType/AttributeValueChoiceType.php b/sources/Forms/FormType/AttributeValueChoiceType.php new file mode 100644 index 000000000..053d5f633 --- /dev/null +++ b/sources/Forms/FormType/AttributeValueChoiceType.php @@ -0,0 +1,79 @@ +setDefault('required', false); + $resolver->setDefault('multiple', true); + + $resolver->setDefault('attr', array( + 'size' => 10, + 'style' => 'height: auto;' + )); + + $resolver->setDefault('inputs', array( + 'object_class' => 'string', + 'attribute' => 'string' + )); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + // on pre submit + $builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event) 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); + } + + }, 1); + } + + private function CheckValue($oValue, $options): bool + { + if(!is_array($oValue)){ + return false; + } + + foreach ($oValue as $v){ + if(!in_array($v, $options['choices'])){ + return false; + } + } + + return true; + } + + public static function GetOptionsFromInputs(array $inputs): array + { + $aValues = []; + + if(!empty($inputs['attribute'])){ + $oAttDef = \MetaModel::GetAttributeDef($inputs['object_class'], $inputs['attribute']); + $aValues = $oAttDef->GetAllowedValues(); + $aValues = $aValues !== null ? array_combine($aValues, $aValues) : []; + } + + return [ + 'choices' => $aValues + ]; + } +} \ No newline at end of file diff --git a/sources/Forms/FormType/OqlType.php b/sources/Forms/FormType/OqlType.php new file mode 100644 index 000000000..5a7ddb8e9 --- /dev/null +++ b/sources/Forms/FormType/OqlType.php @@ -0,0 +1,29 @@ +setDefault('outputs', array( + 'selected_class' => function($oData) { + if($oData === null) + return null; + // extract selected class + preg_match('/SELECT\s+(\w+)/', $oData, $aMatches); + return $aMatches[1] ?? null; + } + )); + } + +} \ No newline at end of file diff --git a/sources/Forms/Forms.php b/sources/Forms/Forms.php new file mode 100644 index 000000000..a2ebbfaa2 --- /dev/null +++ b/sources/Forms/Forms.php @@ -0,0 +1,46 @@ +getFormFactory(); + } + + /** + * Creates a form factory builder with the iTop configuration. + */ + public static function createFormFactoryBuilder(): FormFactoryBuilderInterface + { + return (new FormFactoryBuilder()) + ->addExtension(new HttpFoundationExtension()) + ->addTypeExtension(new FormTypeExtension()) + ->setResolvedTypeFactory(new ResolvedFormTypeFactory()); + } + + /** + * This class cannot be instantiated. + */ + private function __construct() + { + } +} \ No newline at end of file diff --git a/templates/application/forms/itop_console_layout.html.twig b/templates/application/forms/itop_console_layout.html.twig index a89c1d04c..956e69694 100644 --- a/templates/application/forms/itop_console_layout.html.twig +++ b/templates/application/forms/itop_console_layout.html.twig @@ -6,6 +6,7 @@ {% if type == 'text' %}{% set ibo_class='ibo-input-string' %}{% else %}{% set ibo_class='ibo-input-' ~ type %}{% endif %} {% set attr = attr|merge({class: (attr.class|default('') ~ ' ibo-input ' ~ ibo_class)|trim}) %} {{- parent() -}} + onChange="this.form.requestSubmit();" {%- endblock widget_attributes -%} {%- block form_label -%} @@ -21,3 +22,9 @@ {% set row_attr = row_attr|merge({class: (row_attr.class|default('') ~ ' ibo-field ibo-content-block ibo-block ibo-field-small')|trim}) %} {{- parent() -}} {%- endblock form_row -%} + +{%- block form_errors -%} +
+ {{- parent() -}} +
+{%- endblock form_errors -%}