Преглед изворни кода

- New user interface to manage n-n links

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@104 a333f486-631f-4898-b8df-5754b55c2be0
dflaven пре 16 година
родитељ
комит
791cdb2a1c

+ 4 - 0
application/application.inc.php

@@ -9,4 +9,8 @@ require_once('../application/audit.category.class.inc.php');
 require_once('../application/audit.rule.class.inc.php');
 //require_once('../application/menunode.class.inc.php');
 require_once('../application/utils.inc.php');
+
+class ApplicationException extends CoreException
+{
+}
 ?>

+ 59 - 14
application/cmdbabstract.class.inc.php

@@ -51,6 +51,12 @@ abstract class cmdbAbstractObject extends CMDBObject
 		$sHint = htmlentities("$sObjClass::$sObjKey");
 		return "<a href=\"$sPage?operation=details&class=$sObjClass&id=$sObjKey&".$oAppContext->GetForLink()."\" title=\"$sHint\">$sLabel</a>";
 	}
+	
+	public function GetHyperlink()
+	{
+		$aAvailableFields[MetaModel::GetNameAttributeCode(get_class($this))] = $this->GetName();
+		return $this->MakeHyperLink(get_class($this), $this->GetKey(), $aAvailableFields);
+	}
 
 	public function GetDisplayValue($sAttCode)
 	{
@@ -213,13 +219,37 @@ abstract class cmdbAbstractObject extends CMDBObject
 	
 	// Comment by Rom: this helper may be used to display objects of class DBObject
 	//                 -> I am using this to display the changes history 
-	public static function DisplaySet(web_page $oPage, CMDBObjectSet $oSet, $sLinkageAttribute = '', $bDisplayMenu = true)
+	public static function DisplaySet(web_page $oPage, CMDBObjectSet $oSet, $sLinkageAttribute = '', $bDisplayMenu = true, $bSelectMode = false, $iObjectId = 0)
 	{
-		$oPage->add(self::GetDisplaySet($oPage, $oSet, $sLinkageAttribute, $bDisplayMenu));
+		$oPage->add(self::GetDisplaySet($oPage, $oSet, array( 'link_attr' => $sLinkageAttribute, 'object_id' => $iObjectId, 'menu' => $bDisplayMenu, 'selection_mode' => $bSelectMode)));
 	}
 	
-	public static function GetDisplaySet(web_page $oPage, CMDBObjectSet $oSet, $sLinkageAttribute = '', $bDisplayMenu = true)
+	//public static function GetDisplaySet(web_page $oPage, CMDBObjectSet $oSet, $sLinkageAttribute = '', $bDisplayMenu = true, $bSelectMode = false)
+	public static function GetDisplaySet(web_page $oPage, CMDBObjectSet $oSet, $aExtraParams = array())
 	{
+		static $iListId = 0;
+		$iListId++;
+		
+		// Initialize and check the parameters
+		$sLinkageAttribute = isset($aExtraParams['link_attr']) ? $aExtraParams['link_attr'] : '';
+		$iLinkedObjectId = isset($aExtraParams['object_id']) ? $aExtraParams['object_id'] : 0;
+		$sTargetAttr = isset($aExtraParams['target_attr']) ? $aExtraParams['target_attr'] : '';
+		if (!empty($sLinkageAttribute))
+		{
+			if($iLinkedObjectId == 0)
+			{
+				// if 'links' mode is requested the d of the object to link to must be specified
+				throw new ApplicationException("Parameter object_id is mandatory when link_attr is specified. Check the definition of the display template.");
+			}
+			if($sTargetAttr == '')
+			{
+				// if 'links' mode is requested the d of the object to link to must be specified
+				throw new ApplicationException("Parameter target_attr is mandatory when link_attr is specified. Check the definition of the display template.");
+			}
+		}
+		$bDisplayMenu = isset($aExtraParams['menu']) ? $aExtraParams['menu'] == true : true;
+		$bSelectMode = isset($aExtraParams['selection_mode']) ? $aExtraParams['selection_mode'] == true : false;
+		
 		$sHtml = '';
 		$oAppContext = new ApplicationContext();
 		$sClassName = $oSet->GetFilter()->GetClass();
@@ -260,6 +290,10 @@ abstract class cmdbAbstractObject extends CMDBObject
 		}
 		foreach($aList as $sAttCode)
 		{
+			if ($bSelectMode)
+			{
+				$aAttribs['form::select'] = array('label' => "<input type=\"checkbox\" onChange=\"var value = this.checked; $('.selectList{$iListId}').each( function() { this.checked = value; } );\"></input>", 'description' => 'Select / Deselect All');
+			}
 			$aAttribs['key'] = array('label' => '', 'description' => 'Click to display');
 			$aAttribs[$sAttCode] = array('label' => MetaModel::GetLabel($sClassName, $sAttCode), 'description' => MetaModel::GetDescription($sClassName, $sAttCode));
 		}
@@ -268,6 +302,11 @@ abstract class cmdbAbstractObject extends CMDBObject
 		while ($oObj = $oSet->Fetch())
 		{
 			$aRow['key'] = $oObj->GetKey();
+			if ($bSelectMode)
+			{
+				$aRow['form::select'] = "<input type=\"checkBox\" class=\"selectList{$iListId}\" name=\"selectObject[]\" value=\"".$oObj->GetKey()."\"></input>";
+			}
+			$aRow['key'] = $oObj->GetKey();
 			foreach($aList as $sAttCode)
 			{
 				$aRow[$sAttCode] = $oObj->GetAsHTML($sAttCode);
@@ -280,8 +319,14 @@ abstract class cmdbAbstractObject extends CMDBObject
 		if ($bDisplayMenu)
 		{
 			$sColspan = 'colspan="2"';
+			$aMenuExtraParams = array();
+			if (!empty($sLinkageAttribute))
+			{
+				//$aMenuExtraParams['linkage'] = $sLinkageAttribute;
+				$aMenuExtraParams = $aExtraParams;
+			}
 			$sHtml .= '<tr class="containerHeader"><td>&nbsp;'.$oSet->Count().' object(s)</td><td>';
-			$sHtml .= $oMenuBlock->GetRenderContent($oPage, $sLinkageAttribute);
+			$sHtml .= $oMenuBlock->GetRenderContent($oPage, $aMenuExtraParams);
 			$sHtml .= '</td></tr>';
 		}
 		$sHtml .= "<tr><td $sColspan>";
@@ -400,7 +445,7 @@ abstract class cmdbAbstractObject extends CMDBObject
 		$sHtml .= "<div id=\"SimpleSearchForm{$iSearchFormId}\" class=\"mini_tab{$iSearchFormId}\">\n";
 		$sHtml .= "<h1>Search for ".MetaModel::GetName($sClassName)." Objects</h1>\n";
 		$oUnlimitedFilter = new DBObjectSearch($sClassName);
-		$sHtml .= "<form>\n";
+		$sHtml .= "<form id=\"form{$iSearchFormId}\">\n";
 		$index = 0;
 		$sHtml .= "<table>\n";
 		$aFilterCriteria = $oSet->GetFilter()->GetCriteria();
@@ -488,7 +533,7 @@ abstract class cmdbAbstractObject extends CMDBObject
 		// OQL query builder
 		$sHtml .= "<div id=\"OQLQuery{$iSearchFormId}\" style=\"display:none\" class=\"mini_tab{$iSearchFormId}\">\n";
 		$sHtml .= "<h1>OQL Query Builder</h1>\n";
-		$sHtml .= "<form><table style=\"width:80%;\"><tr style=\"vertical-align:top\">\n";
+		$sHtml .= "<form id=\"formOQL{$iSearchFormId}\"><table style=\"width:80%;\"><tr style=\"vertical-align:top\">\n";
 		$sHtml .= "<td style=\"text-align:right\"><label>SELECT&nbsp;</label><select name=\"oql_class\">";
 		$aClasses = MetaModel::EnumChildClasses($sClassName, ENUM_CHILD_CLASSES_ALL);
 		$sSelectedClass = utils::ReadParam('oql_class', $sClassName);
@@ -513,7 +558,7 @@ abstract class cmdbAbstractObject extends CMDBObject
 		return $sHtml;
 	}
 	
-	public static function GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $value = '', $sDisplayValue = '', $iId = '')
+	public static function GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $value = '', $sDisplayValue = '', $iId = '', $sNameSuffix = '')
 	{
 		static $iInputId = 0;
 		if (!empty($iId))
@@ -529,15 +574,15 @@ abstract class cmdbAbstractObject extends CMDBObject
 			switch($oAttDef->GetEditClass())
 			{
 				case 'Date':
-				$sHTMLValue = "<input type=\"text\" size=\"20\" name=\"attr_$sAttCode\" value=\"$value\" id=\"$iInputId\" class=\"date-pick\"/>";
+				$sHTMLValue = "<input type=\"text\" size=\"20\" name=\"attr_{$sAttCode}{$sNameSuffix}\" value=\"$value\" id=\"$iInputId\" class=\"date-pick\"/>";
 				break;
 				
 				case 'Text':
-					$sHTMLValue = "<textarea name=\"attr_$sAttCode\" rows=\"8\" cols=\"40\" id=\"$iInputId\">$value</textarea>";
+					$sHTMLValue = "<textarea name=\"attr_{$sAttCode}{$sNameSuffix}\" rows=\"8\" cols=\"40\" id=\"$iInputId\">$value</textarea>";
 				break;
 	
 				case 'List':
-					$oWidget = new UILinksWidget($sClass, $sAttCode, $iInputId);
+					$oWidget = new UILinksWidget($sClass, $sAttCode, $iInputId, $sNameSuffix);
 					$sHTMLValue = $oWidget->Display($oPage, $value);
 				break;
 							
@@ -549,7 +594,7 @@ abstract class cmdbAbstractObject extends CMDBObject
 					//Enum field or external key, display a combo
 					if (count($aAllowedValues) == 0)
 					{
-						$sHTMLValue = "<input type=\"text\" size=\"70\" value=\"\" name=\"attr_$sAttCode\"  id=\"$iInputId\"/>";
+						$sHTMLValue = "<input type=\"text\" size=\"70\" value=\"\" name=\"attr_{$sAttCode}{$sNameSuffix}\"  id=\"$iInputId\"/>";
 					}
 					else if (count($aAllowedValues) > 50)
 					{
@@ -557,13 +602,13 @@ abstract class cmdbAbstractObject extends CMDBObject
 						// The input for the auto complete
 						$sHTMLValue = "<input type=\"text\" id=\"label_$iInputId\" size=\"50\" name=\"\" value=\"$sDisplayValue\" />";
 						// another hidden input to store & pass the object's Id
-						$sHTMLValue .= "<input type=\"hidden\" id=\"$iInputId\" name=\"attr_$sAttCode\" value=\"$value\" />\n";
+						$sHTMLValue .= "<input type=\"hidden\" id=\"$iInputId\" name=\"attr_{$sAttCode}{$sNameSuffix}\" value=\"$value\" />\n";
 						$oPage->add_ready_script("\$('#label_$iInputId').autocomplete('./ajax.render.php', { minChars:3, onItemSelect:selectItem, onFindValue:findValue, formatItem:formatItem, autoFill:true, keyHolder:'#$iInputId', extraParams:{operation:'autocomplete', sclass:'$sClass',attCode:'".$sAttCode."'}});");
 					}
 					else
 					{
 						// Few choices, use a normal 'select'
-						$sHTMLValue = "<select name=\"attr_$sAttCode\"  id=\"$iInputId\">\n";
+						$sHTMLValue = "<select name=\"attr_{$sAttCode}{$sNameSuffix}\"  id=\"$iInputId\">\n";
 						foreach($aAllowedValues as $key => $display_value)
 						{
 							$sSelected = ($value == $key) ? ' selected' : '';
@@ -574,7 +619,7 @@ abstract class cmdbAbstractObject extends CMDBObject
 				}
 				else
 				{
-					$sHTMLValue = "<input type=\"text\" size=\"50\" name=\"attr_$sAttCode\" value=\"$value\" id=\"$iInputId\">";
+					$sHTMLValue = "<input type=\"text\" size=\"50\" name=\"attr_{$sAttCode}{$sNameSuffix}\" value=\"$value\" id=\"$iInputId\">";
 				}
 			}
 		}

+ 69 - 20
application/displayblock.class.inc.php

@@ -85,11 +85,15 @@ class DisplayBlock
 		{
 			$sEncoding = strtolower($aMatches[1]);
 		}
-		if (preg_match('/ linkage="(.*)"/U',$sITopTag, $aMatches))
+		if (preg_match('/ link_attr="(.*)"/U',$sITopTag, $aMatches))
 		{
 			// The list to display is a list of links to the specified object
-			$sExtKey = strtolower($aMatches[1]);
-			$aParams['linkage'] = $sExtKey; // Name of the Ext. Key that make this linkage
+			$aParams['link_attr'] = $aMatches[1]; // Name of the Ext. Key that make this linkage
+		}
+		if (preg_match('/ object_id="(.*)"/U',$sITopTag, $aMatches))
+		{
+			// The list to display is a list of links to the specified object
+			$aParams['object_id'] = $aMatches[1]; // Id of the object to be linked to
 		}
 		// Parameters contains a list of extra parameters for the block
 		// the syntax is param_name1:value1;param_name2:value2;...
@@ -106,6 +110,21 @@ class DisplayBlock
 				}
 			}
 		}
