Browse Source

#757 Better UI to manage direct linksets... on going...

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@2927 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 11 years ago
parent
commit
9646782b81

+ 41 - 6
application/cmdbabstract.class.inc.php

@@ -2541,7 +2541,8 @@ EOF
 					$this->Set($sAttCode, $iValue);
 				}
 			}
-			else if (($oAttDef->GetEditClass() == 'LinkedSet') && !$oAttDef->IsIndirect() && ($oAttDef->GetEditMode() == LINKSET_EDITMODE_INPLACE))
+			else if (($oAttDef->GetEditClass() == 'LinkedSet') && !$oAttDef->IsIndirect() &&
+			          (($oAttDef->GetEditMode() == LINKSET_EDITMODE_INPLACE) || ($oAttDef->GetEditMode() == LINKSET_EDITMODE_ADDREMOVE)))
 			{
 				$oLinkset = $this->Get($sAttCode);
 				$sLinkedClass = $oLinkset->GetClass();
@@ -2557,13 +2558,16 @@ EOF
 					}
 					else
 					{
-						$aObjSet[] = $oLink;
+						if (!array_key_exists('to_be_removed', $value) || !in_array($oLink->GetKey(), $value['to_be_removed']))
+						{
+							$aObjSet[] = $oLink;
+						}
 					}
 				}
 
 				if (array_key_exists('to_be_created', $value) && (count($value['to_be_created']) > 0))
 				{
-					// Now handle the lniks to be created
+					// Now handle the links to be created
 					foreach($value['to_be_created'] as $aData)
 					{
 						$sSubClass = $aData['class'];
@@ -2578,7 +2582,35 @@ EOF
 						}
 					}
 				}
-
+				if (array_key_exists('to_be_added', $value) && (count($value['to_be_added']) > 0))
+				{
+					// Now handle the links to be added by making the remote object point to self
+					foreach($value['to_be_added'] as $iObjKey)
+					{
+						$oLink = MetaModel::GetObject($sLinkedClass, $iObjKey, false);
+						if ($oLink)
+						{
+							$aObjSet[] = $oLink;
+							$bModified = true;
+						}
+					}
+				}
+				if (array_key_exists('to_be_removed', $value) && (count($value['to_be_removed']) > 0))
+				{
+					// Now handle the links to be removed by making the remote object point to nothing
+					// Keep them in the set (modified), DBWriteLinks will handle them
+					foreach($value['to_be_removed'] as $iObjKey)
+					{
+						$oLink = MetaModel::GetObject($sLinkedClass, $iObjKey, false);
+						if ($oLink)
+						{
+							$sExtKeyToMe = $oAttDef->GetExtKeyToMe();
+							$oLink->Set($sExtKeyToMe, null);
+							$aObjSet[] = $oLink;
+							$bModified = true;
+						}
+					}
+				}
 				if ($bModified)
 				{
 					$oNewSet = DBObjectSet::FromArray($oLinkset->GetClass(), $aObjSet);
@@ -2617,7 +2649,8 @@ EOF
 			{
 				$value = array('fcontents' => utils::ReadPostedDocument("attr_{$sFormPrefix}{$sAttCode}", 'fcontents'));
 			}
-			else if (($oAttDef->GetEditClass() == 'LinkedSet') && !$oAttDef->IsIndirect() && ($oAttDef->GetEditMode() == LINKSET_EDITMODE_INPLACE))
+			else if (($oAttDef->GetEditClass() == 'LinkedSet') && !$oAttDef->IsIndirect() &&
+			         (($oAttDef->GetEditMode() == LINKSET_EDITMODE_INPLACE) || ($oAttDef->GetEditMode() == LINKSET_EDITMODE_ADDREMOVE)) )
 			{
 				$aRawToBeCreated = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbc", '{}', 'raw_data'), true);
 				$aToBeCreated = array();
@@ -2637,7 +2670,9 @@ EOF
 				}
 				
 				$value = array('to_be_created' => $aToBeCreated, 
-							   'to_be_deleted' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbd", '[]', 'raw_data'), true) );
+							   'to_be_deleted' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbd", '[]', 'raw_data'), true), 
+							   'to_be_added' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tba", '[]', 'raw_data'), true),
+							   'to_be_removed' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbr", '[]', 'raw_data'), true) );
 			}
 			else
 			{

+ 135 - 8
application/ui.linksdirectwidget.class.inc.php

@@ -54,6 +54,7 @@ class UILinksWidgetDirect
 			
 			default:
 			$aZList = MetaModel::FlattenZList(MetaModel::GetZListItems($this->sLinkedClass, 'list'));
+			array_unshift($aZList, 'friendlyname');
 		}
 		foreach($aZList as $sLinkedAttCode)
 		{
@@ -97,6 +98,18 @@ class UILinksWidgetDirect
 			$this->DisplayEditInPlace($oPage, $oValue, $aArgs, $sFormPrefix, $oCurrentObj);
 			break;
 			
+			case LINKSET_EDITMODE_ADDREMOVE: // The whole linkset can be edited 'in-place'
+			$sTargetClass = $oLinksetDef->GetLinkedClass();
+			$sExtKeyToMe = $oLinksetDef->GetExtKeyToMe();
+			$oExtKeyDef = MetaModel::GetAttributeDef($sTargetClass, $sExtKeyToMe);
+			$aButtons = array('add');
+			if ($oExtKeyDef->IsNullAllowed())
+			{
+				$aButtons = array('add', 'remove');
+			}
+			$this->DisplayEditInPlace($oPage, $oValue, $aArgs, $sFormPrefix, $oCurrentObj, $aButtons);
+			break;
+			
 			case LINKSET_EDITMODE_ACTIONS:
 			default:
 			$this->DisplayAsBlock($oPage, $oValue, $aArgs = array(), $sFormPrefix, $oCurrentObj, true /* bDisplayMenu*/);
@@ -139,7 +152,7 @@ class UILinksWidgetDirect
 		}	
 	}
 	
