diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 6b568b2dd1..b7114693e6 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -1262,7 +1262,56 @@ class AttributeLinkedSet extends AttributeDefinition $oRemoteAtt = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToMe()); 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; } } @@ -2919,16 +2968,22 @@ class AttributeCaseLog extends AttributeLongText 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) { // First we call the parent so the field is build $oFormField = parent::MakeFormField($oObject, $oFormField); // Then only we set the value $oFormField->SetCurrentValue($this->GetEditValue($oObject->Get($this->GetCode()))); + // And we set the entries + $oFormField->SetEntries($oObject->Get($this->GetCode())->GetAsArray()); return $oFormField; } - } /** @@ -4023,7 +4078,7 @@ class AttributeExternalKey extends AttributeDBFieldVoid static public function GetFormFieldClass() { - return '\\Combodo\\iTop\\Form\\Field\\SelectField'; + return '\\Combodo\\iTop\\Form\\Field\\SelectObjectField'; } public function MakeFormField(DBObject $oObject, $oFormField = null) @@ -4034,7 +4089,11 @@ class AttributeExternalKey extends AttributeDBFieldVoid $sFormFieldClass = static::GetFormFieldClass(); $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 $aFieldDependencies = $this->GetPrerequisiteAttributes(); if (!empty($aFieldDependencies)) @@ -4043,12 +4102,16 @@ class AttributeExternalKey extends AttributeDBFieldVoid $oTmpField = $oFormField; $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 { - $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 diff --git a/js/form_field.js b/js/form_field.js index 7fa6c6e926..61bb61c6ca 100644 --- a/js/form_field.js +++ b/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(); }, on_validation_callback: function(data){ }, 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 @@ -30,7 +30,7 @@ $(function() me.options.validators = oData; }); this.element - .bind('validate get_current_value', function(oEvent, oData){ + .bind('validate get_current_value set_current_value', function(oEvent, oData){ oEvent.stopPropagation(); var callback = me.options[oEvent.type+'_callback']; @@ -124,7 +124,12 @@ $(function() { var bMandatory = (this.options.validators.mandatory !== undefined); // 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); // 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... diff --git a/sources/autoload.php b/sources/autoload.php index fc30d6947f..91d4c0faa9 100644 --- a/sources/autoload.php +++ b/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/durationfield.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/selectfield.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/checkboxfield.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/mandatoryvalidator.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/bootstrap/bsformrenderer.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'; diff --git a/sources/form/field/caselogfield.class.inc.php b/sources/form/field/caselogfield.class.inc.php new file mode 100644 index 0000000000..c14545d96a --- /dev/null +++ b/sources/form/field/caselogfield.class.inc.php @@ -0,0 +1,55 @@ + + +namespace Combodo\iTop\Form\Field; + +use \Closure; +use \DBObject; +use \Combodo\iTop\Form\Field\TextAreaField; + +/** + * Description of CaseLogField + * + * @author Guillaume Lajarige + */ +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; + } + +} diff --git a/sources/form/field/field.class.inc.php b/sources/form/field/field.class.inc.php index 1162fd1249..5c0ac1b6b9 100644 --- a/sources/form/field/field.class.inc.php +++ b/sources/form/field/field.class.inc.php @@ -358,6 +358,16 @@ abstract class Field 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() { // Overload when needed diff --git a/sources/form/field/linkedsetfield.class.inc.php b/sources/form/field/linkedsetfield.class.inc.php new file mode 100644 index 0000000000..ed49aa72cb --- /dev/null +++ b/sources/form/field/linkedsetfield.class.inc.php @@ -0,0 +1,154 @@ + + +namespace Combodo\iTop\Form\Field; + +use \Combodo\iTop\Form\Field\Field; + +/** + * Description of LinkedSetField + * + * @author Guillaume Lajarige + */ +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; + } + +} diff --git a/sources/form/field/selectobjectfield.class.inc.php b/sources/form/field/selectobjectfield.class.inc.php index 1f4ba755ef..dd9f707150 100644 --- a/sources/form/field/selectobjectfield.class.inc.php +++ b/sources/form/field/selectobjectfield.class.inc.php @@ -26,14 +26,16 @@ use Combodo\iTop\Form\Validator\NotEmptyExtKeyValidator; /** * Description of SelectObjectField * + * @author Romain Quetiez */ class SelectObjectField extends Field { protected $oSearch; protected $iMaximumComboLength; protected $iMinAutoCompleteChars; - + protected $bHierarchical; protected $iControlType; + protected $sSearchEndpoint; const CONTROL_SELECT = 1; const CONTROL_RADIO_VERTICAL = 2; @@ -44,22 +46,33 @@ class SelectObjectField extends Field $this->oSearch = null; $this->iMaximumComboLength = null; $this->iMinAutoCompleteChars = 3; + $this->bHierarchical = false; $this->iControlType = self::CONTROL_SELECT; + $this->sSearchEndpoint = null; } public function SetSearch(DBSearch $oSearch) { $this->oSearch = $oSearch; + return $this; } public function SetMaximumComboLength($iMaximumComboLength) { $this->iMaximumComboLength = $iMaximumComboLength; + return $this; } public function SetMinAutoCompleteChars($iMinAutoCompleteChars) { $this->iMinAutoCompleteChars = $iMinAutoCompleteChars; + return $this; + } + + public function SetHierarchical($bHierarchical) + { + $this->bHierarchical = $bHierarchical; + return $this; } public function SetControlType($iControlType) @@ -67,6 +80,12 @@ class SelectObjectField extends Field $this->iControlType = $iControlType; } + public function SetSearchEndpoint($sSearchEndpoint) + { + $this->sSearchEndpoint = $sSearchEndpoint; + return $this; + } + /** * Sets if the field is mandatory or not. * Setting the value will automatically add/remove a MandatoryValidator to the Field @@ -112,8 +131,18 @@ class SelectObjectField extends Field return $this->iMinAutoCompleteChars; } + public function GetHierarchical() + { + return $this->bHierarchical; + } + public function GetControlType() { return $this->iControlType; } + + public function GetSearchEndpoint() + { + return $this->sSearchEndpoint; + } } diff --git a/sources/form/field/subformfield.class.inc.php b/sources/form/field/subformfield.class.inc.php index ccc23bdfb6..d33d3a2d42 100644 --- a/sources/form/field/subformfield.class.inc.php +++ b/sources/form/field/subformfield.class.inc.php @@ -118,6 +118,49 @@ class SubFormField extends Field return $this; } + /** + * 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 * @return Form|null diff --git a/sources/form/form.class.inc.php b/sources/form/form.class.inc.php index df11ce2e40..dc5820962f 100644 --- a/sources/form/form.class.inc.php +++ b/sources/form/form.class.inc.php @@ -37,6 +37,7 @@ class Form protected $aDependencies; protected $bValid; protected $aErrorMessages; + protected $iEditableFieldCount; /** * Default constructor @@ -51,6 +52,7 @@ class Form $this->aDependencies = array(); $this->bValid = true; $this->aErrorMessages = array(); + $this->iEditableFieldCount = null; } /** @@ -371,6 +373,39 @@ class Form return $aRes; } + /** + * 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 * @return Form|null @@ -450,7 +485,11 @@ class Form foreach ($aFieldList as $sId => $oField) { $oField->OnFinalize(); - } + if ($oField->IsEditable()) + { + $this->iEditableFieldCount++; + } + } } /** diff --git a/sources/renderer/bootstrap/bsformrenderer.class.inc.php b/sources/renderer/bootstrap/bsformrenderer.class.inc.php index 7f71b8d60c..77a3007d95 100644 --- a/sources/renderer/bootstrap/bsformrenderer.class.inc.php +++ b/sources/renderer/bootstrap/bsformrenderer.class.inc.php @@ -19,6 +19,7 @@ namespace Combodo\iTop\Renderer\Bootstrap; +use \Silex\Application; use \Combodo\iTop\Renderer\FormRenderer; use \Combodo\iTop\Form\Form; @@ -43,12 +44,14 @@ class BsFormRenderer extends FormRenderer $this->AddSupportedField('LabelField', 'BsSimpleFieldRenderer'); $this->AddSupportedField('StringField', 'BsSimpleFieldRenderer'); $this->AddSupportedField('TextAreaField', 'BsSimpleFieldRenderer'); + $this->AddSupportedField('CaseLogField', 'BsSimpleFieldRenderer'); $this->AddSupportedField('SelectField', 'BsSimpleFieldRenderer'); $this->AddSupportedField('MultipleSelectField', 'BsSimpleFieldRenderer'); $this->AddSupportedField('RadioField', 'BsSimpleFieldRenderer'); $this->AddSupportedField('CheckboxField', 'BsSimpleFieldRenderer'); $this->AddSupportedField('SubFormField', 'BsSubFormFieldRenderer'); - $this->AddSupportedField('SelectObjectField', 'BsSimpleFieldRenderer'); + $this->AddSupportedField('SelectObjectField', 'BsSelectObjectFieldRenderer'); + $this->AddSupportedField('LinkedSetField', 'BsLinkedSetFieldRenderer'); } } diff --git a/sources/renderer/bootstrap/fieldrenderer/bslinkedsetfieldrenderer.class.inc.php b/sources/renderer/bootstrap/fieldrenderer/bslinkedsetfieldrenderer.class.inc.php new file mode 100644 index 0000000000..c3c1d2c1e4 --- /dev/null +++ b/sources/renderer/bootstrap/fieldrenderer/bslinkedsetfieldrenderer.class.inc.php @@ -0,0 +1,456 @@ + + +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 + */ +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('
'); + if ($this->oField->GetLabel() !== '') + { + $oOutput->AddHtml(''); + } + $oOutput->AddHtml('
'); + + // Rendering table + // - Vars + $sTableId = 'table_' . $this->oField->GetGlobalId(); + // - Output + $oOutput->AddHtml( +<< +
+
+ + + + +
+
+
+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()) ? '' : ''; + // - Output + $oOutput->AddJs( +<<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 = $(''); + cellElem.attr('target', '_blank').attr('href', row.attributes[data].url); + } + else + { + cellElem = $(''); + } + cellElem.attr('data-object-id', row.id).html('' + row.attributes[data].value + ''); + + 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( +<<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 + ); + + // Rendering table widget + // - Vars + $sAddButtonEndpoint = str_replace('-sMode-', 'from-attribute', $this->oField->GetSearchEndpoint()); + // - Output + $oOutput->AddJs( + <<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(''); + } + + // End of table rendering + $oOutput->AddHtml(''); + $oOutput->AddHtml(''); + + 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('
'); + $oOutput->AddHtml(''); + $oOutput->AddHtml('
'); + + $oOutput->AddJs( +<<oField->GetFormPath()}', + sFieldId: '{$this->oField->GetId()}' + } + ); + oModalElem.modal('show'); + }); +EOF + ); + } + +} diff --git a/sources/renderer/bootstrap/fieldrenderer/bsselectobjectfieldrenderer.class.inc.php b/sources/renderer/bootstrap/fieldrenderer/bsselectobjectfieldrenderer.class.inc.php new file mode 100644 index 0000000000..eec08f3158 --- /dev/null +++ b/sources/renderer/bootstrap/fieldrenderer/bsselectobjectfieldrenderer.class.inc.php @@ -0,0 +1,402 @@ + + +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 + */ +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('
'); + if ($this->oField->GetLabel() !== '') + { + $oOutput->AddHtml(''); + } + $oOutput->AddHtml('
'); + // - 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('
'); + // - Rendering select + $oOutput->AddHtml('
'); + $oOutput->AddHtml(''); + $oOutput->AddHtml('
'); + // - Closing col for autocomplete & opening col for hierarchy, rendering hierarchy button, closing col and row + $oOutput->AddHtml('
'); + $this->RenderHierarchicalSearch($oOutput); + $oOutput->AddHtml('
'); + // - Closing row + $oOutput->AddHtml('
'); + + // JS FieldChange trigger (:input are not always at the same depth) + $oOutput->AddJs( +<<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( +<<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('
'); + // - Rendering autocomplete search + $oOutput->AddHtml('
'); + $oOutput->AddHtml(''); + $oOutput->AddHtml(''); + $oOutput->AddHtml('
'); + // - Rendering buttons + $oOutput->AddHtml('
'); + $oOutput->AddHtml('
'); + // - Rendering hierarchy button + $this->RenderHierarchicalSearch($oOutput); + // - Rendering regular search + $this->RenderRegularSearch($oOutput); + $oOutput->AddHtml('
'); + $oOutput->AddHtml('
'); + // - Closing row + $oOutput->AddHtml('
'); + + // JS FieldChange trigger (:input are not always at the same depth) + // Note : Not used for that field type + // Attaching JS widget + $oOutput->AddJs( +<<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( +<<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('
{{name}}
'), + pending: $("#page_overlay .content_loader").prop('outerHTML'), + notFound: '
{$sNoResultText}
' + } + }) + .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('
'); + } + // ... 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('
'); + // Showing label / value only if read-only but not hidden + if (!$this->oField->GetHidden()) + { + if ($this->oField->GetLabel() !== '') + { + $oOutput->AddHtml(''); + } + $oOutput->AddHtml('
' . $sFieldValue . '
'); + } + $oOutput->AddHtml(''); + $oOutput->AddHtml('
'); + + + // JS FieldChange trigger (:input are not always at the same depth) + $oOutput->AddJs( +<<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( +<<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(''); + + $oOutput->AddJs( +<<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(''); + + $oOutput->AddJs( +<<oField->GetFormPath()}', + sFieldId: '{$this->oField->GetId()}' + } + ); + oModalElem.modal('show'); + }); +EOF + ); + } + +} diff --git a/sources/renderer/bootstrap/fieldrenderer/bssimplefieldrenderer.class.inc.php b/sources/renderer/bootstrap/fieldrenderer/bssimplefieldrenderer.class.inc.php index a59cd85506..837c4f4956 100644 --- a/sources/renderer/bootstrap/fieldrenderer/bssimplefieldrenderer.class.inc.php +++ b/sources/renderer/bootstrap/fieldrenderer/bssimplefieldrenderer.class.inc.php @@ -46,7 +46,6 @@ class BsSimpleFieldRenderer extends FieldRenderer $sFieldClass = get_class($this->oField); $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 if (!$this->oField->GetReadOnly() && !$this->oField->GetHidden()) { @@ -64,15 +63,59 @@ class BsSimpleFieldRenderer extends FieldRenderer break; case 'Combodo\\iTop\\Form\\Field\\TextAreaField': + case 'Combodo\\iTop\\Form\\Field\\CaseLogField': $bRichEditor = ($this->oField->GetFormat() === TextAreaField::ENUM_FORMAT_HTML); - + $oOutput->AddHtml('
'); if ($this->oField->GetLabel() !== '') { $oOutput->AddHtml(''); } $oOutput->AddHtml('
'); + // First the edition area + $oOutput->AddHtml('
'); $oOutput->AddHtml(''); + $oOutput->AddHtml('
'); + // Then the previous entries if necessary + if ($sFieldClass === 'Combodo\\iTop\\Form\\Field\\CaseLogField') + { + $aEntries = $this->oField->GetEntries(); + if (count($aEntries) > 0) + { + $oOutput->AddHtml('
'); + 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( +<< +
+ {$sEntryHeader} +
+ +
+
+
+ {$aEntries[$i]['message_html']} +
+
+EOF + ); + } + $oOutput->AddHtml('
'); + } + } + $oOutput->AddHtml(''); // Some additional stuff if we are displaying it with a rich editor if ($bRichEditor) @@ -145,7 +188,7 @@ EOF // ... and in read-only mode (or hidden) 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())) { // TODO @@ -190,7 +233,6 @@ EOF case 'Combodo\\iTop\\Form\\Field\\RadioField': case 'Combodo\\iTop\\Form\\Field\\SelectField': 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(); $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\\TextAreaField': + case 'Combodo\\iTop\\Form\\Field\\CaseLogField': case 'Combodo\\iTop\\Form\\Field\\SelectField': case 'Combodo\\iTop\\Form\\Field\\MultipleSelectField': - case 'Combodo\\iTop\\Form\\Field\\SelectObjectField': case 'Combodo\\iTop\\Form\\Field\\HiddenField': $oOutput->AddJs( <<AddJs( <<oField->GetLabel() !== null) && ($this->oField->GetLabel() !== '')) { $oOutput->AddHtml('
' . $this->oField->GetLabel() . ''); diff --git a/sources/renderer/fieldrenderer.class.inc.php b/sources/renderer/fieldrenderer.class.inc.php index 9f59975f28..bf279b867f 100644 --- a/sources/renderer/fieldrenderer.class.inc.php +++ b/sources/renderer/fieldrenderer.class.inc.php @@ -19,6 +19,7 @@ namespace Combodo\iTop\Renderer; +use \Dict; use \DBObject; use \Combodo\iTop\Form\Field\Field; @@ -61,6 +62,32 @@ abstract class FieldRenderer return $this; } + /** + * 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 *