Browse Source

N°952 Portal: Added UI extension APIs similar to those used in the console (Experimental!)

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@4852 a333f486-631f-4898-b8df-5754b55c2be0
glajarige 7 years ago
parent
commit
5047904c50

+ 131 - 3
application/applicationextension.inc.php

@@ -309,11 +309,17 @@ interface iPopupMenuExtension
 	 */
 	 */
 	const MENU_USER_ACTIONS = 5;
 	const MENU_USER_ACTIONS = 5;
     /**
     /**
+     * Insert an item into the Action menu on an object item in an objects list in the portal
+     *
+     * $param is an array('portal_id' => $sPortalId, 'object' => $oObject) containing the portal id and a DBObject instance (the object on the current line)
+     */
+    const PORTAL_OBJLISTITEM_ACTIONS = 7;
+    /**
      * Insert an item into the Action menu on an object details page in the portal
      * 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)
      * $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;
+	const PORTAL_OBJDETAILS_ACTIONS = 8;
 
 
     /**
     /**
      * Insert an item into the Actions menu of a list in the portal
      * Insert an item into the Actions menu of a list in the portal
@@ -330,7 +336,7 @@ interface iPopupMenuExtension
      * $param is the portal id
      * $param is the portal id
      * @todo
      * @todo
      */
      */
-    const PORTAL_USER_ACTIONS = 8;
+    const PORTAL_USER_ACTIONS = 9;
     /**
     /**
      * Insert an item into the navigation menu of the portal
      * Insert an item into the navigation menu of the portal
      * Note: This is not implemented yet !
      * Note: This is not implemented yet !
@@ -338,7 +344,7 @@ interface iPopupMenuExtension
      * $param is the portal id
      * $param is the portal id
      * @todo
      * @todo
      */
      */
