Forráskód Böngészése

#185 Navigation Breadcrumb - Beta version
- Any page has a breadcrumb (except if POST and a number of pages like "new object")
- Added Home + Menu buttons showed when the left pane is closed
- Configuration: breadcrumb.max_count (0 to disable)


git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@4022 a333f486-631f-4898-b8df-5754b55c2be0

romainq 9 éve
szülő
commit
8fd6012a13

+ 88 - 35
application/itopwebpage.class.inc.php

@@ -37,6 +37,7 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
 	private $m_sMessage;
 	private $m_sInitScript;
 	protected $m_oTabs;
+	protected $bBreadCrumbEnabled;
 	protected $sBreadCrumbEntryId;
 	protected $sBreadCrumbEntryLabel;
 	protected $sBreadCrumbEntryDescription;
@@ -50,11 +51,15 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
 
 		ApplicationContext::SetUrlMakerClass('iTopStandardURLMaker');
 
-		$this->sBreadCrumbEntryId = null;
-		$this->sBreadCrumbEntryLabel = null;
-		$this->sBreadCrumbEntryDescription = null;
-		$this->sBreadCrumbEntryUrl = null;
-		$this->sBreadCrumbEntryIcon = '';
+		if ((count($_POST) == 0) || (array_key_exists('loginop', $_POST)))
+		{
+			// Create a breadcrumb entry for the current page, but get its title as late as possible (page title could be changed later)
+			$this->bBreadCrumbEnabled = true;
+		}
+		else
+		{
+			$this->bBreadCrumbEnabled = false;
+		}
 
 		$this->m_sMenu = "";
 		$this->m_sMessage = '';
@@ -107,45 +112,44 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
 		}
 	}
 
-	protected function PrepareLayout()
+	protected function IsMenuPaneVisible()
 	{
-		$bForceMenuPane = utils::ReadParam('force_menu_pane', null);
-		$sInitClosed = '';
-		if (($bForceMenuPane !== null) && ($bForceMenuPane == 0))
+		$bLeftPaneOpen = true;
+		if (MetaModel::GetConfig()->Get('demo_mode'))
 		{
-			$sInitClosed = 'initClosed: true,';
+			// Leave the pane opened
 		}
+		else
+		{
+			if (utils::ReadParam('force_menu_pane', null) === 0)
+			{
+				$bLeftPaneOpen = false;
+			}
+			elseif (appUserPreferences::GetPref('menu_pane', 'open') == 'closed')
+			{
+				$bLeftPaneOpen = false;
+			}
+		}
+		return $bLeftPaneOpen;
+	}
 
