diff --git a/datamodels/2.x/itop-portal-base/portal/public/js/bootstrap-portal-modal.js b/datamodels/2.x/itop-portal-base/portal/public/js/bootstrap-portal-modal.js new file mode 100644 index 000000000..e2fc81d7e --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/public/js/bootstrap-portal-modal.js @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2013-2019 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 + * + * + */ + +/** + * Creates a Bootstrap modal dialog from a base modal element (template or reusable one) and loads the content while displaying a nice loader. + * + * Technical: We made this to work around the base modal interactions as it was not possible to define a loader, nor to clone the base modal natively. + * + * @param oOptions + * @constructor + * @since 2.7.0 + */ +var CreatePortalModal = function (oOptions) +{ + // Set default options + oOptions = $.extend( + true, + { + id: null, // ID of the created modal + base_modal: { + usage: 'clone', // Either 'clone' or 'replace' + selector: '#modal-for-all' // Selector of the modal element used to base this one on + }, + content: '', // Either a string or an object containing the endpoint / data + size: 'lg', // Either 'xs' / 'sm' / 'md' / 'lg' + }, + oOptions + ); + + // Get modal element by either + var oModalElem = null; + // - Create a new modal from template + // Note : This could be better if we check for an existing modal first instead of always creating a new one + if (oOptions.base_modal.usage === 'clone') + { + oModalElem = $(oOptions.base_modal.selector).clone(); + oModalElem.attr('id', oOptions.id) + .appendTo('body'); + } + // - Get an existing modal in the DOM + else + { + oModalElem = $(oOptions.base_modal.selector); + } + + // Resize to small modal + oModalElem.find('.modal-dialog') + .removeClass('modal-lg') + .addClass('modal-' + oOptions.size); + + // Load content + switch (typeof oOptions.content) + { + case 'string': + oModalElem.find('.modal-content').html(oOptions.content); + break; + + case 'object': + // Put loader while fetching content + oModalElem.find('.modal-content').html($('#page_overlay .overlay_content').html()); + // Fetch content in background + oModalElem.find('.modal-content').load( + oOptions.content.endpoint, + oOptions.content.data || {}, + function (sResponseText, sStatus) + { + // Hiding modal in case of error as the general AJAX error handler will display a message + if (sStatus === 'error') + { + oModalElem.modal('hide'); + } + } + ); + break; + + default: + if (window.console) + { + console.log('Could not open modal dialog as the content option was malformed: ', oOptions.content); + } + } + + // Show modal + oModalElem.modal('show'); +}; + +$(document).ready(function() +{ + var oBodyElem = $('body'); + + // Hack to enable a same modal to load content from different urls + oBodyElem.on('hidden.bs.modal', '.modal#modal-for-all', function () + { + $(this).removeData('bs.modal'); + $(this).find('.modal-content').html(GetContentLoaderTemplate()); + }); + + // Hide tooltips when a modal is opening, otherwise it might be overlapping it + oBodyElem.on('show.bs.modal', function () + { + $(this).find('.tooltip.in').tooltip('hide'); + }); + + // Display a error message on modal if the content could not be loaded. + // Note : As of now, we can't display a more detailled message based on the response because Bootstrap doesn't pass response data with the loaded event. + oBodyElem.on('loaded.bs.modal', function (oEvent) + { + var sModalContent = $(oEvent.target).find('.modal-content').html(); + + if ((sModalContent === '') || (sModalContent.replace(/[\n\r\t]+/g, '') === GetContentLoaderTemplate())) + { + $(oEvent.target).modal('hide'); + } + }); + + /** + * Set a listener on the BS modal DATA-API for modals with a custom "itop-portal-modal" toggle. + * This allows us to call our custom handler above and still use the lightest way to instantiate modal: Markup only, no JS + */ + $(document).on('click.bs.modal.data-api', '[data-toggle="itop-portal-modal"]', function (oEvent) + { + if ($(this).is('a')) + { + oEvent.preventDefault(); + } + + // Prepare base options + var oOptions = { + content: { + endpoint: $(this).attr('href') + } + }; + + // Add target modal if necessary + if ($(this).attr('data-target') !== undefined) + { + oOptions.base_modal = { + usage: 'clone', + selector: $(this).attr('data-target') + }; + } + + CreatePortalModal(oOptions); + }); +}); \ No newline at end of file diff --git a/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php b/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php index 4317d7ee7..01b8c0e47 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php +++ b/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php @@ -29,6 +29,7 @@ use CMDBSource; use Combodo\iTop\Form\Field\Field; use Combodo\iTop\Form\Field\FileUploadField; use Combodo\iTop\Form\Field\LabelField; +use Combodo\iTop\Form\Field\SelectObjectField; use Combodo\iTop\Form\Form; use Combodo\iTop\Form\FormManager; use Combodo\iTop\Portal\Helper\ApplicationHelper; @@ -826,6 +827,20 @@ class ObjectFormManager extends FormManager else { $oField->SetReadOnly(true); + + // Specific operation on field + // - SelectObjectField + if ($oField instanceof SelectObjectField) + { + // - Set if remote object can be accessed + if ($this->oContainer !== null && !$oAttDef->IsNull($oField->GetCurrentValue())) + { + $sRemoteObjectFieldClass = $oField->GetSearch()->GetClass(); + $sRemoteObjectFieldId = $oField->GetCurrentValue(); + $bIsRemoteObjectReadAllowed = $this->oContainer->get('security_helper')->IsActionAllowed(UR_ACTION_READ, $sRemoteObjectFieldClass, $sRemoteObjectFieldId); + $oField->SetRemoteObjectAccessible($bIsRemoteObjectReadAllowed); + } + } } } diff --git a/datamodels/2.x/itop-portal-base/portal/templates/layout.html.twig b/datamodels/2.x/itop-portal-base/portal/templates/layout.html.twig index 076d88881..0f250bd56 100644 --- a/datamodels/2.x/itop-portal-base/portal/templates/layout.html.twig +++ b/datamodels/2.x/itop-portal-base/portal/templates/layout.html.twig @@ -96,6 +96,7 @@ + {# Visible.js to check if an element is visible on screen #} @@ -455,26 +456,6 @@ $(document).ready(function(){ {% block pPageReadyScripts %} - // Hack to enable a same modal to load content from different urls - $('body').on('hidden.bs.modal', '.modal#modal-for-all', function () { - $(this).removeData('bs.modal'); - $(this).find('.modal-content').html(GetContentLoaderTemplate()); - }); - // Hide tooltips when a modal is opening, otherwise it might be overlapping it - $('body').on('show.bs.modal', function () { - $(this).find('.tooltip.in').tooltip('hide'); - }); - // Display a error message on modal if the content could not be loaded. - // Note : As of now, we can't display a more detailled message based on the response because Bootstrap doesn't pass response data with the loaded event. - $('body').on('loaded.bs.modal', function (oEvent) { - var sModalContent = $(oEvent.target).find('.modal-content').html(); - - if( (sModalContent === '') || (sModalContent.replace(/[\n\r\t]+/g, '') === GetContentLoaderTemplate()) ) - { - $(oEvent.target).modal('hide'); - } - }); - // Handle AJAX errors (exceptions (500), logout (401), ...) $(document).ajaxError(function(oEvent, oXHR, oSettings, sError){ if(oXHR.status === 401) diff --git a/sources/autoload.php b/sources/autoload.php index 612f727c5..7ecdc8c82 100644 --- a/sources/autoload.php +++ b/sources/autoload.php @@ -42,6 +42,7 @@ 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/remoteobjectfield.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'; diff --git a/sources/form/field/remoteobjectfield.class.inc.php b/sources/form/field/remoteobjectfield.class.inc.php new file mode 100644 index 000000000..747f3126c --- /dev/null +++ b/sources/form/field/remoteobjectfield.class.inc.php @@ -0,0 +1,66 @@ + + * @since 2.7.0 + */ +abstract class RemoteObjectField extends Field +{ + /** @var bool DEFAULT_IS_REMOTE_OBJECT_ACCESSIBLE */ + const DEFAULT_IS_REMOTE_OBJECT_ACCESSIBLE = true; + + /** @var boolean $bIsRemoteObjectAccessible */ + protected $bIsRemoteObjectAccessible; + + /** + * @inheritDoc + */ + public function __construct($sId, Closure $onFinalizeCallback = null) + { + parent::__construct($sId, $onFinalizeCallback); + $this->bIsRemoteObjectAccessible = static::DEFAULT_IS_REMOTE_OBJECT_ACCESSIBLE; + } + + /** + * Return true if the remote object pointed by this field is accessible + * + * @return boolean + */ + public function GetRemoteObjectAccessible() + { + return $this->bIsRemoteObjectAccessible; + } + + /** + * @param boolean $bIsRemoteObjectAccessible + */ + public function SetRemoteObjectAccessible($bIsRemoteObjectAccessible) + { + $this->bIsRemoteObjectAccessible = $bIsRemoteObjectAccessible; + } +} \ No newline at end of file diff --git a/sources/form/field/selectobjectfield.class.inc.php b/sources/form/field/selectobjectfield.class.inc.php index d4f3b90e8..6204b606a 100644 --- a/sources/form/field/selectobjectfield.class.inc.php +++ b/sources/form/field/selectobjectfield.class.inc.php @@ -19,20 +19,20 @@ namespace Combodo\iTop\Form\Field; -use Closure; -use DBSearch; -use DBObjectSet; use BinaryExpression; +use Closure; +use Combodo\iTop\Form\Validator\NotEmptyExtKeyValidator; +use DBObjectSet; +use DBSearch; use FieldExpression; use ScalarExpression; -use Combodo\iTop\Form\Validator\NotEmptyExtKeyValidator; /** * Description of SelectObjectField * * @author Romain Quetiez */ -class SelectObjectField extends Field +class SelectObjectField extends RemoteObjectField { protected $oSearch; protected $iMaximumComboLength; @@ -58,24 +58,28 @@ class SelectObjectField extends Field 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; } @@ -87,6 +91,7 @@ class SelectObjectField extends Field public function SetSearchEndpoint($sSearchEndpoint) { $this->sSearchEndpoint = $sSearchEndpoint; + return $this; } @@ -117,12 +122,13 @@ class SelectObjectField extends Field } $this->bMandatory = $bMandatory; + return $this; } - /** - * @return \DBSearch - */ + /** + * @return \DBSearch + */ public function GetSearch() { return $this->oSearch; @@ -153,14 +159,14 @@ class SelectObjectField extends Field return $this->sSearchEndpoint; } - /** - * Resets current value is not among allowed ones. - * By default, reset is done ONLY when the field is not read-only. - * - * @param boolean $bAlways Set to true to verify even when the field is read-only. - * - * @throws \CoreException - */ + /** + * Resets current value is not among allowed ones. + * By default, reset is done ONLY when the field is not read-only. + * + * @param boolean $bAlways Set to true to verify even when the field is read-only. + * + * @throws \CoreException + */ public function VerifyCurrentValue($bAlways = false) { if(!$this->GetReadOnly() || $bAlways) diff --git a/sources/renderer/bootstrap/fieldrenderer/bsselectobjectfieldrenderer.class.inc.php b/sources/renderer/bootstrap/fieldrenderer/bsselectobjectfieldrenderer.class.inc.php index cc7fcf408..3e0f7de04 100644 --- a/sources/renderer/bootstrap/fieldrenderer/bsselectobjectfieldrenderer.class.inc.php +++ b/sources/renderer/bootstrap/fieldrenderer/bsselectobjectfieldrenderer.class.inc.php @@ -1,24 +1,28 @@ +/** + * Copyright (C) 2013-2019 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 + * + * + */ namespace Combodo\iTop\Renderer\Bootstrap\FieldRenderer; +use ApplicationContext; use Combodo\iTop\Renderer\FieldRenderer; use Combodo\iTop\Renderer\RenderingOutput; use ContextTag; @@ -33,23 +37,25 @@ use MetaModel; * Description of BsSelectObjectFieldRenderer * * @author Guillaume Lajarige + * + * @property \Combodo\iTop\Form\Field\SelectObjectField $oField */ class BsSelectObjectFieldRenderer extends FieldRenderer { - /** - * Returns a RenderingOutput for the FieldRenderer's Field - * - * @return \Combodo\iTop\Renderer\RenderingOutput - * - * @throws \Exception - * @throws \CoreException - * @throws \ArchivedObjectException - */ + /** + * Returns a RenderingOutput for the FieldRenderer's Field + * + * @return \Combodo\iTop\Renderer\RenderingOutput + * + * @throws \Exception + * @throws \CoreException + * @throws \ArchivedObjectException + */ public function Render() { $oOutput = new RenderingOutput(); - $oOutput->AddCssClass('form_field_' . $this->oField->GetDisplayMode()); + $oOutput->AddCssClass('form_field_' . $this->oField->GetDisplayMode()); $sFieldValueClass = $this->oField->GetSearch()->GetClass(); $sFieldMandatoryClass = ($this->oField->GetMandatory()) ? 'form_mandatory' : ''; @@ -61,26 +67,26 @@ class BsSelectObjectFieldRenderer extends FieldRenderer // Rendering field in edition mode if (!$this->oField->GetReadOnly() && !$this->oField->GetHidden()) { - // Debug trace: This is very useful when this kind of field doesn't return the expected values. - if(ContextTag::Check('debug')) - { - IssueLog::Info('Form field #'.$this->oField->GetId().' OQL query: '.$this->oField->GetSearch()->ToOQL(true)); - } + // Debug trace: This is very useful when this kind of field doesn't return the expected values. + if(ContextTag::Check('debug')) + { + IssueLog::Info('Form field #'.$this->oField->GetId().' OQL query: '.$this->oField->GetSearch()->ToOQL(true)); + } // Rendering field - // - Opening container + // - Opening container $oOutput->AddHtml('
'); // Label - $oOutput->AddHtml('
'); + $oOutput->AddHtml('
'); if ($this->oField->GetLabel() !== '') { $oOutput->AddHtml(''); } - $oOutput->AddHtml('
'); + $oOutput->AddHtml('
'); - // Value - $oOutput->AddHtml('
'); + // Value + $oOutput->AddHtml('
'); $oOutput->AddHtml('
'); // - As a select // TODO : This should be changed when we do the radio button display. For now we display everything with select @@ -154,10 +160,10 @@ EOF $sAutocompleteFieldId = 's_ac_' . $this->oField->GetGlobalId(); $sEndpoint = str_replace('-sMode-', 'autocomplete', $this->oField->GetSearchEndpoint()); $sNoResultText = Dict::S('Portal:Autocomplete:NoResult'); - + // Retrieving field value - $currentValue = $this->oField->GetCurrentValue(); - if (!empty($currentValue)) + $currentValue = $this->oField->GetCurrentValue(); + if (!empty($currentValue)) { try { @@ -177,18 +183,18 @@ EOF } // HTML for autocomplete part - // - Opening input group - $oOutput->AddHtml('
'); - // - Rendering autocomplete search - $oOutput->AddHtml(''); - $oOutput->AddHtml(''); - // - Rendering buttons - // - Rendering hierarchy button - $this->RenderHierarchicalSearch($oOutput); - // - Rendering regular search - $this->RenderRegularSearch($oOutput); - // - Closing input group - $oOutput->AddHtml('
'); + // - Opening input group + $oOutput->AddHtml('
'); + // - Rendering autocomplete search + $oOutput->AddHtml(''); + $oOutput->AddHtml(''); + // - Rendering buttons + // - Rendering hierarchy button + $this->RenderHierarchicalSearch($oOutput); + // - Rendering regular search + $this->RenderRegularSearch($oOutput); + // - Closing input group + $oOutput->AddHtml('
'); // JS FieldChange trigger (:input are not always at the same depth) // Note : Not used for that field type @@ -309,9 +315,9 @@ EOF ); } } - $oOutput->AddHtml('
'); + $oOutput->AddHtml('
'); - // - Closing container + // - Closing container $oOutput->AddHtml('
'); } // ... and in read-only mode (or hidden) @@ -322,11 +328,16 @@ EOF { // Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated $oFieldValue = MetaModel::GetObject($sFieldValueClass, $this->oField->GetCurrentValue(), true, true); - $sFieldValue = $oFieldValue->GetName(); + $sFieldHtmlValue = $oFieldValue->GetName(); + if ($this->oField->GetRemoteObjectAccessible()) + { + $sFieldUrl = ApplicationContext::MakeObjectUrl($sFieldValueClass, $this->oField->GetCurrentValue()); + $sFieldHtmlValue = ''.$sFieldHtmlValue.''; + } } else { - $sFieldValue = Dict::S('UI:UndefinedObject'); + $sFieldHtmlValue = Dict::S('UI:UndefinedObject'); } // Opening container @@ -335,18 +346,18 @@ EOF // Showing label / value only if read-only but not hidden if (!$this->oField->GetHidden()) { - // Label - $oOutput->AddHtml('
'); + // Label + $oOutput->AddHtml('
'); if ($this->oField->GetLabel() !== '') { $oOutput->AddHtml(''); } - $oOutput->AddHtml('
'); + $oOutput->AddHtml('
'); - // Value - $oOutput->AddHtml('
'); - $oOutput->AddHtml('
' . $sFieldValue . '
'); - $oOutput->AddHtml('
'); + // Value + $oOutput->AddHtml('
'); + $oOutput->AddHtml('
'.$sFieldHtmlValue.'
'); + $oOutput->AddHtml('
'); } // Adding hidden value @@ -366,8 +377,8 @@ EOF */ protected function RenderHierarchicalSearch(RenderingOutput &$oOutput) { - if ($this->oField->GetHierarchical()) - { + if ($this->oField->GetHierarchical()) + { $sHierarchicalButtonId = 's_hi_' . $this->oField->GetGlobalId(); $sEndpoint = str_replace('-sMode-', 'hierarchy', $this->oField->GetSearchEndpoint()); @@ -415,7 +426,7 @@ EOF $sSearchButtonId = 's_rg_' . $this->oField->GetGlobalId(); $sEndpoint = str_replace('-sMode-', 'from-attribute', $this->oField->GetSearchEndpoint()); - $oOutput->AddHtml('
'); + $oOutput->AddHtml('
'); $oOutput->AddJs( <<