SDK Form demonstrator

This commit is contained in:
Eric Espie
2025-04-11 17:13:22 +02:00
parent c0b39a6ecb
commit c4a9e980da
12 changed files with 407 additions and 0 deletions

View File

@@ -562,7 +562,70 @@ Call $this->AddInitialAttributeFlags($sAttCode, $iFlags) for all the initial att
</event_data>
</event>
</events>
<forms>
<form id="DashletHeaderDynamicProperties" xsi:type="FormType">
<form_types>
<form_type id="title" xsi:type="TextType">
<options>
<label>UI:DashletHeaderDynamic:Prop-Title</label>
<default>UI:DashletHeaderDynamic:Prop-Title:Default</default>
</options>
</form_type>
<form_type id="icon" xsi:type="IconType">
<options>
<label>UI:DashletHeaderDynamic:Prop-Icon</label>
<default>images/icons/icons8-customer.svg</default>
</options>
</form_type>
<form_type id="subtitle" xsi:type="TextType">
<options>
<label>UI:DashletHeaderDynamic:Prop-Subtitle</label>
<default>UI:DashletHeaderDynamic:Prop-Subtitle:Default</default>
</options>
</form_type>
<form_type id="query" xsi:type="QueryType">
<options>
<label>UI:DashletHeaderDynamic:Prop-Query</label>
<default>SELECT Contact</default>
</options>
</form_type>
<form_type id="group_by" xsi:type="GroupByAttcodeType">
<options>
<label>UI:DashletHeaderDynamic:Prop-GroupBy</label>
<default>status</default>
<class_source>query</class_source>
</options>
</form_type>
<form_type id="values" xsi:type="ValuesFromAttcodeType">
<options>
<label>UI:DashletHeaderDynamic:Prop-Values</label>
<default>
<values>
<value id="active"/>
<value id="inactive"/>
</values>
</default>
<attcode_source>group_by</attcode_source>
</options>
</form_type>
</form_types>
</form>
</forms>
<meta>
<form_type_definitions>
<form_type_definition id="IconType">
<options>
<option id="label">
<mandatory>true</mandatory>
<type>string</type>
</option>
<option id="default">
<mandatory>true</mandatory>
<type>string</type>
</option>
</options>
</form_type_definition>
</form_type_definitions>
<classes>
<class id="cmdbAbstractObject" _delta="define">
<methods>

View File