-		$this->add_script(
-<<<EOF
-function ShowAboutBox()
-{
-	$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', {operation: 'about_box'}, function(data){
-		$('body').append(data);
-	});
-	return false;
-}
-EOF
-		);
-
+	protected function PrepareLayout()
+	{
+		$bLeftPaneOpen = true;
 		if (MetaModel::GetConfig()->Get('demo_mode'))
 		{
-			// Leave the pane opened
+			// No pin button
 			$sConfigureWestPane = '';
 		}
 		else
 		{
 			$sConfigureWestPane =
 <<<EOF
-				if (GetUserPreference('menu_pane', 'open') == 'closed')
-				{
-					myLayout.close('west');
-				}
 				myLayout.addPinBtn( "#tPinMenu", "west" );
 EOF;
 		}
+		$sInitClosed = $this->IsMenuPaneVisible() ? '' : 'initClosed: true,';
 
-		
 		$sJSDisconnectedMessage = json_encode(Dict::S('UI:DisconnectedDlgMessage'));
 		$sJSTitle = json_encode(Dict::S('UI:DisconnectedDlgTitle'));
 		$sJSLoginAgain = json_encode(Dict::S('UI:LoginAgain'));
@@ -172,11 +176,12 @@ EOF;
 		paneSize = GetUserPreference('menu_size', 300)
 		myLayout = $('body').layout({
 			west :	{
-						$sInitClosed minSize: 200, size: paneSize, spacing_open: 16, spacing_close: 16, slideTrigger_open: "mouseover", hideTogglerOnSlide: true, enableCursorHotkey: false,
+						$sInitClosed minSize: 200, size: paneSize, spacing_open: 16, spacing_close: 16, slideTrigger_open: "click", hideTogglerOnSlide: true, enableCursorHotkey: false,
 						onclose_end: function(name, elt, state, options, layout)
 						{
 								if (state.isSliding == false)
 								{
+									$('.menu-pane-exclusive').show();
 									SetUserPreference('menu_pane', 'closed', true);
 								}
 						},
@@ -192,6 +197,7 @@ EOF;
 						{
 							if (state.isSliding == false)
 							{
+								$('.menu-pane-exclusive').hide();
 								SetUserPreference('menu_pane', 'open', true);
 							}
 						}
@@ -570,6 +576,7 @@ EOF
 	 */
 	public function SetBreadCrumbEntry($sId, $sLabel, $sDescription, $sUrl = '', $sIcon = '')
 	{
+		$this->bBreadCrumbEnabled = true;
 		$this->sBreadCrumbEntryId = $sId;
 		$this->sBreadCrumbEntryLabel = $sLabel;
 		$this->sBreadCrumbEntryDescription = $sDescription;
@@ -577,6 +584,19 @@ EOF
 		$this->sBreadCrumbEntryIcon = $sIcon;
 	}
 
+	/**
+	 * State that there will be no breadcrumb item for the current page
+	 */
+	public function DisableBreadCrumb()
+	{
+		$this->bBreadCrumbEnabled = false;
+		$this->sBreadCrumbEntryId = null;
+		$this->sBreadCrumbEntryLabel = null;
+		$this->sBreadCrumbEntryDescription = null;
+		$this->sBreadCrumbEntryUrl = null;
+		$this->sBreadCrumbEntryIcon = null;
+	}
+
 	public function AddToMenu($sHtml)
 	{
 		$this->m_sMenu .= $sHtml;
@@ -701,9 +721,6 @@ 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
@@ -715,12 +732,38 @@ EOF
 		
 		$('.multiselect').multiselect($sJSMultiselectOptions);
 
-		$('#itop-breadcrumb').breadcrumb({itop_instance_id: $siTopInstanceId, new_entry: $sNewEntry});
-
 		FixSearchFormsDisposition();
-
 EOF
 		);
+
+		$iBreadCrumbMaxCount = utils::GetConfig()->Get('breadcrumb.max_count');
+		if ($iBreadCrumbMaxCount > 1)
+		{
+			$siTopInstanceId = json_encode(APPROOT);
+			if ($this->bBreadCrumbEnabled)
+			{
+				if (is_null($this->sBreadCrumbEntryId))
+				{
+					$this->sBreadCrumbEntryId = $this->s_title;
+					$this->sBreadCrumbEntryLabel = $this->s_title;
+					$this->sBreadCrumbEntryDescription = $this->s_title;
+					$this->sBreadCrumbEntryUrl = '';
+					$this->sBreadCrumbEntryIcon = utils::GetAbsoluteUrlAppRoot().'images/wrench.png';
+				}
+				$sNewEntry = 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));
+			}
+			else
+			{
+				$sNewEntry = 'null';
+			}
+
+			$this->add_ready_script(
+<<<EOF
+		$('#itop-breadcrumb').breadcrumb({itop_instance_id: $siTopInstanceId, new_entry: $sNewEntry, max_count: $iBreadCrumbMaxCount});
+EOF
+			);
+		}
+
 		if ($this->GetOutputFormat() == 'html')
 		{
 			foreach($this->a_headers as $s_header)
@@ -1009,8 +1052,18 @@ EOF
 			$sHtml .= ' <div id="top-bar" class="ui-helper-clearfix" style="width:100%">';
 			$sHtml .= self::FilterXSS($sApplicationBanner);
 
+			$GoHomeInitialStyle = $this->IsMenuPaneVisible() ? 'display: none;' : '';
+
 			$sHtml .= ' <table id="top-bar-table">';
 			$sHtml .= ' <tr>';
+			$sHtml .= ' <td id="open-left-pane"  class="menu-pane-exclusive" style="'.$GoHomeInitialStyle.'" onclick="$(\'body\').layout().open(\'west\');">';
+			$sHtml .= ' <img src="../images/menu.png">';
+			$sHtml .= ' </td>';
+			$sHtml .= ' <td id="go-home" class="menu-pane-exclusive" style="'.$GoHomeInitialStyle.'">';
+			$sHtml .= ' <a href="'.utils::GetAbsoluteUrlAppRoot().'pages/UI.php"><img src="../images/home.png"></a>';
+			$sHtml .= ' </td>';
+			$sHtml .= ' <td class="top-bar-spacer menu-pane-exclusive" style="'.$GoHomeInitialStyle.'">';
+			$sHtml .= ' </td>';
 			$sHtml .= ' <td id="top-bar-table-breadcrumb">';
 			$sHtml .= ' <div id="itop-breadcrumb"></div>';
 			$sHtml .= ' </td>';

+ 8 - 0
core/config.class.inc.php

@@ -906,6 +906,14 @@ class Config
 			'source_of_value' => '',
 			'show_in_conf_sample' => true,
 		),
+		'breadcrumb.max_count' => array(
+			'type' => 'integer',
+			'description' => 'Maximum number of items kept in the history breadcrumb. Set it to 0 to entirely disable the breadcrumb.',
+			'default' => 8,
+			'value' => 8,
+			'source_of_value' => '',
+			'show_in_conf_sample' => false,
+		),
 	);
 
 	public function IsProperty($sPropCode)

+ 50 - 7
css/light-grey.css

@@ -2202,6 +2202,32 @@ span.refresh-button {
 
 #top-bar-table {
   width: 100%;
+  padding-left: 5px;
+}
+
+#top-bar-table #open-left-pane {
+  text-align: center;
+  width: 40px !important;
+  cursor: pointer;
+}
+
+#top-bar-table #go-home {
+  text-align: center;
+  width: 40px !important;
+}
+#top-bar-table #go-home a {
+  text-decoration: none;
+  color: #555555;
+  font-size: 9pt;
+  padding: 0;
+  background: none;
+  display: inline-block;
+  line-height: 55px;
+  width: 100%;
+}
+
+#top-bar-table .top-bar-spacer {
+  width: 35px !important;
 }
 
 #top-bar-table #top-bar-table-search {