+		if (!empty($aParams['link_attr']))
+		{
+			// Check that all mandatory parameters are present:
+			if(empty($aParams['object_id']))
+			{
+				// if 'links' mode is requested the d of the object to link to must be specified
+				throw new ApplicationException("Parameter object_id is mandatory when link_attr is specified. Check the definition of the display template.");
+			}
+			if(empty($aParams['target_attr']))
+			{
+				// if 'links' mode is requested the d of the object to link to must be specified
+				throw new ApplicationException("Parameter target_attr is mandatory when link_attr is specified. Check the definition of the display template.");
+			}
+
+		}
 		switch($sEncoding)
 		{
 			case 'text/serialize':
@@ -245,17 +264,16 @@ class DisplayBlock
 			break;
 			
 			case 'list':
-			$bDashboardMode = isset($aExtraParams['dashboard']) ? ($aExtraParams['dashboard'] == 'true') : false;
 			if ( ($this->m_oSet->Count()> 0) && (UserRights::IsActionAllowed($this->m_oSet->GetClass(), UR_ACTION_READ, $this->m_oSet) == UR_ALLOWED_YES) )
 			{
-				$sLinkage = isset($aExtraParams['linkage']) ? $aExtraParams['linkage'] : '';
-				$sHtml .= cmdbAbstractObject::GetDisplaySet($oPage, $this->m_oSet, $sLinkage, !$bDashboardMode /* bDisplayMenu */);
+				$sHtml .= cmdbAbstractObject::GetDisplaySet($oPage, $this->m_oSet, $aExtraParams);
 			}
 			else
 			{
 				$sHtml .= $oPage->GetP("No object to display.");
 				$sClass = $this->m_oFilter->GetClass();
-				if (!$bDashboardMode)
+				$bDisplayMenu = isset($this->m_aParams['menu']) ? $this->m_aParams['menu'] == true : true; 
+				if ($bDisplayMenu)
 				{
 					if (UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, $this->m_oSet) == UR_ALLOWED_YES)
 					{
@@ -265,12 +283,37 @@ class DisplayBlock
 			}
 			break;
 			
+			case 'links':
+			//$bDashboardMode = isset($aExtraParams['dashboard']) ? ($aExtraParams['dashboard'] == 'true') : false;
+			//$bSelectMode = isset($aExtraParams['select']) ? ($aExtraParams['select'] == 'true') : false;
+			if ( ($this->m_oSet->Count()> 0) && (UserRights::IsActionAllowed($this->m_oSet->GetClass(), UR_ACTION_READ, $this->m_oSet) == UR_ALLOWED_YES) )
+			{
+				//$sLinkage = isset($aExtraParams['linkage']) ? $aExtraParams['linkage'] : '';
+				$sHtml .= cmdbAbstractObject::GetDisplaySet($oPage, $this->m_oSet, $aExtraParams);
+			}
+			else
+			{
+				$sHtml .= $oPage->GetP("No object to display.");
+				$sClass = $this->m_oFilter->GetClass();
+				$bDisplayMenu = isset($this->m_aParams['menu']) ? $this->m_aParams['menu'] == true : true; 
+				if ($bDisplayMenu)
+				{
+					if (UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, $this->m_oSet) == UR_ALLOWED_YES)
+					{
+						$oAttDef = MetaModel::GetAttributeDef($sClass, $this->m_aParams['target_attr']);
+						$sTargetClass = $oAttDef->GetTargetClass();
+						$sHtml .= $oPage->GetP("<a href=\"../pages/UI.php?operation=modify_links&class=$sClass&link_attr=".$aExtraParams['link_attr']."&id=".$aExtraParams['object_id']."&target_class=$sTargetClass&addObjects=true\">Click here to add new ".Metamodel::GetName($sTargetClass)."s</a>\n");
+					}
+				}
+			}
+			break;
+			
 			case 'details':
 			if (UserRights::IsActionAllowed($this->m_oSet->GetClass(), UR_ACTION_READ, $this->m_oSet) == UR_ALLOWED_YES)
 			{
 				while($oObj = $this->m_oSet->Fetch())
 				{
-					$sHtml .= $oObj->GetDetails($oPage);
+					$sHtml .= $oObj->GetDetails($oPage); // Still used ???
 				}
 			}
 			break;
@@ -311,7 +354,7 @@ class DisplayBlock
 			$oPage->add_ready_script("\$(\"#LnkSearch_$iSearchSectionId\").click(function() {\$(\"#Search_$iSearchSectionId\").slideToggle('normal'); $(\"#LnkSearch_$iSearchSectionId\").toggleClass('open');});");
 			$sHtml .= cmdbAbstractObject::GetSearchForm($oPage, $this->m_oSet, $aExtraParams);
 	 		$sHtml .= "</div>\n";
-	 		$sHtml .= "<div class=\"HRDrawer\"/></div>\n";
+	 		$sHtml .= "<div class=\"HRDrawer\"></div>\n";
 	 		$sHtml .= "<div id=\"LnkSearch_$iSearchSectionId\" class=\"DrawerHandle\">Search</div>\n";
 			break;
 			
@@ -635,12 +678,15 @@ class MenuBlock extends DisplayBlock
 			$bIsBulkModifyAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY, $oSet);
 			$bIsBulkDeleteAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_DELETE, $oSet);
 			// Just one object in the set, possible actions are "new / clone / modify and delete"
