Procházet zdrojové kódy

- Added support for ExternalKey, LinkedSet, linkedSetIndirect, CaseLog to the new portal
- Fixed some bugs on the customfields integration with he portal

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@4003 a333f486-631f-4898-b8df-5754b55c2be0

glajarige před 9 roky
rodič
revize
5338741a6a

+ 69 - 6
core/attributedef.class.inc.php

@@ -1262,7 +1262,56 @@ class AttributeLinkedSet extends AttributeDefinition
 		$oRemoteAtt = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToMe());
 		$oRemoteAtt = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToMe());
 		return $oRemoteAtt;
 		return $oRemoteAtt;
 	}
 	}
-	
+
+	static public function GetFormFieldClass()
+	{
+		return '\\Combodo\\iTop\\Form\\Field\\LinkedSetField';
+	}
+
+	public function MakeFormField(DBObject $oObject, $oFormField = null)
+	{
+		if ($oFormField === null)
+		{
+			$sFormFieldClass = static::GetFormFieldClass();
+			$oFormField = new $sFormFieldClass($this->GetCode());
+		}
+		
+		// Setting target class
+		if (!$this->IsIndirect())
+		{
+			$sTargetClass = $this->GetLinkedClass();
+		}
+		else
+		{
+			$oRemoteAttDef = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote());
+			$sTargetClass = $oRemoteAttDef->GetTargetClass();
+
+			$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 (!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);
+		
+		parent::MakeFormField($oObject, $oFormField);
+		
+		return $oFormField;
+	}
+
 	public function IsPartOfFingerprint() { return false; }
 	public function IsPartOfFingerprint() { return false; }
 }
 }
 
 
@@ -2919,16 +2968,22 @@ class AttributeCaseLog extends AttributeLongText
 		return $this->GetOptional('format', 'html'); // default format for case logs is now HTML
 		return $this->GetOptional('format', 'html'); // default format for case logs is now HTML
 	}
 	}
 
 
+	static public function GetFormFieldClass()
+	{
+		return '\\Combodo\\iTop\\Form\\Field\\CaseLogField';
+	}
+
 	public function MakeFormField(DBObject $oObject, $oFormField = null)
 	public function MakeFormField(DBObject $oObject, $oFormField = null)
 	{
 	{
 		// First we call the parent so the field is build
 		// First we call the parent so the field is build
 		$oFormField = parent::MakeFormField($oObject, $oFormField);
 		$oFormField = parent::MakeFormField($oObject, $oFormField);
 		// Then only we set the value
 		// Then only we set the value
 		$oFormField->SetCurrentValue($this->GetEditValue($oObject->Get($this->GetCode())));
 		$oFormField->SetCurrentValue($this->GetEditValue($oObject->Get($this->GetCode())));
+		// And we set the entries
+		$oFormField->SetEntries($oObject->Get($this->GetCode())->GetAsArray());
 
 
 		return $oFormField;
 		return $oFormField;
 	}
 	}
-
 }
 }
 
 
 /**
 /**
@@ -4023,7 +4078,7 @@ class AttributeExternalKey extends AttributeDBFieldVoid
 
 
 	static public function GetFormFieldClass()
 	static public function GetFormFieldClass()
 	{
 	{
-		return '\\Combodo\\iTop\\Form\\Field\\SelectField';
+		return '\\Combodo\\iTop\\Form\\Field\\SelectObjectField';
 	}
 	}
 
 
 	public function MakeFormField(DBObject $oObject, $oFormField = null)
 	public function MakeFormField(DBObject $oObject, $oFormField = null)
@@ -4034,7 +4089,11 @@ class AttributeExternalKey extends AttributeDBFieldVoid
 			$sFormFieldClass = static::GetFormFieldClass();
 			$sFormFieldClass = static::GetFormFieldClass();
 			$oFormField = new $sFormFieldClass($this->GetCode());
 			$oFormField = new $sFormFieldClass($this->GetCode());
 		}
 		}
-
+		
+		// Setting params
+		$oFormField->SetMaximumComboLength($this->GetMaximumComboLength());
+		$oFormField->SetMinAutoCompleteChars($this->GetMinAutoCompleteChars());
+		$oFormField->SetHierarchical(MetaModel::IsHierarchicalClass($this->GetTargetClass()));
 		// Setting choices regarding the field dependencies
 		// Setting choices regarding the field dependencies
 		$aFieldDependencies = $this->GetPrerequisiteAttributes();
 		$aFieldDependencies = $this->GetPrerequisiteAttributes();
 		if (!empty($aFieldDependencies))
 		if (!empty($aFieldDependencies))
@@ -4043,12 +4102,16 @@ class AttributeExternalKey extends AttributeDBFieldVoid
 			$oTmpField = $oFormField;
 			$oTmpField = $oFormField;
 			$oFormField->SetOnFinalizeCallback(function() use ($oTmpField, $oTmpAttDef, $oObject)
 			$oFormField->SetOnFinalizeCallback(function() use ($oTmpField, $oTmpAttDef, $oObject)
 			{
 			{
-				$oTmpField->SetChoices($oTmpAttDef->GetAllowedValues($oObject->ToArgsForQuery()));
+				$oSearch = DBSearch::FromOQL($oTmpAttDef->GetValuesDef()->GetFilterExpression());
+				$oSearch->SetInternalParams(array('this' => $oObject));
+				$oTmpField->SetSearch($oSearch);
 			});
 			});
 		}
 		}
 		else
 		else
 		{
 		{
-			$oFormField->SetChoices($this->GetAllowedValues($oObject->ToArgsForQuery()));
+			$oSearch = DBSearch::FromOQL($this->GetValuesDef()->GetFilterExpression());
+			$oSearch->SetInternalParams(array('this' => $oObject));
+			$oFormField->SetSearch($oSearch);
 		}
 		}
 
 
 		// If ExtKey is mandatory, we add a validator to ensure that the value 0 is not selected
 		// If ExtKey is mandatory, we add a validator to ensure that the value 0 is not selected

+ 8 - 3
js/form_field.js

@@ -13,7 +13,7 @@ $(function()
 			validate_callback: 'validate',				  // When using an anonymous function, use the 'me' parameter to acces the current widget : function(me){ return me.validate(); },
 			validate_callback: 'validate',				  // When using an anonymous function, use the 'me' parameter to acces the current widget : function(me){ return me.validate(); },
 			on_validation_callback: function(data){  },
 			on_validation_callback: function(data){  },
 			get_current_value_callback: 'getCurrentValue',
 			get_current_value_callback: 'getCurrentValue',
-			
+			set_current_value_callback: function(me, oEvent, oData){ console.log('Form field: set_current_value_callback must be overloaded, this is the default callback.'); }	
 		},
 		},
    
    
 		// the constructor
 		// the constructor
@@ -30,7 +30,7 @@ $(function()
 				me.options.validators = oData;
 				me.options.validators = oData;
 			});
 			});
 			this.element
 			this.element
-			.bind('validate get_current_value', function(oEvent, oData){
+			.bind('validate get_current_value set_current_value', function(oEvent, oData){
 				oEvent.stopPropagation();
 				oEvent.stopPropagation();
 		
 		
 				var callback = me.options[oEvent.type+'_callback'];
 				var callback = me.options[oEvent.type+'_callback'];
@@ -124,7 +124,12 @@ $(function()
 			{
 			{
 				var bMandatory = (this.options.validators.mandatory !== undefined);
 				var bMandatory = (this.options.validators.mandatory !== undefined);
 				// Extracting value for the field
 				// Extracting value for the field
-				var oValue = this.getCurrentValue();
+				var oValue = this.element.triggerHandler('get_current_value');
+				if(oValue === null)
+				{
+					console.log('Form field : Warning, there was no value for "'+this.element.attr('data-field-id')+'"');
+					return oResult;
+				}
 				var aValueKeys = Object.keys(oValue);
 				var aValueKeys = Object.keys(oValue);
 				
 				
 				// This is just a safety check in case a field doesn't always return an object when no value assigned, so we have to check the mandatory validator here...
 				// This is just a safety check in case a field doesn't always return an object when no value assigned, so we have to check the mandatory validator here...

+ 4 - 0
sources/autoload.php

@@ -32,12 +32,14 @@ require_once APPROOT . 'sources/form/field/datefield.class.inc.php';
 require_once APPROOT . 'sources/form/field/datetimefield.class.inc.php';
 require_once APPROOT . 'sources/form/field/datetimefield.class.inc.php';
 require_once APPROOT . 'sources/form/field/durationfield.class.inc.php';
 require_once APPROOT . 'sources/form/field/durationfield.class.inc.php';
 require_once APPROOT . 'sources/form/field/textareafield.class.inc.php';
 require_once APPROOT . 'sources/form/field/textareafield.class.inc.php';
+require_once APPROOT . 'sources/form/field/caselogfield.class.inc.php';
 require_once APPROOT . 'sources/form/field/multiplechoicesfield.class.inc.php';
 require_once APPROOT . 'sources/form/field/multiplechoicesfield.class.inc.php';
 require_once APPROOT . 'sources/form/field/selectfield.class.inc.php';
 require_once APPROOT . 'sources/form/field/selectfield.class.inc.php';
 require_once APPROOT . 'sources/form/field/multipleselectfield.class.inc.php';
 require_once APPROOT . 'sources/form/field/multipleselectfield.class.inc.php';
 require_once APPROOT . 'sources/form/field/selectobjectfield.class.inc.php';
 require_once APPROOT . 'sources/form/field/selectobjectfield.class.inc.php';
 require_once APPROOT . 'sources/form/field/checkboxfield.class.inc.php';
 require_once APPROOT . 'sources/form/field/checkboxfield.class.inc.php';
 require_once APPROOT . 'sources/form/field/radiofield.class.inc.php';
 require_once APPROOT . 'sources/form/field/radiofield.class.inc.php';
+require_once APPROOT . 'sources/form/field/linkedsetfield.class.inc.php';
 require_once APPROOT . 'sources/form/validator/validator.class.inc.php';
 require_once APPROOT . 'sources/form/validator/validator.class.inc.php';
 require_once APPROOT . 'sources/form/validator/mandatoryvalidator.class.inc.php';
 require_once APPROOT . 'sources/form/validator/mandatoryvalidator.class.inc.php';
 require_once APPROOT . 'sources/form/validator/integervalidator.class.inc.php';
 require_once APPROOT . 'sources/form/validator/integervalidator.class.inc.php';
@@ -47,4 +49,6 @@ require_once APPROOT . 'sources/renderer/fieldrenderer.class.inc.php';
 require_once APPROOT . 'sources/renderer/renderingoutput.class.inc.php';
 require_once APPROOT . 'sources/renderer/renderingoutput.class.inc.php';
 require_once APPROOT . 'sources/renderer/bootstrap/bsformrenderer.class.inc.php';
 require_once APPROOT . 'sources/renderer/bootstrap/bsformrenderer.class.inc.php';
 require_once APPROOT . 'sources/renderer/bootstrap/fieldrenderer/bssimplefieldrenderer.class.inc.php';
 require_once APPROOT . 'sources/renderer/bootstrap/fieldrenderer/bssimplefieldrenderer.class.inc.php';
+require_once APPROOT . 'sources/renderer/bootstrap/fieldrenderer/bsselectobjectfieldrenderer.class.inc.php';
+require_once APPROOT . 'sources/renderer/bootstrap/fieldrenderer/bslinkedsetfieldrenderer.class.inc.php';
 require_once APPROOT . 'sources/renderer/bootstrap/fieldrenderer/bssubformfieldrenderer.class.inc.php';
 require_once APPROOT . 'sources/renderer/bootstrap/fieldrenderer/bssubformfieldrenderer.class.inc.php';

+ 55 - 0
sources/form/field/caselogfield.class.inc.php

@@ -0,0 +1,55 @@
+<?php
+
+// Copyright (C) 2010-2016 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify	
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+namespace Combodo\iTop\Form\Field;
+
+use \Closure;
+use \DBObject;
+use \Combodo\iTop\Form\Field\TextAreaField;
+
+/**
+ * Description of CaseLogField
+ *
+ * @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
+ */
+class CaseLogField extends TextAreaField
+{
+	protected $aEntries;
+
+	/**
+	 *
+	 * @return array
+	 */
+	public function GetEntries()
+	{
+		return $this->aEntries;
+	}
+
+	/**
+	 *
+	 * @param array $aEntries
+	 * @return \Combodo\iTop\Form\Field\TextAreaField
+	 */
+	public function SetEntries($aEntries)
+	{
+		$this->aEntries = $aEntries;
+		return $this;
+	}
+
+}

