diff --git a/application/ui.extkeywidget.class.inc.php b/application/ui.extkeywidget.class.inc.php index 1ddb35e0b..fbca8f231 100644 --- a/application/ui.extkeywidget.class.inc.php +++ b/application/ui.extkeywidget.class.inc.php @@ -101,7 +101,7 @@ class UIExtKeyWidget /** * Get the HTML fragment corresponding to the ext key editing widget * @param WebPage $oP The web page used for all the output - * @param Hash $aArgs Extra context arguments + * @param array $aArgs Extra context arguments * @return string The HTML fragment to be inserted into the page */ public function Display(WebPage $oPage, $iMaxComboLength, $bAllowTargetCreation, $sTitle, DBObjectset $oAllowedValues, $value, $iInputId, $bMandatory, $sFieldName, $sFormPrefix = '', $aArgs = array(), $bSearchMode = null, $sDisplayStyle = 'select', $bSearchMultiple = true) @@ -145,7 +145,12 @@ class UIExtKeyWidget throw new Exception('Implementation: null value for allowed values definition'); } $oAllowedValues->SetShowObsoleteData(utils::ShowObsoleteData()); - if ($oAllowedValues->Count() < $iMaxComboLength) + // Don't automatically launch the search if the table is huge + $bDoSearch = !utils::IsHighCardinality($this->sTargetClass); + $sJSDoSearch = $bDoSearch ? 'true' : 'false'; + + // 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 switch($sDisplayStyle) @@ -170,7 +175,6 @@ class UIExtKeyWidget case 'select': case 'list': default: - $sSelectMode = 'true'; $sHelpText = ''; //$this->oAttDef->GetHelpOnEdition(); $sHTMLValue .= "
\n"; @@ -203,13 +207,13 @@ class UIExtKeyWidget if (($oAllowedValues->Count() == 1) && ($bMandatory == 'true') ) { // When there is only once choice, select it by default - $sSelected = ' selected'; + $sSelected = 'selected'; } else { - $sSelected = (is_array($value) && in_array($key, $value)) || ($value == $key) ? ' selected' : ''; + $sSelected = (is_array($value) && in_array($key, $value)) || ($value == $key) ? 'selected' : ''; } - $sHTMLValue .= "\n"; + $sHTMLValue .= "\n"; } $sHTMLValue .= "\n"; $sHTMLValue .= "
\n"; @@ -229,7 +233,7 @@ class UIExtKeyWidget } $oPage->add_ready_script( <<iId} = new ExtKeyWidget('{$this->iId}', '{$this->sTargetClass}', '$sFilter', '$sTitle', true, $sWizHelper, '{$this->sAttCode}', $sJSSearchMode); + oACWidget_{$this->iId} = new ExtKeyWidget('{$this->iId}', '{$this->sTargetClass}', '$sFilter', '$sTitle', true, $sWizHelper, '{$this->sAttCode}', $sJSSearchMode, $sJSDoSearch); oACWidget_{$this->iId}.emptyHtml = "

$sMessage

"; $('#$this->iId').bind('update', function() { oACWidget_{$this->iId}.Update(); } ); $('#$this->iId').bind('change', function() { $(this).trigger('extkeychange') } ); @@ -241,8 +245,6 @@ EOF else { // Too many choices, use an autocomplete - $sSelectMode = 'false'; - // Check that the given value is allowed $oSearch = $oAllowedValues->GetFilter(); $oSearch->AddCondition('id', $value); @@ -261,10 +263,9 @@ EOF $sDisplayValue = $this->GetObjectName($value); } $iMinChars = isset($aArgs['iMinChars']) ? $aArgs['iMinChars'] : 3; //@@@ $this->oAttDef->GetMinAutoCompleteChars(); - $iFieldSize = isset($aArgs['iFieldSize']) ? $aArgs['iFieldSize'] : 20; //@@@ $this->oAttDef->GetMaxSize(); // the input for the auto-complete - $sHTMLValue .= "Count()."\" type=\"text\" id=\"label_$this->iId\" value=\"$sDisplayValue\"/>"; + $sHTMLValue .= "iId\" value=\"$sDisplayValue\"/>"; $sHTMLValue .= "iId}\" style=\"border:0;vertical-align:middle;cursor:pointer;\" src=\"../images/mini_search.gif?t=".utils::GetCacheBusterTimestamp()."\" onClick=\"oACWidget_{$this->iId}.Search();\"/>"; // another hidden input to store & pass the object's Id @@ -274,7 +275,7 @@ EOF // 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); + oACWidget_{$this->iId} = new ExtKeyWidget('{$this->iId}', '{$this->sTargetClass}', '$sFilter', '$sTitle', false, $sWizHelper, '{$this->sAttCode}', $sJSSearchMode, $sJSDoSearch); oACWidget_{$this->iId}.emptyHtml = "