-    const PORTAL_MENU_ACTIONS = 9;
+    const PORTAL_MENU_ACTIONS = 10;
 
 
 	/**
 	/**
 	 * Get the list of items to be added to a menu.
 	 * Get the list of items to be added to a menu.
@@ -617,6 +623,128 @@ interface iPageUIExtension
 }
 }
 
 
 /**
 /**
+ * Implement this interface to add content to any enhanced portal page
+ *
+ * IMPORTANT! Experimental API, may be removed at anytime, we don't recommend to use it just now!
+ *
+ * @package     Extensibility
+ * @api
+ * @since 2.4
+ */
+interface iPortalUIExtension
+{
+    const ENUM_PORTAL_EXT_UI_BODY = 'Body';
+    const ENUM_PORTAL_EXT_UI_NAVIGATION_MENU = 'NavigationMenu';
+    const ENUM_PORTAL_EXT_UI_MAIN_CONTENT = 'MainContent';
+
+    /**
+     * Returns an array of CSS file urls
+     *
+     * @param \Silex\Application $oApp
+     * @return array
+     */
+    public function GetCSSFiles(\Silex\Application $oApp);
+    /**
+     * Returns inline (raw) CSS
+     *
+     * @param \Silex\Application $oApp
+     * @return string
+     */
+    public function GetCSSInline(\Silex\Application $oApp);
+    /**
+     * Returns an array of JS file urls
+     *
+     * @param \Silex\Application $oApp
+     * @return array
+     */
+    public function GetJSFiles(\Silex\Application $oApp);
+    /**
+     * Returns raw JS code
+     *
+     * @param \Silex\Application $oApp
+     * @return string
+     */
+    public function GetJSInline(\Silex\Application $oApp);
+    /**
+     * Returns raw HTML code to put at the end of the <body> tag
+     *
+     * @param \Silex\Application $oApp
+     * @return string
+     */
+    public function GetBodyHTML(\Silex\Application $oApp);
+    /**
+     * Returns raw HTML code to put at the end of the #main-wrapper element
+     *
+     * @param \Silex\Application $oApp
+     * @return string
+     */
+    public function GetMainContentHTML(\Silex\Application $oApp);
+    /**
+     * Returns raw HTML code to put at the end of the #topbar and #sidebar elements
+     *
+     * @param \Silex\Application $oApp
+     * @return string
+     */
+    public function GetNavigationMenuHTML(\Silex\Application $oApp);
+}
+
+/**
+ * IMPORTANT! Experimental API, may be removed at anytime, we don't recommend to use it just now!
+ */
+abstract class AbstractPortalUIExtension implements iPortalUIExtension
+{
+    /**
+     * @inheritDoc
+     */
+    public function GetCSSFiles(\Silex\Application $oApp)
+    {
+        return array();
+    }
+    /**
+     * @inheritDoc
+     */
+    public function GetCSSInline(\Silex\Application $oApp)
+    {
+        return null;
+    }
+    /**
+     * @inheritDoc
+     */
+    public function GetJSFiles(\Silex\Application $oApp)
+    {
+        return array();
+    }
+    /**
+     * @inheritDoc
+     */
+    public function GetJSInline(\Silex\Application $oApp)
+    {
+        return null;
+    }
+    /**
+     * @inheritDoc
+     */
+    public function GetBodyHTML(\Silex\Application $oApp)
+    {
+        return null;
+    }
+    /**
+     * @inheritDoc
+     */
+    public function GetMainContentHTML(\Silex\Application $oApp)
+    {
+        return null;
+    }
+    /**
+     * @inheritDoc
+     */
+    public function GetNavigationMenuHTML(\Silex\Application $oApp)
+    {
+        return null;
+    }
+}
+
+/**
  * Implement this interface to add new operations to the REST/JSON web service
  * Implement this interface to add new operations to the REST/JSON web service
  *  
  *  
  * @package     Extensibility
  * @package     Extensibility

+ 1 - 1
core/metamodel.class.php

@@ -1718,7 +1718,7 @@ abstract class MetaModel
 
 
 		// Build the list of available extensions
 		// Build the list of available extensions
 		//
 		//
-		$aInterfaces = array('iApplicationUIExtension', 'iApplicationObjectExtension', 'iQueryModifier', 'iOnClassInitialization', 'iPopupMenuExtension', 'iPageUIExtension');
+		$aInterfaces = array('iApplicationUIExtension', 'iApplicationObjectExtension', 'iQueryModifier', 'iOnClassInitialization', 'iPopupMenuExtension', 'iPageUIExtension', 'iPortalUIExtension');
 		foreach($aInterfaces as $sInterface)
 		foreach($aInterfaces as $sInterface)
 		{
 		{
 			self::$m_aExtensionClasses[$sInterface] = array();
 			self::$m_aExtensionClasses[$sInterface] = array();

+ 1 - 0
datamodels/2.x/itop-portal-base/cs.dict.itop-portal-base.php

@@ -106,6 +106,7 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', array(
 Dict::Add('CS CZ', 'Czech', 'Čeština', array(
 Dict::Add('CS CZ', 'Czech', 'Čeština', array(
     'Brick:Portal:Manage:Name' => 'Spravovat položky',
     'Brick:Portal:Manage:Name' => 'Spravovat položky',
     'Brick:Portal:Manage:Table:NoData' => 'Žádná položka',
     'Brick:Portal:Manage:Table:NoData' => 'Žádná položka',
+    'Brick:Portal:Manage:Table:ItemActions' => 'Actions~~',
 ));
 ));
 
 
 // ObjectBrick brick
 // ObjectBrick brick

+ 2 - 0
datamodels/2.x/itop-portal-base/de.dict.itop-portal-base.php

@@ -98,9 +98,11 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
 	'Brick:Portal:Browse:Filter:NoData' => 'Kein Eintrag',
 	'Brick:Portal:Browse:Filter:NoData' => 'Kein Eintrag',
 ));
 ));
 
 
+// ManageBrick brick
 Dict::Add('DE DE', 'German', 'Deutsch', array(
 Dict::Add('DE DE', 'German', 'Deutsch', array(
 	'Brick:Portal:Manage:Name' => 'Einträge managen',
 	'Brick:Portal:Manage:Name' => 'Einträge managen',
 	'Brick:Portal:Manage:Table:NoData' => 'Kein Eintrag.',
 	'Brick:Portal:Manage:Table:NoData' => 'Kein Eintrag.',
+    'Brick:Portal:Manage:Table:ItemActions' => 'Actions~~',
 ));
 ));
 
 
 // ObjectBrick brick
 // ObjectBrick brick

+ 1 - 0
datamodels/2.x/itop-portal-base/en.dict.itop-portal-base.php

@@ -102,6 +102,7 @@ Dict::Add('EN US', 'English', 'English', array(
 Dict::Add('EN US', 'English', 'English', array(
 Dict::Add('EN US', 'English', 'English', array(
 	'Brick:Portal:Manage:Name' => 'Manage items',
 	'Brick:Portal:Manage:Name' => 'Manage items',
 	'Brick:Portal:Manage:Table:NoData' => 'No item.',
 	'Brick:Portal:Manage:Table:NoData' => 'No item.',
+    'Brick:Portal:Manage:Table:ItemActions' => 'Actions',
 ));
 ));
 
 
 // ObjectBrick brick
 // ObjectBrick brick

+ 1 - 0
datamodels/2.x/itop-portal-base/es_cr.dict.itop-portal-base.php

@@ -102,6 +102,7 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array(
 Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array(
 Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array(
 	'Brick:Portal:Manage:Name' => 'Administrar elementos',
 	'Brick:Portal:Manage:Name' => 'Administrar elementos',
 	'Brick:Portal:Manage:Table:NoData' => 'Sin objeto.',
 	'Brick:Portal:Manage:Table:NoData' => 'Sin objeto.',
+    'Brick:Portal:Manage:Table:ItemActions' => 'Actions~~',
 ));
 ));
 
 
 // ObjectBrick brick
 // ObjectBrick brick

+ 1 - 0
datamodels/2.x/itop-portal-base/fr.dict.itop-portal-base.php

@@ -102,6 +102,7 @@ Dict::Add('FR FR', 'French', 'Français', array(
 Dict::Add('FR FR', 'French', 'Français', array(
 Dict::Add('FR FR', 'French', 'Français', array(
 	'Brick:Portal:Manage:Name' => 'Gestion d\'éléments',
 	'Brick:Portal:Manage:Name' => 'Gestion d\'éléments',
 	'Brick:Portal:Manage:Table:NoData' => 'Aucun élément',
 	'Brick:Portal:Manage:Table:NoData' => 'Aucun élément',
+    'Brick:Portal:Manage:Table:ItemActions' => 'Actions',
 ));
 ));
 
 
 // ObjectBrick brick
 // ObjectBrick brick

+ 2 - 2
datamodels/2.x/itop-portal-base/module.itop-portal-base.php

@@ -17,7 +17,7 @@ SetupWebPage::AddModule(
 		'portal/src/entities/portalbrick.class.inc.php',
 		'portal/src/entities/portalbrick.class.inc.php',
 		'portal/src/controllers/abstractcontroller.class.inc.php',
 		'portal/src/controllers/abstractcontroller.class.inc.php',
 		'portal/src/controllers/brickcontroller.class.inc.php',
 		'portal/src/controllers/brickcontroller.class.inc.php',
-		'portal/src/routers/abstractrouter.class.inc.php',
+        'portal/src/routers/abstractrouter.class.inc.php',
 	),
 	),
 	'webservice' => array(
 	'webservice' => array(
 	//'webservices.itop-portal-base.php',
 	//'webservices.itop-portal-base.php',
@@ -40,4 +40,4 @@ SetupWebPage::AddModule(
 	),
 	),
 	)
 	)
 );
 );
-?>
+

+ 1 - 0
datamodels/2.x/itop-portal-base/nl.dict.itop-portal-base.php

@@ -96,6 +96,7 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', array(
 Dict::Add('NL NL', 'Dutch', 'Nederlands', array(
 Dict::Add('NL NL', 'Dutch', 'Nederlands', array(
 	'Brick:Portal:Manage:Name' => 'Beheer items',
 	'Brick:Portal:Manage:Name' => 'Beheer items',
 	'Brick:Portal:Manage:Table:NoData' => 'Geen gegevens',
 	'Brick:Portal:Manage:Table:NoData' => 'Geen gegevens',
+    'Brick:Portal:Manage:Table:ItemActions' => 'Actions~~',
 ));
 ));
 
 
 // ObjectBrick brick
 // ObjectBrick brick

+ 44 - 16
datamodels/2.x/itop-portal-base/portal/src/controllers/managebrickcontroller.class.inc.php

@@ -41,6 +41,9 @@ use \VariableExpression;
 use \SQLExpression;
 use \SQLExpression;
 use \UnaryExpression;
 use \UnaryExpression;
 use \Dict;
 use \Dict;
+use \iPopupMenuExtension;
+use \URLButtonItem;
+use \JSButtonItem;
 use \Combodo\iTop\Portal\Helper\ApplicationHelper;
 use \Combodo\iTop\Portal\Helper\ApplicationHelper;
 use \Combodo\iTop\Portal\Helper\SecurityHelper;
 use \Combodo\iTop\Portal\Helper\SecurityHelper;
 use \Combodo\iTop\Portal\Brick\AbstractBrick;
 use \Combodo\iTop\Portal\Brick\AbstractBrick;
@@ -381,6 +384,7 @@ class ManageBrickController extends BrickController
 
 
 		// Retrieving and preparing data for rendering
 		// Retrieving and preparing data for rendering
 		$aGroupingAreasData = array();
 		$aGroupingAreasData = array();
+        $bHasObjectListItemExtension = false;
 		foreach ($aSets as $sKey => $oSet)
 		foreach ($aSets as $sKey => $oSet)
 		{
 		{
 			// Set properties
 			// Set properties
@@ -402,7 +406,6 @@ class ManageBrickController extends BrickController
 
 
 			// Getting items
 			// Getting items
 			$aItems = array();
 			$aItems = array();
-			$aItemsIds = array();
 			// ... For each item
 			// ... For each item
             /** @var DBObject $oCurrentRow */
             /** @var DBObject $oCurrentRow */
 			while ($oCurrentRow = $oSet->Fetch())
 			while ($oCurrentRow = $oSet->Fetch())