-			if (isset($aExtraParams['linkage']))
+			if (isset($aExtraParams['link_attr']))
 			{
-				if ($bIsModifyAllowed) { $aActions[] = array ('label' => 'Add #...', 'url' => "../pages/$sUIPage?operation=new&class=$sClass&$sContext"); }
-				if ($bIsModifyAllowed) { $aActions[] = array ('label' => 'Clone #...', 'url' => "../pages/$sUIPage?operation=clone&class=$sClass&id=$id&$sContext"); }
-				if ($bIsModifyAllowed) { $aActions[] = array ('label' => 'Modify #...', 'url' => "../pages/$sUIPage?operation=modify&class=$sClass&id=$id&$sContext"); }
-				if ($bIsDeleteAllowed) { $aActions[] = array ('label' => 'Remove #', 'url' => "../pages/$sUIPage?operation=delete&class=$sClass&id=$id&$sContext"); }
+				$id = $aExtraParams['object_id'];
+				$sTargetAttr = $aExtraParams['target_attr'];
+				$oAttDef = MetaModel::GetAttributeDef($sClass, $sTargetAttr);
+				$sTargetClass = $oAttDef->GetTargetClass();
+				if ($bIsModifyAllowed) { $aActions[] = array ('label' => 'Add...', 'url' => "../pages/$sUIPage?operation=modify_links&class=$sClass&link_attr=".$aExtraParams['link_attr']."&target_class=$sTargetClass&id=$id&addObjects=true&$sContext"); }
+				if ($bIsModifyAllowed) { $aActions[] = array ('label' => 'Manage...', 'url' => "../pages/$sUIPage?operation=modify_links&class=$sClass&link_attr=".$aExtraParams['link_attr']."&target_class=$sTargetClass&id=$id&sContext"); }
+				if ($bIsDeleteAllowed) { $aActions[] = array ('label' => 'Remove All', 'url' => "#"); }
 			}
 			else
 			{
@@ -681,14 +727,17 @@ class MenuBlock extends DisplayBlock
 			$bIsModifyAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, $oSet);
 			$bIsBulkModifyAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_MODIFY, $oSet);
 			$bIsBulkDeleteAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_BULK_DELETE, $oSet);
