mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-23 18:48:51 +02:00
poc form SDK (extends to form)
This commit is contained in:
@@ -130,7 +130,9 @@ const Collection = function(oForm, objectFormUrl){
|
||||
.then((data) => {
|
||||
const oModalBody = $('#object_modal .modal-body');
|
||||
oModalBody.html(data.template);
|
||||
oForm.handleElement(oModalBody[0]);
|
||||
oModalBody[0].querySelectorAll('form').forEach((formEl) => {
|
||||
oForm.handleElement(formEl);
|
||||
});
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error(error);
|
||||
|
||||
146
js/DI/form.js
146
js/DI/form.js
@@ -52,62 +52,96 @@ const Form = function(oWidget){
|
||||
}
|
||||
|
||||
/**
|
||||
* changeOptions.
|
||||
* updateField.
|
||||
*
|
||||
* @param oEvent
|
||||
* @param sId
|
||||
* @param oForm
|
||||
* @param oElement
|
||||
* @param aDependentAttCodes
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function changeOptions(oEvent, sId){
|
||||
async function updateField(oEvent, oForm, oElement, aDependentAttCodes){
|
||||
|
||||
// retrieve field that's need to be updated
|
||||
const oDependentField = document.getElementById(sId);
|
||||
const sName = oDependentField.getAttribute('name');
|
||||
const sAttCode = oDependentField.getAttribute('data-att-code');
|
||||
const aDependenciesAttCodes = [];
|
||||
|
||||
// retrieve parent form
|
||||
const oForm = oDependentField.closest('form');
|
||||
/////////////////////////////////
|
||||
// I - CONSTRUCT DEPENDENCIES (The original event target but also other required dependencies)
|
||||
|
||||
// retrieve field container
|
||||
const oContainer = oDependentField.closest(aSelectors.dataBlockContainer);
|
||||
// the value of the field change, sAttCodes needs to be updated
|
||||
aDependentAttCodes.forEach((sAttCode) => {
|
||||
|
||||
// set field container loading state
|
||||
oContainer.classList.add('loading');
|
||||
// field to update
|
||||
const oDependsOnElement = oElement.querySelector(String.format(aSelectors.dataAttCode, sAttCode));
|
||||
|
||||
// prepare request body
|
||||
const oFormData = new FormData(oForm);
|
||||
// retrieve field container
|
||||
const oContainer = oDependsOnElement.closest(aSelectors.dataBlockContainer);
|
||||
|
||||
// set field container loading state
|
||||
oContainer.classList.add('loading');
|
||||
|
||||
// retrieve dependency data
|
||||
const sDependsOn = oDependsOnElement.dataset.dependsOn;
|
||||
|
||||
// may have multiple dependencies
|
||||
let aDependsEls = sDependsOn.split(DEPENDS_ON_SEPARATOR);
|
||||
|
||||
aDependsEls.forEach((sAtt) => {
|
||||
if(!aDependenciesAttCodes.includes(sAtt)){
|
||||
aDependenciesAttCodes.push(sAtt);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/////////////////////////////////
|
||||
// II - PREPARE RELOAD REQUEST
|
||||
|
||||
// prepare quest data
|
||||
let sRequestBody = '';
|
||||
function encode(s){ return encodeURIComponent(s).replace(/%20/g,'+'); }
|
||||
for(let pair of oFormData.entries()){
|
||||
if(typeof pair[1]=='string'){
|
||||
sRequestBody += (sRequestBody?'&':'') + encode(pair[0])+'='+encode(pair[1]);
|
||||
let $bFirst = true;
|
||||
|
||||
// iterate throw dependencies...
|
||||
aDependenciesAttCodes.forEach(function(sAtt) {
|
||||
|
||||
const oDependsOnElement = oElement.querySelector(String.format(aSelectors.dataAttCode, sAtt));
|
||||
if(!$bFirst){
|
||||
sRequestBody += '&';
|
||||
}
|
||||
}
|
||||
sRequestBody += '&att_code=' + oDependentField.dataset.attCode;
|
||||
sRequestBody += '&dependency_att_code=' + oEvent.target.dataset.attCode;
|
||||
sRequestBody += 'partial_object['+oDependsOnElement.dataset.attCode + ']=' + oDependsOnElement.value;
|
||||
$bFirst = false;
|
||||
});
|
||||
|
||||
sRequestBody += '&att_codes=' + aDependentAttCodes.join(',');
|
||||
sRequestBody += '&dependency_att_codes=' + aDependenciesAttCodes.join(',');
|
||||
|
||||
/////////////////////////////////
|
||||
// III - UPDATE THE FORM
|
||||
|
||||
// update fom
|
||||
const sUpdateFormResponse = await updateForm(sRequestBody, oForm.dataset.reloadUrl, oForm.getAttribute('method'));
|
||||
const oHtml = oToolkit.parseTextToHtml(sUpdateFormResponse);
|
||||
let oSingle = oHtml.getElementById('object_single_attribute');
|
||||
oContainer.replaceWith(oSingle.firstChild);
|
||||
|
||||
// remove loading state
|
||||
oContainer.classList.remove('loading');
|
||||
const sReloadResponse = await updateForm(sRequestBody, oForm.dataset.reloadUrl, oForm.getAttribute('method'));
|
||||
|
||||
// update new dependent field
|
||||
const oNewDependentField = document.querySelector(`[id$="${sAttCode}"]`);
|
||||
oNewDependentField.setAttribute('name', sName);
|
||||
oNewDependentField.setAttribute('id', sId);
|
||||
oNewDependentField.setAttribute('data-att-code', sAttCode);
|
||||
const oReloadedElement = oToolkit.parseTextToHtml(sReloadResponse);
|
||||
|
||||
// init dynamics
|
||||
initDependencies(oContainer);
|
||||
initDynamicsInvisible(oContainer);
|
||||
initDynamicsDisable(oContainer);
|
||||
let oPartial = oReloadedElement.getElementById('partial_object');
|
||||
|
||||
aDependentAttCodes.forEach((sAtt) => {
|
||||
|
||||
// dependent element
|
||||
const oDependentElement = oElement.querySelector(String.format(aSelectors.dataAttCode, sAtt));
|
||||
const oContainer = oDependentElement.closest(aSelectors.dataBlockContainer);
|
||||
const sId = oDependentElement.getAttribute('id');
|
||||
const sName = oDependentElement.getAttribute('name');
|
||||
|
||||
// new element
|
||||
const oNewElement = oPartial.querySelector(String.format(aSelectors.dataAttCode, sAtt));
|
||||
const oNewContainer = oNewElement.closest(aSelectors.dataBlockContainer);
|
||||
oNewElement.setAttribute('id', sId);
|
||||
oNewElement.setAttribute('name', sName);
|
||||
|
||||
// replace element
|
||||
oContainer.replaceWith(oNewContainer);
|
||||
});
|
||||
|
||||
// init widgets
|
||||
oWidget.handleElement(oContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +151,13 @@ const Form = function(oWidget){
|
||||
*/
|
||||
function initDependencies(oElement){
|
||||
|
||||
// get all dependent fields
|
||||
// retrieve parent form
|
||||
const oForm = oElement.closest('form');
|
||||
|
||||
// compute dependencies map
|
||||
let aMapDependencies = {};
|
||||
|
||||
// get all field with dependencies
|
||||
const aDependentsFields = oElement.querySelectorAll(aSelectors.dataDependsOn);
|
||||
|
||||
// iterate throw dependent fields...
|
||||
@@ -129,18 +169,30 @@ const Form = function(oWidget){
|
||||
// may have multiple dependencies
|
||||
let aDependsEls = sDependsOn.split(DEPENDS_ON_SEPARATOR);
|
||||
|
||||
// iterate throw dependencies...
|
||||
// iterate throw the dependencies...
|
||||
aDependsEls.forEach(function(sEl){
|
||||
|
||||
// retrieve dependency
|
||||
const oDependsOnElement = oElement.querySelector(`[id$="${sEl}"]`);
|
||||
|
||||
// listen for changes
|
||||
if(oDependsOnElement != null){
|
||||
oDependsOnElement.addEventListener('change', (event) => changeOptions(event, oDependentField.id));
|
||||
// add dependency att code to map
|
||||
if(!(sEl in aMapDependencies)){
|
||||
aMapDependencies[sEl] = [];
|
||||
}
|
||||
aMapDependencies[sEl].push(oDependentField.dataset.attCode);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// iterate throw dependencies map...
|
||||
for(let sAttCode in aMapDependencies){
|
||||
|
||||
// retrieve corresponding field
|
||||
const oDependsOnElement = oElement.querySelector(String.format(aSelectors.dataAttCode, sAttCode));
|
||||
|
||||
// listen changes
|
||||
if(oDependsOnElement !== null){
|
||||
oDependsOnElement.addEventListener('change', (event) => updateField(event, oForm, oElement, aMapDependencies[sAttCode]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Combodo\iTop\DI\Controller;
|
||||
|
||||
use Combodo\iTop\DI\Form\Type\Compound\ConfigurationType;
|
||||
use Combodo\iTop\DI\Form\Type\Compound\ObjectSingleAttributeType;
|
||||
use Combodo\iTop\DI\Form\Type\Compound\PartialObjectType;
|
||||
use Combodo\iTop\DI\Form\Type\Compound\ObjectType;
|
||||
use Combodo\iTop\DI\Services\ObjectService;
|
||||
use Exception;
|
||||
@@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
class Controller extends AbstractController
|
||||
{
|
||||
@@ -209,8 +210,9 @@ class Controller extends AbstractController
|
||||
}
|
||||
|
||||
// create form with request data (dependent field)
|
||||
$oForm = $this->createForm(ObjectType::class, $oObject, [
|
||||
$oForm = $this->createForm(PartialObjectType::class, $oObject, [
|
||||
'object_class' => $class,
|
||||
'att_codes' => explode(',', $request->get('dependency_att_codes'))
|
||||
]);
|
||||
|
||||
// handle form data
|
||||
@@ -220,9 +222,9 @@ class Controller extends AbstractController
|
||||
$oObject->ComputeValues();
|
||||
|
||||
// create a new form for affected field with updated (but not persist) data
|
||||
$oForm = $this->createForm(ObjectSingleAttributeType::class, $oObject, [
|
||||
$oForm = $this->createForm(PartialObjectType::class, $oObject, [
|
||||
'object_class' => $class,
|
||||
'att_code' => $request->get('att_code')
|
||||
'att_codes' => explode(',', $request->get('att_codes'))
|
||||
]);
|
||||
|
||||
// return object form
|
||||
|
||||
@@ -103,6 +103,7 @@ class AttributeBuilder
|
||||
$aFormType['options']['allow_target_creation'] = $oAttributeDefinition->AllowTargetCreation();
|
||||
$aFormType['options']['object_class'] = $oAttributeDefinition->GetTargetClass();
|
||||
$aFormType['options']['att_code'] = $oAttributeDefinition->GetCode();
|
||||
$aFormType['options']['is_locked'] = $bIsLocked;
|
||||
try{
|
||||
$oObjectsSet = MetaModel::GetAllowedValuesAsObjectSet($oAttributeDefinition->GetHostClass(), $oAttributeDefinition->GetCode(), []);
|
||||
$aFormType['options']['choices'] = $this->oObjectService->ToChoices($oObjectsSet);
|
||||
@@ -155,7 +156,7 @@ class AttributeBuilder
|
||||
$aFormType['options']['is_abstract'] = MetaModel::IsAbstract(LinkSetModel::GetTargetClass($oAttributeDefinition));
|
||||
$aFormType['options']['target_class'] = LinkSetModel::GetTargetClass($oAttributeDefinition);
|
||||
if($aFormType['options']['is_abstract']){
|
||||
$aFormType['options']['object_classes'] = MetaModel::EnumChildClasses(LinkSetModel::GetTargetClass($oAttributeDefinition));
|
||||
$aFormType['options']['object_classes'] = $this->oObjectService->listConcreteChildClasses(LinkSetModel::GetTargetClass($oAttributeDefinition));
|
||||
}
|
||||
$aFormType['options']['entry_options'] = [
|
||||
'object_class' => $oAttributeDefinition->GetLinkedClass(),
|
||||
|
||||
@@ -35,6 +35,7 @@ class ExternalKeyType extends AbstractType implements IFormTypeOptionModifier
|
||||
'allow_target_creation' => false,
|
||||
'object_class' => null,
|
||||
'att_code' => null,
|
||||
'is_locked' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -54,9 +55,16 @@ class ExternalKeyType extends AbstractType implements IFormTypeOptionModifier
|
||||
public function getNewOptions(array $aInitialOptions, DBObject $oObject) : array
|
||||
{
|
||||
try{
|
||||
// $iVal = $oObject->Get($aInitialOptions['att_code']); // because we can't list all items du to performance, we want to force current value to be present, even if it's not part of the result
|
||||
$oObjectsSet = MetaModel::GetAllowedValuesAsObjectSet(get_class($oObject), $aInitialOptions['att_code'], ['this' => $oObject]/*, null, $iVal*/);
|
||||
$iVal = $oObject->Get($aInitialOptions['att_code']); // because we can't list all items du to performance, we want to force current value to be present, even if it's not part of the result
|
||||
$oObjectsSet = MetaModel::GetAllowedValuesAsObjectSet(get_class($oObject), $aInitialOptions['att_code'], ['this' => $oObject]);
|
||||
$aInitialOptions['choices'] = $this->oObjectService->ToChoices($oObjectsSet);
|
||||
if($aInitialOptions['is_locked']){
|
||||
$aInitialOptions['choices']['Object in creation'] = 0;
|
||||
}
|
||||
|
||||
// !!! ensure current value is part of result
|
||||
$object = MetaModel::GetObject($aInitialOptions['object_class'], $iVal);
|
||||
$aInitialOptions['choices'][$object->GetName()] = $iVal;
|
||||
}
|
||||
catch(Exception $e){
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
* Used to handle depends on fields.
|
||||
*
|
||||
*/
|
||||
class ObjectSingleAttributeType extends AbstractType
|
||||
class PartialObjectType extends AbstractType
|
||||
{
|
||||
/** @var ObjectFormListener object form modifier */
|
||||
private ObjectFormListener $oObjectFormModifier;
|
||||
@@ -37,7 +37,7 @@ class ObjectSingleAttributeType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults( [
|
||||
'object_class' => null,
|
||||
'att_code' => null
|
||||
'att_codes' => null
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -46,11 +46,15 @@ class ObjectSingleAttributeType extends AbstractType
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
// build form from options
|
||||
$sFormType = $this->oAttributeBuilder->createAttribute($options['object_class'], $options['att_code']);
|
||||
// add requested attributes form types...
|
||||
foreach ($options['att_codes'] as $sAttCode){
|
||||
|
||||
// add form field
|
||||
$builder->add($options['att_code'], $sFormType['type'], $sFormType['options']);
|
||||
// create attribute type
|
||||
$sFormType = $this->oAttributeBuilder->createAttribute($options['object_class'], $sAttCode, null);
|
||||
|
||||
// build form type
|
||||
$builder->add($sAttCode, $sFormType['type'], $sFormType['options']);
|
||||
}
|
||||
|
||||
// dynamic form handling
|
||||
$builder->addEventSubscriber($this->oObjectFormModifier);
|
||||
@@ -9,6 +9,7 @@ use DBObject;
|
||||
use DBObjectSet;
|
||||
use MetaModel;
|
||||
use ormLinkSet;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
class ObjectService
|
||||
{
|
||||
@@ -18,16 +19,20 @@ class ObjectService
|
||||
/** @var string database name */
|
||||
private string $sDbName;
|
||||
|
||||
/** @var \Symfony\Component\Stopwatch\Stopwatch */
|
||||
private Stopwatch $oStopWatch;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param $sDbHost
|
||||
* @param $sDbName
|
||||
*/
|
||||
public function __construct($sDbHost, $sDbName)
|
||||
public function __construct($sDbHost, $sDbName, Stopwatch $oStopWatch)
|
||||
{
|
||||
$this->sDbHost = $sDbHost;
|
||||
$this->sDbName = $sDbName;
|
||||
$this->oStopWatch = $oStopWatch;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,6 +66,8 @@ class ObjectService
|
||||
*/
|
||||
public function ToChoices(DBObjectSet $oObjectsSet) : array
|
||||
{
|
||||
$this->oStopWatch->start('ToChoices');
|
||||
|
||||
$aChoices = [];
|
||||
|
||||
// Retrieve friendly name complementary specification
|
||||
@@ -75,11 +82,13 @@ class ObjectService
|
||||
$oObjectsSet->OptimizeColumnLoad([$oObjectsSet->GetClassAlias() => $aDefaultFieldsToLoad]);
|
||||
|
||||
$i = 0;
|
||||
while ($i < 30 && $oObj = $oObjectsSet->Fetch()) {
|
||||
while ($i < 10 && $oObj = $oObjectsSet->Fetch()) {
|
||||
$aChoices[$oObj->GetName()] = $oObj->GetKey();
|
||||
$i++;
|
||||
}
|
||||
|
||||
$this->oStopWatch->stop('ToChoices');
|
||||
|
||||
return $aChoices;
|
||||
}
|
||||
|
||||
@@ -143,4 +152,13 @@ class ObjectService
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function listConcreteChildClasses(string $sObjectClass)
|
||||
{
|
||||
$aChildClasses = MetaModel::EnumChildClasses($sObjectClass);
|
||||
return array_filter($aChildClasses, function ($sChildClass){
|
||||
return !MetaModel::IsAbstract($sChildClass);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -108,8 +108,6 @@
|
||||
|
||||
</table>
|
||||
|
||||
{% set buttonDataAttributes = 'data-object-class="' ~ object_class ~ '" data-ext-key-to-me="' ~ ext_key_to_me ~ '"' %}
|
||||
|
||||
{% if is_abstract %}
|
||||
{# create item button #}
|
||||
<div class="d-inline-block">
|
||||
@@ -119,7 +117,7 @@
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown_scroll_300">
|
||||
{% for object_class in object_classes %}
|
||||
<li><a class="dropdown-item create_item_link" href="#" {{ buttonDataAttributes|raw }}>{{ object_class }}</a></li>
|
||||
<li><a class="dropdown-item create_item_link" href="#" data-object-class="{{ object_class }}" data-ext-key-to-me="{{ ext_key_to_me }}">{{ object_class }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -129,7 +127,7 @@
|
||||
<button type="button" class="add_item_link btn btn-secondary btn-sm" data-collection-holder-class="{{ form.vars.id }}">Add {{ target_class }}</button>
|
||||
{% else %}
|
||||
{# create item button #}
|
||||
<button type="button" class="btn btn-secondary create_item_link btn-sm" {{ buttonDataAttributes|raw }}>Create {{ target_class }}</button>
|
||||
<button type="button" class="btn btn-secondary create_item_link btn-sm" data-object-class="{{ target_class }}" data-ext-key-to-me="{{ ext_key_to_me }}">Create {{ target_class }}</button>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -46,7 +46,9 @@
|
||||
|
||||
{# Form #}
|
||||
const oForm = new Form(oWidget);
|
||||
oForm.handleElement(document);
|
||||
document.querySelectorAll('form').forEach((formEl) => {
|
||||
oForm.handleElement(formEl);
|
||||
});
|
||||
|
||||
{# Collection #}
|
||||
const oCollection = new Collection(oForm, '{{ path('object_form', {'class': 'object_class', 'id' : 0, 'name': 'form_name'}) }}');
|
||||
|
||||
Reference in New Issue
Block a user