Pārlūkot izejas kodu

Implementation of a new extension "iPopupMenuExtension" to allow a module to add menu items almost anywhere inside iTop.

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@2220 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 12 gadi atpakaļ
vecāks
revīzija
b9880ec30d

+ 144 - 2
application/applicationextension.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010 Combodo SARL
+// Copyright (C) 2010-2012 Combodo SARL
 //
 //   This program is free software; you can redistribute it and/or modify
 //   it under the terms of the GNU General Public License as published by
@@ -48,4 +48,146 @@ interface iApplicationObjectExtension
 	public function OnDBDelete($oObject, $oChange = null);
 }
 
-?>
+/**
+ * New extension to add menu items in the "popup" menus inside iTop. Provides a greater flexibility than
+ * iApplicationUIExtension::EnumAllowedActions.
+ * 
+ * To add some menus into iTop, declare a class that implements this interface, it will be called automatically
+ * by the application, as long as the class definition is included somewhere in the code
+ */
+interface iPopupMenuExtension
+{
+	// Possible types of menu into which new items can be added
+	const MENU_OBJLIST_ACTIONS = 1; 	// $param is a DBObjectSet containing the list of objects
+	const MENU_OBJLIST_TOOLKIT = 2;		// $param is a DBObjectSet containing the list of objects
+	const MENU_OBJDETAILS_ACTIONS = 3;	// $param is a DBObject instance: the object currently displayed
+	const MENU_DASHBOARD_ACTIONS = 4;	// $param is a Dashboard instance: the dashboard currently displayed
+	const MENU_USER_ACTIONS = 5;		// $param is a null ??
+
+	/**
+	 * Get the list of items to be added to a menu. The items will be inserted in the menu in the order of the returned array
+	 * @param int $iMenuId The identifier of the type of menu, as listed by the constants MENU_xxx above
+	 * @param mixed $param Depends on $iMenuId, see the constants defined above
+	 * @return Array An array of ApplicationPopupMenuItem or an empty array if no action is to be added to the menu
+	 */
+	public static function EnumItems($iMenuId, $param);
+}
+
+/**
+ * Each menu items is defined by an instance of an object derived from the class
+ * ApplicationPopupMenu below
+ *
+ */
+abstract class ApplicationPopupMenuItem
+{
+	protected $sUID;
+	protected $sLabel;
+	
+	public function __construct($sUID, $sLabel)
+	{
+		$this->sUID = $sUID;
+		$this->sLabel = $sLabel;
+	}
+	
+	public function GetUID()
+	{
+		return $this->sUID;
+	}
+	
+	public function GetLabel()
+	{
+		return $this->sLabel;
+	}
+	
+	/**
+	 * Returns the components to create a popup menu item in HTML
+	 * @return Hash A hash array: array('label' => , 'url' => , 'target' => , 'onclick' => )
+	 */
+	abstract public function GetMenuItem();
+
+	public function GetLinkedScripts()
+	{
+		return array();
+	}
+}
+
+/**
+ * Class for adding an item into a popup menu that browses to the given URL
+ */
+class URLPopupMenuItem extends ApplicationPopupMenuItem
+{
+	protected $sURL;
+	protected $sTarget;
+	
+	/**
+	 * Class for adding an item that browses to the given URL
+	 * @param string $sUID The unique identifier of this menu in iTop... make sure you pass something unique enough
+	 * @param string $sLabel The display label of the menu (must be localized)
+	 * @param string $sURL If the menu is an hyperlink, provide the absolute hyperlink here
+	 * @param string $sTarget In case the menu is an hyperlink and a specific target is needed (_blank for example), pass it here
+	 */
+	public function __construct($sUID, $sLabel, $sURL, $sTarget = '_top')
+	{
+		parent::__construct($sUID, $sLabel);
+		$this->sURL = $sURL;
+		$this->sTarget = $sTarget;
+	}
+	
+	public function GetMenuItem()
+	{
+		return array ('label' => $this->GetLabel(), 'url' => $this->sURL, 'target' => $this->sTarget);	
+	}
+}
+
+/**
+ * Class for adding an item into a popup menu that triggers some Javascript code
+ */
+class JSPopupMenuItem extends ApplicationPopupMenuItem
+{
+	protected $sJSCode;
+	protected $aIncludeJSFiles;
+	
+	/**
+	 * Class for adding an item that triggers some Javascript code
+	 * @param string $sUID The unique identifier of this menu in iTop... make sure you pass something unique enough
+	 * @param string $sLabel The display label of the menu (must be localized)
+	 * @param string $sJSCode In case the menu consists in executing some havascript code inside the page, pass it here. If supplied $sURL ans $sTarget will be ignored
+	 * @param array $aIncludeJSFiles An array of file URLs to be included (once) to provide some JS libraries for the page.
+	 */
+	public function __construct($sUID, $sLabel, $sJSCode, $aIncludeJSFiles = array())
+	{
+		parent::__construct($sUID, $sLabel);
+		$this->sJSCode = $sJSCode;
+		$this->aIncludeJSFiles = $aIncludeJSFiles;
+	}
+	
+	public function GetMenuItem()
+	{
+		return array ('label' => $this->GetLabel(), 'onclick' => $this->sJSCode, 'url' => '#');
+	}
+	
+	public function GetLinkedScripts()
+	{
+		return $this->aIncludeJSFiles;
+	}
+}
+
+/**
+ * Class for adding a separator (horizontal line, not selectable) the output
+ * will automatically reduce several consecutive separators to just one
+ */
+class SeparatorPopupMenuItem extends ApplicationPopupMenuItem
+{
+	/**
+	 * Class for inserting a separator into a popup menu
+	 */
+	public function __construct()
+	{
+		parent::__construct('', '');
+	}
+	
+	public function GetMenuItem()
+	{
+		return array ('label' => '<hr class="menu-separator">', 'url' => '');
+	}
+}

