Parcourir la source

#185 Navigation Breadcrumb - A beta version, based on the navigation history. Comments welcome!

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@4000 a333f486-631f-4898-b8df-5754b55c2be0
romainq il y a 9 ans
Parent
commit
6de6d4fdcc

+ 9 - 0
application/displayblock.class.inc.php

@@ -1203,6 +1203,15 @@ EOF
 		$oNewCondition = Expression::FromOQL($sOQLCondition);
 		return $oNewCondition;		
 	}
+
+	/**
+	 * For the result to be meaningful, this function must be called AFTER GetRenderContent() (or Display())
+	 * @return int
+	 */
+	public function GetDisplayedCount()
+	{
+		return $this->m_oSet->Count();
+	}
 }
 
 /**

+ 57 - 11
application/itopwebpage.class.inc.php

@@ -37,6 +37,11 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
 	private $m_sMessage;
 	private $m_sInitScript;
 	protected $m_oTabs;
+	protected $sBreadCrumbEntryId;
+	protected $sBreadCrumbEntryLabel;
+	protected $sBreadCrumbEntryDescription;
+	protected $sBreadCrumbEntryUrl;
+	protected $sBreadCrumbEntryIcon;
 
 	public function __construct($sTitle, $bPrintable = false)
 	{
@@ -45,6 +50,12 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
 
 		ApplicationContext::SetUrlMakerClass('iTopStandardURLMaker');
 
+		$this->sBreadCrumbEntryId = null;
+		$this->sBreadCrumbEntryLabel = null;
+		$this->sBreadCrumbEntryDescription = null;
+		$this->sBreadCrumbEntryUrl = null;
+		$this->sBreadCrumbEntryIcon = '';
+
 		$this->m_sMenu = "";
 		$this->m_sMessage = '';
 		$this->SetRootUrl(utils::GetAbsoluteUrlAppRoot());
@@ -55,7 +66,7 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
 		$this->add_linked_stylesheet("../css/fg.menu.css");
 		$this->add_linked_stylesheet("../css/jquery.multiselect.css");
 		$this->add_linked_stylesheet("../css/magnific-popup.css");
-		
+
 		$this->add_linked_script('../js/jquery.layout.min.js');
 		$this->add_linked_script('../js/jquery.ba-bbq.min.js');
 		$this->add_linked_script("../js/jquery.treeview.js");
@@ -79,7 +90,8 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
 		$this->add_linked_script('../js/ajaxfileupload.js');
 		$this->add_linked_script('../js/jquery.mousewheel.js');
 		$this->add_linked_script('../js/jquery.magnific-popup.min.js');
-			
+		$this->add_linked_script('../js/breadcrumb.js');
+
 		
 		$sSearchAny = addslashes(Dict::S('UI:SearchValue:Any'));
 		$sSearchNbSelected = addslashes(Dict::S('UI:SearchValue:NbSelected'));
@@ -244,7 +256,7 @@ EOF;
 				});
 			}
 		});
-					
+
 		$('.resizable').filter(':visible').resizable();
 	}
 	catch(err)
@@ -517,7 +529,23 @@ EOF
 		}
 EOF
 		);
-	}	
+	}
+
+	/**
+	 * @param string $sId Identifies the item, to search after it in the current breadcrumb
+	 * @param string $sLabel Label of the breadcrumb item
+	 * @param string $sDescription More information, displayed as a tooltip
+	 * @param string $sUrl Specify a URL if the current URL as perceived on the browser side is not relevant
+	 * @param string $sIcon Icon (relative or absolute) path that will be displayed next to the label
+	 */
+	public function SetBreadCrumbEntry($sId, $sLabel, $sDescription, $sUrl = '', $sIcon = '')
+	{
+		$this->sBreadCrumbEntryId = $sId;
+		$this->sBreadCrumbEntryLabel = $sLabel;
+		$this->sBreadCrumbEntryDescription = $sDescription;
+		$this->sBreadCrumbEntryUrl = $sUrl;
+		$this->sBreadCrumbEntryIcon = $sIcon;
+	}
 
 	public function AddToMenu($sHtml)
 	{
@@ -643,7 +671,9 @@ EOF
 			'selectedList' => 1,
 		);
 		$sJSMultiselectOptions = json_encode($aMultiselectOptions);
