bslinkedsetfieldrenderer.class.inc.php 17 KB

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