Kaynağa Gözat

Implemented a new (optional) UI for managing 1:n linksets.

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@2290 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 12 yıl önce
ebeveyn
işleme
5ad1d358d5

+ 28 - 2
application/ajaxwebpage.class.inc.php

@@ -113,7 +113,7 @@ class ajax_page extends WebPage
         }
 		if (count($this->m_aTabs) > 0)
 		{
-					$this->add_ready_script(
+			$this->add_ready_script(
 <<<EOF
 			// The "tab widgets" to handle.
 			var tabs = $('div[id^=tabbedContent]');
@@ -183,7 +183,33 @@ EOF
 			$this->s_content = str_replace("\$Tabs:$sTabContainerName\$", $sTabs, $this->s_content);
 			$container_index++;
 		}
-	
+		
+		// Additional UI widgets to be activated inside the ajax fragment ??
+    	if ($this->sContentType == 'text/html')
+		{
+			$this->add_ready_script(
+<<<EOF
+	$(".date-pick").datepicker({
+			showOn: 'button',
+			buttonImage: '../images/calendar.png',
+			buttonImageOnly: true,
+			dateFormat: 'yy-mm-dd',
+			constrainInput: false,
+			changeMonth: true,
+			changeYear: true
+		});
+	$(".datetime-pick").datepicker({
+			showOn: 'button',
+			buttonImage: '../images/calendar.png',
+			buttonImageOnly: true,
+			dateFormat: 'yy-mm-dd 00:00:00',
+			constrainInput: false,
+			changeMonth: true,
+			changeYear: true
+		});
+EOF
+			);
+		}	
         $s_captured_output = ob_get_contents();
         ob_end_clean();
         if (($this->sContentType == 'text/html') &&  ($this->sContentDisposition == 'inline'))

+ 97 - 50
application/cmdbabstract.class.inc.php

@@ -36,6 +36,7 @@ require_once(APPROOT.'/application/applicationextension.inc.php');
 require_once(APPROOT.'/application/utils.inc.php');
 require_once(APPROOT.'/application/applicationcontext.class.inc.php');
 require_once(APPROOT.'/application/ui.linkswidget.class.inc.php');
+require_once(APPROOT.'/application/ui.linksdirectwidget.class.inc.php');
 require_once(APPROOT.'/application/ui.passwordwidget.class.inc.php');
 require_once(APPROOT.'/application/ui.extkeywidget.class.inc.php');
 require_once(APPROOT.'/application/ui.htmleditorwidget.class.inc.php');
@@ -266,58 +267,25 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 			if ($bEditMode && (!$bReadOnly))
 			{
 				$sInputId = $this->m_iFormId.'_'.$sAttCode;
-				if (get_class($oAttDef) == 'AttributeLinkedSet')
-				{
-					// 1:n links
-					$sTargetClass = $oAttDef->GetLinkedClass();
-					if ($this->IsNew())
-					{
-						$oPage->p(Dict::Format('UI:BeforeAdding_Class_ObjectsSaveThisObject', MetaModel::GetName($sTargetClass)));
-					}
-					else
-					{
-						$oPage->p(MetaModel::GetClassIcon($sTargetClass)."&nbsp;".$oAttDef->GetDescription());
-	
-						$oFilter = new DBObjectSearch($sTargetClass);
-						$oFilter->AddCondition($oAttDef->GetExtKeyToMe(), $this->GetKey(),'=');
-	
-						$aDefaults = array($oAttDef->GetExtKeyToMe() => $this->GetKey());
-						$oAppContext = new ApplicationContext();
-						foreach($oAppContext->GetNames() as $sKey)
-						{
-							// The linked object inherits the parent's value for the context
-							if (MetaModel::IsValidAttCode($sClass, $sKey))
-							{
-								$aDefaults[$sKey] = $this->Get($sKey);
-							}
-						}
-						$aParams = array(
-							'target_attr' => $oAttDef->GetExtKeyToMe(),
-							'object_id' => $this->GetKey(),
-							'menu' => true,
-							'default' => $aDefaults,
-							'table_id' => $sClass.'_'.$sAttCode,
-						);
-	
-						$oBlock = new DisplayBlock($oFilter, 'list', false);
-						$oBlock->Display($oPage, $sInputId, $aParams);
-					}
-				}
-				else // get_class($oAttDef) == 'AttributeLinkedSetIndirect'
+
+				$sLinkedClass = $oAttDef->GetLinkedClass();
+				if ($oAttDef->IsIndirect())
 				{
-					// n:n links
-					$sLinkedClass = $oAttDef->GetLinkedClass();
 					$oLinkingAttDef = 	MetaModel::GetAttributeDef($sLinkedClass, $oAttDef->GetExtKeyToRemote());
 					$sTargetClass = $oLinkingAttDef->GetTargetClass();
-					$oPage->p(MetaModel::GetClassIcon($sTargetClass)."&nbsp;".$oAttDef->GetDescription().'<span id="busy_'.$sInputId.'"></span>');
-
-					$sValue = $this->Get($sAttCode);
-					$sDisplayValue = ''; // not used
-					$aArgs = array('this' => $this);
-					$sHTMLValue = "<span id=\"field_{$sInputId}\">".self::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $sValue, $sDisplayValue, $sInputId, '', $iFlags, $aArgs).'</span>';
-					$aFieldsMap[$sAttCode] = $sInputId;
-					$oPage->add($sHTMLValue);
 				}
+				else
+				{
+					$sTargetClass = $sLinkedClass;
+				}
+				$oPage->p(MetaModel::GetClassIcon($sTargetClass)."&nbsp;".$oAttDef->GetDescription().'<span id="busy_'.$sInputId.'"></span>');
+
+				$oValue = $this->Get($sAttCode);
+				$sDisplayValue = ''; // not used
+				$aArgs = array('this' => $this);
+				$sHTMLValue = "<span id=\"field_{$sInputId}\">".self::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $oValue, $sDisplayValue, $sInputId, '', $iFlags, $aArgs).'</span>';
+				$aFieldsMap[$sAttCode] = $sInputId;
+				$oPage->add($sHTMLValue);
 			}
 			else
 			{
@@ -414,6 +382,7 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 		$iInputId = 0;
 		$aFieldsMap = array();
 		$aFieldsComments = (isset($aExtraParams['fieldsComments'])) ? $aExtraParams['fieldsComments'] : array();
+		$aExtraFlags = (isset($aExtraParams['fieldsFlags'])) ? $aExtraParams['fieldsFlags'] : array();
 		$bFieldComments = (count($aFieldsComments) > 0);
 		
 		foreach($aDetailsStruct as $sTab => $aCols )
@@ -474,6 +443,11 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 						{
 							$iFlags = $iFlags & ~OPT_ATT_READONLY; // Mandatory fields cannot be read-only when creating an object
 						}
+						if (array_key_exists($sAttCode, $aExtraFlags))
+						{
+							// the caller may override some flags if needed
+							$iFlags = $iFlags | $aExtraFlags[$sAttCode];
+						}
 						$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
 						if ( (!$oAttDef->IsLinkSet()) && (($iFlags & OPT_ATT_HIDDEN) == 0))
 						{
@@ -1608,12 +1582,19 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 				break;
 
 				case 'LinkedSet':
+					if ($oAttDef->IsIndirect())
+					{
+						$oWidget = new UILinksWidget($sClass, $sAttCode, $iId, $sNameSuffix, $oAttDef->DuplicatesAllowed(), $aArgs);
+					}
+					else
+					{
+						$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iId, $sNameSuffix, $aArgs);
+					}					
 					$aEventsList[] ='validate';
 					$aEventsList[] ='change';
-					$oWidget = new UILinksWidget($sClass, $sAttCode, $iId, $sNameSuffix, $oAttDef->DuplicatesAllowed(), $aArgs);
 					$oObj = isset($aArgs['this']) ? $aArgs['this'] : null;
 					$sHTMLValue = $oWidget->Display($oPage, $value, array(), $sFormPrefix, $oObj);
-				break;
+					break;
 							
 				case 'Document':
 					$aEventsList[] ='validate';
@@ -2434,6 +2415,50 @@ EOF
 					$this->Set($sAttCode, $iValue);
 				}
 			}