+ 29 - 3
application/dashboard.class.inc.php

@@ -40,8 +40,11 @@ abstract class Dashboard
 	
 	public function FromXml($sXml)
 	{
+		$this->aCells = array(); // reset the content of the dashboard
+		set_error_handler(array('Dashboard', 'ErrorHandler'));
 		$oDoc = new DOMDocument();
 		$oDoc->loadXML($sXml);
+		restore_error_handler();
 		$this->oDOMNode = $oDoc->getElementsByTagName('dashboard')->item(0);
 		
 		$oLayoutNode = $this->oDOMNode->getElementsByTagName('layout')->item(0);
@@ -68,6 +71,21 @@ abstract class Dashboard
 		}
 	}
 	
+	/**
+	 * Error handler to turn XML loading warnings into exceptions
+	 */
+	public static function ErrorHandler($errno, $errstr, $errfile, $errline)
+	{
+		if ($errno == E_WARNING && (substr_count($errstr,"DOMDocument::loadXML()")>0))
+		{
+			throw new DOMException($errstr);
+		}
+		else
+		{
+			return false;
+		}
+	}
+
 	public function ToXml()
 	{
 		$oDoc = new DOMDocument();
@@ -365,13 +383,21 @@ class RuntimeDashboard extends Dashboard
 		if (!$bEditMode)
 		{
 			$sEditMenu = "<td><span id=\"DashboardMenu\"><ul><li><img src=\"../images/edit.png\"><ul>";
+		
+			$aActions = array();
+			$oEdit = new JSPopupMenuItem('UI:Dashboard:Edit', Dict::S('UI:Dashboard:Edit'), "return EditDashboard('{$this->sId}')");
+			$aActions[$oEdit->GetUID()] = $oEdit->GetMenuItem();
 
-			$sEditMenu .= "<li><a href=\"#\" onclick=\"return EditDashboard('{$this->sId}')\">".Dict::S('UI:Dashboard:Edit')."</a></li>";
 			if ($this->bCustomized)
 			{
-				$sEditMenu .= "<li><a href=\"#\" onclick=\"if (confirm('".addslashes(Dict::S('UI:Dashboard:RevertConfirm'))."')) return RevertDashboard('{$this->sId}'); else return false;\">".Dict::S('UI:Dashboard:Revert')."</a></li>";
+				$oRevert = new JSPopupMenuItem('UI:Dashboard:RevertConfirm', Dict::S('UI:Dashboard:Revert'),
+												"if (confirm('".addslashes(Dict::S('UI:Dashboard:RevertConfirm'))."')) return RevertDashboard('{$this->sId}'); else return false");
+				$aActions[$oRevert->GetUID()] = $oRevert->GetMenuItem();
 			}
-			$sEditMenu .= "</ul></li></ul></span></td>";
+			utils::GetPopupMenuItems($oPage, iPopupMenuExtension::MENU_DASHBOARD_ACTIONS, $this, $aActions);
+			$sEditMenu .= $oPage->RenderPopupMenuItems($aActions);
+					
+
 			$sEditMenu = addslashes($sEditMenu);
 			//$sEditBtn = addslashes('<div style="display: inline-block; height: 55px; width:200px;vertical-align:center;line-height:60px;text-align:left;"><button onclick="EditDashboard(\''.$this->sId.'\');">Edit This Page</button></div>');
 			$oPage->add_ready_script(

+ 12 - 3
application/datatable.class.inc.php

@@ -21,10 +21,11 @@
  * @author      Denis Flaven <denis.flaven@combodo.com>
  * @license     http://www.opensource.org/licenses/gpl-3.0.html GPL
  */
+
 class DataTable
 {
 	protected $iListId;		// Unique ID inside the web page
-	protected $sTableId;	// identifier for sqve the settings (combined with the class aliases)
+	protected $sTableId;	// identifier for saving the settings (combined with the class aliases)
 	protected $oSet;		// The set of objects to display
 	protected $aClassAliases;	// The aliases (alias => class) inside the set
 	protected $iNbObjects;		// Total number of objects inthe set
@@ -250,8 +251,16 @@ EOF;
 	protected function GetToolkitMenu(WebPage $oPage, $aExtraParams)
 	{
 		$sMenuTitle = Dict::S('UI:ConfigureThisList');
-		$sHtml = '<div class="itop_popup toolkit_menu" id="tk_'.$this->iListId.'"><ul><li><img src="../images/toolkit_menu.png"><ul><li><a  onclick="$(\'#datatable_dlg_'.$this->iListId.'\').dialog(\'open\');">'.$sMenuTitle.'</a></li></li></ul></div>';
-		//$oPage->add_ready_script("$('#tk_{$this->iListId} > ul').popupmenu();");
+		$sHtml = '<div class="itop_popup toolkit_menu" id="tk_'.$this->iListId.'"><ul><li><img src="../images/toolkit_menu.png"><ul>';
+		
+		$oMenuItem1 = new JSPopupMenuItem('iTop::ConfigureList', $sMenuTitle, "$('#datatable_dlg_".$this->iListId."').dialog('open');");
+		$aActions = array(
+			$oMenuItem1->GetUID() => $oMenuItem1->GetMenuItem(),
+		);
+		$this->oSet->Rewind();
+		utils::GetPopupMenuItems($oPage, iPopupMenuExtension::MENU_OBJLIST_TOOLKIT, $this->oSet, $aActions);
+		$this->oSet->Rewind();
+		$sHtml .= $oPage->RenderPopupMenuItems($aActions);
 		return $sHtml;
 	}
 	

+ 106 - 33
application/displayblock.class.inc.php

@@ -1359,7 +1359,7 @@ class MenuBlock extends DisplayBlock
 					{
 						// New plugins can provide javascript handlers via the 'onclick' property
 						//TODO: enable extension of different menus by checking the 'target' property ??
-						$aActions[$sLabel] = array ('label' => $sLabel, 'url' => isset($data['url']) ? $data['url'] : '#', 'url' => isset($data['onclick']) ? $data['onclick'] : '');
+						$aActions[$sLabel] = array ('label' => $sLabel, 'url' => isset($data['url']) ? $data['url'] : '#', 'onclick' => isset($data['onclick']) ? $data['onclick'] : '');
 					}
 					else
 					{
@@ -1368,6 +1368,24 @@ class MenuBlock extends DisplayBlock
 					}
 				}
 			}
+			
+			// New extensions based on iPopupMenuItem interface 
+			switch($this->m_sStyle)
+			{
+				case 'list':
+				$oSet->Rewind();
+				$param  = $oSet;
+				$iMenuId = iPopupMenuExtension::MENU_OBJLIST_ACTIONS;
+				break;
+				
+				case 'details':
+				$oSet->Rewind();
+				$param  = $oSet->Fetch();
+				$iMenuId = iPopupMenuExtension::MENU_OBJDETAILS_ACTIONS;
+				break;
+				
+			}
+			utils::GetPopupMenuItems($oPage, $iMenuId, $param, $aActions);
 		}
 		$aFavoriteActions = array();
 		$aCallSpec = array($sClass, 'GetShortcutActions');
@@ -1395,38 +1413,10 @@ class MenuBlock extends DisplayBlock
 		else
 		{
 			$sHtml .= "<div class=\"itop_popup actions_menu\"><ul>\n<li>".Dict::S('UI:Menu:Actions')."\n<ul>\n";
-		}	
-		$sPrevUrl = '';
-		foreach ($aActions as $key => $aAction)
-		{
-			if (in_array($key, $aShortcutActions))
-			{
-				$aFavoriteActions[] = $aAction;
-			}
-			else
-			{
-				$sClass = isset($aAction['class']) ? " class=\"{$aAction['class']}\"" : "";
-				$sOnClick = isset($aAction['onclick']) ? " onclick=\"{$aAction['onclick']}\"" : "";
-				if (empty($aAction['url']))
-				{
-					if ($sPrevUrl != '') // Don't output consecutively two separators...
-					{
-						$sHtml .= "<li>{$aAction['label']}</li>\n";			
-					}
-					$sPrevUrl = '';
-				}
-				else
-				{
-					$sHtml .= "<li><a href=\"{$aAction['url']}\"$sClass $sOnClick>{$aAction['label']}</a></li>\n";
-					$sPrevUrl = $aAction['url'];
-				}
-			}
-		}
-		$sHtml .= "</ul>\n</li>\n</ul></div>";
-		foreach(array_reverse($aFavoriteActions) as $aAction)
-		{
-			$sHtml .= "<div class=\"actions_button\"><a href='{$aAction['url']}'>{$aAction['label']}</a></div>";			
 		}
+
+		$sHtml .= $oPage->RenderPopupMenuItems($aActions, $aFavoriteActions);
+
 		static $bPopupScript = false;
 		if (!$bPopupScript)
 		{
@@ -1456,4 +1446,87 @@ class MenuBlock extends DisplayBlock
 		}
 	}	
 }