-		
+
+		$siTopInstanceId = json_encode(APPROOT);
+		$sNewEntry = is_null($this->sBreadCrumbEntryId) ? 'null' : json_encode(array('id' => $this->sBreadCrumbEntryId, 'url' => $this->sBreadCrumbEntryUrl, 'label' => htmlentities($this->sBreadCrumbEntryLabel, ENT_QUOTES, 'UTF-8'), 'description' => htmlentities($this->sBreadCrumbEntryDescription, ENT_QUOTES, 'UTF-8'), 'icon' => $this->sBreadCrumbEntryIcon));
 		$this->add_ready_script(
 <<<EOF
 		// Since the event is only triggered when the hash changes, we need to trigger
@@ -655,6 +685,8 @@ EOF
 		
 		$('.multiselect').multiselect($sJSMultiselectOptions);
 
+		$('#itop-breadcrumb').breadcrumb({itop_instance_id: $siTopInstanceId, new_entry: $sNewEntry});
+
 		FixSearchFormsDisposition();
 
 EOF
@@ -897,7 +929,7 @@ EOF
 			
 			if (!empty($sNorthPane))
 			{
-				$sNorthPane = '<div id="bottom-pane" class="ui-layout-north">'.$sNorthPane.'</div>';
+				$sNorthPane = '<div id="top-pane" class="ui-layout-north">'.$sNorthPane.'</div>';
 			}
 			
 			if (!empty($sSouthPane))
@@ -944,13 +976,27 @@ EOF
 			$sHtml .= '</div>';
 
 			$sHtml .= '<div class="ui-layout-center">';
-			$sHtml .= ' <div id="top-bar" style="width:100%">';
+			$sHtml .= ' <div id="top-bar" class="ui-helper-clearfix" style="width:100%">';
 			$sHtml .= self::FilterXSS($sApplicationBanner);
+
+			$sHtml .= ' <table id="top-bar-table">';
+			$sHtml .= ' <tr>';
+			$sHtml .= ' <td id="top-bar-table-breadcrumb">';
+			$sHtml .= ' <div id="itop-breadcrumb"></div>';
+			$sHtml .= ' </td>';
+			$sHtml .= ' <td id="top-bar-table-search">';
 			$sHtml .= '		<div id="global-search"><form action="'.utils::GetAbsoluteUrlAppRoot().'pages/UI.php"><table><tr><td></td><td><div id="global-search-area"><input id="global-search-input" type="text" name="text" placeholder="'.$sText.'"></input><div '.$sOnClick.' id="global-search-image"></div></div></td>';
-			//$sHtml .= '<td><input type="image" src="../images/searchBtn.png"/></a></td>';
-			$sHtml .= '<td><a id="help-link" href="'.$sOnlineHelpUrl.'" target="_blank"><img title="'.Dict::S('UI:Help').'" src="../images/help.png?itopversion='.ITOP_VERSION.'"/></td>';
-			$sHtml .= '<td>'.self::FilterXSS($sLogOffMenu).'</td><td><input type="hidden" name="operation" value="full_text"/></td></tr></table></form></div>';
-			//echo '<td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<input type="hidden" name="operation" value="full_text"/></td></tr></table></form></div>';
+			$sHtml .= '     <td><a id="help-link" href="'.$sOnlineHelpUrl.'" target="_blank"><img title="'.Dict::S('UI:Help').'" src="../images/help.png?itopversion='.ITOP_VERSION.'"/></td>';
+			$sHtml .= '     <td>'.self::FilterXSS($sLogOffMenu).'</td><td><input type="hidden" name="operation" value="full_text"/></td></tr></table></form></div>';
+			$sHtml .= ' </td>';
+			$sHtml .= ' </tr>';
+			$sHtml .= ' </table>';
+
+//			$sHtml .= '		<div id="global-search"><form action="'.utils::GetAbsoluteUrlAppRoot().'pages/UI.php"><table><tr><td></td><td><div id="global-search-area"><input id="global-search-input" type="text" name="text" placeholder="'.$sText.'"></input><div '.$sOnClick.' id="global-search-image"></div></div></td>';
+//			$sHtml .= '<td><a id="help-link" href="'.$sOnlineHelpUrl.'" target="_blank"><img title="'.Dict::S('UI:Help').'" src="../images/help.png?itopversion='.ITOP_VERSION.'"/></td>';
+//			$sHtml .= '<td>'.self::FilterXSS($sLogOffMenu).'</td><td><input type="hidden" name="operation" value="full_text"/></td></tr></table></form></div>';
+//			$sHtml .= ' <div id="itop-breadcrumb"></div>';
+
 			$sHtml .= ' </div>';
 			$sHtml .= ' <div class="ui-layout-content" style="overflow:auto;">';
 			$sHtml .= ' <!-- Beginning of page content -->';

+ 68 - 9
application/menunode.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2013 Combodo SARL
+// Copyright (C) 2010-2016 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Construction and display of the application's main menu
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2016 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -297,16 +297,26 @@ class ApplicationMenu
 		$sMenuId = $oAppContext->GetCurrentValue('menu', null);		
 		if ($sMenuId  === null)
 		{
+			$sMenuId == self::GetDefaultMenuId();
+		}
+		return $sMenuId;
+	}	
+
+	static public function GetDefaultMenuId()
+	{
+		static $sDefaultMenuId = null;
+		if (is_null($sDefaultMenuId))
+		{
 			// Make sure the root menu is sorted on 'rank'
 			usort(self::$aRootMenus, array('ApplicationMenu', 'CompareOnRank'));
 			$oFirstGroup = self::GetMenuNode(self::$aRootMenus[0]['index']);
 			$aChildren = self::$aMenusIndex[$oFirstGroup->GetIndex()]['children'];
 			usort($aChildren, array('ApplicationMenu', 'CompareOnRank'));
 			$oMenuNode = self::GetMenuNode($aChildren[0]['index']);
-			$sMenuId = $oMenuNode->GetMenuId();
+			$sDefaultMenuId = $oMenuNode->GetMenuId();
 		}
-		return $sMenuId;
-	}	
+		return $sDefaultMenuId;
+	}
 }
 
 /**
@@ -339,6 +349,7 @@ abstract class MenuNode
 {
 	protected $sMenuId;
 	protected $index;
+	protected $iParentIndex;
 
 	/**
 	 * Properties reflecting how the node has been declared
@@ -379,6 +390,7 @@ abstract class MenuNode
 	public function __construct($sMenuId, $iParentIndex = -1, $fRank = 0, $sEnableClass = null, $iActionCode = null, $iAllowedResults = UR_ALLOWED_YES, $sEnableStimulus = null)
 	{
 		$this->sMenuId = $sMenuId;
+		$this->iParentIndex = $iParentIndex;
 		$this->aReflectionProperties = array();
 		if (strlen($sEnableClass) > 0)
 		{
@@ -411,7 +423,21 @@ abstract class MenuNode
 	
 	public function GetLabel()
 	{
-		return Dict::S("Menu:$this->sMenuId+", "");
+		$sRet = Dict::S("Menu:$this->sMenuId+", "");
+		if ($sRet === '')
+		{
+			if ($this->iParentIndex != -1)
+			{
+				$oParentMenu = ApplicationMenu::GetMenuNode($this->iParentIndex);
+				$sRet = $oParentMenu->GetTitle().' / '.$this->GetTitle();
+			}
+			else
+			{
+				$sRet = $this->GetTitle();
+			}
+			//$sRet = $this->GetTitle();
+		}
+		return $sRet;
 	}
 	
 	public function GetIndex()
@@ -647,11 +673,12 @@ class OQLMenuNode extends MenuNode
 			$this->bSearch, // Search pane
 			true, // Search open
 			$oPage, 
-			array_merge($this->m_aParams, $aExtraParams)
+			array_merge($this->m_aParams, $aExtraParams),
+			true
 		);
 	}
 
-	public static function RenderOQLSearch($sOql, $sTitle, $sUsageId, $bSearchPane, $bSearchOpen, WebPage $oPage, $aExtraParams = array())
+	public static function RenderOQLSearch($sOql, $sTitle, $sUsageId, $bSearchPane, $bSearchOpen, WebPage $oPage, $aExtraParams = array(), $bEnableBreadcrumb = false)
 	{
 		$sUsageId = utils::GetSafeId($sUsageId);
 		$oSearch = DBObjectSearch::FromOQL($sOql);
@@ -669,6 +696,15 @@ class OQLMenuNode extends MenuNode
 		$aParams = array_merge(array('table_id' => $sUsageId), $aExtraParams);
 		$oBlock = new DisplayBlock($oSearch, 'list', false /* Asynchronous */, $aParams);
 		$oBlock->Display($oPage, $sUsageId);