-	protected function DisplayEditInPlace(WebPage $oPage, DBObjectSet $oValue, $aArgs = array(), $sFormPrefix, $oCurrentObj)
+	protected function DisplayEditInPlace(WebPage $oPage, DBObjectSet $oValue, $aArgs = array(), $sFormPrefix, $oCurrentObj, $aButtons = array('create', 'delete'))
 	{
 		$aAttribs = $this->GetTableConfig();
 		
@@ -165,10 +178,16 @@ class UILinksWidgetDirect
 			// 'modify' => 'Modify...' , 
 			'creation_title' => Dict::Format('UI:CreationTitle_Class', MetaModel::GetName($this->sLinkedClass)),
 			'create' => Dict::Format('UI:ClickToCreateNew', MetaModel::GetName($this->sLinkedClass)),
+			'remove' => Dict::S('UI:Button:Remove'),
+			'add' => Dict::Format('UI:AddAnExisting_Class', MetaModel::GetName($this->sLinkedClass)),
+			'selection_title' => Dict::Format('UI:SelectionOf_Class', MetaModel::GetName($this->sLinkedClass)),
 		);
-		$sSubmitUrl = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php';
+		$oContext = new ApplicationContext();
+		$sSubmitUrl = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?'.$oContext->GetForLink();
 		$sJSONLabels = json_encode($aLabels);
-		$oPage->add_ready_script("$('#{$this->sInputid}').directlinks({class_name: '$this->sClass', att_code: '$this->sAttCode', input_name:'$sInputName', labels: $sJSONLabels, sumit_to: '$sSubmitUrl' });");
+		$sJSONButtons = json_encode($aButtons);
+		$sWizHelper = 'oWizardHelper'.$sFormPrefix;
+		$oPage->add_ready_script("$('#{$this->sInputid}').directlinks({class_name: '$this->sClass', att_code: '$this->sAttCode', input_name:'$sInputName', labels: $sJSONLabels, submit_to: '$sSubmitUrl', buttons: $sJSONButtons, oWizardHelper: $sWizHelper });");
 	}
 	
 	public function GetObjectCreationDlg(WebPage $oPage, $sProposedRealClass = '')
@@ -221,6 +240,71 @@ class UILinksWidgetDirect
 		$oPage->add('</div></div>');
 	}
 	
