bslinkedsetfieldrenderer.class.inc.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. <?php
  2. // Copyright (C) 2010-2016 Combodo SARL
  3. //
  4. // This file is part of iTop.
  5. //
  6. // iTop is free software; you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // iTop is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with iTop. If not, see <http://www.gnu.org/licenses/>
  18. namespace Combodo\iTop\Renderer\Bootstrap\FieldRenderer;
  19. use \Exception;
  20. use \utils;
  21. use \IssueLog;
  22. use \Dict;
  23. use \UserRights;
  24. use \InlineImage;
  25. use \DBObjectSet;
  26. use \MetaModel;
  27. use \Combodo\iTop\Renderer\FieldRenderer;
  28. use \Combodo\iTop\Renderer\RenderingOutput;
  29. use \Combodo\iTop\Form\Field\LinkedSetField;
  30. /**
  31. * Description of BsLinkedSetFieldRenderer
  32. *
  33. * @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
  34. */
  35. class BsLinkedSetFieldRenderer extends FieldRenderer
  36. {
  37. /**
  38. * Returns a RenderingOutput for the FieldRenderer's Field
  39. *
  40. * @return \Combodo\iTop\Renderer\RenderingOutput
  41. */
  42. public function Render()
  43. {
  44. $oOutput = new RenderingOutput();
  45. $sFieldMandatoryClass = ($this->oField->GetMandatory()) ? 'form_mandatory' : '';
  46. // Vars to build the table
  47. $sAttributesToDisplayAsJson = json_encode($this->oField->GetAttributesToDisplay());
  48. $sAttCodesToDisplayAsJson = json_encode($this->oField->GetAttributesToDisplay(true));
  49. $aItems = array();
  50. $aItemIds = array();
  51. $this->PrepareItems($aItems, $aItemIds);
  52. $sItemsAsJson = json_encode($aItems);
  53. $sItemIdsAsJson = htmlentities(json_encode($aItemIds), ENT_QUOTES, 'UTF-8');
  54. if (!$this->oField->GetHidden())
  55. {
  56. // Rendering field
  57. $sIsEditable = ($this->oField->GetReadOnly()) ? 'false' : 'true';
  58. $sCollapseTogglerIconVisibleClass = 'glyphicon-menu-down';
  59. $sCollapseTogglerIconHiddenClass = 'glyphicon-menu-down collapsed';
  60. $sCollapseTogglerClass = 'form_linkedset_toggler';
  61. $sCollapseTogglerId = $sCollapseTogglerClass . '_' . $this->oField->GetGlobalId();
  62. $sFieldWrapperId = 'form_linkedset_wrapper_' . $this->oField->GetGlobalId();
  63. // Preparing collapsed state
  64. if($this->oField->GetDisplayOpened())
  65. {
  66. $sCollapseTogglerExpanded = 'true';
  67. $sCollapseTogglerIconClass = $sCollapseTogglerIconVisibleClass;
  68. $sCollapseJSInitState = 'true';
  69. }
  70. else
  71. {
  72. $sCollapseTogglerClass .= ' collapsed';
  73. $sCollapseTogglerExpanded = 'false';
  74. $sCollapseTogglerIconClass = $sCollapseTogglerIconHiddenClass;
  75. $sCollapseJSInitState = 'false';
  76. }
  77. $oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '">');
  78. if ($this->oField->GetLabel() !== '')
  79. {
  80. $oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">')
  81. ->AddHtml('<a id="' . $sCollapseTogglerId . '" class="' . $sCollapseTogglerClass . '" data-toggle="collapse" href="#' . $sFieldWrapperId . '" aria-expanded="' . $sCollapseTogglerExpanded . '" aria-controls="' . $sFieldWrapperId . '">')
  82. ->AddHtml($this->oField->GetLabel(), true)
  83. ->AddHtml('<span class="text">' . count($aItemIds) . '</span>')
  84. ->AddHtml('<span class="glyphicon ' . $sCollapseTogglerIconClass . '"></>')
  85. ->AddHtml('</a>')
  86. ->AddHtml('</label>');
  87. }
  88. $oOutput->AddHtml('<div class="help-block"></div>');
  89. // Rendering table
  90. // - Vars
  91. $sTableId = 'table_' . $this->oField->GetGlobalId();
  92. // - Output
  93. $oOutput->AddHtml(
  94. <<<EOF
  95. <div class="form_linkedset_wrapper collapse" id="{$sFieldWrapperId}">
  96. <div class="row">
  97. <div class="col-xs-12">
  98. <input type="hidden" id="{$this->oField->GetGlobalId()}" name="{$this->oField->GetId()}" value="{$sItemIdsAsJson}" />
  99. <table id="{$sTableId}" data-field-id="{$this->oField->GetId()}" class="table table-striped table-bordered responsive" cellspacing="0" width="100%">
  100. <tbody>
  101. </tbody>
  102. </table>
  103. </div>
  104. </div>
  105. EOF
  106. );
  107. // Rendering table widget
  108. // - Vars
  109. $sEmptyTableLabel = htmlentities(Dict::S(($this->oField->GetReadOnly()) ? 'Portal:Datatables:Language:EmptyTable' : 'UI:Message:EmptyList:UseAdd'), ENT_QUOTES, 'UTF-8');
  110. $sLabelGeneralCheckbox = htmlentities(Dict::S('Core:BulkExport:CheckAll') . ' / ' . Dict::S('Core:BulkExport:UncheckAll'), ENT_QUOTES, 'UTF-8');
  111. $sSelectionOptionHtml = ($this->oField->GetReadOnly()) ? 'false' : '{"style": "multi"}';
  112. $sSelectionInputGlobalHtml = ($this->oField->GetReadOnly()) ? '' : '<span class="row_input"><input type="checkbox" id="' . $this->oField->GetGlobalId() . '_check_all" name="' . $this->oField->GetGlobalId() . '_check_all" title="' . $sLabelGeneralCheckbox . '" /></span>';
  113. $sSelectionInputHtml = ($this->oField->GetReadOnly()) ? '' : '<span class="row_input"><input type="checkbox" name="' . $this->oField->GetGlobalId() . '" /></span>';
  114. // - Output
  115. $oOutput->AddJs(
  116. <<<EOF
  117. // Collapse handlers
  118. // - Collapsing by default to optimize form space
  119. // It would be better to be able to construct the widget as collapsed, but in this case, datatables thinks the container is very small and therefore renders the table as if it was in microbox.
  120. $('#{$sFieldWrapperId}').collapse({toggle: {$sCollapseJSInitState}});
  121. // - Change toggle icon class
  122. $('#{$sFieldWrapperId}').on('shown.bs.collapse', function(){
  123. // Creating the table if null (first expand). If we create it on start, it will be displayed as if it was in a micro screen due to the div being "display: none;"
  124. if(oTable_{$this->oField->GetGlobalId()} === undefined)
  125. {
  126. buildTable_{$this->oField->GetGlobalId()}();
  127. }
  128. })
  129. .on('show.bs.collapse', function(){
  130. $('#{$sCollapseTogglerId} > span.glyphicon').removeClass('{$sCollapseTogglerIconHiddenClass}').addClass('{$sCollapseTogglerIconVisibleClass}');
  131. })
  132. .on('hide.bs.collapse', function(){
  133. $('#{$sCollapseTogglerId} > span.glyphicon').removeClass('{$sCollapseTogglerIconVisibleClass}').addClass('{$sCollapseTogglerIconHiddenClass}');
  134. });
  135. // Places a loader in the empty datatables
  136. $('#{$sTableId} > tbody').html('<tr><td class="datatables_overlay" colspan="100">' + $('#page_overlay').html() + '</td></tr>');
  137. // Prepares data for datatables
  138. var oColumnProperties_{$this->oField->GetGlobalId()} = {$sAttributesToDisplayAsJson};
  139. var oRawDatas_{$this->oField->GetGlobalId()} = {$sItemsAsJson};
  140. var oTable_{$this->oField->GetGlobalId()};
  141. var oSelectedItems_{$this->oField->GetGlobalId()} = {};
  142. var getColumnsDefinition_{$this->oField->GetGlobalId()} = function()
  143. {
  144. var aColumnsDefinition = [];
  145. if({$sIsEditable})
  146. {
  147. aColumnsDefinition.push({
  148. "width": "auto",
  149. "searchable": false,
  150. "sortable": false,
  151. "title": '{$sSelectionInputGlobalHtml}',
  152. "type": "html",
  153. "data": "",
  154. "render": function(data, type, row)
  155. {
  156. var oCheckboxElem = $('{$sSelectionInputHtml}');
  157. oCheckboxElem.find(':input').attr('data-object-id', row.id).attr('data-target-object-id', row.target_id);
  158. return oCheckboxElem.prop('outerHTML');
  159. }
  160. });
  161. }
  162. for(sKey in oColumnProperties_{$this->oField->GetGlobalId()})
  163. {
  164. // Level main column
  165. aColumnsDefinition.push({
  166. "width": "auto",
  167. "searchable": true,
  168. "sortable": true,
  169. "title": oColumnProperties_{$this->oField->GetGlobalId()}[sKey],
  170. "defaultContent": "",
  171. "type": "html",
  172. "data": "attributes."+sKey+".att_code",
  173. "render": function(data, type, row){
  174. var cellElem;
  175. // Preparing the cell data
  176. if(row.attributes[data].url !== undefined)
  177. {
  178. cellElem = $('<a></a>');
  179. cellElem.attr('target', '_blank').attr('href', row.attributes[data].url);
  180. }
  181. else
  182. {
  183. cellElem = $('<span></span>');
  184. }
  185. cellElem.html('<span>' + row.attributes[data].value + '</span>');
  186. return cellElem.prop('outerHTML');
  187. },
  188. });
  189. }
  190. return aColumnsDefinition;
  191. };
  192. // Helper to build the datatable
  193. // Note : Those options should be externalized in an library so we can use them on any DataTables for the portal.
  194. // We would just have to override / complete the necessary elements
  195. var buildTable_{$this->oField->GetGlobalId()} = function()
  196. {
  197. var iDefaultOrderColumnIndex = ({$sIsEditable}) ? 1 : 0;
  198. // Instanciates datatables
  199. oTable_{$this->oField->GetGlobalId()} = $('#{$sTableId}').DataTable({
  200. "language": {
  201. "emptyTable": "{$sEmptyTableLabel}"
  202. },
  203. "displayLength": -1,
  204. "scrollY": "300px",
  205. "scrollCollapse": true,
  206. "order": [[iDefaultOrderColumnIndex, "asc"]],
  207. "dom": 't',
  208. "columns": getColumnsDefinition_{$this->oField->GetGlobalId()}(),
  209. "select": {$sSelectionOptionHtml},
  210. "rowId": "id",
  211. "data": oRawDatas_{$this->oField->GetGlobalId()},
  212. });
  213. // Handles items selection/deselection
  214. // - Directly on the table
  215. oTable_{$this->oField->GetGlobalId()}.off('select').on('select', function(oEvent, dt, type, indexes){
  216. var aData = oTable_{$this->oField->GetGlobalId()}.rows(indexes).data().toArray();
  217. // Checking input
  218. $('#{$sTableId} tbody tr[role="row"].selected td:first-child input').prop('checked', true);
  219. // Saving values in temp array
  220. for(var i in aData)
  221. {
  222. var iItemId = aData[i].id;
  223. if(!(iItemId in oSelectedItems_{$this->oField->GetGlobalId()}))
  224. {
  225. oSelectedItems_{$this->oField->GetGlobalId()}[iItemId] = aData[i].name;
  226. }
  227. }
  228. // Updating remove button
  229. updateRemoveButtonState_{$this->oField->GetGlobalId()}();
  230. });
  231. oTable_{$this->oField->GetGlobalId()}.off('deselect').on('deselect', function(oEvent, dt, type, indexes){
  232. var aData = oTable_{$this->oField->GetGlobalId()}.rows(indexes).data().toArray();
  233. // Checking input
  234. $('#{$sTableId} tbody tr[role="row"]:not(.selected) td:first-child input').prop('checked', false);
  235. // Saving values in temp array
  236. for(var i in aData)
  237. {
  238. var iItemId = aData[i].id;
  239. if(iItemId in oSelectedItems_{$this->oField->GetGlobalId()})
  240. {
  241. delete oSelectedItems_{$this->oField->GetGlobalId()}[iItemId];
  242. }
  243. }
  244. // Unchecking global checkbox
  245. $('#{$this->oField->GetGlobalId()}_check_all').prop('checked', false);
  246. // Updating remove button
  247. updateRemoveButtonState_{$this->oField->GetGlobalId()}();
  248. });
  249. // - From the global button
  250. $('#{$this->oField->GetGlobalId()}_check_all').off('click').on('click', function(oEvent){
  251. if($(this).prop('checked'))
  252. {
  253. oTable_{$this->oField->GetGlobalId()}.rows().select();
  254. }
  255. else
  256. {
  257. oTable_{$this->oField->GetGlobalId()}.rows().deselect();
  258. }
  259. updateRemoveButtonState_{$this->oField->GetGlobalId()}();
  260. });
  261. };
  262. EOF
  263. );
  264. // Attaching JS widget
  265. $sObjectInformationsUrl = $this->oField->GetInformationEndpoint();
  266. $oOutput->AddJs(
  267. <<<EOF
  268. $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field({
  269. 'validators': {$this->GetValidatorsAsJson()},
  270. 'get_current_value_callback': function(me, oEvent, oData){
  271. var value = null;
  272. // Retrieving JSON value as a string and not an object
  273. //
  274. // Note : The value is passed as a string instead of an array because the attribute would not be included in the posted data when empty.
  275. // Which was an issue when deleting all objects from linkedset
  276. //
  277. // Old code : value = JSON.parse(me.element.find('#{$this->oField->GetGlobalId()}').val());
  278. value = me.element.find('#{$this->oField->GetGlobalId()}').val();
  279. return value;
  280. },
  281. 'set_current_value_callback': function(me, oEvent, oData){
  282. // When we have data (meaning that we picked objects from search)
  283. if(oData !== undefined && Object.keys(oData.values).length > 0)
  284. {
  285. // Showing loader while retrieving informations
  286. $('#page_overlay').fadeIn(200);
  287. // Retrieving new rows ids
  288. var aObjectIds = Object.keys(oData.values);
  289. // Retrieving rows informations so we can add them
  290. $.post(
  291. '{$sObjectInformationsUrl}',
  292. {
  293. sObjectClass: '{$this->oField->GetTargetClass()}',
  294. aObjectIds: aObjectIds,
  295. aObjectAttCodes: $sAttCodesToDisplayAsJson
  296. },
  297. function(oData){
  298. // Updating datatables
  299. if(oData.items !== undefined)
  300. {
  301. for(var i in oData.items)
  302. {
  303. // Adding target item id information
  304. oData.items[i].target_id = oData.items[i].id;
  305. // Adding item to table only if it's not already there
  306. if($('#{$sTableId} tr[role="row"] > td input[data-target-object-id="' + oData.items[i].target_id + '"], #{$sTableId} tr[role="row"] > td input[data-target-object-id="' + (oData.items[i].target_id*-1) + '"]').length === 0)
  307. {
  308. // Making id negative in order to recognize it when persisting
  309. oData.items[i].id = -1 * parseInt(oData.items[i].id);
  310. oTable_{$this->oField->GetGlobalId()}.row.add(oData.items[i]);
  311. }
  312. }
  313. oTable_{$this->oField->GetGlobalId()}.draw();
  314. }
  315. }
  316. )
  317. .done(function(oData){
  318. // Updating hidden field
  319. var aData = oTable_{$this->oField->GetGlobalId()}.rows().data().toArray();
  320. var aObjectIds = [];
  321. for(var i in aData)
  322. {
  323. aObjectIds.push({id: aData[i].id});
  324. }
  325. $('#{$this->oField->GetGlobalId()}').val(JSON.stringify(aObjectIds));
  326. // Updating items count
  327. updateItemCount_{$this->oField->GetGlobalId()}();
  328. // Updating global checkbox
  329. $('#{$this->oField->GetGlobalId()}_check_all').prop('checked', false);
  330. })
  331. .always(function(oData){
  332. // Hiding loader
  333. $('#page_overlay').fadeOut(200);
  334. });
  335. }
  336. // We come from a button
  337. else
  338. {
  339. // Updating hidden field
  340. var aData = oTable_{$this->oField->GetGlobalId()}.rows().data().toArray();
  341. var aObjectIds = [];
  342. for(var i in aData)
  343. {
  344. aObjectIds.push({id: aData[i].id});
  345. }
  346. $('#{$this->oField->GetGlobalId()}').val(JSON.stringify(aObjectIds));
  347. // Updating items count
  348. updateItemCount_{$this->oField->GetGlobalId()}();
  349. // Updating global checkbox
  350. $('#{$this->oField->GetGlobalId()}_check_all').prop('checked', false);
  351. }
  352. }
  353. });
  354. EOF
  355. );
  356. // Additional features if in edition mode
  357. if (!$this->oField->GetReadOnly())
  358. {
  359. // Rendering table
  360. // - Vars
  361. $sButtonRemoveId = 'btn_remove_' . $this->oField->GetGlobalId();
  362. $sButtonAddId = 'btn_add_' . $this->oField->GetGlobalId();
  363. $sLabelRemove = Dict::S('UI:Button:Remove');
  364. $sLabelAdd = Dict::S('UI:Button:AddObject');
  365. // - Output
  366. $oOutput->AddHtml(
  367. <<<EOF
  368. <div class="row">
  369. <div class="col-xs-12">
  370. <div class="btn-group" role="group">
  371. <button type="button" class="btn btn-sm btn-danger" id="{$sButtonRemoveId}" title="{$sLabelRemove}" disabled><span class="glyphicon glyphicon-minus"></span></button>
  372. <button type="button" class="btn btn-sm btn-default" id="{$sButtonAddId}" title="{$sLabelAdd}"><span class="glyphicon glyphicon-plus"></span></button>
  373. </div>
  374. </div>
  375. </div>
  376. EOF
  377. );
  378. // Rendering table widget
  379. // - Vars
  380. $sAddButtonEndpoint = str_replace('-sMode-', 'from-attribute', $this->oField->GetSearchEndpoint());
  381. // - Output
  382. $oOutput->AddJs(
  383. <<<EOF
  384. // Handles items selection/deselection
  385. // - Remove button state handler
  386. var updateRemoveButtonState_{$this->oField->GetGlobalId()} = function()
  387. {
  388. var bIsDisabled = (Object.keys(oSelectedItems_{$this->oField->GetGlobalId()}).length == 0);
  389. $('#{$sButtonRemoveId}').prop('disabled', bIsDisabled);
  390. };
  391. // - Item count state handler
  392. var updateItemCount_{$this->oField->GetGlobalId()} = function()
  393. {
  394. $('#{$sCollapseTogglerId} > .text').text( oTable_{$this->oField->GetGlobalId()}.rows().count() );
  395. };
  396. // Handles items remove/add
  397. $('#{$sButtonRemoveId}').off('click').on('click', function(){
  398. // Removing items from table
  399. oTable_{$this->oField->GetGlobalId()}.rows({selected: true}).remove().draw();
  400. // Resetting selected items
  401. oSelectedItems_{$this->oField->GetGlobalId()} = {};
  402. // Updating form value
  403. $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").triggerHandler('set_current_value');
  404. // Updating global checkbox state
  405. $('#{$this->oField->GetGlobalId()}_check_all').prop('checked', false);
  406. // Updating remove button
  407. updateRemoveButtonState_{$this->oField->GetGlobalId()}();
  408. });
  409. $('#{$sButtonAddId}').off('click').on('click', function(){
  410. // Preparing current values
  411. var aObjectIdsToIgnore = [];
  412. $('#{$sTableId} tr[role="row"] > td input[data-target-object-id]').each(function(iIndex, oElem){
  413. aObjectIdsToIgnore.push( $(oElem).attr('data-target-object-id') );
  414. });
  415. // Creating a new modal
  416. var oModalElem;
  417. if($('.modal[data-source-element="{$sButtonAddId}"]').length === 0)
  418. {
  419. oModalElem = $('#modal-for-all').clone();
  420. oModalElem.attr('id', '').attr('data-source-element', '{$sButtonAddId}').appendTo('body');
  421. }
  422. else
  423. {
  424. oModalElem = $('.modal[data-source-element="{$sButtonAddId}"]').first();
  425. }
  426. // Resizing to small modal
  427. oModalElem.find('.modal-dialog').removeClass('modal-sm').addClass('modal-lg');
  428. // Loading content
  429. oModalElem.find('.modal-content').html($('#page_overlay .overlay_content').html());
  430. oModalElem.find('.modal-content').load(
  431. '{$sAddButtonEndpoint}',
  432. {
  433. sFormPath: '{$this->oField->GetFormPath()}',
  434. sFieldId: '{$this->oField->GetId()}',
  435. aObjectIdsToIgnore : aObjectIdsToIgnore
  436. },
  437. function(sResponseText, sStatus, oXHR){
  438. // Hiding modal in case of error as the general AJAX error handler will display a message
  439. if(sStatus === 'error')
  440. {
  441. oModalElem.modal('hide');
  442. }
  443. }
  444. );
  445. oModalElem.modal('show');
  446. });
  447. EOF
  448. );
  449. }
  450. }
  451. // ... and in hidden mode
  452. else
  453. {
  454. $oOutput->AddHtml('<input type="hidden" id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" value="' . $sItemIdsAsJson . '" />');
  455. }
  456. // End of table rendering
  457. $oOutput->AddHtml('</div>');
  458. $oOutput->AddHtml('</div>');
  459. return $oOutput;
  460. }
  461. protected function PrepareItems(&$aItems, &$aItemIds)
  462. {
  463. $oValueSet = $this->oField->GetCurrentValue();
  464. $oValueSet->OptimizeColumnLoad(array($this->oField->GetTargetClass() => $this->oField->GetAttributesToDisplay(true)));
  465. while ($oItem = $oValueSet->Fetch())
  466. {
  467. // In case of indirect linked set, we must retrieve the remote object
  468. if ($this->oField->IsIndirect())
  469. {
  470. try{
  471. // Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated
  472. $oRemoteItem = MetaModel::GetObject($this->oField->GetTargetClass(), $oItem->Get($this->oField->GetExtKeyToRemote()), true, true);
  473. }
  474. catch(Exception $e)
  475. {
  476. // In some cases we can't retrieve an object from a linkedset, eg. when the extkey to remote is 0 due to a database corruption.
  477. // Rather than crashing we rather just skip the object like in the administration console
  478. IssueLog::Error('Could not retrieve object of linkedset in form #'.$this->oField->GetFormPath().' for field #'.$this->oField->GetId().'. Message: '.$e->getMessage());
  479. continue;
  480. }
  481. }
  482. else
  483. {
  484. $oRemoteItem = $oItem;
  485. }
  486. $aItemProperties = array(
  487. 'id' => ($this->oField->IsIndirect() && $oItem->IsNew()) ? -1*$oRemoteItem->GetKey() : $oItem->GetKey(),
  488. 'target_id' => $oRemoteItem->GetKey(),
  489. 'name' => $oItem->GetName(),
  490. 'attributes' => array()
  491. );
  492. // Target object others attributes
  493. foreach ($this->oField->GetAttributesToDisplay(true) as $sAttCode)
  494. {
  495. if ($sAttCode !== 'id')
  496. {
  497. $aAttProperties = array(
  498. 'att_code' => $sAttCode
  499. );
  500. $oAttDef = MetaModel::GetAttributeDef($this->oField->GetTargetClass(), $sAttCode);
  501. if ($oAttDef->IsExternalKey())
  502. {
  503. $aAttProperties['value'] = $oRemoteItem->Get($sAttCode . '_friendlyname');
  504. }
  505. else
  506. {
  507. $aAttProperties['value'] = $oAttDef->GetValueLabel($oRemoteItem->Get($sAttCode));
  508. }
  509. $aItemProperties['attributes'][$sAttCode] = $aAttProperties;
  510. }
  511. }
  512. $aItems[] = $aItemProperties;
  513. $aItemIds[] = array('id' => $aItemProperties['id']);
  514. }
  515. }
  516. }