@@ -433,12 +436,12 @@ class ManageBrickController extends BrickController
 						// - Then set allowed action
 						// - Then set allowed action
 						if ($sActionType !== null)
 						if ($sActionType !== null)
 						{
 						{
-							$aActions[] = array(
-								'type' => $sActionType,
-								'class' => $sCurrentClass,
-								'id' => $oCurrentRow->GetKey(),
+                            $aActions[] = array(
+                                'type' => $sActionType,
+                                'class' => $sCurrentClass,
+                                'id' => $oCurrentRow->GetKey(),
                                 'opening_target' => $oBrick->GetOpeningTarget(),
                                 'opening_target' => $oBrick->GetOpeningTarget(),
-							);
+                            );
 						}
 						}
 					}
 					}
 
 
@@ -479,25 +482,50 @@ class ManageBrickController extends BrickController
 						'actions' => $aActions
 						'actions' => $aActions
 					);
 					);
 				}
 				}
+
+				// ... Checking menu extensions
+                $aItemButtons = array();
+                foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance)
+                {
+                    foreach($oExtensionInstance->EnumItems(iPopupMenuExtension::PORTAL_OBJLISTITEM_ACTIONS, array('portal_id' => $oApp['combodo.portal.instance.id'], 'object' => $oCurrentRow)) as $oMenuItem)
+                    {
+                        if (is_object($oMenuItem))
+                        {
+                            if($oMenuItem instanceof JSButtonItem)
+                            {
+                                $aItemButtons[] = $oMenuItem->GetMenuItem() + array('js_files' => $oMenuItem->GetLinkedScripts(), 'type' => 'button');
+                            }
+                            elseif($oMenuItem instanceof URLButtonItem)
+                            {
+                                $aItemButtons[] = $oMenuItem->GetMenuItem() + array('type' => 'link');
+                            }
+                        }
+                    }
+                }
 				
 				
 				// ... And item's properties
 				// ... And item's properties
 				$aItems[] = array(
 				$aItems[] = array(
 					'id' => $oCurrentRow->GetKey(),
 					'id' => $oCurrentRow->GetKey(),
 					'class' => $sCurrentClass,
 					'class' => $sCurrentClass,
 					'attributes' => $aItemAttrs,
 					'attributes' => $aItemAttrs,
-					'highlight_class' => $oCurrentRow->GetHilightClass()
+					'highlight_class' => $oCurrentRow->GetHilightClass(),
+                    'actions' => $aItemButtons,
 				);
 				);
-				$aItemsIds = $oCurrentRow->GetKey();
+
+                if(!empty($aItemButtons))
+                {
+                    $bHasObjectListItemExtension = true;
+                }
 			}
 			}
 
 