-			if (isset($aExtraParams['linkage']))
+			if (isset($aExtraParams['link_attr']))
 			{
+				$id = $aExtraParams['object_id'];
+				$sTargetAttr = $aExtraParams['target_attr'];
+				$oAttDef = MetaModel::GetAttributeDef($sClass, $sTargetAttr);
+				$sTargetClass = $oAttDef->GetTargetClass();
 				$bIsDeleteAllowed = UserRights::IsActionAllowed($sClass, UR_ACTION_DELETE, $oSet);
-				if ($bIsModifyAllowed) { $aActions[] = array ('label' => 'Add #...', 'url' => "../pages/$sUIPage?operation=new&class=$sClass&$sContext"); }
-				if ($bIsDeleteAllowed) { $aActions[] = array ('label' => 'Remove #', 'url' => "../pages/$sUIPage?operation=delete&class=$sClass&id=$id&$sContext"); }
-				if ($bIsModifyAllowed) { $aActions[] = array ('label' => 'Modify #...', 'url' => "../pages/$sUIPage?operation=modify&class=$sClass&id=$id&$sContext"); }
-				if ($bIsBulkModifyAllowed) { $aActions[] = array ('label' => 'Modify All #...', 'url' => "../pages/$sUIPage?operation=new&class=$sClass&$sContext"); }
-				if ($bIsBulkDeleteAllowed) { $aActions[] = array ('label' => 'Remove All #...', 'url' => "#"); }
+				if ($bIsModifyAllowed) { $aActions[] = array ('label' => 'Add...', 'url' => "../pages/$sUIPage?operation=modify_links&class=$sClass&link_attr=".$aExtraParams['link_attr']."&target_class=$sTargetClass&id=$id&addObjects=true&$sContext"); }
+				//if ($bIsBulkModifyAllowed) { $aActions[] = array ('label' => 'Add...', 'url' => "../pages/$sUIPage?operation=modify_links&class=$sClass&linkage=".$aExtraParams['linkage']."&id=$id&addObjects=true&$sContext"); }
+				if ($bIsBulkModifyAllowed) { $aActions[] = array ('label' => 'Manage...', 'url' => "../pages/$sUIPage?operation=modify_links&class=$sClass&link_attr=".$aExtraParams['link_attr']."&target_class=$sTargetClass&id=$id&sContext"); }
+				if ($bIsBulkDeleteAllowed) { $aActions[] = array ('label' => 'Remove All...', 'url' => "#"); }
 			}
 			else
 			{

+ 16 - 3
application/template.class.inc.php

@@ -134,7 +134,7 @@ class DisplayTemplate
 			break;
 			
 			case 'itoptab':
-				$oPage->SetCurrentTab($aAttributes['name']);
+				$oPage->SetCurrentTab(str_replace('_', ' ', $aAttributes['name']));
 				$oTemplate = new DisplayTemplate($sContent);
 				$oTemplate->Render($oPage, array()); // no params to apply, they have already been applied
 				//$oPage->p('iTop Tab Content:<pre>'.htmlentities($sContent).'</pre>');
@@ -153,9 +153,22 @@ class DisplayTemplate
 				$sBlockClass = $aAttributes['blockclass'];
 				$sBlockType = $aAttributes['type'];
 				$aExtraParams = array();
-				if (isset($aAttributes['linkage']))
+				if (isset($aAttributes['link_attr']))
 				{
-					$aExtraParams['linkage'] = $aAttributes['linkage'];
+					$aExtraParams['link_attr'] = $aAttributes['link_attr'];
+					// Check that all mandatory parameters are present:
+					if(empty($aAttributes['object_id']))
+					{
+						// if 'links' mode is requested the d of the object to link to must be specified
+						throw new ApplicationException("Parameter object_id is mandatory when link_attr is specified. Check the definition of the display template.");
+					}
+					if(empty($aAttributes['target_attr']))
+					{
+						// if 'links' mode is requested the d of the object to link to must be specified
+						throw new ApplicationException("Parameter target_attr is mandatory when link_attr is specified. Check the definition of the display template.");
+					}
+					$aExtraParams['object_id'] = $aAttributes['object_id'];
+					$aExtraParams['target_attr'] = $aAttributes['target_attr'];
 				}
 
 				switch($aAttributes['encoding'])

+ 4 - 2
application/ui.linkswidget.class.inc.php

@@ -6,12 +6,14 @@ class UILinksWidget
 {
 	protected $m_sClass;
 	protected $m_sAttCode;
+	protected $m_sNameSuffix;
 	protected $m_iInputId;
 	
-	public function __construct($sClass, $sAttCode, $iInputId)
+	public function __construct($sClass, $sAttCode, $iInputId, $sNameSuffix = '')
 	{
 		$this->m_sClass = $sClass;
 		$this->m_sAttCode = $sAttCode;
+		$this->m_sNameSuffix = $sNameSuffix;
 		$this->m_iInputId = $iInputId;
 	}
 	
@@ -76,7 +78,7 @@ class UILinksWidget
 			$sHTMLValue .= "&nbsp;<input type=\"button\" value=\"Browse...\"  class=\"action\" onClick=\"return ManageObjects('$sTitle', '$sTargetClass', '$this->m_iInputId', '$sExtKeyToRemote');\"/>";
 			// another hidden input to store & pass the object's Id
 			$sHTMLValue .= "<input type=\"hidden\" id=\"id_ac_{$this->m_iInputId}\"/>\n";
-			$sHTMLValue .= "<input type=\"hidden\" id=\"{$this->m_iInputId}\" name=\"attr_{$this->m_sAttCode}\" value=\"\"/>\n";
+			$sHTMLValue .= "<input type=\"hidden\" id=\"{$this->m_iInputId}\" name=\"attr_{$this->m_sAttCode}{$this->m_sNameSuffix}\" value=\"\"/>\n";
 			$oPage->add_ready_script("\$('#{$this->m_iInputId}').val('$sJSON');\n\$('#ac_{$this->m_iInputId}').autocomplete('./ajax.render.php', { minChars:3, onItemSelect:selectItem, onFindValue:findValue, formatItem:formatItem, autoFill:true, keyHolder:'#id_ac_{$this->m_iInputId}', extraParams:{operation:'ui.linkswidget', sclass:'{$this->m_sClass}', attCode:'{$this->m_sAttCode}', max:30}});");
 		}
 		else

+ 380 - 0
application/uilinkswizard.class.inc.php

@@ -0,0 +1,380 @@
+<?php
+class UILinksWizard
+{
+	protected $m_sClass;
+	protected $m_sLinkageAttr;
+	protected $m_iObjectId;
+	protected $m_sLinkedClass;
+	protected $m_sLinkingAttCode;
+	protected $m_aEditableFields;
+	protected $m_aTableConfig;
+	
+	public function __construct($sClass,  $sLinkageAttr, $iObjectId, $sLinkedClass = '')
+	{
+		$this->m_sClass = $sClass;
+		$this->m_sLinkageAttr = $sLinkageAttr;
+		$this->m_iObjectId = $iObjectId;
+		$this->m_sLinkedClass = $sLinkedClass; // Will try to guess below, if it's empty
+		$this->m_sLinkingAttCode = ''; // Will be filled once we've found the attribute corresponding to the linked class
+		
+		$this->m_aEditableFields = array();
+		$this->m_aTableConfig = array();
+		$this->m_aTableConfig['form::checkbox'] = array( 'label' => "<input class=\"select_all\" type=\"checkbox\" value=\"1\" onChange=\"var value = this.checked; $('.selection').each( function() { this.checked = value; } );OnSelectChange();\">", 'description' => "Select / Deselect All");
+		foreach(MetaModel::GetAttributesList($this->m_sClass) as $sAttCode)
+		{
+			$oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
+			if ($oAttDef->IsExternalKey() && ($sAttCode != $this->m_sLinkageAttr))
+			{
+				if (empty($this->m_sLinkedClass))
+				{
+					// This is a class of objects we can manage !
+					// Since nothing was specify, any class will do !
+					$this->m_sLinkedClass = $oAttDef->GetTargetClass();
+					$this->m_sLinkingAttCode = $sAttCode;
+				}
+				else if ($this->m_sLinkedClass == $oAttDef->GetTargetClass())
+				{
+					// This is the class of objects we want to manage !
+					$this->m_sLinkingAttCode = $sAttCode;
+				}
+			}
+			else if ( (!$oAttDef->IsExternalKey()) && (!$oAttDef->IsExternalField()))
+			{
+				$this->m_aEditableFields[] = $sAttCode;
+				$this->m_aTableConfig[$sAttCode] = array( 'label' => $oAttDef->GetLabel(), 'description' => $oAttDef->GetDescription());
+			}
+		}
+		if (empty($this->m_sLinkedClass))
+		{
+			throw( new Exception("Incorrect link definition: the class of objects to manage: '$sLinkedClass' was not found as an external key in the class '$sClass'"));
+		}
+		foreach(MetaModel::GetZListItems($this->m_sLinkedClass, 'list') as $sFieldCode)
+		{
+			$oAttDef = MetaModel::GetAttributeDef($this->m_sLinkedClass, $sFieldCode);
+			$this->m_aTableConfig['static::'.$sFieldCode] = array( 'label' => $oAttDef->GetLabel(), 'description' => $oAttDef->GetDescription());
+		}
+	}
+
+	public function Display(web_page $oP, UserContext $oContext, $aExtraParams = array())
+	{
+		$oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $this->m_sLinkageAttr);
+		$sTargetClass = $oAttDef->GetTargetClass();
+		$oTargetObj = $oContext->GetObject($sTargetClass, $this->m_iObjectId);
+
+		$oP->set_title("iTop - ".MetaModel::GetName($this->m_sLinkedClass)." objects linked with ".MetaModel::GetName(get_class($oTargetObj)).": ".$oTargetObj->GetName());
+		$oP->add("<div class=\"wizContainer\">\n");
+		$oP->add("<form method=\"post\">\n");
+		$oP->add("<div class=\"page_header\">\n");
+		$oP->add("<input type=\"hidden\" id=\"linksToRemove\" name=\"linksToRemove\" value=\"\">\n");
+		$oP->add("<input type=\"hidden\" name=\"operation\" value=\"do_modify_links\">\n");
+		$oP->add("<input type=\"hidden\" name=\"class\" value=\"{$this->m_sClass}\">\n");
+		$oP->add("<input type=\"hidden\" name=\"linkage\" value=\"{$this->m_sLinkageAttr}\">\n");
+		$oP->add("<input type=\"hidden\" name=\"object_id\" value=\"{$this->m_iObjectId}\">\n");
+		$oP->add("<input type=\"hidden\" name=\"linking_attcode\" value=\"{$this->m_sLinkingAttCode}\">\n");
+		$oP->add("<h1>Manage ".MetaModel::GetName($this->m_sLinkedClass)."s linked with ".MetaModel::GetName(get_class($oTargetObj)).": <span class=\"hilite\">".$oTargetObj->GetHyperlink()."</span></h1>\n");
+		$oP->add("</div>\n");
+		$oP->add("<script type=\"text/javascript\">\n");
+		$oP->add("function OnSelectChange()
+		{
+			var nbChecked = $('.selection:checked').length;
+			if (nbChecked > 0)
+			{
+				$('#btnRemove').attr('disabled','');
+			}
+			else
+			{
+				$('#btnRemove').attr('disabled','disabled');
+			}
+		}
+		
+		function RemoveSelected()
+		{
+			$('.selection:checked').each(
+				function()
+				{
+					$('#linksToRemove').val($('#linksToRemove').val() + ' ' + this.value);
+					$('#row_'+this.value).remove();
+				}
+			);
+			// Disable the button since all the selected items have been removed
+			$('#btnRemove').attr('disabled','disabled');
+			// Re-run the zebra plugin to properly highlight the remaining lines
+			$('.listResults').trigger('update');
+			
+		}
+		
+		function AddObjects()
+		{
+			// TO DO: compute the list of objects already linked with the current Object
+			$.get( 'ajax.render.php', { 'operation': 'addObjects',
+										'class': '{$this->m_sClass}',
+										'linkageAttr': '{$this->m_sLinkageAttr}',
+										'linkedClass': '{$this->m_sLinkedClass}',
+										'objectId': '{$this->m_iObjectId}'
+										}, 
+				function(data)
+				{
+					$('#ModalDlg').html(data);
+					$('#ModalDlg').jqmShow();
+				},
+				'html'
+			);
+		}
+		
+		function SearchObjectsToAdd(currentFormId)
+		{
+			var theMap = { 'class': '{$this->m_sClass}',
+						   'linkageAttr': '{$this->m_sLinkageAttr}',
+						   'linkedClass': '{$this->m_sLinkedClass}',
+						   'objectId': '{$this->m_iObjectId}'
+						 }
+			
+			// Gather the parameters from the search form
+			$('#'+currentFormId+' :input').each(
+				function(i)
+				{
+					if (this.name != '')
+					{
+						theMap[this.name] = this.value;
+					}
+				}
+			);
+			theMap['operation'] = 'searchObjectsToAdd';
+			
+			// Run the query and display the results
+			$.get( 'ajax.render.php', theMap, 
+				function(data)
+				{
+					$('#SearchResultsToAdd').html(data);
+				},
+				'html'
+			);
+
+			return false;
+		}
+		
+		function DoAddObjects(currentFormId)
+		{
+			var theMap = { 'class': '{$this->m_sClass}',
+						   'linkageAttr': '{$this->m_sLinkageAttr}',
+						   'linkedClass': '{$this->m_sLinkedClass}',
+						   'objectId': '{$this->m_iObjectId}'
+						 }
+			
+			// Gather the parameters from the search form
+			$('#'+currentFormId+' :input').each(
+				function(i)
+				{
+					if ( (this.name != '') && ((this.type != 'checkbox') || (this.checked)) ) 
+					{
+						//console.log(this.type);
+						arrayExpr = /\[\]$/;
+						if (arrayExpr.test(this.name))
+						{
+							// Array
+							if (theMap[this.name] == undefined)
+							{
+								theMap[this.name] = new Array();
+							}
+							theMap[this.name].push(this.value);
+						}
+						else
+						{
+							theMap[this.name] = this.value;
+						}
+					}
+				}
+			);
+			theMap['operation'] = 'doAddObjects';
+			
+			// Run the query and display the results
+			$.get( 'ajax.render.php', theMap, 
+				function(data)
+				{
+					//console.log('Data: ' + data);
+					if (data != '')
+					{
+						$('#empty_row').remove();
+					}
+					$('.listResults tbody').append(data);
+					$('.listResults').trigger('update');
+					$('.listResults').tablesorter( { headers: { 0:{sorter: false }}, widgets: ['zebra']} ); // sortable and zebra tables 
+				},
+				'html'
+			);
+			$('#ModalDlg').jqmHide();
+			return false;
+		}
+		
+		function InitForm()
+		{
+			// make sure that the form is clean
+			$('.selection').each( function() { this.checked = false; });
+			$('#btnRemove').attr('disabled','disabled');
+			$('#linksToRemove').val('');
+		}
+		");
+		$oP->Add("</script>\n");
+		$oP->add_ready_script("InitForm();");
+		$oFilter = $oContext->NewFilter($this->m_sClass);
+		$oFilter->AddCondition($this->m_sLinkageAttr, $this->m_iObjectId, '=');
+		$oSet = new DBObjectSet($oFilter);
+		$aForm = array();
+		while($oCurrentLink = $oSet->Fetch())
+		{
+			$aRow = array();
+			$key = $oCurrentLink->GetKey();
+			$oLinkedObj = $oContext->GetObject($this->m_sLinkedClass, $oCurrentLink->Get($this->m_sLinkingAttCode));
+
+			$aForm[$key] = $this->GetFormRow($oP, $oLinkedObj, $oCurrentLink);
+		}
+		//var_dump($aTableLabels);
+		//var_dump($aForm);
+		$this->DisplayFormTable($oP, $this->m_aTableConfig, $aForm);
+		$oP->add("<span style=\"float:left;\">&nbsp;&nbsp;&nbsp;<img src=\"../images/tv-item-last.gif\">&nbsp;&nbsp;<input id=\"btnRemove\" type=\"button\" value=\" Remove ".MetaModel::GetName($this->m_sLinkedClass)."s \" onClick=\"RemoveSelected();\" >");
+		$oP->add("&nbsp;&nbsp;&nbsp;<input id=\"btnAdd\" type=\"button\" value=\" Add ".MetaModel::GetName($this->m_sLinkedClass)."s... \" onClick=\"AddObjects();\"></span>\n");
+		$oP->add("<span style=\"float:right;\"><input id=\"btnCancel\" type=\"button\" value=\" Cancel \" onClick=\"goBack();\">");
+		$oP->add("&nbsp;&nbsp;&nbsp;<input id=\"btnOk\" type=\"submit\" value=\" Ok \"></span>\n");
+		$oP->add("<span style=\"clear:both;\"><p>&nbsp;</p></span>\n");
+		$oP->add("</div>\n");
+		$oP->add("</form>\n");
+		if (isset($aExtraParams['StartWithAdd']) && ($aExtraParams['StartWithAdd']))
+		{
+			$oP->add_ready_script("AddObjects();");
+		}
+	}
+	
+	protected function GetFormRow($oP, $oLinkedObj, $currentLink = null )
+	{
+		$aRow = array();
+		if(is_object($currentLink))
+		{
+			$key = $currentLink->GetKey();
+			$sNameSuffix = "[$key]"; // To make a tabular form
+			$aRow['form::checkbox'] = "<input class=\"selection\" type=\"checkbox\" onChange=\"OnSelectChange();\" value=\"$key\">";
+			$aRow['form::checkbox'] .= "<input type=\"hidden\" name=\"linkId[$key]\" value=\"$key\">";
+			foreach($this->m_aEditableFields as $sFieldCode)
+			{
+				$oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sFieldCode);
+				$aRow[$sFieldCode] = cmdbAbstractObject::GetFormElementForField($oP, $this->m_sClass, $sFieldCode, $oAttDef, $currentLink->Get($sFieldCode), '' /* DisplayValue */, $key, $sNameSuffix);
+			}
+		}
+		else
+		{
+			// form for creating a new record
+			$sNameSuffix = "[$currentLink]"; // To make a tabular form
+			$aRow['form::checkbox'] = "<input class=\"selection\" type=\"checkbox\" onChange=\"OnSelectChange();\" value=\"$currentLink\">";
+			$aRow['form::checkbox'] .= "<input type=\"hidden\" name=\"linkId[$currentLink]\" value=\"$currentLink\">";
+			foreach($this->m_aEditableFields as $sFieldCode)
+			{
+				$oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sFieldCode);
+				$aRow[$sFieldCode] = cmdbAbstractObject::GetFormElementForField($oP, $this->m_sClass, $sFieldCode, $oAttDef, '' /* TO DO/ call GetDefaultValue */, '' /* DisplayValue */, '' /* id */, $sNameSuffix);
+			}
+		}
+		foreach(MetaModel::GetZListItems($this->m_sLinkedClass, 'list') as $sFieldCode)
+		{
+			$aRow['static::'.$sFieldCode] = $oLinkedObj->GetAsHTML($sFieldCode);
+		}
+		return $aRow;
+	}
+	
+	protected function DisplayFormTable(web_page $oP, $aConfig, $aData)
+	{
+		$oP->add("<table class=\"listResults\">\n");
+		// Header
+		$oP->add("<thead>\n");
+		$oP->add("<tr>\n");
+		foreach($aConfig as $sName=>$aDef)
+		{
+			$oP->add("<th title=\"".$aDef['description']."\">".$aDef['label']."</th>\n");
+		}
+		$oP->add("</tr>\n");
+		$oP->add("</thead>\n");
+		
+		// Content
+		$oP->add("</tbody>\n");
+		if (count($aData) == 0)
+		{
+			$oP->add("<tr id=\"empty_row\"><td colspan=\"".count($aConfig)."\" style=\"text-align:center;\">The list is empty, use 'Add...' to add elements.</td></td>");
+		}
+		else
+		{
+			foreach($aData as $iRowId => $aRow)
+			{
+				$this->DisplayFormRow($oP, $aConfig, $aRow, $iRowId);
+			}		
+		}
+		$oP->add("</tbody>\n");
+		
+		// Footer
+		$oP->add("</table>\n");
+	}
+	
+	protected function DisplayFormRow(web_page $oP, $aConfig, $aRow, $iRowId)
+	{
+		$oP->add("<tr id=\"row_$iRowId\">\n");
+		foreach($aConfig as $sName=>$void)
+		{
+			$oP->add("<td>".$aRow[$sName]."</td>\n");
+		}
+		$oP->add("</tr>\n");
+	}
+	
+	public function DisplayAddForm(web_page $oP, UserContext $oContext)
+	{
+		$oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $this->m_sLinkageAttr);
+		$sTargetClass = $oAttDef->GetTargetClass();
+		$oTargetObj = $oContext->GetObject($sTargetClass, $this->m_iObjectId);
+		$oP->add("<div class=\"wizContainer\">\n");
+		$oP->add("<div class=\"page_header\">\n");
+		$oP->add("<h1>Add ".MetaModel::GetName($this->m_sLinkedClass)."s to ".MetaModel::GetName(get_class($oTargetObj)).": <span class=\"hilite\">".$oTargetObj->GetHyperlink()."</span></h1>\n");
+		$oP->add("</div>\n");
+
+		$oFilter = $oContext->NewFilter($this->m_sLinkedClass);
+		$oSet = new CMDBObjectSet($oFilter);
+		$oBlock = new DisplayBlock($oFilter, 'search', false);
+		$oBlock->Display($oP, 'SearchFormToAdd', array('open' => true));
+		$oP->Add("<form id=\"ObjectsAddForm\" OnSubmit=\"return DoAddObjects(this.id);\">\n");
+		$oP->Add("<div id=\"SearchResultsToAdd\">\n");
+		$oP->Add("<div style=\"height: 100px; background: #fff;border-color:#F6F6F1 #E6E6E1 #E6E6E1 #F6F6F1; border-style:solid; border-width:3px; text-align: center; vertical-align: center;\"><p>Use the search form above to search for objects to be added.</p></div>\n");
+		$oP->Add("</div>\n");
+		$oP->add("<input type=\"button\" value=\"Cancel\" onClick=\"$('#ModalDlg').jqmHide();\">&nbsp;&nbsp;<input type=\"submit\" value=\" Add \">");
+		$oP->Add("</div>\n");
+		$oP->Add("</form>\n");
+		$oP->add_ready_script("$('div#SearchFormToAdd form').attr('onSubmit', 'var the_form = this; return SearchObjectsToAdd(the_form.id);');");
+	}
+
+	public function SearchObjectsToAdd(web_page $oP, UserContext $oContext)
+	{
+		$oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $this->m_sLinkageAttr);
+
+		$oFilter = $oContext->NewFilter($this->m_sLinkedClass);
+		$oSet = new CMDBObjectSet($oFilter);
+		$oBlock = new DisplayBlock($oFilter, 'list', false);
+		$oBlock->Display($oP, 'ResultsToAdd', array('menu' => false, 'selection_mode' => true)); // Don't display the 'Actions' menu on the results
+	}
+	
+	public function DoAddObjects(web_page $oP, UserContext $oContext, $aLinkedObjectIds = array())
+	{
+		//$oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $this->m_sLinkageAttr);
+		//$sTargetClass = $oAttDef->GetTargetClass();
+		//$oP->Add("<!-- nb of objects to add: ".count($aLinkedObjectIds)." -->\n"); // Just to make sure it's not empty
+		$aTable = array();
+		foreach($aLinkedObjectIds as $iObjectId)
+		{
+			$oLinkedObj = $oContext->GetObject($this->m_sLinkedClass, $iObjectId);
+			if (is_object($oLinkedObj))
+			{
+				$aRow = $this->GetFormRow($oP, $oLinkedObj, -$iObjectId ); // Not yet created link get negative Ids
+				$this->DisplayFormRow($oP, $this->m_aTableConfig, $aRow, -$iObjectId); 
+			}
+			else
+			{
+				echo "Object: $sTargetClass - Id: $iObjectId not found <br/>\n";
+			}
+		}
+		//var_dump($aTable);
+		//$oP->Add("<!-- end of added list -->\n"); // Just to make sure it's not empty
+	}
+}
+?>

