bslinkedsetfieldrenderer.class.inc.php 19 KB

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