-			// Now that we retrieved items, we check which can be edited, which can be view and which cannot be opened
-            //
-            // Note: Now that we do checks here and not through the SecurityHelper while fetching objects, we might bypass datamodel security regarding the object class!
-//            $oScopeQuery = $oApp['scope_validator']->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sCurrentClass, UR_ACTION_MODIFY);
-//			if($oSearchEditableItems !== null)
-//            {
-//                $oSearchEditableItems->A
-//            }
+			// Adding an extra column for object list item extensions
+            if($bHasObjectListItemExtension === true)
+            {
+                $aColumnsDefinition['_ui_extensions'] = array(
+                    'title' => Dict::S('Brick:Portal:Manage:Table:ItemActions'),
+                    'type' => 'html',
+                );
+            }
 
 
 			$aGroupingAreasData[$sKey] = array(
 			$aGroupingAreasData[$sKey] = array(
 				'sId' => $sKey,
 				'sId' => $sKey,

+ 96 - 5
datamodels/2.x/itop-portal-base/portal/src/helpers/applicationhelper.class.inc.php

@@ -30,11 +30,13 @@ use \Dict;
 use \utils;
 use \utils;
 use \IssueLog;
 use \IssueLog;
 use \UserRights;
 use \UserRights;
+use \CMDBSource;
 use \DOMFormatException;
 use \DOMFormatException;
 use \ModuleDesign;
 use \ModuleDesign;
 use \MetaModel;
 use \MetaModel;
 use \DBObjectSearch;
 use \DBObjectSearch;
 use \DBObjectSet;
 use \DBObjectSet;
+use \iPortalUIExtension;
 use \Combodo\iTop\Portal\Brick\AbstractBrick;
 use \Combodo\iTop\Portal\Brick\AbstractBrick;
 
 
 /**
 /**
@@ -200,7 +202,7 @@ class ApplicationHelper
 	 */
 	 */
 	static function RegisterTwigExtensions(Twig_Environment &$oTwigEnv)
 	static function RegisterTwigExtensions(Twig_Environment &$oTwigEnv)
 	{
 	{
-		// A filter to translate a string via the Dict::S function
+		// Filter to translate a string via the Dict::S function
 		// Usage in twig : {{ 'String:ToTranslate'|dict_s }}
 		// Usage in twig : {{ 'String:ToTranslate'|dict_s }}
         $oTwigEnv->addFilter(new Twig_SimpleFilter('dict_s', function($sStringCode, $sDefault = null, $bUserLanguageOnly = false)
         $oTwigEnv->addFilter(new Twig_SimpleFilter('dict_s', function($sStringCode, $sDefault = null, $bUserLanguageOnly = false)
 		{
 		{
@@ -208,7 +210,7 @@ class ApplicationHelper
 		})
 		})
 		);
 		);
 
 
-		// A filter to format a string via the Dict::Format function
+		// Filter to format a string via the Dict::Format function
 		// Usage in twig : {{ 'String:ToTranslate'|dict_format() }}
 		// Usage in twig : {{ 'String:ToTranslate'|dict_format() }}
         $oTwigEnv->addFilter(new Twig_SimpleFilter('dict_format', function($sStringCode, $sParam01 = null, $sParam02 = null, $sParam03 = null, $sParam04 = null)
         $oTwigEnv->addFilter(new Twig_SimpleFilter('dict_format', function($sStringCode, $sParam01 = null, $sParam02 = null, $sParam03 = null, $sParam04 = null)
 		{
 		{
@@ -216,12 +218,12 @@ class ApplicationHelper
 		})
 		})
 		);
 		);
 
 
-		// Filters to enable base64 encode/decode
+		// Filter to enable base64 encode/decode
 		// Usage in twig : {{ 'String to encode'|base64_encode }}
 		// Usage in twig : {{ 'String to encode'|base64_encode }}
         $oTwigEnv->addFilter(new Twig_SimpleFilter('base64_encode', 'base64_encode'));
         $oTwigEnv->addFilter(new Twig_SimpleFilter('base64_encode', 'base64_encode'));
         $oTwigEnv->addFilter(new Twig_SimpleFilter('base64_decode', 'base64_decode'));
         $oTwigEnv->addFilter(new Twig_SimpleFilter('base64_decode', 'base64_decode'));
 
 
-		// Filters to enable json decode  (encode already exists)
+		// Filter to enable json decode  (encode already exists)
 		// Usage in twig : {{ aSomeArray|json_decode }}
 		// Usage in twig : {{ aSomeArray|json_decode }}
         $oTwigEnv->addFilter(new Twig_SimpleFilter('json_decode', function($sJsonString, $bAssoc = false)
         $oTwigEnv->addFilter(new Twig_SimpleFilter('json_decode', function($sJsonString, $bAssoc = false)
 		{
 		{
@@ -243,6 +245,22 @@ class ApplicationHelper
 
 
 			return $sUrl;
 			return $sUrl;
 		}));
 		}));
+
+        // Filter to add a module's version to an url
+        $oTwigEnv->addFilter(new Twig_SimpleFilter('add_module_version', function($sUrl, $sModuleName){
+            $sModuleVersion = utils::GetCompiledModuleVersion($sModuleName);
+
+            if (strpos($sUrl, '?') === false)
+            {
+                $sUrl = $sUrl . "?moduleversion=" . $sModuleVersion;
+            }
+            else
+            {
+                $sUrl = $sUrl . "&moduleversion=" . $sModuleVersion;
+            }
+
+            return $sUrl;
+        }));
 	}
 	}
 
 
 	/**
 	/**
@@ -398,8 +416,15 @@ class ApplicationHelper
 				),
 				),
 				'portals' => array(),
 				'portals' => array(),
 				'forms' => array(),
 				'forms' => array(),
+                'ui_extensions' => array(
+                    'css_files' => array(),
+                    'css_inline' => null,
+                    'js_files' => array(),
+                    'js_inline' => null,
+                    'html' => array(),
+                ),
 				'bricks' => array(),
 				'bricks' => array(),
-				'bricks_total_width' => 0
+				'bricks_total_width' => 0,
 			);
 			);
 			// - Global portal properties
 			// - Global portal properties
 			foreach ($oDesign->GetNodes('/module_design/properties/*') as $oPropertyNode)
 			foreach ($oDesign->GetNodes('/module_design/properties/*') as $oPropertyNode)
@@ -510,6 +535,8 @@ class ApplicationHelper
             static::LoadLifecycleConfiguration($oApp, $oDesign);
             static::LoadLifecycleConfiguration($oApp, $oDesign);
 			// - Presentation lists
 			// - Presentation lists
 			$aPortalConf['lists'] = static::LoadListsConfiguration($oApp, $oDesign);
 			$aPortalConf['lists'] = static::LoadListsConfiguration($oApp, $oDesign);
+			// - UI extensions
+            $aPortalConf['ui_extensions'] = static::LoadUIExtensions($oApp);
 			// - Action rules
 			// - Action rules
 			static::LoadActionRulesConfiguration($oApp, $oDesign);
 			static::LoadActionRulesConfiguration($oApp, $oDesign);
 			// - Generating CSS files
 			// - Generating CSS files
@@ -1242,4 +1269,68 @@ class ApplicationHelper
 		return $aClassesLists;
 		return $aClassesLists;
 	}
 	}
 
 
+    /**
+     * Loads portal UI extensions
+     *
+     * @param \Silex\Application $oApp
+     * @return array
+     */
+	static protected function LoadUIExtensions(Application $oApp)
+    {
+        $aUIExtensions = array(
+            'css_files' => array(),
+            'css_inline' => null,
+            'js_files' => array(),
+            'js_inline' => null,
+            'html' => array(),
+        );
+        $aUIExtensionHooks = array(
+            iPortalUIExtension::ENUM_PORTAL_EXT_UI_BODY,
+            iPortalUIExtension::ENUM_PORTAL_EXT_UI_NAVIGATION_MENU,
+            iPortalUIExtension::ENUM_PORTAL_EXT_UI_MAIN_CONTENT,
+        );
+
+        /** @var iPortalUIExtension $oExtensionInstance */
+        foreach(MetaModel::EnumPlugins('iPortalUIExtension') as $oExtensionInstance)
+        {
+                // Adding CSS files
+                $aUIExtensions['css_files'] = array_merge($aUIExtensions['css_files'], $oExtensionInstance->GetCSSFiles($oApp));
+
+                // Adding CSS inline
+                $sCSSInline = $oExtensionInstance->GetCSSInline($oApp);
+                if($sCSSInline !== null)
+                {
+                    $aUIExtensions['css_inline'] .= "\n\n" . $sCSSInline;
+                }
+
+                // Adding JS files
+                $aUIExtensions['js_files'] = array_merge($aUIExtensions['js_files'], $oExtensionInstance->GetJSFiles($oApp));
+
+                // Adding JS inline
+                $sJSInline = $oExtensionInstance->GetJSInline($oApp);
+                if($sJSInline !== null)
+                {
+                    // Note: Semi-colon is to prevent previous script that would have omitted it.
+                    $aUIExtensions['js_inline'] .= "\n\n;\n" . $sJSInline;
+                }
+
+                // Adding HTML for each hook
+                foreach($aUIExtensionHooks as $sUIExtensionHook)
+                {
+                    $sFunctionName = 'Get'.$sUIExtensionHook.'HTML';
+                    $sHTML = $oExtensionInstance->$sFunctionName($oApp);
+                    if($sHTML !== null)
+                    {
+                        if(!array_key_exists($sUIExtensionHook, $aUIExtensions['html']))
+                        {
+                            $aUIExtensions['html'][$sUIExtensionHook] = '';
+                        }
+                        $aUIExtensions['html'][$sUIExtensionHook] .= "\n\n" . $sHTML;
+                    }
+                }
+        }
+
+        return $aUIExtensions;
+    }
+
 }
 }