+ 10 - 0
sources/form/field/field.class.inc.php

@@ -358,6 +358,16 @@ abstract class Field
 		return $this;
 		return $this;
 	}
 	}
 
 
+	/**
+	 * Returns if the field is editable. Meaning that it is not editable nor hidden.
+	 * 
+	 * @return boolean
+	 */
+	public function IsEditable()
+	{
+		return (!$this->bReadOnly && !$this->bHidden);
+	}
+
 	public function OnCancel()
 	public function OnCancel()
 	{
 	{
 		// Overload when needed
 		// Overload when needed

+ 154 - 0
sources/form/field/linkedsetfield.class.inc.php

@@ -0,0 +1,154 @@
+<?php
+
+// Copyright (C) 2010-2016 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify	
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+namespace Combodo\iTop\Form\Field;
+
+use \Combodo\iTop\Form\Field\Field;
+
+/**
+ * Description of LinkedSetField
+ *
+ * @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
+ */
+class LinkedSetField extends Field
+{
+	protected $sTargetClass;
+	protected $sExtKeyToRemote;
+	protected $bIndirect;
+	protected $aAttributesToDisplay;
+	protected $sSearchEndpoint;
+	protected $sInformationEndpoint;
+
+	public function __construct($sId, \Closure $onFinalizeCallback = null)
+	{
+		$this->sTargetClass = null;
+		$this->sExtKeyToRemote = null;
+		$this->bIndirect = false;
+		$this->aAttributesToDisplay = array();
+		$this->sSearchEndpoint = null;
+		$this->sInformationEndpoint = null;
+
+		parent::__construct($sId, $onFinalizeCallback);
+	}
+
+	/**
+	 *
+	 * @return string
+	 */
+	public function GetTargetClass()
+	{
+		return $this->sTargetClass;
+	}
+
+	/**
+	 *
+	 * @param string $sTargetClass
+	 * @return \Combodo\iTop\Form\Field\LinkedSetField
+	 */
+	public function SetTargetClass($sTargetClass)
+	{
+		$this->sTargetClass = $sTargetClass;
+		return $sTargetClass;
+	}
+
+	/**
+	 *
+	 * @return string
+	 */
+	public function GetExtKeyToRemote()
+	{
+		return $this->sExtKeyToRemote;
+	}
+
+	/**
+	 *
+	 * @param string $sExtKeyToRemote
+	 * @return \Combodo\iTop\Form\Field\LinkedSetField
+	 */
+	public function SetExtKeyToRemote($sExtKeyToRemote)
+	{
+		$this->sExtKeyToRemote = $sExtKeyToRemote;
+		return $sExtKeyToRemote;
+	}
+
+	/**
+	 *
+	 * @return boolean
+	 */
+	public function IsIndirect()
+	{
+		return $this->bIndirect;
+	}
+
+	/**
+	 *
+	 * @param boolean $bIndirect
+	 * @return \Combodo\iTop\Form\Field\LinkedSetField
+	 */
+	public function SetIndirect($bIndirect)
+	{
+		$this->bIndirect = $bIndirect;
+		return $this;
+	}
+
+	/**
+	 * Returns a hash array of attributes to be displayed in the linkedset in the form $sAttCode => $sAttLabel
+	 *
+	 * @param $bAttCodesOnly If set to true, will return only the attcodes
+	 * @return array
+	 */
+	public function GetAttributesToDisplay($bAttCodesOnly = false)
+	{
+		return ($bAttCodesOnly) ? array_keys($this->aAttributesToDisplay) : $this->aAttributesToDisplay;
+	}
+
+	/**
+	 *
+	 * @param array $aAttCodes
+	 * @return \Combodo\iTop\Form\Field\LinkedSetField
+	 */
+	public function SetAttributesToDisplay(array $aAttributesToDisplay)
+	{
+		$this->aAttributesToDisplay = $aAttributesToDisplay;
+		return $this;
+	}
+
+	public function GetSearchEndpoint()
+	{
+		return $this->sSearchEndpoint;
+	}
+
+	public function SetSearchEndpoint($sSearchEndpoint)
+	{
+		$this->sSearchEndpoint = $sSearchEndpoint;
+		return $this;
+	}
+
+	public function GetInformationEndpoint()
+	{
+		return $this->sInformationEndpoint;
+	}
+
+	public function SetInformationEndpoint($sInformationEndpoint)
+	{
+		$this->sInformationEndpoint = $sInformationEndpoint;
+		return $this;
+	}
+
+}

+ 30 - 1
sources/form/field/selectobjectfield.class.inc.php

@@ -26,14 +26,16 @@ use Combodo\iTop\Form\Validator\NotEmptyExtKeyValidator;
 /**
 /**
  * Description of SelectObjectField
  * Description of SelectObjectField
  *
  *
+ * @author Romain Quetiez <romain.quetiez@combodo.com>
  */
  */
 class SelectObjectField extends Field
 class SelectObjectField extends Field
 {
 {
 	protected $oSearch;
 	protected $oSearch;
 	protected $iMaximumComboLength;
 	protected $iMaximumComboLength;
 	protected $iMinAutoCompleteChars;
 	protected $iMinAutoCompleteChars;
-
+	protected $bHierarchical;
 	protected $iControlType;
 	protected $iControlType;
+	protected $sSearchEndpoint;
 
 
 	const CONTROL_SELECT = 1;
 	const CONTROL_SELECT = 1;
 	const CONTROL_RADIO_VERTICAL = 2;
 	const CONTROL_RADIO_VERTICAL = 2;
@@ -44,22 +46,33 @@ class SelectObjectField extends Field
 		$this->oSearch = null;
 		$this->oSearch = null;
 		$this->iMaximumComboLength = null;
 		$this->iMaximumComboLength = null;
 		$this->iMinAutoCompleteChars = 3;
 		$this->iMinAutoCompleteChars = 3;
+		$this->bHierarchical = false;
 		$this->iControlType = self::CONTROL_SELECT;
 		$this->iControlType = self::CONTROL_SELECT;
+		$this->sSearchEndpoint = null;
 	}
 	}
 
 
 	public function SetSearch(DBSearch $oSearch)
 	public function SetSearch(DBSearch $oSearch)
 	{
 	{
 		$this->oSearch = $oSearch;
 		$this->oSearch = $oSearch;
+		return $this;
 	}
 	}
 
 
 	public function SetMaximumComboLength($iMaximumComboLength)
 	public function SetMaximumComboLength($iMaximumComboLength)
 	{
 	{
 		$this->iMaximumComboLength = $iMaximumComboLength;
 		$this->iMaximumComboLength = $iMaximumComboLength;
+		return $this;
 	}
 	}
 
 
 	public function SetMinAutoCompleteChars($iMinAutoCompleteChars)
 	public function SetMinAutoCompleteChars($iMinAutoCompleteChars)
 	{
 	{
 		$this->iMinAutoCompleteChars = $iMinAutoCompleteChars;
 		$this->iMinAutoCompleteChars = $iMinAutoCompleteChars;
+		return $this;
+	}
+
+	public function SetHierarchical($bHierarchical)
+	{
+		$this->bHierarchical = $bHierarchical;
+		return $this;
 	}
 	}
 
 
 	public function SetControlType($iControlType)
 	public function SetControlType($iControlType)
@@ -67,6 +80,12 @@ class SelectObjectField extends Field
 		$this->iControlType = $iControlType;
 		$this->iControlType = $iControlType;
 	}
 	}
 
 