@@ -2213,13 +2239,16 @@ span.refresh-button {
   overflow: hidden;
   float: left;
   background: #f1f1f1;
-  margin-left: -20px;
+}
+
+#itop-breadcrumb ul {
+  display: inline;
 }
 
 #itop-breadcrumb li {
   list-style: none;
   float: left;
-  margin: 0 22px 6px 0;
+  margin: 0 22px 0px 0;
 }
 #itop-breadcrumb li .icon img {
   height: 15px;
@@ -2231,11 +2260,6 @@ span.refresh-button {
   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;
@@ -2243,6 +2267,11 @@ span.refresh-button {
   padding: 0;
   background: none;
 }
+#itop-breadcrumb li a:hover .icon img {
+  -webkit-filter: none;
+  filter: none;
+  opacity: 1;
+}
 #itop-breadcrumb li a span.truncate {
   max-width: 200px;
   white-space: nowrap;
@@ -2266,6 +2295,20 @@ span.refresh-button {
 #itop-breadcrumb li:last-child a::after {
   display: none;
 }
+#itop-breadcrumb li .itop-breadcrumb-current {
+  text-decoration: none;
+  color: #555555;
+  font-size: 9pt;
+  padding: 0;
+  background: none;
+}
+#itop-breadcrumb li .itop-breadcrumb-current span.truncate {
+  max-width: 200px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: inline-block;
+}
 
 
 .ui-datepicker-buttonpane, .ui-timepicker-div {

+ 61 - 8
css/light-grey.scss

@@ -1622,6 +1622,37 @@ span.refresh-button {
 
 #top-bar-table {
   width: 100%;
+  padding-left: 5px;
+
+  $top-button-width: 40px;
+  $top-button-heigth: 55px;
+  $top-button-spacer: 35px;
+
+  #open-left-pane {
+	text-align: center;
+	width: $top-button-width !important;
+	cursor: pointer;
+  }
+  #go-home {
+	text-align: center;
+	width: $top-button-width !important;
+
+	a {
+	  text-decoration: none;
+	  color: #555;
+	  font-size: 9pt;
+	  padding: 0;
+	  background: none;
+
+	  // Make the whole cell clickable
+	  display: inline-block;
+	  line-height: $top-button-heigth;
+	  width: 100%;
+	}
+  }
+  .top-bar-spacer{
+	width: $top-button-spacer !important;
+  }
   #top-bar-table-search{
     width: 347px;
   }