+ 126 - 45
datamodels/2.x/itop-portal-base/portal/src/views/bricks/manage/layout.html.twig

@@ -107,54 +107,130 @@
 			
 			
 			for(key in tableProperties)
 			for(key in tableProperties)
 			{
 			{
-				columnsDefinition.push({
-					"width": "auto",
-					"searchable": true,
-					"sortable": (sDataLoading === '{{ constant('Combodo\\iTop\\Portal\\Brick\\AbstractBrick::ENUM_DATA_LOADING_FULL') }}'),
-					"title": tableProperties[key].title,
-					"defaultContent": "",
-					"type": "html",
-					"data": "attributes."+key+".att_code",
-					"render": function(att_code, type, row){
-						var cellElem;
-						var itemActions;
-						var itemPrimarayAction;
-						
-						// Preparing action on the cell
-						// Note : For now we will use only one action, the secondary actions are therefore not implemented. Only the data structure is done.
-						itemActions = row.attributes[att_code].actions;
-						
-						// Preparing the cell data
-						cellElem = (itemActions.length > 0) ? $('<a></a>') : $('<span></span>');
-						cellElem.html(row.attributes[att_code].value);
-						// Building actions
-						if(itemActions.length > 0)
-						{
-							// - Primary action
-							itemPrimaryAction = itemActions[0];
-							switch(itemPrimaryAction.type)
+			    // Regular attribute columns
+			    if(key !== '_ui_extensions') {
+                    columnsDefinition.push({
+                        "width": "auto",
+                        "searchable": true,
+                        "sortable": (sDataLoading === '{{ constant('Combodo\\iTop\\Portal\\Brick\\AbstractBrick::ENUM_DATA_LOADING_FULL') }}'),
+                        "title": tableProperties[key].title,
+                        "defaultContent": "",
+                        "type": "html",
+                        "data": "attributes." + key + ".att_code",
+                        "render": function (att_code, type, row) {
+                            var cellElem;
+                            var itemActions;
+                            var itemPrimarayAction;
+
+                            // Preparing action on the cell
+                            // Note : For now we will use only one action, the secondary actions are therefore not implemented. Only the data structure is done.
+                            itemActions = row.attributes[att_code].actions;
+
+                            // Preparing the cell data
+                            cellElem = (itemActions.length > 0) ? $('<a></a>') : $('<span></span>');
+                            cellElem.html(row.attributes[att_code].value);
+                            // Building actions
+                            if (itemActions.length > 0) {
+                                // - Primary action
+                                itemPrimaryAction = itemActions[0];
+                                switch (itemPrimaryAction.type) {
+                                    case '{{ constant('Combodo\\iTop\\Portal\\Brick\\ManageBrick::ENUM_ACTION_VIEW') }}':
+                                        url = '{{ app.url_generator.generate('p_object_view', {'sObjectClass': '-objectClass-', 'sObjectId': '-objectId-'})|raw }}'.replace(/-objectClass-/, itemPrimaryAction.class).replace(/-objectId-/, itemPrimaryAction.id);
+                                        break;
+                                    case '{{ constant('Combodo\\iTop\\Portal\\Brick\\ManageBrick::ENUM_ACTION_EDIT') }}':
+                                        url = '{{ app.url_generator.generate('p_object_edit', {'sObjectClass': '-objectClass-', 'sObjectId': '-objectId-'})|raw }}'.replace(/-objectClass-/, itemPrimaryAction.class).replace(/-objectId-/, itemPrimaryAction.id);
+                                        break;
+                                    default:
+                                        url = '#';
+                                        //console.log('Action "'+itemPrimaryAction+'" not implemented');
+                                        break;
+                                }
+                                SetActionUrl(cellElem, url);
+                                SetActionOpeningTarget(cellElem, itemPrimaryAction.opening_target);
+
+                                // - Secondary actions
+                                // Not done for now, only the data structure is ready in case we need it later
+                            }
+
+                            return cellElem.prop('outerHTML');
+                        },
+                    });
+                }
+                // UI extensions buttons
+                else
+				{
+                    columnsDefinition.push({
+                        "width": "auto",
+                        "searchable": false,
+                        "sortable": (sDataLoading === '{{ constant('Combodo\\iTop\\Portal\\Brick\\AbstractBrick::ENUM_DATA_LOADING_FULL') }}'),
+                        "title": tableProperties[key].title,
+                        "defaultContent": "",
+                        "type": "html",
+                        "data": "attributes." + key + ".att_code",
+                        "render": function (att_code, type, row) {
+                            var cellElem = $('<div class="group-actions-wrapper"></div>');
+                            var actionsCount = row.actions.length;
+
+                            // Adding menu wrapper in case there are several actions
+                            var actionsElem = $('<div></div>');
+                            actionsElem.appendTo(cellElem);
+                            if(actionsCount > 1) {
+                                actionsElem.addClass('group-actions pull-right');
+
+                                // Adding hamburger icon toggler
+                                actionsElem.append(
+                                    $('<a class="glyphicon glyphicon-menu-hamburger" data-toggle="collapse" data-target="#item-actions-menu-' + row.id + '"></a>')
+                                );
+
+                                // Adding sub menu
+                                var actionsSSMenuElem = $('<div id="item-actions-menu-' + row.id + '" class="item-action-wrapper panel panel-default"></div>')
+                                    .appendTo(actionsElem);
+                                var actionsSSMenuContainerElem = $('<div class="panel-body"></div>')
+                                    .appendTo(actionsSSMenuElem);
+                            }
+
+                            // Adding actions
+							for(var i in row.actions)
 							{
 							{
-								case '{{ constant('Combodo\\iTop\\Portal\\Brick\\ManageBrick::ENUM_ACTION_VIEW') }}':
-									url = '{{ app.url_generator.generate('p_object_view', {'sObjectClass': '-objectClass-', 'sObjectId': '-objectId-'})|raw }}'.replace(/-objectClass-/, itemPrimaryAction.class).replace(/-objectId-/, itemPrimaryAction.id);
-									break;
-								case '{{ constant('Combodo\\iTop\\Portal\\Brick\\ManageBrick::ENUM_ACTION_EDIT') }}':
-									url = '{{ app.url_generator.generate('p_object_edit', {'sObjectClass': '-objectClass-', 'sObjectId': '-objectId-'})|raw }}'.replace(/-objectClass-/, itemPrimaryAction.class).replace(/-objectId-/, itemPrimaryAction.id);
-									break;
-								default:
-								    url = '#';
-									//console.log('Action "'+itemPrimaryAction+'" not implemented');
-									break;
+								var actionDef = row.actions[i];
+								var actionElem = $('<a></a>')
+									.attr('href', actionDef.url)
+									.append( $('<span></span>').html(actionDef.label) );
+
+								// Adding css classes to action
+								for(var j in actionDef.css_classes)
+								{
+									actionElem.addClass(actionDef.css_classes[j]);
+								}
+
+								// Performing specific treatment regarding the action type
+								if(actionDef.type === 'button')
+								{
+									// External files
+									// Note: Not supported yet
+
+									// On click callback
+									actionElem.attr('onclick', actionDef.onclick);
+								}
+								else if(actionDef.type === 'link')
+								{
+									actionElem.attr('target', actionDef.target);
+								}
+
+								if(actionsCount > 1)
+								{
+                                    actionsSSMenuContainerElem.append( $('<p></p>').append(actionElem) );
+								}
+								else
+								{
+                                    actionsElem.append( actionElem );
+								}
 							}
 							}
-                            SetActionUrl(cellElem, url);
-                            SetActionOpeningTarget(cellElem, itemPrimaryAction.opening_target);
-							
-							// - Secondary actions
-							// Not done for now, only the data structure is ready in case we need it later
+
+                            return cellElem.prop('outerHTML');
 						}
 						}
-						
-						return cellElem.prop('outerHTML');
-					},
-				});
+                    });
+				}
 			}
 			}
 			
 			
 			return columnsDefinition;
 			return columnsDefinition;
