From 700d11fa8ff34552df18cdf24c33dbb6c9c210d2 Mon Sep 17 00:00:00 2001 From: acognet Date: Tue, 18 Aug 2020 10:06:10 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B02508=20-=20Include=20Obsolescence=20icon?= =?UTF-8?q?=20within=20list=20and=20autocomplete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/itopwebpage.class.inc.php | 9 +- application/ui.extkeywidget.class.inc.php | 309 +++++++++++++++++- core/valuesetdef.class.inc.php | 133 +++++++- js/extkeywidget.js | 165 ++++++---- ...oleselectobjectfieldrenderer.class.inc.php | 3 +- 5 files changed, 542 insertions(+), 77 deletions(-) diff --git a/application/itopwebpage.class.inc.php b/application/itopwebpage.class.inc.php index 330f2aef5..f50b04a0a 100644 --- a/application/itopwebpage.class.inc.php +++ b/application/itopwebpage.class.inc.php @@ -79,6 +79,7 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage $this->add_linked_stylesheet("../css/c3.min.css"); $this->add_linked_stylesheet("../css/font-awesome/css/all.min.css"); $this->add_linked_stylesheet("../js/ckeditor/plugins/codesnippet/lib/highlight/styles/obsidian.css"); + $this->add_linked_stylesheet("../css/selectize.default.css"); $this->add_linked_script('../js/jquery.layout.min.js'); $this->add_linked_script('../js/jquery.ba-bbq.min.js'); @@ -106,6 +107,7 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage $this->add_linked_script('../js/moment-with-locales.min.js'); $this->add_linked_script('../js/showdown.min.js'); $this->add_linked_script('../js/newsroom_menu.js'); + $this->add_linked_script("../js/selectize.min.js"); $this->add_dict_entry('UI:FillAllMandatoryFields'); @@ -804,21 +806,18 @@ JS break; default: - $sHtml = ''; $oAppContext = new ApplicationContext(); $iCurrentOrganization = $oAppContext->GetCurrentValue('org_id'); $sHtml = '
'; $sHtml .= '
'; //iId\">"; + $sJsonOptions=json_encode($aOptions); + $oPage->add_ready_script( + <<iId} = new ExtKeyWidget('{$this->iId}', '{$this->sTargetClass}', '$sFilter', '$sTitle', true, $sWizHelper, '{$this->sAttCode}', $sJSSearchMode, $sJSDoSearch); + oACWidget_{$this->iId}.emptyHtml = "

$sMessage

"; + oACWidget_{$this->iId}.AddSelectize('$sJsonOptions','$sDisplayValue'); + $('#$this->iId').bind('update', function() { oACWidget_{$this->iId}.Update(); } ); + $('#$this->iId').bind('change', function() { $(this).trigger('extkeychange') } ); + +JS + ); + } + else + { + // Too many choices, use an autocomplete + // Check that the given value is allowed + $oSearch = $oAllowedValues->GetFilter(); + $oSearch->AddCondition('id', $value); + $oSet = new DBObjectSet($oSearch); + if ($oSet->Count() == 0) + { + $value = null; + } + + if (is_null($value) || ($value == 0)) // Null values are displayed as '' + { + $sDisplayValue = isset($aArgs['sDefaultValue']) ? $aArgs['sDefaultValue'] : ''; + } + else + { + $sDisplayValue = $this->GetObjectName($value); + } + $iMinChars = isset($aArgs['iMinChars']) ? $aArgs['iMinChars'] : 2; //@@@ $this->oAttDef->GetMinAutoCompleteChars(); + + // the input for the auto-complete + $sHTMLValue .= "iId\" value=\"$sDisplayValue\"/>"; + $sHTMLValue .= "
iId}\" onClick=\"oACWidget_{$this->iId}.Search();\">
"; + + // another hidden input to store & pass the object's Id + $sHTMLValue .= "iId\" name=\"{$sAttrFieldPrefix}{$sFieldName}\" value=\"".htmlentities($value, ENT_QUOTES, 'UTF-8')."\" />\n"; + + $JSSearchMode = $this->bSearchMode ? 'true' : 'false'; + // Scripts to start the autocomplete and bind some events to it + $oPage->add_ready_script( + <<iId} = new ExtKeyWidget('{$this->iId}', '{$this->sTargetClass}', '$sFilter', '$sTitle', false, $sWizHelper, '{$this->sAttCode}', $sJSSearchMode, $sJSDoSearch); + oACWidget_{$this->iId}.emptyHtml = "