+	public function SetSearchEndpoint($sSearchEndpoint)
+	{
+		$this->sSearchEndpoint = $sSearchEndpoint;
+		return $this;
+	}
+
 	/**
 	/**
 	 * Sets if the field is mandatory or not.
 	 * Sets if the field is mandatory or not.
 	 * Setting the value will automatically add/remove a MandatoryValidator to the Field
 	 * Setting the value will automatically add/remove a MandatoryValidator to the Field
@@ -112,8 +131,18 @@ class SelectObjectField extends Field
 		return $this->iMinAutoCompleteChars;
 		return $this->iMinAutoCompleteChars;
 	}
 	}
 
 
+	public function GetHierarchical()
+	{
+		return $this->bHierarchical;
+	}
+
 	public function GetControlType()
 	public function GetControlType()
 	{
 	{
 		return $this->iControlType;
 		return $this->iControlType;
 	}
 	}
+
+	public function GetSearchEndpoint()
+	{
+		return $this->sSearchEndpoint;
+	}
 }
 }

+ 43 - 0
sources/form/field/subformfield.class.inc.php

@@ -119,6 +119,49 @@ class SubFormField extends Field
 	}
 	}
 
 
 	/**
 	/**
+	 * Sets the mandatory flag on all the fields on the form
+	 *
+	 * @param boolean $bMandatory
+	 */
+	public function SetMandatory($bMandatory)
+	{
+		foreach ($this->oForm->GetFields() as $oField)
+		{
+			$oField->SetMandatory($bMandatory);
+		}
+		parent::SetMandatory($bMandatory);
+	}
+
+	/**
+	 * Sets the read-only flag on all the fields on the form
+	 *
+	 * @param boolean $bReadOnly
+	 */
+	public function SetReadOnly($bReadOnly)
+	{
+		foreach ($this->oForm->GetFields() as $oField)
+		{
+			$oField->SetReadOnly($bReadOnly);
+			$oField->SetMandatory(false);
+		}
+		parent::SetReadOnly($bReadOnly);
+	}
+
+	/**
+	 * Sets the hidden flag on all the fields on the form
+	 *
+	 * @param boolean $bHidden
+	 */
+	public function SetHidden($bHidden)
+	{
+		foreach ($this->oForm->GetFields() as $oField)
+		{
+			$oField->SetHidden($bHidden);
+		}
+		parent::SetHidden($bHidden);
+	}
+
+	/**
 	 * @param $sFormPath
 	 * @param $sFormPath
 	 * @return Form|null
 	 * @return Form|null
 	 */
 	 */

+ 40 - 1
sources/form/form.class.inc.php

@@ -37,6 +37,7 @@ class Form
 	protected $aDependencies;
 	protected $aDependencies;
 	protected $bValid;
 	protected $bValid;
 	protected $aErrorMessages;
 	protected $aErrorMessages;
+	protected $iEditableFieldCount;
 
 
 	/**
 	/**
 	 * Default constructor
 	 * Default constructor
@@ -51,6 +52,7 @@ class Form
 		$this->aDependencies = array();
 		$this->aDependencies = array();
 		$this->bValid = true;
 		$this->bValid = true;
 		$this->aErrorMessages = array();
 		$this->aErrorMessages = array();
+		$this->iEditableFieldCount = null;
 	}
 	}
 
 
 	/**
 	/**
@@ -372,6 +374,39 @@ class Form
     }
     }
 
 
 	/**
 	/**
+	 * Returns the number of editable fields in this form.
+	 *
+	 * @return integer
+	 */
+	public function GetEditableFieldCount($bForce = false)
+	{
+		// Count is usally done by the Finalize function but it can be done there if Finalize hasn't been called yet or if we choose to force it.
+		if (($this->iEditableFieldCount === null) || ($bForce === true))
+		{
+			$this->iEditableFieldCount = 0;
+			foreach ($this->aFields as $oField)
+			{
+				if ($oField->IsEditable())
+				{
+					$this->iEditableFieldCount++;
+				}
+			}
+		}
+
+		return $this->iEditableFieldCount;
+	}
+
+	/**
+	 * Returns true if the form has at least one editable field
+	 *
+	 * @return boolean
+	 */
+	public function HasEditableFields()
+	{
+		return ($this->GetEditableFieldCount() > 0);
+	}
+
+	/**
 	 * @param $sFormPath
 	 * @param $sFormPath
 	 * @return Form|null
 	 * @return Form|null
 	 */
 	 */