+
+		if ($bEnableBreadcrumb && ($oPage instanceof iTopWebPage))
+		{
+			// Breadcrumb
+			//$iCount = $oBlock->GetDisplayedCount();
+			$sPageId = "ui-search-".$oSearch->GetClass();
+			$sLabel = MetaModel::GetName($oSearch->GetClass());
+			$oPage->SetBreadCrumbEntry($sPageId, $sLabel, $sTitle, '', '../images/breadcrumb-search.png');
+		}
 	}
 }
 
@@ -920,6 +956,29 @@ EOF
 				$sId = addslashes($this->sMenuId);
 				$oPage->add_ready_script("EditDashboard('$sId');");
 			}
+			else
+			{
+				$oParentMenu = ApplicationMenu::GetMenuNode($this->iParentIndex);
+				$sParentTitle = $oParentMenu->GetTitle();
+				$sThisTitle = $this->GetTitle();
+				if ($sParentTitle != $sThisTitle)
+				{
+					$sDescription = $sParentTitle.' / '.$sThisTitle;
+				}
+				else
+				{
+					$sDescription = $sThisTitle;
+				}
+				if ($this->sMenuId == ApplicationMenu::GetDefaultMenuId())
+				{
+					$sIcon = '../images/breadcrumb_home.png';
+				}
+				else
+				{
+					$sIcon = '../images/breadcrumb-dashboard.png';
+				}
+				$oPage->SetBreadCrumbEntry("ui-dashboard-".$this->sMenuId, $this->GetTitle(), $sDescription, '', $sIcon);
+			}
 		}
 		else
 		{
@@ -950,7 +1009,7 @@ EOF
 		}
 		else
 		{
-			$oPage->p("Error: failed to load dashboard file: '{$this->sDashboardFile}'");
+			throw new Exception("Error: failed to load dashboard file: '{$this->sDashboardFile}'");
 		}
 	}
 	