+	public function GetObjectsSelectionDlg($oPage, $oCurrentObj)
+	{
+		$sHtml = "<div class=\"wizContainer\" style=\"vertical-align:top;\">\n";
+		$oFilter = new DBObjectSearch($this->sLinkedClass);
+		if ($oCurrentObj != null)
+		{
+			$this->SetSearchDefaultFromContext($oCurrentObj, $oFilter);
+		}
+		$oBlock = new DisplayBlock($oFilter, 'search', false);
+		$sHtml .= $oBlock->GetDisplay($oPage, "SearchFormToAdd_{$this->sInputid}", array('open' => true));
+		$sHtml .= "<form id=\"ObjectsAddForm_{$this->sInputid}\">\n";
+		$sHtml .= "<div id=\"SearchResultsToAdd_{$this->sInputid}\" style=\"vertical-align:top;background: #fff;height:100%;overflow:auto;padding:0;border:0;\">\n";
+		$sHtml .= "<div style=\"background: #fff; border:0; text-align:center; vertical-align:middle;\"><p>".Dict::S('UI:Message:EmptyList:UseSearchForm')."</p></div>\n";
+		$sHtml .= "</div>\n";
+		$sHtml .= "<input type=\"hidden\" id=\"count_{$this->sInputid}\" value=\"0\"/>";
+		$sHtml .= "<button type=\"button\" class=\"cancel\">".Dict::S('UI:Button:Cancel')."</button>&nbsp;&nbsp;<button class=\"ok\" disabled=\"disabled\">".Dict::S('UI:Button:Add')."</button>";
+		$sHtml .= "</div>\n";
+		$sHtml .= "</form>\n";
+		$oPage->add($sHtml);
+		//$oPage->add_ready_script("$('#SearchFormToAdd_{$this->sAttCode}{$this->sNameSuffix} form').bind('submit.uilinksWizard', oWidget{$this->sInputId}.SearchObjectsToAdd);");
+		//$oPage->add_ready_script("$('#SearchFormToAdd_{$this->sAttCode}{$this->sNameSuffix}').resize(oWidget{$this->siInputId}.UpdateSizes);");
+	}
+	
+	/**
+	 * Search for objects to be linked to the current object (i.e "remote" objects)
+	 * @param WebPage $oP The page used for the output (usually an AjaxWebPage)
+	 * @param string $sRemoteClass Name of the "remote" class to perform the search on, must be a derived class of $this->sLinkedClass
+	 * @param array $aAlreadyLinked Array of indentifiers of objects which are already linke to the current object (or about to be linked)
+	 * @param DBObject $oCurrentObj The object currently being edited... if known...
+	 */
+	public function SearchObjectsToAdd(WebPage $oP, $sRemoteClass = '', $aAlreadyLinked = array(), $oCurrentObj = null)
+	{
+		if ($sRemoteClass == '')
+		{
+			$sRemoteClass = $this->sLinkedClass;
+		}
+		$oFilter = new DBObjectSearch($sRemoteClass);
+		if (($oCurrentObj != null) && MetaModel::IsSameFamilyBranch($sRemoteClass, $this->sClass))
+		{
+			// Prevent linking to self if the linked object is of the same family
+			// and laready present in the database
+			if (!$oCurrentObj->IsNew())
+			{
+				$oFilter->AddCondition('id', $oCurrentObj->GetKey(), '!=');
+			}
+		}
+		if (count($aAlreadyLinked) > 0)
+		{
+			$oFilter->AddCondition('id', $aAlreadyLinked, 'NOTIN');
+		}
+		$oSet = new CMDBObjectSet($oFilter);
+		$oBlock = new DisplayBlock($oFilter, 'list', false);
+		$oBlock->Display($oP, "ResultsToAdd_{$this->sInputid}", array('menu' => false, 'cssCount'=> '#count_'.$this->sInputid , 'selection_mode' => true, 'table_id' => 'add_'.$this->sInputid)); // Don't display the 'Actions' menu on the results
+	}
+
+	public function DoAddObjects(WebPage $oP, $oFullSetFilter)
+	{
+		$aLinkedObjectIds = utils::ReadMultipleSelection($oFullSetFilter);
+		foreach($aLinkedObjectIds as $iObjectId)
+		{
+			$oLinkObj = MetaModel::GetObject($this->sLinkedClass, $iObjectId);
+			$oP->add($this->GetObjectRow($oP, $oLinkObj, $oLinkObj->GetKey()));
+		}
+	}
+	
 	public function GetObjectModificationDlg()
 	{
 		
@@ -238,9 +322,9 @@ class UILinksWidgetDirect
 		}
 		return $aAttribs;	
 	}
+	
 	public function GetRow($oPage, $sRealClass, $aValues, $iTempId)
 	{
-		$aAttribs = $this->GetTableConfig();
 		if ($sRealClass == '')
 		{
 			$sRealClass = $this->sLinkedClass;
@@ -248,17 +332,60 @@ class UILinksWidgetDirect
 		$oLinkObj = new $sRealClass();
 		$oLinkObj->UpdateObjectFromPostedForm($this->sInputid);
 		
+		return $this->GetObjectRow($oPage, $oLinkObj, $iTempId);
+	}
+	
+	protected function GetObjectRow($oPage, $oLinkObj, $iTempId)
+	{
+		$aAttribs = $this->GetTableConfig();
 		$aRow = array();
-		$aRow['form::select'] = '<input type="checkbox" class="selectList'.$this->sInputid.'" value="'.(-$iTempId).'"/>';
+		$aRow['form::select'] = '<input type="checkbox" class="selectList'.$this->sInputid.'" value="'.($iTempId).'"/>';
 		foreach($this->aZlist as $sLinkedAttCode)
 		{
 			$aRow[$sLinkedAttCode] = $oLinkObj->GetAsHTML($sLinkedAttCode);
 		}
-		return $oPage->GetTableRow($aRow, $aAttribs);
+		return $oPage->GetTableRow($aRow, $aAttribs);		
 	}
 	
-	public function UpdateFromArray($oObj, $aData)
+	/**
+	 * Initializes the default search parameters based on 1) a 'current' object and 2) the silos defined by the context
+	 * @param DBObject $oSourceObj
+	 * @param DBObjectSearch $oSearch
+	 */
+	protected function SetSearchDefaultFromContext($oSourceObj, &$oSearch)
 	{
-		
+		$oAppContext = new ApplicationContext();
+		$sSrcClass = get_class($oSourceObj);
+		$sDestClass = $oSearch->GetClass();
+		foreach($oAppContext->GetNames() as $key)
+		{
+			// Find the value of the object corresponding to each 'context' parameter
+			$aCallSpec = array($sSrcClass, 'MapContextParam');
+			$sAttCode = '';
+			if (is_callable($aCallSpec))
+			{
+				$sAttCode = call_user_func($aCallSpec, $key); // Returns null when there is no mapping for this parameter					
+			}
+
+			if (MetaModel::IsValidAttCode($sSrcClass, $sAttCode))
+			{
+				$oAttDef = MetaModel::GetAttributeDef($sSrcClass, $sAttCode);
+				$defaultValue = $oSourceObj->Get($sAttCode);
+
+				// Find the attcode for the same 'context' parameter in the destination class
+				// and sets its value as the default value for the search condition
+				$aCallSpec = array($sDestClass, 'MapContextParam');
+				$sAttCode = '';
+				if (is_callable($aCallSpec))
+				{
+					$sAttCode = call_user_func($aCallSpec, $key); // Returns null when there is no mapping for this parameter					
+				}
+	
+				if (MetaModel::IsValidAttCode($sDestClass, $sAttCode) && !empty($defaultValue))
+				{
+					$oSearch->AddCondition($sAttCode, $defaultValue);
+				}
+			}
+		}
 	}
 }

