Selaa lähdekoodia

Dashboard re-engineering

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@2782 a333f486-631f-4898-b8df-5754b55c2be0
romainq 12 vuotta sitten
vanhempi
commit
63c0224f25

+ 140 - 95
application/dashboard.class.inc.php

@@ -34,15 +34,16 @@ abstract class Dashboard
 	protected $oDOMNode;
 	protected $sId;
 	protected $aCells;
+	protected $oMetaModel;
 	
 	public function __construct($sId)
 	{
-		$this->sLayoutClass = null;
+		$this->sLayoutClass = 'DashboardLayoutOneCol';
 		$this->aCells = array();
 		$this->oDOMNode = null;
 		$this->sId = $sId;
 	}
-	
+
 	public function FromXml($sXml)
 	{
 		$this->aCells = array(); // reset the content of the dashboard
@@ -50,60 +51,83 @@ abstract class Dashboard
 		$oDoc = new DOMDocument();
 		$oDoc->loadXML($sXml);
 		restore_error_handler();
+		$this->FromDOMDocument($oDoc);
+	}
+	
+	public function FromDOMDocument(DOMDocument $oDoc)
+	{
 		$this->oDOMNode = $oDoc->getElementsByTagName('dashboard')->item(0);
 		
-		$oLayoutNode = $this->oDOMNode->getElementsByTagName('layout')->item(0);
-		$this->sLayoutClass = $oLayoutNode->textContent;
+		if ($oLayoutNode = $this->oDOMNode->getElementsByTagName('layout')->item(0))
+		{
+			$this->sLayoutClass = $oLayoutNode->textContent;
+		}
+		else
+		{
+			$this->sLayoutClass = 'DashboardLayoutOneCol';
+		}
 		
-		$oTitleNode = $this->oDOMNode->getElementsByTagName('title')->item(0);
-		$this->sTitle = $oTitleNode->textContent;
+		if ($oTitleNode = $this->oDOMNode->getElementsByTagName('title')->item(0))
+		{
+			$this->sTitle = $oTitleNode->textContent;
+		}
+		else
+		{
+			$this->sTitle = '';
+		}
 		
-		$oCellsNode = $this->oDOMNode->getElementsByTagName('cells')->item(0);
-		$oCellsList = $oCellsNode->getElementsByTagName('cell');
-		$aCellOrder = array();
-		$iCellRank = 0;
-		foreach($oCellsList as $oCellNode)
+		if ($oCellsNode = $this->oDOMNode->getElementsByTagName('cells')->item(0))
 		{
-			$aDashletList = array();
-			$oCellRank =  $oCellNode->getElementsByTagName('rank')->item(0);
-			if ($oCellRank)
-			{
-				$iCellRank = (float)$oCellRank->textContent;
-			}
-			$oDashletsNode = $oCellNode->getElementsByTagName('dashlets')->item(0);
-			$oDashletList = $oDashletsNode->getElementsByTagName('dashlet');
-			$iRank = 0;
-			$aDashletOrder = array();
-			foreach($oDashletList as $oDomNode)
+			$oCellsList = $oCellsNode->getElementsByTagName('cell');
+			$aCellOrder = array();
+			$iCellRank = 0;
+			foreach($oCellsList as $oCellNode)
 			{
-				$sDashletClass = $oDomNode->getAttribute('xsi:type');
-				$oRank =  $oDomNode->getElementsByTagName('rank')->item(0);
-				if ($oRank)
+				$aDashletList = array();
+				$oCellRank =  $oCellNode->getElementsByTagName('rank')->item(0);
+				if ($oCellRank)
 				{
-					$iRank = (float)$oRank->textContent;
+					$iCellRank = (float)$oCellRank->textContent;
+				}
+				$oDashletsNode = $oCellNode->getElementsByTagName('dashlets')->item(0);
+				{
+					$oDashletList = $oDashletsNode->getElementsByTagName('dashlet');
+					$iRank = 0;
+					$aDashletOrder = array();
+					foreach($oDashletList as $oDomNode)
+					{
+						$sDashletClass = $oDomNode->getAttribute('xsi:type');
+						$oRank =  $oDomNode->getElementsByTagName('rank')->item(0);
+						if ($oRank)
+						{
+							$iRank = (float)$oRank->textContent;
+						}
+						$sId = $oDomNode->getAttribute('id');
+						$oNewDashlet = new $sDashletClass($this->oMetaModel, $sId);
+						$oNewDashlet->FromDOMNode($oDomNode);
+						$aDashletOrder[] = array('rank' => $iRank, 'dashlet' => $oNewDashlet);
+					}
+					usort($aDashletOrder, array(get_class($this), 'SortOnRank'));
+					$aDashletList = array();
+					foreach($aDashletOrder as $aItem)
+					{
+						$aDashletList[] = $aItem['dashlet'];
+					}
+					$aCellOrder[] = array('rank' => $iCellRank, 'dashlets' => $aDashletList);
 				}
-				$sId = $oDomNode->getAttribute('id');
-				$oNewDashlet = new $sDashletClass(new ModelReflectionRuntime(), $sId);
-				$oNewDashlet->FromDOMNode($oDomNode);
-				$aDashletOrder[] = array('rank' => $iRank, 'dashlet' => $oNewDashlet);
 			}
-			usort($aDashletOrder, array(get_class($this), 'SortOnRank'));
-			$aDashletList = array();
-			foreach($aDashletOrder as $aItem)
+			usort($aCellOrder, array(get_class($this), 'SortOnRank'));
+			foreach($aCellOrder as $aItem)
 			{
-				$aDashletList[] = $aItem['dashlet'];
+				$this->aCells[] = $aItem['dashlets'];
 			}
-			$aCellOrder[] = array('rank' => $iCellRank, 'dashlets' => $aDashletList);
 		}
-		usort($aCellOrder, array(get_class($this), 'SortOnRank'));
-		foreach($aCellOrder as $aItem)
+		else
 		{
-			$this->aCells[] = $aItem['dashlets'];
+			$this->aCells = array();
 		}
-		
-		
 	}
-	
+
 	static function SortOnRank($aItem1, $aItem2)
 	{
 		return ($aItem1['rank'] > $aItem2['rank']) ? +1 : -1;
@@ -133,14 +157,24 @@ abstract class Dashboard
 		$oMainNode->setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance");
 		$oDoc->appendChild($oMainNode);
 		
+		$this->ToDOMNode($oMainNode);
+
+		$sXml = $oDoc->saveXML();
+		return $sXml;
+	}
+
+	public function ToDOMNode($oDefinition)
+	{
+		$oDoc = $oDefinition->ownerDocument;
+
 		$oNode = $oDoc->createElement('layout', $this->sLayoutClass);
-		$oMainNode->appendChild($oNode);
+		$oDefinition->appendChild($oNode);
 
 		$oNode = $oDoc->createElement('title', $this->sTitle);
-		$oMainNode->appendChild($oNode);
+		$oDefinition->appendChild($oNode);
 
 		$oCellsNode = $oDoc->createElement('cells');
-		$oMainNode->appendChild($oCellsNode);
+		$oDefinition->appendChild($oCellsNode);
 		
 		$iCellRank = 0;
 		foreach ($this->aCells as $aCell)
@@ -167,11 +201,9 @@ abstract class Dashboard
 				$oDashlet->ToDOMNode($oNode);
 			}
 		}
-
-		$sXml = $oDoc->saveXML();
-		return $sXml;
 	}
 
+
 	public function FromParams($aParams)
 	{
 		$this->sLayoutClass = $aParams['layout_class'];
@@ -184,7 +216,7 @@ abstract class Dashboard
 			{
 				$sDashletClass = $aDashletParams['dashlet_class'];
 				$sId = $aDashletParams['dashlet_id'];
-				$oNewDashlet = new $sDashletClass(new ModelReflectionRuntime(), $sId);
+				$oNewDashlet = new $sDashletClass($this->oMetaModel, $sId);
 				
 				$oForm = $oNewDashlet->GetForm();
 				$oForm->SetParamsContainer($sId);
@@ -197,7 +229,7 @@ abstract class Dashboard
 		}
 		
 	}
-	
+
 	public function Save()
 	{
 		
@@ -267,24 +299,43 @@ abstract class Dashboard
 		$oPage->add('</div>');
 		
 		$oForm = new DesignerForm();
+
+		$oField = new DesignerHiddenField('dashboard_id', '', $this->sId);
+		$oForm->AddField($oField);
+
 		$oField = new DesignerLongTextField('dashboard_title', Dict::S('UI:DashboardEdit:DashboardTitle'), $this->sTitle);
 		$oForm->AddField($oField);
 		$this->SetFormParams($oForm);
-		$oForm->RenderAsPropertySheet($oPage, false, ':itop-dashboard');	
+		$oForm->RenderAsPropertySheet($oPage, false, '.itop-dashboard');	
 		
 		$oPage->add('</div>');
 		$oPage->add_ready_script(
 <<<EOF
 	$('#select_layout').buttonset();
-	$('#select_layout input').click( function() {
-		var sLayoutClass = $(this).val();
-		$(':itop-dashboard').dashboard('option', {layout_class: sLayoutClass});
-	} );
-	$('#row_attr_dashboard_title').property_field('option', {parent_selector: ':itop-dashboard', auto_apply: false, 'do_apply': function() {
-			var sTitle = $('#attr_dashboard_title').val();
-			$(':itop-dashboard').dashboard('option', {title: sTitle});
-			return true;
-		}
+	$('#select_dashlet').droppable({
+		accept: '.dashlet',
+		drop: function(event, ui) {
+			$( this ).find( ".placeholder" ).remove();
+			var oDashlet = ui.draggable.data('itopDashlet');
+			oDashlet._remove_dashlet();
+		},
+	});
+
+	$('#event_bus').bind('dashlet-selected', function(event, data){
+		var sDashletId = data.dashlet_id;
+		var sPropId = 'dashlet_properties_'+sDashletId;
+		$('.dashlet_properties').each(function() {
+			var sId = $(this).attr('id');
+			var bShow = (sId == sPropId);
+			if (bShow)
+			{
+				$(this).show();
+			}
+			else
+			{
+				$(this).hide();
+			}
+		});
 	});
 EOF
 		);
@@ -319,7 +370,6 @@ EOF
 
 		$oPage->add('</div>');
 		$oPage->add_ready_script("$('.dashlet_icon').draggable({helper: 'clone', appendTo: 'body', zIndex: 10000, revert:'invalid'});");
-		$oPage->add_ready_script("$('.layout_cell').droppable({accept:'.dashlet_icon', hoverClass:'dragHover'});");
 	}
 	
 	public function RenderDashletsProperties($oPage)
@@ -339,7 +389,7 @@ EOF
 					$oPage->add('<div class="dashlet_properties" id="dashlet_properties_'.$sId.'" style="display:none">');
 					$oForm = $oDashlet->GetForm();
 					$this->SetFormParams($oForm);
-					$oForm->RenderAsPropertySheet($oPage, false, ':itop-dashboard');		
+					$oForm->RenderAsPropertySheet($oPage, false, '.itop-dashboard');		
 					$oPage->add('</div>');
 				}
 			}