@@ -275,6 +351,11 @@
 					}
 					}
 				});
 				});
 			{% endfor %}
 			{% endfor %}
+
+            // Auto collapse item actions popup
+            $('body').click(function(){
+                $('table .item-action-wrapper.collapse.in').collapse('hide');
+            });
 		});
 		});
 	</script>
 	</script>
 {% endblock %}
 {% endblock %}

+ 2 - 2
datamodels/2.x/itop-portal-base/portal/src/views/bricks/object/plugins_buttons.html.twig

@@ -23,7 +23,7 @@
 			{% if bGroupButtons == true %}
 			{% if bGroupButtons == true %}
 				<li>
 				<li>
 			{% endif %}
 			{% endif %}
-			<a class="{{ sButtonCssClasses }} {{ aButton.css_classes|join(' ') }}" href="{{ aButton.url }}" onclick="{{ aButton.onclick }}">{{ aButton.label }}</a>
+			<a class="{{ sButtonCssClasses }} {{ aButton.css_classes|join(' ') }}" href="{{ aButton.url }}" onclick="{{ aButton.onclick }}">{{ aButton.label|raw }}</a>
 			{% if bGroupButtons == true %}
 			{% if bGroupButtons == true %}
 				</li>
 				</li>
 			{% endif %}
 			{% endif %}
@@ -37,7 +37,7 @@
             {% if bGroupButtons == true %}
             {% if bGroupButtons == true %}
 				<li>
 				<li>
 			{% endif %}
 			{% endif %}
-			<a class="{{ sButtonCssClasses }} {{ aButton.css_classes|join(' ') }}" href="{{ aButton.url }}" target="{{ aButton.target }}">{{ aButton.label }}</a>
+			<a class="{{ sButtonCssClasses }} {{ aButton.css_classes|join(' ') }}" href="{{ aButton.url }}" target="{{ aButton.target }}">{{ aButton.label|raw }}</a>
             {% if bGroupButtons == true %}
             {% if bGroupButtons == true %}
 				</li>
 				</li>
             {% endif %}
             {% endif %}

+ 57 - 0
datamodels/2.x/itop-portal-base/portal/src/views/layout.html.twig

@@ -24,6 +24,7 @@
 	{% endblock %}
 	{% endblock %}
 	<title>{% block pPageTitle %}{% if sPageTitle is defined and sPageTitle is not null %}{{ sPageTitle }} - {{ constant('ITOP_APPLICATION') }}{% else %}{{ 'Page:DefaultTitle'|dict_s }}{% endif %}{% endblock %}</title>
 	<title>{% block pPageTitle %}{% if sPageTitle is defined and sPageTitle is not null %}{{ sPageTitle }} - {{ constant('ITOP_APPLICATION') }}{% else %}{{ 'Page:DefaultTitle'|dict_s }}{% endif %}{% endblock %}</title>
 	<link rel="shortcut icon" href="{{ app['combodo.absolute_url'] ~ 'images/favicon.ico'|add_itop_version }}" />
 	<link rel="shortcut icon" href="{{ app['combodo.absolute_url'] ~ 'images/favicon.ico'|add_itop_version }}" />