+ 3 - 3
application/shortcut.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2013 Combodo SARL
+// Copyright (C) 2010-2016 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -21,7 +21,7 @@
  * Persistent class Shortcut and derived
  * Shortcuts of any kind
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2016 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -200,7 +200,7 @@ class ShortcutOQL extends Shortcut
 		$bSearchOpen = false;
 		try
 		{
-			OQLMenuNode::RenderOQLSearch($this->Get('oql'), $this->Get('name'), 'shortcut_'.$this->GetKey(), $bSearchPane, $bSearchOpen, $oPage, $aExtraParams);
+			OQLMenuNode::RenderOQLSearch($this->Get('oql'), $this->Get('name'), 'shortcut_'.$this->GetKey(), $bSearchPane, $bSearchOpen, $oPage, $aExtraParams, true);
 		}
 		catch (Exception $e)
 		{

+ 68 - 1
css/light-grey.css

@@ -1151,7 +1151,6 @@ div#logo div {
 
 
 #top-bar {
-  height: 55px;
   background: #f1f1f1;
   text-align: right;
 }
@@ -2201,3 +2200,71 @@ span.refresh-button {
 }
 
 
+#top-bar-table {
+  width: 100%;
+}
+
+#top-bar-table #top-bar-table-search {
+  width: 347px;
+}
+
+
+#itop-breadcrumb {
+  overflow: hidden;
+  float: left;
+  background: #f1f1f1;
+  margin-left: -20px;
+}
+
+#itop-breadcrumb li {
+  list-style: none;
+  float: left;
+  margin: 0 22px 6px 0;
+}
+#itop-breadcrumb li .icon img {
+  height: 15px;
+  width: auto;
+  margin-right: 5px;
+  -webkit-filter: grayscale(100%);
+  filter: grayscale(100%);
+  filter: gray;
+  filter: url("data:image/svg+xml;utf8,<svg version='1.1' xmlns='http://www.w3.org/2000/svg' height='0'><filter id='greyscale'><feColorMatrix type='matrix' values='0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0' /></filter></svg>#greyscale");
+  opacity: 0.5;
+}
+#itop-breadcrumb li:hover .icon img {
+  -webkit-filter: none;
+  filter: none;
+  opacity: 1;
+}
+#itop-breadcrumb li a {
+  text-decoration: none;
+  color: #555555;
+  font-size: 9pt;
+  padding: 0;
+  background: none;
+}
+#itop-breadcrumb li a span.truncate {
+  max-width: 200px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: inline-block;
+}
+#itop-breadcrumb li a:hover {
+  text-decoration: none;
+  color: #e87c1e;
+}
+#itop-breadcrumb li a::after {
+  content: '';
+  position: absolute;
+  background-image: url(../images/breadcrumb-separator.png?v=v2.2.0);
+  background-repeat: no-repeat;
+  width: 8px;
+  height: 16px;
+  margin-left: 5px;
+}
+#itop-breadcrumb li:last-child a::after {
+  display: none;
+}
+
+