@@ -42,6 +42,7 @@ register_shutdown_function(function()
$sReservedMemory = null;
if (!is_null($err = error_get_last()) && ($err['type'] == E_ERROR))
{
var_export($err);
// Remove stack trace from MySQLException (since 2.7.2 see N°3174)
$sMessage = $err['message'];
if (strpos($sMessage, 'MySQLException') !== false) {

View File

@@ -430,6 +430,12 @@ return array(
'Combodo\\iTop\\Dependencies\\NPM\\iTopNPM' => $baseDir . '/sources/Dependencies/NPM/iTopNPM.php',
'Combodo\\iTop\\DesignDocument' => $baseDir . '/core/designdocument.class.inc.php',
'Combodo\\iTop\\DesignElement' => $baseDir . '/core/designdocument.class.inc.php',
'Combodo\\iTop\\FormType\\Base\\HiddenType' => $baseDir . '/sources/FormType/Base/HiddenType.php',
'Combodo\\iTop\\FormType\\Base\\TextType' => $baseDir . '/sources/FormType/Base/TextType.php',
'Combodo\\iTop\\FormType\\Base\\TextareaType' => $baseDir . '/sources/FormType/Base/TextareaType.php',
'Combodo\\iTop\\FormType\\Orm\\AttCodeGroupByType' => $baseDir . '/sources/FormType/Orm/AttCodeGroupByType.php',
'Combodo\\iTop\\FormType\\Orm\\QueryType' => $baseDir . '/sources/FormType/Orm/QueryType.php',
'Combodo\\iTop\\FormType\\Orm\\ValuesFromAttcodeType' => $baseDir . '/sources/FormType/Orm/ValuesFromAttcodeType.php',
'Combodo\\iTop\\Form\\Field\\AbstractSimpleField' => $baseDir . '/sources/Form/Field/AbstractSimpleField.php',
'Combodo\\iTop\\Form\\Field\\BlobField' => $baseDir . '/sources/Form/Field/BlobField.php',
'Combodo\\iTop\\Form\\Field\\CaseLogField' => $baseDir . '/sources/Form/Field/CaseLogField.php',

View File

@@ -785,6 +785,12 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Dependencies\\NPM\\iTopNPM' => __DIR__ . '/../..' . '/sources/Dependencies/NPM/iTopNPM.php',
'Combodo\\iTop\\DesignDocument' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php',
'Combodo\\iTop\\DesignElement' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php',
'Combodo\\iTop\\FormType\\Base\\HiddenType' => __DIR__ . '/../..' . '/sources/FormType/Base/HiddenType.php',
'Combodo\\iTop\\FormType\\Base\\TextType' => __DIR__ . '/../..' . '/sources/FormType/Base/TextType.php',
'Combodo\\iTop\\FormType\\Base\\TextareaType' => __DIR__ . '/../..' . '/sources/FormType/Base/TextareaType.php',
'Combodo\\iTop\\FormType\\Orm\\AttCodeGroupByType' => __DIR__ . '/../..' . '/sources/FormType/Orm/AttCodeGroupByType.php',
'Combodo\\iTop\\FormType\\Orm\\QueryType' => __DIR__ . '/../..' . '/sources/FormType/Orm/QueryType.php',
'Combodo\\iTop\\FormType\\Orm\\ValuesFromAttcodeType' => __DIR__ . '/../..' . '/sources/FormType/Orm/ValuesFromAttcodeType.php',
'Combodo\\iTop\\Form\\Field\\AbstractSimpleField' => __DIR__ . '/../..' . '/sources/Form/Field/AbstractSimpleField.php',
'Combodo\\iTop\\Form\\Field\\BlobField' => __DIR__ . '/../..' . '/sources/Form/Field/BlobField.php',
'Combodo\\iTop\\Form\\Field\\CaseLogField' => __DIR__ . '/../..' . '/sources/Form/Field/CaseLogField.php',

View File

@@ -697,6 +697,9 @@ abstract class Controller extends AbstractController
public function GetForm(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
{
if (is_null($data)) {
$data = $type::GetDefaultData();
}
return $this->GetFormBuilder($type, $data,$options)->getForm();
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\FormType\Base;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType as SymfonyHiddenType;
class HiddenType extends AbstractType
{
public function getParent(): ?string
{
return SymfonyHiddenType::class;
}
public function getBlockPrefix(): string
{
return 'itop_hidden';
}
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\FormType\Base;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType as SymfonyTextType;
class TextType extends AbstractType
{
public function getParent(): ?string
{
return SymfonyTextType::class;
}
public function getBlockPrefix(): string
{
return 'itop_text';
}
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\FormType\Base;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType as SymfonyTextareaType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TextareaType extends AbstractType
{
public function getParent(): ?string
{
return SymfonyTextareaType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
}
public function getBlockPrefix(): string
{
return 'itop_textarea';
}
}

View File

@@ -0,0 +1,124 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\FormType\Orm;
use Combodo\iTop\FormType\Base\HiddenType;
use Dict;
use Exception;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType as SymfonyChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AttCodeGroupByType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('hidden', HiddenType::class, ['mapped' => false]);
$sRelatedNode = $options['query_source'];
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($sRelatedNode): void {
$oForm = $event->getForm();
$sCurrentValue = $oForm->getParent()->get($sRelatedNode)->getData();
$this->BuildSubField($oForm, $sCurrentValue);
});
$builder->get('hidden')->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use ($sRelatedNode): void {
$oForm = $event->getForm()->getParent();
$sCurrentValue = $oForm->getParent()->get($sRelatedNode)->getData();
$this->BuildSubField($oForm, $sCurrentValue);
});
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setRequired('query_source');
$resolver->setAllowedTypes('query_source', 'string');
}
public function BuildSubField(FormInterface $oForm, string $sQuery): void
{
$aData = $oForm->getParent()->getData();
\IssueLog::Info('Form Data: '.var_export($aData, true));
//$aFormOptions['inherit_data'] = true;
$aFormOptions['choices'] = $this->GetGroupByOptions($sQuery);
$aFormOptions['multiple'] = false;
// create the field, this is similar the $builder->add()
// field name, field type, field options
$oForm->add('selected', SymfonyChoiceType::class, $aFormOptions);
}
protected $oModelReflection;
protected function GetGroupByOptions($sOql)
{
$this->oModelReflection = new \ModelReflectionRuntime();
$aGroupBy = array();
try
{
$oQuery = $this->oModelReflection->GetQuery($sOql);
$sClass = $oQuery->GetClass();
foreach($this->oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType)
{
// For external fields, find the real type of the target
$sExtFieldAttCode = $sAttCode;
$sTargetClass = $sClass;
while (is_a($sAttType, 'AttributeExternalField', true))
{
$sExtKeyAttCode = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtFieldAttCode, 'extkey_attcode');
$sTargetAttCode = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtFieldAttCode, 'target_attcode');
$sTargetClass = $this->oModelReflection->GetAttributeProperty($sTargetClass, $sExtKeyAttCode, 'targetclass');
$aTargetAttCodes = $this->oModelReflection->ListAttributes($sTargetClass);
$sAttType = $aTargetAttCodes[$sTargetAttCode];
$sExtFieldAttCode = $sTargetAttCode;
}
$aForbidenAttType = [
'AttributeLinkedSet',
'AttributeFriendlyName',
'iAttributeNoGroupBy', //we cannot only use iAttributeNoGroupBy since this method is also used by the designer who do not have access to the classes' PHP reflection API. So the known classes has to be listed altogether
'AttributeOneWayPassword',
'AttributeEncryptedString',
'AttributePassword',
];
foreach ($aForbidenAttType as $sForbidenAttType) {
if (is_a($sAttType, $sForbidenAttType, true))
{
continue 2;
}
}
$sLabel = $this->oModelReflection->GetLabel($sClass, $sAttCode);
if (!in_array($sLabel, $aGroupBy))
{
$aGroupBy[$sAttCode] = $sLabel;
if (is_a($sAttType, 'AttributeDateTime', true))
{
$aGroupBy[$sAttCode.':hour'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Hour', $sLabel);
$aGroupBy[$sAttCode.':month'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Month', $sLabel);
$aGroupBy[$sAttCode.':day_of_week'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-DayOfWeek', $sLabel);
$aGroupBy[$sAttCode.':day_of_month'] = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-DayOfMonth', $sLabel);
}
}
}
asort($aGroupBy);
}
catch (Exception $e)
{
// Fallback in case of OQL problem
}
return array_flip($aGroupBy);
}
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\FormType\Orm;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType as SymfonyTextareaType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class QueryType extends AbstractType
{
public function getParent(): ?string
{
return SymfonyTextareaType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefault('attr', ['class' => 'ibo-is-code ibo-query-oql']);
}
public function getBlockPrefix(): string
{
return 'itop_query';
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\FormType\Orm;
use Combodo\iTop\FormType\Base\HiddenType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType as SymfonyChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ValuesFromAttcodeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('hidden', HiddenType::class, ['mapped' => false]);
$sAttCodeType = $options['attcode_source'];
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($sAttCodeType): void {
$oForm = $event->getForm();
$sAttCode = $oForm->getParent()->get($sAttCodeType)->get('selected')->getData();
$this->BuildSubField($oForm, $sAttCode);
});
$builder->get('hidden')->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use ($sAttCodeType): void {
$oForm = $event->getForm()->getParent();
$sAttCode = $oForm->getParent()->get($sAttCodeType)->get('selected')->getData();
$this->BuildSubField($oForm, $sAttCode);
});
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setRequired('attcode_source');
$resolver->setAllowedTypes('attcode_source', 'string');
}
public function BuildSubField(FormInterface $oForm, string $sAttCode): void
{
$aData = $oForm->getParent()->getData();
\IssueLog::Info('Form Data: '.var_export($aData, true));
$sQuery = $aData['query'];
$sClass = \DBSearch::FromOQL($sQuery)->GetClass();
$oAttDef = \MetaModel::GetAttributeDef($sClass, $sAttCode);
//$aFormOptions['inherit_data'] = true;
$aFormOptions['choices'] = array_flip($oAttDef->GetAllowedValues());
$aFormOptions['multiple'] = true;
// create the field, this is similar the $builder->add()
// field name, field type, field options
$oForm->add('selected', SymfonyChoiceType::class, $aFormOptions);
}
}

View File

@@ -0,0 +1,37 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Application\FormType\Base;
use Combodo\iTop\FormType\Orm\AttCodeGroupByType;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Forms;
class TextTypeTest extends iTopTestCase
{
public function GetFormBuilder(string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface
{
$oFormFactory = Forms::createFormFactoryBuilder()
->addExtension(new HttpFoundationExtension())
->getFormFactory();
return $oFormFactory->createBuilder($type, $data,$options);
}
public function GetForm(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
{
return $this->GetFormBuilder($type, $data,$options)->getForm();
}
public function testTextType()
{
$oFormView = $this->GetForm(AttCodeGroupByType::class)->createView();
return;
}
}