+
 	{% block pPageStylesheets %}
 	{% block pPageStylesheets %}
 		{# First bootstrap core, lib themes, then bootstrap theme, portal adjustements #}
 		{# First bootstrap core, lib themes, then bootstrap theme, portal adjustements #}
 		<link href="{{ app['combodo.portal.base.absolute_url'] ~ 'lib/bootstrap/css/bootstrap.min.css'|add_itop_version }}" rel="stylesheet">
 		<link href="{{ app['combodo.portal.base.absolute_url'] ~ 'lib/bootstrap/css/bootstrap.min.css'|add_itop_version }}" rel="stylesheet">
@@ -47,6 +48,12 @@
 		<link href="{{ app['combodo.portal.instance.conf'].properties.themes.bootstrap|add_itop_version }}" rel="stylesheet" id="css_bootstrap_theme">
 		<link href="{{ app['combodo.portal.instance.conf'].properties.themes.bootstrap|add_itop_version }}" rel="stylesheet" id="css_bootstrap_theme">
 		{# - Portal adjustments for BS theme #}
 		{# - Portal adjustments for BS theme #}
 		<link href="{{ app['combodo.portal.instance.conf'].properties.themes.portal|add_itop_version }}" rel="stylesheet" id="css_portal">
 		<link href="{{ app['combodo.portal.instance.conf'].properties.themes.portal|add_itop_version }}" rel="stylesheet" id="css_portal">
+		{# UI Extensions CSS, in an undefined order #}
+        {% if app['combodo.portal.instance.conf'].ui_extensions.css_files is defined %}
+            {% for css_file in app['combodo.portal.instance.conf'].ui_extensions.css_files %}
+				<link href="{{ css_file|add_itop_version }}" rel="stylesheet">
+            {% endfor %}
+        {% endif %}
 		{# Custom CSS that is supposed to do adjustments to the portal #}
 		{# Custom CSS that is supposed to do adjustments to the portal #}
 		{% if app['combodo.portal.instance.conf'].properties.themes.custom is defined %}
 		{% if app['combodo.portal.instance.conf'].properties.themes.custom is defined %}
 			<link href="{{ app['combodo.portal.instance.conf'].properties.themes.custom|add_itop_version }}" rel="stylesheet">
 			<link href="{{ app['combodo.portal.instance.conf'].properties.themes.custom|add_itop_version }}" rel="stylesheet">
@@ -58,6 +65,16 @@
 			{% endfor %}
 			{% endfor %}
 		{% endif %}
 		{% endif %}
 	{% endblock %}
 	{% endblock %}
+
+	{% block pStyleinline %}
+        {# UI Extensions inline CSS #}
+        {% if app['combodo.portal.instance.conf'].ui_extensions.css_inline is not null %}
+			<style>
+				{{ app['combodo.portal.instance.conf'].ui_extensions.css_inline|raw }}
+			</style>
+        {% endif %}
+	{% endblock %}
+
 	{% block pPageScripts %}
 	{% block pPageScripts %}
 		<script type="text/javascript" src="{{ app['combodo.portal.base.absolute_url'] ~ 'lib/jquery/jquery-1.11.3.min.js'|add_itop_version }}"></script>
 		<script type="text/javascript" src="{{ app['combodo.portal.base.absolute_url'] ~ 'lib/jquery/jquery-1.11.3.min.js'|add_itop_version }}"></script>
 		<script type="text/javascript" src="{{ app['combodo.portal.base.absolute_url'] ~ 'lib/jquery-ui/jquery-ui-1.11.4.min.js'|add_itop_version }}"></script>
 		<script type="text/javascript" src="{{ app['combodo.portal.base.absolute_url'] ~ 'lib/jquery-ui/jquery-ui-1.11.4.min.js'|add_itop_version }}"></script>
@@ -100,6 +117,12 @@
 		<script type="text/javascript" src="{{ app['combodo.portal.base.absolute_url'] ~ 'js/portal_form_handler.js'|add_itop_version }}"></script>
 		<script type="text/javascript" src="{{ app['combodo.portal.base.absolute_url'] ~ 'js/portal_form_handler.js'|add_itop_version }}"></script>
 		<script type="text/javascript" src="{{ app['combodo.portal.base.absolute_url'] ~ 'js/portal_form_field.js'|add_itop_version }}"></script>
 		<script type="text/javascript" src="{{ app['combodo.portal.base.absolute_url'] ~ 'js/portal_form_field.js'|add_itop_version }}"></script>
 		<script type="text/javascript" src="{{ app['combodo.portal.base.absolute_url'] ~ 'js/portal_form_field_html.js'|add_itop_version }}"></script>
 		<script type="text/javascript" src="{{ app['combodo.portal.base.absolute_url'] ~ 'js/portal_form_field_html.js'|add_itop_version }}"></script>
+        {# UI Extensions JS, in an undefined order #}
+        {% if app['combodo.portal.instance.conf'].ui_extensions.js_files is defined %}
+			{% for js_file in app['combodo.portal.instance.conf'].ui_extensions.js_files %}
+				<script type="text/javascript" src="{{ js_file|add_itop_version }}"></script>
+			{% endfor %}
+        {% endif %}
 	{% endblock %}
 	{% endblock %}
 </head>
 </head>
 <body class="{% block pPageBodyClass %}{% endblock %}">
 <body class="{% block pPageBodyClass %}{% endblock %}">
@@ -179,6 +202,12 @@
 								{% endif %}
 								{% endif %}
 							</ul>
 							</ul>
 						</div>
 						</div>
+
+                        {% block pPageUIExtensionNavigationMenuTopbar %}
+                            {% if app['combodo.portal.instance.conf'].ui_extensions.html[constant('iPortalUIExtension::ENUM_PORTAL_EXT_UI_NAVIGATION_MENU')] is defined %}
+                                {{ app['combodo.portal.instance.conf'].ui_extensions.html[constant('iPortalUIExtension::ENUM_PORTAL_EXT_UI_NAVIGATION_MENU')]|raw }}
+                            {% endif %}
+                        {% endblock %}
 					</div>
 					</div>
 				</nav>
 				</nav>
 			{% endblock %}
 			{% endblock %}
@@ -235,6 +264,13 @@
 							</ul>
 							</ul>
 						{% endblock %}
 						{% endblock %}
 					</div>
 					</div>
+
+                    {% block pPageUIExtensionNavigationMenuSidebar %}
+                        {% if app['combodo.portal.instance.conf'].ui_extensions.html[constant('iPortalUIExtension::ENUM_PORTAL_EXT_UI_NAVIGATION_MENU')] is defined %}
+                            {{ app['combodo.portal.instance.conf'].ui_extensions.html[constant('iPortalUIExtension::ENUM_PORTAL_EXT_UI_NAVIGATION_MENU')]|raw }}
+                        {% endif %}
+                    {% endblock %}
+
 					{% if app['combodo.portal.instance.conf'].properties.logo is not null %}
 					{% if app['combodo.portal.instance.conf'].properties.logo is not null %}
 						<div class="logo">
 						<div class="logo">
 							{% block pNavigationSideMenuLogo %}
 							{% block pNavigationSideMenuLogo %}
@@ -268,6 +304,12 @@
 					</section>
 					</section>
 				</div>
 				</div>
 			</div>
 			</div>
+
+			{% block pPageUIExtensionMainContent %}
+                {% if app['combodo.portal.instance.conf'].ui_extensions.html[constant('iPortalUIExtension::ENUM_PORTAL_EXT_UI_MAIN_CONTENT')] is defined %}
+                    {{ app['combodo.portal.instance.conf'].ui_extensions.html[constant('iPortalUIExtension::ENUM_PORTAL_EXT_UI_MAIN_CONTENT')]|raw }}
+                {% endif %}
+			{% endblock %}
 		</div>
 		</div>
 		{% endblock %}
 		{% endblock %}
 		
 		
@@ -313,6 +355,12 @@
 				</div>
 				</div>
 			</div>
 			</div>
 		{% endblock %}
 		{% endblock %}
+
+		{% block pPageUIExtensionBody %}
+			{% if app['combodo.portal.instance.conf'].ui_extensions.html[constant('iPortalUIExtension::ENUM_PORTAL_EXT_UI_BODY')] is defined %}
+				{{ app['combodo.portal.instance.conf'].ui_extensions.html[constant('iPortalUIExtension::ENUM_PORTAL_EXT_UI_BODY')]|raw }}
+			{% endif %}
+		{% endblock %}
 	{% endblock %}
 	{% endblock %}
 	
 	
 	{% block pPageLiveScripts %}
 	{% block pPageLiveScripts %}
@@ -410,5 +458,14 @@
 			});
 			});
 		</script>
 		</script>
 	{% endblock %}
 	{% endblock %}
+
+	{% block pPageExtensionsScripts %}
+        {# UI Extensions inline JS #}
+        {% if app['combodo.portal.instance.conf'].ui_extensions.js_inline is not null %}
+			<script type="text/javascript">
+				{{ app['combodo.portal.instance.conf'].ui_extensions.js_inline|raw }}
+			</script>
+        {% endif %}
+	{% endblock %}
 </body>
 </body>
 </html>
 </html>

+ 3 - 0
datamodels/2.x/itop-portal-base/portal/web/css/portal.css

@@ -582,6 +582,9 @@ footer {
   font-size: 0.8em;
   font-size: 0.8em;
 }
 }
 /* Secondary actions */
 /* Secondary actions */
+.list-group-item-actions .group-actions-wrapper, .mosaic-group-item-actions .group-actions-wrapper, table .group-actions-wrapper {
+  text-align: center;
+}
 table .group-actions {
 table .group-actions {
   position: relative;
   position: relative;
 }
 }

+ 5 - 0
datamodels/2.x/itop-portal-base/portal/web/css/portal.scss

@@ -612,6 +612,11 @@ footer{
 }
 }
 
 
 /* Secondary actions */
 /* Secondary actions */
+.list-group-item-actions .group-actions-wrapper,
+.mosaic-group-item-actions .group-actions-wrapper,
+table .group-actions-wrapper{
+	text-align: center;
+}
 table .group-actions{
 table .group-actions{
 	position: relative;
 	position: relative;
 }
 }

+ 2 - 0
datamodels/2.x/itop-portal-base/portal/web/index.php

@@ -31,6 +31,7 @@ require_once APPROOT . '/core/moduledesign.class.inc.php';
 require_once APPROOT . '/application/loginwebpage.class.inc.php';
 require_once APPROOT . '/application/loginwebpage.class.inc.php';
 require_once APPROOT . '/sources/autoload.php';
 require_once APPROOT . '/sources/autoload.php';
 // Portal
 // Portal
+// Note: This could be prevented by adding namespaces to composer
 require_once __DIR__ . '/../src/providers/urlgeneratorserviceprovider.class.inc.php';
 require_once __DIR__ . '/../src/providers/urlgeneratorserviceprovider.class.inc.php';
 require_once __DIR__ . '/../src/helpers/urlgeneratorhelper.class.inc.php';
 require_once __DIR__ . '/../src/helpers/urlgeneratorhelper.class.inc.php';
 require_once __DIR__ . '/../src/providers/contextmanipulatorserviceprovider.class.inc.php';
 require_once __DIR__ . '/../src/providers/contextmanipulatorserviceprovider.class.inc.php';
@@ -109,6 +110,7 @@ $oApp->before(function(Symfony\Component\HttpFoundation\Request $oRequest, Silex
     $oApp['debug'] = $bDebug;
     $oApp['debug'] = $bDebug;
     $oApp['combodo.current_environment'] = utils::GetCurrentEnvironment();
     $oApp['combodo.current_environment'] = utils::GetCurrentEnvironment();
     $oApp['combodo.absolute_url'] = utils::GetAbsoluteUrlAppRoot();
     $oApp['combodo.absolute_url'] = utils::GetAbsoluteUrlAppRoot();
+    $oApp['combodo.modules.absolute_url'] = utils::GetAbsoluteUrlAppRoot() . 'env-' . utils::GetCurrentEnvironment();
     $oApp['combodo.portal.base.absolute_url'] = utils::GetAbsoluteUrlAppRoot() . 'env-' . utils::GetCurrentEnvironment() . '/itop-portal-base/portal/web/';
     $oApp['combodo.portal.base.absolute_url'] = utils::GetAbsoluteUrlAppRoot() . 'env-' . utils::GetCurrentEnvironment() . '/itop-portal-base/portal/web/';
     $oApp['combodo.portal.base.absolute_path'] = MODULESROOT . '/itop-portal-base/portal/web/';
     $oApp['combodo.portal.base.absolute_path'] = MODULESROOT . '/itop-portal-base/portal/web/';
     $oApp['combodo.portal.instance.absolute_url'] = utils::GetAbsoluteUrlAppRoot() . 'env-' . utils::GetCurrentEnvironment() . '/' . PORTAL_MODULE_ID . '/';
     $oApp['combodo.portal.instance.absolute_url'] = utils::GetAbsoluteUrlAppRoot() . 'env-' . utils::GetCurrentEnvironment() . '/' . PORTAL_MODULE_ID . '/';

+ 1 - 0
datamodels/2.x/itop-portal-base/ru.dict.itop-portal-base.php

@@ -84,6 +84,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
 Dict::Add('RU RU', 'Russian', 'Русский', array(
 Dict::Add('RU RU', 'Russian', 'Русский', array(
     'Brick:Portal:Manage:Name' => 'Управление элементами',
     'Brick:Portal:Manage:Name' => 'Управление элементами',
     'Brick:Portal:Manage:Table:NoData' => 'Нет элементов',
     'Brick:Portal:Manage:Table:NoData' => 'Нет элементов',
+    'Brick:Portal:Manage:Table:ItemActions' => 'Actions~~',
 ));
 ));
 
 
 // ObjectBrick brick
 // ObjectBrick brick