+ 2 - 2
business/templates/server.html

@@ -16,10 +16,10 @@
 		<itopblock blockclass="DisplayBlock" type="list" encoding="text/oql" linkage="device_id">SELECT bizInterface WHERE device_id = $pkey$</itopblock>
 	</itoptab>
 	<itoptab name="Contacts">
-		<itopblock blockclass="DisplayBlock" type="list" linkage="object_id" encoding="text/sibusql">lnkContactRealObject: object_id = $pkey$</itopblock>
+		<itopblock blockclass="DisplayBlock" type="links" link_attr="object_id" object_id="$pkey$" target_attr="contact_id" encoding="text/sibusql">lnkContactRealObject: object_id = $pkey$</itopblock>
 	</itoptab>
 		<itoptab name="Incidents">
-		<itopblock blockclass="DisplayBlock" type="list" encoding="text/sibusql">bizIncidentTicket: PKEY IS ticket_id IN (lnkInfraTicket: infra_id = $pkey$)</itopblock>
+		<itopblock blockclass="DisplayBlock" type="links" link_attr="infra_id" object_id="$pkey$" target_attr="ticket_id" encoding="text/sibusql">lnkInfraTicket: infra_id = $pkey$</itopblock>
 	</itoptab>
 	<itoptab name="Changes">
 		<itopblock blockclass="DisplayBlock" type="list" encoding="text/sibusql">bizChangeTicket: PKEY IS ticket_id IN (lnkInfraChangeTicket: infra_id = $pkey$)</itopblock>