@@ -368,11 +418,12 @@ EOF
 class RuntimeDashboard extends Dashboard
 {
 	protected $bCustomized;
-	
+
 	public function __construct($sId)
 	{
 		parent::__construct($sId);
 		$this->bCustomized = false;
+		$this->oMetaModel = new ModelReflectionRuntime();
 	}
 		
 	public function SetCustomFlag($bCustomized)
@@ -482,7 +533,28 @@ EOF
 			);
 		}
 	}
-	
+
+	public function RenderProperties($oPage)
+	{
+		parent::RenderProperties($oPage);
+
+		$oPage->add_ready_script(
+<<<EOF
+	$('#select_layout input').click( function() {
+		var sLayoutClass = $(this).val();
+		$('.itop-dashboard').runtimedashboard('option', {layout_class: sLayoutClass});
+	} );
+	$('#row_attr_dashboard_title').property_field('option', {parent_selector: '.itop-dashboard', auto_apply: false, 'do_apply': function() {
+			var sTitle = $('#attr_dashboard_title').val();
+			$('.itop-dashboard').runtimedashboard('option', {title: sTitle});
+			return true;
+		}
+	});
+EOF
+		);
+	}
+
+
 	public function RenderEditor($oPage)
 	{
 		$oPage->add('<div id="dashboard_editor">');
@@ -521,7 +593,7 @@ $('#dashboard_editor').dialog({
 	title: '$sDialogTitle',
 	buttons: [
 	{ text: "$sOkButtonLabel", click: function() {
-		var oDashboard = $(':itop-dashboard').data('itopDashboard');
+		var oDashboard = $('.itop-dashboard').data('itopRuntimedashboard');
 		if (oDashboard.is_dirty())
 		{
 			if (!confirm('$sAutoApplyConfirmationMessage'))
@@ -537,7 +609,7 @@ $('#dashboard_editor').dialog({
 		oDashboard.save();
 	} },
 	{ text: "$sCancelButtonLabel", click: function() {
-		var oDashboard = $(':itop-dashboard').data('itopDashboard');
+		var oDashboard = $('.itop-dashboard').data('itopRuntimedashboard');
 		if (oDashboard.is_modified())
 		{
 			if (!confirm('$sCancelConfirmationMessage'))
@@ -553,40 +625,13 @@ $('#dashboard_editor').dialog({
 	close: function() { $(this).remove(); }
 });
 
-$('#dashboard_editor .ui-layout-center').dashboard({
+$('#dashboard_editor .ui-layout-center').runtimedashboard({
 	dashboard_id: '$sId', layout_class: '$sLayoutClass', title: '$sTitle',
 	submit_to: '$sUrl', submit_parameters: {operation: 'save_dashboard'},
 	render_to: '$sUrl', render_parameters: {operation: 'render_dashboard'},
 	new_dashlet_parameters: {operation: 'new_dashlet'}
 });
 
-$('#select_dashlet').droppable({
-	accept: '.dashlet',
-	drop: function(event, ui) {
-		$( this ).find( ".placeholder" ).remove();
-		var oDashlet = ui.draggable;
-		oDashlet.remove();
-	},
-});
-
-$('#event_bus').bind('dashlet-selected', function(event, data){
-		var sDashletId = data.dashlet_id;
-		var sPropId = 'dashlet_properties_'+sDashletId;
-		$('.dashlet_properties').each(function() {
-			var sId = $(this).attr('id');
-			var bShow = (sId == sPropId);
-			if (bShow)
-			{
-				$(this).show();
-			}
-			else
-			{
-				$(this).hide();
-			}
-		});
-
-	});
-	
 dashboard_prop_size = GetUserPreference('dashboard_prop_size', 350);
 $('#dashboard_editor').layout({
 	east: {
@@ -607,7 +652,7 @@ $('#dashboard_editor').layout({
 window.onbeforeunload = function() {
 	if (!window.bLeavingOnUserAction)
 	{
-		var oDashboard = $(':itop-dashboard').data('itopDashboard');
+		var oDashboard = $('.itop-dashboard').data('itopRuntimedashboard');
 		if (oDashboard)
 		{
 			if (oDashboard.is_dirty())
@@ -688,7 +733,7 @@ EOF
 		foreach($aDashlets as $sDashletClass => $aDashletInfo)
 		{
 			$oSubForm = new DesignerForm();
-			$oDashlet = new $sDashletClass(new ModelReflectionRuntime(), 0);
+			$oDashlet = new $sDashletClass($this->oMetaModel, 0);
 			$oDashlet->GetPropertiesFieldsFromOQL($oSubForm, $sOQL);
 			
 			$oSelectorField->AddSubForm($oSubForm, $aDashletInfo['label'], $aDashletInfo['class']);

+ 5 - 3
application/dashboardlayout.class.inc.php

@@ -107,14 +107,16 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
 		$oPage->add('<table style="width:100%"><tbody>');
 		$iCellIdx = 0;
 		$fColSize = 100 / $this->iNbCols;
-		$sStyle = $bEditMode ? 'style="border: 1px #ccc dashed; width:'.$fColSize.'%;" class="layout_cell edit_mode"' : 'style="width: '.$fColSize.'%;" class="dashboard"';
+		$sStyle = $bEditMode ? 'border: 1px #ccc dashed; width:'.$fColSize.'%;' : 'width: '.$fColSize.'%;';
+		$sClass = $bEditMode ? 'layout_cell edit_mode' : 'dashboard';
 		$iNbRows = ceil(count($aCells) / $this->iNbCols);
 		for($iRows = 0; $iRows < $iNbRows; $iRows++)
 		{
 			$oPage->add('<tr>');
 			for($iCols = 0; $iCols < $this->iNbCols; $iCols++)
 			{
-				$oPage->add("<td $sStyle>");
+				$sCellClass = ($iRows == $iNbRows-1) ? $sClass.' layout_last_used_rank' : $sClass;
+				$oPage->add("<td style=\"$sStyle\" class=\"$sCellClass\" data-dashboard-cell-index=\"$iCellIdx\">");
 				if (array_key_exists($iCellIdx, $aCells))
 				{
 					$aDashlets = $aCells[$iCellIdx];
@@ -144,7 +146,7 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
 		}
 		if ($bEditMode) // Add one row for extensibility
 		{
-			$sStyle = 'style="border: 1px #ccc dashed; width:'.$fColSize.'%;" class="layout_cell edit_mode layout_extension"';
+			$sStyle = 'style="border: 1px #ccc dashed; width:'.$fColSize.'%;" class="layout_cell edit_mode layout_extension" data-dashboard-cell-index="'.$iCellIdx.'"';
 			$oPage->add('<tr>');
 			for($iCols = 0; $iCols < $this->iNbCols; $iCols++)
 			{

+ 334 - 112
application/dashlet.class.inc.php

@@ -87,11 +87,10 @@ abstract class Dashlet
 	{
 		foreach ($this->aProperties as $sProperty => $value)
 		{
-			$this->oDOMNode = $oDOMNode->getElementsByTagName($sProperty)->item(0);
-			if ($this->oDOMNode != null)
+			$oPropNode = $oDOMNode->getElementsByTagName($sProperty)->item(0);
+			if ($oPropNode != null)
 			{
-				$newvalue = $this->Str2Prop($sProperty, $this->oDOMNode->textContent);
-				$this->aProperties[$sProperty] = $newvalue;
+				$this->aProperties[$sProperty] = $this->PropertyFromDOMNode($oPropNode, $sProperty);
 			}
 		}
 	}
@@ -100,12 +99,26 @@ abstract class Dashlet
 	{
 		foreach ($this->aProperties as $sProperty => $value)
 		{
-			$sXmlValue = $this->Prop2Str($value);
-			$oPropNode = $oDOMNode->ownerDocument->createElement($sProperty, $sXmlValue);
+			$oPropNode = $oDOMNode->ownerDocument->createElement($sProperty);
 			$oDOMNode->appendChild($oPropNode);
+			$this->PropertyToDOMNode($oPropNode, $sProperty, $value);
 		}
 	}
-	
+
+
+	protected function PropertyFromDOMNode($oDOMNode, $sProperty)
+	{
+		$res = $this->Str2Prop($sProperty, $oDOMNode->textContent);
+		return $res;
+	}
+
+	protected function PropertyToDOMNode($oDOMNode, $sProperty, $value)
+	{
+		$sXmlValue = $this->Prop2Str($value);
+		$oTextNode = $oDOMNode->ownerDocument->createTextNode($sXmlValue);
+		$oDOMNode->appendChild($oTextNode);
+	}
+
 	public function FromXml($sXml)
 	{
 		$oDomDoc = new DOMDocument('1.0', 'UTF-8');
@@ -139,10 +152,24 @@ abstract class Dashlet
 				$oPage->add('<div class="'.$sCSSClasses.'">');
 			}
 		}
+		else
+		{
+			foreach ($this->aCSSClasses as $sCSSClass)
+			{
+				$oPage->add_ready_script("$('#dashlet_".$sId."').addClass('$sCSSClass');");
+			}
+		}
 		
 		try
 		{
-			$this->Render($oPage, $bEditMode, $aExtraParams);
+			if (get_class($this->oModelReflection) == 'ModelReflectionRuntime')
+			{
+				$this->Render($oPage, $bEditMode, $aExtraParams);
+			}
+			else
+			{
+				$this->RenderNoData($oPage, $bEditMode, $aExtraParams);
+			}
 		}
 		catch(UnknownClassOqlException $e)
 		{
@@ -195,6 +222,12 @@ EOF
 	}
 	
 	abstract public function Render($oPage, $bEditMode = false, $aExtraParams = array());
+
+	/* Rendering without the real data */
+	public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = array())
+	{
+		$this->Render($oPage, $bEditMode, $aExtraParams);
+	}
 		
 	abstract public function GetPropertiesFields(DesignerForm $oForm);
 	
@@ -312,7 +345,7 @@ class DashletPlainText extends Dashlet
 		$sText = htmlentities($this->aProperties['text'], ENT_QUOTES, 'UTF-8');
 
 		$sId = 'plaintext_'.($bEditMode? 'edit_' : '').$this->sId;
-		$oPage->add('<div id='.$sId.'" class="dashlet-content">'.$sText.'</div>');
+		$oPage->add('<div id="'.$sId.'" class="dashlet-content">'.$sText.'</div>');
 	}
 
 	public function GetPropertiesFields(DesignerForm $oForm)
@@ -365,6 +398,27 @@ class DashletObjectList extends Dashlet
 		$oPage->add('</div>');
 	}
 
+	public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = array())
+	{
+		$sTitle = $this->aProperties['title'];
+		$sQuery = $this->aProperties['query'];
+		$sShowMenu = $this->aProperties['menu'] ? '1' : '0';
+
+		$oPage->add('<div class="dashlet-content">');
+		$sHtmlTitle = htmlentities($this->oModelReflection->DictString($sTitle), ENT_QUOTES, 'UTF-8'); // done in the itop block
+		if ($sHtmlTitle != '')
+		{
+			$oPage->add('<h1>'.$sHtmlTitle.'</h1>');
+		}
+		$oQuery = $this->oModelReflection->GetQuery($sQuery);
+		$sClass = $oQuery->GetClass();
+		$oPage->add('<div id="block_fake_'.$this->sId.'" class="display_block">');
+		$oPage->p(Dict::S('UI:NoObjectToDisplay'));
+		$oPage->p('<a href="">'.Dict::Format('UI:ClickToCreateNew', $this->oModelReflection->GetName($sClass)).'</a>');
+		$oPage->add('</div>');
+		$oPage->add('</div>');
+	}
+
 	public function GetPropertiesFields(DesignerForm $oForm)
 	{
 		$oField = new DesignerTextField('title', Dict::S('UI:DashletObjectList:Prop-Title'), $this->aProperties['title']);
@@ -531,10 +585,74 @@ abstract class DashletGroupBy extends Dashlet
 		}
 	}
 
+	public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = array())
+	{
+		$sTitle = $this->aProperties['title'];
+		$sQuery = $this->aProperties['query'];
+		$sGroupBy = $this->aProperties['group_by'];
+		$sStyle = $this->aProperties['style'];
+
+		$oQuery = $this->oModelReflection->GetQuery($sQuery);
+		$sClass = $oQuery->GetClass();
+
+
+		$aDisplayValues = array();
+		$sAttLabel = '';
+		if ($this->oModelReflection->IsValidAttCode($sClass, $sGroupBy))
+		{
+			$sAttLabel = $this->oModelReflection->GetLabel($sClass, $sGroupBy);
+			$aAllowed = $this->oModelReflection->GetAllowedValues_att($sClass, $sGroupBy);
+			if ($aAllowed) // null for non enums
+			{
+				$iTotal = 0;
+				foreach ($aAllowed as $sValue => $sValueLabel)
+				{
+					$iCount = (int) rand(2, 100);
+					$iTotal += $iCount;
+					$aDisplayValues[] = array(
+						'label' => $sValueLabel,
+						'count' => $iCount
+					);
+				}
+			}
+		}
+		else
+		{
+			$oPage->add('<div class="dashlet-content">Sorry, grouping by date/time cannot be viewed in the designer</div>');
+		}
+
+		$oPage->add('<div class="dashlet-content">');
+
+		$sBlockId = 'block_fake_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occuring in the same DOM)
+
+		$oPage->add('<div id="'.$sBlockId.'" class="display_block">');
+		$oPage->add('<p>'.Dict::Format('UI:Pagination:HeaderNoSelection', $iTotal).'</p>');
+		$oPage->add('<table class="listResults">');
+		$oPage->add('<thead>');
+		$oPage->add('<tr>');
+		$oPage->add('<th class="header" title="">'.$sAttLabel.'</th>');
+		$oPage->add('<th class="header" title="'.Dict::S('UI:GroupBy:Count+').'">'.Dict::S('UI:GroupBy:Count').'</th>');
+		$oPage->add('</tr>');
+		$oPage->add('</thead>');
+		$oPage->add('<tbody>');
+		foreach($aDisplayValues as $aDisplayData)
+		{
+			$oPage->add('<tr class="even">');
+			$oPage->add('<td class=""><span title="Active">'.$aDisplayData['label'].'</span></td>');
+			$oPage->add('<td class=""><a href="">'.$aDisplayData['count'].'</a></td>');
+			$oPage->add('</tr>');
+		}
+		$oPage->add('</tbody>');
+		$oPage->add('</table>');
+		$oPage->add('</div>');
+
+		$oPage->add('</div>');
+	}
+
 	protected function GetGroupByOptions($sOql)
 	{
-		$oSearch = DBObjectSearch::FromOQL($sOql);
-		$sClass = $oSearch->GetClass();
+		$oQuery = $this->oModelReflection->GetQuery($sOql);
+		$sClass = $oQuery->GetClass();
 		$aGroupBy = array();
 		foreach($this->oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType)
 		{
@@ -604,11 +722,11 @@ abstract class DashletGroupBy extends Dashlet
 			try
 			{
 				$sCurrQuery = $aValues['query'];
-				$oCurrSearch = DBObjectSearch::FromOQL($sCurrQuery);
+				$oCurrSearch = $this->oModelReflection->GetQuery($sCurrQuery);
 				$sCurrClass = $oCurrSearch->GetClass();
 	
 				$sPrevQuery = $this->aProperties['query'];
-				$oPrevSearch = DBObjectSearch::FromOQL($sPrevQuery);
+				$oPrevSearch = $this->oModelReflection->GetQuery($sPrevQuery);
 				$sPrevClass = $oPrevSearch->GetClass();
 	
 				if ($sCurrClass != $sPrevClass)
@@ -631,15 +749,15 @@ abstract class DashletGroupBy extends Dashlet
 			{
 				// Style changed, mutate to the specified type of chart
 				case 'pie':
-				$oDashlet = new DashletGroupByPie($this->sId);
+				$oDashlet = new DashletGroupByPie($this->oModelReflection, $this->sId);
 				break;
 					
 				case 'bars':
-				$oDashlet = new DashletGroupByBars($this->sId);
+				$oDashlet = new DashletGroupByBars($this->oModelReflection, $this->sId);
 				break;
 					
 				case 'table':
-				$oDashlet = new DashletGroupByTable($this->sId);
+				$oDashlet = new DashletGroupByTable($this->oModelReflection, $this->sId);
 				break;
 			}
 			$oDashlet->FromParams($aValues);
@@ -756,9 +874,8 @@ class DashletHeaderStatic extends Dashlet
 	{
 		parent::__construct($oModelReflection, $sId);
 		$this->aProperties['title'] = Dict::S('UI:DashletHeaderStatic:Prop-Title:Default');
-		$sIcon = $this->oModelReflection->GetClassIcon('Contact', false);
-		$sIcon = str_replace(utils::GetAbsoluteUrlModulesRoot(), '', $sIcon);
-		$this->aProperties['icon'] = $sIcon;
+		$oIconSelect = $this->oModelReflection->GetIconSelectionField('icon');
+		$this->aProperties['icon'] = $oIconSelect->GetDefaultValue('Contact');
 	}
 	
 	public function Render($oPage, $bEditMode = false, $aExtraParams = array())
@@ -766,13 +883,14 @@ class DashletHeaderStatic extends Dashlet
 		$sTitle = $this->aProperties['title'];
 		$sIcon = $this->aProperties['icon'];
 
-		$sIconPath = utils::GetAbsoluteUrlModulesRoot().$sIcon;
+		$oIconSelect = $this->oModelReflection->GetIconSelectionField('icon');
+		$sIconPath = $oIconSelect->MakeFileUrl($sIcon);
 
 		$oPage->add('<div class="dashlet-content">');
 		$oPage->add('<div class="main_header">');
 
 		$oPage->add('<img src="'.$sIconPath.'">');
-		$oPage->add('<h1>'.Dict::S($sTitle).'</h1>');
+		$oPage->add('<h1>'.$this->oModelReflection->DictString($sTitle).'</h1>');
 
 		$oPage->add('</div>');
 		$oPage->add('</div>');
@@ -783,18 +901,36 @@ class DashletHeaderStatic extends Dashlet
 		$oField = new DesignerTextField('title', Dict::S('UI:DashletHeaderStatic:Prop-Title'), $this->aProperties['title']);
 		$oForm->AddField($oField);
 		
-		$oField = new DesignerIconSelectionField('icon', Dict::S('UI:DashletHeaderStatic:Prop-Icon'), $this->aProperties['icon']);
-		$aAllIcons = self::FindIcons(APPROOT.'env-'.utils::GetCurrentEnvironment());
-		ksort($aAllIcons);
-		$aValues = array();
-		foreach($aAllIcons as $sFilePath)
-		{
-			$aValues[] = array('value' => $sFilePath, 'label' => basename($sFilePath), 'icon' => utils::GetAbsoluteUrlModulesRoot().$sFilePath);
-		}
-		$oField->SetAllowedValues($aValues);
+		$oField = $this->oModelReflection->GetIconSelectionField('icon', Dict::S('UI:DashletHeaderStatic:Prop-Icon'), $this->aProperties['icon']);
 		$oForm->AddField($oField);
 	}
 	
+	protected function PropertyFromDOMNode($oDOMNode, $sProperty)
+	{
+		if ($sProperty == 'icon')
+		{
+			$oIconField = $this->oModelReflection->GetIconSelectionField('icon');
+			return $oIconField->ValueFromDOMNode($oDOMNode);
+		}
+		else
+		{
+			return parent::PropertyFromDOMNode($oDOMNode, $sProperty);
+		}
+	}
+
+	protected function PropertyToDOMNode($oDOMNode, $sProperty, $value)
+	{
+		if ($sProperty == 'icon')
+		{
+			$oIconField = $this->oModelReflection->GetIconSelectionField('icon');
+			$oIconField->ValueToDOMNode($oDOMNode, $value);
+		}
+		else
+		{
+			parent::PropertyToDOMNode($oDOMNode, $sProperty, $value);
+		}
+	}
+
 	static public function GetInfo()
 	{
 		return array(
@@ -803,30 +939,6 @@ class DashletHeaderStatic extends Dashlet
 			'description' => Dict::S('UI:DashletHeaderStatic:Description'),
 		);
 	}
-	
-	static public function FindIcons($sBaseDir, $sDir = '')
-	{
-		$aResult = array();
-		// Populate automatically the list of icon files
-		if ($hDir = @opendir($sBaseDir.'/'.$sDir))
-		{
-			while (($sFile = readdir($hDir)) !== false)
-			{
-				$aMatches = array();
-				if (($sFile != '.') && ($sFile != '..') && ($sFile != 'lifecycle') && is_dir($sBaseDir.'/'.$sDir.'/'.$sFile))
-				{
-					$sDirSubPath = ($sDir == '') ? $sFile : $sDir.'/'.$sFile;
-					$aResult = array_merge($aResult, self::FindIcons($sBaseDir, $sDirSubPath));
-				}
-				if (preg_match("/\.(png|jpg|jpeg|gif)$/i", $sFile, $aMatches)) // png, jp(e)g and gif are considered valid
-				{
-					$aResult[$sFile.'_'.$sDir] = $sDir.'/'.$sFile;
-				}
-			}
-			closedir($hDir);
-		}
-		return $aResult;
-	}
 }
 
 
@@ -836,28 +948,27 @@ class DashletHeaderDynamic extends Dashlet
 	{
 		parent::__construct($oModelReflection, $sId);
 		$this->aProperties['title'] = Dict::S('UI:DashletHeaderDynamic:Prop-Title:Default');
-		$sIcon = $this->oModelReflection->GetClassIcon('Contact', false);
-		$sIcon = str_replace(utils::GetAbsoluteUrlModulesRoot(), '', $sIcon);
-		$this->aProperties['icon'] = $sIcon;
+		$oIconSelect = $this->oModelReflection->GetIconSelectionField('icon');
+		$this->aProperties['icon'] = $oIconSelect->GetDefaultValue('Contact');
 		$this->aProperties['subtitle'] = Dict::S('UI:DashletHeaderDynamic:Prop-Subtitle:Default');
 		$this->aProperties['query'] = 'SELECT Contact';
 		$this->aProperties['group_by'] = 'status';
 		$this->aProperties['values'] = array('active', 'inactive');
 	}
-	
-	public function Render($oPage, $bEditMode = false, $aExtraParams = array())
+
+	protected function GetValues()
 	{
-		$sTitle = $this->aProperties['title'];
-		$sIcon = $this->aProperties['icon'];
-		$sSubtitle = $this->aProperties['subtitle'];
 		$sQuery = $this->aProperties['query'];
 		$sGroupBy = $this->aProperties['group_by'];
 		$aValues = $this->aProperties['values'];
 
-		$oFilter = DBObjectSearch::FromOQL($sQuery);
-		$sClass = $oFilter->GetClass();
+		if (empty($aValues))
+		{
+			$aValues = array();
+		}
 
-		$sIconPath = utils::GetAbsoluteUrlModulesRoot().$sIcon;
+		$oQuery = $this->oModelReflection->GetQuery($sQuery);
+		$sClass = $oQuery->GetClass();
 
 		if ($this->oModelReflection->IsValidAttCode($sClass, $sGroupBy))
 		{
@@ -870,6 +981,21 @@ class DashletHeaderDynamic extends Dashlet
 				}
 			}
 		}
+		return $aValues;
+	}
+
+	public function Render($oPage, $bEditMode = false, $aExtraParams = array())
+	{
+		$sTitle = $this->aProperties['title'];
+		$sIcon = $this->aProperties['icon'];
+		$sSubtitle = $this->aProperties['subtitle'];
+		$sQuery = $this->aProperties['query'];
+		$sGroupBy = $this->aProperties['group_by'];
+
+		$oIconSelect = $this->oModelReflection->GetIconSelectionField('icon');
+		$sIconPath = $oIconSelect->MakeFileUrl($sIcon);
+
+		$aValues = $this->GetValues();
 		if (count($aValues) > 0)
 		{
 			// Stats grouped by <group_by>
@@ -897,6 +1023,7 @@ class DashletHeaderDynamic extends Dashlet
 
 		$oPage->add('<img src="'.$sIconPath.'">');
 
+		$oFilter = DBObjectSearch::FromOQL($sQuery);
 		$oBlock = new DisplayBlock($oFilter, 'summary');
 		$sBlockId = 'block_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occuring in the same DOM)
 		$oBlock->Display($oPage, $sBlockId, $aExtraParams);
@@ -905,20 +1032,78 @@ class DashletHeaderDynamic extends Dashlet
 		$oPage->add('</div>');
 	}
 
+	public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = array())
+	{
+		$sTitle = $this->aProperties['title'];
+		$sIcon = $this->aProperties['icon'];
+		$sSubtitle = $this->aProperties['subtitle'];
+		$sQuery = $this->aProperties['query'];
+		$sGroupBy = $this->aProperties['group_by'];
+		$aValues = $this->aProperties['values'];
+
+		$oQuery = $this->oModelReflection->GetQuery($sQuery);
+		$sClass = $oQuery->GetClass();
+
+		$oIconSelect = $this->oModelReflection->GetIconSelectionField('icon');
+		$sIconPath = $oIconSelect->MakeFileUrl($sIcon);
+
+		$oPage->add('<div class="dashlet-content">');
+		$oPage->add('<div class="main_header">');
+
+		$oPage->add('<img src="'.$sIconPath.'">');
+
+		$sBlockId = 'block_fake_'.$this->sId.($bEditMode ? '_edit' : ''); // make a unique id (edition occuring in the same DOM)
+
+		$iTotal = 0;
+		$aValues = $this->GetValues();
+		if (count($aValues) > 0)
+		{
+			// Stats grouped by <group_by>
+		}
+		else
+		{
+			// Simple stats
+		}
+
+		$oPage->add('<div class="display_block" id="'.$sBlockId.'">');
+		$oPage->add('<div class="summary-details">');
+		$oPage->add('<table><tbody>');
+		$oPage->add('<tr>');
+		foreach ($aValues as $sValue)
+		{
+			$sValueLabel = $this->oModelReflection->GetValueLabel($sClass, $sGroupBy, $sValue);
+   		$oPage->add('	<th>'.$sValueLabel.'</th>');
+   	}
+		$oPage->add('</tr>');
+		$oPage->add('<tr>');
+		foreach ($aValues as $sValue)
+		{
+			$iCount = (int) rand(2, 100);
+			$iTotal += $iCount;
+			$oPage->add('	<td>'.$iCount.'</td>');
+		}
+		$oPage->add('</tr>');
+		$oPage->add('</tbody></table>');
+		$oPage->add('</div>');
+
+		$sTitle = $this->oModelReflection->DictString($sTitle);
+		$sSubtitle = $this->oModelReflection->DictFormat($sSubtitle, $iTotal);
+//		$sSubtitle = "original: $sSubtitle, S:".$this->oModelReflection->DictString($sSubtitle).", Format: '".$this->oModelReflection->DictFormat($sSubtitle, $iTotal)."'";
+
+		$oPage->add('<h1>'.$sTitle.'</h1>');
+		$oPage->add('<a href="" class="summary">'.$sSubtitle.'</a>');
+		$oPage->add('</div>');
+
+		$oPage->add('</div>');
+		$oPage->add('</div>');
+	}
+
 	public function GetPropertiesFields(DesignerForm $oForm)
 	{
 		$oField = new DesignerTextField('title', Dict::S('UI:DashletHeaderDynamic:Prop-Title'), $this->aProperties['title']);
 		$oForm->AddField($oField);
 
-		$oField = new DesignerIconSelectionField('icon', Dict::S('UI:DashletHeaderDynamic:Prop-Icon'), $this->aProperties['icon']);
-		$aAllIcons = DashletHeaderStatic::FindIcons(APPROOT.'env-'.utils::GetCurrentEnvironment());
-		ksort($aAllIcons);
-		$aValues = array();
-		foreach($aAllIcons as $sFilePath)
-		{
-			$aValues[] = array('value' => $sFilePath, 'label' => basename($sFilePath), 'icon' => utils::GetAbsoluteUrlModulesRoot().$sFilePath);
-		}
-		$oField->SetAllowedValues($aValues);
+		$oField = $this->oModelReflection->GetIconSelectionField('icon', Dict::S('UI:DashletHeaderDynamic:Prop-Icon'), $this->aProperties['icon']);
 		$oForm->AddField($oField);
 
 		$oField = new DesignerTextField('subtitle', Dict::S('UI:DashletHeaderDynamic:Prop-Subtitle'), $this->aProperties['subtitle']);
@@ -931,8 +1116,8 @@ class DashletHeaderDynamic extends Dashlet
 		try
 		{
 			// Group by field: build the list of possible values (attribute codes + ...)
-			$oSearch = DBObjectSearch::FromOQL($this->aProperties['query']);
-			$sClass = $oSearch->GetClass();
+			$oQuery = $this->oModelReflection->GetQuery($this->aProperties['query']);
+			$sClass = $oQuery->GetClass();
 			$aGroupBy = array();
 			foreach($this->oModelReflection->ListAttributes($sClass, 'AttributeEnum,AttributeFinalClass') as $sAttCode => $sAttType)
 			{
@@ -975,11 +1160,11 @@ class DashletHeaderDynamic extends Dashlet
 			try
 			{
 				$sCurrQuery = $aValues['query'];
-				$oCurrSearch = DBObjectSearch::FromOQL($sCurrQuery);
+				$oCurrSearch = $this->oModelReflection->GetQuery($sCurrQuery);
 				$sCurrClass = $oCurrSearch->GetClass();
 	
 				$sPrevQuery = $this->aProperties['query'];
-				$oPrevSearch = DBObjectSearch::FromOQL($sPrevQuery);
+				$oPrevSearch = $this->oModelReflection->GetQuery($sPrevQuery);
 				$sPrevClass = $oPrevSearch->GetClass();
 	
 				if ($sCurrClass != $sPrevClass)
@@ -1003,6 +1188,32 @@ class DashletHeaderDynamic extends Dashlet
 		return parent::Update($aValues, $aUpdatedFields);
 	}
 	
+	protected function PropertyFromDOMNode($oDOMNode, $sProperty)
+	{
+		if ($sProperty == 'icon')
+		{
+			$oIconField = $this->oModelReflection->GetIconSelectionField('icon');
+			return $oIconField->ValueFromDOMNode($oDOMNode);
+		}
+		else
+		{
+			return parent::PropertyFromDOMNode($oDOMNode, $sProperty);
+		}
+	}
+
+	protected function PropertyToDOMNode($oDOMNode, $sProperty, $value)
+	{
+		if ($sProperty == 'icon')
+		{
+			$oIconField = $this->oModelReflection->GetIconSelectionField('icon');
+			$oIconField->ValueToDOMNode($oDOMNode, $value);
+		}
+		else
+		{
+			parent::PropertyToDOMNode($oDOMNode, $sProperty, $value);
+		}
+	}
+
 	static public function GetInfo()
 	{
 		return array(
@@ -1023,7 +1234,7 @@ class DashletBadge extends Dashlet
 		$this->aCSSClasses[] = 'dashlet-inline';
 		$this->aCSSClasses[] = 'dashlet-badge';
 	}
-	
+
 	public function Render($oPage, $bEditMode = false, $aExtraParams = array())
 	{
 		$sClass = $this->aProperties['class'];
@@ -1039,52 +1250,63 @@ class DashletBadge extends Dashlet
 		$oBlock->Display($oPage, $sBlockId, $aExtraParams);
 
 		$oPage->add('</div>');
-		if ($bEditMode)
-		{
-			// Since the container div is not rendered the same way in edit mode, add the 'inline' style to it
-			$oPage->add_ready_script("$('#dashlet_".$this->sId."').addClass('dashlet-inline');");
-		}
 	}
 
-	public function GetPropertiesFields(DesignerForm $oForm)
+	public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = array())
 	{
+		$sClass = $this->aProperties['class'];
 
-		$oClassesSet = new ValueSetEnumClasses('bizmodel', array());
-		$aClasses = $oClassesSet->GetValues(array());
-		
-		$aLinkClasses = array();
-	
-		foreach($this->oModelReflection->GetClasses('bizmodel') as $sClass)
-		{	
-			foreach($this->oModelReflection->ListAttributes($sClass, 'AttributeLinkedSetIndirect') as $sAttCode => $sAttType)
-			{
-				$sLinkedClass = $this->oModelReflection->GetAttributeProperty($sClass, $sAttCode, 'linked_class');
-				if ($sLinkedClass != null)
-				{
-					$aLinkClasses[$sLinkedClass] = true;
-				}
-			}
-		}
-			
-		
-		$oField = new DesignerIconSelectionField('class', Dict::S('UI:DashletBadge:Prop-Class'), $this->aProperties['class']);
-		ksort($aClasses);
-		$aValues = array();
-		foreach($aClasses as $sClass => $sClass)
+		$sIconUrl = $this->oModelReflection->GetClassIcon($sClass, false);
+		$sClassLabel = $this->oModelReflection->GetName($sClass);
+
+		$oPage->add('<div class="dashlet-content">');
+
+		$oPage->add('<div id="block_fake_'.$this->sId.'" class="display_block">');
+		$oPage->add('<p>');
+		$oPage->add('   <a class="actions" href=""><img src="'.$sIconUrl.'" style="vertical-align:middle;float;left;margin-right:10px;border:0;">'.$sClassLabel.': 947</a>');
+		$oPage->add('</p>');
+		$oPage->add('<p>');
+		$oPage->add('   <a href="">'.Dict::Format('UI:ClickToCreateNew', $sClassLabel).'</a>');
+		$oPage->add('   <br/>');
+		$oPage->add('   <a href="http://localhost/trunk/pages/UI.php?operation=search_form&amp;class=Server&amp;c[menu]=WelcomeMenuPage">Search for Server objects</a>');
+		$oPage->add('</p>');
+		$oPage->add('</div>');
+
+		$oPage->add('</div>');
+	}
+
+	static protected $aClassList = null;
+
+	public function GetPropertiesFields(DesignerForm $oForm)
+	{
+		if (is_null(self::$aClassList))
 		{
-			if (!array_key_exists($sClass, $aLinkClasses))
+			// Cache the ordered list of classes (ordered on the label)
+			// (has a significant impact when editing a page with lots of badges)
+			//
+			$aClasses = array();
+			foreach($this->oModelReflection->GetClasses('bizmodel', true /*exclude links*/) as $sClass)
+			{	
+				$aClasses[$sClass] = $this->oModelReflection->GetName($sClass);
+			}
+			asort($aClasses);
+
+			self::$aClassList = array();
+			foreach($aClasses as $sClass => $sLabel)
 			{
 				$sIconUrl = $this->oModelReflection->GetClassIcon($sClass, false);
 				$sIconFilePath = str_replace(utils::GetAbsoluteUrlAppRoot(), APPROOT, $sIconUrl);
-				if (($sIconUrl == '') || !file_exists($sIconFilePath))
+				if ($sIconUrl == '')
 				{
-					// The icon does not exist, leet's use a transparent one of the same size.
+					// The icon does not exist, let's use a transparent one of the same size.
 					$sIconUrl = utils::GetAbsoluteUrlAppRoot().'images/transparent_32_32.png';
 				}
-				$aValues[] = array('value' => $sClass, 'label' => $this->oModelReflection->GetName($sClass), 'icon' => $sIconUrl);
+				self::$aClassList[] = array('value' => $sClass, 'label' => $sLabel, 'icon' => $sIconUrl);
 			}
 		}
-		$oField->SetAllowedValues($aValues);
+
+		$oField = new DesignerIconSelectionField('class', Dict::S('UI:DashletBadge:Prop-Class'), $this->aProperties['class']);
+		$oField->SetAllowedValues(self::$aClassList);
 		
 		$oForm->AddField($oField);
 	}

+ 73 - 1
application/forms.class.inc.php

@@ -828,19 +828,26 @@ class DesignerHiddenField extends DesignerFormField
 
 class DesignerIconSelectionField extends DesignerFormField
 {
+	protected $sUploadUrl;
 	protected $aAllowedValues;
 	
 	public function __construct($sCode, $sLabel = '', $defaultValue = '')
 	{
 		parent::__construct($sCode, $sLabel, $defaultValue);
 		$this->bAutoApply = true;
+		$this->sUploadUrl = null;
 	}
 	
 	public function SetAllowedValues($aAllowedValues)
 	{
 		$this->aAllowedValues = $aAllowedValues;
 	}
-	
+
+	public function EnableUpload($sIconUploadUrl)
+	{
+		$this->sUploadUrl = $sIconUploadUrl;
+	}
+
 	public function Render(WebPage $oP, $sFormId, $sRenderMode='dialog')
 	{
 		$sId = $this->oForm->GetFieldId($this->sCode);
@@ -868,6 +875,71 @@ EOF
 	}
 }
 
+class RunTimeIconSelectionField extends DesignerIconSelectionField
+{
+	public function __construct($sCode, $sLabel = '', $defaultValue = '')
+	{
+		parent::__construct($sCode, $sLabel, $defaultValue);
+
+		$aAllIcons = self::FindIconsOnDisk(APPROOT.'env-'.utils::GetCurrentEnvironment());
+		ksort($aAllIcons);
+		$aValues = array();
+		foreach($aAllIcons as $sFilePath)
+		{
+			$aValues[] = array('value' => $sFilePath, 'label' => basename($sFilePath), 'icon' => utils::GetAbsoluteUrlModulesRoot().$sFilePath);
+		}
+		$this->SetAllowedValues($aValues);
+	}
+
+	static protected function FindIconsOnDisk($sBaseDir, $sDir = '')
+	{
+		$aResult = array();
+		// Populate automatically the list of icon files
+		if ($hDir = @opendir($sBaseDir.'/'.$sDir))
+		{
+			while (($sFile = readdir($hDir)) !== false)
+			{
+				$aMatches = array();
+				if (($sFile != '.') && ($sFile != '..') && ($sFile != 'lifecycle') && is_dir($sBaseDir.'/'.$sDir.'/'.$sFile))
+				{
+					$sDirSubPath = ($sDir == '') ? $sFile : $sDir.'/'.$sFile;
+					$aResult = array_merge($aResult, self::FindIconsOnDisk($sBaseDir, $sDirSubPath));
+				}
+				if (preg_match("/\.(png|jpg|jpeg|gif)$/i", $sFile, $aMatches)) // png, jp(e)g and gif are considered valid
+				{
+					$aResult[$sFile.'_'.$sDir] = $sDir.'/'.$sFile;
+				}
+			}
+			closedir($hDir);
+		}
+		return $aResult;
+	}
+
+	public function ValueFromDOMNode($oDOMNode)
+	{
+		return $oDOMNode->textContent;
+	}
+
+	public function ValueToDOMNode($oDOMNode, $value)
+	{
+		$oTextNode = $oDOMNode->ownerDocument->createTextNode($value);
+		$oDOMNode->appendChild($oTextNode);
+	}
+
+	public function MakeFileUrl($value)
+	{
+		return utils::GetAbsoluteUrlModulesRoot().$value;
+	}
+
+	public function GetDefaultValue($sClass = 'Contact')
+	{
+		$sIconPath = MetaModel::GetClassIcon($sClass, false);
+		$sIcon = str_replace(utils::GetAbsoluteUrlModulesRoot(), '', $sIconPath);
+		return $sIcon;	
+	}
+}
+
+
 class DesignerSortableField extends DesignerFormField
 {
 	protected $aAllowedValues;

+ 115 - 19
core/modelreflection.class.inc.php

@@ -24,25 +24,59 @@
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  
-interface ModelReflection
+abstract class ModelReflection
 {
-	public function GetClassIcon($sClass, $bImgTag = true); 
-	public function IsValidAttCode($sClass, $sAttCode);
-	public function GetName($sClass);
-	public function GetLabel($sClass, $sAttCodeEx);
-	public function ListAttributes($sClass, $sScope = null);
-	public function GetAttributeProperty($sClass, $sAttCode, $sPropName, $default = null);
-	public function GetAllowedValues_att($sClass, $sAttCode);
-	public function HasChildrenClasses($sClass);
-	public function GetClasses($sCategories = '');
-	public function IsValidClass($sClass);
-	public function IsSameFamilyBranch($sClassA, $sClassB);
-	public function GetParentClass($sClass);
-	public function GetFiltersList($sClass);
-	public function IsValidFilterCode($sClass, $sFilterCode);
+	abstract public function GetClassIcon($sClass, $bImgTag = true); 
+	abstract public function IsValidAttCode($sClass, $sAttCode);
+	abstract public function GetName($sClass);
+	abstract public function GetLabel($sClass, $sAttCodeEx);
+	abstract public function GetValueLabel($sClass, $sAttCode, $sValue);
+	abstract public function ListAttributes($sClass, $sScope = null);
+	abstract public function GetAttributeProperty($sClass, $sAttCode, $sPropName, $default = null);
+	abstract public function GetAllowedValues_att($sClass, $sAttCode);
+	abstract public function HasChildrenClasses($sClass);
+	abstract public function GetClasses($sCategories = '', $bExcludeLinks = false);
+	abstract public function IsValidClass($sClass);
+	abstract public function IsSameFamilyBranch($sClassA, $sClassB);
+	abstract public function GetParentClass($sClass);
+	abstract public function GetFiltersList($sClass);
+	abstract public function IsValidFilterCode($sClass, $sFilterCode);
+
+	abstract public function GetQuery($sOQL);
+
+	abstract public function DictString($sStringCode, $sDefault = null, $bUserLanguageOnly = false);
+
+	public function DictFormat($sFormatCode /*, ... arguments ....*/)
+	{
+		$sLocalizedFormat = $this->DictString($sFormatCode);
+		$aArguments = func_get_args();
+		array_shift($aArguments);
+		
+		if ($sLocalizedFormat == $sFormatCode)
+		{
+			// Make sure the information will be displayed (ex: an error occuring before the dictionary gets loaded)
+			return $sFormatCode.' - '.implode(', ', $aArguments);
+		}
+
+		return vsprintf($sLocalizedFormat, $aArguments);
+	}
+
+	abstract public function GetIconSelectionField($sCode, $sLabel = '', $defaultValue = '');
 }
 
-class ModelReflectionRuntime implements ModelReflection
+abstract class QueryReflection
+{
+	/**
+	 * Throws an exception in case of an invalid syntax
+	 */
+	abstract public function __construct($sOQL);
+
+	abstract public function GetClass();
+	abstract public function GetClassAlias();
+}
+
+
+class ModelReflectionRuntime extends ModelReflection
 {
 	public function __construct()
 	{
@@ -68,6 +102,12 @@ class ModelReflectionRuntime implements ModelReflection
 		return MetaModel::GetLabel($sClass, $sAttCodeEx);
 	}
  
+	public function GetValueLabel($sClass, $sAttCode, $sValue)
+	{
+		$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+		return $oAttDef->GetValueLabel($sValue);
+	}
+
 	public function ListAttributes($sClass, $sScope = null)
 	{
 		$aScope = null;
@@ -133,9 +173,26 @@ class ModelReflectionRuntime implements ModelReflection
 		return MetaModel::HasChildrenClasses($sClass);
 	}
  
-	public function GetClasses($sCategories = '')
+	public function GetClasses($sCategories = '', $bExcludeLinks = false)
 	{
-		return MetaModel::GetClasses($sCategories);
+		$aClasses = MetaModel::GetClasses($sCategories);
+		if ($bExcludeLinks)
+		{
+			$aExcluded = ProfilesConfig::GetLinkClasses(); // table computed at compile time
+			$aRes = array();
+			foreach ($aClasses as $sClass)
+			{
+				if (!array_key_exists($sClass, $aExcluded))
+				{
+					$aRes[] = $sClass;
+				}
+			}
+		}
+		else
+		{
+			$aRes = $aClasses;
+		}
+		return $aRes;
 	}
 
 	public function IsValidClass($sClass)
@@ -162,4 +219,43 @@ class ModelReflectionRuntime implements ModelReflection
 	{
 		return MetaModel::IsValidFilterCode($sClass, $sFilterCode);
 	}
-}
+
+	public function GetQuery($sOQL)
+	{
+		return new QueryReflectionRuntime($sOQL);
+	}
+
+	public function DictString($sStringCode, $sDefault = null, $bUserLanguageOnly = false)
+	{
+		return Dict::S($sStringCode, $sDefault, $bUserLanguageOnly);
+	}
+
+	public function GetIconSelectionField($sCode, $sLabel = '', $defaultValue = '')
+	{
+		return new RunTimeIconSelectionField($sCode, $sLabel, $defaultValue);
+	}
+}
+
+
+class QueryReflectionRuntime extends QueryReflection
+{
+	protected $oFilter;
+
+	/**
+	 *	throws an exception in case of a wrong syntax
+	 */
+	public function __construct($sOQL)
+	{
+		$this->oFilter = DBObjectSearch::FromOQL($sOQL);
+	}
+
+	public function GetClass()
+	{
+		return $this->oFilter->GetClass();
+	}
+
+	public function GetClassAlias()
+	{
+		return $this->oFilter->GetClassAlias();
+	}
+}

+ 185 - 86
js/dashboard.js

@@ -1,4 +1,9 @@
 // jQuery UI style "widget" for editing an iTop "dashboard"
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// dashboard
+//
 $(function()
 {
 	// the widget definition, where "itop" is the namespace,
@@ -25,23 +30,18 @@ $(function()
 
 			this.element
 			.addClass('itop-dashboard')
-			.bind('mark_as_modified.itop-dashboard', function(){me.mark_as_modified();} );
+			.bind('add_dashlet.itop_dashboard', function(event, oParams){
+				me.add_dashlet(oParams);
+			});
 
-			this.ajax_div = $('<div></div>').appendTo(this.element);
+			this.ajax_div = $('<div></div>');
+			this.element.after(this.ajax_div);
 			this._make_draggable();
-			this.bModified = false;
-			
 		},
 	
 		// called when created, and later when changing options
 		_refresh: function()
 		{
-			var oParams = this._get_state(this.options.render_parameters);
-			var me = this;
-			$.post(this.options.render_to, oParams, function(data){
-				me.element.html(data);
-				me._make_draggable();
-			});
 		},
 		// events bound via _bind are removed automatically
 		// revert other modifications here
@@ -58,7 +58,6 @@ $(function()
 		{
 			// in 1.9 would use _superApply
 			this._superApply(arguments);
-			this._refresh();
 		},
 		// _setOption is called for each individual option that is changing
 		_setOption: function( key, value )
@@ -96,6 +95,170 @@ $(function()
 			
 			return oState;
 		},
+		_get_new_id: function()
+		{
+			var iMaxId = 0;
+			this.element.find(':itop-dashlet').each(function() {
+				var oDashlet = $(this).data('itopDashlet');
+				if(oDashlet)
+				{
+					var oDashletParams = oDashlet.get_params();
+					var id = parseInt(oDashletParams.dashlet_id, 10);
+					if (id > iMaxId) iMaxId = id;
+				}
+			});
+			return 1 + iMaxId;			
+		},
+		_make_draggable: function()
+		{
+			var me = this;
+			this.element.find('.dashlet').draggable({
+				revert: 'invalid', appendTo: 'body', zIndex: 9999,
+				helper: function() {
+					var oDragItem = $(this).dashlet('get_drag_icon');
+					return oDragItem;
+				},
+				cursorAt: { top: 16, left: 16 }
+			});
+			this.element.find('table td').droppable({
+				accept: '.dashlet,.dashlet_icon',
+				drop: function(event, ui) {
+					$( this ).find( ".placeholder" ).remove();
+					var bRefresh = $(this).hasClass('layout_extension');
+					var oDropped = ui.draggable;
+					if (oDropped.hasClass('dashlet'))
+					{
+						// moving around a dashlet
+						oDropped.detach();
+						oDropped.css({top: 0, left: 0});
+						oDropped.appendTo($(this));
+
+						var oDashlet = ui.draggable.data('itopDashlet');
+						me.on_dashlet_moved(oDashlet, $(this), bRefresh);
+					}
+					else
+					{
+						// inserting a new dashlet
+						var sDashletClass = ui.draggable.attr('dashlet_class');
+						$('.itop-dashboard').trigger('add_dashlet', {dashlet_class: sDashletClass, container: $(this), refresh: bRefresh });
+					}
+				}
+			});	
+		},
+		add_dashlet: function(options)
+		{
+			// 1) Create empty divs for the dashlet and its properties
+			//
+			var sDashletId = this._get_new_id();
+			var oDashlet = $('<div class="dashlet" id="dashlet_'+sDashletId+'"/>');
+			oDashlet.appendTo(options.container);
+			var oDashletProperties = $('<div class="dashlet_properties" id="dashlet_properties_'+sDashletId+'"/>');
+			oDashletProperties.appendTo($('#dashlet_properties'));
+
+			// 2) Ajax call to fill the divs with default values
+			//    => in return, it must call add_dashlet_finalize
+			//
+			this.add_dashlet_ajax(options, sDashletId);
+		},
+		add_dashlet_finalize: function(options, sDashletId, sDashletClass)
+		{
+			$('#dashlet_'+sDashletId)
+			.dashlet({dashlet_id: sDashletId, dashlet_class: sDashletClass})
+			.dashlet('deselect_all')
+			.dashlet('select')
+			.draggable({
+				revert: 'invalid', appendTo: 'body', zIndex: 9999,
+				helper: function() {
+					var oDragItem = $(this).dashlet('get_drag_icon');
+					return oDragItem;
+				},
+				cursorAt: { top: 16, left: 16 }
+			});
+			if (options.refresh)
+			{
+				this._refresh();
+			}
+		},
+		on_dashlet_moved: function(oDashlet, oReceiver, bRefresh)
+		{
+			if (bRefresh)
+			{
+				// The layout was extended... refresh the whole dashboard
+				this._refresh();
+			}
+		}
+	});	
+});
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// runtimedashboard (extends dashboard)
+//
+$(function()
+{
+	// the widget definition, where "itop" is the namespace,
+	// "dashboard" the widget name
+	$.widget( "itop.runtimedashboard", $.itop.dashboard,
+	{
+		// default options
+		options:
+		{
+			dashboard_id: '',
+			layout_class: '',
+			title: '',
+			submit_to: 'index.php',
+			submit_parameters: {},
+			render_to: 'index.php',
+			render_parameters: {},
+			new_dashlet_parameters: {}
+		},
+	
+		// the constructor
+		_create: function()
+		{
+			var me = this; 
+
+			this._superApply(arguments);
+
+			this.element
+			.addClass('itop-runtimedashboard')
+			.bind('mark_as_modified.itop-dashboard', function(){me.mark_as_modified();} );
+
+			this.bModified = false;
+		},
+	
+		// events bound via _bind are removed automatically
+		// revert other modifications here
+		_destroy: function()
+		{
+			this.element
+			.removeClass('itop-runtimedashboard');
+
+			this._superApply(arguments);
+		},
+		// _setOptions is called with a hash of all options that are changing
+		_setOptions: function()
+		{
+			this._superApply(arguments);
+			this._refresh();
+		},
+		// _setOption is called for each individual option that is changing
+		_setOption: function( key, value )
+		{
+			this._superApply(arguments);
+		},
+		// called when created, and later when changing options
+		_refresh: function()
+		{
+			this._super();
+
+			var oParams = this._get_state(this.options.render_parameters);
+			var me = this;
+			$.post(this.options.render_to, oParams, function(data){
+				me.element.html(data);
+				me._make_draggable();
+			});
+		},
 		// Modified means: at least one change has been applied
 		mark_as_modified: function()
 		{
@@ -130,14 +293,8 @@ $(function()
 				me.ajax_div.html(data);
 			});
 		},
-		add_dashlet: function(options)
+		add_dashlet_ajax: function(options, sDashletId)
 		{
-			var sDashletId = this._get_new_id();
-			var oDashlet = $('<div class="dashlet" id="dashlet_'+sDashletId+'"/>');
-			oDashlet.appendTo(options.container);
-			var oDashletProperties = $('<div class="dashlet_properties" id="dashlet_properties_'+sDashletId+'"/>');
-			oDashletProperties.appendTo($('#dashlet_properties'));
-			
 			var oParams = this.options.new_dashlet_parameters;
 			var sDashletClass = options.dashlet_class;
 			oParams.dashlet_class = sDashletClass;
@@ -145,79 +302,17 @@ $(function()
 			var me = this;
 			$.post(this.options.render_to, oParams, function(data){
 				me.ajax_div.html(data);
-				$('#dashlet_'+sDashletId)
-				.dashlet({dashlet_id: sDashletId, dashlet_class: sDashletClass})
-				.dashlet('deselect_all')
-				.dashlet('select')
-				.draggable({
-					revert: 'invalid', appendTo: 'body', zIndex: 9999,
-					helper: function() {
-						var oDragItem = $(this).dashlet('get_drag_icon');
-						return oDragItem;
-					},
-					cursorAt: { top: 16, left: 16 }
-				});
-				if (options.refresh)
-				{
-					me._refresh();
-				}
+				me.add_dashlet_finalize(options, sDashletId, sDashletClass);
 			});
-		},
-		_get_new_id: function()
-		{
-			var iMaxId = 0;
-			this.element.find(':itop-dashlet').each(function() {
-				var oDashlet = $(this).data('itopDashlet');
-				if(oDashlet)
-				{
-					var oDashletParams = oDashlet.get_params();
-					var id = parseInt(oDashletParams.dashlet_id, 10);
-					if (id > iMaxId) iMaxId = id;
-				}
-			});
-			return 1 + iMaxId;			
-		},
-		_make_draggable: function()
-		{
-			var me = this;
-			this.element.find('.dashlet').draggable({
-				revert: 'invalid', appendTo: 'body', zIndex: 9999,
-				helper: function() {
-					var oDragItem = $(this).dashlet('get_drag_icon');
-					return oDragItem;
-				},
-				cursorAt: { top: 16, left: 16 }
-			});
-			this.element.find('table td').droppable({
-				accept: '.dashlet,.dashlet_icon',
-				drop: function(event, ui) {
-					$( this ).find( ".placeholder" ).remove();
-					var bRefresh = $(this).hasClass('layout_extension');
-					var oDashlet = ui.draggable;
-					if (oDashlet.hasClass('dashlet'))
-					{
-						// moving around a dashlet
-						oDashlet.detach();
-						oDashlet.css({top: 0, left: 0});
-						oDashlet.appendTo($(this));
-						if( bRefresh )
-						{
-							// The layout was extended... refresh the whole dashboard
-							me._refresh();
-						}
-					}
-					else
-					{
-						// inserting a new dashlet
-						var sDashletClass = ui.draggable.attr('dashlet_class');
-						$(':itop-dashboard').dashboard('add_dashlet', {dashlet_class: sDashletClass, container: $(this), refresh: bRefresh });
-					}
-				}
-			});	
 		}
 	});	
 });
 
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// Helper to upload the file selected in the "import dashboard" dialog
+//
 function UploadDashboard(oOptions)
 {
 	var sFileId = 'dashboard_upload_file';
@@ -229,6 +324,10 @@ function UploadDashboard(oOptions)
 }
 
 
+////////////////////////////////////////////////////////////////////////////////
+//
+// dashboard_upload_dlg
+//
 //jQuery UI style "widget" for managing a "import dashboard" dialog (file upload)
 $(function()
 {

+ 6 - 1
js/dashlet.js

@@ -123,8 +123,13 @@ $(function()
 		},
 		_remove_dashlet: function()
 		{
-			$('#dashlet_properties_'+this.options.dashlet_id).remove();
+			var iDashletId = this.options.dashlet_id;
+			var sDashletClass = this.options.dashlet_class;
+			var oContainer = this.element.parent();
+
+			$('#dashlet_properties_'+iDashletId).remove();
 			this.element.remove();
+			$('#event_bus').trigger('dashlet-removed', {'dashlet_id': iDashletId, 'dashlet_class': sDashletClass, 'container': oContainer});
 		}
 	});	
 });

+ 1 - 1
js/property_field.js

@@ -187,7 +187,7 @@ $(function()
 			});
 			this.element.closest('form').find('.itop-property-field').each(function()
 			{
-				var oWidget = $(this).data('property_field');
+				var oWidget = $(this).data('itopProperty_field');
 				if (oWidget && oWidget._is_visible())
 				{
 					var oVal = oWidget._get_committed_value();

+ 3 - 4
pages/ajax.render.php

@@ -745,12 +745,11 @@ try
 			$sHtml = addslashes($oPage->end_capture($offset));
 			$sHtml = str_replace("\n", '', $sHtml);
 			$sHtml = str_replace("\r", '', $sHtml);
-			
-			$oPage->add_script("$('#dashlet_$sDashletId').html('$sHtml')"); // in ajax web page add_script has the same effect as add_ready_script
+			$oPage->add_script("$('#dashlet_$sDashletId').html('$sHtml');"); // in ajax web page add_script has the same effect as add_ready_script
 																			// but is executed BEFORE all 'ready_scripts'
 			$oForm = $oDashlet->GetForm(); // Rebuild the form since the values/content changed
 			$oForm->SetSubmitParams(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php', array('operation' => 'update_dashlet_property'));
-			$sHtml = addslashes($oForm->RenderAsPropertySheet($oPage, true /* bReturnHtml */, ':itop-dashboard'));
+			$sHtml = addslashes($oForm->RenderAsPropertySheet($oPage, true /* bReturnHtml */, '.itop-dashboard'));
 			$sHtml = str_replace("\n", '', $sHtml);
 			$sHtml = str_replace("\r", '', $sHtml);
 			$oPage->add_script("$('#dashlet_properties_$sDashletId').html('$sHtml')"); // in ajax web page add_script has the same effect as add_ready_script																	   // but is executed BEFORE all 'ready_scripts'
@@ -803,7 +802,7 @@ try
 			{
 				$oForm = $oDashlet->GetForm(); // Rebuild the form since the values/content changed
 				$oForm->SetSubmitParams(utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php', array('operation' => 'update_dashlet_property'));
-				$sHtml = addslashes($oForm->RenderAsPropertySheet($oPage, true /* bReturnHtml */, ':itop-dashboard'));
+				$sHtml = addslashes($oForm->RenderAsPropertySheet($oPage, true /* bReturnHtml */, '.itop-dashboard'));
 				$sHtml = str_replace("\n", '', $sHtml);
 				$sHtml = str_replace("\r", '', $sHtml);
 				$oPage->add_script("$('#dashlet_properties_$sDashletId').html('$sHtml')"); // in ajax web page add_script has the same effect as add_ready_script																	   // but is executed BEFORE all 'ready_scripts'

+ 46 - 32
setup/compiler.class.inc.php

@@ -567,42 +567,13 @@ EOF;
 			$aClassParams['display_template'] = "utils::GetAbsoluteUrlModulesRoot().'$sDisplayTemplate'";
 		}
 	
+		$this->CompileFiles($oProperties, $sTargetDir.'/'.$sModuleRelativeDir, '');
 		if (($sIcon = $oProperties->GetChildText('icon')) && (strlen($sIcon) > 0))
 		{
 			$sIcon = $sModuleRelativeDir.'/'.$sIcon;
 			$aClassParams['icon'] = "utils::GetAbsoluteUrlModulesRoot().'$sIcon'";
 		}
-		else // si <fileref ref="nnn">
-		{
-			$oIcon = $oProperties->GetOptionalElement('icon');
-			if ($oIcon)
-			{
-				$oFileRef = $oIcon->GetOptionalElement('fileref');
-				if ($oFileRef)
-				{
-					$iFileId = $oFileRef->getAttribute('ref');
-					$sXPath = "/itop_design/files/file[@id='$iFileId']";
-					$oNodes = $this->oFactory->GetNodes($sXPath);
-					if ($oNodes->length == 0)
-					{
-						throw new DOMFormatException('Could not find the file with ref '.$iFileId);
-					}
 
-					$sName = $oNodes->item(0)->GetChildText('name');
-					$sData = base64_decode($oNodes->item(0)->GetChildText('data'));
-					$aPathInfo = pathinfo($sName);
-					$sFile = 'icon-file'.$iFileId.'.'.$aPathInfo['extension'];
-					$sFilePath = $sTargetDir.'/'.$sModuleRelativeDir.'/'.$sFile;
-					file_put_contents($sFilePath, $sData);
-					if (!file_exists($sFilePath))
-					{
-						throw new Exception('Could not write icon file '.$sFilePath);
-					}
-					$aClassParams['icon'] = "utils::GetAbsoluteUrlModulesRoot().'$sModuleRelativeDir/$sFile'";
-				}
-			}
-		}
-	
 		$oOrder = $oProperties->GetOptionalElement('order');
 		if ($oOrder)
 		{
@@ -1028,6 +999,8 @@ EOF;
 
 	protected function CompileMenu($oMenu, $sTargetDir, $sModuleRelativeDir, $oP)
 	{
+		$this->CompileFiles($oMenu, $sTargetDir.'/'.$sModuleRelativeDir, $sModuleRelativeDir);
+
 		$sMenuId = $oMenu->getAttribute("id");
 		$sMenuClass = $oMenu->getAttribute("xsi:type");
 
@@ -1065,11 +1038,11 @@ EOF;
 				}
 				$sFileName = strtolower(str_replace(array(':', '/', '\\', '*'), '_', $sMenuId)).'_dashboard_menu.xml';
 				$sTemplateSpec = $this->PathToPHP($sFileName, $sModuleRelativeDir);
-				
+
 				$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);
@@ -1325,6 +1298,11 @@ class ProfilesConfig
 
 	protected static \$aLINKTOCLASSES = $sLinkToClasses;
 
+	public static function GetLinkClasses()
+	{
+		return self::\$aLINKTOCLASSES;
+	}
+
 	public static function GetProfileActionGrant(\$iProfileId, \$sClass, \$sAction)
 	{
 		// Search for a grant, starting from the most explicit declaration,
@@ -1462,6 +1440,42 @@ EOF;
 		$sDictFile = $sTargetDir.'/dictionaries/'.$sSafeLang.'.dict.php';
 		file_put_contents($sDictFile, $sPHPDict);
 	}
+
+	// Transform the file references into the corresponding filename (and create the file in the relevant directory)
+	//
+	protected function CompileFiles($oNode, $sTargetDir, $sRelativePath)
+	{
+		$oFileRefs = $oNode->GetNodes(".//fileref");
+		foreach ($oFileRefs as $oFileRef)
+		{
+			$iFileId = $oFileRef->getAttribute('ref');
+			if ($iFileId > 0)
+			{
+				$oNodes = $this->oFactory->GetNodes("/itop_design/files/file[@id='$iFileId']");
+				if ($oNodes->length == 0)
+				{
+					throw new DOMFormatException('Could not find the file with ref '.$iFileId);
+				}
+	
+				$sName = $oNodes->item(0)->GetChildText('name');
+				$sData = base64_decode($oNodes->item(0)->GetChildText('data'));
+				$aPathInfo = pathinfo($sName);
+				$sFile = 'icon-file'.$iFileId.'.'.$aPathInfo['extension'];
+				$sFilePath = $sTargetDir.'/images/'.$sFile;
+				@mkdir($sTargetDir.'/images');
+				file_put_contents($sFilePath, $sData);
+				if (!file_exists($sFilePath))
+				{
+					throw new Exception('Could not write icon file '.$sFilePath);
+				}
+				$oParentNode = $oFileRef->parentNode;
+				$oParentNode->removeChild($oFileRef);
+				
+				$oTextNode = $oParentNode->ownerDocument->createTextNode($sRelativePath.'/images/'.$sFile);
+				$oParentNode->appendChild($oTextNode);
+			}
+		}
+	}
 }
 
 ?>

+ 60 - 2
setup/modelfactory.class.inc.php

@@ -1441,11 +1441,25 @@ class MFElement extends DOMElement
 	}
 
 	/**
+	 * Extracts some nodes from the DOM (active nodes only !!!)
+	 * @param string $sXPath A XPath expression
+	 * @return DOMNodeList
+	 */
+	public function GetNodeById($sXPath, $sId)
+	{
+		return $this->ownerDocument->GetNodeById($sXPath, $sId, $this);
+	}
+
+	/**
 	 * For debugging purposes - but this is currently buggy: the whole document is rendered
 	 */
 	public function Dump($bReturnRes = false)
 	{
-		$sXml = $this->ownerDocument->saveXML($this);
+		$oMFDoc = new MFDocument();
+		$oClone = $oMFDoc->importNode($this->cloneNode(true), true);
+		$oMFDoc->appendChild($oClone);
+
+		$sXml = $oMFDoc->saveXML($oClone);
 		if ($bReturnRes)
 		{
 			return $sXml;
@@ -1713,6 +1727,9 @@ class MFElement extends DOMElement
 	 */	
 	public function AddChildNode(MFElement $oNode)
 	{
+		// First: cleanup any flag behind the new node
+		$oNode->ApplyChanges();
+	
 		$oExisting = $this->FindExistingChildNode($oNode);
 		if ($oExisting)
 		{
@@ -1740,6 +1757,9 @@ class MFElement extends DOMElement
 	 */	
 	public function RedefineChildNode(MFElement $oNode, $sSearchId = null)
 	{
+		// First: cleanup any flag behind the new node
+		$oNode->ApplyChanges();
+
 		$oExisting = $this->FindExistingChildNode($oNode, $sSearchId);
 		if (!$oExisting)
 		{
@@ -1906,7 +1926,45 @@ class MFElement extends DOMElement
 			$aCurrentRules = $oRulesNode->GetNodeAsArrayOfItems();
 		}
 		return $aCurrentRules;
-	 }	 	
+	 }
+
+	/**
+	 * List changes below a given node (see also MFDocument::ListChanges)	
+	 */	
+	public function ListChanges()
+	{
+		// Note: omitting the dot will make the query be global to the whole document!!!
+		return $this->GetNodes('.//*[@_alteration or @_old_id]');
+	}
+
+	/**
+	 * List changes below a given node (see also MFDocument::ApplyChanges)	
+	 */	
+	public function ApplyChanges()
+	{
+		$oNodes = $this->ListChanges();
+		foreach($oNodes as $oNode)
+		{
+			$sOperation = $oNode->GetAttribute('_alteration');
+			switch($sOperation)
+			{
+				case 'added':
+				case 'replaced':
+				// marked as added or modified, just reset the flag
+				$oNode->removeAttribute('_alteration');
+				break;
+				
+				case 'removed':
+				// marked as deleted, let's remove the node from the tree
+				$oNode->parentNode->removeChild($oNode);
+				break;
+			}
+			if ($oNode->hasAttribute('_old_id'))
+			{
+				$oNode->removeAttribute('_old_id');
+			}
+		}
+	}
 }
 
 /**