@@ -450,7 +485,11 @@ class Form
 		foreach ($aFieldList as $sId => $oField)
 		foreach ($aFieldList as $sId => $oField)
 		{
 		{
 			$oField->OnFinalize();
 			$oField->OnFinalize();
-        }
+			if ($oField->IsEditable())
+			{
+				$this->iEditableFieldCount++;
+			}
+		}
     }
     }
 
 
 	/**
 	/**

+ 4 - 1
sources/renderer/bootstrap/bsformrenderer.class.inc.php

@@ -19,6 +19,7 @@
 
 
 namespace Combodo\iTop\Renderer\Bootstrap;
 namespace Combodo\iTop\Renderer\Bootstrap;
 
 
+use \Silex\Application;
 use \Combodo\iTop\Renderer\FormRenderer;
 use \Combodo\iTop\Renderer\FormRenderer;
 use \Combodo\iTop\Form\Form;
 use \Combodo\iTop\Form\Form;
 
 
@@ -43,12 +44,14 @@ class BsFormRenderer extends FormRenderer
 		$this->AddSupportedField('LabelField', 'BsSimpleFieldRenderer');
 		$this->AddSupportedField('LabelField', 'BsSimpleFieldRenderer');
 		$this->AddSupportedField('StringField', 'BsSimpleFieldRenderer');
 		$this->AddSupportedField('StringField', 'BsSimpleFieldRenderer');
 		$this->AddSupportedField('TextAreaField', 'BsSimpleFieldRenderer');
 		$this->AddSupportedField('TextAreaField', 'BsSimpleFieldRenderer');
+		$this->AddSupportedField('CaseLogField', 'BsSimpleFieldRenderer');
 		$this->AddSupportedField('SelectField', 'BsSimpleFieldRenderer');
 		$this->AddSupportedField('SelectField', 'BsSimpleFieldRenderer');
 		$this->AddSupportedField('MultipleSelectField', 'BsSimpleFieldRenderer');
 		$this->AddSupportedField('MultipleSelectField', 'BsSimpleFieldRenderer');
 		$this->AddSupportedField('RadioField', 'BsSimpleFieldRenderer');
 		$this->AddSupportedField('RadioField', 'BsSimpleFieldRenderer');
 		$this->AddSupportedField('CheckboxField', 'BsSimpleFieldRenderer');
 		$this->AddSupportedField('CheckboxField', 'BsSimpleFieldRenderer');
 		$this->AddSupportedField('SubFormField', 'BsSubFormFieldRenderer');
 		$this->AddSupportedField('SubFormField', 'BsSubFormFieldRenderer');
-		$this->AddSupportedField('SelectObjectField', 'BsSimpleFieldRenderer');
+		$this->AddSupportedField('SelectObjectField', 'BsSelectObjectFieldRenderer');
+		$this->AddSupportedField('LinkedSetField', 'BsLinkedSetFieldRenderer');
 	}
 	}
 
 
 }
 }

+ 456 - 0
sources/renderer/bootstrap/fieldrenderer/bslinkedsetfieldrenderer.class.inc.php

@@ -0,0 +1,456 @@
+<?php
+
+// Copyright (C) 2010-2016 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify	
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+namespace Combodo\iTop\Renderer\Bootstrap\FieldRenderer;
+
+use \utils;
+use \Dict;
+use \UserRights;
+use \InlineImage;
+use \DBObjectSet;
+use \MetaModel;
+use \Combodo\iTop\Renderer\FieldRenderer;
+use \Combodo\iTop\Renderer\RenderingOutput;
+use \Combodo\iTop\Form\Field\LinkedSetField;
+
+/**
+ * Description of BsSelectObjectFieldRenderer
+ *
+ * @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
+ */
+class BsLinkedSetFieldRenderer extends FieldRenderer
+{
+
+	/**
+	 * Returns a RenderingOutput for the FieldRenderer's Field
+	 *
+	 * @return \Combodo\iTop\Renderer\RenderingOutput
+	 */
+	public function Render()
+	{
+		$oOutput = new RenderingOutput();
+		$sFieldMandatoryClass = ($this->oField->GetMandatory()) ? 'form_mandatory' : '';
+		// Vars to build the table
+		$sAttributesToDisplayAsJson = json_encode($this->oField->GetAttributesToDisplay());
+		$sAttCodesToDisplayAsJson = json_encode($this->oField->GetAttributesToDisplay(true));
+		$aItems = array();
+		$aItemIds = array();
+		$this->PrepareItems($aItems, $aItemIds);
+		$sItemsAsJson = json_encode($aItems);
+		$sItemIdsAsJson = htmlentities(json_encode($aItemIds), ENT_QUOTES, 'UTF-8');
+		
+		if (!$this->oField->GetHidden())
+		{
+			// Rendering field
+			$oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '">');
+			if ($this->oField->GetLabel() !== '')
+			{
+				$oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">')->AddHtml($this->oField->GetLabel(), true)->AddHtml('</label>');
+			}
+			$oOutput->AddHtml('<div class="help-block"></div>');
+
+			// Rendering table
+			// - Vars
+			$sTableId = 'table_' . $this->oField->GetGlobalId();
+			// - Output
+			$oOutput->AddHtml(
+<<<EOF
+				<div class="form_linkedset_wrapper">
+					<div class="row">
+						<div class="col-xs-12">
+							<input type="hidden" id="{$this->oField->GetGlobalId()}" name="{$this->oField->GetId()}" value="{$sItemIdsAsJson}" />
+							<table id="{$sTableId}" data-field-id="{$this->oField->GetId()}" class="table table-striped table-bordered responsive" cellspacing="0" width="100%">
+								<tbody>
+								</tbody>
+							</table>
+						</div>
+					</div>
+EOF
+			);
+
+			// Rendering table widget
+			// - Vars
+			$sEmptyTableLabel = htmlentities(Dict::S('UI:Message:EmptyList:UseAdd'), ENT_QUOTES, 'UTF-8');
+			$sSelectionOptionHtml = ($this->oField->GetReadOnly()) ? 'false' : '{"style": "multi"}';
+			$sSelectionInputHtml = ($this->oField->GetReadOnly()) ? '' : '<span class="row_input"><input type="checkbox" name="' . $this->oField->GetId() . '" /></span>';
+			// - Output
+			$oOutput->AddJs(
+<<<EOF
+				var oColumnProperties_{$this->oField->GetGlobalId()} = {$sAttributesToDisplayAsJson};
+				var oRawDatas_{$this->oField->GetGlobalId()} = {$sItemsAsJson};
+				var oTable_{$this->oField->GetGlobalId()};
+				var oSelectedItems_{$this->oField->GetGlobalId()} = {};
+
+				var getColumnsDefinition_{$this->oField->GetGlobalId()} = function()
+				{
+					var aColumnsDefinition = [];
+					var sFirstColumnId = Object.keys(oColumnProperties_{$this->oField->GetGlobalId()})[0];
+
+					for(sKey in oColumnProperties_{$this->oField->GetGlobalId()})
+					{
+						// Level main column
+						aColumnsDefinition.push({
+							"width": "auto",
+							"searchable": true,
+							"sortable": true,
+							"title": oColumnProperties_{$this->oField->GetGlobalId()}[sKey],
+							"defaultContent": "",
+							"type": "html",
+							"data": "attributes."+sKey+".att_code",
+							"render": function(data, type, row){
+								var cellElem;
+
+								// Preparing the cell data
+								if(row.attributes[data].url !== undefined)
+								{
+									cellElem = $('<a></a>');
+									cellElem.attr('target', '_blank').attr('href', row.attributes[data].url);
+								}
+								else
+								{
+									cellElem = $('<span></span>');
+								}
+								cellElem.attr('data-object-id', row.id).html('<span>' + row.attributes[data].value + '</span>');
+
+								if(data === sFirstColumnId)
+								{
+									cellElem.prepend('{$sSelectionInputHtml}');
+								}
+
+								return cellElem.prop('outerHTML');
+							},
+						});
+					}
+
+					return aColumnsDefinition;
+				};
+
+				// Note : Those options should be externalized in an library so we can use them on any DataTables for the portal.
+				// We would just have to override / complete the necessary elements
+				oTable_{$this->oField->GetGlobalId()} = $('#{$sTableId}').DataTable({
+					"language": {
+						"emptyTable":	  "{$sEmptyTableLabel}"
+					},
+					"displayLength": -1,
+					"scrollY": "300px",
+					"scrollCollapse": true,
+					"dom": 't',
+					"columns": getColumnsDefinition_{$this->oField->GetGlobalId()}(),
+					"select": {$sSelectionOptionHtml},
+					"rowId": "id",
+					"data": oRawDatas_{$this->oField->GetGlobalId()},
+				});
+EOF
+			);
+
+			// Attaching JS widget
+			$sObjectInformationsUrl = $this->oField->GetInformationEndpoint();
+			$oOutput->AddJs(
+<<<EOF
+				$("[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;
+
+						value = JSON.parse(me.element.find('#{$this->oField->GetGlobalId()}').val());
+
+						return value;
+					},
+					'set_current_value_callback': function(me, oEvent, oData){
+						// When we have data (meaning that we picked objects from search)
+						if(oData !== undefined && Object.keys(oData.values).length > 0)
+						{
+							// Retrieving new rows ids
+							var aObjectIds = Object.keys(oData.values);
+
+							// Retrieving rows informations so we can add them
+							$.post(
+								'{$sObjectInformationsUrl}',
+								{
+									sObjectClass: '{$this->oField->GetTargetClass()}',
+									aObjectIds: aObjectIds,
+									aObjectAttCodes: $sAttCodesToDisplayAsJson
+								},
+								function(oData){
+									// Updating datatables
+									if(oData.items !== undefined)
+									{
+										for(var i in oData.items)
+										{
+											// Adding item to table only if it's not already there
+											if($('#{$sTableId} tr#' + oData.items[i].id + '[role="row"]').length === 0)
+											{
+												// Making id negative in order to recognize it when persisting
+												oData.items[i].id = -1 * parseInt(oData.items[i].id);
+												oTable_{$this->oField->GetGlobalId()}.row.add(oData.items[i]);
+											}
+										}
+										oTable_{$this->oField->GetGlobalId()}.draw();
+									}
+								}
+							)
+							.done(function(oData){
+								// Updating hidden field
+								var aData = oTable_{$this->oField->GetGlobalId()}.rows().data().toArray();
+								var aObjectIds = [];
+
+								for(var i in aData)
+								{
+									aObjectIds.push({id: aData[i].id});
+								}
+
+								$('#{$this->oField->GetGlobalId()}').val(JSON.stringify(aObjectIds));
+							});
+						}
+						// We come from a button
+						else
+						{
+							// Updating hidden field
+							var aData = oTable_{$this->oField->GetGlobalId()}.rows().data().toArray();
+							var aObjectIds = [];
+
+							for(var i in aData)
+							{
+								aObjectIds.push({id: aData[i].id});
+							}
+
+							$('#{$this->oField->GetGlobalId()}').val(JSON.stringify(aObjectIds));
+						}
+					}
+				});
+EOF
+			);
+
+			// Additional features if in edition mode
+			if (!$this->oField->GetReadOnly())
+			{
+				// Rendering table
+				// - Vars
+				$sButtonAllId = 'btn_all_' . $this->oField->GetGlobalId();
+				$sButtonNoneId = 'btn_none_' . $this->oField->GetGlobalId();
+				$sButtonRemoveId = 'btn_remove_' . $this->oField->GetGlobalId();
+				$sButtonAddId = 'btn_add_' . $this->oField->GetGlobalId();
+				$sLabelAll = Dict::S('Core:BulkExport:CheckAll');
+				$sLabelNone = Dict::S('Core:BulkExport:UncheckAll');
+				$sLabelRemove = Dict::S('UI:Button:Remove');
+				$sLabelAdd = Dict::S('UI:Button:AddObject');
+				// - Output
+				$oOutput->AddHtml(
+<<<EOF
+					<div class="row">
+						<div class="col-xs-6">
+							<button type="button" class="btn btn-secondary" id="{$sButtonAllId}">{$sLabelAll}</button>
+							<button type="button" class="btn btn-secondary" id="{$sButtonNoneId}">{$sLabelNone}</button>
+						</div>
+						<div class="col-xs-6 text-right">
+							<button type="button" class="btn btn-danger" id="{$sButtonRemoveId}">{$sLabelRemove}</button>
+							<button type="button" class="btn btn-default" id="{$sButtonAddId}">{$sLabelAdd}</button>
+						</div>
+					</div>
+EOF
+				);
+
+				// Rendering table widget
+				// - Vars
+				$sAddButtonEndpoint = str_replace('-sMode-', 'from-attribute', $this->oField->GetSearchEndpoint());
+				// - Output
+				$oOutput->AddJs(
+	<<<EOF
+					// Handles items selection/deselection
+					// - Directly on the table
+					oTable_{$this->oField->GetGlobalId()}.off('select').on('select', function(oEvent, dt, type, indexes){
+						var aData = oTable_{$this->oField->GetGlobalId()}.rows(indexes).data().toArray();
+
+						// Checking input
+						$('#{$sTableId} tr[role="row"].selected td:first-child input').prop('checked', true);
+						// Saving values in temp array
+						for(var i in aData)
+						{
+							var iItemId = aData[i].id;
+							if(!(iItemId in oSelectedItems_{$this->oField->GetGlobalId()}))
+							{
+								oSelectedItems_{$this->oField->GetGlobalId()}[iItemId] = aData[i].name;
+							}
+						}
+					});
+					oTable_{$this->oField->GetGlobalId()}.off('deselect').on('deselect', function(oEvent, dt, type, indexes){
+						var aData = oTable_{$this->oField->GetGlobalId()}.rows(indexes).data().toArray();
+
+						// Checking input
+						$('#{$sTableId} tr[role="row"]:not(.selected) td:first-child input').prop('checked', false);
+						// Saving values in temp array
+						for(var i in aData)
+						{
+							var iItemId = aData[i].id;
+							if(iItemId in oSelectedItems_{$this->oField->GetGlobalId()})
+							{
+								delete oSelectedItems_{$this->oField->GetGlobalId()}[iItemId];
+							}
+						}
+					});
+					// - From the bottom buttons
+					$('#{$sButtonAllId}').off('click').on('click', function(){
+						oTable_{$this->oField->GetGlobalId()}.rows().select();
+					});
+					$('#{$sButtonNoneId}').off('click').on('click', function(){
+						oTable_{$this->oField->GetGlobalId()}.rows().deselect();
+					});
+
+					// Handles items remove/add
+					$('#{$sButtonRemoveId}').off('click').on('click', function(){
+						oTable_{$this->oField->GetGlobalId()}.rows({selected: true}).remove().draw();
+						$("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").triggerHandler('set_current_value');
+					});
+					$('#{$sButtonAddId}').off('click').on('click', function(){
+						// Creating a new modal
+						var oModalElem;
+						if($('.modal[data-source-element="{$sButtonAddId}"]').length === 0)
+						{
+							oModalElem = $('#modal-for-all').clone();
+							oModalElem.attr('id', '').attr('data-source-element', '{$sButtonAddId}').appendTo('body');
+						}
+						else
+						{
+							oModalElem = $('.modal[data-source-element="{$sButtonAddId}"]').first();
+						}
+						// Resizing to small modal
+						oModalElem.find('.modal-dialog').removeClass('modal-sm').addClass('modal-lg');
+						// Loading content
+						oModalElem.find('.modal-content').html($('#page_overlay .overlay_content').html());
+						oModalElem.find('.modal-content').load(
+							'{$sAddButtonEndpoint}',
+							{
+								sFormPath: '{$this->oField->GetFormPath()}',
+								sFieldId: '{$this->oField->GetId()}'
+							}
+						);
+						oModalElem.modal('show');
+					});
+EOF
+				);
+			}
+		}
+		// ... and in hidden mode
+		else
+		{
+			$oOutput->AddHtml('<input type="hidden" id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" value="' . $sItemIdsAsJson . '" />');
+		}
+
+		// End of table rendering
+		$oOutput->AddHtml('</div>');
+		$oOutput->AddHtml('</div>');
+
+		return $oOutput;
+	}
+
+	protected function PrepareItems(&$aItems, &$aItemIds)
+	{
+		$oValueSet = $this->oField->GetCurrentValue();
+		$oValueSet->OptimizeColumnLoad(array($this->oField->GetTargetClass() => $this->oField->GetAttributesToDisplay(true)));
+		while ($oItem = $oValueSet->Fetch())
+		{
+			$aItemProperties = array(
+				'id' => $oItem->GetKey(),
+				'name' => $oItem->GetName(),
+				'attributes' => array()
+			);
+
+			// In case of indirect linked set, we must retrieve the remote object
+			if ($this->oField->IsIndirect())
+			{
+				$oRemoteItem = MetaModel::GetObject($this->oField->GetTargetClass(), $oItem->Get($this->oField->GetExtKeyToRemote()));
+			}
+			else
+			{
+				$oRemoteItem = $oItem;
+			}
+
+			foreach ($this->oField->GetAttributesToDisplay(true) as $sAttCode)
+			{
+				if ($sAttCode !== 'id')
+				{
+					$aAttProperties = array(
+						'att_code' => $sAttCode
+					);
+
+					$oAttDef = MetaModel::GetAttributeDef($this->oField->GetTargetClass(), $sAttCode);
+					if ($oAttDef->IsExternalKey())
+					{
+						$aAttProperties['value'] = $oRemoteItem->Get($sAttCode . '_friendlyname');
+					}
+					else
+					{
+						$aAttProperties['value'] = $oAttDef->GetValueLabel($oRemoteItem->Get($sAttCode));
+					}
+
+					$aItemProperties['attributes'][$sAttCode] = $aAttProperties;
+				}
+			}
+
+			$aItems[] = $aItemProperties;
+			$aItemIds[] = array('id' => $oItem->GetKey());
+		}
+	}
+
+	/**
+	 * Renders an regular search button
+	 *
+	 * @param RenderingOutput $oOutput
+	 */
+	protected function RenderRegularSearch(RenderingOutput &$oOutput)
+	{
+		$sSearchButtonId = 's_rg_' . $this->oField->GetGlobalId();
+		$sEndpoint = str_replace('-sMode-', 'from-attribute', $this->oField->GetSearchEndpoint());
+
+		$oOutput->AddHtml('<div class="col-xs-2 col-lg-1">');
+		$oOutput->AddHtml('<button type="button" class="btn btn-default" id="' . $sSearchButtonId . '">S</button>');
+		$oOutput->AddHtml('</div>');
+
+		$oOutput->AddJs(
+<<<EOF
+			$('#{$sSearchButtonId}').off('click').on('click', function(){
+				// Creating a new modal
+				var oModalElem;
+				if($('.modal[data-source-element="{$sSearchButtonId}"]').length === 0)
+				{
+					oModalElem = $('#modal-for-all').clone();
+					oModalElem.attr('id', '').attr('data-source-element', '{$sSearchButtonId}').appendTo('body');
+				}
+				else
+				{
+					oModalElem = $('.modal[data-source-element="{$sSearchButtonId}"]').first();
+				}
+				// Resizing to small modal
+				oModalElem.find('.modal-dialog').removeClass('modal-sm').addClass('modal-lg');
+				// Loading content
+				oModalElem.find('.modal-content').html($('#page_overlay .overlay_content').html());
+				oModalElem.find('.modal-content').load(
+					'{$sEndpoint}',
+					{
+						sFormPath: '{$this->oField->GetFormPath()}',
+						sFieldId: '{$this->oField->GetId()}'
+					}
+				);
+				oModalElem.modal('show');
+			});
+EOF
+		);
+	}
+
+}