+ 1 - 0
core/attributedef.class.inc.php

@@ -92,6 +92,7 @@ define('LINKSET_EDITMODE_NONE', 0); // The linkset cannot be edited at all from
 define('LINKSET_EDITMODE_ADDONLY', 1); // The only possible action is to open a new window to create a new object
 define('LINKSET_EDITMODE_ACTIONS', 2); // Show the usual 'Actions' popup menu
 define('LINKSET_EDITMODE_INPLACE', 3); // The "linked" objects can be created/modified/deleted in place
+define('LINKSET_EDITMODE_ADDREMOVE', 4); // The "linked" objects can be added/removed in place
 
 
 /**

+ 20 - 5
core/dbobject.class.php

@@ -1201,24 +1201,39 @@ abstract class DBObject
 		foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef)
 		{
 			if (!$oAttDef->IsLinkSet()) continue;
-
+			
+			$oOriginalSet = $this->m_aOrigValues[$sAttCode];
+			if ($oOriginalSet != null)
+			{
+				$aOriginalList = $oOriginalSet->ToArray();
+			}
+			else
+			{
+				$aOriginalList = array();
+			}
+			
 			$oLinks = $this->Get($sAttCode);
 			$oLinks->Rewind();
 			while ($oLinkedObject = $oLinks->Fetch())
 			{
-				$oLinkedObject->Set($oAttDef->GetExtKeyToMe(), $this->m_iKey);
+				if (!array_key_exists($oLinkedObject->GetKey(), $aOriginalList))
+				{
+					// New object added to the set, make it point properly
+					$oLinkedObject->Set($oAttDef->GetExtKeyToMe(), $this->m_iKey);
+				}
 				if ($oLinkedObject->IsModified())
 				{
+					// Objects can be modified because:
+					// 1) They've just been added into the set, so their ExtKey is modified
+					// 2) They are about to be removed from the set BUT NOT deleted, their ExtKey has been reset
 					$oLinkedObject->DBWrite();
 				}
 			}
 
 			// Delete the objects that were initialy present and disappeared from the list
 			// (if any)
-			$oOriginalSet = $this->m_aOrigValues[$sAttCode];
-			if ($oOriginalSet != null)
+			if (count($aOriginalList) > 0)
 			{
-				$aOriginalList = $oOriginalSet->ToArray();
 				$aNewSet = $oLinks->ToArray();
 				
 				foreach($aOriginalList as $iId => $oObject)

+ 4 - 4
datamodels/2.x/itop-change-mgmt/datamodel.itop-change-mgmt.xml

@@ -108,28 +108,28 @@
         <field id="related_request_list" xsi:type="AttributeLinkedSet">
           <linked_class>UserRequest</linked_class>
           <ext_key_to_me>parent_change_id</ext_key_to_me>
-          <edit_mode>none</edit_mode>
+          <edit_mode>add_remove</edit_mode>
           <count_min>0</count_min>
           <count_max>0</count_max>
         </field>
         <field id="related_incident_list" xsi:type="AttributeLinkedSet">
           <linked_class>Incident</linked_class>
           <ext_key_to_me>parent_change_id</ext_key_to_me>
-          <edit_mode>none</edit_mode>
+          <edit_mode>add_remove</edit_mode>
           <count_min>0</count_min>
           <count_max>0</count_max>
         </field>
         <field id="related_problems_list" xsi:type="AttributeLinkedSet">
           <linked_class>Problem</linked_class>
           <ext_key_to_me>related_change_id</ext_key_to_me>
-          <edit_mode>none</edit_mode>
+          <edit_mode>add_remove</edit_mode>
           <count_min>0</count_min>
           <count_max>0</count_max>
         </field>
         <field id="child_changes_list" xsi:type="AttributeLinkedSet">
           <linked_class>Change</linked_class>
           <ext_key_to_me>parent_id</ext_key_to_me>
-          <edit_mode>none</edit_mode>
+          <edit_mode>add_remove</edit_mode>
           <count_min>0</count_min>
           <count_max>0</count_max>
         </field>

+ 4 - 1
dictionaries/dictionary.itop.ui.php

@@ -1189,6 +1189,9 @@ When associated with a trigger, each action is given an "order" number, specifyi
 	'UI:CSVImportCreated_items' => 'Created: %1$d',
 	'UI:CSVImportModified_items' => 'Modified: %1$d',
 	'UI:CSVImportUnchanged_items' => 'Unchanged: %1$d',
-	
+
+	'UI:Button:Remove' => 'Remove',
+	'UI:AddAnExisting_Class' => 'Add objects of type %1$s...',
+	'UI:SelectionOf_Class' => 'Selection of objects of type %1$s',
 ));
 ?>

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

@@ -1025,5 +1025,9 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
 	'UI:CSVImportCreated_items' => 'Créations: %1$d',
 	'UI:CSVImportModified_items' => 'Modifications: %1$d',
 	'UI:CSVImportUnchanged_items' => 'Inchangés: %1$d',
+
+	'UI:Button:Remove' => 'Enlever',
+	'UI:AddAnExisting_Class' => 'Ajouter des objets de type %1$s...',
+	'UI:SelectionOf_Class' => 'Sélection d\'objets de type %1$s',
 ));
 ?>

+ 287 - 26
js/linksdirectwidget.js

@@ -16,8 +16,13 @@ $(function()
 			labels: { 'delete': 'Delete',
 				  	  modify: 'Modify...' , 
 				  	  creation_title: 'Creation of a new object...' , 
-					  create: 'Create...'
-					}
+					  create: 'Create...',
+					  add: 'Add...',
+					  remove: 'Remove',
+					  selection_title: 'Objects selection'
+					},
+			buttons: ['create', 'delete'],
+			oWizardHelper: null
 		},
 	
 		// the constructor
@@ -31,34 +36,49 @@ $(function()
 			
 			this.datatable = this.element.find('table.listResults');
 			
-			this.deleteBtn = $('<button type="button">' + this.options.labels['delete'] + '</button>');
-			this.modifyBtn = $('<button type="button">' + this.options.labels['modify'] + '</button>');
-			this.createBtn = $('<button type="button">' + this.options.labels['create'] + '</button>');
+			var aButtonsTypes = ['delete', 'remove', 'modify', 'add', 'create'];
+			this.oButtons = {};
+			for(k in aButtonsTypes)
+			{
+				this.oButtons[aButtonsTypes[k]] =  $('<button type="button">' + this.options.labels[aButtonsTypes[k]] + '</button>');
+			}
 			this.indicator = $('<span></span>');
 			this.inputToBeCreated = $('<input type="hidden" name="'+this.options.input_name+'_tbc" value="{}">');
 			this.toBeCreated = {};
 			this.inputToBeDeleted = $('<input type="hidden" name="'+this.options.input_name+'_tbd" value="[]">');
 			this.toBeDeleted = [];
+			this.inputToBeAdded = $('<input type="hidden" name="'+this.options.input_name+'_tba" value="[]">');
+			this.toBeAdded = [];
+			this.inputToBeRemoved = $('<input type="hidden" name="'+this.options.input_name+'_tbr" value="[]">');
+			this.toBeRemoved = [];
 			
 			
 			this.element
 				.after(this.inputToBeCreated)
 				.after(this.inputToBeDeleted)				
+				.after(this.inputToBeAdded)				
+				.after(this.inputToBeRemoved)				
 			 	.after('<span style="float:left">&nbsp;&nbsp;&nbsp;<img src="../images/tv-item-last.gif">&nbsp;&nbsp;&nbsp;')
-			 	.after(this.indicator).after(this.createBtn).after('&nbsp;&nbsp;&nbsp')
-			 	.after(this.modifyBtn).after('&nbsp;&nbsp;&nbsp')
-			 	.after(this.deleteBtn);
+			 	.after(this.indicator);
+			for(k in this.options.buttons)
+			{
+				this.element.after(this.oButtons[this.options.buttons[k]]).after('&nbsp;&nbsp;&nbsp;');
+			}
 			
 			this.element.find('.selectList'+this.id).bind('change', function() { me._updateButtons(); });
-			this.deleteBtn.click(function() {
+			this.oButtons['delete'].click(function() {
 				$('.selectList'+me.id+':checked', me.element).each( function() { me._deleteRow($(this)); });
 			});
-			this.createBtn.click(function() {
+			this.oButtons['create'].click(function() {
 				me._createRow();
 			});
-			
-			this.modifyBtn.hide(); //hidden for now since it's not yet implemented
-			
+			this.oButtons['remove'].click(function() {
+				$('.selectList'+me.id+':checked', me.element).each( function() { me._removeRow($(this)); });
+			});
+			this.oButtons['add'].click(function() {
+				me._selectToAdd();
+			});
+						
 			this._updateButtons();
 		},
 	
@@ -94,18 +114,21 @@ $(function()
 			switch(oChecked.length)
 			{
 				case 0:
-					this.deleteBtn.attr('disabled', 'disabled');
-					this.modifyBtn.attr('disabled', 'disabled');
+					this.oButtons['delete'].attr('disabled', 'disabled');
+					this.oButtons['remove'].attr('disabled', 'disabled');
+					this.oButtons['modify'].attr('disabled', 'disabled');
 				break;
 			
 				case 1:
-					this.deleteBtn.removeAttr('disabled');
-					this.modifyBtn.removeAttr('disabled');
+					this.oButtons['delete'].removeAttr('disabled');
+					this.oButtons['remove'].removeAttr('disabled');
+					this.oButtons['modify'].removeAttr('disabled');
 				break;
 				
 				default:
-					this.deleteBtn.removeAttr('disabled');
-					this.modifyBtn.attr('disabled', 'disabled');
+					this.oButtons['delete'].removeAttr('disabled');
+					this.oButtons['remove'].removeAttr('disabled');
+					this.oButtons['modify'].attr('disabled', 'disabled');
 				break;
 			}
 		},
@@ -122,7 +145,7 @@ $(function()
 		},
 		_createRow: function()
 		{
-			this.createBtn.attr('disabled', 'disabled');
+			this.oButtons['create'].attr('disabled', 'disabled');
 			this.indicator.html('<img src="../images/indicator.gif">');
 			oParams = this.options.submit_parameters;
 			oParams.operation = 'createObject';
@@ -147,10 +170,184 @@ $(function()
 					close: function() { me._onDlgClose(); }
 				});
 				me.indicator.html('');
-				me.createBtn.removeAttr('disabled');
+				me.oButtons['create'].removeAttr('disabled');
 				me._updateDlgSize();
 			});
 		},
+		_selectToAdd: function()
+		{
+			this.oButtons['add'].attr('disabled', 'disabled');
+			this.indicator.html('<img src="../images/indicator.gif">');
+			oParams = this.options.submit_parameters;
+			oParams.operation = 'selectObjectsToAdd';
+			oParams['class'] = this.options.class_name;
+			oParams.real_class = '';
+			oParams.att_code = this.options.att_code;
+			oParams.iInputId = this.id;
+			if (this.options.oWizardHelper)
+			{
+				this.options.oWizardHelper.UpdateWizard();
+				oParams.json = this.options.oWizardHelper.ToJSON();
+			}
+			var me = this;
+			$.post(this.options.submit_to, oParams, function(data){
+				me.oDlg = $('<div></div>');
+				$('body').append(me.oDlg);
+				me.oDlg.html(data);
+				me.oDlg.find('form').removeAttr('onsubmit').bind('submit', function() { me._onSearchToAdd(); return false; } );
+				me.oDlg.find('button.cancel').unbind('click').click( function() { me.oDlg.dialog('close'); } );
+				me.oDlg.find('button.ok').unbind('click').click( function() { me._onDoAdd(); } );
+				
+				me.oDlg.dialog({
+					title: me.options.labels['selection_title'],
+					modal: true,
+					width: 'auto',
+					height: 'auto',
+					position: { my: "center", at: "center", of: window },
+					close: function() { me._onDlgClose(); }
+				});
+				me.indicator.html('');
+				me.oButtons['add'].removeAttr('disabled');
+				me._onSearchToAdd();
+				me._updateDlgSize();
+			});
+		},
+		_onSearchToAdd: function()
+		{
+			var oParams = {};
+			// Gather the parameters from the search form
+			$('#SearchFormToAdd_'+this.id+' :input').each( function() {
+					if (this.name != '')
+					{
+						var val = $(this).val(); // supports multiselect as well
+						if (val !== null)
+						{
+							oParams[this.name] = val;					
+						}
+					}
+			});
+			// Gather the already linked target objects
+			oParams.aAlreadyLinked = new Array();
+			$('#'+this.id+' .listResults td input:checkbox').each(function(){
+					iKey = parseInt(this.value, 10); // Numbers are in base 10
+					oParams.aAlreadyLinked.push(iKey);
+				}
+			);			oParams.operation = 'searchObjectsToAdd2';
+			oParams['class'] = this.options.class_name;
+			oParams.real_class = '';
+			oParams.att_code = this.options.att_code;
+			oParams.iInputId = this.id;
+			if (this.options.oWizardHelper)
+			{
+				this.options.oWizardHelper.UpdateWizard();
+				oParams.json = this.options.oWizardHelper.ToJSON();
+			}
+			var me = this;
+			$('#SearchResultsToAdd_'+me.id).block();
+			$.post(this.options.submit_to, oParams, function(data) {
+				
+				$('#SearchResultsToAdd_'+me.id).html(data);
+				$('#SearchResultsToAdd_'+me.id+' .listResults').tableHover();
+				$('#count_'+me.id).change(function() {
+					var c = this.value;
+					me._onUpdateDlgButtons(c);
+				});
+				$('#SearchResultsToAdd_'+me.id).unblock();
+			});
+			//alert("C'est parti mon kiki !");
+			return false; // Stay on the page, no submit
+		},
+		_getSelection: function(sName)
+		{
+			// Gather the parameters from the search form
+			var oMap = {};
+			var oContext = $('#SearchResultsToAdd_'+this.id);
+			var selectionMode = $(':input[name=selectionMode]', oContext);
+			if (selectionMode.length > 0)
+			{
+				// Paginated table retrieve the mode and the exceptions
+				var sMode = selectionMode.val();
+				oMap['selectionMode'] = sMode;
+				$('#fs_SearchFormToAdd_'+this.id+' :input').each(
+						function(i)
+						{
+							oMap[this.name] = this.value;
+						}
+					);
+				$(':input[name^=storedSelection]', oContext).each(function() {
+					if (oMap[this.name] == undefined)
+					{
+						oMap[this.name] = new Array();
+					}
+					oMap[this.name].push(this.value);
+				});
+				// Retrieve the 'filter' definition
+				var table = $('#ResultsToAdd_'+this.id).find('table.listResults')[0];
+				oMap['filter'] = table.config.filter;
+				oMap['extra_params'] = table.config.extra_params;
+			}
+			// Normal table, retrieve all the checked check-boxes
+			$(':checked[name^=selectObject]', oContext).each(
+				function(i)
+				{
+					if ( (this.name != '') && ((this.type != 'checkbox') || (this.checked)) ) 
+					{
+						arrayExpr = /\[\]$/;
+						if (arrayExpr.test(this.name))
+						{
+							// Array
+							if (oMap[this.name] == undefined)
+							{
+								oMap[this.name] = new Array();
+							}
+							oMap[this.name].push(this.value);
+						}
+						else
+						{
+							oMap[this.name] = this.value;
+						}						
+					}
+				}
+			);
+			return oMap;
+		},
+		_onUpdateDlgButtons: function(iCount)
+		{
+			if (iCount > 0)
+			{
+				this.oDlg.find('button.ok').removeAttr('disabled');
+			}
+			else
+			{
+				this.oDlg.find('button.ok').attr('disabled', 'disabled');				
+			}
+		},
+		_onDoAdd:function()
+		{
+			var oParams = this._getSelection('selectObject');
+			this.oDlg.dialog('close');
+			oParams.operation = 'doAddObjects2';
+			oParams['class'] = this.options.class_name;
+			oParams.att_code = this.options.att_code;
+			oParams.iInputId = this.id;
+			var me = this;
+			$.post(this.options.submit_to, oParams, function(data) {
+				var oInserted = $(data);
+				oInserted.find('input:checkbox').each(function() {
+					var iKey = parseInt($(this).val(), 10); // Number in base 10
+					me.toBeAdded.push(iKey);
+					me.toBeRemoved = me._ArrayRemove(me.toBeRemoved, iKey);
+					me.toBeDeleted = me._ArrayRemove(me.toBeDeleted, iKey);
+				});
+				me.inputToBeAdded.val(JSON.stringify(me.toBeAdded));
+				me.inputToBeRemoved.val(JSON.stringify(me.toBeRemoved));
+				me.inputToBeDeleted.val(JSON.stringify(me.toBeDeleted));
+				me.datatable.find('tbody').append(data);
+				me._updateTable();
+				me.indicator.html('');
+				me.oButtons['add'].removeAttr('disabled');
+			});
+		},
 		subclassSelected: function()
 		{
 			var sRealClass = this.oDlg.find('select[name="class"]').val();
@@ -204,14 +401,14 @@ $(function()
 				oParams.tempId = nextIdx;
 				var me = this;
 
-				this.createBtn.attr('disabled', 'disabled');
+				this.oButtons['create'].attr('disabled', 'disabled');
 				this.indicator.html('<img src="../images/indicator.gif">');
 
 				$.post(this.options.submit_to, oParams, function(data){
 					me.datatable.find('tbody').append(data);
 					me._updateTable();
 					me.indicator.html('');
-					me.createBtn.removeAttr('disabled');
+					me.oButtons['create'].removeAttr('disabled');
 				});
 			}
 		},
@@ -227,13 +424,53 @@ $(function()
 			if (iObjKey > 0)
 			{
 				// Existing objet: add it to the "to be deleted" list
-				this.toBeDeleted.push(iObjKey);
-				this.inputToBeDeleted.val(JSON.stringify(this.toBeDeleted));
+				// if it has not just been added now
+				if (this._InArray(this.toBeAdded, iObjKey))
+				{
+					this.toBeAdded = this._ArrayRemove(this.toBeAdded, iObjKey);
+					this.inputToBeAdded.val(JSON.stringify(this.toBeAdded));					
+				}
+				else
+				{
+					this.toBeDeleted.push(iObjKey);					
+					this.inputToBeDeleted.val(JSON.stringify(this.toBeDeleted));
+				}
+			}
+			else
+			{
+				// Object to be created, just remove it from the "to be created" list
+				this.toBeCreated = this._ArrayRemove(this.toBeCreated, iObjKey);
+				this.inputToBeCreated.val(JSON.stringify(this.toBeCreated));
+			}
+			// Now remove the row from the table
+			oRow = oCheckbox.closest('tr');
+			oRow.remove();
+			this._updateButtons();
+			this._updateTable();
+		},
+		_removeRow: function(oCheckbox)
+		{
+			var iObjKey = parseInt(oCheckbox.val(), 10); // Number in base 10
+			
+			if (iObjKey > 0)
+			{
+				// Existing objet: add it to the "to be removed" list
+				// if it has not just been added now
+				if (this._InArray(this.toBeAdded, iObjKey))
+				{
+					this.toBeAdded = this._ArrayRemove(this.toBeAdded, iObjKey);
+					this.inputToBeAdded.val(JSON.stringify(this.toBeAdded));					
+				}
+				else
+				{
+					this.toBeRemoved.push(iObjKey);					
+					this.inputToBeRemoved.val(JSON.stringify(this.toBeRemoved));
+				}
 			}
 			else
 			{
 				// Object to be created, just remove it from the "to be created" list
-				this.toBeCreated[-iObjKey] = undefined;
+				this.toBeCreated = this._ArrayRemove(this.toBeCreated, iObjKey);
 				this.inputToBeCreated.val(JSON.stringify(this.toBeCreated));
 			}
 			// Now remove the row from the table
@@ -241,6 +478,30 @@ $(function()
 			oRow.remove();
 			this._updateButtons();
 			this._updateTable();
+		},
+		_InArray: function(aArrayToSearch, needle)
+		{
+			aRes = [];
+			for(k in aArrayToSearch)
+			{
+				if (aArrayToSearch[k] == needle)
+				{
+					return true;
+				}
+			}
+			return false;
+		},
+		_ArrayRemove: function(aArrayToFilter, needle)
+		{
+			aRes = [];
+			for(k in aArrayToFilter)
+			{
+				if (aArrayToFilter[k] != needle)
+				{
+					aRes.push(aArrayToFilter[k]);
+				}
+			}
+			return aRes;
 		}
 	});	
 });

+ 64 - 1
pages/ajax.render.php

@@ -285,7 +285,68 @@ try
 		$aValues = utils::ReadParam('values', array(), false, 'raw_data');
 		$oPage->SetContentType('text/html');
 		$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iInputId);
-		$oPage->add($oWidget->GetRow($oPage, $sRealClass, $aValues, $iTempId));
+		$oPage->add($oWidget->GetRow($oPage, $sRealClass, $aValues, -$iTempId));
+		break;
+		
+		// ui.linksdirectwidget
+		case 'selectObjectsToAdd':
+		$oPage->SetContentType('text/html');
+		$sClass = utils::ReadParam('class', '', false, 'class');
+		$sJson = utils::ReadParam('json', '', false, 'raw_data');
+		$oObj = null;
+		if ($sJson != '')
+		{
+			$oWizardHelper = WizardHelper::FromJSON($sJson);
+			$oObj = $oWizardHelper->GetTargetObject();
+		}
+		$sRealClass = utils::ReadParam('real_class', '', false, 'class');
+		$sAttCode = utils::ReadParam('att_code', '');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$iCurrObjectId =  utils::ReadParam('iObjId', 0);
+		$oPage->SetContentType('text/html');
+		$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iInputId);
+		$oWidget->GetObjectsSelectionDlg($oPage, $oObj);
+		break;
+			
+		// ui.linksdirectwidget
+		case 'searchObjectsToAdd2':
+		$oPage->SetContentType('text/html');
+		$sClass = utils::ReadParam('class', '', false, 'class');
+		$sRealClass = utils::ReadParam('real_class', '', false, 'class');
+		$sAttCode = utils::ReadParam('att_code', '');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$aAlreadyLinked =  utils::ReadParam('aAlreadyLinked', array());
+		$sJson = utils::ReadParam('json', '', false, 'raw_data');
+		$oObj = null;
+		if ($sJson != '')
+		{
+			$oWizardHelper = WizardHelper::FromJSON($sJson);
+			$oObj = $oWizardHelper->GetTargetObject();
+		}
+		$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iInputId);
+		$oWidget->SearchObjectsToAdd($oPage, $sRealClass, $aAlreadyLinked, $oObj);
+		break;
+		
+		// ui.linksdirectwidget
+		case 'doAddObjects2':
+		$oPage->SetContentType('text/html');
+		$oPage->SetContentType('text/html');
+		$sClass = utils::ReadParam('class', '', false, 'class');
+		$sRealClass = utils::ReadParam('real_class', '', false, 'class');
+		$sAttCode = utils::ReadParam('att_code', '');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$iCurrObjectId =  utils::ReadParam('iObjId', 0);
+		$sFilter = utils::ReadParam('filter', '');
+		if ($sFilter != '')
+		{
+			$oFullSetFilter = DBObjectSearch::unserialize($sFilter);
+		}
+		else
+		{
+			$oFullSetFilter = new DBObjectSearch($sRemoteClass);		
+		}
+		$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iInputId);
+		$oWidget->DoAddObjects($oPage, $oFullSetFilter);	
 		break;
 		
 		////////////////////////////////////////////////////////////
@@ -456,6 +517,8 @@ try
 		$oWidget->DoAddObjects($oPage, $oFullSetFilter, $oObj);	
 		break;
 			
+		////////////////////////////////////////////////////////////
+		
 		case 'wizard_helper_preview':
 		$oPage->SetContentType('text/html');
 		$sJson = utils::ReadParam('json_obj', '', false, 'raw_data');

+ 1 - 0
setup/compiler.class.inc.php

@@ -425,6 +425,7 @@ EOF;
 			'add_only' => 'LINKSET_EDITMODE_ADDONLY',
 			'actions' => 'LINKSET_EDITMODE_ACTIONS',
 			'in_place' => 'LINKSET_EDITMODE_INPLACE',
+			'add_remove' => 'LINKSET_EDITMODE_ADDREMOVE',
 		);
 	
 		if (!array_key_exists($sEditMode, $aXmlToPHP))