diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index abe3116a2..d688ae436 100644 --- a/application/cmdbabstract.class.inc.php +++ b/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 { diff --git a/application/ui.linksdirectwidget.class.inc.php b/application/ui.linksdirectwidget.class.inc.php index 94e537d04..f6b02ff96 100644 --- a/application/ui.linksdirectwidget.class.inc.php +++ b/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(''); } + public function GetObjectsSelectionDlg($oPage, $oCurrentObj) + { + $sHtml = "
')
- .after(this.indicator).after(this.createBtn).after('  ')
- .after(this.modifyBtn).after('  ')
- .after(this.deleteBtn);
+ .after(this.indicator);
+ for(k in this.options.buttons)
+ {
+ this.element.after(this.oButtons[this.options.buttons[k]]).after(' ');
+ }
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('
');
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('
');
+ 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 = $('');
+ $('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('
');
$.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,22 @@ $(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[-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 +447,61 @@ $(function()
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 = 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();
+ },
+ _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;
}
});
});
\ No newline at end of file
diff --git a/pages/ajax.render.php b/pages/ajax.render.php
index 534a06af3..8b1e189eb 100644
--- a/pages/ajax.render.php
+++ b/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');
diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php
index dc7163fcd..42759fad7 100644
--- a/setup/compiler.class.inc.php
+++ b/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))