+			else if (($oAttDef->GetEditClass() == 'LinkedSet') && !$oAttDef->IsIndirect() && ($oAttDef->GetEditMode() == LINKSET_EDITMODE_INPLACE))
+			{
+				$oLinkset = $this->Get($sAttCode);
+				$sLinkedClass = $oLinkset->GetClass();
+				$aObjSet = array();
+				$oLinkset->Rewind();
+				$bModified = false;
+				while($oLink = $oLinkset->Fetch())
+				{
+					if (in_array($oLink->GetKey(), $value['to_be_deleted']))
+					{
+						// The link is to be deleted, don't copy it in the array
+						$bModified = true;
+					}
+					else
+					{
+						$aObjSet[] = $oLink;
+					}
+				}
+
+				if (array_key_exists('to_be_created', $value) && (count($value['to_be_created']) > 0))
+				{
+					// Now handle the lniks to be created
+					foreach($value['to_be_created'] as $aData)
+					{
+						$sSubClass = $aData['class'];
+						if ( ($sLinkedClass == $sSubClass) || (is_subclass_of($sSubClass, $sLinkedClass)) )
+						{
+							$aObjData = $aData['data'];
+							
+							$oLink = new $sSubClass;
+							$oLink->UpdateObjectFromArray($aObjData);
+							$aObjSet[] = $oLink;
+							$bModified = true;
+						}
+					}
+				}
+
+				if ($bModified)
+				{
+					$oNewSet = DBObjectSet::FromArray($oLinkset->GetClass(), $aObjSet);
+					$this->Set($sAttCode, $oNewSet);
+				}		
+			}
 			else
 			{
 				if (!is_null($value))
@@ -2466,6 +2491,28 @@ EOF
 			{
 				$value = array('fcontents' => utils::ReadPostedDocument("attr_{$sFormPrefix}{$sAttCode}", 'fcontents'));
 			}
+			else if (($oAttDef->GetEditClass() == 'LinkedSet') && !$oAttDef->IsIndirect() && ($oAttDef->GetEditMode() == LINKSET_EDITMODE_INPLACE))
+			{
+				$aRawToBeCreated = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbc", '{}', 'raw_data'), true);
+				$aToBeCreated = array();
+				foreach($aRawToBeCreated as $aData)
+				{
+					$sSubFormPrefix = $aData['formPrefix'];
+					$sObjClass = $aData['class'];
+					$aObjData = array();
+					foreach($aData as $sKey => $value)
+					{
+						if (preg_match("/^attr_$sSubFormPrefix(.*)$/", $sKey, $aMatches))
+						{
+							$aObjData[$aMatches[1]] = $value;
+						}
+					}
+					$aToBeCreated[] = array('class' => $sObjClass, 'data' => $aObjData);
+				}
+				
+				$value = array('to_be_created' => $aToBeCreated, 
+							   'to_be_deleted' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbd", '[]', 'raw_data'), true) );
+			}
 			else
 			{
 				$value = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", null, 'raw_data');

+ 3 - 1
application/displayblock.class.inc.php

@@ -568,11 +568,13 @@ class DisplayBlock
 					{
 						if ((UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES))
 						{
+							$sLinkTarget = '';
 							$oAppContext = new ApplicationContext();
 							$sParams = $oAppContext->GetForLink();
 							// 1:n links, populate the target object as a default value when creating a new linked object
 							if (isset($aExtraParams['target_attr']))
 							{
+								$sLinkTarget = ' target="_blank" ';
 								$aExtraParams['default'][$aExtraParams['target_attr']] = $aExtraParams['object_id'];
 							}
 							$sDefault = '';
@@ -584,7 +586,7 @@ class DisplayBlock
 								}
 							}
 							
-							$sHtml .= $oPage->GetP("<a href=\"".utils::GetAbsoluteUrlAppRoot()."pages/UI.php?operation=new&class=$sClass&$sParams{$sDefault}\">".Dict::Format('UI:ClickToCreateNew', Metamodel::GetName($sClass))."</a>\n");
+							$sHtml .= $oPage->GetP("<a{$sLinkTarget} href=\"".utils::GetAbsoluteUrlAppRoot()."pages/UI.php?operation=new&class=$sClass&$sParams{$sDefault}\">".Dict::Format('UI:ClickToCreateNew', Metamodel::GetName($sClass))."</a>\n");
 						}
 					}
 				}

