diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index f7a9fe164..973c2e130 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -398,6 +398,54 @@ EOF $this->aFieldsMap[$sAttCode] = $sInputId; } + + /** + * @param \iTopWebPage $oPage + * @param $bEditMode + * + * @throws \CoreException + * @throws \Exception + */ + public function DisplayDashboards($oPage, $bEditMode) + { + if ($bEditMode || $this->IsNew()) + { + return; + } + + $aList = $this->FlattenZList(MetaModel::GetZListItems(get_class($this), 'details')); + if (count($aList) == 0) + { + // Empty ZList defined, display all the dashboard attributes defined + $aList = array_keys(MetaModel::ListAttributeDefs(get_class($this))); + } + $sClass = get_class($this); + foreach($aList as $sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + // Display mode + if (!$oAttDef instanceof AttributeDashboard) + { + continue; + } // Process only dashboards attributes... + + $oPage->SetCurrentTab($oAttDef->GetLabel()); + + // Load the dashboard + $oDashboard = $oAttDef->GetDashboard(); + if (is_null($oDashboard)) + { + continue; + } + + $sDivId = $oDashboard->GetId(); + $oPage->add('
'); + $oDashboard->Render($oPage, false, array()); + $oPage->add('
'); + $oDashboard->RenderEditionTools($oPage); + } + } + /** * @param \WebPage $oPage * @param bool $bEditMode @@ -857,6 +905,18 @@ EOF } + /** + * @param \iTopWebPage $oPage + * @param bool $bEditMode + * + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \DictExceptionMissingString + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + * @throws \OQLException + */ function DisplayDetails(WebPage $oPage, $bEditMode = false) { $sTemplate = Utils::ReadFromFile(MetaModel::GetDisplayTemplate(get_class($this))); @@ -884,6 +944,7 @@ EOF $oPage->SetCurrentTab(Dict::S('UI:PropertiesTab')); $this->DisplayBareProperties($oPage, $bEditMode); $this->DisplayBareRelations($oPage, $bEditMode); + $this->DisplayDashboards($oPage, $bEditMode); //$oPage->SetCurrentTab(Dict::S('UI:HistoryTab')); //$this->DisplayBareHistory($oPage, $bEditMode); $oPage->AddAjaxTab(Dict::S('UI:HistoryTab'), @@ -2849,6 +2910,10 @@ EOF $sDisplayValue .= "
".Dict::Format('UI:DownloadDocument_', $oDocument->GetDownloadLink(get_class($this), $this->GetKey(), $sAttCode)).", \n"; } + elseif ($oAttDef instanceof AttributeDashboard) + { + $sDisplayValue = ''; + } else { $sDisplayValue = $this->GetAsHTML($sAttCode); diff --git a/application/dashboard.class.inc.php b/application/dashboard.class.inc.php index fe1c8eef2..c34bd785f 100644 --- a/application/dashboard.class.inc.php +++ b/application/dashboard.class.inc.php @@ -50,6 +50,11 @@ abstract class Dashboard $this->sId = $sId; } + /** + * @param $sXml + * + * @throws \Exception + */ public function FromXml($sXml) { $this->aCells = array(); // reset the content of the dashboard @@ -509,6 +514,7 @@ EOF * Return an array of dashlets available for selection. * * @return array + * @throws \ReflectionException */ protected function GetAvailableDashlets() { @@ -561,11 +567,21 @@ EOF } return 'DashletUnknown'; } + + /** + * @return mixed + */ + public function GetId() + { + return $this->sId; + } } class RuntimeDashboard extends Dashboard { protected $bCustomized; + private $sDefinitionFile = ''; + public function __construct($sId) { @@ -630,7 +646,53 @@ class RuntimeDashboard extends Dashboard utils::PopArchiveMode(); } } - + + /** + * @param string $sDashboardFile file name relative to the current module folder + * @param string $sDashBoardCode code of the dashboard either menu_id or __ + * + * @return null|RuntimeDashboard + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + */ + public static function GetDashboard($sDashboardFile, $sDashBoardCode) + { + $bCustomized = false; + + // Search for an eventual user defined dashboard + $oUDSearch = new DBObjectSearch('UserDashboard'); + $oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '='); + $oUDSearch->AddCondition('menu_code', $sDashBoardCode, '='); + $oUDSet = new DBObjectSet($oUDSearch); + if ($oUDSet->Count() > 0) + { + // Assuming there is at most one couple {user, menu}! + $oUserDashboard = $oUDSet->Fetch(); + $sDashboardDefinition = $oUserDashboard->Get('contents'); + $bCustomized = true; + } + else + { + $sDashboardDefinition = @file_get_contents($sDashboardFile); + } + + if ($sDashboardDefinition !== false) + { + $oDashboard = new RuntimeDashboard($sDashBoardCode); + $oDashboard->FromXml($sDashboardDefinition); + $oDashboard->SetCustomFlag($bCustomized); + $oDashboard->SetDefinitionFile($sDashboardFile); + } + else + { + $oDashboard = null; + } + return $oDashboard; + } + public function RenderEditionTools(WebPage $oPage) { $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.iframe-transport.js'); @@ -638,7 +700,8 @@ class RuntimeDashboard extends Dashboard $sEditMenu = "
    • "; $aActions = array(); - $oEdit = new JSPopupMenuItem('UI:Dashboard:Edit', Dict::S('UI:Dashboard:Edit'), "return EditDashboard('{$this->sId}')"); + $sFile = addslashes($this->sDefinitionFile); + $oEdit = new JSPopupMenuItem('UI:Dashboard:Edit', Dict::S('UI:Dashboard:Edit'), "return EditDashboard('{$this->sId}', '$sFile')"); $aActions[$oEdit->GetUID()] = $oEdit->GetMenuItem(); if ($this->bCustomized) @@ -662,9 +725,9 @@ EOF ); $oPage->add_script( <<sDefinitionFile; + } + + /** + * @param string $sDefinitionFile + */ + public function SetDefinitionFile($sDefinitionFile) + { + $this->sDefinitionFile = $sDefinitionFile; + } } \ No newline at end of file diff --git a/application/menunode.class.inc.php b/application/menunode.class.inc.php index 9f7d248c7..c67137b94 100644 --- a/application/menunode.class.inc.php +++ b/application/menunode.class.inc.php @@ -1149,33 +1149,7 @@ class DashboardMenuNode extends MenuNode */ public function GetDashboard() { - $sDashboardDefinition = @file_get_contents($this->sDashboardFile); - if ($sDashboardDefinition !== false) - { - $bCustomized = false; - - // Search for an eventual user defined dashboard, overloading the existing one - $oUDSearch = new DBObjectSearch('UserDashboard'); - $oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '='); - $oUDSearch->AddCondition('menu_code', $this->sMenuId, '='); - $oUDSet = new DBObjectSet($oUDSearch); - if ($oUDSet->Count() > 0) - { - // Assuming there is at most one couple {user, menu}! - $oUserDashboard = $oUDSet->Fetch(); - $sDashboardDefinition = $oUserDashboard->Get('contents'); - $bCustomized = true; - - } - $oDashboard = new RuntimeDashboard($this->sMenuId); - $oDashboard->FromXml($sDashboardDefinition); - $oDashboard->SetCustomFlag($bCustomized); - } - else - { - $oDashboard = null; - } - return $oDashboard; + return RuntimeDashboard::GetDashboard($this->sDashboardFile, $this->sMenuId); } /** @@ -1199,6 +1173,7 @@ class DashboardMenuNode extends MenuNode if ($oDashboard->GetAutoReload()) { $sId = $this->sMenuId; + $sFile = addslashes($oDashboard->GetDefinitionFile()); $sExtraParams = json_encode($aExtraParams); $iReloadInterval = 1000 * $oDashboard->GetAutoReloadInterval(); $oPage->add_script( @@ -1213,7 +1188,7 @@ class DashboardMenuNode extends MenuNode { $('.dashboard_contents#'+sDivId).block(); $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', - { operation: 'reload_dashboard', dashboard_id: '$sId', extra_params: oExtraParams}, + { operation: 'reload_dashboard', dashboard_id: '$sId', file: '$sFile', extra_params: oExtraParams}, function(data){ $('.dashboard_contents#'+sDivId).html(data); $('.dashboard_contents#'+sDivId).unblock(); diff --git a/application/utils.inc.php b/application/utils.inc.php index cdf90877b..cdc4374b1 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -1104,19 +1104,22 @@ class utils break; case iPopupMenuExtension::MENU_DASHBOARD_ACTIONS: - // $param is a Dashboard - $oAppContext = new ApplicationContext(); - $aParams = $oAppContext->GetAsHash(); - $sMenuId = ApplicationMenu::GetActiveNodeId(); - $sDlgTitle = addslashes(Dict::S('UI:ImportDashboardTitle')); - $sDlgText = addslashes(Dict::S('UI:ImportDashboardText')); - $sCloseBtn = addslashes(Dict::S('UI:Button:Cancel')); - $aResult = array( - new SeparatorPopupMenuItem(), - new URLPopupMenuItem('UI:ExportDashboard', Dict::S('UI:ExportDashBoard'), utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=export_dashboard&id='.$sMenuId), - new JSPopupMenuItem('UI:ImportDashboard', Dict::S('UI:ImportDashBoard'), "UploadDashboard({dashboard_id: '$sMenuId', title: '$sDlgTitle', text: '$sDlgText', close_btn: '$sCloseBtn' })"), - ); - break; + // $param is a Dashboard + /** @var \RuntimeDashboard $oDashboard */ + $oDashboard = $param; + $sDashboardId = $oDashboard->GetId(); + $sDashboardFile = $oDashboard->GetDefinitionFile(); + $sDlgTitle = addslashes(Dict::S('UI:ImportDashboardTitle')); + $sDlgText = addslashes(Dict::S('UI:ImportDashboardText')); + $sCloseBtn = addslashes(Dict::S('UI:Button:Cancel')); + $sDashboardFileJS = addslashes($sDashboardFile); + $sDashboardFileURL = urlencode($sDashboardFile); + $aResult = array( + new SeparatorPopupMenuItem(), + new URLPopupMenuItem('UI:ExportDashboard', Dict::S('UI:ExportDashBoard'), utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=export_dashboard&id='.$sDashboardId.'&file='.$sDashboardFileURL), + new JSPopupMenuItem('UI:ImportDashboard', Dict::S('UI:ImportDashBoard'), "UploadDashboard({dashboard_id: '$sDashboardId', file: '$sDashboardFileJS', title: '$sDlgTitle', text: '$sDlgText', close_btn: '$sCloseBtn' })"), + ); + break; default: // Unknown type of menu, do nothing diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 3f3d2d79c..0de09da88 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -1249,6 +1249,53 @@ abstract class AttributeDefinition } } +class AttributeDashboard extends AttributeDefinition +{ + static public function ListExpectedParams() + { + return array_merge(parent::ListExpectedParams(), + array("definition_file")); + } + + public function GetDashboard() + { + $sAttCode = $this->GetCode(); + $sClass = MetaModel::GetAttributeOrigin($this->GetHostClass(), $sAttCode); + $sFilePath = APPROOT.'env-'.utils::GetCurrentEnvironment().'/'.$this->Get('definition_file'); + return RuntimeDashboard::GetDashboard($sFilePath, $sClass.'__'.$sAttCode); + } + + public function IsWritable() + { + return false; + } + + public function GetEditClass() + { + return ""; + } + + public function GetDefaultValue(DBObject $oHostObject = null) + { + return null; + } + + public function GetBasicFilterOperators() + { + return array(); + } + + public function GetBasicFilterLooseOperator() + { + return '='; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + return ''; + } +} + /** * Set of objects directly linked to an object, and being part of its definition * diff --git a/css/light-grey.css b/css/light-grey.css index e8d9b4ae8..40e5f25e1 100644 --- a/css/light-grey.css +++ b/css/light-grey.css @@ -2251,6 +2251,7 @@ a.summary, a.summary:hover { } .dashboard_contents { width: 100%; + background-color: #fff; } #DashboardMenu { display: block; diff --git a/css/light-grey.scss b/css/light-grey.scss index 249c8403c..faeb8f7c9 100644 --- a/css/light-grey.scss +++ b/css/light-grey.scss @@ -2603,6 +2603,7 @@ a.summary, a.summary:hover { .dashboard_contents { width: 100%; + background-color: $white; } #DashboardMenu { diff --git a/datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml b/datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml index 806d1f413..1bb7bda84 100755 --- a/datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml +++ b/datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml @@ -412,6 +412,57 @@ true + + + DashboardLayoutOneCol + Test Modified + + false + 300 + + + + 0 + + + 0 + Menu:RequestManagement + itop-welcome-itil/images/user-request-deadline.png + Menu:UserRequest:OpenRequests + SELECT UserRequest WHERE status != "closed" + status + assigned,escalated_tto,escalated_ttr,new,resolved + + + + + 1 + + + 0 + Callers + SELECT UserRequest + caller_id + + count + + + function + desc + + + + + 2 + + + 0 + + + + + + @@ -1458,6 +1509,9 @@ 40 + + 45 + 50 diff --git a/dictionaries/en.dictionary.itop.ui.php b/dictionaries/en.dictionary.itop.ui.php index 8e13d427c..20cc7ac69 100644 --- a/dictionaries/en.dictionary.itop.ui.php +++ b/dictionaries/en.dictionary.itop.ui.php @@ -441,8 +441,8 @@ Dict::Add('EN US', 'English', 'English', array( 'UI:Error:ObjectAlreadyCloned' => 'Error: the object has already been cloned!', 'UI:Error:ObjectAlreadyCreated' => 'Error: the object has already been created!', 'UI:Error:Invalid_Stimulus_On_Object_In_State' => 'Error: invalid stimulus "%1$s" on object %2$s in state "%3$s".', - - + 'UI:Error:InvalidDashboardFile' => 'Error: invalid dashboard file', + 'UI:GroupBy:Count' => 'Count', 'UI:GroupBy:Count+' => 'Number of elements', 'UI:CountOfObjects' => '%1$d objects matching the criteria.', diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php index 8b1a9e062..18f7c194d 100644 --- a/dictionaries/fr.dictionary.itop.ui.php +++ b/dictionaries/fr.dictionary.itop.ui.php @@ -308,6 +308,7 @@ Dict::Add('FR FR', 'French', 'Français', array( 'UI:Error:ObjectAlreadyCloned' => 'Erreur: l\'objet a déjà été dupliqué !', 'UI:Error:ObjectAlreadyCreated' => 'Erreur: l\'objet a déjà été créé !', 'UI:Error:Invalid_Stimulus_On_Object_In_State' => 'Erreur: le stimulus "%1$s" n\'est pas valide pour l\'objet %2$s dans l\'état "%3$s".', + 'UI:Error:InvalidDashboardFile' => 'Erreur: Le fichier tableau de bord est invalide', 'UI:GroupBy:Count' => 'Nombre', 'UI:GroupBy:Count+' => 'Nombre d\'éléments', 'UI:CountOfObjects' => '%1$d objets correspondants aux critères.', diff --git a/pages/ajax.render.php b/pages/ajax.render.php index a9d0b006d..0e615fddb 100644 --- a/pages/ajax.render.php +++ b/pages/ajax.render.php @@ -910,25 +910,68 @@ try } break; + case 'export_dashboard': + $sDashboardId = utils::ReadParam('id', '', false, 'raw_data'); + $sDashboardFile = utils::ReadParam('file', '', false, 'raw_data'); + $oDashboard = RuntimeDashboard::GetDashboard($sDashboardFile, $sDashboardId); + if (!is_null($oDashboard)) + { + $oPage->TrashUnexpectedOutput(); + $oPage->SetContentType('text/xml'); + $oPage->SetContentDisposition('attachment', 'dashboard_'.$oDashboard->GetTitle().'.xml'); + $oPage->add($oDashboard->ToXml()); + } + break; + + case 'import_dashboard': + $sDashboardId = utils::ReadParam('id', '', false, 'raw_data'); + $sDashboardFile = utils::ReadParam('file', '', false, 'raw_data'); + $oDashboard = RuntimeDashboard::GetDashboard($sDashboardFile, $sDashboardId); + $aResult = array('error' => ''); + if (!is_null($oDashboard)) + { + try + { + $oDoc = utils::ReadPostedDocument('dashboard_upload_file'); + $oDashboard->FromXml($oDoc->GetData()); + $oDashboard->Save(); + } catch (DOMException $e) + { + $aResult = array('error' => Dict::S('UI:Error:InvalidDashboardFile')); + } catch (Exception $e) + { + $aResult = array('error' => $e->getMessage()); + } + } + else + { + $aResult['error'] = 'Dashboard id="'.$sMenuId.'" not found.'; + } + $oPage->add(json_encode($aResult)); + break; + case 'reload_dashboard': $oPage->SetContentType('text/html'); $sDashboardId = utils::ReadParam('dashboard_id', '', false, 'raw_data'); $aExtraParams = utils::ReadParam('extra_params', '', false, 'raw_data'); - ApplicationMenu::LoadAdditionalMenus(); - $idx = ApplicationMenu::GetMenuIndexById($sDashboardId); - $oMenu = ApplicationMenu::GetMenuNode($idx); - $oDashboard = $oMenu->GetDashboard(); - $oDashboard->Render($oPage, false, $aExtraParams); + $sDashboardFile = utils::ReadParam('file', '', false, 'raw_data'); + $oDashboard = RuntimeDashboard::GetDashboard($sDashboardFile, $sDashboardId); + $aResult = array('error' => ''); + if (!is_null($oDashboard)) + { + $oDashboard->Render($oPage, false, $aExtraParams); + } $oPage->add_ready_script("$('.dashboard_contents table.listResults').tableHover(); $('.dashboard_contents table.listResults').tablesorter( { widgets: ['myZebra', 'truncatedList']} );"); break; case 'dashboard_editor': $sId = utils::ReadParam('id', '', false, 'raw_data'); - ApplicationMenu::LoadAdditionalMenus(); - $idx = ApplicationMenu::GetMenuIndexById($sId); - /** @var \DashboardMenuNode $oMenu */ - $oMenu = ApplicationMenu::GetMenuNode($idx); - $oMenu->RenderEditor($oPage); + $sDashboardFile = utils::ReadParam('file', '', false, 'raw_data'); + $oDashboard = RuntimeDashboard::GetDashboard($sDashboardFile, $sId); + if (!is_null($oDashboard)) + { + $oDashboard->RenderEditor($oPage); + } break; case 'new_dashlet': @@ -1024,15 +1067,7 @@ try $oDashboard->FromParams($aParams); $oDashboard->Save(); // trigger a reload of the current page since the dashboard just changed - $oPage->add_ready_script( - <<add_ready_script("sLocation = new String(window.location.href); window.location.href=sLocation.replace('&edit=1', '');"); // reloads the page, doing a GET even if we arrived via a POST + $oPage->add_ready_script("location.reload(true);"); break; case 'revert_dashboard': @@ -1041,7 +1076,7 @@ EOF $oDashboard->Revert(); // trigger a reload of the current page since the dashboard just changed - $oPage->add_ready_script("window.location.href=window.location.href;"); // reloads the page, doing a GET even if we arrived via a POST + $oPage->add_ready_script("location.reload(true);"); break; case 'render_dashboard': @@ -1165,51 +1200,6 @@ EOF } break; - case 'export_dashboard': - $sMenuId = utils::ReadParam('id', '', false, 'raw_data'); - ApplicationMenu::LoadAdditionalMenus(); - $index = ApplicationMenu::GetMenuIndexById($sMenuId); - $oMenu = ApplicationMenu::GetMenuNode($index); - if ($oMenu instanceof DashboardMenuNode) - { - $oDashboard = $oMenu->GetDashboard(); - - $oPage->TrashUnexpectedOutput(); - $oPage->SetContentType('text/xml'); - $oPage->SetContentDisposition('attachment', $oMenu->GetLabel().'.xml'); - $oPage->add($oDashboard->ToXml()); - } - break; - - case 'import_dashboard': - $sMenuId = utils::ReadParam('id', '', false, 'raw_data'); - ApplicationMenu::LoadAdditionalMenus(); - $index = ApplicationMenu::GetMenuIndexById($sMenuId); - $oMenu = ApplicationMenu::GetMenuNode($index); - $aResult = array('error' => ''); - try - { - if ($oMenu instanceof DashboardMenuNode) - { - $oDoc = utils::ReadPostedDocument('dashboard_upload_file'); - $oDashboard = $oMenu->GetDashboard(); - $oDashboard->FromXml($oDoc->GetData()); - $oDashboard->Save(); - } - else - { - $aResult['error'] = 'Dashboard id="'.$sMenuId.'" not found.'; - } - } catch (DOMException $e) - { - $aResult = array('error' => Dict::S('UI:Error:InvalidDashboardFile')); - } catch (Exception $e) - { - $aResult = array('error' => $e->getMessage()); - } - $oPage->add(json_encode($aResult)); - break; - case 'about_box': $oPage->SetContentType('text/html'); diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index 995965533..69e5c03b3 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -1433,6 +1433,37 @@ EOF $aParameters['depends_on'] = $sDependencies; $aParameters['class_field'] = $this->GetMandatoryPropString($oField, 'class_field'); } + elseif ($sAttType == 'AttributeDashboard') + { + $aTagFieldsInfo[] = $sAttCode; + $aParameters['definition_file'] = $this->GetPropString($oField, 'definition_file'); + + if ($aParameters['definition_file'] == null) + { + $oDashboardDefinition = $oField->GetOptionalElement('definition'); + if ($oDashboardDefinition == null) + { + throw(new DOMFormatException('Missing definition for Dashboard Attribute "'.$sAttCode.'" expecting either a tag "definition_file" or "definition".')); + } + $sFileName = strtolower($sClass).'__'.strtolower($sAttCode).'_dashboard.xml'; + + $oXMLDoc = new DOMDocument('1.0', 'UTF-8'); + $oXMLDoc->formatOutput = true; // indent (must be loaded with option LIBXML_NOBLANKS) + $oXMLDoc->preserveWhiteSpace = true; // otherwise the formatOutput option would have no effect + + $oRootNode = $oXMLDoc->createElement('dashboard'); // make sure that the document is not empty + $oRootNode->setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance"); + $oXMLDoc->appendChild($oRootNode); + foreach($oDashboardDefinition->childNodes as $oNode) + { + $oDefNode = $oXMLDoc->importNode($oNode, true); // layout, cells, etc Nodes and below + $oRootNode->appendChild($oDefNode); + } + $sFileName = $sModuleRelativeDir.'/'.$sFileName; + $oXMLDoc->save($sTempTargetDir.'/'.$sFileName); + $aParameters['definition_file'] = "'".str_replace("'", "\\'", $sFileName)."'"; + } + } else { $aParameters['allowed_values'] = 'null'; // or "new ValueSetEnum('SELECT xxxx')" @@ -1970,7 +2001,7 @@ EOF; { throw(new DOMFormatException('Missing definition for Dashboard menu "'.$sMenuId.'" expecting either a tag "definition_file" or "definition".')); } - $sFileName = strtolower(str_replace(array(':', '/', '\\', '*'), '_', $sMenuId)).'_dashboard_menu.xml'; + $sFileName = strtolower(str_replace(array(':', '/', '\\', '*'), '_', $sMenuId)).'_dashboard.xml'; $sTemplateSpec = $this->PathToPHP($sFileName, $sModuleRelativeDir); $oXMLDoc = new DOMDocument('1.0', 'UTF-8');