+ 81 - 2
css/light-grey.scss

@@ -8,6 +8,10 @@ $popup-menu-highlight-color = $highlight-color;
 $popup-menu-text-color: #000;
 $popup-menu-background-color: #fff;
 $popup-menu-text-higlight-color: #fff;
+$breadcrumb-color: #555;
+$breadcrumb-text-color: #fff;
+$breadcrumb-highlight-color: $highlight-color;
+$breadcrumb-text-highlight-color: #fff;
 
 /* CSS Document */
 body {
@@ -868,7 +872,6 @@ div#logo div {
 	overflow: hidden;
 }
 #top-bar {
-	height: 55px;
 	background: $frame-background-color;
 	text-align: right;
 }
@@ -1615,4 +1618,80 @@ span.refresh-button {
 }
 .history_entry_truncated .history_truncated_toggler {
 	background-position: 0 -192px;
-}
+}
+
+#top-bar-table {
+  width: 100%;
+  #top-bar-table-search{
+    width: 347px;
+  }
+}
+
+#itop-breadcrumb{
+  overflow: hidden;
+  float: left;
+  background: $frame-background-color;
+  margin-left: -20px;
+
+  li{
+	list-style: none;
+	float: left;
+	margin: 0 22px 6px 0;
+
+	.icon img{
+	  height: 15px;
+	  width: auto;
+	  margin-right: 5px;
+
+	  -webkit-filter: unquote("grayscale(100%)");
+	  filter: unquote("grayscale(100%)");
+	  filter: unquote("gray");
+	  filter: url("data:image/svg+xml;utf8,<svg version='1.1' xmlns='http://www.w3.org/2000/svg' height='0'><filter id='greyscale'><feColorMatrix type='matrix' values='0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0' /></filter></svg>#greyscale");
+
+	  // IE has no filter option: at least, have some effect when hovering...
+	  opacity: 0.5;
+	}
+
+	&:hover .icon img{
+	  -webkit-filter: none;
+	  filter: none;
+	  opacity: 1;
+	}
+
+	a{
+	  text-decoration: none;
+	  color: #555;
+	  font-size: 9pt;
+	  padding: 0;
+	  background: none;
+	  span.truncate
+	  {
+		// Ellipsis
+		max-width: 200px;
+		white-space: nowrap;
+		overflow: hidden;
+		text-overflow: ellipsis;
+		display: inline-block;
+	  }
+
+	  &:hover{
+		text-decoration: none;
+		color: $highlight-color;
+	  }
+
+	  &::after{
+		content:'';
+		position: absolute;
+		background-image: url(../images/breadcrumb-separator.png?v=#{$version});
+		background-repeat: no-repeat;
+		width: 8px;
+		height: 16px;
+		margin-left: 5px;
+	  }
+	}
+
+	&:last-child a::after{
+	  display: none;
+	}
+  }
+}