+ 402 - 0
sources/renderer/bootstrap/fieldrenderer/bsselectobjectfieldrenderer.class.inc.php

@@ -0,0 +1,402 @@
+<?php
+
+// Copyright (C) 2010-2016 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify	
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+namespace Combodo\iTop\Renderer\Bootstrap\FieldRenderer;
+
+use \utils;
+use \Dict;
+use \UserRights;
+use \InlineImage;
+use \DBObjectSet;
+use \MetaModel;
+use \Combodo\iTop\Renderer\FieldRenderer;
+use \Combodo\iTop\Renderer\RenderingOutput;
+use \Combodo\iTop\Form\Field\SelectObjectField;
+
+/**
+ * Description of BsSelectObjectFieldRenderer
+ *
+ * @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
+ */
+class BsSelectObjectFieldRenderer extends FieldRenderer
+{
+
+	/**
+	 * Returns a RenderingOutput for the FieldRenderer's Field
+	 *
+	 * @return \Combodo\iTop\Renderer\RenderingOutput
+	 */
+	public function Render()
+	{
+		$oOutput = new RenderingOutput();
+		$sFieldValueClass = $this->oField->GetSearch()->GetClass();
+		$sFieldMandatoryClass = ($this->oField->GetMandatory()) ? 'form_mandatory' : '';
+		$iFieldControlType = $this->oField->GetControlType();
+
+		// TODO : Remove this when hierarchical search supported
+		$this->oField->SetHierarchical(false);
+
+		// Rendering field in edition mode
+		if (!$this->oField->GetReadOnly() && !$this->oField->GetHidden())
+		{
+			// Rendering field
+			$oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '">');
+			if ($this->oField->GetLabel() !== '')
+			{
+				$oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">')->AddHtml($this->oField->GetLabel(), true)->AddHtml('</label>');
+			}
+			$oOutput->AddHtml('<div class="help-block"></div>');
+			// - As a select
+			if ($iFieldControlType === SelectObjectField::CONTROL_SELECT)
+			{
+				// Checking if regular select or autocomplete
+				$oSearch = $this->oField->GetSearch()->DeepClone();
+				$oCountSet = new DBObjectSet($oSearch);
+				$iSetCount = $oCountSet->Count();
+				$bRegularSelect = ($iSetCount <= $this->oField->GetMaximumComboLength());
+				unset($oCountSet);
+				
+				// - For regular select
+				if ($bRegularSelect)
+				{
+					// HTML for select part
+					// - Opening row
+					$oOutput->AddHtml('<div class="row">');
+					// - Rendering select
+					$oOutput->AddHtml('<div class="col-xs-' . ( $this->oField->GetHierarchical() ? 10 : 12 ) . ' col-sm-' . ( $this->oField->GetHierarchical() ? 9 : 12 ) . ' col-md-' . ( $this->oField->GetHierarchical() ? 10 : 12 ) . '">');
+					$oOutput->AddHtml('<select id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" class="form-control">');
+					$oOutput->AddHtml('<option value="">')->AddHtml(Dict::S('UI:SelectOne'), false)->AddHtml('</option>');
+					// - Retrieving choices
+					$oChoicesSet = new DBObjectSet($oSearch);
+					while ($oChoice = $oChoicesSet->Fetch())
+					{
+						// Note : The test is a double equal on purpose as the type of the value received from the XHR is not always the same as the type of the allowed values. (eg : string vs int)
+						$sSelectedAtt = ($this->oField->GetCurrentValue() == $oChoice->GetKey()) ? 'selected' : '';
+						$oOutput->AddHtml('<option value="' . $oChoice->GetKey() . '" ' . $sSelectedAtt . ' >')->AddHtml($oChoice->GetName(), false)->AddHtml('</option>');
+					}
+					unset($oChoicesSet);
+					$oOutput->AddHtml('</select>');
+					$oOutput->AddHtml('</div>');
+					// - Closing col for autocomplete & opening col for hierarchy, rendering hierarchy button, closing col and row
+					$oOutput->AddHtml('<div class="col-xs-' . ( $this->oField->GetHierarchical() ? 2 : 0 ) . ' col-sm-' . ( $this->oField->GetHierarchical() ? 3 : 0 ) . ' col-md-' . ( $this->oField->GetHierarchical() ? 2 : 0 ) . ' text-right">');
+					$this->RenderHierarchicalSearch($oOutput);
+					$oOutput->AddHtml('</div>');
+					// - Closing row
+					$oOutput->AddHtml('</div>');
+
+					// JS FieldChange trigger (:input are not always at the same depth)
+					$oOutput->AddJs(
+<<<EOF
+						$("#{$this->oField->GetGlobalId()}").off("change keyup").on("change keyup", function(){
+							var me = this;
+
+							$(this).closest(".field_set").trigger("field_change", {
+								id: $(me).attr("id"),
+								name: $(me).closest(".form_field").attr("data-field-id"),
+								value: $(me).val()
+							});
+						});
+EOF
+					);
+
+					// Attaching JS widget
+					$oOutput->AddJs(
+<<<EOF
+						$("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field({
+							'validators': {$this->GetValidatorsAsJson()}
+						});
+EOF
+					);
+				}
+				// - For autocomplete
+				else
+				{
+					$sAutocompleteFieldId = 's_ac_' . $this->oField->GetGlobalId();
+					$sEndpoint = str_replace('-sMode-', 'autocomplete', $this->oField->GetSearchEndpoint());
+					$sNoResultText = Dict::S('Portal:Autocomplete:NoResult');
+
+					// Retrieving field value
+					if ($this->oField->GetCurrentValue() !== null && $this->oField->GetCurrentValue() !== 0)
+					{
+						$oFieldValue = MetaModel::GetObject($sFieldValueClass, $this->oField->GetCurrentValue());
+						$sFieldValue = $oFieldValue->GetName();
+					}
+					else
+					{
+						$sFieldValue = '';
+					}
+
+					// HTML for autocomplete part
+					// - Opening row
+					$oOutput->AddHtml('<div class="row">');
+					// - Rendering autocomplete search
+					$oOutput->AddHtml('<div class="col-xs-' . ( $this->oField->GetHierarchical() ? 9 : 10 ) . ' col-sm-' . ( $this->oField->GetHierarchical() ? 8 : 9 ) . ' col-md-' . ( $this->oField->GetHierarchical() ? 8 : 10 ) . ' col-lg-' . ( $this->oField->GetHierarchical() ? 9 : 10 ) . '">');
+					$oOutput->AddHtml('<input type="text" id="' . $sAutocompleteFieldId . '" name="' . $sAutocompleteFieldId . '" value="')->AddHtml($sFieldValue, true)->AddHtml('" data-target-id="' . $this->oField->GetGlobalId() . ' "class="form-control" />');
+					$oOutput->AddHtml('<input type="hidden" id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" value="' . $this->oField->GetCurrentValue() . '" />');
+					$oOutput->AddHtml('</div>');
+					// - Rendering buttons
+					$oOutput->AddHtml('<div class="col-xs-' . ( $this->oField->GetHierarchical() ? 3 : 2 ) . ' col-sm-' . ( $this->oField->GetHierarchical() ? 4 : 3 ) . ' col-md-' . ( $this->oField->GetHierarchical() ? 4 : 2 ) . ' col-lg-' . ( $this->oField->GetHierarchical() ? 3 : 2 ) . ' text-right">');
+					$oOutput->AddHtml('<div class="btn-group" role="group">');
+					//   - Rendering hierarchy button
+					$this->RenderHierarchicalSearch($oOutput);
+					//   - Rendering regular search
+					$this->RenderRegularSearch($oOutput);
+					$oOutput->AddHtml('</div>');
+					$oOutput->AddHtml('</div>');
+					// - Closing row
+					$oOutput->AddHtml('</div>');
+
+					// JS FieldChange trigger (:input are not always at the same depth)
+					// Note : Not used for that field type
+					// Attaching JS widget
+					$oOutput->AddJs(
+<<<EOF
+					$("[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;
+
+							value = me.element.find('#{$this->oField->GetGlobalId()}').val();
+
+							return value;
+						},
+						'set_current_value_callback': function(me, oEvent, oData){
+							var sItemId = Object.keys(oData.value)[0];
+							var sItemName = oData.value[sItemId];
+
+							// Updating autocomplete field
+							me.element.find('#{$this->oField->GetGlobalId()}').val(sItemId);
+							me.element.find('#{$sAutocompleteFieldId}').val(sItemName);
+							oAutocompleteSource_{$this->oField->GetId()}.index.datums[sItemId] = {id: sItemId, name: sItemName};
+						}
+					});
+EOF
+					);
+
+					// Preparing JS part for autocomplete
+					$oOutput->AddJs(
+<<<EOF
+						var oAutocompleteSource_{$this->oField->GetId()} = new Bloodhound({
+							queryTokenizer: Bloodhound.tokenizers.whitespace,
+							datumTokenizer: Bloodhound.tokenizers.whitespace,
+							remote: {
+								url : '{$sEndpoint}',
+								prepare: function(query, settings){
+									settings.type = "POST";
+									settings.contentType = "application/json; charset=UTF-8";
+									settings.data = {
+											sQuery: query
+									}
+									return settings;
+								},
+								filter: function(response){
+									var oItems = response.results.items;
+									// Manualy adding data from remote to the index.datums so we can check data later
+									for(var sItemKey in oItems)
+									{
+										oAutocompleteSource_{$this->oField->GetId()}.index.datums[oItems[sItemKey].id] = oItems[sItemKey];
+									}
+									return oItems;
+								}
+							}
+						});
+
+						$('#$sAutocompleteFieldId').typeahead({
+							hint: true,
+							hightlight: true,
+							minLength: {$this->oField->GetMinAutoCompleteChars()}
+						},{
+							name: '{$this->oField->GetId()}',
+							source: oAutocompleteSource_{$this->oField->GetId()},
+							limit: 20,
+							display: 'name',
+							templates: {
+								suggestion: Handlebars.compile('<div>{{name}}</div>'),
+								pending: $("#page_overlay .content_loader").prop('outerHTML'),
+								notFound: '<div class="no_result">{$sNoResultText}</div>'
+							}
+						})
+						.off('typeahead:select').on('typeahead:select', function(oEvent, oSuggestion){
+							$('#{$this->oField->GetGlobalId()}').val(oSuggestion.id);
+						})
+						.off('typeahead:change').on('typeahead:change', function(oEvent, oSuggestion){
+							// Checking if the value is a correct value. This is necessary because the user could empty the field / remove some chars and typeahead would not update the hidden input
+							var oDatums = oAutocompleteSource_{$this->oField->GetId()}.index.datums;
+							var bFound = false;
+							for(var i in oDatums)
+							{
+								if(oDatums[i].name == oSuggestion)
+								{
+									bFound = true;
+									$('#{$this->oField->GetGlobalId()}').val(oDatums[i].id);
+									break;
+								}
+							}
+							// Emptying the fields if value is incorrect
+							if(!bFound)
+							{
+								$('#{$this->oField->GetGlobalId()}').val(0);
+								$('#{$sAutocompleteFieldId}').val('');
+							}
+						});
+EOF
+					);
+				}
+			}
+			$oOutput->AddHtml('</div>');
+		}
+		// ... and in read-only mode (or hidden)
+		else
+		{
+			// Retrieving field value
+			if ($this->oField->GetCurrentValue() !== null && $this->oField->GetCurrentValue() !== 0 && $this->oField->GetCurrentValue() !== '')
+			{
+				$oFieldValue = MetaModel::GetObject($sFieldValueClass, $this->oField->GetCurrentValue());
+				$sFieldValue = $oFieldValue->GetName();
+			}
+			else
+			{
+				$sFieldValue = Dict::S('UI:UndefinedObject');
+			}
+
+			$oOutput->AddHtml('<div class="form-group">');
+			// Showing label / value only if read-only but not hidden
+			if (!$this->oField->GetHidden())
+			{
+				if ($this->oField->GetLabel() !== '')
+				{
+					$oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">')->AddHtml($this->oField->GetLabel(), true)->AddHtml('</label>');
+				}
+				$oOutput->AddHtml('<div class="form-control-static">' . $sFieldValue . '</div>');
+			}
+			$oOutput->AddHtml('<input type="hidden" id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" value="' . $this->oField->GetCurrentValue() . '" class="form-control" />');
+			$oOutput->AddHtml('</div>');
+
+
+			// JS FieldChange trigger (:input are not always at the same depth)
+			$oOutput->AddJs(
+<<<EOF
+				$("#{$this->oField->GetGlobalId()}").off("change keyup").on("change keyup", function(){
+					var me = this;
+
+					$(this).closest(".field_set").trigger("field_change", {
+						id: $(me).attr("id"),
+						name: $(me).closest(".form_field").attr("data-field-id"),
+						value: $(me).val()
+					});
+				});
+EOF
+			);
+
+			// Attaching JS widget
+			$oOutput->AddJs(
+<<<EOF
+				$("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field({
+					'validators': {$this->GetValidatorsAsJson()}
+				});
+EOF
+			);
+		}
+
+		return $oOutput;
+	}
+
+	/**
+	 * Renders an hierarchical search button
+	 *
+	 * @param RenderingOutput $oOutput
+	 */
+	protected function RenderHierarchicalSearch(RenderingOutput &$oOutput)
+	{
+		if ($this->oField->GetHierarchical())
+		{
+			$sHierarchicalButtonId = 's_hi_' . $this->oField->GetGlobalId();
+			$sEndpoint = str_replace('-sMode-', 'hierarchy', $this->oField->GetSearchEndpoint());
+
+			$oOutput->AddHtml('<button type="button" class="btn btn-default" id="' . $sHierarchicalButtonId . '"><span class="glyphicon glyphicon-ext-hierarchy"></span></button>');
+
+			$oOutput->AddJs(
+<<<EOF
+				$('#{$sHierarchicalButtonId}').off('click').on('click', function(){
+					// Creating a new modal
+					// Note : This could be better if we check for an existing modal first instead of always creating a new one
+					var oModalElem = $('#modal-for-all').clone();
+					oModalElem.attr('id', '').attr('data-source-element', '{$sHierarchicalButtonId}').appendTo('body');
+					// Resizing to small modal
+					oModalElem.find('.modal-dialog').removeClass('modal-lg').addClass('modal-sm');
+					// Loading content
+					oModalElem.find('.modal-content').html($('#page_overlay .overlay_content').html());
+					oModalElem.find('.modal-content').load(
+						'{$sEndpoint}',
+						{
+							sFormPath: '{$this->oField->GetFormPath()}',
+							sFieldId: '{$this->oField->GetId()}'
+						}
+					);
+					oModalElem.modal('show');
+				});
+EOF
+			);
+		}
+	}
+
+	/**
+	 * Renders an regular search button
+	 *
+	 * @param RenderingOutput $oOutput
+	 */
+	protected function RenderRegularSearch(RenderingOutput &$oOutput)
+	{
+		$sSearchButtonId = 's_rg_' . $this->oField->GetGlobalId();
+		$sEndpoint = str_replace('-sMode-', 'from-attribute', $this->oField->GetSearchEndpoint());
+
+		$oOutput->AddHtml('<button type="button" class="btn btn-default" id="' . $sSearchButtonId . '"><span class="glyphicon glyphicon-search" aria-hidden="true"></span></button>');
+		
+		$oOutput->AddJs(
+<<<EOF
+			$('#{$sSearchButtonId}').off('click').on('click', function(){
+				// Creating a new modal
+				var oModalElem;
+				if($('.modal[data-source-element="{$sSearchButtonId}"]').length === 0)
+				{
+					oModalElem = $('#modal-for-all').clone();
+					oModalElem.attr('id', '').attr('data-source-element', '{$sSearchButtonId}').appendTo('body');
+				}
+				else
+				{
+					oModalElem = $('.modal[data-source-element="{$sSearchButtonId}"]').first();
+				}
+				// Resizing to small modal
+				oModalElem.find('.modal-dialog').removeClass('modal-sm').addClass('modal-lg');
+				// Loading content
+				oModalElem.find('.modal-content').html($('#page_overlay .overlay_content').html());
+				oModalElem.find('.modal-content').load(
+					'{$sEndpoint}',
+					{
+						sFormPath: '{$this->oField->GetFormPath()}',
+						sFieldId: '{$this->oField->GetId()}'
+					}
+				);
+				oModalElem.modal('show');
+			});
+EOF
+		);
+	}
+
+}

