diff --git a/application/displayblock.class.inc.php b/application/displayblock.class.inc.php index 3b9d2ece4..c24a4f632 100644 --- a/application/displayblock.class.inc.php +++ b/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(); + } } /** diff --git a/application/itopwebpage.class.inc.php b/application/itopwebpage.class.inc.php index cb5e8a25a..1be2498bf 100644 --- a/application/itopwebpage.class.inc.php +++ b/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( <<'; + $sNorthPane = '
'.$sNorthPane.'
'; } if (!empty($sSouthPane)) @@ -944,13 +976,27 @@ EOF $sHtml .= ''; $sHtml .= '
'; - $sHtml .= '
'; + $sHtml .= '
'; $sHtml .= self::FilterXSS($sApplicationBanner); + + $sHtml .= ' '; + $sHtml .= ' '; + $sHtml .= ' '; + $sHtml .= '
'; + $sHtml .= '
'; + $sHtml .= '
        
'; + $sHtml .= ' '; + $sHtml .= ' '.self::FilterXSS($sLogOffMenu).'
'; + $sHtml .= ' '; + $sHtml .= ' '; + $sHtml .= ' '; + +// $sHtml .= ' '; +// $sHtml .= '
'; + $sHtml .= '
'; $sHtml .= '
'; $sHtml .= ' '; diff --git a/application/menunode.class.inc.php b/application/menunode.class.inc.php index c9d97d4f0..00d418515 100644 --- a/application/menunode.class.inc.php +++ b/application/menunode.class.inc.php @@ -1,5 +1,5 @@ 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')); @@ -303,10 +313,10 @@ class ApplicationMenu $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}'"); } } diff --git a/application/shortcut.class.inc.php b/application/shortcut.class.inc.php index e80d32467..2249b7cff 100644 --- a/application/shortcut.class.inc.php +++ b/application/shortcut.class.inc.php @@ -1,5 +1,5 @@ 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) { diff --git a/css/light-grey.css b/css/light-grey.css index 3084f10ac..6c9f870fc 100644 --- a/css/light-grey.css +++ b/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,#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; +} + + diff --git a/css/light-grey.scss b/css/light-grey.scss index 3ccb0bbac..7843a027b 100644 --- a/css/light-grey.scss +++ b/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; -} \ No newline at end of file +} + +#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,#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; + } + } +} diff --git a/images/breadcrumb-dashboard.png b/images/breadcrumb-dashboard.png new file mode 100644 index 000000000..f3f8494cf Binary files /dev/null and b/images/breadcrumb-dashboard.png differ diff --git a/images/breadcrumb-search.png b/images/breadcrumb-search.png new file mode 100644 index 000000000..8eb919a80 Binary files /dev/null and b/images/breadcrumb-search.png differ diff --git a/images/breadcrumb-separator.png b/images/breadcrumb-separator.png new file mode 100644 index 000000000..0eba6f0e0 Binary files /dev/null and b/images/breadcrumb-separator.png differ diff --git a/images/breadcrumb_home.png b/images/breadcrumb_home.png new file mode 100644 index 000000000..2e8b0a684 Binary files /dev/null and b/images/breadcrumb_home.png differ diff --git a/js/breadcrumb.js b/js/breadcrumb.js new file mode 100644 index 000000000..fa26c9ef1 --- /dev/null +++ b/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 = ''; + $('#itop-breadcrumb').html(sBreadCrumbHtml); + } + else + { + // Sorry! No Web Storage support.. + //$('#itop-breadcrumb').html('Session storage not available for the current browser'); + } + }, + // 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 ); + } + }); +}); diff --git a/pages/UI.php b/pages/UI.php index b82eae743..145e950f6 100644 --- a/pages/UI.php +++ b/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("

".Dict::S('UI:FatalErrorMessage')."

\n"); - $oP->error(Dict::Format('UI:Error_Details', $e->getMessage())); + $oP->error(Dict::Format('UI:Error_Details', $e->getMessage())); $oP->output(); if (MetaModel::IsLogEnabledIssue()) diff --git a/pages/UniversalSearch.php b/pages/UniversalSearch.php index 1ccb7d062..d2d208898 100644 --- a/pages/UniversalSearch.php +++ b/pages/UniversalSearch.php @@ -1,5 +1,5 @@ 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\n"); diff --git a/pages/notifications.php b/pages/notifications.php index 88de4685d..024c43f33 100644 --- a/pages/notifications.php +++ b/pages/notifications.php @@ -1,5 +1,5 @@ add(''); +$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('
'); $oP->add(''); diff --git a/pages/run_query.php b/pages/run_query.php index 2546e02d7..239d966a4 100644 --- a/pages/run_query.php +++ b/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'));