+ 1 - 1
business/templates/team.html

@@ -7,7 +7,7 @@
 <itopblock blockclass="DisplayBlock" asynchronous="true" type="bare_details" encoding="text/sibusql">$class$: pkey = $pkey$</itopblock>
 <itoptabs>
 	<itoptab name="Members">
-		<itopblock blockclass="DisplayBlock" type="list" encoding="text/sibusql">bizContact: PKEY IS object_id IN (lnkContactRealObject: contact_id = $pkey$)</itopblock>
+		<itopblock blockclass="DisplayBlock" type="list" link_attr="contact_id" object_id="$pkey$" target_attr="object_id" encoding="text/oql">SELECT lnkContactRealObject WHERE contact_id=$pkey$</itopblock>
 	</itoptab>
 	<itoptab name="Teams">
 		<itopblock blockclass="DisplayBlock" type="list" encoding="text/sibusql">bizTeam: PKEY IS object_id IN (lnkContactRealObject: contact_id = $pkey$)</itopblock>

+ 1 - 1
business/templates/ticket.html

@@ -10,7 +10,7 @@
 		<itopblock blockclass="DisplayBlock" type="list" encoding="text/sibusql">lnkInfraTicket: ticket_id = $pkey$</itopblock>
 	</itoptab>
 	<itoptab name="Related_Incidents">