$sMessage

"; $('#label_$this->iId').autocomplete(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', { scroll:true, minChars:{$iMinChars}, autoFill:false, matchContains:true, mustMatch: true, keyHolder:'#{$this->iId}', extraParams:{operation:'ac_extkey', sTargetClass:'{$this->sTargetClass}',sFilter:'$sFilter',bSearchMode:$JSSearchMode, json: function() { return $sWizHelperJSON; } }}); $('#label_$this->iId').keyup(function() { if ($(this).val() == '') { $('#$this->iId').val(''); } } ); // Useful for search forms: empty value in the "label", means no value, immediatly ! @@ -376,9 +377,13 @@ EOF /** * Search for objects to be selected + * * @param WebPage $oP The page used for the output (usually an AjaxWebPage) + * @param $sFilter * @param string $sRemoteClass Name of the "remote" class to perform the search on, must be a derived class of m_sRemoteClass - * @param Array $aAlreadyLinkedIds List of IDs of objects of "remote" class already linked, to be filtered out of the search + * @param null $oObj + * + * @throws \OQLException */ public function SearchObjectsToSelect(WebPage $oP, $sFilter, $sRemoteClass = '', $oObj = null) { @@ -403,10 +408,14 @@ EOF /** * Search for objects to be selected + * * @param WebPage $oP The page used for the output (usually an AjaxWebPage) * @param string $sFilter The OQL expression used to define/limit limit the scope of possible values * @param DBObject $oObj The current object for the OQL context * @param string $sContains The text of the autocomplete to filter the results + * @param string $sOutputFormat + * + * @throws \CoreException */ public function AutoComplete(WebPage $oP, $sFilter, $oObj = null, $sContains, $sOutputFormat = self::ENUM_OUTPUT_FORMAT_CSV) { @@ -419,8 +428,26 @@ EOF $iCurrentExtKeyId = (is_null($oObj) || $this->sAttCode === '') ? 0 : $oObj->Get($this->sAttCode); $oValuesSet = new ValueSetObjects($sFilter, 'friendlyname'); // Bypass GetName() to avoid the encoding by htmlentities + $iMax = 150; + $oValuesSet->SetLimit($iMax); + $oValuesSet->SetSort(false); $oValuesSet->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', $this->bSearchMode); - $aValues = $oValuesSet->GetValues(array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId), $sContains); + + $aValues = $oValuesSet->GetValues(array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId), $sContains, 'equals_start_with'); + $iMax -= count($aValues); + if ($iMax > 0) + { + $oValuesSet->SetLimit($iMax); + $aValuesContains = $oValuesSet->GetValues(array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId), $sContains, 'contains'); + asort($aValuesContains); + foreach($aValuesContains as $sKey => $sFriendlyName) + { + if (!isset($aValues[$sKey])) + { + $aValues[$sKey] = $sFriendlyName; + } + } + } switch($sOutputFormat) { @@ -461,12 +488,15 @@ EOF } } - /** + /** * Get the form to select a leaf class from the $this->sTargetClass (that should be abstract) * Note: Inspired from UILinksWidgetDirect::GetObjectCreationDialog() * - * @param WebPage $oPage - */ + * @param WebPage $oPage + * + * @throws \CoreException + * @throws \DictExceptionMissingString + */ public function GetClassSelectionForm(WebPage $oPage) { // For security reasons: check that the "proposed" class is actually a subclass of the linked class @@ -568,21 +598,11 @@ EOF { throw new Exception('Implementation: null value for allowed values definition'); } - try - { - $oFilter = DBObjectSearch::FromOQL($sFilter); - $oFilter->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', $this->bSearchMode); - $oSet = new DBObjectSet($oFilter, array(), array('this' => $oObj, 'current_extkey_id' => $currValue)); - } - catch(MissingQueryArgument $e) - { - // When used in a search form the $this parameter may be missing, in this case return all possible values... - // TODO check if we can improve this behavior... - $sOQL = 'SELECT '.$this->m_sTargetClass; - $oFilter = DBObjectSearch::FromOQL($sOQL); - $oFilter->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', $this->bSearchMode); - $oSet = new DBObjectSet($oFilter); - } + + $oFilter = DBObjectSearch::FromOQL($sFilter); + $oFilter->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', $this->bSearchMode); + $oSet = new DBObjectSet($oFilter, array(), array('this' => $oObj, 'current_extkey_id' => $currValue)); + $oSet->SetShowObsoleteData(utils::ShowObsoleteData()); $sHKAttCode = MetaModel::IsHierarchicalClass($this->sTargetClass); diff --git a/application/ui.linksdirectwidget.class.inc.php b/application/ui.linksdirectwidget.class.inc.php index a4056352c..db2459869 100644 --- a/application/ui.linksdirectwidget.class.inc.php +++ b/application/ui.linksdirectwidget.class.inc.php @@ -275,7 +275,10 @@ class UILinksWidgetDirect $sJSONLabels = json_encode($aLabels); $sJSONButtons = json_encode($aButtons); $sWizHelper = 'oWizardHelper'.$sFormPrefix; - $oPage->add_ready_script("$('#{$this->sInputid}').directlinks({class_name: '$this->sClass', att_code: '$this->sAttCode', input_name:'$sInputName', labels: $sJSONLabels, submit_to: '$sSubmitUrl', buttons: $sJSONButtons, oWizardHelper: $sWizHelper });"); + // Don't automatically launch the search if the table is huge + $bDoSearch = !utils::IsHighCardinality($this->sLinkedClass); + $sJSDoSearch = $bDoSearch ? 'true' : 'false'; + $oPage->add_ready_script("$('#{$this->sInputid}').directlinks({class_name: '$this->sClass', att_code: '$this->sAttCode', input_name:'$sInputName', labels: $sJSONLabels, submit_to: '$sSubmitUrl', buttons: $sJSONButtons, oWizardHelper: $sWizHelper, do_search: $sJSDoSearch});"); } /** @@ -337,6 +340,7 @@ class UILinksWidgetDirect 'result_list_outer_selector' => "SearchResultsToAdd_{$this->sInputid}", 'table_id' => "add_{$this->sInputid}", 'table_inner_id' => "ResultsToAdd_{$this->sInputid}", + 'selection_mode' => true, 'cssCount' => "#count_{$this->sInputid}", 'query_params' => $oFilter->GetInternalParams(), 'hidden_criteria' => $sHiddenCriteria, diff --git a/application/ui.linkswidget.class.inc.php b/application/ui.linkswidget.class.inc.php index eaed45110..914c6c766 100644 --- a/application/ui.linkswidget.class.inc.php +++ b/application/ui.linkswidget.class.inc.php @@ -194,7 +194,7 @@ class UILinksWidget $aArgs['prefix'] = $sPrefix; $aArgs['wizHelper'] = "oWizardHelper{$this->m_iInputId}_".($iUniqueId < 0 ? -$iUniqueId : $iUniqueId); $aArgs['this'] = $oNewLinkObj; - $aRow['form::checkbox'] = "m_iInputId.".OnSelectChange();\" value=\"-$iUniqueId\">"; + $aRow['form::checkbox'] = "m_iInputId.".OnChange();\" value=\"-$iUniqueId\">"; foreach($this->m_aEditableFields as $sFieldCode) { $sFieldId = $this->m_iInputId.'_'.$sFieldCode.'['.-$iUniqueId.']'; @@ -365,13 +365,16 @@ EOF } $sHtmlValue .= $this->DisplayFormTable($oPage, $this->m_aTableConfig, $aForm); $sDuplicates = ($this->m_bDuplicatesAllowed) ? 'true' : 'false'; + // Don't automatically launch the search if the table is huge + $bDoSearch = !utils::IsHighCardinality($this->m_sRemoteClass); + $sJSDoSearch = $bDoSearch ? 'true' : 'false'; $sWizHelper = 'oWizardHelper'.$sFormPrefix; $oPage->add_ready_script(<<m_iInputId} = new LinksWidget('{$this->m_sAttCode}{$this->m_sNameSuffix}', '{$this->m_sClass}', '{$this->m_sAttCode}', '{$this->m_iInputId}', '{$this->m_sNameSuffix}', $sDuplicates, $sWizHelper, '{$this->m_sExtKeyToRemote}'); + oWidget{$this->m_iInputId} = new LinksWidget('{$this->m_sAttCode}{$this->m_sNameSuffix}', '{$this->m_sClass}', '{$this->m_sAttCode}', '{$this->m_iInputId}', '{$this->m_sNameSuffix}', $sDuplicates, $sWizHelper, '{$this->m_sExtKeyToRemote}', $sJSDoSearch); oWidget{$this->m_iInputId}.Init(); EOF ); - $sHtmlValue .= "     m_sAttCode}{$this->m_sNameSuffix}_btnRemove\" type=\"button\" value=\"".Dict::S('UI:RemoveLinkedObjectsOf_Class')."\" onClick=\"oWidget{$this->m_iInputId}.RemoveSelected();\" >"; + $sHtmlValue .= "     m_sAttCode}{$this->m_sNameSuffix}_btnRemove\" type=\"button\" value=\"".Dict::S('UI:RemoveLinkedObjectsOf_Class')."\" onClick=\"oWidget{$this->m_iInputId}.Removeed();\" >"; $sHtmlValue .= "   m_sAttCode}{$this->m_sNameSuffix}_btnAdd\" type=\"button\" value=\"".Dict::Format('UI:AddLinkedObjectsOf_Class', MetaModel::GetName($this->m_sRemoteClass))."\" onClick=\"oWidget{$this->m_iInputId}.AddObjects();\">m_sAttCode}{$this->m_sNameSuffix}_indicatorAdd\">\n"; $sHtmlValue .= "

 