+ 48 - 6
sources/renderer/bootstrap/fieldrenderer/bssimplefieldrenderer.class.inc.php

@@ -46,7 +46,6 @@ class BsSimpleFieldRenderer extends FieldRenderer
 		$sFieldClass = get_class($this->oField);
 		$sFieldClass = get_class($this->oField);
 		$sFieldMandatoryClass = ($this->oField->GetMandatory()) ? 'form_mandatory' : '';
 		$sFieldMandatoryClass = ($this->oField->GetMandatory()) ? 'form_mandatory' : '';
 		
 		
-		// TODO : Shouldn't we have a field type so we don't have to maintain FQN classname ?
 		// Rendering field in edition mode
 		// Rendering field in edition mode
 		if (!$this->oField->GetReadOnly() && !$this->oField->GetHidden())
 		if (!$this->oField->GetReadOnly() && !$this->oField->GetHidden())
 		{
 		{
@@ -64,16 +63,60 @@ class BsSimpleFieldRenderer extends FieldRenderer
 					break;
 					break;
 
 
 				case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
 				case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
+				case 'Combodo\\iTop\\Form\\Field\\CaseLogField':
 					$bRichEditor = ($this->oField->GetFormat() === TextAreaField::ENUM_FORMAT_HTML);
 					$bRichEditor = ($this->oField->GetFormat() === TextAreaField::ENUM_FORMAT_HTML);
-
+					
 					$oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '">');
 					$oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '">');
 					if ($this->oField->GetLabel() !== '')
 					if ($this->oField->GetLabel() !== '')
 					{
 					{
 						$oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">')->AddHtml($this->oField->GetLabel(), true)->AddHtml('</label>');
 						$oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">')->AddHtml($this->oField->GetLabel(), true)->AddHtml('</label>');
 					}
 					}
 					$oOutput->AddHtml('<div class="help-block"></div>');
 					$oOutput->AddHtml('<div class="help-block"></div>');
