N°636 Portal: Action buttons can now be added to object details page through the iPopupMenuItemExtension

SVN:trunk[4600]
This commit is contained in:
Guillaume Lajarige
2017-03-17 13:03:09 +00:00
parent cf0792cd64
commit 95e04178ea
8 changed files with 214 additions and 19 deletions

View File

@@ -308,6 +308,37 @@ interface iPopupMenuExtension
* $param is null
*/
const MENU_USER_ACTIONS = 5;
/**
* Insert an item into the Action menu on an object details page in the portal
*
* $param is an array('portal_id' => $sPortalId, 'object' => $oObject) containing the portal id and a DBObject instance (the object currently displayed)
*/
const PORTAL_OBJDETAILS_ACTIONS = 7;
/**
* Insert an item into the Actions menu of a list in the portal
* Note: This is not implemented yet !
*
* $param is an array('portal_id' => $sPortalId, 'object_set' => $oSet) containing DBObjectSet containing the list of objects
* @todo
*/
const PORTAL_OBJLIST_ACTIONS = 6;
/**
* Insert an item into the user menu of the portal
* Note: This is not implemented yet !
*
* $param is the portal id
* @todo
*/
const PORTAL_USER_ACTIONS = 8;
/**
* Insert an item into the navigation menu of the portal
* Note: This is not implemented yet !
*
* $param is the portal id
* @todo
*/
const PORTAL_MENU_ACTIONS = 9;
/**
* Get the list of items to be added to a menu.
@@ -334,17 +365,21 @@ abstract class ApplicationPopupMenuItem
protected $sUID;
/** @ignore */
protected $sLabel;
/** @ignore */
protected $aCssClasses;
/**
* Constructor
*
* @param string $sUID The unique identifier of this menu in iTop... make sure you pass something unique enough
* @param string $sLabel The display label of the menu (must be localized)
*/
* @param string $sLabel The display label of the menu (must be localized)
* @param array $aCssClasses The CSS classes to add to the menu
*/
public function __construct($sUID, $sLabel)
{
$this->sUID = $sUID;
$this->sLabel = $sLabel;
$this->aCssClasses = array();
}
/**
@@ -368,6 +403,35 @@ abstract class ApplicationPopupMenuItem
{
return $this->sLabel;
}
/**
* Get the CSS classes
*
* @return array
* @ignore
*/
public function GetCssClasses()
{
return $this->aCssClasses;
}
/**
* @param $aCssClasses
*/
public function SetCssClasses($aCssClasses)
{
$this->aCssClasses = $aCssClasses;
}
/**
* Adds a CSS class to the CSS classes that will be put on the menu item
*
* @param $sCssClass
*/
public function AddCssClass($sCssClass)
{
$this->aCssClasses[] = $sCssClass;
}
/**
* Returns the components to create a popup menu item in HTML
@@ -415,7 +479,7 @@ class URLPopupMenuItem extends ApplicationPopupMenuItem
/** @ignore */
public function GetMenuItem()
{
return array ('label' => $this->GetLabel(), 'url' => $this->sURL, 'target' => $this->sTarget);
return array ('label' => $this->GetLabel(), 'url' => $this->sURL, 'target' => $this->sTarget, 'css_classes' => $this->aCssClasses);
}
}
@@ -451,7 +515,7 @@ class JSPopupMenuItem extends ApplicationPopupMenuItem
public function GetMenuItem()
{
// Note: the semicolumn is a must here!
return array ('label' => $this->GetLabel(), 'onclick' => $this->sJSCode.'; return false;', 'url' => '#');
return array ('label' => $this->GetLabel(), 'onclick' => $this->sJSCode.'; return false;', 'url' => '#', 'css_classes' => $this->aCssClasses);
}
/** @ignore */
@@ -483,10 +547,34 @@ class SeparatorPopupMenuItem extends ApplicationPopupMenuItem
/** @ignore */
public function GetMenuItem()
{
return array ('label' => '<hr class="menu-separator">', 'url' => '');
return array ('label' => '<hr class="menu-separator">', 'url' => '', 'css_classes' => $this->aCssClasses);
}
}
/**
* Class for adding an item as a button that browses to the given URL
*
* @package Extensibility
* @api
* @since 2.0
*/
class URLButtonItem extends URLPopupMenuItem
{
}
/**
* Class for adding an item as a button that runs some JS code
*
* @package Extensibility
* @api
* @since 2.0
*/
class JSButtonItem extends JSPopupMenuItem
{
}
/**
* Implement this interface to add content to any iTopWebPage
*

View File

@@ -734,7 +734,7 @@ class WebPage implements Page
{
foreach ($aActions as $aAction)
{
$sClass = isset($aAction['class']) ? " class=\"{$aAction['class']}\"" : "";
$sClass = isset($aAction['css_classes']) ? ' class="'.implode(' ', $aAction['css_classes']).'"' : '';
$sOnClick = isset($aAction['onclick']) ? ' onclick="'.htmlspecialchars($aAction['onclick'], ENT_QUOTES, "UTF-8").'"' : '';
$sTarget = isset($aAction['target']) ? " target=\"{$aAction['target']}\"" : "";
if (empty($aAction['url']))

View File

@@ -43,6 +43,9 @@ use \cmdbAbstractObject;
use \AttributeEnum;
use \AttributeFinalClass;
use \UserRights;
use \iPopupMenuExtension;
use \URLButtonItem;
use \JSButtonItem;
use \Combodo\iTop\Portal\Helper\ApplicationHelper;
use \Combodo\iTop\Portal\Helper\SecurityHelper;
use \Combodo\iTop\Portal\Helper\ContextManipulatorHelper;
@@ -101,10 +104,13 @@ class ObjectController extends AbstractController
// Add an edit button if user is allowed
if (SecurityHelper::IsActionAllowed($oApp, UR_ACTION_MODIFY, $sObjectClass, $sObjectId))
{
$aData['form']['buttons']['links'][] = array(
'label' => Dict::S('UI:Menu:Modify'),
'url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId))
);
$oModifyButton = new URLButtonItem(
'modify_object',
Dict::S('UI:Menu:Modify'),
$oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId))
);
// Putting this one first
$aData['form']['buttons']['links'][] = $oModifyButton->GetMenuItem();
}
// Preparing response
@@ -467,7 +473,9 @@ class ObjectController extends AbstractController
// Preparing transitions only if we are currently going through one
$aFormData['buttons'] = array(
'transitions' => array()
'transitions' => array(),
'actions' => array(),
'links' => array(),
);
if ($sMode !== 'apply_stimulus')
{
@@ -482,6 +490,25 @@ class ObjectController extends AbstractController
$aFormData['buttons']['transitions'][$sStimulusCode] = $aStimuli[$sStimulusCode]->GetLabel();
}
}
// Add plugins buttons
foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance)
{
foreach($oExtensionInstance->EnumItems(iPopupMenuExtension::PORTAL_OBJDETAILS_ACTIONS, array('portal_id' => $oApp['combodo.portal.instance.id'], 'object' => $oObject)) as $oMenuItem)
{
if (is_object($oMenuItem))
{
if($oMenuItem instanceof JSButtonItem)
{
$aFormData['buttons']['actions'][] = $oMenuItem->GetMenuItem() + array('js_files' => $oMenuItem->GetLinkedScripts());
}
elseif($oMenuItem instanceof URLButtonItem)
{
$aFormData['buttons']['links'][] = $oMenuItem->GetMenuItem();
}
}
}
}
}
// Preparing callback urls
$aCallbackUrls = $oApp['context_manipulator']->GetCallbackUrls($oApp, $aActionRules, $oObject, $bModal);
@@ -1499,5 +1526,3 @@ class ObjectController extends AbstractController
}
}
?>

View File

@@ -21,6 +21,13 @@
</div>
<div class="form_buttons">
{% block pFormButtons %}
{# Misc. buttons #}
{% if form.buttons is defined and (form.buttons.actions is defined or form.buttons.links is defined) %}
<div class="form_btn_misc">
{% include 'itop-portal-base/portal/src/views/bricks/object/plugins_buttons.html.twig' with {'aButtons': form.buttons} %}
</div>
{% endif %}
{# Transition buttons #}
{% if form.buttons is defined and form.buttons.transitions is defined and form.buttons.transitions|length > 0 %}
<div class="form_btn_transitions">
{% for sStimulusCode, sStimulusLabel in form.buttons.transitions %}
@@ -29,6 +36,7 @@
</div>
{% endif %}
<div class="form_btn_regular">
{# If form has editable fields, we display cancel / submit buttons #}
{% if form.editable_fields_count is defined and form.editable_fields_count > 0 %}
<button class="btn btn-default form_btn_cancel" type="button" value="cancel" title="{{ 'Portal:Button:Cancel'|dict_s }}" data-dismiss="modal">
<span class="glyphicon glyphicon-remove"></span>
@@ -39,6 +47,7 @@
{{ 'Portal:Button:Submit'|dict_s }}
</button>
{% else %}
{# Modal mode #}
{% if tIsModal %}
<input class="btn btn-default form_btn_cancel" type="button" value="{{ 'Portal:Button:Close'|dict_s }}" data-dismiss="modal">
{% endif %}

View File

@@ -5,16 +5,16 @@
{# This layout is exactly the same as the mode_create.html.twig, we duplicated it in case we need to have some subtle differences #}
{% block pFormButtons %}
{% if form.buttons is defined and form.buttons.links is defined %}
<div class="form_btn_transitions">
{% for aLink in form.buttons.links %}
<a class="btn btn-primary" href="{{ aLink.url }}">{{ aLink.label }}</a>
{% endfor %}
{# Misc. buttons #}
{% if form.buttons is defined and (form.buttons.actions is defined or form.buttons.links is defined) %}
<div class="form_btn_misc">
{% include 'itop-portal-base/portal/src/views/bricks/object/plugins_buttons.html.twig' with {'aButtons': form.buttons} %}
</div>
{% endif %}
{% if tIsModal is defined and tIsModal == true %}
<div class="form_btn_regular">
<input class="btn btn-primary form_btn_cancel" type="button" value="{{ 'Portal:Button:Close'|dict_s }}" data-dismiss="modal">
<input class="btn btn-primary form_btn_cancel" type="button" value="{{ 'Portal:Button:Close'|dict_s }}" data-dismiss="modal" />
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,61 @@
{% set iLinkButtonsCount = (aButtons.links is defined) ? aButtons.links|length : 0 %}
{% set iActionButtonsCount = (aButtons.actions is defined) ? aButtons.actions|length : 0 %}
{% set iButtonsCount = iLinkButtonsCount + iActionButtonsCount %}
{% set iButtonsGroupThreshold = 2 %}
{% set bGroupButtons = (iButtonsCount > iButtonsGroupThreshold) ? true : false %}
{% set sButtonCssClasses = (bGroupButtons) ? '' : 'btn btn-default' %}
{% set aJSFiles = [] %}
{% if bGroupButtons == true %}
<div class="dropup">
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ 'UI:Menu:OtherActions'|dict_s }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% endif %}
{# JSs buttons #}
{% if aButtons.actions is defined %}
{% for aButton in aButtons.actions %}
{# Adding button #}
{% if bGroupButtons == true %}
<li>
{% endif %}
<a class="{{ sButtonCssClasses }} {{ aButton.css_classes|join(' ') }}" href="{{ aButton.url }}" onclick="{{ aButton.onclick }}">{{ aButton.label }}</a>
{% if bGroupButtons == true %}
</li>
{% endif %}
{# Preparing JS files to load #}
{% set aJSFiles = aJSFiles|merge(aButton.js_files) %}
{% endfor %}
{% endif %}
{# URLs buttons #}
{% if aButtons.links is defined %}
{% for aButton in aButtons.links %}
{% if bGroupButtons == true %}
<li>
{% endif %}
<a class="{{ sButtonCssClasses }} {{ aButton.css_classes|join(' ') }}" href="{{ aButton.url }}" target="{{ aButton.target }}">{{ aButton.label }}</a>
{% if bGroupButtons == true %}
</li>
{% endif %}
{% endfor %}
{% endif %}
{% if bGroupButtons == true %}
</ul>
</div>
{% endif %}
{# Loading JS files #}
{% if aJSFiles|length > 0 %}
<script type="text/javascript">
{% for sJSFile in aJSFiles %}
console.log('loading {{ sJSFile }}');
$.getScript('{{ sJSFile }}');
{% endfor %}
</script>
{% endif %}

View File

@@ -1056,6 +1056,9 @@ table .group-actions {
padding-top: 20px;
text-align: center;
}
.form_buttons .form_btn_misc {
margin-bottom: 20px;
}
.form_buttons .form_btn_transitions {
margin-bottom: 20px;
}
@@ -1067,6 +1070,9 @@ table .group-actions {
}
@media (min-width: 768px) {
/* Making regular button sticky */
.form_buttons .form_btn_misc {
float: left !important;
}
.form_buttons .form_btn_transitions {
float: right !important;
margin-left: 3px;

View File

@@ -1126,6 +1126,9 @@ table .group-actions .item-action-wrapper .panel-body > p:last-child{
padding-top: 20px;
text-align: center;
}
.form_buttons .form_btn_misc {
margin-bottom: 20px;
}
.form_buttons .form_btn_transitions{
margin-bottom: 20px;
}
@@ -1136,6 +1139,9 @@ table .group-actions .item-action-wrapper .panel-body > p:last-child{
display: none;
}
@media (min-width: 768px){
.form_buttons .form_btn_misc{
float: left !important;
}
.form_buttons .form_btn_transitions{
float: right !important;
margin-left: 3px;