-?>
+
+/**
+ * Some dummy menus for testing
+ */
+class ExtraMenus implements iPopupMenuExtension
+{
+	/*
+	const MENU_OBJLIST_ACTIONS = 1; 	// $param is a DBObjectSet containing the list of objects
+	const MENU_OBJLIST_TOOLKIT = 2;		// $param is a DBObjectSet containing the list of objects
+	const MENU_OBJDETAILS_ACTIONS = 3;	// $param is a DBObject instance: the object currently displayed
+	const MENU_DASHBOARD_ACTIONS = 4;	// $param is a Dashboard instance: the dashboard currently displayed
+	const MENU_USER_ACTIONS = 5;		// $param is a null ??
+	*/
+	
+	/**
+	 * Get the list of items to be added to a menu. The items will be inserted in the menu in the order of the returned array
+	 * @param int $iMenuId The identifier of the type of menu, as listed by the constants MENU_xxx above
+	 * @param mixed $param Depends on $iMenuId see the constants define above
+	 * @return Array An array of ApplicationPopupMenuItem or an empty array if no action is to be added to the menu
+	 */
+	public static function EnumItems($iMenuId, $param)
+	{
+		switch($iMenuId)
+		{
+			/*
+			case iPopupMenuExtension::MENU_OBJLIST_ACTIONS:
+			// $param is a DBObjectSet
+			$aResult = array(
+				new JSPopupMenuItem('Test::Item1', 'List Test 1', "alert('Test 1')"),
+				new JSPopupMenuItem('Test::Item2', 'List Test 2', "alert('Test 2')"),
+			);
+			break;
+			
+			case iPopupMenuExtension::MENU_OBJLIST_TOOLKIT:
+			// $param is a DBObjectSet
+			$aResult = array(
+				new JSPopupMenuItem('Test::Item1', 'Toolkit Test 1', "alert('Test 1')"),
+				new JSPopupMenuItem('Test::Item2', 'Toolkit Test 2', "alert('Test 2')"),
+			);
+			break;
+			
+			case iPopupMenuExtension::MENU_OBJDETAILS_ACTIONS:
+			// $param is a DBObject
+			$aResult = array(
+				new JSPopupMenuItem('Test::Item1', 'Object Test 1', "alert('Test 1')"),
+				new JSPopupMenuItem('Test::Item2', 'Object Test 2', "alert('Test 2')"),
+			);
+			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' })",
+									array('../js/ajaxfileupload.js')),
+			);
+			break;
+			
+			/*
+			case iPopupMenuExtension::MENU_USER_ACTIONS:
+			// $param is null ??
+			$aResult = array(
+				new SeparatorPopupMenuItem(),
+				new JSPopupMenuItem('Test::Item1', 'Reset preferences...', "alert('Test 1')"),
+				new JSPopupMenuItem('Test::Item2', 'Do Something Stupid', "alert('Hey Dude !')"),
+			);
+			break;
+			*/
+			
+			default:
+			// Unknown type of menu, do nothing
+			$aResult = array();
+		}
+		return $aResult;
+	}
+}

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

