N°1408 - AttributeDashboard first implementation

This commit is contained in:
Eric
2018-10-04 10:42:17 +02:00
parent 6e5d4834f1
commit c13b6ea13a
12 changed files with 360 additions and 113 deletions

View File

@@ -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('<div class="dashboard_contents" id="'.$sDivId.'">');
$oDashboard->Render($oPage, false, array());
$oPage->add('</div>');
$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 .= "<br/>".Dict::Format('UI:DownloadDocument_',
$oDocument->GetDownloadLink(get_class($this), $this->GetKey(), $sAttCode)).", \n";
}
elseif ($oAttDef instanceof AttributeDashboard)
{
$sDisplayValue = '';
}
else
{
$sDisplayValue = $this->GetAsHTML($sAttCode);

View File

@@ -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 <class>__<attcode>
*
* @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 = "<div id=\"DashboardMenu\"><ul><li><img src=\"../images/pencil-menu.png\"><ul>";
$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(
<<<EOF
function EditDashboard(sId)
function EditDashboard(sId, sDashboardFile)
{
$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', {operation: 'dashboard_editor', id: sId},
$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', {operation: 'dashboard_editor', id: sId, file: sDashboardFile},
function(data)
{
$('body').append(data);
@@ -1007,4 +1070,20 @@ $('#dashlet_creation_dlg').dialog({
EOF
);
}
/**
* @return string
*/
public function GetDefinitionFile()
{
return $this->sDefinitionFile;
}
/**
* @param string $sDefinitionFile
*/
public function SetDefinitionFile($sDefinitionFile)
{
$this->sDefinitionFile = $sDefinitionFile;
}
}

View File

@@ -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();

View File

@@ -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

View File

@@ -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
*

View File

@@ -2251,6 +2251,7 @@ a.summary, a.summary:hover {
}
.dashboard_contents {
width: 100%;
background-color: #fff;
}
#DashboardMenu {
display: block;

View File

@@ -2603,6 +2603,7 @@ a.summary, a.summary:hover {
.dashboard_contents {
width: 100%;
background-color: $white;
}
#DashboardMenu {

View File

@@ -412,6 +412,57 @@
<default_value/>
<is_null_allowed>true</is_null_allowed>
</field>
<field id="dashboard" xsi:type="AttributeDashboard">
<definition>
<layout>DashboardLayoutOneCol</layout>
<title>Test Modified</title>
<auto_reload>
<enabled>false</enabled>
<interval>300</interval>
</auto_reload>
<cells>
<cell id="0">
<rank>0</rank>
<dashlets>
<dashlet id="9" xsi:type="DashletHeaderDynamic">
<rank>0</rank>
<title>Menu:RequestManagement</title>
<icon>itop-welcome-itil/images/user-request-deadline.png</icon>
<subtitle>Menu:UserRequest:OpenRequests</subtitle>
<query>SELECT UserRequest WHERE status != "closed"</query>
<group_by>status</group_by>
<values>assigned,escalated_tto,escalated_ttr,new,resolved</values>
</dashlet>
</dashlets>
</cell>
<cell id="1">
<rank>1</rank>
<dashlets>
<dashlet id="10" xsi:type="DashletGroupByBars">
<rank>0</rank>
<title>Callers</title>
<query>SELECT UserRequest</query>
<group_by>caller_id</group_by>
<style>bars</style>
<aggregation_function>count</aggregation_function>
<aggregation_attribute></aggregation_attribute>
<limit></limit>
<order_by>function</order_by>
<order_direction>desc</order_direction>
</dashlet>
</dashlets>
</cell>
<cell id="2">
<rank>2</rank>
<dashlets>
<dashlet id="0" xsi:type="DashletEmptyCell">
<rank>0</rank>
</dashlet>
</dashlets>
</cell>
</cells>
</definition>
</field>
</fields>
<lifecycle>
<highlight_scale>
@@ -1458,6 +1509,9 @@
<item id="workorders_list">
<rank>40</rank>
</item>
<item id="dashboard">
<rank>45</rank>
</item>
<item id="col:col1">
<rank>50</rank>
<items>

View File

@@ -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.',

View File

@@ -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.',

View File

@@ -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(
<<<EOF
var sLocation = new String(window.location.href);
var sNewLocation = sLocation.replace('&edit=1', '');
sNewLocation = sLocation.replace(/#(.?)$/, ''); // Strips everything after the hash, since IF the URL does not change AND contains a hash, then Chrome does not reload the page
window.location.href = sNewLocation;
EOF
);
$oPage->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');

View File

@@ -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');