BIN
images/breadcrumb-dashboard.png


BIN
images/breadcrumb-search.png


BIN
images/breadcrumb-separator.png


BIN
images/breadcrumb_home.png


+ 111 - 0
js/breadcrumb.js

@@ -0,0 +1,111 @@
+//iTop Form field
+;
+$(function()
+{
+	// the widget definition, where 'itop' is the namespace,
+	// 'breadcrumb' the widget name
+	$.widget( 'itop.breadcrumb',
+	{
+		// default options
+		options:
+		{
+			itop_instance_id: '',
+			new_entry: null,
+		},
+   
+		// the constructor
+		_create: function()
+		{
+			var me = this;
+			
+			this.element
+			.addClass('breadcrumb');
+
+			if(typeof(Storage) !== "undefined")
+			{
+				var sBreadCrumbStorageKey = this.options.itop_instance_id + 'breadcrumb-v1';
+				var aBreadCrumb = [];
+				var sBreadCrumbData = sessionStorage.getItem(sBreadCrumbStorageKey);
+				if (sBreadCrumbData !== null)
+				{
+					aBreadCrumb = JSON.parse(sBreadCrumbData);
+				}
+				var iDisplayableItems = aBreadCrumb.length;
+
+                if (this.options.new_entry !== null) {
+                    var sUrl = this.options.new_entry.url;
+                    if (sUrl.length == 0) {
+                        sUrl = window.location.href;
+                    }
+                    // Eliminate items having the same id, before appending the new item
+                    var aBreadCrumb = $.grep(aBreadCrumb, function(item, ipos){
+                        if (item.id == me.options.new_entry.id) return false;
+                        else return true;
+                    });
+                    aBreadCrumb.push({
+                        id: this.options.new_entry.id,
+                        label: this.options.new_entry.label,
+						description: this.options.new_entry.description,
+                        icon: this.options.new_entry.icon,
+                        url: sUrl
+                    });
+                    // Keep only the last N items
+                    aBreadCrumb = aBreadCrumb.slice(-8);
+					// Do not show the last = current item
+					iDisplayableItems = aBreadCrumb.length - 1;
+                }
+				sBreadCrumbData = JSON.stringify(aBreadCrumb);
+				sessionStorage.setItem(sBreadCrumbStorageKey, sBreadCrumbData);
+				var sBreadCrumbHtml = '<ul>';
+				for (iEntry in aBreadCrumb)
+				{
+                    //if (iEntry >= iDisplayableItems) break; // skip the current page
+					var oEntry = aBreadCrumb[iEntry];
+					if (oEntry['label'].length > 0)
+					{
+                        var sIconSpec = '';
+                        if (oEntry['icon'].length > 0)
+                        {
+                            sIconSpec = '<span class="icon"><img src="'+oEntry['icon']+'"/></span>';
+                        }
+						var sTitle = oEntry['description'];
+						if (sTitle.length == 0) {
+							sTitle = oEntry['label'];
+						}
+						sBreadCrumbHtml += '<li><a class="itop-breadcrumb-link" breadcrumb-entry="'+iEntry+'" href="'+oEntry['url']+'" title="'+sTitle+'">'+sIconSpec+'<span class="truncate">'+oEntry['label']+'</span></a></li>';
+					}
+				}
+				sBreadCrumbHtml += '</ul>';
+				$('#itop-breadcrumb').html(sBreadCrumbHtml);
+			}
+			else
+			{
+				// Sorry! No Web Storage support..
+				//$('#itop-breadcrumb').html('<span style="display:none;">Session storage not available for the current browser</span>');
+			}
+		},
+		// called when created, and later when changing options
+		_refresh: function()
+		{
+
+		},
+		// events bound via _bind are removed automatically
+		// revert other modifications here
+		_destroy: function()
+		{
+			this.element
+			.removeClass('breadcrumb');
+		},
+		// _setOptions is called with a hash of all options that are changing
+		// always refresh when changing options
+		_setOptions: function()
+		{
+			this._superApply(arguments);
+		},
+		// _setOption is called for each individual option that is changing
+		_setOption: function( key, value )
+		{
+			this._super( key, value );
+		}
+	});
+});