$sMessage

"; + oACWidget_{$this->iId}.AddAutocomplete($iMinChars, $sWizHelperJSON); + if ($('#ac_dlg_{$this->iId}').length == 0) + { + $('body').append('
'); + } +JS + ); + } + if ($bExtensions && MetaModel::IsHierarchicalClass($this->sTargetClass) !== false) + { + $sHTMLValue .= "
iId}\" onClick=\"oACWidget_{$this->iId}.HKDisplay();\">
"; + $oPage->add_ready_script( + <<iId}').length == 0) + { + $('body').append('
'); + } +JS + ); + } + if ($bCreate && $bExtensions) + { + $sCallbackName = (MetaModel::IsAbstract($this->sTargetClass)) ? 'SelectObjectClass' : 'CreateObject'; + + $sHTMLValue .= "
iId}\" onClick=\"oACWidget_{$this->iId}.{$sCallbackName}();\">
"; + $oPage->add_ready_script( + <<iId}').length == 0) + { + $('body').append('
'); + } +JS + ); + } + $sHTMLValue .= "
"; + + // Note: This test is no longer necessary as we changed the markup to extract validation decoration in the standard .field_input_xxx container + //if (($sDisplayStyle == 'select') || ($sDisplayStyle == 'list')) + //{ + $sHTMLValue .= "iId}\">iId}\">"; + //} + + return $sHTMLValue; + } + + /** + * @since 2.8.0 N°2508 - Include Obsolescence icon within list and autocomplete + * Get the HTML fragment corresponding to the ext key editing widget + * @param WebPage $oP The web page used for all the output + * @param array $aArgs Extra context arguments + * @return string The HTML fragment to be inserted into the page + */ + public function DisplayRadio(WebPage $oPage, $iMaxComboLength, $bAllowTargetCreation, DBObjectset $oAllowedValues, $value, $sFieldName, $sDisplayStyle) + { + $oPage->add_linked_script('../js/forms-json-utils.js'); + + $bCreate = (!$this->bSearchMode) && (UserRights::IsActionAllowed($this->sTargetClass, UR_ACTION_BULK_MODIFY) && $bAllowTargetCreation); + $bExtensions = true; + $sAttrFieldPrefix = ($this->bSearchMode) ? '' : 'attr_'; + + $sHTMLValue = "
"; + $sFilter = addslashes($oAllowedValues->GetFilter()->ToOQL()); + + if (is_null($oAllowedValues)) + { + throw new Exception('Implementation: null value for allowed values definition'); + } + $oAllowedValues->SetShowObsoleteData(utils::ShowObsoleteData()); + + // We just need to compare the number of entries with MaxComboLength, so no need to get the real count. + if (!$oAllowedValues->CountExceeds($iMaxComboLength)) + { + // Discrete list of values, use a SELECT or RADIO buttons depending on the config + $sValidationField = null; + + $bVertical = ($sDisplayStyle != 'radio_horizontal'); + $bExtensions = false; + $oAllowedValues->Rewind(); + $aAllowedValues = array(); + while($oObj = $oAllowedValues->Fetch()) + { + $aAllowedValues[$oObj->GetKey()] = $oObj->GetName(); + } + $sHTMLValue .= $oPage->GetRadioButtons($aAllowedValues, $value, $this->iId, "{$sAttrFieldPrefix}{$sFieldName}", false /* $bMandatory will be placed manually */, $bVertical, $sValidationField); + $aEventsList[] ='change'; + } + else + { + $sHTMLValue .= "unable to display. Too much values"; + } + if ($bExtensions && MetaModel::IsHierarchicalClass($this->sTargetClass) !== false) + { + $sHTMLValue .= "
iId}\" onClick=\"oACWidget_{$this->iId}.HKDisplay();\">
"; + $oPage->add_ready_script( + <<iId}').length == 0) + { + $('body').append('
'); + } +JS + ); + } + if ($bCreate && $bExtensions) + { + $sCallbackName = (MetaModel::IsAbstract($this->sTargetClass)) ? 'SelectObjectClass' : 'CreateObject'; + + $sHTMLValue .= "
iId}\" onClick=\"oACWidget_{$this->iId}.{$sCallbackName}();\">
"; + $oPage->add_ready_script( + <<iId}').length == 0) + { + $('body').append('
'); + } +JS + ); + } + $sHTMLValue .= "
"; + + // Note: This test is no longer necessary as we changed the markup to extract validation decoration in the standard .field_input_xxx container + //if (($sDisplayStyle == 'select') || ($sDisplayStyle == 'list')) + //{ + $sHTMLValue .= "iId}\">iId}\">"; + //} + + return $sHTMLValue; + } + /** + * @deprecated Use DisplayBob * Get the HTML fragment corresponding to the ext key editing widget * @param WebPage $oP The web page used for all the output * @param array $aArgs Extra context arguments @@ -440,12 +733,12 @@ EOF $oValuesSet->SetSort(false); $oValuesSet->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', $this->bSearchMode); $oValuesSet->SetLimit($iMax); - $aValuesContains = $oValuesSet->GetValues(array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId), $sContains, 'start_with'); + $aValuesContains = $oValuesSet->GetValuesForAutocomplete(array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId), $sContains, 'start_with'); asort($aValuesContains); $aValues = $aValuesContains; if (sizeof($aValues) < $iMax) { - $aValuesContains = $oValuesSet->GetValues(array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId), $sContains, 'contains'); + $aValuesContains = $oValuesSet->GetValuesForAutocomplete(array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId), $sContains, 'contains'); asort($aValuesContains); $iSize = sizeof($aValuesContains); foreach ($aValuesContains as $sKey => $sFriendlyName) @@ -462,7 +755,7 @@ EOF } elseif (!in_array($sContains, $aValues)) { - $aValuesEquals = $oValuesSet->GetValues(array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId), $sContains, 'equals'); + $aValuesEquals = $oValuesSet->GetValuesForAutocomplete(array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId), $sContains, 'equals'); $aValues = array_merge($aValuesEquals, $aValues); } @@ -471,9 +764,9 @@ EOF case static::ENUM_OUTPUT_FORMAT_JSON: $aJsonMap = array(); - foreach ($aValues as $sKey => $sLabel) + foreach ($aValues as $sKey => $aValue) { - $aJsonMap[] = array('value' => $sKey, 'label' => $sLabel); + $aJsonMap[] = array('value' => $sKey, 'label' => $aValue['label'], 'obsolescence_flag' => $aValue['obsolescence_flag']); } $oP->SetContentType('application/json'); @@ -481,9 +774,9 @@ EOF break; case static::ENUM_OUTPUT_FORMAT_CSV: - foreach($aValues as $sKey => $sFriendlyName) + foreach($aValues as $sKey => $aValue) { - $oP->add(trim($sFriendlyName)."\t".$sKey."\n"); + $oP->add(trim($aValue['label'])."\t".$sKey."\n"); } break; default: diff --git a/core/valuesetdef.class.inc.php b/core/valuesetdef.class.inc.php index 0299f5c1e..ed4347e6b 100644 --- a/core/valuesetdef.class.inc.php +++ b/core/valuesetdef.class.inc.php @@ -220,7 +220,7 @@ class ValueSetObjects extends ValueSetDefinition $this->m_sOperation = $sOperation; $this->m_aValues = array(); - + if ($this->m_bAllowAllData) { $oFilter = DBObjectSearch::FromOQL_AllData($this->m_sFilterExpr); @@ -348,6 +348,137 @@ class ValueSetObjects extends ValueSetDefinition { $this->m_bSort = $bSort; } + + public function GetValuesForAutocomplete($aArgs, $sContains = '', $sOperation = 'contains', $aAdditionalFields = array()) + { + if (!$this->m_bIsLoaded || ($sContains != $this->m_sContains) || ($sOperation != $this->m_sOperation)) + { + $this->LoadValuesForAutocomplete($aArgs, $sContains, $sOperation, $aAdditionalFields); + $this->m_bIsLoaded = true; + } + // The results are already filtered and sorted (on friendly name) + $aRet = $this->m_aValues; + return $aRet; + } + + /** + * @param $aArgs + * @param string $sContains + * @param string $sOperation 'contains' or 'equals_start_with' + * + * @return bool + * @throws \CoreException + * @throws \OQLException + */ + protected function LoadValuesForAutocomplete($aArgs, $sContains = '', $sOperation = 'contains', $aAdditionalFields = array()) + { + $this->m_sContains = $sContains; + $this->m_sOperation = $sOperation; + + $this->m_aValues = array(); + + if ($this->m_bAllowAllData) + { + $oFilter = DBObjectSearch::FromOQL_AllData($this->m_sFilterExpr); + } + else + { + $oFilter = DBObjectSearch::FromOQL($this->m_sFilterExpr); + $oFilter->SetShowObsoleteData(utils::ShowObsoleteData()); + } + if (!$oFilter) return false; + if (!is_null($this->m_oExtraCondition)) + { + $oFilter = $oFilter->Intersect($this->m_oExtraCondition); + } + foreach($this->m_aModifierProperties as $sPluginClass => $aProperties) + { + foreach ($aProperties as $sProperty => $value) + { + $oFilter->SetModifierProperty($sPluginClass, $sProperty, $value); + } + } + + $oExpression = DBObjectSearch::GetPolymorphicExpression($oFilter->GetClass(), 'friendlyname'); + $sClass = $oFilter->GetClass(); + + switch ($sOperation) + { + case 'equals': + $aAttributes = MetaModel::GetFriendlyNameAttributeCodeList($sClass); + $sClassAlias = $oFilter->GetClassAlias(); + $aFilters = array(); + $oValueExpr = new ScalarExpression($sContains); + foreach($aAttributes as $sAttribute) + { + $oNewFilter = $oFilter->DeepClone(); + $oNameExpr = new FieldExpression($sAttribute, $sClassAlias); + $oCondition = new BinaryExpression($oNameExpr, '=', $oValueExpr); + $oNewFilter->AddConditionExpression($oCondition); + $aFilters[] = $oNewFilter; + } + // Unions are much faster than OR conditions + $oFilter = new DBUnionSearch($aFilters); + break; + case 'start_with': + $aAttributes = MetaModel::GetFriendlyNameAttributeCodeList($sClass); + $sClassAlias = $oFilter->GetClassAlias(); + $aFilters = array(); + $oValueExpr = new ScalarExpression($sContains.'%'); + foreach($aAttributes as $sAttribute) + { + $oNewFilter = $oFilter->DeepClone(); + $oNameExpr = new FieldExpression($sAttribute, $sClassAlias); + $oCondition = new BinaryExpression($oNameExpr, 'LIKE', $oValueExpr); + $oNewFilter->AddConditionExpression($oCondition); + $aFilters[] = $oNewFilter; + } + // Unions are much faster than OR conditions + $oFilter = new DBUnionSearch($aFilters); + break; + + default: + $oValueExpr = new ScalarExpression('%'.$sContains.'%'); + $oNameExpr = new FieldExpression('friendlyname', $oFilter->GetClassAlias()); + $oNewCondition = new BinaryExpression($oNameExpr, 'LIKE', $oValueExpr); + $oFilter->AddConditionExpression($oNewCondition); + break; + } + + $oObjects = new DBObjectSet($oFilter, $this->m_aOrderBy, $aArgs, null, $this->m_iLimit, 0, $this->m_bSort); + if (empty($this->m_sValueAttCode)) + { + $aAttToLoad = array($oFilter->GetClassAlias() => array('friendlyname')); + } + else + { + $aAttToLoad = array($oFilter->GetClassAlias() => array($this->m_sValueAttCode)); + } + $aAttToLoad=array_merge($aAttToLoad,$aAdditionalFields); + $oObjects->OptimizeColumnLoad($aAttToLoad); + while ($oObject = $oObjects->Fetch()) + { + $aData=[]; + if (empty($this->m_sValueAttCode)) + { + $aData['label'] = $oObject->GetName(); + } + else + { + $aData['label'] = $oObject->Get($this->m_sValueAttCode); + } + if($oObject->IsObsolete()) + { + $aData['obsolescence_flag']='1'; + } + foreach ($aAdditionalFields as $sFieldName) + { + $aData[$sFieldName] = $oObject->Get($sFieldName); + } + $this->m_aValues[$oObject->GetKey()] = $aData; + } + return true; + } } diff --git a/js/extkeywidget.js b/js/extkeywidget.js index 8a258437f..34e951e80 100644 --- a/js/extkeywidget.js +++ b/js/extkeywidget.js @@ -38,78 +38,121 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper $('#'+this.id+'_btnRemove').prop('disabled',true); $('#'+this.id+'_linksToRemove').val(''); } + this.AddSelectize = function(options, initValue) + { + $('#'+me.id).selectize({ + render: { + item: function(item) { + if ( item.obsolescence_flag == 1) + { + console.warn("ici"); + val = ''+item.label; + } + else + { + val = item.label; + } + return $("
") + .append(val); + }, + option: function(item) { + console.warn(item); + console.warn(item.obsolescence_flag); + console.warn($.inArray(item, 'obsolescence_flag')); + if ( item.obsolescence_flag == 1) + { + console.warn("ici"); + val = ''+item.label; + } + else + { + val = item.label; + } + return $("
") + .append(val); + } + }, + items:[initValue], + valueField: 'value', + labelField: 'label', + searchField: ['value'], + options:JSON.parse(options), + maxItems: 1, + }); + } this.AddAutocomplete = function(iMinChars, sWizHelperJSON) { var hasFocus = 0; var cache = {}; $('#label_'+me.id).autocomplete({ - source: function (request, response) { - term = request.term.toLowerCase().latinise().replace(/[\u0300-\u036f]/g, ""); + source: function (request, response) { + term = request.term.toLowerCase().latinise().replace(/[\u0300-\u036f]/g, ""); - if (term in cache) - { - response(cache[term]); - return; - } - if (term.indexOf(this.previous) >= 0 && cache[this.previous] != null && cache[this.previous].length < 120) - { - //we have already all the possibility in cache - var data = []; - $.each(cache[this.previous], function (key, value) { - if (value.label.toLowerCase().latinise().replace(/[\u0300-\u036f]/g, "").indexOf(term) >= 0) - { - data.push(value); - } - }); - cache[term] = data; - response(data); - } - else - { - $.post({ - url: GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', - dataType: "json", - data: { - q: request.term, - operation: 'ac_extkey', - sTargetClass: me.sTargetClass, - sFilter: me.sFilter, - bSearchMode: me.bSearchMode, - sOutputFormat: 'json', - json: function () { - return sWizHelperJSON; - } - }, - success: function (data) { - cache[term] = data; - response(data); - } - }); - - } - }, - autoFocus: true, - minLength: iMinChars, - focus: function (event, ui) { - // $('#label_$this->iId').val( ui.item.label ); - return false; - }, - select: function (event, ui) { - $('#'+me.id).val(ui.item.value); - $('#label_'+me.id).val(ui.item.label); - $('#'+me.id).trigger('validate'); - $('#'+me.id).trigger('extkeychange'); - $('#'+me.id).trigger('change'); - return false; + if (term in cache) + { + response(cache[term]); + return; } - }) - .autocomplete("instance")._renderItem = function (ul, item) { + if (term.indexOf(this.previous) >= 0 && cache[this.previous] != null && cache[this.previous].length < 120) + { + //we have already all the possibility in cache + var data = []; + $.each(cache[this.previous], function (key, value) { + if (value.label.toLowerCase().latinise().replace(/[\u0300-\u036f]/g, "").indexOf(term) >= 0) + { + data.push(value); + } + }); + cache[term] = data; + response(data); + } + else + { + $.post({ + url: GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', + dataType: "json", + data: { + q: request.term, + operation: 'ac_extkey', + sTargetClass: me.sTargetClass, + sFilter: me.sFilter, + bSearchMode: me.bSearchMode, + sOutputFormat: 'json', + json: function () { + return sWizHelperJSON; + } + }, + success: function (data) { + cache[term] = data; + response(data); + } + }); + + } + }, + autoFocus: true, + minLength: iMinChars, + focus: function (event, ui) { + // $('#label_$this->iId').val( ui.item.label ); + return false; + }, + select: function (event, ui) { + $('#'+me.id).val(ui.item.value); + $('#label_'+me.id).val(ui.item.label); + $('#'+me.id).trigger('validate'); + $('#'+me.id).trigger('extkeychange'); + $('#'+me.id).trigger('change'); + return false; + } + }) + .autocomplete("instance")._renderItem = function (ul, item) { var term = this.term.replace("/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi", "\\$1"); var val = item.label.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)("+term+")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); - if (item.obsolete == 'yes') + if (item.obsolescence_flag == '1') { - val = val+' old'; + val = ' '+val; } + val=''+val+''; return $("
  • ") .append(val) .appendTo(ul); diff --git a/sources/renderer/console/fieldrenderer/consoleselectobjectfieldrenderer.class.inc.php b/sources/renderer/console/fieldrenderer/consoleselectobjectfieldrenderer.class.inc.php index 3012832e6..a96b1a5ee 100644 --- a/sources/renderer/console/fieldrenderer/consoleselectobjectfieldrenderer.class.inc.php +++ b/sources/renderer/console/fieldrenderer/consoleselectobjectfieldrenderer.class.inc.php @@ -91,11 +91,10 @@ class ConsoleSelectObjectFieldRenderer extends FieldRenderer $sFormPrefix = ''; $oWidget = new \UIExtKeyWidget($sTargetClass, $sFieldId, '', true); $aArgs = array(); - $sDisplayStyle = 'select'; $sTitle = $this->oField->GetLabel(); require_once(APPROOT.'application/capturewebpage.class.inc.php'); $oPage = new \CaptureWebPage(); - $sHTMLValue = $oWidget->Display($oPage, $iMaxComboLength, false /* $bAllowTargetCreation */, $sTitle, $oSet, $this->oField->GetCurrentValue(), $sFieldId, $this->oField->GetMandatory(), $sFieldName, $sFormPrefix, $aArgs, null, $sDisplayStyle); + $sHTMLValue = $oWidget->DisplaySelect($oPage, $iMaxComboLength, false /* $bAllowTargetCreation */, $sTitle, $oSet, $this->oField->GetCurrentValue(), $this->oField->GetMandatory(), $sFieldName, $sFormPrefix, $aArgs); $oOutput->AddHtml($sHTMLValue); $oOutput->AddHtml($oPage->GetHtml()); $oOutput->AddJs($oPage->GetJS());