@@ -650,18 +650,24 @@ EOF
 			}
 			$sLogOffMenu = "<span id=\"logOffBtn\"><ul><li><img src=\"../images/onOffBtn.png\"><ul>";
 			$sLogOffMenu .= "<li><span>$sLogonMessage</span></li>\n";
-			$sLogOffMenu .= "<li><a href=\"".utils::GetAbsoluteUrlAppRoot()."pages/preferences.php?".$oAppContext->GetForLink()."\">".Dict::S('UI:Preferences')."</a></li>\n";
+			$aActions = array();
+
+			$oPrefs = new URLPopupMenuItem('UI:Preferences', Dict::S('UI:Preferences'), utils::GetAbsoluteUrlAppRoot()."pages/preferences.php?".$oAppContext->GetForLink());
+			$aActions[$oPrefs->GetUID()] = $oPrefs->GetMenuItem();
 				
 			if (utils::CanLogOff())
 			{
-				//$sLogOffMenu .= "<li><a href=\"../pages/UI.php?loginop=logoff\">".Dict::S('UI:LogOffMenu')."</a></li>\n";
-				$sLogOffMenu .= "<li><a href=\"".utils::GetAbsoluteUrlAppRoot()."pages/logoff.php\">".Dict::S('UI:LogOffMenu')."</a></li>\n";
+				$oLogOff = new URLPopupMenuItem('UI:LogOffMenu', Dict::S('UI:LogOffMenu'), utils::GetAbsoluteUrlAppRoot().'pages/logoff.php');
+				$aActions[$oLogOff->GetUID()] = $oLogOff->GetMenuItem();
 			}
 			if (UserRights::CanChangePassword())
 			{
-				$sLogOffMenu .= "<li><a href=\"".utils::GetAbsoluteUrlAppRoot()."pages/UI.php?loginop=change_pwd\">".Dict::S('UI:ChangePwdMenu')."</a></li>\n";
+				$oChangePwd = new URLPopupMenuItem('UI:ChangePwdMenu', Dict::S('UI:ChangePwdMenu'), utils::GetAbsoluteUrlAppRoot().'pages/UI.php?loginop=change_pwd');
+				$aActions[$oChangePwd->GetUID()] = $oChangePwd->GetMenuItem();
 			}