\n"; $sHtmlValue .= "\n"; diff --git a/application/utils.inc.php b/application/utils.inc.php index ec2ae3cb2..cec12fade 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -1924,4 +1924,17 @@ class utils } return COMPILATION_TIMESTAMP; } + + /** + * Check if the given class if configured as a high cardinality class. + * + * @param $sClass + * + * @return bool + */ + public static function IsHighCardinality($sClass) + { + $aHugeClasses = MetaModel::GetConfig()->Get('high_cardinality_classes'); + return in_array($sClass, $aHugeClasses); + } } diff --git a/core/config.class.inc.php b/core/config.class.inc.php index f34a49a3f..16c19599b 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -1111,12 +1111,20 @@ class Config ), 'search_manual_submit' => array( 'type' => 'array', - 'description' => 'Force manual submit of search requests (class => true)', + 'description' => 'Force manual submit of search all requests', 'default' => false, 'value' => true, 'source_of_value' => '', 'show_in_conf_sample' => true, ), + 'high_cardinality_classes' => array( + 'type' => 'array', + 'description' => 'List of classes with high cardinality (Force manual submit of search)', + 'default' => array(), + 'value' => array(), + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), ); public function IsProperty($sPropCode) diff --git a/core/dbobjectset.class.php b/core/dbobjectset.class.php index 438f5ea5a..8c6fecc6e 100644 --- a/core/dbobjectset.class.php +++ b/core/dbobjectset.class.php @@ -77,18 +77,20 @@ class DBObjectSet implements iDBObjectSetIterator * @var mysqli_result */ protected $m_oSQLResult; + protected $m_bSort; /** * Create a new set based on a Search definition. * * @param DBSearch $oFilter The search filter defining the objects which are part of the set (multiple columns/objects per row are supported) - * @param hash $aOrderBy Array of '[.]attcode' => bAscending - * @param hash $aArgs Values to substitute for the search/query parameters (if any). Format: param_name => value - * @param hash $aExtendedDataSpec + * @param array $aOrderBy Array of '[.]attcode' => bAscending + * @param array $aArgs Values to substitute for the search/query parameters (if any). Format: param_name => value + * @param array $aExtendedDataSpec * @param int $iLimitCount Maximum number of rows to load (i.e. equivalent to MySQL's LIMIT start, count) * @param int $iLimitStart Index of the first row to load (i.e. equivalent to MySQL's LIMIT start, count) + * @param bool $bSort if false no order by is done */ - public function __construct(DBSearch $oFilter, $aOrderBy = array(), $aArgs = array(), $aExtendedDataSpec = null, $iLimitCount = 0, $iLimitStart = 0) + public function __construct(DBSearch $oFilter, $aOrderBy = array(), $aArgs = array(), $aExtendedDataSpec = null, $iLimitCount = 0, $iLimitStart = 0, $bSort = true) { $this->m_oFilter = $oFilter->DeepClone(); $this->m_aAddedIds = array(); @@ -98,6 +100,7 @@ class DBObjectSet implements iDBObjectSetIterator $this->m_aExtendedDataSpec = $aExtendedDataSpec; $this->m_iLimitCount = $iLimitCount; $this->m_iLimitStart = $iLimitStart; + $this->m_bSort = $bSort; $this->m_iNumTotalDBRows = null; $this->m_iNumLoadedDBRows = 0; @@ -601,10 +604,15 @@ class DBObjectSet implements iDBObjectSetIterator * * Limitation: the sort order has no effect on objects added in-memory * - * @return hash Format: field_code => boolean (true = ascending, false = descending) + * @return array Format: field_code => boolean (true = ascending, false = descending) */ public function GetRealSortOrder() { + if (!$this->m_bSort) + { + // No order by + return array(); + } // Get the class default sort order if not specified with the API // if (empty($this->m_aOrderBy)) @@ -719,7 +727,19 @@ class DBObjectSet implements iDBObjectSetIterator return $this->m_iNumTotalDBRows + count($this->m_aAddedObjects); // Does it fix Trac #887 ?? } - + + public function CountExceeds($iLimit) + { + $sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, $iLimit + 2, 0, true); + $resQuery = CMDBSource::Query($sSQL); + if (!$resQuery) return (0 > $iLimit); + + $aRow = CMDBSource::FetchArray($resQuery); + CMDBSource::FreeResult($resQuery); + + return (intval($aRow['COUNT']) > $iLimit); + } + /** * Number of rows available in memory (loaded from DB + added in memory) * diff --git a/core/metamodel.class.php b/core/metamodel.class.php index 1e0abab86..03e2103ce 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -638,6 +638,20 @@ abstract class MetaModel return reset($aAttributes); } + /** + * Returns the list of attributes composing the friendlyname + * + * @param $sClass + * + * @return array + */ + final static public function GetFriendlyNameAttributeCodeList($sClass) + { + $aNameSpec = self::GetNameSpec($sClass); + $aAttributes = $aNameSpec[1]; + return $aAttributes; + } + /** * @param string $sClass * @@ -5034,6 +5048,7 @@ abstract class MetaModel // Check that any defined field exists // $aTableInfo['Fields'][$sKeyField]['used'] = true; + $aFriendlynameAttcodes = self::GetFriendlyNameAttributeCodeList($sClass); foreach(self::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) { if (!$oAttDef->CopyOnAllTables()) @@ -5050,6 +5065,14 @@ abstract class MetaModel $aTableInfo['Fields'][$sField]['used'] = true; $bIndexNeeded = $oAttDef->RequiresIndex(); + if (!$bIndexNeeded) + { + // Add an index on the columns of the friendlyname + if (in_array($sField, $aFriendlynameAttcodes)) + { + $bIndexNeeded = true; + } + } $sFieldDefinition = "`$sField` $sDBFieldSpec"; if (!CMDBSource::IsField($sTable, $sField)) diff --git a/core/sqlobjectquery.class.inc.php b/core/sqlobjectquery.class.inc.php index 47e89ae39..821e76883 100644 --- a/core/sqlobjectquery.class.inc.php +++ b/core/sqlobjectquery.class.inc.php @@ -339,6 +339,14 @@ class SQLObjectQuery extends SQLQuery $this->PrepareRendering(); $sFrom = self::ClauseFrom($this->__aFrom, $sIndent); $sWhere = self::ClauseWhere($this->m_oConditionExpr, $aArgs); + if ($iLimitCount > 0) + { + $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; + } + else + { + $sLimit = ''; + } if ($bGetCount) { if (count($this->__aSelectedIdFields) > 0) @@ -349,11 +357,13 @@ class SQLObjectQuery extends SQLQuery $aCountFields[] = "COALESCE($sFieldExpr, 0)"; // Null values are excluded from the count } $sCountFields = implode(', ', $aCountFields); - $sSQL = "SELECT$sLineSep COUNT(DISTINCT $sCountFields) AS COUNT$sLineSep FROM $sFrom$sLineSep WHERE $sWhere"; + // Count can be limited for performance reason, in this case the total amount is not important, + // we only need to know if the number of entries is greater than a certain amount. + $sSQL = "SELECT COUNT(*) AS COUNT FROM (SELECT$sLineSep DISTINCT $sCountFields $sLineSep FROM $sFrom$sLineSep WHERE $sWhere $sLimit) AS _tatooine_"; } else { - $sSQL = "SELECT$sLineSep COUNT(*) AS COUNT$sLineSep FROM $sFrom$sLineSep WHERE $sWhere"; + $sSQL = "SELECT COUNT(*) AS COUNT FROM (SELECT$sLineSep 1 $sLineSep FROM $sFrom$sLineSep WHERE $sWhere $sLimit) AS _tatooine_"; } } else @@ -364,14 +374,7 @@ class SQLObjectQuery extends SQLQuery { $sOrderBy = "ORDER BY $sOrderBy$sLineSep"; } - if ($iLimitCount > 0) - { - $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; - } - else - { - $sLimit = ''; - } + $sSQL = "SELECT$sLineSep DISTINCT $sSelect$sLineSep FROM $sFrom$sLineSep WHERE $sWhere$sLineSep $sOrderBy $sLimit"; } return $sSQL; diff --git a/core/sqlunionquery.class.inc.php b/core/sqlunionquery.class.inc.php index 5d3e81320..6ff2bb1a0 100644 --- a/core/sqlunionquery.class.inc.php +++ b/core/sqlunionquery.class.inc.php @@ -103,29 +103,33 @@ class SQLUnionQuery extends SQLQuery // Render SELECTS without orderby/limit/count $aSelects[] = $oSQLQuery->RenderSelect(array(), $aArgs, 0, 0, false, $bBeautifulQuery); } - $sSelects = '('.implode(")$sLineSep UNION$sLineSep(", $aSelects).')'; + if ($iLimitCount > 0) + { + $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; + } + else + { + $sLimit = ''; + } if ($bGetCount) { + $sSelects = '('.implode(" $sLimit)$sLineSep UNION$sLineSep(", $aSelects)." $sLimit)"; $sFrom = "($sLineSep$sSelects$sLineSep) as __selects__"; - $sSQL = "SELECT$sLineSep COUNT(*) AS COUNT$sLineSep FROM $sFrom$sLineSep"; + $sSQL = "SELECT COUNT(*) AS COUNT FROM (SELECT$sLineSep 1 $sLineSep FROM $sFrom$sLineSep) AS _union_tatooine_"; } else { $sOrderBy = $this->aQueries[0]->RenderOrderByClause($aOrderBy); if (!empty($sOrderBy)) { - $sOrderBy = "ORDER BY $sOrderBy$sLineSep"; - } - if ($iLimitCount > 0) - { - $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; + $sOrderBy = "ORDER BY $sOrderBy$sLineSep $sLimit"; + $sSQL = '('.implode(")$sLineSep UNION$sLineSep (", $aSelects).')'.$sLineSep.$sOrderBy; } else { - $sLimit = ''; + $sSQL = '('.implode(" $sLimit)$sLineSep UNION$sLineSep (", $aSelects)." $sLimit)"; } - $sSQL = $sSelects.$sLineSep.$sOrderBy.' '.$sLimit; } return $sSQL; } diff --git a/core/valuesetdef.class.inc.php b/core/valuesetdef.class.inc.php index e9846f77d..251083762 100644 --- a/core/valuesetdef.class.inc.php +++ b/core/valuesetdef.class.inc.php @@ -52,7 +52,7 @@ abstract class ValueSetDefinition } - public function GetValues($aArgs, $sContains = '') + public function GetValues($aArgs, $sContains = '', $sOperation = 'contains') { if (!$this->m_bIsLoaded) { @@ -93,12 +93,16 @@ abstract class ValueSetDefinition class ValueSetObjects extends ValueSetDefinition { protected $m_sContains; + protected $m_sOperation; protected $m_sFilterExpr; // in OQL protected $m_sValueAttCode; protected $m_aOrderBy; protected $m_aExtraConditions; private $m_bAllowAllData; private $m_aModifierProperties; + private $m_bSort; + private $m_iLimit; + /** * @param hash $aOrderBy Array of '[.]attcode' => bAscending @@ -106,12 +110,15 @@ class ValueSetObjects extends ValueSetDefinition public function __construct($sFilterExp, $sValueAttCode = '', $aOrderBy = array(), $bAllowAllData = false, $aModifierProperties = array()) { $this->m_sContains = ''; + $this->m_sOperation = ''; $this->m_sFilterExpr = $sFilterExp; $this->m_sValueAttCode = $sValueAttCode; $this->m_aOrderBy = $aOrderBy; $this->m_bAllowAllData = $bAllowAllData; $this->m_aModifierProperties = $aModifierProperties; $this->m_aExtraConditions = array(); + $this->m_bSort = true; + $this->m_iLimit = 0; } public function SetModifierProperty($sPluginClass, $sProperty, $value) @@ -163,11 +170,11 @@ class ValueSetObjects extends ValueSetDefinition return new DBObjectSet($oFilter, $this->m_aOrderBy, $aArgs); } - public function GetValues($aArgs, $sContains = '') + public function GetValues($aArgs, $sContains = '', $sOperation = 'contains') { - if (!$this->m_bIsLoaded || ($sContains != $this->m_sContains)) + if (!$this->m_bIsLoaded || ($sContains != $this->m_sContains) || ($sOperation != $this->m_sOperation)) { - $this->LoadValues($aArgs, $sContains); + $this->LoadValues($aArgs, $sContains, $sOperation); $this->m_bIsLoaded = true; } // The results are already filtered and sorted (on friendly name) @@ -175,9 +182,19 @@ class ValueSetObjects extends ValueSetDefinition return $aRet; } - protected function LoadValues($aArgs, $sContains = '') + /** + * @param $aArgs + * @param string $sContains + * @param string $sOperation 'contains' or 'equals_start_with' + * + * @return bool + * @throws \CoreException + * @throws \OQLException + */ + protected function LoadValues($aArgs, $sContains = '', $sOperation = 'contains') { $this->m_sContains = $sContains; + $this->m_sOperation = $sOperation; $this->m_aValues = array(); @@ -202,12 +219,54 @@ class ValueSetObjects extends ValueSetDefinition } } - $oValueExpr = new ScalarExpression('%'.$sContains.'%'); - $oNameExpr = new FieldExpression('friendlyname', $oFilter->GetClassAlias()); - $oNewCondition = new BinaryExpression($oNameExpr, 'LIKE', $oValueExpr); - $oFilter->AddConditionExpression($oNewCondition); + switch ($sOperation) + { + case 'equals_start_with': + $aAttributes = MetaModel::GetFriendlyNameAttributeCodeList($oFilter->GetClass()); + $sClassAlias = $oFilter->GetClassAlias(); + $aFilters = array(); + // Equals first + $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; + } + // start with next + $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; - $oObjects = new DBObjectSet($oFilter, $this->m_aOrderBy, $aArgs); + 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)); + } + $oObjects->OptimizeColumnLoad($aAttToLoad); while ($oObject = $oObjects->Fetch()) { if (empty($this->m_sValueAttCode)) @@ -231,6 +290,22 @@ class ValueSetObjects extends ValueSetDefinition { return $this->m_sFilterExpr; } + + /** + * @param $iLimit + */ + public function SetLimit($iLimit) + { + $this->m_iLimit = $iLimit; + } + + /** + * @param $bSort + */ + public function SetSort($bSort) + { + $this->m_bSort = $bSort; + } } diff --git a/js/extkeywidget.js b/js/extkeywidget.js index 0858eaca2..6ab23e0cc 100644 --- a/js/extkeywidget.js +++ b/js/extkeywidget.js @@ -15,7 +15,7 @@ // You should have received a copy of the GNU Affero General Public License // along with iTop. If not, see -function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper, sAttCode, bSearchMode) +function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper, sAttCode, bSearchMode, bDoSearch) { this.id = id; this.sOriginalTargetClass = sTargetClass; @@ -29,6 +29,7 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper this.ajax_request = null; this.bSelectMode = bSelectMode; // true if the edited field is a SELECT, false if it's an autocomplete this.bSearchMode = bSearchMode; // true if selecting a value in the context of a search form + this.bDoSearch = bDoSearch; // false if the search is not launched var me = this; this.Init = function() @@ -94,7 +95,13 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper me.UpdateSizes(); me.UpdateButtons(); me.ajax_request = null; - me.DoSearchObjects(); + $('#count_'+me.id).change(function(){ + me.UpdateButtons(); + }); + if (me.bDoSearch) + { + me.DoSearchObjects(); + } }, 'html' ); @@ -196,9 +203,6 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper $('#fr_'+me.id+' input:radio').click(function() { me.UpdateButtons(); }); me.UpdateButtons(); me.ajax_request = null; - $('#count_'+me.id).change(function(){ - me.UpdateButtons(); - }); me.UpdateSizes(); }, 'html' diff --git a/js/linksdirectwidget.js b/js/linksdirectwidget.js index 93fd414a8..e1ad6cf5f 100644 --- a/js/linksdirectwidget.js +++ b/js/linksdirectwidget.js @@ -11,6 +11,7 @@ $(function() input_name: '', class_name: '', att_code: '', + do_search: true, submit_to: '../pages/ajax.render.php', submit_parameters: {}, labels: { 'delete': 'Delete', @@ -225,7 +226,17 @@ $(function() }); me.indicator.html(''); me.oButtons['add'].removeAttr('disabled'); - me._onSearchToAdd(); + if (me.options.do_search) + { + me._onSearchToAdd(); + } + else + { + $('#count_'+me.id).change(function() { + var c = this.value; + me._onUpdateDlgButtons(c); + }); + } me._updateDlgPosition(); me._onSearchDlgUpdateSize(); }); diff --git a/js/linkswidget.js b/js/linkswidget.js index 61b652746..a295b198a 100644 --- a/js/linkswidget.js +++ b/js/linkswidget.js @@ -16,7 +16,7 @@ // along with iTop. If not, see // JavaScript Document -function LinksWidget(id, sClass, sAttCode, iInputId, sSuffix, bDuplicates, oWizHelper, sExtKeyToRemote) +function LinksWidget(id, sClass, sAttCode, iInputId, sSuffix, bDuplicates, oWizHelper, sExtKeyToRemote, bDoSearch) { this.id = id; this.iInputId = iInputId; @@ -30,6 +30,7 @@ function LinksWidget(id, sClass, sAttCode, iInputId, sSuffix, bDuplicates, oWizH this.aAdded = []; this.aRemoved = []; this.aModified = {}; + this.bDoSearch = bDoSearch; // false if the search is not launched var me = this; this.Init = function() @@ -132,11 +133,22 @@ function LinksWidget(id, sClass, sAttCode, iInputId, sSuffix, bDuplicates, oWizH "data": theMap, "dataType": "html" }) - .done(function (data) { + .done(function (data) + { $('#dlg_'+me.id).html(data); $('#dlg_'+me.id).dialog('open'); me.UpdateSizes(null, null); - me.SearchObjectsToAdd(); + if (me.bDoSearch) + { + me.SearchObjectsToAdd(); + } + else + { + $('#count_'+me.id).change(function () { + var c = this.value; + me.UpdateButtons(c); + }); + } $('#'+me.id+'_indicatorAdd').html(''); }) ; diff --git a/sources/application/search/searchform.class.inc.php b/sources/application/search/searchform.class.inc.php index cf1624297..538c6bcc5 100644 --- a/sources/application/search/searchform.class.inc.php +++ b/sources/application/search/searchform.class.inc.php @@ -147,32 +147,21 @@ class SearchForm $bAutoSubmit = true; $mSubmitParam = utils::GetConfig()->Get('search_manual_submit'); - if (is_array($mSubmitParam)) - { - // List of classes - if (isset($mSubmitParam[$sClassName])) - { - $bAutoSubmit = !$mSubmitParam[$sClassName]; - } - else - { - // Search for child classes - foreach($mSubmitParam as $sConfigClass => $bFlag) - { - $aChildClasses = MetaModel::EnumChildClasses($sConfigClass); - if (in_array($sClassName, $aChildClasses)) - { - $bAutoSubmit = !$bFlag; - break; - } - } - - } - } - else if ($mSubmitParam !== false) + if ($mSubmitParam !== false) { $bAutoSubmit = false; } + else + { + $mSubmitParam = utils::GetConfig()->Get('high_cardinality_classes'); + if (is_array($mSubmitParam)) + { + if (in_array($sClassName, $mSubmitParam)) + { + $bAutoSubmit = false; + } + } + } $sAction = (isset($aExtraParams['action'])) ? $aExtraParams['action'] : utils::GetAbsoluteUrlAppRoot().'pages/UI.php'; $sStyle = ($bOpen == 'true') ? '' : 'closed'; @@ -404,6 +393,7 @@ class SearchForm */ public static function GetFieldAllowedValues($oAttrDef) { + $iMaxComboLength = MetaModel::GetConfig()->Get('max_combo_length'); if ($oAttrDef->IsExternalKey(EXTKEY_ABSOLUTE)) { if ($oAttrDef instanceof AttributeExternalField) @@ -426,10 +416,9 @@ class SearchForm } $oSearch->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', true); $oSet = new DBObjectSet($oSearch); - $iCount = $oSet->Count(); - if ($iCount > MetaModel::GetConfig()->Get('max_combo_length')) + if ($oSet->CountExceeds($iMaxComboLength)) { - return array('autocomplete' => true, 'count' => $iCount); + return array('autocomplete' => true); } if ($oAttrDef instanceof AttributeExternalField) { @@ -438,7 +427,7 @@ class SearchForm { $aAllowedValues[$oObject->GetKey()] = $oObject->GetName(); } - return array('values' => $aAllowedValues, 'count' => $iCount); + return array('values' => $aAllowedValues); } } else @@ -447,18 +436,16 @@ class SearchForm { /** @var DBObjectSet $oSet */ $oSet = $oAttrDef->GetAllowedValuesAsObjectSet(); - $iCount = $oSet->Count(); - if ($iCount > MetaModel::GetConfig()->Get('max_combo_length')) + if ($oSet->CountExceeds($iMaxComboLength)) { - return array('autocomplete' => true, 'count' => $iCount); + return array('autocomplete' => true); } } } $aAllowedValues = $oAttrDef->GetAllowedValues(); - $iCount = is_array($aAllowedValues) ? count($aAllowedValues) : 0; - return array('values' => $aAllowedValues, 'count' => $iCount); + return array('values' => $aAllowedValues); } /**