bslinkedsetfieldrenderer.class.inc.php 22 KB

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