123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525 |
- <?php
- // Copyright (C) 2010-2016 Combodo SARL
- //
- // This file is part of iTop.
- //
- // iTop is free software; you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // iTop is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with iTop. If not, see <http://www.gnu.org/licenses/>
- namespace Combodo\iTop\Renderer\Bootstrap\FieldRenderer;
- use \utils;
- use \Dict;
- use \UserRights;
- use \InlineImage;
- use \DBObjectSet;
- use \MetaModel;
- use \Combodo\iTop\Renderer\FieldRenderer;
- use \Combodo\iTop\Renderer\RenderingOutput;
- use \Combodo\iTop\Form\Field\LinkedSetField;
- /**
- * Description of BsLinkedSetFieldRenderer
- *
- * @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
- */
- class BsLinkedSetFieldRenderer extends FieldRenderer
- {
- /**
- * Returns a RenderingOutput for the FieldRenderer's Field
- *
- * @return \Combodo\iTop\Renderer\RenderingOutput
- */
- public function Render()
- {
- $oOutput = new RenderingOutput();
- $sFieldMandatoryClass = ($this->oField->GetMandatory()) ? 'form_mandatory' : '';
- // Vars to build the table
- $sAttributesToDisplayAsJson = json_encode($this->oField->GetAttributesToDisplay());
- $sAttCodesToDisplayAsJson = json_encode($this->oField->GetAttributesToDisplay(true));
- $aItems = array();
- $aItemIds = array();
- $this->PrepareItems($aItems, $aItemIds);
- $sItemsAsJson = json_encode($aItems);
- $sItemIdsAsJson = htmlentities(json_encode($aItemIds), ENT_QUOTES, 'UTF-8');
-
- if (!$this->oField->GetHidden())
- {
- // Rendering field
- $sIsEditable = ($this->oField->GetReadOnly()) ? 'false' : 'true';
- $sCollapseTogglerVisibleClass = 'glyphicon-menu-down';
- $sCollapseTogglerHiddenClass = 'glyphicon-menu-down collapsed';
- $sCollapseTogglerId = 'form_linkedset_toggler_' . $this->oField->GetGlobalId();
- $sFieldWrapperId = 'form_linkedset_wrapper_' . $this->oField->GetGlobalId();
- $oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '">');
- if ($this->oField->GetLabel() !== '')
- {
- $oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">')
- ->AddHtml($this->oField->GetLabel(), true)
- ->AddHtml('<a id="' . $sCollapseTogglerId . '" class="form_linkedset_toggler" data-toggle="collapse" href="#' . $sFieldWrapperId . '" aria-expanded="false" aria-controls="' . $sFieldWrapperId . '">')
- ->AddHtml('<span class="text">' . count($aItemIds) . '</span>')
- ->AddHtml('<span class="glyphicon ' . $sCollapseTogglerHiddenClass . '"></>')
- ->AddHtml('</a>')
- ->AddHtml('</label>');
- }
- $oOutput->AddHtml('<div class="help-block"></div>');
- // Rendering table
- // - Vars
- $sTableId = 'table_' . $this->oField->GetGlobalId();
- // - Output
- $oOutput->AddHtml(
- <<<EOF
- <div class="form_linkedset_wrapper collapse" id="{$sFieldWrapperId}">
- <div class="row">
- <div class="col-xs-12">
- <input type="hidden" id="{$this->oField->GetGlobalId()}" name="{$this->oField->GetId()}" value="{$sItemIdsAsJson}" />
- <table id="{$sTableId}" data-field-id="{$this->oField->GetId()}" class="table table-striped table-bordered responsive" cellspacing="0" width="100%">
- <tbody>
- </tbody>
- </table>
- </div>
- </div>
- EOF
- );
- // Rendering table widget
- // - Vars
- $sEmptyTableLabel = htmlentities(Dict::S(($this->oField->GetReadOnly()) ? 'Portal:Datatables:Language:EmptyTable' : 'UI:Message:EmptyList:UseAdd'), ENT_QUOTES, 'UTF-8');
- $sLabelGeneralCheckbox = htmlentities(Dict::S('Core:BulkExport:CheckAll') . ' / ' . Dict::S('Core:BulkExport:UncheckAll'), ENT_QUOTES, 'UTF-8');
- $sSelectionOptionHtml = ($this->oField->GetReadOnly()) ? 'false' : '{"style": "multi"}';
- $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>';
- $sSelectionInputHtml = ($this->oField->GetReadOnly()) ? '' : '<span class="row_input"><input type="checkbox" name="' . $this->oField->GetGlobalId() . '" /></span>';
- // - Output
- $oOutput->AddJs(
- <<<EOF
- // Collapse handlers
- // - Collapsing by default to optimize form space
- // 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.
- $('#{$sFieldWrapperId}').collapse({toggle: false});
- // - Change toggle icon class
- $('#{$sFieldWrapperId}').on('shown.bs.collapse', function(){
- // 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;"
- if(oTable_{$this->oField->GetGlobalId()} === undefined)
- {
- buildTable_{$this->oField->GetGlobalId()}();
- }
- })
- .on('show.bs.collapse', function(){
- $('#{$sCollapseTogglerId} > span.glyphicon').removeClass('{$sCollapseTogglerHiddenClass}').addClass('{$sCollapseTogglerVisibleClass}');
- })
- .on('hide.bs.collapse', function(){
- $('#{$sCollapseTogglerId} > span.glyphicon').removeClass('{$sCollapseTogglerVisibleClass}').addClass('{$sCollapseTogglerHiddenClass}');
- });
- // Places a loader in the empty datatables
- $('#{$sTableId} > tbody').html('<tr><td class="datatables_overlay" colspan="100">' + $('#page_overlay').html() + '</td></tr>');
- // Prepares data for datatables
- var oColumnProperties_{$this->oField->GetGlobalId()} = {$sAttributesToDisplayAsJson};
- var oRawDatas_{$this->oField->GetGlobalId()} = {$sItemsAsJson};
- var oTable_{$this->oField->GetGlobalId()};
- var oSelectedItems_{$this->oField->GetGlobalId()} = {};
- var getColumnsDefinition_{$this->oField->GetGlobalId()} = function()
- {
- var aColumnsDefinition = [];
- if({$sIsEditable})
- {
- aColumnsDefinition.push({
- "width": "auto",
- "searchable": false,
- "sortable": false,
- "title": '{$sSelectionInputGlobalHtml}',
- "type": "html",
- "data": "",
- "render": function(data, type, row)
- {
- var oCheckboxElem = $('{$sSelectionInputHtml}');
- oCheckboxElem.find(':input').attr('data-object-id', row.id).attr('data-target-object-id', row.target_id);
- return oCheckboxElem.prop('outerHTML');
- }
- });
- }
- for(sKey in oColumnProperties_{$this->oField->GetGlobalId()})
- {
- // Level main column
- aColumnsDefinition.push({
- "width": "auto",
- "searchable": true,
- "sortable": true,
- "title": oColumnProperties_{$this->oField->GetGlobalId()}[sKey],
- "defaultContent": "",
- "type": "html",
- "data": "attributes."+sKey+".att_code",
- "render": function(data, type, row){
- var cellElem;
- // Preparing the cell data
- if(row.attributes[data].url !== undefined)
- {
- cellElem = $('<a></a>');
- cellElem.attr('target', '_blank').attr('href', row.attributes[data].url);
- }
- else
- {
- cellElem = $('<span></span>');
- }
- cellElem.html('<span>' + row.attributes[data].value + '</span>');
- return cellElem.prop('outerHTML');
- },
- });
- }
- return aColumnsDefinition;
- };
- // Helper to build the datatable
- // Note : Those options should be externalized in an library so we can use them on any DataTables for the portal.
- // We would just have to override / complete the necessary elements
- var buildTable_{$this->oField->GetGlobalId()} = function()
- {
- var iDefaultOrderColumnIndex = ({$sIsEditable}) ? 1 : 0;
- // Instanciates datatables
- oTable_{$this->oField->GetGlobalId()} = $('#{$sTableId}').DataTable({
- "language": {
- "emptyTable": "{$sEmptyTableLabel}"
- },
- "displayLength": -1,
- "scrollY": "300px",
- "scrollCollapse": true,
- "order": [[iDefaultOrderColumnIndex, "asc"]],
- "dom": 't',
- "columns": getColumnsDefinition_{$this->oField->GetGlobalId()}(),
- "select": {$sSelectionOptionHtml},
- "rowId": "id",
- "data": oRawDatas_{$this->oField->GetGlobalId()},
- });
-
- // Handles items selection/deselection
- // - Directly on the table
- oTable_{$this->oField->GetGlobalId()}.off('select').on('select', function(oEvent, dt, type, indexes){
- var aData = oTable_{$this->oField->GetGlobalId()}.rows(indexes).data().toArray();
- // Checking input
- $('#{$sTableId} tbody tr[role="row"].selected td:first-child input').prop('checked', true);
- // Saving values in temp array
- for(var i in aData)
- {
- var iItemId = aData[i].id;
- if(!(iItemId in oSelectedItems_{$this->oField->GetGlobalId()}))
- {
- oSelectedItems_{$this->oField->GetGlobalId()}[iItemId] = aData[i].name;
- }
- }
- // Updating remove button
- updateRemoveButtonState_{$this->oField->GetGlobalId()}();
- });
- oTable_{$this->oField->GetGlobalId()}.off('deselect').on('deselect', function(oEvent, dt, type, indexes){
- var aData = oTable_{$this->oField->GetGlobalId()}.rows(indexes).data().toArray();
- // Checking input
- $('#{$sTableId} tbody tr[role="row"]:not(.selected) td:first-child input').prop('checked', false);
- // Saving values in temp array
- for(var i in aData)
- {
- var iItemId = aData[i].id;
- if(iItemId in oSelectedItems_{$this->oField->GetGlobalId()})
- {
- delete oSelectedItems_{$this->oField->GetGlobalId()}[iItemId];
- }
- }
- // Unchecking global checkbox
- $('#{$this->oField->GetGlobalId()}_check_all').prop('checked', false);
- // Updating remove button
- updateRemoveButtonState_{$this->oField->GetGlobalId()}();
- });
- // - From the global button
- $('#{$this->oField->GetGlobalId()}_check_all').off('click').on('click', function(oEvent){
- if($(this).prop('checked'))
- {
- oTable_{$this->oField->GetGlobalId()}.rows().select();
- }
- else
- {
- oTable_{$this->oField->GetGlobalId()}.rows().deselect();
- }
- updateRemoveButtonState_{$this->oField->GetGlobalId()}();
- });
- };
- EOF
- );
- // Attaching JS widget
- $sObjectInformationsUrl = $this->oField->GetInformationEndpoint();
- $oOutput->AddJs(
- <<<EOF
- $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field({
- 'validators': {$this->GetValidatorsAsJson()},
- 'get_current_value_callback': function(me, oEvent, oData){
- var value = null;
- // Retrieving JSON value as a string and not an object
- //
- // 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.
- // Which was an issue when deleting all objects from linkedset
- //
- // Old code : value = JSON.parse(me.element.find('#{$this->oField->GetGlobalId()}').val());
- value = me.element.find('#{$this->oField->GetGlobalId()}').val();
- return value;
- },
- 'set_current_value_callback': function(me, oEvent, oData){
- // When we have data (meaning that we picked objects from search)
- if(oData !== undefined && Object.keys(oData.values).length > 0)
- {
- // Showing loader while retrieving informations
- $('#page_overlay').fadeIn(200);
- // Retrieving new rows ids
- var aObjectIds = Object.keys(oData.values);
- // Retrieving rows informations so we can add them
- $.post(
- '{$sObjectInformationsUrl}',
- {
- sObjectClass: '{$this->oField->GetTargetClass()}',
- aObjectIds: aObjectIds,
- aObjectAttCodes: $sAttCodesToDisplayAsJson
- },
- function(oData){
- // Updating datatables
- if(oData.items !== undefined)
- {
- for(var i in oData.items)
- {
- // Adding target item id information
- oData.items[i].target_id = oData.items[i].id;
- // Adding item to table only if it's not already there
- 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)
- {
- // Making id negative in order to recognize it when persisting
- oData.items[i].id = -1 * parseInt(oData.items[i].id);
- oTable_{$this->oField->GetGlobalId()}.row.add(oData.items[i]);
- }
- }
- oTable_{$this->oField->GetGlobalId()}.draw();
- }
- }
- )
- .done(function(oData){
- // Updating hidden field
- var aData = oTable_{$this->oField->GetGlobalId()}.rows().data().toArray();
- var aObjectIds = [];
- for(var i in aData)
- {
- aObjectIds.push({id: aData[i].id});
- }
- $('#{$this->oField->GetGlobalId()}').val(JSON.stringify(aObjectIds));
- // Updating items count
- updateItemCount_{$this->oField->GetGlobalId()}();
- // Updating global checkbox
- $('#{$this->oField->GetGlobalId()}_check_all').prop('checked', false);
- })
- .always(function(oData){
- // Hiding loader
- $('#page_overlay').fadeOut(200);
- });
- }
- // We come from a button
- else
- {
- // Updating hidden field
- var aData = oTable_{$this->oField->GetGlobalId()}.rows().data().toArray();
- var aObjectIds = [];
- for(var i in aData)
- {
- aObjectIds.push({id: aData[i].id});
- }
- $('#{$this->oField->GetGlobalId()}').val(JSON.stringify(aObjectIds));
- // Updating items count
- updateItemCount_{$this->oField->GetGlobalId()}();
- // Updating global checkbox
- $('#{$this->oField->GetGlobalId()}_check_all').prop('checked', false);
- }
- }
- });
- EOF
- );
- // Additional features if in edition mode
- if (!$this->oField->GetReadOnly())
- {
- // Rendering table
- // - Vars
- $sButtonRemoveId = 'btn_remove_' . $this->oField->GetGlobalId();
- $sButtonAddId = 'btn_add_' . $this->oField->GetGlobalId();
- $sLabelRemove = Dict::S('UI:Button:Remove');
- $sLabelAdd = Dict::S('UI:Button:AddObject');
- // - Output
- $oOutput->AddHtml(
- <<<EOF
- <div class="row">
- <div class="col-xs-12">
- <div class="btn-group" role="group">
- <button type="button" class="btn btn-sm btn-danger" id="{$sButtonRemoveId}" title="{$sLabelRemove}" disabled><span class="glyphicon glyphicon-minus"></span></button>
- <button type="button" class="btn btn-sm btn-default" id="{$sButtonAddId}" title="{$sLabelAdd}"><span class="glyphicon glyphicon-plus"></span></button>
- </div>
- </div>
- </div>
- EOF
- );
- // Rendering table widget
- // - Vars
- $sAddButtonEndpoint = str_replace('-sMode-', 'from-attribute', $this->oField->GetSearchEndpoint());
- // - Output
- $oOutput->AddJs(
- <<<EOF
- // Handles items selection/deselection
- // - Remove button state handler
- var updateRemoveButtonState_{$this->oField->GetGlobalId()} = function()
- {
- var bIsDisabled = (Object.keys(oSelectedItems_{$this->oField->GetGlobalId()}).length == 0);
- $('#{$sButtonRemoveId}').prop('disabled', bIsDisabled);
- };
- // - Item count state handler
- var updateItemCount_{$this->oField->GetGlobalId()} = function()
- {
- $('#{$sCollapseTogglerId} > .text').text( oTable_{$this->oField->GetGlobalId()}.rows().count() );
- };
- // Handles items remove/add
- $('#{$sButtonRemoveId}').off('click').on('click', function(){
- // Removing items from table
- oTable_{$this->oField->GetGlobalId()}.rows({selected: true}).remove().draw();
- // Resetting selected items
- oSelectedItems_{$this->oField->GetGlobalId()} = {};
- // Updating form value
- $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").triggerHandler('set_current_value');
- // Updating global checkbox state
- $('#{$this->oField->GetGlobalId()}_check_all').prop('checked', false);
- // Updating remove button
- updateRemoveButtonState_{$this->oField->GetGlobalId()}();
- });
- $('#{$sButtonAddId}').off('click').on('click', function(){
- // Preparing current values
- var aObjectIdsToIgnore = [];
- $('#{$sTableId} tr[role="row"] > td input[data-target-object-id]').each(function(iIndex, oElem){
- aObjectIdsToIgnore.push( $(oElem).attr('data-target-object-id') );
- });
- // Creating a new modal
- var oModalElem;
- if($('.modal[data-source-element="{$sButtonAddId}"]').length === 0)
- {
- oModalElem = $('#modal-for-all').clone();
- oModalElem.attr('id', '').attr('data-source-element', '{$sButtonAddId}').appendTo('body');
- }
- else
- {
- oModalElem = $('.modal[data-source-element="{$sButtonAddId}"]').first();
- }
- // Resizing to small modal
- oModalElem.find('.modal-dialog').removeClass('modal-sm').addClass('modal-lg');
- // Loading content
- oModalElem.find('.modal-content').html($('#page_overlay .overlay_content').html());
- oModalElem.find('.modal-content').load(
- '{$sAddButtonEndpoint}',
- {
- sFormPath: '{$this->oField->GetFormPath()}',
- sFieldId: '{$this->oField->GetId()}',
- aObjectIdsToIgnore : aObjectIdsToIgnore
- }
- );
- oModalElem.modal('show');
- });
- EOF
- );
- }
- }
- // ... and in hidden mode
- else
- {
- $oOutput->AddHtml('<input type="hidden" id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" value="' . $sItemIdsAsJson . '" />');
- }
- // End of table rendering
- $oOutput->AddHtml('</div>');
- $oOutput->AddHtml('</div>');
- return $oOutput;
- }
- protected function PrepareItems(&$aItems, &$aItemIds)
- {
- $oValueSet = $this->oField->GetCurrentValue();
- $oValueSet->OptimizeColumnLoad(array($this->oField->GetTargetClass() => $this->oField->GetAttributesToDisplay(true)));
- while ($oItem = $oValueSet->Fetch())
- {
- // In case of indirect linked set, we must retrieve the remote object
- if ($this->oField->IsIndirect())
- {
- // Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated
- $oRemoteItem = MetaModel::GetObject($this->oField->GetTargetClass(), $oItem->Get($this->oField->GetExtKeyToRemote()), true, true);
- }
- else
- {
- $oRemoteItem = $oItem;
- }
- $aItemProperties = array(
- 'id' => $oItem->GetKey(),
- 'target_id' => $oRemoteItem->GetKey(),
- 'name' => $oItem->GetName(),
- 'attributes' => array()
- );
- // Target object others attributes
- foreach ($this->oField->GetAttributesToDisplay(true) as $sAttCode)
- {
- if ($sAttCode !== 'id')
- {
- $aAttProperties = array(
- 'att_code' => $sAttCode
- );
- $oAttDef = MetaModel::GetAttributeDef($this->oField->GetTargetClass(), $sAttCode);
- if ($oAttDef->IsExternalKey())
- {
- $aAttProperties['value'] = $oRemoteItem->Get($sAttCode . '_friendlyname');
- }
- else
- {
- $aAttProperties['value'] = $oAttDef->GetValueLabel($oRemoteItem->Get($sAttCode));
- }
- $aItemProperties['attributes'][$sAttCode] = $aAttProperties;
- }
- }
-
- $aItems[] = $aItemProperties;
- $aItemIds[] = array('id' => $oItem->GetKey());
- }
- }
- }
|