+					// First the edition area
+					$oOutput->AddHtml('<div>');
 					$oOutput->AddHtml('<textarea id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" class="form-control" rows="8">' . $this->oField->GetCurrentValue() . '</textarea>');
 					$oOutput->AddHtml('<textarea id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" class="form-control" rows="8">' . $this->oField->GetCurrentValue() . '</textarea>');
 					$oOutput->AddHtml('</div>');
 					$oOutput->AddHtml('</div>');
+					// Then the previous entries if necessary
+					if ($sFieldClass === 'Combodo\\iTop\\Form\\Field\\CaseLogField')
+					{
+						$aEntries = $this->oField->GetEntries();
+						if (count($aEntries) > 0)
+						{
+							$oOutput->AddHtml('<div>');
+							for ($i = 0; $i < count($aEntries); $i++)
+							{
+								$sEntryDate = $aEntries[$i]['date'];
+								$sEntryUser = $aEntries[$i]['user_login'];
+								$sEntryHeader = Dict::Format('UI:CaseLog:Header_Date_UserName', $sEntryDate, $sEntryUser);
+
+								// Only the last 2 entries are expanded by default
+								$sEntryContentExpanded = ($i < 2) ? 'true' : 'false';
+								$sEntryHeaderButtonClass = ($i < 2) ? '' : 'collapsed';
+								$sEntryContentClass = ($i < 2) ? 'in' : '';
+								$sEntryContentId = 'caselog_field_entry_content-' . $this->oField->GetGlobalId() . '-' . $i;
+
+								// Note : We use CKEditor stylesheet to format this
+								$oOutput->AddHtml(
+<<<EOF
+									<div class="caselog_field_entry cke_inner">
+										<div class="caselog_field_entry_header">
+											{$sEntryHeader}
+											<div class="pull-right">
+												<span class="caselog_field_entry_button {$sEntryHeaderButtonClass}" data-toggle="collapse" href="#{$sEntryContentId}" aria-expanded="{$sEntryContentExpanded}" aria-controls="{$sEntryContentId}"></span>
+											</div>
+										</div>
+										<div class="caselog_field_entry_content collapse {$sEntryContentClass}" id="{$sEntryContentId}">
+											{$aEntries[$i]['message_html']}
+										</div>
+									</div>
+EOF
+								);
+							}
+							$oOutput->AddHtml('</div>');
+						}
+					}
+
+					$oOutput->AddHtml('</div>');
 					// Some additional stuff if we are displaying it with a rich editor
 					// Some additional stuff if we are displaying it with a rich editor
 					if ($bRichEditor)
 					if ($bRichEditor)
 					{
 					{
@@ -145,7 +188,7 @@ EOF
 		// ... and in read-only mode (or hidden)
 		// ... and in read-only mode (or hidden)
 		else
 		else
 		{
 		{
-			// ... specific rendering for fields with mulltiple values
+			// ... specific rendering for fields with multiple values
 			if (($this->oField instanceof Combodo\iTop\Form\Field\MultipleChoicesField) && ($this->oField->GetMultipleValuesEnabled()))
 			if (($this->oField instanceof Combodo\iTop\Form\Field\MultipleChoicesField) && ($this->oField->GetMultipleValuesEnabled()))
 			{
 			{
 				// TODO
 				// TODO
@@ -190,7 +233,6 @@ EOF
 					case 'Combodo\\iTop\\Form\\Field\\RadioField':
 					case 'Combodo\\iTop\\Form\\Field\\RadioField':
 					case 'Combodo\\iTop\\Form\\Field\\SelectField':
 					case 'Combodo\\iTop\\Form\\Field\\SelectField':
 					case 'Combodo\\iTop\\Form\\Field\\MultipleSelectField':
 					case 'Combodo\\iTop\\Form\\Field\\MultipleSelectField':
-					case 'Combodo\\iTop\\Form\\Field\\SelectObjectField': // TODO : This should be check for external key, as we would display it differently
 						$aFieldChoices = $this->oField->GetChoices();
 						$aFieldChoices = $this->oField->GetChoices();
 						$sFieldValue = (isset($aFieldChoices[$this->oField->GetCurrentValue()])) ? $aFieldChoices[$this->oField->GetCurrentValue()] : Dict::S('UI:UndefinedObject');
 						$sFieldValue = (isset($aFieldChoices[$this->oField->GetCurrentValue()])) ? $aFieldChoices[$this->oField->GetCurrentValue()] : Dict::S('UI:UndefinedObject');
 
 
@@ -216,9 +258,9 @@ EOF
 		{
 		{
 			case 'Combodo\\iTop\\Form\\Field\\StringField':
 			case 'Combodo\\iTop\\Form\\Field\\StringField':
 			case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
 			case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
+			case 'Combodo\\iTop\\Form\\Field\\CaseLogField':
 			case 'Combodo\\iTop\\Form\\Field\\SelectField':
 			case 'Combodo\\iTop\\Form\\Field\\SelectField':
 			case 'Combodo\\iTop\\Form\\Field\\MultipleSelectField':
 			case 'Combodo\\iTop\\Form\\Field\\MultipleSelectField':
-			case 'Combodo\\iTop\\Form\\Field\\SelectObjectField':
 			case 'Combodo\\iTop\\Form\\Field\\HiddenField':
 			case 'Combodo\\iTop\\Form\\Field\\HiddenField':
 				$oOutput->AddJs(
 				$oOutput->AddJs(
 <<<EOF
 <<<EOF
@@ -272,7 +314,6 @@ EOF
 			case 'Combodo\\iTop\\Form\\Field\\StringField':
 			case 'Combodo\\iTop\\Form\\Field\\StringField':
 			case 'Combodo\\iTop\\Form\\Field\\SelectField':
 			case 'Combodo\\iTop\\Form\\Field\\SelectField':
 			case 'Combodo\\iTop\\Form\\Field\\MultipleSelectField':
 			case 'Combodo\\iTop\\Form\\Field\\MultipleSelectField':
-			case 'Combodo\\iTop\\Form\\Field\\SelectObjectField':
 			case 'Combodo\\iTop\\Form\\Field\\HiddenField':
 			case 'Combodo\\iTop\\Form\\Field\\HiddenField':
 			case 'Combodo\\iTop\\Form\\Field\\RadioField':
 			case 'Combodo\\iTop\\Form\\Field\\RadioField':
 			case 'Combodo\\iTop\\Form\\Field\\CheckboxField':
 			case 'Combodo\\iTop\\Form\\Field\\CheckboxField':
@@ -283,6 +324,7 @@ EOF
 				);
 				);
 				break;
 				break;
 			case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
 			case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
+			case 'Combodo\\iTop\\Form\\Field\\CaseLogField':
 				$oOutput->AddJs(
 				$oOutput->AddJs(
 					<<<EOF
 					<<<EOF
 					$("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field_html($sFormFieldOptions);
 					$("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field_html($sFormFieldOptions);

+ 1 - 1
sources/renderer/bootstrap/fieldrenderer/bssubformfieldrenderer.class.inc.php

@@ -28,7 +28,7 @@ class BsSubFormFieldRenderer extends FieldRenderer
 	public function Render()
 	public function Render()
 	{
 	{
 		$oOutput = new RenderingOutput();
 		$oOutput = new RenderingOutput();
-
+		
 		if (($this->oField->GetLabel() !== null) && ($this->oField->GetLabel() !== ''))
 		if (($this->oField->GetLabel() !== null) && ($this->oField->GetLabel() !== ''))
 		{
 		{
 			$oOutput->AddHtml('<fieldset><legend>' . $this->oField->GetLabel() . '</legend>');
 			$oOutput->AddHtml('<fieldset><legend>' . $this->oField->GetLabel() . '</legend>');

+ 27 - 0
sources/renderer/fieldrenderer.class.inc.php

@@ -19,6 +19,7 @@
 
 
 namespace Combodo\iTop\Renderer;
 namespace Combodo\iTop\Renderer;
 
 
+use \Dict;
 use \DBObject;
 use \DBObject;
 use \Combodo\iTop\Form\Field\Field;
 use \Combodo\iTop\Form\Field\Field;
 
 
@@ -62,6 +63,32 @@ abstract class FieldRenderer
 	}
 	}
 
 
 	/**
 	/**
+	 * Returns a JSON encoded string that contains the field's validators as an array.
+	 *
+	 * eg :
+	 * {
+	 *   validator_id_1 : {reg_exp: /[0-9]/, message: "Error message"},
+	 *   validator_id_2 : {reg_exp: /[a-z]/, message: "Another error message"},
+	 * 	 ...
+	 * }
+	 *
+	 * @return string
+	 */
+	protected function GetValidatorsAsJson()
+	{
+		$aValidators = array();
+		foreach ($this->oField->GetValidators() as $oValidator)
+		{
+			$aValidators[$oValidator::GetName()] = array(
+				'reg_exp' => $oValidator->GetRegExp(),
+				'message' => Dict::S($oValidator->GetErrorMessage())
+			);
+		}
+		// - Formatting options
+		return json_encode($aValidators);
+	}
+
+	/**
 	 * Renders a Field as a RenderingOutput
 	 * Renders a Field as a RenderingOutput
 	 *
 	 *
 	 * @return \Combodo\iTop\Renderer\RenderingOutput
 	 * @return \Combodo\iTop\Renderer\RenderingOutput