-			$sLogOffMenu .= "</ul>\n</li>\n</ul></span>\n";
+			utils::GetPopupMenuItems($this, iPopupMenuExtension::MENU_USER_ACTIONS, null, $aActions);
+			$sLogOffMenu .= $this->RenderPopupMenuItems($aActions);
+
 
 			$sRestrictions = '';
 			if (!MetaModel::DBHasAccess(ACCESS_ADMIN_WRITE))

+ 1 - 1
application/menunode.class.inc.php

@@ -812,7 +812,7 @@ class DashboardMenuNode extends MenuNode
 		return parent::GetHyperlink($aExtraParams);
 	}
 
-	protected function GetDashboard()
+	public function GetDashboard()
 	{
 		$sDashboardDefinition = @file_get_contents($this->sDashboardFile);		
 		if ($sDashboardDefinition !== false)

+ 21 - 0
application/utils.inc.php

@@ -769,6 +769,27 @@ class utils
 	}
 	
 	/**
+	 * Get the "Back" button to go out of the current environment
+	 */
+	public static function GetPopupMenuItems($oPage, $iMenuId, $param, &$aActions)
+	{
+		foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance)
+		{
+			foreach($oExtensionInstance->EnumItems($iMenuId, $param) as $oMenuItem)
+			{
+				if (is_object($oMenuItem))
+				{
+					$aActions[$oMenuItem->GetUID()] = $oMenuItem->GetMenuItem();
+					
+					foreach($oMenuItem->GetLinkedScripts() as $sLinkedScript)
+					{
+						$oPage->add_linked_script($sLinkedScript);
+					}
+				}
+			}
+		}
+	}
+	/**
 	 * Get target configuration file name (including full path)
 	 */
 	public static function GetConfigFilePath()