-		<itopblock blockclass="DisplayBlock" type="list" encoding="text/sibusql">lnkRelatedTicket: ticket_id = $pkey$</itopblock>
+		<itopblock blockclass="DisplayBlock" type="links" link_attr="ticket_id" object_id="$pkey$" target_attr="rel_ticket_id" encoding="text/sibusql">lnkRelatedTicket: ticket_id = $pkey$</itopblock>
 	</itoptab>
 	<itoptab name="Contacts_to_Notify">
 		<itopblock blockclass="DisplayBlock" type="list" encoding="text/sibusql">lnkContactTicket: ticket_id = $pkey$</itopblock>

+ 118 - 1
pages/UI.php

@@ -335,7 +335,7 @@ switch($operation)
 			}
 			if (!is_object($oObj))
 			{
-				// new object or or that can't be retrieved (corrupted id or object not allowed to this user)
+				// new object or that can't be retrieved (corrupted id or object not allowed to this user)
 				$id = '';
 				$oObj = MetaModel::NewObject($sClass);
 			}
@@ -701,6 +701,123 @@ switch($operation)
 	}
 	break;
 
+	case 'modify_links':
+	$sClass = utils::ReadParam('class', '');
+	$sLinkAttr = utils::ReadParam('link_attr', '');
+	$sTargetClass = utils::ReadParam('target_class', '');
+	$id = utils::ReadParam('id', '');
+	$bAddObjects = utils::ReadParam('addObjects', false);
+	if ( empty($sClass) || empty($id) || empty($sLinkAttr) || empty($sTargetClass)) // TO DO: check that the class name is valid !
+	{
+		$oP->set_title("iTop - Error");
+		$oP->add("<p>4 parameters are mandatory for this operation: class, id, target_class and link_attr.</p>\n");
+	}
+	else
+	{
+		require_once('../application/uilinkswizard.class.inc.php');
+		$oWizard = new UILinksWizard($sClass, $sLinkAttr, $id, $sTargetClass);
+		$oWizard->Display($oP, $oContext, array('StartWithAdd' => $bAddObjects));		
+	}
+	break;
+	
+	case 'do_modify_links':
+	$aLinks = utils::ReadParam('linkId', array(), 'post');
+	$sLinksToRemove = trim(utils::ReadParam('linksToRemove', '', 'post'));
+	$aLinksToRemove = array();
+	if (!empty($sLinksToRemove))
+	{
+		$aLinksToRemove = explode(' ', trim($sLinksToRemove));
+	}
+	$sClass = utils::ReadParam('class', '', 'post');
+	$sLinkageAtt = utils::ReadParam('linkage', '', 'post');
+	$iObjectId = utils::ReadParam('object_id', '', 'post');
+	$sLinkingAttCode = utils::ReadParam('linking_attcode', '', 'post');
+	$oMyChange = MetaModel::NewObject("CMDBChange");
+	$oMyChange->Set("date", time());
+	if (UserRights::GetUser() != UserRights::GetRealUser())
+	{
+		$sUserString = UserRights::GetRealUser()." on behalf of ".UserRights::GetUser();
+	}
+	else
+	{
+		$sUserString = UserRights::GetUser();
+	}
+	$oMyChange->Set("userinfo", $sUserString);
+	$iChangeId = $oMyChange->DBInsert();
+	
+	// Delete links that are to be deleted
+	foreach($aLinksToRemove as $iLinkId)
+	{
+		if ($iLinkId > 0) // Negative IDs are objects that were not even created
+		{
+			$oLink = $oContext->GetObject($sClass, $iLinkId);
+			$oLink->DBDeleteTracked($oMyChange);
+		}
+	}
+	
+	$aEditableFields = array();
+	$aData = array();
+	foreach(MetaModel::GetAttributesList($sClass) as $sAttCode)
+	{
+		$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+		if ( (!$oAttDef->IsExternalKey()) && (!$oAttDef->IsExternalField()))
+		{
+			$aEditableFields[] = $sAttCode;
+			$aData[$sAttCode] = utils::ReadParam('attr_'.$sAttCode, array(), 'post');
+		}
+	}
+	
+	// Update existing links or create new links
+	foreach($aLinks as $iLinkId)
+	{
+		if ($iLinkId > 0)
+		{
+			// This is an existing link to be modified
+			$oLink = $oContext->GetObject($sClass, $iLinkId);
+			
+			// Update all the attributes of the link
+			foreach($aEditableFields as $sAttCode)
+			{
+				$value = $aData[$sAttCode][$iLinkId];
+				$oLink->Set($sAttCode, $value);
+			}
+			if ($oLink->IsModified())
+			{
+				$oLink->DBUpdateTracked($oMyChange);
+			}
+			//echo "Updated link:<br/>\n";
+			//var_dump($oLink);
+		}
+		else
+		{
+			// A new link must be created
+			$oLink = MetaModel::NewObject($sClass);
+			$oLinkedObjectId = -$iLinkId;
+			// Set all the attributes of the link
+			foreach($aEditableFields as $sAttCode)
+			{
+				$value = $aData[$sAttCode][$iLinkId];
+				$oLink->Set($sAttCode, $value);
+			}
+			// And the two external keys
+			$oLink->Set($sLinkageAtt, $iObjectId);
+			$oLink->Set($sLinkingAttCode, $oLinkedObjectId);
+			// then save it
+			//echo "Created link:<br/>\n";
+			//var_dump($oLink);
+			$oLink->DBInsertTracked($oMyChange);
+		}
+	}
+	// Display again the details of the linked object
+	$oAttDef = MetaModel::GetAttributeDef($sClass, $sLinkageAtt);
+	$sTargetClass = $oAttDef->GetTargetClass();
+	$oObj = $oContext->GetObject($sTargetClass, $iObjectId);
+	
+	$oSearch = $oContext->NewFilter(get_class($oObj));
+	$oBlock = new DisplayBlock($oSearch, 'search', false);
+	$oBlock->Display($oP, 0);
+	$oObj->DisplayDetails($oP);
+	break;
 	
 	default:
 	$oActiveNode->RenderContent($oP, $oAppContext->GetAsHash());