@@ -1631,12 +1662,15 @@ span.refresh-button {
   overflow: hidden;
   float: left;
   background: $frame-background-color;
-  margin-left: -20px;
+
+  ul {
+	display: inline;
+  }
 
   li{
 	list-style: none;
 	float: left;
-	margin: 0 22px 6px 0;
+	margin: 0 22px 0px 0;
 
 	.icon img{
 	  height: 15px;
@@ -1652,18 +1686,19 @@ span.refresh-button {
 	  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;
+
+	  &:hover .icon img{
+		-webkit-filter: none;
+		filter: none;
+		opacity: 1;
+	  }
+
 	  span.truncate
 	  {
 		// Ellipsis
@@ -1693,6 +1728,24 @@ span.refresh-button {
 	&:last-child a::after{
 	  display: none;
 	}
+
+	.itop-breadcrumb-current{
+	  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;
+	  }
+
+	}
   }
 }
 .ui-datepicker-buttonpane, .ui-timepicker-div {

BIN
images/home.png


BIN
images/menu.png


BIN
images/wrench.png


+ 48 - 14
js/breadcrumb.js

@@ -11,6 +11,7 @@ $(function()
 		{
 			itop_instance_id: '',
 			new_entry: null,
+			max_count: 8
 		},
    
 		// the constructor
@@ -23,14 +24,12 @@ $(function()
 
 			if(typeof(Storage) !== "undefined")
 			{
-				var sBreadCrumbStorageKey = this.options.itop_instance_id + 'breadcrumb-v1';
-				var aBreadCrumb = [];
-				var sBreadCrumbData = sessionStorage.getItem(sBreadCrumbStorageKey);
-				if (sBreadCrumbData !== null)
+				$(window).bind( 'hashchange', function(e)
 				{
-					aBreadCrumb = JSON.parse(sBreadCrumbData);
-				}
-				var iDisplayableItems = aBreadCrumb.length;
+					me._RefreshLatestEntry();
+				});
+
+				aBreadCrumb = this._Read();
 
                 if (this.options.new_entry !== null) {
                     var sUrl = this.options.new_entry.url;
@@ -49,13 +48,10 @@ $(function()
                         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;
+                    // Keep only the last <max_count> items
+                    aBreadCrumb = aBreadCrumb.slice(-(this.options.max_count));
                 }
-				sBreadCrumbData = JSON.stringify(aBreadCrumb);
-				sessionStorage.setItem(sBreadCrumbStorageKey, sBreadCrumbData);
+				this._Write(aBreadCrumb);
 				var sBreadCrumbHtml = '<ul>';
 				for (iEntry in aBreadCrumb)
 				{
@@ -72,7 +68,15 @@ $(function()
 						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>';
+						if ((this.options.new_entry !== null) && (iEntry == aBreadCrumb.length - 1))
+						{
+							// Last entry is the current page
+							sBreadCrumbHtml += '<li><div class="itop-breadcrumb-current" breadcrumb-entry="'+iEntry+'" title="'+sTitle+'">'+sIconSpec+'<span class="truncate">'+oEntry['label']+'</span></div></li>';
+						}
+						else
+						{
+							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>';
@@ -106,6 +110,36 @@ $(function()
 		_setOption: function( key, value )
 		{
 			this._super( key, value );
+		},
+		_Read: function()
+		{
+			var sBreadCrumbStorageKey = this.options.itop_instance_id + 'breadcrumb-v1';
+			var aBreadCrumb = [];
+			var sBreadCrumbData = sessionStorage.getItem(sBreadCrumbStorageKey);
+			if (sBreadCrumbData !== null)
+			{
+				aBreadCrumb = JSON.parse(sBreadCrumbData);
+			}
+			return aBreadCrumb;
+		},
+		_Write: function(aBreadCrumb)
+		{
+			var sBreadCrumbStorageKey = this.options.itop_instance_id + 'breadcrumb-v1';
+			sBreadCrumbData = JSON.stringify(aBreadCrumb);
+			sessionStorage.setItem(sBreadCrumbStorageKey, sBreadCrumbData);
+		},
+		// Refresh the latest entry (navigating to a tab)
+		_RefreshLatestEntry: function()
+		{
+			aBreadCrumb = this._Read();
+			var iDisplayableItems = aBreadCrumb.length;
+
+			if (this.options.new_entry !== null) {
+				// The current page is the last entry in the breadcrumb, let's refresh it
+				aBreadCrumb[aBreadCrumb.length - 1].url = window.location.href;
+				$('#itop-breadcrumb li:last-of-type a').attr('href', window.location.href);
+			}
+			this._Write(aBreadCrumb);
 		}
 	});
 });

+ 34 - 7
pages/UI.php

@@ -319,7 +319,7 @@ try
 		$oP->add_linked_script("../js/linksdirectwidget.js");
 		$oP->add_linked_script("../js/extkeywidget.js");
 		$oP->add_linked_script("../js/jquery.blockUI.js");
-		break;		
+		break;
 	}
 		
 	switch($operation)
@@ -363,13 +363,19 @@ 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));
+					$sIcon = MetaModel::GetClassIcon($sClass, false);
+					if ($sIcon == '')
+					{
+						$sIcon = utils::GetAbsoluteUrlAppRoot().'images/breadcrumb_object.png';
+					}
+					$oP->SetBreadCrumbEntry("ui-details-$sClass-$id", $oObj->Get('friendlyname'), MetaModel::GetName($sClass).': '.$oObj->Get('friendlyname'), '', $sIcon);
 					DisplayDetails($oP, $sClass, $oObj, $id);
 				}				
 			}
 		break;
 
 		case 'release_lock_and_details':
+		$oP->DisableBreadCrumb();
 		$sClass = utils::ReadParam('class', '');
 		$id = utils::ReadParam('id', '');
 		$oObj = MetaModel::GetObject($sClass, $id);
@@ -561,6 +567,7 @@ EOF
 		///////////////////////////////////////////////////////////////////////////////////////////
 
 		case 'modify': // Form to modify an object
+			$oP->DisableBreadCrumb();
 			$sClass = utils::ReadParam('class', '', false, 'class');
 			$id = utils::ReadParam('id', '');
 			if ( empty($sClass) || empty($id)) // TO DO: check that the class name is valid !
@@ -590,6 +597,7 @@ EOF
 		///////////////////////////////////////////////////////////////////////////////////////////
 
 		case 'select_for_modify_all': // Select the list of objects to be modified (bulk modify)
+		$oP->DisableBreadCrumb();
 		$oP->set_title(Dict::S('UI:ModifyAllPageTitle'));
 		$sFilter = utils::ReadParam('filter', '', false, 'raw_data');
 		if (empty($sFilter))
@@ -607,6 +615,7 @@ EOF
 		///////////////////////////////////////////////////////////////////////////////////////////
 
 		case 'form_for_modify_all': // Form to modify multiple objects (bulk modify)
+		$oP->DisableBreadCrumb();
 		$sFilter = utils::ReadParam('filter', '', false, 'raw_data');
 		$sClass = utils::ReadParam('class', '', false, 'class');
 		$oFullSetFilter = DBObjectSearch::unserialize($sFilter);
@@ -619,6 +628,7 @@ EOF
 		///////////////////////////////////////////////////////////////////////////////////////////
 
 		case 'preview_or_modify_all': // Preview or apply bulk modify
+		$oP->DisableBreadCrumb();
 		$sFilter = utils::ReadParam('filter', '', false, 'raw_data');
 		// TO DO: limit the search filter by the user context
 		$oFilter = DBObjectSearch::unserialize($sFilter); // TO DO : check that the filter is valid
@@ -643,6 +653,7 @@ EOF
 		///////////////////////////////////////////////////////////////////////////////////////////
 
 		case 'new': // Form to create a new object
+			$oP->DisableBreadCrumb();
 			$sClass = utils::ReadParam('class', '', false, 'class');
 			$sStateCode = utils::ReadParam('state', '');
 			$bCheckSubClass = utils::ReadParam('checkSubclass', true);
@@ -757,6 +768,7 @@ EOF
 		///////////////////////////////////////////////////////////////////////////////////////////
 
 		case 'apply_modify': // Applying the modifications to an existing object
+			$oP->DisableBreadCrumb();
 			$sClass = utils::ReadPostedParam('class', '');
 			$sClassLabel = MetaModel::GetName($sClass);
 			$id = utils::ReadPostedParam('id', '');
@@ -860,6 +872,7 @@ EOF
 		///////////////////////////////////////////////////////////////////////////////////////////
 
 		case 'select_for_deletion': // Select multiple objects for deletion
+			$oP->DisableBreadCrumb();
 			$sFilter = utils::ReadParam('filter', '', false, 'raw_data');
 			if (empty($sFilter))
 			{
@@ -876,6 +889,7 @@ EOF
 		///////////////////////////////////////////////////////////////////////////////////////////
 
 		case 'bulk_delete_confirmed': // Confirm bulk deletion of objects
+			$oP->DisableBreadCrumb();
 			$sTransactionId = utils::ReadPostedParam('transaction_id', '');
 			if (!utils::IsTransactionValid($sTransactionId))
 			{
@@ -887,6 +901,7 @@ EOF
 
 		case 'delete':
 		case 'bulk_delete': // Actual bulk deletion (if confirmed)
+			$oP->DisableBreadCrumb();
 			$sClass = utils::ReadParam('class', '', false, 'class');
 			$sClassLabel = MetaModel::GetName($sClass);
 			$aObjects = array();
@@ -938,6 +953,7 @@ EOF
 		///////////////////////////////////////////////////////////////////////////////////////////
 
 		case 'apply_new': // Creation of a new object
+		$oP->DisableBreadCrumb();
 		$sClass = utils::ReadPostedParam('class', '', 'class');
 		$sClassLabel = MetaModel::GetName($sClass);
 		$sTransactionId = utils::ReadPostedParam('transaction_id', '');
@@ -1008,6 +1024,7 @@ EOF
 		///////////////////////////////////////////////////////////////////////////////////////////
 
 		case 'select_bulk_stimulus': // Form displayed when applying a stimulus to many objects
+		$oP->DisableBreadCrumb();
 		$sFilter = utils::ReadParam('filter', '', false, 'raw_data');
 		$sStimulus = utils::ReadParam('stimulus', '');
 		$sState = utils::ReadParam('state', '');
@@ -1031,6 +1048,7 @@ EOF
 		break;
 		
 		case 'bulk_stimulus':
+		$oP->DisableBreadCrumb();
 		$sFilter = utils::ReadParam('filter', '', false, 'raw_data');
 		$sStimulus = utils::ReadParam('stimulus', '');
 		$sState = utils::ReadParam('state', '');
@@ -1196,6 +1214,7 @@ EOF
 		break;
 		
 		case 'bulk_apply_stimulus':
+		$oP->DisableBreadCrumb();
 		$bPreviewMode = utils::ReadPostedParam('preview_mode', false);
 		$sFilter = utils::ReadPostedParam('filter', '', false, 'raw_data');
 		$sStimulus = utils::ReadPostedParam('stimulus', '');
@@ -1329,6 +1348,7 @@ EOF
 		break;
 
 		case 'stimulus': // Form displayed when applying a stimulus (state change)
+		$oP->DisableBreadCrumb();
 		$sClass = utils::ReadParam('class', '', false, 'class');
 		$id = utils::ReadParam('id', '');
 		$sStimulus = utils::ReadParam('stimulus', '');
@@ -1351,6 +1371,7 @@ EOF
 		///////////////////////////////////////////////////////////////////////////////////////////
 
 		case 'apply_stimulus': // Actual state change
+		$oP->DisableBreadCrumb();
 		$sClass = utils::ReadPostedParam('class', '');
 		$id = utils::ReadPostedParam('id', '');
 		$sTransactionId = utils::ReadPostedParam('transaction_id', '');
@@ -1489,14 +1510,19 @@ EOF
 		$sRelation = utils::ReadParam('relation', 'impact');
 		$sDirection = utils::ReadParam('direction', 'down');
 		$iGroupingThreshold = utils::ReadParam('g', 5);
-		
+
 		$oObj = MetaModel::GetObject($sClass, $id);
 		$iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
 		$aSourceObjects = array($oObj);
-		
+
 		$oP->set_title(MetaModel::GetRelationDescription($sRelation).' '.$oObj->GetName());
-		
-		if ($sRelation == 'depends on')
+
+		$sPageId = "ui-relation-graph-".$sClass.'::'.$id;
+		$sLabel = $oObj->GetName().' '.MetaModel::GetRelationLabel($sRelation);
+		$sDescription = MetaModel::GetRelationDescription($sRelation).' '.$oObj->GetName();
+		$oP->SetBreadCrumbEntry($sPageId, $sLabel, $sDescription);
+
+			if ($sRelation == 'depends on')
 		{
 			$sRelation = 'impacts';
 			$sDirection = 'up';
@@ -1561,6 +1587,7 @@ EOF
 		///////////////////////////////////////////////////////////////////////////////////////////
 		
 		case 'kill_lock':
+		$oP->DisableBreadCrumb();
 		$sClass = utils::ReadParam('class', '');
 		$id = utils::ReadParam('id', '');
 		iTopOwnershipLock::KillLock($sClass, $id);
@@ -1571,6 +1598,7 @@ EOF
 		///////////////////////////////////////////////////////////////////////////////////////////
 
 		case 'cancel': // An action was cancelled
+		$oP->DisableBreadCrumb();
 		$oP->set_title(Dict::S('UI:OperationCancelled'));
 		$oP->add('<h1>'.Dict::S('UI:OperationCancelled').'</h1>');
 		break;
@@ -1582,7 +1610,6 @@ EOF
 		$oMenuNode = ApplicationMenu::GetMenuNode(ApplicationMenu::GetMenuIndexById(ApplicationMenu::GetActiveNodeId()));
 		if (is_object($oMenuNode))
 		{
-		
 			$oMenuNode->RenderContent($oP, $oAppContext->GetAsHash());
 			$oP->set_title($oMenuNode->GetLabel());
 		}

+ 2 - 0
pages/UniversalSearch.php

@@ -54,6 +54,8 @@ $sOQLClause = utils::ReadParam('oql_clause', '', false, 'raw_data');
 $sFilter = utils::ReadParam('filter', '', false, 'raw_data');
 $sOperation = utils::ReadParam('operation', '');
 
+$oP->SetBreadCrumbEntry('ui-tool-universalsearch', Dict::S('Menu:UniversalSearchMenu'), Dict::S('Menu:UniversalSearchMenu+'), '', utils::GetAbsoluteUrlAppRoot().'images/wrench.png');
+
 // First part: select the class to search for
 $oP->add("<form>");
 $oP->add(Dict::S('UI:UniversalSearch:LabelSelectTheClass')."<select style=\"width: 150px;\" id=\"select_class\" name=\"baseClass\" onChange=\"this.form.submit();\">");

+ 7 - 3
pages/audit.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2015 Combodo SARL
+// Copyright (C) 2010-2016 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -19,7 +19,7 @@
 /**
  * Execute and shows the data quality audit
  *
- * @copyright   Copyright (C) 2010-2015 Combodo SARL
+ * @copyright   Copyright (C) 2010-2016 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 /**
@@ -156,10 +156,11 @@ try
 	LoginWebPage::DoLogin(); // Check user rights and prompt if needed
 	
 	$oP = new iTopWebPage(Dict::S('UI:Audit:Title'));
-	
+
 	switch($operation)
 	{
 		case 'csv':
+		$oP->DisableBreadCrumb();
 		// Big result sets cause long OQL that cannot be passed (serialized) as a GET parameter
 		// Therefore we don't use the standard "search_oql" operation of UI.php to display the CSV
 		$iCategory = utils::ReadParam('category', '');
@@ -221,6 +222,8 @@ try
 		break;
 						
 		case 'errors':
+		$sTitle = 'Audit Errors';
+		$oP->SetBreadCrumbEntry('ui-tool-auditerrors', $sTitle, '', '', utils::GetAbsoluteUrlAppRoot().'images/wrench.png');
 		$iCategory = utils::ReadParam('category', '');
 		$iRuleIndex = utils::ReadParam('rule', 0);
 	
@@ -244,6 +247,7 @@ try
 		
 		case 'audit':
 		default:
+		$oP->SetBreadCrumbEntry('ui-tool-audit', Dict::S('Menu:Audit'), Dict::S('UI:Audit:InteractiveAudit'), '', utils::GetAbsoluteUrlAppRoot().'images/wrench.png');
 		$oP->add('<div class="page_header"><h1>'.Dict::S('UI:Audit:InteractiveAudit').'</h1><img style="margin-top: -20px; margin-right: 10px; float: right;" src="../images/clean.png"/></div>');
 		$oAuditFilter = new DBObjectSearch('AuditCategory');
 		$oCategoriesSet = new DBObjectSet($oAuditFilter);

+ 4 - 3
pages/csvimport.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2016 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
  * CSV Import Page
  * Wizard to import CSV (or TSV) data into the database
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2016 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 try
@@ -39,7 +39,8 @@ try
 	$iStep = utils::ReadParam('step', 1);
 	
 	$oPage = new iTopWebPage(Dict::S('UI:Title:BulkImport'));
-	
+	$oPage->SetBreadCrumbEntry('ui-tool-bulkimport', Dict::S('Menu:CSVImportMenu'), Dict::S('UI:Title:BulkImport+'), '', utils::GetAbsoluteUrlAppRoot().'images/wrench.png');
+
 	/**
 	 * Helper function to build a select from the list of valid classes for a given action
 	 * @param string $sName The name of the select in the HTML form

+ 1 - 0
pages/run_query.php

@@ -99,6 +99,7 @@ $sOperation = utils::ReadParam('operation', 'menu');
 $oAppContext = new ApplicationContext();
 
 $oP = new iTopWebPage(Dict::S('UI:RunQuery:Title'));
+$oP->SetBreadCrumbEntry('ui-tool-runquery', Dict::S('Menu:RunQueriesMenu'), Dict::S('Menu:RunQueriesMenu+'), '', utils::GetAbsoluteUrlAppRoot().'images/wrench.png');
 
 // Main program
 $sExpression = utils::ReadParam('expression', '', false, 'raw_data');

+ 4 - 2
pages/schema.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2016 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Presentation of the data model
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2016 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -570,6 +570,8 @@ $operation = utils::ReadParam('operation', '');
 $oPage = new iTopWebPage(Dict::S('UI:Schema:Title'));
 $oPage->no_cache();
 
+$oPage->SetBreadCrumbEntry('ui-tool-datamodel', Dict::S('Menu:DataModelMenu'), Dict::S('Menu:DataModelMenu+'), '', utils::GetAbsoluteUrlAppRoot().'images/wrench.png');
+
 $operation = utils::ReadParam('operation', '');
 
 switch($operation)

+ 3 - 2
webservices/export-v2.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2015 Combodo SARL
+// Copyright (C) 2015-2016 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -19,7 +19,7 @@
 /**
  * Export data specified by an OQL or a query phrasebook entry
  *
- * @copyright   Copyright (C) 2015 Combodo SARL
+ * @copyright   Copyright (C) 2015-2016 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -336,6 +336,7 @@ EOF
 	else
 	{
 		$oP = new iTopWebPage('iTop Export');
+		$oP->SetBreadCrumbEntry('ui-tool-export', Dict::S('Menu:ExportMenu'), Dict::S('Menu:ExportMenu+'), '', utils::GetAbsoluteUrlAppRoot().'images/wrench.png');
 	}
 	
 	if ($sExpression === null)