+ 24 - 16
pages/UI.php

@@ -20,7 +20,7 @@
 /**
  * Main page of iTop
  *
- * @copyright   Copyright (C) 2010-2015 Combodo SARL
+ * @copyright   Copyright (C) 2010-2016 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -174,6 +174,12 @@ function DisplaySearchSet($oP, $oFilter, $bSearchForm = true, $sBaseClass = '',
 		{
 			$oBlock = new DisplayBlock($oFilter, 'list', false);
 			$oBlock->Display($oP, 1);
+
+			// Breadcrumb
+			//$iCount = $oBlock->GetDisplayedCount();
+			$sPageId = "ui-search-".$oFilter->GetClass();
+			$sLabel = MetaModel::GetName($oFilter->GetClass());
+			$oP->SetBreadCrumbEntry($sPageId, $sLabel, '', '', '../images/breadcrumb-search.png');
 		}
 	}
 }
@@ -356,11 +362,13 @@ try
 				}
 				if (!is_null($oObj))
 				{
+					$sClass = get_class($oObj); // get the leaf class
+					$oP->SetBreadCrumbEntry("ui-details-$sClass-$id", $oObj->Get('friendlyname'), $sClass.': '.$oObj->Get('friendlyname'), '', MetaModel::GetClassIcon($sClass, false));
 					DisplayDetails($oP, $sClass, $oObj, $id);
 				}				
 			}
 		break;
-		
+
 		case 'release_lock_and_details':
 		$sClass = utils::ReadParam('class', '');
 		$id = utils::ReadParam('id', '');
@@ -414,19 +422,19 @@ try
 		///////////////////////////////////////////////////////////////////////////////////////////
 
 		case 'search_form': // Search form
-		$sClass = utils::ReadParam('class', '', false, 'class');
-		$sFormat = utils::ReadParam('format', 'html');
-		$bSearchForm = utils::ReadParam('search_form', true);
-		$bDoSearch = utils::ReadParam('do_search', true);
-		if (empty($sClass))
-		{
-			throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'class'));
-		}
-		$oP->set_title(Dict::S('UI:SearchResultsPageTitle'));
-		$oFilter =  new DBObjectSearch($sClass);
-		DisplaySearchSet($oP, $oFilter, $bSearchForm, '' /* sBaseClass */, $sFormat, $bDoSearch);
-		break;
-		
+			$sClass = utils::ReadParam('class', '', false, 'class');
+			$sFormat = utils::ReadParam('format', 'html');
+			$bSearchForm = utils::ReadParam('search_form', true);
+			$bDoSearch = utils::ReadParam('do_search', true);
+			if (empty($sClass))
+			{
+				throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'class'));
+			}
+			$oP->set_title(Dict::S('UI:SearchResultsPageTitle'));
+			$oFilter =  new DBObjectSearch($sClass);
+			DisplaySearchSet($oP, $oFilter, $bSearchForm, '' /* sBaseClass */, $sFormat, $bDoSearch);
+			break;
+
 		///////////////////////////////////////////////////////////////////////////////////////////
 
 		case 'search': // Serialized DBSearch