+ 34 - 0
pages/ajax.render.php

@@ -32,6 +32,40 @@ $sStyle = utils::ReadParam('style', 'list');
 
 switch($operation)
 {
+	case 'addObjects':
+	require_once('../application/uilinkswizard.class.inc.php');
+	
+	$sClass = utils::ReadParam('class', '', 'get');
+	$sLinkedClass = utils::ReadParam('linkedClass', '', 'get');
+	$sLinkageAttr = utils::ReadParam('linkageAttr', '', 'get');
+	$iObjectId = utils::ReadParam('objectId', '', 'get');
+	$oLinksWizard = new UILinksWizard($sClass,  $sLinkageAttr, $iObjectId, $sLinkedClass);
+	$oLinksWizard->DisplayAddForm($oPage, $oContext);
+	break;
+	
+	case 'searchObjectsToAdd':
+	require_once('../application/uilinkswizard.class.inc.php');
+	
+	$sClass = utils::ReadParam('class', '', 'get');
+	$sLinkedClass = utils::ReadParam('linkedClass', '', 'get');
+	$sLinkageAttr = utils::ReadParam('linkageAttr', '', 'get');
+	$iObjectId = utils::ReadParam('objectId', '', 'get');
+	$oLinksWizard = new UILinksWizard($sClass,  $sLinkageAttr, $iObjectId, $sLinkedClass);
+	$oLinksWizard->SearchObjectsToAdd($oPage, $oContext);
+	break;
+	
+	case 'doAddObjects':
+	require_once('../application/uilinkswizard.class.inc.php');
+	
+	$sClass = utils::ReadParam('class', '', 'get');
+	$sLinkedClass = utils::ReadParam('linkedClass', '', 'get');
+	$sLinkageAttr = utils::ReadParam('linkageAttr', '', 'get');
+	$iObjectId = utils::ReadParam('objectId', '', 'get');
+	$aLinkedObjectIds = utils::ReadParam('selectObject', array(), 'get');
+	$oLinksWizard = new UILinksWizard($sClass,  $sLinkageAttr, $iObjectId, $sLinkedClass);
+	$oLinksWizard->DoAddObjects($oPage, $oContext, $aLinkedObjectIds);
+	break;
+	
 	case 'wizard_helper_preview':
 	$sJson = utils::ReadParam('json_obj', '', 'post');
 	$oWizardHelper = WizardHelper::FromJSON($sJson);