+ 31 - 0
application/webpage.class.inc.php

@@ -604,5 +604,36 @@ class WebPage implements Page
 			$this->a_OutputOptions[$sFormat][$sOptionName] = $sValue;
 		}
 	}
+	
+	public function RenderPopupMenuItems($aActions, $aFavoriteActions = array())
+	{
+		$sPrevUrl = '';
+		$sHtml = '';
+		foreach ($aActions as $aAction)
+		{
+			$sClass = isset($aAction['class']) ? " class=\"{$aAction['class']}\"" : "";
+			$sOnClick = isset($aAction['onclick']) ? " onclick=\"{$aAction['onclick']}\"" : "";
+			if (empty($aAction['url']))
+			{
+				if ($sPrevUrl != '') // Don't output consecutively two separators...
+				{
+					$sHtml .= "<li>{$aAction['label']}</li>";			
+				}
+				$sPrevUrl = '';
+			}
+			else
+			{
+				$sHtml .= "<li><a href=\"{$aAction['url']}\"$sClass $sOnClick>{$aAction['label']}</a></li>";
+				$sPrevUrl = $aAction['url'];
+			}
+		}
+		$sHtml .= "</ul></li></ul></div>";
+		foreach(array_reverse($aFavoriteActions) as $aAction)
+		{
+			$sHtml .= "<div class=\"actions_button\"><a href='{$aAction['url']}'>{$aAction['label']}</a></div>";			
+		}
+		
+		return $sHtml;
+	}
 }
 ?>

+ 0 - 1
datamodel/itop-attachments/main.attachments.php

@@ -356,7 +356,6 @@ EOF
 			$oPage->add('<div style="clear:both"></div>');			
 			$sMaxUpload = $this->GetMaxUpload();
 			$oPage->p(Dict::S('Attachments:AddAttachment').'<input type="file" name="file" id="file" onChange="ajaxFileUpload();"><span style="display:none;" id="attachment_loading">&nbsp;<img src="../images/indicator.gif"></span> '.$sMaxUpload);
-			//$oPage->p('<input type="button" onClick="ajaxFileUpload();" value=" Upload !">');
 			$oPage->p('<span style="display:none;" id="attachment_loading">Loading, please wait...</span>');
 			$oPage->p('<input type="hidden" id="attachment_plugin"/>');
 			$oPage->add('</fieldset>');

+ 5 - 0
dictionaries/dictionary.itop.ui.php

@@ -988,6 +988,11 @@ When associated with a trigger, each action is given an "order" number, specifyi
 	'UI:Dashboard:Edit' => 'Edit This Page...',
 	'UI:Dashboard:Revert' => 'Revert To Original Version...',
 	'UI:Dashboard:RevertConfirm' => 'Every changes made to the original version will be lost. Please confirm that you want to do this.',
+	'UI:ExportDashBoard' => 'Export to a file',
+	'UI:ImportDashBoard' => 'Import from a file...',
+	'UI:ImportDashboardTitle' => 'Import From a File',
+	'UI:ImportDashboardText' => 'Select a dashboard file to import:',
+
 
 	'UI:DashletCreation:Title' => 'Create a new Dashlet',
 	'UI:DashletCreation:Dashboard' => 'Dashboard',

+ 4 - 0
dictionaries/fr.dictionary.itop.ui.php

@@ -832,6 +832,10 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
 	'UI:Dashboard:Edit' => 'Editer cette page...',
 	'UI:Dashboard:Revert' => 'Revenir à la version d\'origine...',
 	'UI:Dashboard:RevertConfirm' => 'Toutes modifications apportées à la version d\'origine seront perdues. Veuillez confirmer l\'opération.',
+	'UI:ExportDashBoard' => 'Exporter dans un fichier',
+	'UI:ImportDashBoard' => 'Importer depuis un fichier...',
+	'UI:ImportDashboardTitle' => 'Importation depuis un fichier',
+	'UI:ImportDashboardText' => 'Choisissez un fichier de définition de tableau de bord :',
 
 	'UI:DashletCreation:Title' => 'Créer un Indicateur',
 	'UI:DashletCreation:Dashboard' => 'Tableau de bord',