@@ -1633,7 +1641,7 @@ catch(Exception $e)
 	require_once(APPROOT.'/setup/setuppage.class.inc.php');
 	$oP = new SetupPage(Dict::S('UI:PageTitle:FatalError'));
 	$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");	
-	$oP->error(Dict::Format('UI:Error_Details', $e->getMessage()));		
+	$oP->error(Dict::Format('UI:Error_Details', $e->getMessage()));
 	$oP->output();
 
 	if (MetaModel::IsLogEnabledIssue())

+ 9 - 3
pages/UniversalSearch.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2015 Combodo SARL
+// Copyright (C) 2010-2016 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Analytical search
  *
- * @copyright   Copyright (C) 2010-2015 Combodo SARL
+ * @copyright   Copyright (C) 2010-2016 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -113,7 +113,13 @@ if ($oFilter != null)
 	// Search results	
 	$oResultBlock = new DisplayBlock($oFilter, 'list', false);
 	$oResultBlock->Display($oP, 1);
-	
+
+	// Breadcrumb
+	//$iCount = $oBlock->GetDisplayedCount();
+	$sPageId = "ui-search-".$oFilter->GetClass();
+	$sLabel = MetaModel::GetName($oFilter->GetClass());
+	$oP->SetBreadCrumbEntry($sPageId, $sLabel, '', '', '../images/breadcrumb-search.png');
+
 	// Menu node
 	$sFilter = $oFilter->ToOQL();
 	$oP->add("\n<!-- $sFilter -->\n");

+ 4 - 2
pages/notifications.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2013 Combodo SARL
+// Copyright (C) 2013-2016 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Page to configuration the notifications (triggers and actions)
  *
- * @copyright   Copyright (C) 2013 Combodo SARL
+ * @copyright   Copyright (C) 2013-2016 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -43,6 +43,8 @@ $oP->add('<div class="page_header" style="padding:0.5em;">');
 $oP->add('<h1>'.dict::S('UI:NotificationsMenu:Title').'</h1>');
 $oP->add('</div>');
 
+$oP->SetBreadCrumbEntry('ui-tool-notifications', Dict::S('Menu:NotificationsMenu'), Dict::S('Menu:NotificationsMenu+'), '', '../images/bell.png');
+
 $oP->StartCollapsibleSection(Dict::S('UI:NotificationsMenu:Help'), true);
 $oP->add('<div style="padding: 1em; font-size:10pt;background:#E8F3CF;margin-top: 0.25em;">');
 $oP->add('<img src="../images/bell.png" style="margin-top: -60px; margin-right: 10px; float: right;">');

+ 23 - 0
pages/run_query.php

@@ -192,6 +192,29 @@ try
 		$oResultBlock = new DisplayBlock($oFilter, 'list', false);
 		$oResultBlock->Display($oP, 'runquery');
 
+		// Breadcrumb
+		//$iCount = $oResultBlock->GetDisplayedCount();
+		$sPageId = "ui-search-".$oFilter->GetClass();
+		$sLabel = MetaModel::GetName($oFilter->GetClass());
+		$aArgs = array();
+		foreach (array_merge($_POST, $_GET) as $sKey => $value)
+		{
+			if (is_array($value))
+			{
+				$aItems = array();
+				foreach($value as $sItemKey => $sItemValue)
+				{
+					$aArgs[] = $sKey.'['.$sItemKey.']='.urlencode($sItemValue);
+				}
+			}
+			else
+			{
+				$aArgs[] = $sKey.'='.urlencode($value);
+			}
+		}
+		$sUrl = utils::GetAbsoluteUrlAppRoot().'pages/run_query.php?'.implode('&', $aArgs);
+		$oP->SetBreadCrumbEntry($sPageId, $sLabel, $oFilter->ToOQL(true), $sUrl, '../images/breadcrumb-search.png');
+
 		$oP->p('');
 		$oP->StartCollapsibleSection(Dict::S('UI:RunQuery:MoreInfo'), false);
 		$oP->p(Dict::S('UI:RunQuery:DevelopedQuery').htmlentities($oFilter->ToOQL(), ENT_QUOTES, 'UTF-8'));