N°803 - Allow display & edition of attributes on n:n relations on Portal

This commit is contained in:
Benjamin Dalsass
2023-06-05 16:19:06 +02:00
parent 519751faa1
commit 8eb1053daa
9 changed files with 542 additions and 2074 deletions

View File

@@ -2385,43 +2385,59 @@ class AttributeLinkedSet extends AttributeDefinition
{
if ($oFormField === null)
{
$sFormFieldClass = static::GetFormFieldClass();
$oFormField = new $sFormFieldClass($this->GetCode());
}
$sFormFieldClass = static::GetFormFieldClass();
$oFormField = new $sFormFieldClass($this->GetCode());
}
// Setting target class
// Setting target class
if (!$this->IsIndirect()) {
$sTargetClass = $this->GetLinkedClass();
} else {
/** @var \AttributeExternalKey $oRemoteAttDef */
/** @var \AttributeLinkedSetIndirect $this */
$oRemoteAttDef = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote());
$sTargetClass = $oRemoteAttDef->GetTargetClass();
$sTargetClass = $this->GetLinkedClass();
} else {
/** @var \AttributeExternalKey $oRemoteAttDef */
/** @var \AttributeLinkedSetIndirect $this */
$oRemoteAttDef = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote());
$sTargetClass = $oRemoteAttDef->GetTargetClass();
/** @var \AttributeLinkedSetIndirect $this */
$oFormField->SetExtKeyToRemote($this->GetExtKeyToRemote());
}
$oFormField->SetTargetClass($sTargetClass);
$oFormField->SetIndirect($this->IsIndirect());
// Setting attcodes to display
$aAttCodesToDisplay = MetaModel::FlattenZList(MetaModel::GetZListItems($sTargetClass, 'list'));
// - Adding friendlyname attribute to the list is not already in it
$sTitleAttCode = MetaModel::GetFriendlyNameAttributeCode($sTargetClass);
if (($sTitleAttCode !== null) && !in_array($sTitleAttCode, $aAttCodesToDisplay)) {
$aAttCodesToDisplay = array_merge(array($sTitleAttCode), $aAttCodesToDisplay);
}
// - Adding attribute labels
$aAttributesToDisplay = array();
foreach ($aAttCodesToDisplay as $sAttCodeToDisplay) {
$oAttDefToDisplay = MetaModel::GetAttributeDef($sTargetClass, $sAttCodeToDisplay);
$aAttributesToDisplay[$sAttCodeToDisplay] = $oAttDefToDisplay->GetLabel();
}
$oFormField->SetAttributesToDisplay($aAttributesToDisplay);
/** @var \AttributeLinkedSetIndirect $this */
$oFormField->SetExtKeyToRemote($this->GetExtKeyToRemote());
}
$oFormField->SetTargetClass($sTargetClass);
$oFormField->SetLinkedClass($this->GetLinkedClass());
$oFormField->SetIndirect($this->IsIndirect());
// Setting attcodes to display
$aAttCodesToDisplay = MetaModel::FlattenZList(MetaModel::GetZListItems($sTargetClass, 'list'));
// - Adding friendlyname attribute to the list is not already in it
$sTitleAttCode = MetaModel::GetFriendlyNameAttributeCode($sTargetClass);
if (($sTitleAttCode !== null) && !in_array($sTitleAttCode, $aAttCodesToDisplay)) {
$aAttCodesToDisplay = array_merge(array($sTitleAttCode), $aAttCodesToDisplay);
}
// - Adding attribute properties
$aAttributesToDisplay = array();
foreach ($aAttCodesToDisplay as $sAttCodeToDisplay) {
$oAttDefToDisplay = MetaModel::GetAttributeDef($sTargetClass, $sAttCodeToDisplay);
$aAttributesToDisplay[$sAttCodeToDisplay] = [
'label' => $oAttDefToDisplay->GetLabel(),
'mandatory' => !$oAttDefToDisplay->IsNullAllowed(),
];
}
$oFormField->SetAttributesToDisplay($aAttributesToDisplay);
parent::MakeFormField($oObject, $oFormField);
// Append lnk attributes (filtered from zlist)
$aLnkAttDefToDisplay = MetaModel::GetZListAttDefsFilteredForIndirectLinkClass($this->m_sHostClass, $this->m_sCode);
$aLnkAttributesToDisplay = array();
foreach ($aLnkAttDefToDisplay as $oLnkAttDefToDisplay) {
$aLnkAttributesToDisplay[$oLnkAttDefToDisplay->GetCode()] = [
'sortable' => false,
'label' => $oLnkAttDefToDisplay->GetLabel(),
'mandatory' => !$oLnkAttDefToDisplay->IsNullAllowed(),
];
}
$oFormField->SetLnkAttributesToDisplay($aLnkAttributesToDisplay);
return $oFormField;
}
parent::MakeFormField($oObject, $oFormField);
return $oFormField;
}
public function IsPartOfFingerprint()
{

File diff suppressed because one or more lines are too long

View File

@@ -1802,3 +1802,22 @@ table .group-actions .item-action-wrapper .panel-body > p:last-child{
.wiki_broken_link {
text-decoration: line-through;
}
/**********************************************************/
/* Shameful area (things that should be refactored soon) */
/**********************************************************/
/* Hide attributes label in link set edition, will be fixed during attributes refactoring */
.form_linkedset_wrapper label {
display: none;
}
/* Add mandatory field column label */
.form_linkedset_wrapper .dataTables_scrollHead th.mandatory:after {
content: "*";
position: relative;
left: 3px;
color: #EA7D1E;
font-size: 0.9em;
}

View File

@@ -29,6 +29,7 @@ use BinaryExpression;
use Combodo\iTop\Portal\Brick\CreateBrick;
use Combodo\iTop\Portal\Helper\ApplicationHelper;
use Combodo\iTop\Portal\Helper\ContextManipulatorHelper;
use Combodo\iTop\Renderer\Bootstrap\FieldRenderer\BsLinkedSetFieldRenderer;
use DBObject;
use DBObjectSearch;
use DBObjectSet;
@@ -1312,14 +1313,20 @@ class ObjectController extends BrickController
/** @var \Combodo\iTop\Portal\Helper\ScopeValidatorHelper $oScopeValidator */
$oScopeValidator = $this->get('scope_validator');
$aData = array();
// Data array
$aData = array(
'js_inline' => '',
'css_inline' => '',
);
// Retrieving parameters
$sObjectClass = $oRequestManipulator->ReadParam('sObjectClass', '');
$sLinkClass = $oRequestManipulator->ReadParam('sLinkClass', '');
$aObjectIds = $oRequestManipulator->ReadParam('aObjectIds', array(), FILTER_UNSAFE_RAW);
$aObjectAttCodes = $oRequestManipulator->ReadParam('aObjectAttCodes', array(), FILTER_UNSAFE_RAW);
if (empty($sObjectClass) || empty($aObjectIds) || empty($aObjectAttCodes))
{
$aLinkAttCodes = $oRequestManipulator->ReadParam('aLinkAttCodes', array(), FILTER_UNSAFE_RAW);
if (empty($sObjectClass) || empty($aObjectIds) || empty($aObjectAttCodes)) {
IssueLog::Info(__METHOD__.' at line '.__LINE__.' : sObjectClass, aObjectIds and aObjectAttCodes expected, "'.$sObjectClass.'", "'.implode('/',
$aObjectIds).'" given.');
throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR, 'Invalid request data, some information are missing');
@@ -1338,15 +1345,35 @@ class ObjectController extends BrickController
// Checking that id is in the AttCodes
// Note: We do that AFTER the array is used in OptimizeColumnLoad() because the function doesn't support this anymore.
if (!in_array('id', $aObjectAttCodes))
{
if (!in_array('id', $aObjectAttCodes)) {
$aObjectAttCodes = array_merge(array('id'), $aObjectAttCodes);
}
// Retrieving objects
while ($oObject = $oSet->Fetch())
{
$aData['items'][] = $this->PrepareObjectInformation($oObject, $aObjectAttCodes);
while ($oObject = $oSet->Fetch()) {
// Prepare link data
$aObjectData = $this->PrepareObjectInformation($oObject, $aObjectAttCodes);
// New link object (needed for renderers)
$oNewLink = new $sLinkClass();
foreach ($aLinkAttCodes as $sAttCode) {
$oAttDef = MetaModel::GetAttributeDef($sLinkClass, $sAttCode);
$oField = $oAttDef->MakeFormField($oNewLink);
$sFieldRendererClass = BsLinkedSetFieldRenderer::GetFieldRendererClass($oField);
$sValue = $oAttDef->GetAsHTML($oNewLink->Get($sAttCode));
if ($sFieldRendererClass !== null) {
$oFieldRenderer = new $sFieldRendererClass($oField);
$oFieldOutput = $oFieldRenderer->Render();
$sValue = $oFieldOutput->GetHtml();
}
$aObjectData['attributes'][$sAttCode] = [
'att_code' => $sAttCode,
'value' => $sValue,
'css_inline' => $oFieldOutput->GetCss(),
'js_inline' => $oFieldOutput->GetJs(),
];
}
$aData['items'][] = $aObjectData;
}
return new JsonResponse($aData);
@@ -1356,7 +1383,7 @@ class ObjectController extends BrickController
* Prepare a DBObject information as an array for a client side usage (typically, add a row in a table)
*
* @param \DBObject $oObject
* @param array $aAttCodes
* @param array $aAttCodes
*
* @return array
*
@@ -1372,8 +1399,8 @@ class ObjectController extends BrickController
$sObjectClass = get_class($oObject);
$aObjectData = array(
'id' => $oObject->GetKey(),
'name' => $oObject->GetName(),
'id' => $oObject->GetKey(),
'name' => $oObject->GetName(),
'attributes' => array(),
);

View File

@@ -44,6 +44,7 @@ use Exception;
use ExceptionLog;
use InlineImage;
use IssueLog;
use LogChannels;
use MetaModel;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -859,7 +860,10 @@ class ObjectFormManager extends FormManager
foreach ($aAttCodesToDisplay as $sAttCodeToDisplay)
{
$oAttDefToDisplay = MetaModel::GetAttributeDef($oField->GetTargetClass(), $sAttCodeToDisplay);
$aAttributesToDisplay[$sAttCodeToDisplay] = $oAttDefToDisplay->GetLabel();
$aAttributesToDisplay[$sAttCodeToDisplay] = [
'label' => $oAttDefToDisplay->GetLabel(),
'mandatory' => !$oAttDefToDisplay->IsNullAllowed(),
];
}
$oField->SetAttributesToDisplay($aAttributesToDisplay);
}
@@ -869,7 +873,7 @@ class ObjectFormManager extends FormManager
/** @var \ormLinkSet $oFieldOriginalSet */
$oFieldOriginalSet = $oField->GetCurrentValue();
while ($oLink = $oFieldOriginalSet->Fetch()) {
foreach ($oFieldOriginalSet as $oLink) {
if ($oField->IsIndirect()) {
$iRemoteKey = $oLink->Get($oAttDef->GetExtKeyToRemote());
} else {
@@ -1099,7 +1103,7 @@ class ObjectFormManager extends FormManager
{
$aData = parent::OnSubmit($aArgs);
if (! $aData['valid']) {
if (!$aData['valid']) {
return $aData;
}
@@ -1282,6 +1286,15 @@ class ObjectFormManager extends FormManager
$oLink = MetaModel::NewObject($sLinkedClass);
$oLink->Set($oAttDef->GetExtKeyToRemote(), $iObjKey);
$oLink->Set($oAttDef->GetExtKeyToMe(), $this->oObject->GetKey());
// Set link attributes values...
foreach ($aObjdata as $sLinkAttCode => $oAttValue) {
if (!is_scalar($oAttValue)) {
IssueLog::Debug("ObjectFormManager::OnUpdate invalid link attribute value, $sLinkAttCode is not a scalar value", LogChannels::PORTAL);
continue;
}
$oLink->Set($sLinkAttCode, $oAttValue);
}
$oLinkSet->AddItem($oLink);
}
// ... or adding remote object when linkset id direct
else
@@ -1290,15 +1303,36 @@ class ObjectFormManager extends FormManager
$oLink = MetaModel::GetObject($sLinkedClass, $iObjKey, false, true);
}
if ($oLink !== null)
{
if ($oLink !== null) {
$oLinkSet->AddItem($oLink);
}
}
}
// Checking links to modify
// TODO: Not implemented yet as we can't change lnk properties in the portal
if ($oAttDef->IsIndirect() && isset($value['current'])) {
foreach ($value['current'] as $iObjKey => $aObjData) {
if ($iObjKey < 0) {
continue;
}
$oLink = null;
$oLinkSet->Rewind();
foreach ($oLinkSet as $oItem) {
if ($oItem->Get('id') != $iObjKey) {
continue;
}
$oLink = $oItem;
foreach ($aObjData as $sLinkAttCode => $oAttValue) {
if (!is_scalar($oAttValue)) {
IssueLog::Debug("ObjectFormManager::OnUpdate invalid link attribute value, $sLinkAttCode is not a scalar value", LogChannels::PORTAL);
continue;
}
$oLink->Set($sLinkAttCode, $oAttValue);
}
$oLinkSet->ModifyItem($oLink);
}
}
}
// Setting value in the object
$this->oObject->Set($sAttCode, $oLinkSet);

View File

@@ -70,4 +70,7 @@ Dict::Add('EN US', 'English', 'English', array(
'UI:Links:Bulk:LinkExistForAllObjects' => 'All objets are already linked',
'UI:Links:Bulk:LinkExistForOneObject' => 'One object is linked',
'UI:Links:Bulk:LinkExistForXObjects' => '{count} objects are linked',
// New item
'UI:Links:NewItem' => 'New item',
));

View File

@@ -71,4 +71,7 @@ Dict::Add('FR FR', 'French', 'Français', array(
'UI:Links:Bulk:LinkExistForAllObjects' => 'Tous les objets sont déjà liés',
'UI:Links:Bulk:LinkExistForOneObject' => 'Un objet est lié',
'UI:Links:Bulk:LinkExistForXObjects' => '{count} objets sont liés',
// New item
'UI:Links:NewItem' => 'Nouvel element',
));

View File

@@ -21,6 +21,8 @@
namespace Combodo\iTop\Form\Field;
use Closure;
use Dict;
use ormLinkSet;
/**
* Description of LinkedSetField
@@ -35,10 +37,12 @@ class LinkedSetField extends Field
/** @var bool DEFAULT_DISPLAY_OPENED */
const DEFAULT_DISPLAY_OPENED = false;
/** @var bool DEFAULT_DISPLAY_LIMITED_ACCESS_ITEMS */
const DEFAULT_DISPLAY_LIMITED_ACCESS_ITEMS = false;
const DEFAULT_DISPLAY_LIMITED_ACCESS_ITEMS = false;
/** @var string $sTargetClass */
protected $sTargetClass;
/** @var string $sLinkedClass */
protected $sLinkedClass;
/** @var string $sExtKeyToRemote */
protected $sExtKeyToRemote;
/** @var bool $bIndirect */
@@ -51,6 +55,8 @@ class LinkedSetField extends Field
protected $aLimitedAccessItemIDs;
/** @var array $aAttributesToDisplay */
protected $aAttributesToDisplay;
/** @var array $aLnkAttributesToDisplay */
protected $aLnkAttributesToDisplay;
/** @var string $sSearchEndpoint */
protected $sSearchEndpoint;
/** @var string $sInformationEndpoint */
@@ -68,6 +74,7 @@ class LinkedSetField extends Field
$this->bDisplayLimitedAccessItems = static::DEFAULT_DISPLAY_LIMITED_ACCESS_ITEMS;
$this->aLimitedAccessItemIDs = array();
$this->aAttributesToDisplay = array();
$this->aLnkAttributesToDisplay = array();
$this->sSearchEndpoint = null;
$this->sInformationEndpoint = null;
@@ -96,6 +103,31 @@ class LinkedSetField extends Field
return $this;
}
/**
* @return string
* @since 3.1
*
*/
public function GetLinkedClass()
{
return $this->sLinkedClass;
}
/**
*
* @since 3.1
*
* @param string $sLinkedClass
*
* @return $this
*/
public function SetLinkedClass(string $sLinkedClass)
{
$this->sLinkedClass = $sLinkedClass;
return $this;
}
/**
*
* @return string
@@ -237,6 +269,35 @@ class LinkedSetField extends Field
return $this;
}
/**
* Returns a hash array of attributes to be displayed in the linkedset in the form $sAttCode => $sAttLabel
*
* @since 3.1
*
* @param boolean $bAttCodesOnly If set to true, will return only the attcodes
*
* @return array
*/
public function GetLnkAttributesToDisplay(bool $bAttCodesOnly = false)
{
return ($bAttCodesOnly) ? array_keys($this->aLnkAttributesToDisplay) : $this->aLnkAttributesToDisplay;
}
/**
*
* @since 3.1
*
* @param array $aAttributesToDisplay
*
* @return $this
*/
public function SetLnkAttributesToDisplay(array $aAttributesToDisplay)
{
$this->aLnkAttributesToDisplay = $aAttributesToDisplay;
return $this;
}
/**
* @return string|null
*/
@@ -288,4 +349,30 @@ class LinkedSetField extends Field
{
return in_array($iItemID, $this->aLimitedAccessItemIDs, false);
}
/** @inheritdoc @since 3.1 */
public function Validate()
{
$bValid = parent::Validate();
/** @var ormLinkSet $oSet */
$oSet = $this->GetCurrentValue();
/** @var \DBObject $oItem */
foreach ($oSet as $oItem) {
list($bRes, $aIssues) = $oItem->CheckToWrite();
if ($bRes === false) {
foreach ($aIssues as $sIssue) {
$sItem = $oItem->Get('friendlyname') != '' ? $oItem->Get('friendlyname') : Dict::S('UI:Links:NewItem');
$this->AddErrorMessage('<b>'.$sItem.' : </b>'.$sIssue);
}
$bValid = false;
}
}
$oSet->Rewind();
return $bValid;
}
}

View File

@@ -22,6 +22,11 @@ namespace Combodo\iTop\Renderer\Bootstrap\FieldRenderer;
use ApplicationContext;
use AttributeFriendlyName;
use Combodo\iTop\Form\Field\Field;
use Combodo\iTop\Renderer\Bootstrap\BsFieldRendererMappings;
use Combodo\iTop\Renderer\FieldRenderer;
use Combodo\iTop\Renderer\RenderingOutput;
use DBObject;
use Dict;
use Exception;
use IssueLog;
@@ -43,52 +48,62 @@ class BsLinkedSetFieldRenderer extends BsFieldRenderer
*/
public function Render()
{
$oOutput = parent::Render();
$oOutput = parent::Render();
$sFieldMandatoryClass = ($this->oField->GetMandatory()) ? 'form_mandatory' : '';
$sFieldDescriptionForHTMLTag = ($this->oField->HasDescription()) ? 'data-tooltip-content="'.utils::HtmlEntities($this->oField->GetDescription()).'"' : '';
// Merge lnk and remote class attributes to display
$aAttributesToDisplay = array_merge($this->oField->GetLnkAttributesToDisplay(), $this->oField->GetAttributesToDisplay());
$iLinkAttributesToDisplayCount = count($this->oField->GetLnkAttributesToDisplay()) + 1;
// Vars to build the table
$sAttributesToDisplayAsJson = json_encode($this->oField->GetAttributesToDisplay());
$sAttributesToDisplayAsJson = json_encode($aAttributesToDisplay);
$sAttCodesToDisplayAsJson = json_encode($this->oField->GetAttributesToDisplay(true));
$sLnkAttCodesToDisplayAsJson = json_encode($this->oField->GetLnkAttributesToDisplay(true));
$aItems = array();
$aItemIds = array();
$this->PrepareItems($aItems, $aItemIds);
$aAddedItemIds = array();
$aAddedTargetIds = array();
$this->InjectRendererFileAssets($this->oField->GetLinkedClass(), $this->oField->GetLnkAttributesToDisplay(true), $oOutput);
$this->PrepareItems($aItems, $aItemIds, $oOutput, $aAddedItemIds, $aAddedTargetIds);
$sItemsAsJson = json_encode($aItems);
$sItemIdsAsJson = utils::EscapeHtml(json_encode(array('current' => $aItemIds)));
$sItemIdsAsJson = utils::EscapeHtml(json_encode(array('current' => $aItemIds, 'add' => $aAddedItemIds)));
if (!$this->oField->GetHidden())
{
foreach ($aAddedTargetIds as $sId) {
$aItemIds[$sId] = array();
}
if (!$this->oField->GetHidden()) {
// Rendering field
$sIsEditable = ($this->oField->GetReadOnly()) ? 'false' : 'true';
$sCollapseTogglerIconVisibleClass = 'glyphicon-menu-down';
$sCollapseTogglerIconHiddenClass = 'glyphicon-menu-down collapsed';
$sCollapseTogglerClass = 'form_linkedset_toggler';
$sCollapseTogglerId = $sCollapseTogglerClass . '_' . $this->oField->GetGlobalId();
$sFieldWrapperId = 'form_linkedset_wrapper_' . $this->oField->GetGlobalId();
$sCollapseTogglerId = $sCollapseTogglerClass.'_'.$this->oField->GetGlobalId();
$sFieldWrapperId = 'form_linkedset_wrapper_'.$this->oField->GetGlobalId();
// Preparing collapsed state
if($this->oField->GetDisplayOpened())
{
$sCollapseTogglerExpanded = 'true';
$sCollapseTogglerIconClass = $sCollapseTogglerIconVisibleClass;
$sCollapseJSInitState = 'true';
}
else
{
$sCollapseTogglerClass .= ' collapsed';
$sCollapseTogglerExpanded = 'false';
$sCollapseTogglerIconClass = $sCollapseTogglerIconHiddenClass;
$sCollapseJSInitState = 'false';
}
if ($this->oField->GetDisplayOpened()) {
$sCollapseTogglerExpanded = 'true';
$sCollapseTogglerIconClass = $sCollapseTogglerIconVisibleClass;
$sCollapseJSInitState = 'true';
} else {
$sCollapseTogglerClass .= ' collapsed';
$sCollapseTogglerExpanded = 'false';
$sCollapseTogglerIconClass = $sCollapseTogglerIconHiddenClass;
$sCollapseJSInitState = 'false';
}
$oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '">');
if ($this->oField->GetLabel() !== '')
{
$oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label" '.$sFieldDescriptionForHTMLTag.'>')
->AddHtml('<a id="' . $sCollapseTogglerId . '" class="' . $sCollapseTogglerClass . '" data-toggle="collapse" href="#' . $sFieldWrapperId . '" aria-expanded="' . $sCollapseTogglerExpanded . '" aria-controls="' . $sFieldWrapperId . '">')
$oOutput->AddHtml('<div class="form-group '.$sFieldMandatoryClass.'">');
if ($this->oField->GetLabel() !== '') {
$oOutput->AddHtml('<label for="'.$this->oField->GetGlobalId().'" class="control-label" '.$sFieldDescriptionForHTMLTag.'>')
->AddHtml('<a id="'.$sCollapseTogglerId.'" class="'.$sCollapseTogglerClass.'" data-toggle="collapse" href="#'.$sFieldWrapperId.'" aria-expanded="'.$sCollapseTogglerExpanded.'" aria-controls="'.$sFieldWrapperId.'">')
->AddHtml($this->oField->GetLabel(), true)
->AddHtml('<span class="text">' . count($aItemIds) . '</span>')
->AddHtml('<span class="glyphicon ' . $sCollapseTogglerIconClass . '"></>')
->AddHtml('<span class="text">'.count($aItemIds).'</span>')
->AddHtml('<span class="glyphicon '.$sCollapseTogglerIconClass.'"></>')
->AddHtml('</a>')
->AddHtml('</label>');
}
@@ -99,7 +114,7 @@ class BsLinkedSetFieldRenderer extends BsFieldRenderer
$sTableId = 'table_' . $this->oField->GetGlobalId();
// - Output
$oOutput->AddHtml(
<<<EOF
<<<EOF
<div class="form_linkedset_wrapper collapse" id="{$sFieldWrapperId}">
<div class="row">
<div class="col-xs-12">
@@ -150,6 +165,7 @@ EOF
var oRawDatas_{$this->oField->GetGlobalId()} = {$sItemsAsJson};
var oTable_{$this->oField->GetGlobalId()};
var oSelectedItems_{$this->oField->GetGlobalId()} = {};
var oRenderersJs_{$this->oField->GetGlobalId()} = '';
var getColumnsDefinition_{$this->oField->GetGlobalId()} = function()
{
@@ -182,15 +198,17 @@ EOF
for(sKey in oColumnProperties_{$this->oField->GetGlobalId()})
{
aColumnProperties = oColumnProperties_{$this->oField->GetGlobalId()}[sKey];
// Level main column
aColumnsDefinition.push({
"width": "auto",
"searchable": true,
"sortable": true,
"title": oColumnProperties_{$this->oField->GetGlobalId()}[sKey],
"sortable": !aColumnProperties.sortable,
"title": aColumnProperties.label,
"defaultContent": "",
"type": "html",
"data": "attributes."+sKey+".att_code",
"className": aColumnProperties.mandatory ? 'mandatory' : '',
"render": function(data, type, row){
var cellElem;
@@ -205,7 +223,7 @@ EOF
cellElem = $('<span></span>');
}
cellElem.html('<span>' + row.attributes[data].value + '</span>');
return cellElem.prop('outerHTML');
},
});
@@ -219,7 +237,7 @@ EOF
// We would just have to override / complete the necessary elements
var buildTable_{$this->oField->GetGlobalId()} = function()
{
var iDefaultOrderColumnIndex = ({$sIsEditable}) ? 1 : 0;
var iDefaultOrderColumnIndex = {$iLinkAttributesToDisplayCount};
// Instantiates datatables
oTable_{$this->oField->GetGlobalId()} = $('#{$sTableId}').DataTable({
@@ -255,7 +273,25 @@ EOF
},
});
});
// Store attributes inline css and js
for (var key in oData.attributes) {
const aElement = oData.attributes[key];
if(aElement.css_inline !== undefined){
$('td:first-child', oRow).append($('<style>' + aElement.css_inline + '</style>'));
}
if(aElement.js_inline !== undefined){
oRenderersJs_{$this->oField->GetGlobalId()} += aElement.js_inline;
}
}
},
"initComplete": function(){
// Execute inline js provided by attributes renderers
eval(oRenderersJs_{$this->oField->GetGlobalId()});
},
});
// Handles items selection/deselection
@@ -326,21 +362,46 @@ JS
// Attaching JS widget
$sObjectInformationsUrl = $this->oField->GetInformationEndpoint();
$oOutput->AddJs(
<<<EOF
<<<JS
$("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field({
'validators': {$this->GetValidatorsAsJson()},
'get_current_value_callback': function(me, oEvent, oData){
var value = null;
// Retrieving JSON value as a string and not an object
//
// Note : The value is passed as a string instead of an array because the attribute would not be included in the posted data when empty.
// Which was an issue when deleting all objects from linkedset
//
// Old code : value = JSON.parse(me.element.find('#{$this->oField->GetGlobalId()}').val());
value = me.element.find('#{$this->oField->GetGlobalId()}').val();
return value;
// Read linked set value as array
var aValue = JSON.parse(me.element.find('#{$this->oField->GetGlobalId()}').val());
// Iterate throw table rows and extract link attributes input values...
$('tbody tr', me.element).each(function(){
// Extract link id
const sId = $(this).attr('id');
// Security
if(sId !== undefined){
// Prepare link attributes values
const aValues = {};
// Extract inputs values...
$('input,select,textarea', $(this)).each(function(){
if($(this).attr('id') !== undefined){
aValues[$(this).attr('name')] = $(this).val();
}
});
// Set values
if(aValue.current !== undefined && aValue.current[sId] !== undefined){
aValue.current[sId] = aValues;
}
const iAddId = -parseInt(sId);
if(aValue.add !== undefined && aValue.add[iAddId] !== undefined){
aValue.add[iAddId] = aValues;
}
}
});
return JSON.stringify(aValue);
},
'set_current_value_callback': function(me, oEvent, oData){
// When we have data (meaning that we picked objects from search)
@@ -351,16 +412,19 @@ JS
// Retrieving new rows ids
var aObjectIds = Object.keys(oData.values);
// Retrieving rows informations so we can add them
$.post(
'{$sObjectInformationsUrl}',
{
sObjectClass: '{$this->oField->GetTargetClass()}',
sLinkClass: '{$this->oField->GetLinkedClass()}',
aObjectIds: aObjectIds,
aObjectAttCodes: $sAttCodesToDisplayAsJson
aObjectAttCodes: $sAttCodesToDisplayAsJson,
aLinkAttCodes: $sLnkAttCodesToDisplayAsJson,
},
function(oData){
// Updating datatables
if(oData.items !== undefined)
{
@@ -376,11 +440,15 @@ JS
oData.items[i].id = -1 * parseInt(oData.items[i].id);
oTable_{$this->oField->GetGlobalId()}.row.add(oData.items[i]);
}
}
oTable_{$this->oField->GetGlobalId()}.draw();
// Execute inline js for each attributes renderers
for(key in oData.items[i].attributes){
eval(oData.items[i].attributes[key].js_inline)
}
// Updating input
updateInputValue_{$this->oField->GetGlobalId()}();
}
@@ -409,7 +477,7 @@ JS
}
}
});
EOF
JS
);
// Rendering table
@@ -455,7 +523,7 @@ EOF
{
// Retrieving table rows
var aData = oTable_{$this->oField->GetGlobalId()}.rows().data().toArray();
// Retrieving input values
var oValues = JSON.parse($('#{$this->oField->GetGlobalId()}').val());
oValues.add = {};
@@ -553,98 +621,206 @@ JS
* @throws \Exception
* @throws \CoreException
*/
protected function PrepareItems(&$aItems, &$aItemIds)
protected function PrepareItems(&$aItems, &$aItemIds, $oOutput, &$aAddedItemIds, &$aAddedTargetIds)
{
/** @var \ormLinkSet $oValueSet */
$oValueSet = $this->oField->GetCurrentValue();
$oValueSet->OptimizeColumnLoad(array($this->oField->GetTargetClass() => $this->oField->GetAttributesToDisplay(true)));
while ($oItem = $oValueSet->Fetch())
{
while ($oItem = $oValueSet->Fetch()) {
// In case of indirect linked set, we must retrieve the remote object
if ($this->oField->IsIndirect())
{
try{
// Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated
$oRemoteItem = MetaModel::GetObject($this->oField->GetTargetClass(), $oItem->Get($this->oField->GetExtKeyToRemote()), true, true);
}
catch(Exception $e)
{
// In some cases we can't retrieve an object from a linkedset, eg. when the extkey to remote is 0 due to a database corruption.
// Rather than crashing we rather just skip the object like in the administration console
IssueLog::Error('Could not retrieve object of linkedset in form #'.$this->oField->GetFormPath().' for field #'.$this->oField->GetId().'. Message: '.$e->getMessage());
continue;
}
}
else
{
if ($this->oField->IsIndirect()) {
try {
// Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated
$oRemoteItem = MetaModel::GetObject($this->oField->GetTargetClass(), $oItem->Get($this->oField->GetExtKeyToRemote()), true, true);
}
catch (Exception $e) {
// In some cases we can't retrieve an object from a linkedset, eg. when the extkey to remote is 0 due to a database corruption.
// Rather than crashing we rather just skip the object like in the administration console
IssueLog::Error('Could not retrieve object of linkedset in form #'.$this->oField->GetFormPath().' for field #'.$this->oField->GetId().'. Message: '.$e->getMessage());
continue;
}
} else {
$oRemoteItem = $oItem;
}
// Skip item if not supposed to be displayed
$bLimitedAccessItem = $this->oField->IsLimitedAccessItem($oRemoteItem->GetKey());
if ($bLimitedAccessItem && !$this->oField->GetDisplayLimitedAccessItems())
{
if ($bLimitedAccessItem && !$this->oField->GetDisplayLimitedAccessItems()) {
continue;
}
$aItemProperties = array(
'id' => ($this->oField->IsIndirect() && $oItem->IsNew()) ? -1*$oRemoteItem->GetKey() : $oItem->GetKey(),
'target_id' => $oRemoteItem->GetKey(),
'name' => $oItem->GetName(),
'attributes' => array(),
'id' => ($this->oField->IsIndirect() && $oItem->IsNew()) ? -1 * $oRemoteItem->GetKey() : $oItem->GetKey(),
'target_id' => $oRemoteItem->GetKey(),
'name' => $oItem->GetName(),
'attributes' => array(),
'limited_access' => $bLimitedAccessItem,
'disabled' => true,
'active' => false,
'inactive' => true,
'disabled' => true,
'active' => false,
'inactive' => true,
'not-selectable' => true,
);
);
// Target object others attributes
// TODO: Support for AttributeImage, AttributeBlob
foreach ($this->oField->GetAttributesToDisplay(true) as $sAttCode)
{
if ($sAttCode !== 'id')
{
$aAttProperties = array(
'att_code' => $sAttCode
);
// Link attributes to display
$this->PrepareItem($oItem, $this->oField->GetLinkedClass(), $this->oField->GetLnkAttributesToDisplay(true), true, $aItemProperties, $oOutput);
$oAttDef = MetaModel::GetAttributeDef($this->oField->GetTargetClass(), $sAttCode);
if ($oAttDef->IsExternalKey())
{
/** @var \AttributeExternalKey $oAttDef */
$aAttProperties['value'] = $oRemoteItem->Get($sAttCode . '_friendlyname');
// Remote attributes to display
$this->PrepareItem($oRemoteItem, $this->oField->GetTargetClass(), $this->oField->GetAttributesToDisplay(true), false, $aItemProperties, $oOutput);
// Checking if user can access object's external key
$sObjectUrl = ApplicationContext::MakeObjectUrl($oAttDef->GetTargetClass(), $oRemoteItem->Get($sAttCode));
if(!empty($sObjectUrl))
{
$aAttProperties['url'] = $sObjectUrl;
}
}
else
{
$aAttProperties['value'] = $oAttDef->GetAsHTML($oRemoteItem->Get($sAttCode));
if ($oAttDef instanceof AttributeFriendlyName)
{
// Checking if user can access object
$sObjectUrl = ApplicationContext::MakeObjectUrl(get_class($oRemoteItem), $oRemoteItem->GetKey());
if(!empty($sObjectUrl))
{
$aAttProperties['url'] = $sObjectUrl;
}
}
}
$aItemProperties['attributes'][$sAttCode] = $aAttProperties;
}
}
// Remap objects to avoid added item to be considered as current item when form validation isn't valid
// and form reconstruct
$aItems[] = $aItemProperties;
$aItemIds[$aItemProperties['id']] = array();
if ($oItem->IsNew()) {
$aAddedItemIds[-1 * $aItemProperties['id']] = array();
$aAddedTargetIds[] = $oRemoteItem->GetKey();
} else {
$aItemIds[$aItemProperties['id']] = array();
}
}
$oValueSet->rewind();
}
/**
* @param string $sClass
* @param array $aAttributesCodesToDisplay
* @param $oOutput
*
* @return void
* @throws \CoreException
*/
protected function InjectRendererFileAssets(string $sClass, array $aAttributesCodesToDisplay, $oOutput)
{
$oItem = MetaModel::NewObject($sClass);
// Iterate throw attributes...
foreach ($aAttributesCodesToDisplay as $sAttCode) {
// Retrieve attribute definition
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
$oField = $oAttDef->MakeFormField($oItem);
$sFieldRendererClass = static::GetFieldRendererClass($oField);
if ($sFieldRendererClass !== null) {
/** @var FieldRenderer $oFieldRenderer */
$oFieldRenderer = new $sFieldRendererClass($oField);
$oFieldOutput = $oFieldRenderer->Render();
static::TransferFieldRendererGlobalOutput($oFieldOutput, $oOutput);
}
}
}
/**
* @param \DBObject $oItem
* @param string $sClass
* @param array $aAttributesCodesToDisplay
* @param bool $bIsEditable
* @param array $aItemProperties
* @param $oOutput
*
* @return void
* @throws \ArchivedObjectException
* @throws \CoreException
*/
protected function PrepareItem(DBObject $oItem, string $sClass, array $aAttributesCodesToDisplay, bool $bIsEditable, array &$aItemProperties, $oOutput)
{
// Iterate throw attributes...
foreach ($aAttributesCodesToDisplay as $sAttCode) {
if ($sAttCode !== 'id') {
// Prepare attribute properties
$aAttProperties = array(
'att_code' => $sAttCode,
);
// Retrieve attribute definition
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
// External key specific
if ($bIsEditable) {
$oField = $oAttDef->MakeFormField($oItem);
$sFieldRendererClass = static::GetFieldRendererClass($oField);
if ($sFieldRendererClass !== null) {
/** @var FieldRenderer $oFieldRenderer */
$oFieldRenderer = new $sFieldRendererClass($oField);
$oFieldOutput = $oFieldRenderer->Render();
$aAttProperties['js_inline'] = $oFieldOutput->GetJs();
$aAttProperties['css_inline'] = $oFieldOutput->GetCss();
$aAttProperties['value'] = $oFieldOutput->GetHtml();
}
} else if ($oAttDef->IsExternalKey()) {
/** @var \AttributeExternalKey $oAttDef */
$aAttProperties['value'] = $oItem->Get($sAttCode.'_friendlyname');
// Checking if user can access object's external key
$sObjectUrl = ApplicationContext::MakeObjectUrl($sClass, $oItem->Get($sAttCode));
if (!empty($sObjectUrl)) {
$aAttProperties['url'] = $sObjectUrl;
}
} else { // Others attributes
$aAttProperties['value'] = $oAttDef->GetAsHTML($oItem->Get($sAttCode));
if ($oAttDef instanceof AttributeFriendlyName) {
// Checking if user can access object
$sObjectUrl = ApplicationContext::MakeObjectUrl($sClass, $oItem->GetKey());
if (!empty($sObjectUrl)) {
$aAttProperties['url'] = $sObjectUrl;
}
}
}
$aItemProperties['attributes'][$sAttCode] = $aAttProperties;
}
}
}
/**
* Transfer field renderer output to page output.
*
* @param \Combodo\iTop\Renderer\RenderingOutput $oFieldOutput
* @param \Combodo\iTop\Renderer\RenderingOutput $oPageOutput
*
* @return void
*/
public static function TransferFieldRendererGlobalOutput(RenderingOutput $oFieldOutput, RenderingOutput $oPageOutput)
{
foreach ($oFieldOutput->GetJsFiles() as $sJsFile) {
$oPageOutput->AddJsFile($sJsFile);
}
foreach ($oFieldOutput->GetCssFiles() as $sCssFile) {
$oPageOutput->AddCssFile($sCssFile);
}
}
/**
* Retrieve a field renderer class.
*
* @param \Combodo\iTop\Form\Field\Field $oField
*
* @return string|null
*/
public static function GetFieldRendererClass(Field $oField): ?string
{
$aRegisteredFields = BsFieldRendererMappings::RegisterSupportedFields();
$sFieldClass = get_class($oField);
foreach ($aRegisteredFields as $aRegisteredField) {
if ($aRegisteredField['field'] === $sFieldClass) {
return $aRegisteredField['field_renderer'];
}
}
return null;
}
}