+ 121 - 1
js/dashboard.js

@@ -219,4 +219,124 @@ $(function()
 			});	
 		}
 	});	
-});
+});
+
+function UploadDashboard(oOptions)
+{
+	var sFileId = 'dashboard_upload_file';
+	var oDlg = $('<div id="dashboard_upload_dlg"><form><p>'+oOptions.text+'</p><p><input type="file" id="'+sFileId+'" name="dashboard_upload_file"></p></form></div>');
+	$('body').append(oDlg);
+	oOptions.file_id = sFileId;
+	
+	oDlg.dashboard_upload_dlg(oOptions);
+}
+
+
+//jQuery UI style "widget" for managing a "import dashboard" dialog (file upload)
+$(function()
+{
+	// the widget definition, where "itop" is the namespace,
+	// "dashboard-upload-dlg" the widget name
+	$.widget( "itop.dashboard_upload_dlg",
+	{
+		// default options
+		options:
+		{
+			dashboard_id: '',
+			file_id: '',
+			text: 'Select a dashboard file to import',
+			title: 'Dahsboard Import',
+			close_btn: 'Close',
+			submit_to: GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?operation=import_dashboard'
+		},
+	
+		// the constructor
+		_create: function()
+		{
+			var me = this; 
+
+			var oButtons = {};
+			oButtons[this.options.close_btn] = function() {
+				me.element.dialog('close');
+				//me.onClose();
+			};
+			$('#'+this.options.file_id).bind('change', function() { me._doUpload(); } );
+			this.element
+			.addClass('itop-dashboard_upload_dlg')
+			.dialog({
+				modal: true,
+				width: 500,
+				height: 'auto',
+				title: this.options.title,
+				close: function() { me._onClose(); },
+				buttons: oButtons
+			});
+		},
+	
+		// 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('itop-dashboard_upload_dlg');
+
+			// call the original destroy method since we overwrote it
+			$.Widget.prototype.destroy.call( this );			
+		},
+		// _setOptions is called with a hash of all options that are changing
+		_setOptions: function()
+		{
+			// in 1.9 would use _superApply
+			$.Widget.prototype._setOptions.apply( this, arguments );
+			this._refresh();
+		},
+		// _setOption is called for each individual option that is changing
+		_setOption: function( key, value )
+		{
+			// in 1.9 would use _super
+			$.Widget.prototype._setOption.call( this, key, value );
+		},
+		_onClose: function()
+		{
+			this.element.remove();
+		},
+		_doUpload: function()
+		{
+			var me = this;
+			$.ajaxFileUpload
+			(
+				{
+					url: me.options.submit_to+'&id='+me.options.dashboard_id, 
+					secureuri:false,
+					fileElementId: me.options.file_id,
+					dataType: 'json',
+					success: function (data, status)
+					{
+						if(typeof(data.error) != 'undefined')
+						{
+							if(data.error != '')
+							{
+								alert(data.error);
+								me.element.dialog('close');
+							}
+							else
+							{
+								me.element.dialog('close');
+								location.reload();
+							}
+						}
+					},
+					error: function (data, status, e)
+					{
+						alert(e);
+						me.element.dialog('close');
+					}
+				}
+			)			
+		}
+	});
+});

+ 50 - 0
pages/ajax.render.php

@@ -820,6 +820,56 @@ try
 		}
 		break;
 		
+		case 'export_dashboard':
+		$sMenuId = utils::ReadParam('id', '');
+		ApplicationMenu::LoadAdditionalMenus();
+		$index = ApplicationMenu::GetMenuIndexById($sMenuId);
+		$oMenu = ApplicationMenu::GetMenuNode($index);
+		if ($oMenu instanceof DashboardMenuNode)
+		{
+			$oDashboard = $oMenu->GetDashboard();
+			$sPreviousContent = ob_get_clean();
+			if (trim($sPreviousContent) != '')
+			{
+				IssueLog::Error("Output already started before downloading file:\nContent was:'$sPreviousContent'\n");
+			}
+			$oPage->SetContentType('text/xml');
+			$oPage->SetContentDisposition('attachment', $oMenu->GetLabel().'.xml');
+			$oPage->add($oDashboard->ToXml());
+		}
+		break;
+		
+		case 'import_dashboard':
+		$sMenuId = utils::ReadParam('id', '');
+		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;
+		
 		default:
 		$oPage->p("Invalid query.");
 	}