+ 1 - 18
application/itopwebpage.class.inc.php

@@ -281,7 +281,7 @@ EOF
 			changeMonth: true,
 			changeYear: true
 		});
-		$(".datetime-pick").datepicker({
+	$(".datetime-pick").datepicker({
 			showOn: 'button',
 			buttonImage: '../images/calendar.png',
 			buttonImageOnly: true,
@@ -290,23 +290,6 @@ EOF
 			changeMonth: true,
 			changeYear: true
 		});
-	// Restore the persisted sortable order, for all sortable lists... if any
-	$('.sortable').each(function()
-	{
-		var sTemp = GetUserPreference(this.id+'_order', undefined);
-		if (sTemp != undefined)
-		{
-			var aSerialized = sTemp.split(',');
-			var sortable = $(this);
-			$.each(aSerialized, function(i,v) {
-			  var item = $('#menu_'+v);
-			  if (item.length >  0) // Check that the menu exists
-			  {
-					sortable.append(item);
-			  }
-			});
-		}
-	});
 
 	// Make sortable, everything that claims to be sortable
 	$('.sortable').sortable( {axis: 'y', cursor: 'move', handle: '.drag_handle', stop: function()

+ 238 - 0
application/ui.linksdirectwidget.class.inc.php

@@ -0,0 +1,238 @@
+<?php
+// Copyright (C) 2012 Combodo SARL
+//
+//   This program is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU General Public License as published by
+//   the Free Software Foundation; version 3 of the License.
+//
+//   This program is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU General Public License for more details.
+//
+//   You should have received a copy of the GNU General Public License
+//   along with this program; if not, write to the Free Software
+//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+class UILinksWidgetDirect
+{
+	protected $sClass;
+	protected $sAttCode;
+	protected $sInputid;
+	protected $sNameSuffix;
+	protected $sLinkedClass;
+	
+	public function __construct($sClass, $sAttCode, $sInputId, $sNameSuffix = '')
+	{
+		$this->sClass = $sClass;
+		$this->sAttCode = $sAttCode;
+		$this->sInputid = $sInputId;
+		$this->sNameSuffix = $sNameSuffix;
+		$this->aZlist = array();
+		$this->sLinkedClass = '';
+		
+		// Compute the list of attributes visible from the given objet:
+		// All the attributes from the "list" Zlist of the Link class except
+		// the ExternalKey that points to the current object and its related external fields
+		$oLinksetDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+		$this->sLinkedClass = $oLinksetDef->GetLinkedClass();
+		$sExtKeyToMe = $oLinksetDef->GetExtKeyToMe();
+		$aZList = MetaModel::FlattenZList(MetaModel::GetZListItems($this->sLinkedClass, 'list'));
+		foreach($aZList as $sLinkedAttCode)
+		{
+			if ($sLinkedAttCode != $sExtKeyToMe)
+			{
+				$oAttDef = MetaModel::GetAttributeDef($this->sLinkedClass, $sLinkedAttCode);
+				
+				if (!$oAttDef->IsExternalField() || ($oAttDef->GetKeyAttCode() != $sExtKeyToMe) )
+				{
+					$this->aZlist[] = $sLinkedAttCode;
+				}
+			}
+		}
+		
+	}
+	
+	public function Display(WebPage $oPage, DBObjectSet $oValue, $aArgs = array(), $sFormPrefix, $oCurrentObj)
+	{
+		$oLinksetDef = MetaModel::GetAttributeDef($this->sClass, $this->sAttCode);
+		switch($oLinksetDef->GetEditMode())
+		{
+			case LINKSET_EDITMODE_NONE: // The linkset is read-only
+			$this->DisplayAsBlock($oPage, $oValue, $aArgs = array(), $sFormPrefix, $oCurrentObj, false /* bDisplayMenu*/);
+			break;
+			
+			case LINKSET_EDITMODE_ADDONLY: // The only possible action is to open (in a new window) the form to create a new object
+			if ($oCurrentObj && !$oCurrentObj->IsNew())
+			{
+				$sTargetClass = $oLinksetDef->GetLinkedClass();
+				$sExtKeyToMe = $oLinksetDef->GetExtKeyToMe();
+				$sDefault = "default[$sExtKeyToMe]=".$oCurrentObj->GetKey();
+				$oAppContext = new ApplicationContext();
+				$sParams = $oAppContext->GetForLink();
+				$oPage->p("<a target=\"_blank\" href=\"".utils::GetAbsoluteUrlAppRoot()."pages/UI.php?operation=new&class=$sTargetClass&$sParams{$sDefault}\">".Dict::Format('UI:ClickToCreateNew', Metamodel::GetName($sTargetClass))."</a>\n");
+			}
+			$this->DisplayAsBlock($oPage, $oValue, $aArgs = array(), $sFormPrefix, $oCurrentObj, false /* bDisplayMenu*/);
+			break;
+			
+			case LINKSET_EDITMODE_INPLACE: // The whole linkset can be edited 'in-place'
+			$this->DisplayEditInPlace($oPage, $oValue, $aArgs, $sFormPrefix, $oCurrentObj);
+			break;
+			
+			case LINKSET_EDITMODE_ACTIONS:
+			default:
+			$this->DisplayAsBlock($oPage, $oValue, $aArgs = array(), $sFormPrefix, $oCurrentObj, true /* bDisplayMenu*/);
+		}
+	}
+	
+	protected function DisplayAsBlock(WebPage $oPage, DBObjectSet $oValue, $aArgs = array(), $sFormPrefix, $oCurrentObj, $bDisplayMenu)
+	{
+		$oLinksetDef = MetaModel::GetAttributeDef($this->sClass, $this->sAttCode);
+		$sTargetClass = $oLinksetDef->GetLinkedClass();
+		if ($oCurrentObj && $oCurrentObj->IsNew() && $bDisplayMenu)
+		{
+			$oPage->p(Dict::Format('UI:BeforeAdding_Class_ObjectsSaveThisObject', MetaModel::GetName($sTargetClass)));
+		}
+		else
+		{
+			$oFilter = new DBObjectSearch($sTargetClass);
+			$oFilter->AddCondition($oLinksetDef->GetExtKeyToMe(), $oCurrentObj->GetKey(),'=');
+
+			$aDefaults = array($oLinksetDef->GetExtKeyToMe() => $oCurrentObj->GetKey());
+			$oAppContext = new ApplicationContext();
+			foreach($oAppContext->GetNames() as $sKey)
+			{
+				// The linked object inherits the parent's value for the context
+				if (MetaModel::IsValidAttCode($this->sClass, $sKey) && $oCurrentObj)
+				{
+					$aDefaults[$sKey] = $oCurrentObj->Get($sKey);
+				}
+			}
+			$aParams = array(
+				'target_attr' => $oLinksetDef->GetExtKeyToMe(),
+				'object_id' => $oCurrentObj ? $oCurrentObj->GetKey() : null,
+				'menu' => $bDisplayMenu,
+				'default' => $aDefaults,
+				'table_id' => $this->sClass.'_'.$this->sAttCode,
+			);
+
+			$oBlock = new DisplayBlock($oFilter, 'list', false);
+			$oBlock->Display($oPage, $this->sInputid, $aParams);
+		}	
+	}
+	
+	protected function DisplayEditInPlace(WebPage $oPage, DBObjectSet $oValue, $aArgs = array(), $sFormPrefix, $oCurrentObj)
+	{
+		$aAttribs = $this->GetTableConfig();
+		
+		$oValue->Rewind();
+		$oPage->add('<table class="listContainer" id="'.$this->sInputid.'"><tr><td>');
+
+		$aData = array();
+		while($oLinkObj = $oValue->Fetch())
+		{
+			$aRow = array();
+			$aRow['form::select'] = '<input type="checkbox" class="selectList'.$this->sInputid.'" value="'.$oLinkObj->GetKey().'"/>';
+			foreach($this->aZlist as $sLinkedAttCode)
+			{
+				$aRow[$sLinkedAttCode] = $oLinkObj->GetAsHTML($sLinkedAttCode);
+			}
+			$aData[] = $aRow;
+		}
+		$oPage->table($aAttribs, $aData);
+		$oPage->add('</td></tr></table>'); //listcontainer
+		$sInputName = $sFormPrefix.'attr_'.$this->sAttCode;
+		$oPage->add_ready_script("$('#{$this->sInputid}').directlinks({class_name: '$this->sClass', att_code: '$this->sAttCode', input_name:'$sInputName' });");
+	}
+	
+	public function GetObjectCreationDlg(WebPage $oPage, $sProposedRealClass = '')
+	{
+		// For security reasons: check that the "proposed" class is actually a subclass of the linked class
+		// and that the current user is allowed to create objects of this class
+		$sRealClass = '';
+		$oPage->add('<div class="wizContainer" style="vertical-align:top;"><div>');
+		$aSubClasses = MetaModel::EnumChildClasses($this->sLinkedClass, ENUM_CHILD_CLASSES_ALL); // Including the specified class itself
+		$aPossibleClasses = array();
+		foreach($aSubClasses as $sCandidateClass)
+		{
+			if (!MetaModel::IsAbstract($sCandidateClass) && (UserRights::IsActionAllowed($sCandidateClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES))
+			{
+				if ($sCandidateClass == $sProposedRealClass)
+				{
+					$sRealClass = $sProposedRealClass;
+				}
+				$aPossibleClasses[$sCandidateClass] = MetaModel::GetName($sCandidateClass);
+			}
+		}
+		// Only one of the subclasses can be instantiated...
+		if (count($aPossibleClasses) == 1)
+		{
+			$aKeys = array_keys($aPossibleClasses);
+			$sRealClass = $aKeys[0];
+		}
+		
+		if ($sRealClass != '')
+		{
+			$oPage->add("<h1>".MetaModel::GetClassIcon($sRealClass)."&nbsp;".Dict::Format('UI:CreationTitle_Class', MetaModel::GetName($sRealClass))."</h1>\n");
+			$oLinksetDef = MetaModel::GetAttributeDef($this->sClass, $this->sAttCode);
+			$sExtKeyToMe = $oLinksetDef->GetExtKeyToMe();
+			$aFieldFlags = array( $sExtKeyToMe => OPT_ATT_HIDDEN);
+		 	cmdbAbstractObject::DisplayCreationForm($oPage, $sRealClass, null, array(), array('formPrefix' => $this->sInputid, 'noRelations' => true, 'fieldsFlags' => $aFieldFlags));	
+		}
+		else
+		{
+			$sClassLabel = MetaModel::GetName($this->sLinkedClass);
+			$oPage->add('<p>'.Dict::Format('UI:SelectTheTypeOf_Class_ToCreate', $sClassLabel));
+			$oPage->add('<nobr><select name="class">');
+			asort($aPossibleClasses);
+			foreach($aPossibleClasses as $sClassName => $sClassLabel)
+			{
+				$oPage->add("<option value=\"$sClassName\">$sClassLabel</option>");
+			}
+			$oPage->add('</select>');
+			$oPage->add('&nbsp; <button type="button" onclick="$(\'#'.$this->sInputid.'\').directlinks(\'subclassSelected\');">'.Dict::S('UI:Button:Apply').'</button><span class="indicator" style="display:inline-block;width:16px"></span></nobr></p>');
+		}
+		$oPage->add('</div></div>');
+	}
+	
+	public function GetObjectModificationDlg()
+	{
+		
+	}
+	
+	protected function GetTableConfig()
+	{
+		$aAttribs = array();
+		$aAttribs['form::select'] = array('label' => "<input type=\"checkbox\" onClick=\"CheckAll('.selectList{$this->sInputid}:not(:disabled)', this.checked);\" class=\"checkAll\"></input>", 'description' => Dict::S('UI:SelectAllToggle+'));
+
+		foreach($this->aZlist as $sLinkedAttCode)
+		{
+			$oAttDef = MetaModel::GetAttributeDef($this->sLinkedClass, $sLinkedAttCode);
+			$aAttribs[$sLinkedAttCode] = array('label' => MetaModel::GetLabel($this->sLinkedClass, $sLinkedAttCode), 'description' => $oAttDef->GetOrderByHint());
+		}
+		return $aAttribs;	
+	}
+	public function GetRow($oPage, $sRealClass, $aValues, $iTempId)
+	{
+		$aAttribs = $this->GetTableConfig();
+		if ($sRealClass == '')
+		{
+			$sRealClass = $this->sLinkedClass;
+		}
+		$oLinkObj = new $sRealClass();
+		$oLinkObj->UpdateObjectFromPostedForm($this->sInputid);
+		
+		$aRow = array();
+		$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);
+	}
+	
+	public function UpdateFromArray($oObj, $aData)
+	{
+		
+	}
+}

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

@@ -85,6 +85,10 @@ define('LINKSET_TRACKING_LIST', 1); // Do track added/removed items
 define('LINKSET_TRACKING_DETAILS', 2); // Do track modified items
 define('LINKSET_TRACKING_ALL', 3); // Do track added/removed/modified items
 
+define('LINKSET_EDITMODE_NONE', 0); // The linkset cannot be edited at all from inside this object
+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
 
 
 /**
@@ -563,7 +567,7 @@ class AttributeLinkedSet extends AttributeDefinition
 		return array_merge(parent::ListExpectedParams(), array("allowed_values", "depends_on", "linked_class", "ext_key_to_me", "count_min", "count_max"));
 	}
 
-	public function GetEditClass() {return "List";}
+	public function GetEditClass() {return "LinkedSet";}
 
 	public function IsWritable() {return true;} 
 	public function IsLinkSet() {return true;} 
@@ -593,6 +597,11 @@ class AttributeLinkedSet extends AttributeDefinition
 		return $this->GetOptional('tracking_level', LINKSET_TRACKING_LIST);
 	}
 
+	public function GetEditMode()
+	{
+		return $this->GetOptional('edit_mode', LINKSET_EDITMODE_ACTIONS);
+	}
+	
 	public function GetLinkedClass() {return $this->Get('linked_class');}
 	public function GetExtKeyToMe() {return $this->Get('ext_key_to_me');}
 

+ 249 - 0
js/linksdirectwidget.js

@@ -0,0 +1,249 @@
+// jQuery UI style "widget" for managing 1:n links "in-place"
+$(function()
+{
+	// the widget definition, where "itop" is the namespace,
+	// "directlinks" the widget name
+	$.widget( "itop.directlinks",
+	{
+		// default options
+		options:
+		{
+			input_name: '',
+			class_name: '',
+			att_code: '',
+			submit_to: GetAbsoluteUrlAppRoot()+'pages/ajax.render.php',
+			submit_parameters: {},
+			labels: { 'delete': 'Delete',
+				  	  modify: 'Modify...' , 
+				  	  creation_title: 'Creation of a new object...' , 
+					  create: 'Create...'
+					}
+		},
+	
+		// the constructor
+		_create: function()
+		{
+			var me = this;
+			this.id = this.element.attr('id');
+
+			this.element
+			.addClass('itop-directlinks');
+			
+			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>');
+			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.element
+				.after(this.inputToBeCreated)
+				.after(this.inputToBeDeleted)				
+			 	.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);
+			
+			this.element.find('.selectList'+this.id).bind('change', function() { me._updateButtons(); });
+			this.deleteBtn.click(function() {
+				$('.selectList'+me.id+':checked', me.element).each( function() { me._deleteRow($(this)); });
+			});
+			this.createBtn.click(function() {
+				me._createRow();
+			});
+			
+			this.modifyBtn.hide(); //hidden for now since it's not yet implemented
+			
+			this._updateButtons();
+		},
+	
+		// called when created, and later when changing options
+		_refresh: function()
+		{
+			this._updateButtons();
+		},
+		// events bound via _bind are removed automatically
+		// revert other modifications here
+		destroy: function()
+		{
+			this.element
+			.removeClass('itop-directlinks');
+			
+			// call the original destroy method since we overwrote it
+			$.Widget.prototype.destroy.call( this );			
+		},
+		// _setOptions is called with a hash of all options that are changing
+		_setOptions: function()
+		{
+			// in 1.9 would use _superApply
+			$.Widget.prototype._setOptions.apply( this, arguments );
+		},
+		// _setOption is called for each individual option that is changing
+		_setOption: function( key, value )
+		{
+			// in 1.9 would use _super
+			$.Widget.prototype._setOption.call( this, key, value );
+			
+			if (key == 'fields') this._refresh();
+		},
+		_updateButtons: function()
+		{
+			var oChecked = $('.selectList'+this.id+':checked', this.element);
+			switch(oChecked.length)
+			{
+				case 0:
+					this.deleteBtn.attr('disabled', 'disabled');
+					this.modifyBtn.attr('disabled', 'disabled');
+				break;
+			
+				case 1:
+					this.deleteBtn.removeAttr('disabled');
+					this.modifyBtn.removeAttr('disabled');
+				break;
+				
+				default:
+					this.deleteBtn.removeAttr('disabled');
+					this.modifyBtn.attr('disabled', 'disabled');
+				break;
+			}
+		},
+		_updateTable: function()
+		{
+			var me = this;
+			this.datatable.trigger("update").trigger("applyWidgets");
+			this.datatable.tableHover();
+			this.datatable.find('.selectList'+this.id).bind('change', function() { me._updateButtons(); });
+		},
+		_updateDlgSize: function()
+		{
+			this.oDlg.dialog('option', { position: { my: "center", at: "center", of: window }});
+		},
+		_createRow: function()
+		{
+			this.createBtn.attr('disabled', 'disabled');
+			this.indicator.html('<img src="../images/indicator.gif">');
+			oParams = this.options.submit_parameters;
+			oParams.operation = 'createObject';
+			oParams['class'] = this.options.class_name;
+			oParams.real_class = '';
+			oParams.att_code = this.options.att_code;
+			oParams.iInputId = this.id;
+			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._onCreateRow(); return false; } );
+				me.oDlg.find('button.cancel').unbind('click').click( function() { me.oDlg.dialog('close'); } );
+				
+				me.oDlg.dialog({
+					title: me.options.labels['creation_title'],
+					modal: true,
+					width: 'auto',
+					height: 'auto',
+					position: { my: "center", at: "center", of: window },
+					close: function() { me._onDlgClose(); }
+				});
+				me.indicator.html('');
+				me.createBtn.removeAttr('disabled');
+				me._updateDlgSize();
+			});
+		},
+		subclassSelected: function()
+		{
+			var sRealClass = this.oDlg.find('select[name="class"]').val();
+			oParams = this.options.submit_parameters;
+			oParams.operation = 'createObject';
+			oParams['class'] = this.options.class_name;
+			oParams.real_class = sRealClass;
+			oParams.att_code = this.options.att_code;
+			oParams.iInputId = this.id;
+			var me = this;
+			me.oDlg.find('button').attr('disabled', 'disabled');
+			me.oDlg.find('span.indicator').html('<img src="../images/indicator.gif">');
+			$.post(this.options.submit_to, oParams, function(data){
+				me.oDlg.html(data);
+				me.oDlg.find('form').removeAttr('onsubmit').bind('submit', function() { me._onCreateRow(); return false; } );
+				me.oDlg.find('button.cancel').unbind('click').click( function() { me.oDlg.dialog('close'); } );
+				me._updateDlgSize();				
+			});
+		},
+		_onCreateRow: function()
+		{
+			// Validate the form
+			var sFormId = this.oDlg.find('form').attr('id');
+			if (CheckFields(sFormId, true))
+			{
+				// Gather the values from the form
+				oParams = this.options.submit_parameters;
+				var oValues = {};
+				this.oDlg.find(':input').each( function() {
+					if (this.name != '')
+					{
+						oParams[this.name] = this.value;
+						oValues[this.name] = this.value;
+					}
+				});
+				var nextIdx = 0;
+				for(k in this.toBeCreated)
+				{
+					nextIdx++;
+				}
+				nextIdx++;
+				this.toBeCreated[nextIdx] = oValues;
+				this.inputToBeCreated.val(JSON.stringify(this.toBeCreated));
+				this.oDlg.dialog('close');
+				
+				oParams = this.options.submit_parameters;
+				oParams.operation = 'getLinksetRow';
+				oParams['class'] = this.options.class_name;
+				oParams.att_code = this.options.att_code;
+				oParams.iInputId = this.id;
+				oParams.tempId = nextIdx;
+				var me = this;
+
+				this.createBtn.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');
+				});
+			}
+		},
+		_onDlgClose: function()
+		{
+			this.oDlg.remove();
+			this.oDlg = null;
+		},
+		_deleteRow: function(oCheckbox)
+		{
+			var iObjKey = parseInt(oCheckbox.val(), 10); // Number in base 10
+			
+			if (iObjKey > 0)
+			{
+				// Existing objet: add it to the "to be deleted" list
+				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[-iObjKey] = undefined;
+				this.inputToBeCreated.val(JSON.stringify(this.toBeCreated));
+			}
+			// Now remove the row from the table
+			oRow = oCheckbox.closest('tr');
+			oRow.remove();
+			this._updateButtons();
+			this._updateTable();
+		}
+	});	
+});

+ 1 - 0
pages/UI.php

@@ -532,6 +532,7 @@ try
 		$oP->add_linked_script("../js/wizardhelper.js");
 		$oP->add_linked_script("../js/wizard.utils.js");
 		$oP->add_linked_script("../js/linkswidget.js");
+		$oP->add_linked_script("../js/linksdirectwidget.js");
 		$oP->add_linked_script("../js/extkeywidget.js");
 		$oP->add_linked_script("../js/jquery.blockUI.js");
 		break;		

+ 24 - 0
pages/ajax.render.php

@@ -238,6 +238,30 @@ try
 		$oWidget->SearchObjectsToAdd($oPage, $sRemoteClass, $aAlreadyLinked);	
 		break;
 		
+		//ui.linksdirectwidget
+		case 'createObject':
+		$sClass = utils::ReadParam('class', '', false, 'class');
+		$sRealClass = utils::ReadParam('real_class', '', false, 'class');
+		$sAttCode = utils::ReadParam('att_code', '');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$oPage->SetContentType('text/html');
+		$oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iInputId);
+		$oWidget->GetObjectCreationDlg($oPage, $sRealClass);
+		break;
+		
+		// ui.linksdirectwidget
+		case 'getLinksetRow':
+		$sClass = utils::ReadParam('class', '', false, 'class');
+		$sRealClass = utils::ReadParam('real_class', '', false, 'class');
+		$sAttCode = utils::ReadParam('att_code', '');
+		$iInputId = utils::ReadParam('iInputId', '');
+		$iTempId = utils::ReadParam('tempId', '');
+		$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));
+		break;
+		
 		////////////////////////////////////////////////////////////
 		
 		// ui.extkeywidget

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

@@ -307,7 +307,28 @@ EOF;
 		return $aXmlToPHP[$sTrackingLevel];
 	}
 
+	/**
+	 * Helper to format the edit-mode for direct linkset
+	 * @param string $sEditMode Value set from within the XML
+	 * Returns string PHP flag
+	 */ 
+	protected function EditModeToPHP($sEditMode)
+	{
+		static $aXmlToPHP = array(
+			'none' => 'LINKSET_EDITMODE_NONE',
+			'add_only' => 'LINKSET_EDITMODE_ADDONLY',
+			'actions' => 'LINKSET_EDITMODE_ACTIONS',
+			'in_place' => 'LINKSET_EDITMODE_INPLACE',
+		);
+	
+		if (!array_key_exists($sEditMode, $aXmlToPHP))
+		{
+			throw new exception("Edit mode: unknown value '$sTrackingLevel'");
+		}
+		return $aXmlToPHP[$sEditMode];
+	}
 
+	
 	/**
 	 * Format a path (file or url) as an absolute path or relative to the module or the app
 	 */ 
@@ -584,6 +605,11 @@ EOF;
 				{
 					$aParameters['tracking_level'] = $this->TrackingLevelToPHP($sTrackingLevel);
 				}
+				$sEditMode = $oField->GetChildText('edit_mode');
+				if (!is_null($sEditMode))
+				{
+					$aParameters['edit_mode'] = $this->EditModeToPHP($sEditMode);
+				}
 				$aParameters['depends_on'] = $sDependencies;
 			}
 			elseif ($sAttType == 'AttributeExternalKey')