Advanced search: Alpha version.

SVN:trunk[5608]
This commit is contained in:
Guillaume Lajarige
2018-04-05 07:05:58 +00:00
64 changed files with 12558 additions and 3099 deletions

View File

@@ -45,6 +45,11 @@ require_once(APPROOT.'/application/ui.extkeywidget.class.inc.php');
require_once(APPROOT.'/application/ui.htmleditorwidget.class.inc.php');
require_once(APPROOT.'/application/datatable.class.inc.php');
require_once(APPROOT.'/sources/renderer/console/consoleformrenderer.class.inc.php');
require_once(APPROOT.'/sources/application/search/searchform.class.inc.php');
require_once(APPROOT.'/sources/application/search/criterionparser.class.inc.php');
require_once(APPROOT.'/sources/application/search/criterionconversionabstract.class.inc.php');
require_once(APPROOT.'/sources/application/search/criterionconversion/criteriontooql.class.inc.php');
require_once(APPROOT.'/sources/application/search/criterionconversion/criteriontosearchform.class.inc.php');
abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
{
@@ -604,7 +609,7 @@ EOF
function GetBareProperties(WebPage $oPage, $bEditMode = false, $sPrefix, $aExtraParams = array())
{
$sHtml = '';
$oAppContext = new ApplicationContext();
$oAppContext = new ApplicationContext();
$sStateAttCode = MetaModel::GetStateAttributeCode(get_class($this));
$aDetails = array();
$sClass = get_class($this);
@@ -620,28 +625,28 @@ EOF
$aFieldsComments = (isset($aExtraParams['fieldsComments'])) ? $aExtraParams['fieldsComments'] : array();
$aExtraFlags = (isset($aExtraParams['fieldsFlags'])) ? $aExtraParams['fieldsFlags'] : array();
$bFieldComments = (count($aFieldsComments) > 0);
foreach($aDetailsStruct as $sTab => $aCols )
foreach($aDetailsStruct as $sTab => $aCols)
{
$aDetails[$sTab] = array();
$aTableStyles[] = 'vertical-align:top';
$aTableClasses = array();
$aTableStyles[] = 'vertical-align:top';
$aTableClasses = array();
$aColStyles[] = 'vertical-align:top';
$aColClasses = array();
ksort($aCols);
ksort($aCols);
$iColCount = count($aCols);
if($iColCount > 1)
{
$aTableClasses[] = 'n-cols-details';
$aTableClasses[] = $iColCount.'-cols-details';
if ($iColCount > 1)
{
$aTableClasses[] = 'n-cols-details';
$aTableClasses[] = $iColCount.'-cols-details';
$aColStyles[] = 'width:'.floor(100/$iColCount).'%';
}
else
{
$aTableClasses[] = 'one-col-details';
}
$aColStyles[] = 'width:'.floor(100 / $iColCount).'%';
}
else
{
$aTableClasses[] = 'one-col-details';
}
$oPage->SetCurrentTab(Dict::S($sTab));
$oPage->add('<table style="'.implode('; ', $aTableStyles).'" class="'.implode(' ', $aTableClasses).'" data-mode="'.$sEditMode.'"><tr>');
@@ -654,7 +659,7 @@ EOF
$aDetails[$sTab][$sColIndex] = array();
foreach($aFieldsets as $sFieldsetName => $aFields)
{
if (!empty($sFieldsetName) && ($sFieldsetName[0] != '_'))
if (!empty($sFieldsetName) && ($sFieldsetName[0] != '_'))
{
$sLabel = $sFieldsetName;
}
@@ -683,90 +688,93 @@ EOF
{
$sComments = isset($aFieldsComments[$sAttCode]) ? $aFieldsComments[$sAttCode] : '';
$sInfos = '';
$iFlags = $this->GetFormAttributeFlags($sAttCode);
if (array_key_exists($sAttCode, $aExtraFlags))
{
// the caller may override some flags if needed
$iFlags = $iFlags | $aExtraFlags[$sAttCode];
}
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
if ( (!$oAttDef->IsLinkSet()) && (($iFlags & OPT_ATT_HIDDEN) == 0))
{
$sInputId = $this->m_iFormId.'_'.$sAttCode;
if ($oAttDef->IsWritable())
$sComments = isset($aFieldsComments[$sAttCode]) ? $aFieldsComments[$sAttCode] : '';
$sInfos = '';
$iFlags = $this->GetFormAttributeFlags($sAttCode);
if (array_key_exists($sAttCode, $aExtraFlags))
{
if ($sStateAttCode == $sAttCode)
// the caller may override some flags if needed
$iFlags = $iFlags | $aExtraFlags[$sAttCode];
}
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
if ((!$oAttDef->IsLinkSet()) && (($iFlags & OPT_ATT_HIDDEN) == 0))
{
$sInputId = $this->m_iFormId.'_'.$sAttCode;
if ($oAttDef->IsWritable())
{
// State attribute is always read-only from the UI
$sHTMLValue = $this->GetStateLabel();
$val = array('label' => '<label>'.$oAttDef->GetLabel().'</label>', 'value' => $sHTMLValue, 'comments' => $sComments, 'infos' => $sInfos, 'attcode' => $sAttCode);
}
else
{
if ($iFlags & (OPT_ATT_READONLY|OPT_ATT_SLAVE))
if ($sStateAttCode == $sAttCode)
{
// Check if the attribute is not read-only because of a synchro...
if ($iFlags & OPT_ATT_SLAVE)
{
$aReasons = array();
$iSynchroFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons);
$sSynchroIcon = "&nbsp;<img id=\"synchro_$sInputId\" src=\"../images/transp-lock.png\" style=\"vertical-align:middle\"/>";
$sTip = '';
foreach($aReasons as $aRow)
{
$sDescription = htmlentities($aRow['description'], ENT_QUOTES, 'UTF-8');
$sDescription = str_replace(array("\r\n", "\n"), "<br/>", $sDescription);
$sTip .= "<div class='synchro-source'>";
$sTip .= "<div class='synchro-source-title'>Synchronized with {$aRow['name']}</div>";
$sTip .= "<div class='synchro-source-description'>$sDescription</div>";
}
$sTip = addslashes($sTip);
$oPage->add_ready_script("$('#synchro_$sInputId').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );");
$sComments = $sSynchroIcon;
}
// Attribute is read-only
$sHTMLValue = "<span id=\"field_{$sInputId}\">".$this->GetAsHTML($sAttCode).'</span>';
// State attribute is always read-only from the UI
$sHTMLValue = $this->GetStateLabel();
$val = array('label' => '<label>'.$oAttDef->GetLabel().'</label>', 'value' => $sHTMLValue, 'comments' => $sComments, 'infos' => $sInfos, 'attcode' => $sAttCode);
}
else
{
$sValue = $this->Get($sAttCode);
$sDisplayValue = $this->GetEditValue($sAttCode);
$aArgs = array('this' => $this, 'formPrefix' => $sPrefix);
$sHTMLValue = "".self::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $sValue, $sDisplayValue, $sInputId, '', $iFlags, $aArgs).'';
if ($iFlags & (OPT_ATT_READONLY | OPT_ATT_SLAVE))
{
// Check if the attribute is not read-only because of a synchro...
if ($iFlags & OPT_ATT_SLAVE)
{
$aReasons = array();
$iSynchroFlags = $this->GetSynchroReplicaFlags($sAttCode, $aReasons);
$sSynchroIcon = "&nbsp;<img id=\"synchro_$sInputId\" src=\"../images/transp-lock.png\" style=\"vertical-align:middle\"/>";
$sTip = '';
foreach($aReasons as $aRow)
{
$sDescription = htmlentities($aRow['description'], ENT_QUOTES, 'UTF-8');
$sDescription = str_replace(array("\r\n", "\n"), "<br/>", $sDescription);
$sTip .= "<div class='synchro-source'>";
$sTip .= "<div class='synchro-source-title'>Synchronized with {$aRow['name']}</div>";
$sTip .= "<div class='synchro-source-description'>$sDescription</div>";
}
$sTip = addslashes($sTip);
$oPage->add_ready_script("$('#synchro_$sInputId').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );");
$sComments = $sSynchroIcon;
}
// Attribute is read-only
$sHTMLValue = "<span id=\"field_{$sInputId}\">".$this->GetAsHTML($sAttCode).'</span>';
}
else
{
$sValue = $this->Get($sAttCode);
$sDisplayValue = $this->GetEditValue($sAttCode);
$aArgs = array('this' => $this, 'formPrefix' => $sPrefix);
$sHTMLValue = "".self::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $sValue, $sDisplayValue, $sInputId, '', $iFlags, $aArgs).'';
}
$aFieldsMap[$sAttCode] = $sInputId;
$val = array('label' => '<span title="'.$oAttDef->GetDescription().'">'.$oAttDef->GetLabel().'</span>', 'value' => $sHTMLValue, 'comments' => $sComments, 'infos' => $sInfos, 'attcode' => $sAttCode);
}
}
else
{
$val = array(
'label' => '<span title="'.$oAttDef->GetDescription().'">'.$oAttDef->GetLabel().'</span>',
'value' => "<span id=\"field_{$sInputId}\">".$this->GetAsHTML($sAttCode)."</span>",
'comments' => $sComments,
'infos' => $sInfos,
'attcode' => $sAttCode
);
$aFieldsMap[$sAttCode] = $sInputId;
$val = array('label' => '<span title="'.$oAttDef->GetDescription().'">'.$oAttDef->GetLabel().'</span>', 'value' => $sHTMLValue, 'comments' => $sComments, 'infos' => $sInfos, 'attcode' => $sAttCode);
}
// Checking how the field should be rendered
// Note: For view mode, this is done in cmdbAbstractObject::GetFieldAsHtml()
// Note 2: Shouldn't this be a property of the AttDef instead an array that we have to maintain?
if (in_array($oAttDef->GetEditClass(), array('Text', 'HTML', 'CaseLog', 'CustomFields', 'OQLExpression')))
{
$val['layout'] = 'large';
}
else
{
$val['layout'] = 'small';
}
}
else
{
$val = array('label' => '<span title="'.$oAttDef->GetDescription().'">'.$oAttDef->GetLabel().'</span>', 'value' => "<span id=\"field_{$sInputId}\">".$this->GetAsHTML($sAttCode)."</span>", 'comments' => $sComments, 'infos' => $sInfos, 'attcode' => $sAttCode);
$aFieldsMap[$sAttCode] = $sInputId;
$val = null; // Skip this field
}
// Checking how the field should be rendered
// Note: For view mode, this is done in cmdbAbstractObject::GetFieldAsHtml()
// Note 2: Shouldn't this be a property of the AttDef instead an array that we have to maintain?
if(in_array($oAttDef->GetEditClass(), array('Text', 'HTML', 'CaseLog', 'CustomFields', 'OQLExpression')))
{
$val['layout'] = 'large';
}
else
{
$val['layout'] = 'small';
}
}
else
{
$val = null; // Skip this field
}
}
else
{
@@ -776,10 +784,10 @@ EOF
if ($val != null)
{
// The field is visible, add it to the current column
// The field is visible, add it to the current column
$aDetails[$sTab][$sColIndex][] = $val;
$iInputId++;
}
}
}
}
if (!empty($sPreviousLabel))
@@ -796,6 +804,7 @@ EOF
}
$oPage->add('</tr></table>');
}
return $aFieldsMap;
}
@@ -814,6 +823,7 @@ EOF
{
// Object's details
// template not found display the object using the *old style*
$oPage->add('<div id="search-widget-results-outer">');
$this->DisplayBareHeader($oPage, $bEditMode);
$oPage->AddTabContainer(OBJECT_PROPERTIES_TAB);
$oPage->SetCurrentTabContainer(OBJECT_PROPERTIES_TAB);
@@ -823,6 +833,7 @@ EOF
//$oPage->SetCurrentTab(Dict::S('UI:HistoryTab'));
//$this->DisplayBareHistory($oPage, $bEditMode);
$oPage->AddAjaxTab(Dict::S('UI:HistoryTab'), utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=history&class='.get_class($this).'&id='.$this->GetKey());
$oPage->add('</div>');
}
}
@@ -1527,238 +1538,21 @@ EOF
$oPage->add(self::GetSearchForm($oPage, $oSet, $aExtraParams));
}
/**
* @param WebPage $oPage
* @param CMDBObjectSet $oSet
* @param array $aExtraParams
*
* @return string
* @throws CoreException
* @throws DictExceptionMissingString
*/
public static function GetSearchForm(WebPage $oPage, CMDBObjectSet $oSet, $aExtraParams = array())
{
static $iSearchFormId = 0;
$bMultiSelect = false;
$oAppContext = new ApplicationContext();
$sHtml = '';
$numCols=4;
$sClassName = $oSet->GetFilter()->GetClass();
$oSearchForm = new \Combodo\iTop\Application\Search\SearchForm();
// Romain: temporarily removed the tab "OQL query" because it was not finalized
// (especially when used to add a link)
/*
$sHtml .= "<div class=\"mini_tabs\" id=\"mini_tabs{$iSearchFormId}\"><ul>
<li><a href=\"#\" onClick=\"$('div.mini_tab{$iSearchFormId}').toggle();$('#mini_tabs{$iSearchFormId} ul li a').toggleClass('selected');\">".Dict::S('UI:OQLQueryTab')."</a></li>
<li><a class=\"selected\" href=\"#\" onClick=\"$('div.mini_tab{$iSearchFormId}').toggle();$('#mini_tabs{$iSearchFormId} ul li a').toggleClass('selected');\">".Dict::S('UI:SimpleSearchTab')."</a></li>
</ul></div>\n";
*/
// Simple search form
if (isset($aExtraParams['currentId']))
{
$sSearchFormId = $aExtraParams['currentId'];
}
else
{
$iSearchFormId = $oPage->GetUniqueId();
$sSearchFormId = 'SimpleSearchForm'.$iSearchFormId;
$sHtml .= "<div id=\"ds_$sSearchFormId\" class=\"mini_tab{$iSearchFormId}\">\n";
}
// Check if the current class has some sub-classes
if (isset($aExtraParams['baseClass']))
{
$sRootClass = $aExtraParams['baseClass'];
}
else
{
$sRootClass = $sClassName;
}
$aSubClasses = MetaModel::GetSubclasses($sRootClass);
if (count($aSubClasses) > 0)
{
$aOptions = array();
$aOptions[MetaModel::GetName($sRootClass)] = "<option value=\"$sRootClass\">".MetaModel::GetName($sRootClass)."</options>\n";
foreach($aSubClasses as $sSubclassName)
{
$aOptions[MetaModel::GetName($sSubclassName)] = "<option value=\"$sSubclassName\">".MetaModel::GetName($sSubclassName)."</options>\n";
}
$aOptions[MetaModel::GetName($sClassName)] = "<option selected value=\"$sClassName\">".MetaModel::GetName($sClassName)."</options>\n";
ksort($aOptions);
$sContext = $oAppContext->GetForLink();
$sClassesCombo = "<select name=\"class\" onChange=\"ReloadSearchForm('$sSearchFormId', this.value, '$sRootClass', '$sContext')\">\n".implode('', $aOptions)."</select>\n";
}
else
{
$sClassesCombo = MetaModel::GetName($sClassName);
}
$oUnlimitedFilter = new DBObjectSearch($sClassName);
$sAction = (isset($aExtraParams['action'])) ? $aExtraParams['action'] : utils::GetAbsoluteUrlAppRoot().'pages/UI.php';
$sHtml .= "<form id=\"fs_{$sSearchFormId}\" action=\"{$sAction}\">\n"; // Don't use $_SERVER['SCRIPT_NAME'] since the form may be called asynchronously (from ajax.php)
$sHtml .= "<h2>".Dict::Format('UI:SearchFor_Class_Objects', $sClassesCombo)."</h2>\n";
$index = 0;
$sHtml .= "<div>\n";
$aMapCriteria = array();
$aList = MetaModel::GetZListItems($sClassName, 'standard_search');
$aConsts = $oSet->ListConstantFields(); // Some fields are constants based on the query/context
$sClassAlias = $oSet->GetFilter()->GetClassAlias();
foreach($aList as $sFilterCode)
{
//$oAppContext->Reset($sFilterCode); // Make sure the same parameter will not be passed twice
$sHtml .= '<div class="SearchAttribute" style="white-space: nowrap;padding:5px;display:inline-block;">';
$sFilterValue = isset($aConsts[$sClassAlias][$sFilterCode]) ? $aConsts[$sClassAlias][$sFilterCode] : '';
$sFilterValue = utils::ReadParam($sFilterCode, $sFilterValue, false, 'raw_data');
$sFilterOpCode = null; // Use the default 'loose' OpCode
if (empty($sFilterValue))
{
if (isset($aMapCriteria[$sFilterCode]))
{
if (count($aMapCriteria[$sFilterCode]) > 1)
{
$sFilterValue = Dict::S('UI:SearchValue:Mixed');
}
else
{
$sFilterValue = $aMapCriteria[$sFilterCode][0]['value'];
$sFilterOpCode = $aMapCriteria[$sFilterCode][0]['opcode'];
}
// Todo: Investigate...
if ($sFilterCode != 'company')
{
$oUnlimitedFilter->AddCondition($sFilterCode, $sFilterValue, $sFilterOpCode);
}
}
}
$oAttDef = MetaModel::GetAttributeDef($sClassName, $sFilterCode);
if ($oAttDef->IsExternalKey(EXTKEY_ABSOLUTE))
{
$oKeyAttDef = $oAttDef->GetFinalAttDef();
$sKeyAttClass = $oKeyAttDef->GetHostClass();
$sKeyAttCode = $oKeyAttDef->GetCode();
$sTargetClass = $oKeyAttDef->GetTargetClass();
$oSearch = new DBObjectSearch($sTargetClass);
$oSearch->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', true);
$oAllowedValues = new DBObjectSet($oSearch);
$iFieldSize = $oKeyAttDef->GetMaxSize();
$iMaxComboLength = $oKeyAttDef->GetMaximumComboLength();
$sHtml .= "<label>".MetaModel::GetFilterLabel($sKeyAttClass, $sKeyAttCode).":</label>&nbsp;";
$aExtKeyParams = $aExtraParams;
$aExtKeyParams['iFieldSize'] = $oKeyAttDef->GetMaxSize();
$aExtKeyParams['iMinChars'] = $oKeyAttDef->GetMinAutoCompleteChars();
$sHtml .= UIExtKeyWidget::DisplayFromAttCode($oPage, $sKeyAttCode, $sKeyAttClass, $oAttDef->GetLabel(), $oAllowedValues, $sFilterValue, $sSearchFormId.'search_'.$sFilterCode, false, $sFilterCode, '', $aExtKeyParams, true);
}
else
{
$aAllowedValues = MetaModel::GetAllowedValues_flt($sClassName, $sFilterCode, $aExtraParams);
if (is_null($aAllowedValues))
{
// Any value is possible, display an input box
$sHtml .= "<label>".MetaModel::GetFilterLabel($sClassName, $sFilterCode).":</label>&nbsp;<input class=\"textSearch\" name=\"$sFilterCode\" value=\"".htmlentities($sFilterValue, ENT_QUOTES, 'utf-8')."\"/>\n";
}
else
{
//Enum field, display a multi-select combo
$sValue = "<select class=\"multiselect\" size=\"1\" name=\"{$sFilterCode}[]\" multiple>\n";
$bMultiSelect = true;
//$sValue .= "<option value=\"\">".Dict::S('UI:SearchValue:Any')."</option>\n";
asort($aAllowedValues);
foreach($aAllowedValues as $key => $value)
{
if (is_array($sFilterValue) && in_array($key, $sFilterValue))
{
$sSelected = ' selected';
}
else if ($sFilterValue == $key)
{
$sSelected = ' selected';
}
else
{
$sSelected = '';
}
$sValue .= "<option value=\"$key\"$sSelected>$value</option>\n";
}
$sValue .= "</select>\n";
$sHtml .= "<label>".MetaModel::GetFilterLabel($sClassName, $sFilterCode).":</label>&nbsp;$sValue\n";
}
}
unset($aExtraParams[$sFilterCode]);
// Finally, add a tooltip if one is defined for this attribute definition
$sTip = $oAttDef->GetHelpOnSmartSearch();
if (strlen($sTip) > 0)
{
$sTip = addslashes($sTip);
$sTip = str_replace(array("\n", "\r"), " ", $sTip);
// :input does represent in form visible input (INPUT, SELECT, TEXTAREA)
$oPage->add_ready_script("$('form#fs_$sSearchFormId :input[name={$sFilterCode}]').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );");
}
$index++;
$sHtml .= '</div> ';
}
$sHtml .= "</div>\n";
$sHtml .= "<p align=\"right\"><input type=\"submit\" value=\"".Dict::S('UI:Button:Search')."\"></p>\n";
if (isset($aExtraParams['table_id']))
{
// Rename to avoid collisions...
$aExtraParams['_table_id_'] = $aExtraParams['table_id'];
unset($aExtraParams['table_id']);
}
foreach($aExtraParams as $sName => $sValue)
{
if (is_scalar($sValue))
{
$sHtml .= "<input type=\"hidden\" name=\"$sName\" value=\"".htmlentities($sValue, ENT_QUOTES, 'UTF-8')."\" />\n";
}
}
$sHtml .= "<input type=\"hidden\" name=\"class\" value=\"$sClassName\" />\n";
$sHtml .= "<input type=\"hidden\" name=\"dosearch\" value=\"1\" />\n";
$sHtml .= "<input type=\"hidden\" name=\"operation\" value=\"search_form\" />\n";
$sHtml .= $oAppContext->GetForForm();
$sHtml .= "</form>\n";
if (!isset($aExtraParams['currentId']))
{
$sHtml .= "</div><!-- Simple search form -->\n";
}
if ($bMultiSelect)
{
$aOptions = array(
'header' => true,
'checkAllText' => Dict::S('UI:SearchValue:CheckAll'),
'uncheckAllText' => Dict::S('UI:SearchValue:UncheckAll'),
'noneSelectedText' => Dict::S('UI:SearchValue:Any'),
'selectedText' => Dict::S('UI:SearchValue:NbSelected'),
'selectedList' => 1,
);
$sJSOptions = json_encode($aOptions);
$oPage->add_ready_script("$('.multiselect').multiselect($sJSOptions);");
}
/*
// OQL query builder
$sHtml .= "<div id=\"OQLQuery{$iSearchFormId}\" style=\"display:none\" class=\"mini_tab{$iSearchFormId}\">\n";
$sHtml .= "<h1>".Dict::S('UI:OQLQueryBuilderTitle')."</h1>\n";
$sHtml .= "<form id=\"formOQL{$iSearchFormId}\"><table style=\"width:80%;\"><tr style=\"vertical-align:top\">\n";
$sHtml .= "<td style=\"text-align:right\"><label>SELECT&nbsp;</label><select name=\"oql_class\">";
$aClasses = MetaModel::EnumChildClasses($sClassName, ENUM_CHILD_CLASSES_ALL);
$sSelectedClass = utils::ReadParam('oql_class', $sClassName, false, 'class');
$sOQLClause = utils::ReadParam('oql_clause', '', false, 'raw_data');
asort($aClasses);
foreach($aClasses as $sChildClass)
{
$sSelected = ($sChildClass == $sSelectedClass) ? 'selected' : '';
$sHtml.= "<option value=\"$sChildClass\" $sSelected>".MetaModel::GetName($sChildClass)."</option>\n";
}
$sHtml .= "</select>&nbsp;</td><td>\n";
$sHtml .= "<textarea name=\"oql_clause\" style=\"width:100%\">$sOQLClause</textarea></td></tr>\n";
$sHtml .= "<tr><td colspan=\"2\" style=\"text-align:right\"><input type=\"submit\" value=\"".Dict::S('UI:Button:Query')."\"></td></tr>\n";
$sHtml .= "<input type=\"hidden\" name=\"dosearch\" value=\"1\" />\n";
foreach($aExtraParams as $sName => $sValue)
{
if (is_scalar($sValue))
{
$sHtml .= "<input type=\"hidden\" name=\"$sName\" value=\"$sValue\" />\n";
}
}
$sHtml .= "<input type=\"hidden\" name=\"operation\" value=\"search_oql\" />\n";
$sHtml .= $oAppContext->GetForForm();
$sHtml .= "</table></form>\n";
$sHtml .= "</div><!-- OQL query form -->\n";
*/
return $sHtml;
return $oSearchForm->GetSearchForm($oPage, $oSet, $aExtraParams);
}
/**

View File

@@ -571,33 +571,6 @@ EOF
{
$oPage->add_ready_script("oTable.trigger(\"fakesorton\", [$sFakeSortList]);");
}
//if ($iNbPages == 1)
if (false)
{
if (isset($aExtraParams['cssCount']))
{
$sCssCount = $aExtraParams['cssCount'];
if ($sSelectMode == 'single')
{
$sSelectSelector = ":radio[name^=selectObj]";
}
else if ($sSelectMode == 'multiple')
{
$sSelectSelector = ":checkbox[name^=selectObj]";
}
$oPage->add_ready_script(
<<<EOF
$('#{$this->iListId} table.listResults $sSelectSelector').change(function() {
var c = $('{$sCssCount}');
var v = $('#{$this->iListId} table.listResults $sSelectSelector:checked').length;
c.val(v);
$('#{$this->iListId} .selectedCount').text(v);
c.trigger('change');
});
EOF
);
}
}
return $sHtml;
}

View File

@@ -220,46 +220,13 @@ class DisplayBlock
$aExtraParams['currentId'] = $sId;
$sExtraParams = addslashes(str_replace('"', "'", json_encode($aExtraParams))); // JSON encode, change the style of the quotes and escape them
$bAutoReload = false;
if (isset($aExtraParams['auto_reload']))
{
if ($aExtraParams['auto_reload'] === true)
{
// Note: does not work in the switch (case true) because a positive number evaluates to true!!!
$aExtraParams['auto_reload'] = 'standard';
}
switch($aExtraParams['auto_reload'])
{
case 'fast':
$bAutoReload = true;
$iReloadInterval = MetaModel::GetConfig()->GetFastReloadInterval()*1000;
break;
case 'standard':
case 'true':
$bAutoReload = true;
$iReloadInterval = MetaModel::GetConfig()->GetStandardReloadInterval()*1000;
break;
default:
if (is_numeric($aExtraParams['auto_reload']) && ($aExtraParams['auto_reload'] > 0))
{
$bAutoReload = true;
$iReloadInterval = max(MetaModel::GetConfig()->Get('min_reload_interval'), $aExtraParams['auto_reload'])*1000;
}
else
{
// incorrect config, ignore it
$bAutoReload = false;
}
}
}
$sFilter = $this->m_oFilter->serialize(); // Used either for asynchronous or auto_reload
if (!$this->m_bAsynchronous)
{
// render now
$sHtml .= "<div id=\"$sId\" class=\"display_block\">\n";
$sHtml .= "<div id=\"$sId\" class=\"display_block\" >\n";
$sHtml .= $this->GetRenderContent($oPage, $aExtraParams, $sId);
$sHtml .= "</div>\n";
}
@@ -273,17 +240,36 @@ class DisplayBlock
$.post("ajax.render.php?style='.$this->m_sStyle.'",
{ operation: "ajax", filter: "'.$sFilter.'", extra_params: "'.$sExtraParams.'" },
function(data){
$("#'.$sId.'").empty();
$("#'.$sId.'").append(data);
$("#'.$sId.'").removeClass("loading");
$("#'.$sId.'")
.empty()
.append(data)
.removeClass("loading")
;
}
);
');
}
if (($bAutoReload) && ($this->m_sStyle != 'search')) // Search form do NOT auto-reload
{
$oPage->add_script('setInterval("ReloadBlock(\''.$sId.'\', \''.$this->m_sStyle.'\', \''.$sFilter.'\', \"'.$sExtraParams.'\")", '.$iReloadInterval.');');
}
if ($this->m_sStyle == 'list') // Search form need to extract result list extra data, the simplest way is to expose this configuration
{
$listJsonExtraParams = json_encode(json_encode($aExtraParams));
$oPage->add_ready_script("
$('#$sId').data('sExtraParams', ".$listJsonExtraParams.");
// console.debug($('#$sId').data());
// console.debug($('#$sId'));
// console.debug('#$sId');
");
// $oPage->add_ready_script("console.debug($('#Menu_UserRequest_OpenRequests').data());");
}
return $sHtml;
}
@@ -919,21 +905,10 @@ class DisplayBlock
case 'search':
if (!$oPage->IsPrintableVersion())
{
$sStyle = (isset($aExtraParams['open']) && ($aExtraParams['open'] == 'true')) ? 'SearchDrawer' : 'SearchDrawer DrawerClosed';
$sHtml .= "<div id=\"ds_$sId\" class=\"$sStyle\">\n";
$oPage->add_ready_script(
<<<EOF
$("#dh_$sId").click( function() {
$("#ds_$sId").slideToggle('normal', function() { $("#ds_$sId").parent().resize(); FixSearchFormsDisposition(); $("#dh_$sId").trigger('toggle_complete'); } );
$("#dh_$sId").toggleClass('open');
});
EOF
);
$sHtml .= "<div id=\"ds_$sId\" class=\"search_box\">\n";
$aExtraParams['currentId'] = $sId;
$sHtml .= cmdbAbstractObject::GetSearchForm($oPage, $this->m_oSet, $aExtraParams);
$sHtml .= "</div>\n";
$sHtml .= "<div class=\"HRDrawer\"></div>\n";
$sHtml .= "<div id=\"dh_$sId\" class=\"DrawerHandle\">".Dict::S('UI:SearchToggle')."</div>\n";
}
break;
@@ -1126,6 +1101,56 @@ EOF
// Unsupported style, do nothing.
$sHtml .= Dict::format('UI:Error:UnsupportedStyleOfBlock', $this->m_sStyle);
}
$bAutoReload = false;
if (isset($aExtraParams['auto_reload']))
{
if ($aExtraParams['auto_reload'] === true)
{
// Note: does not work in the switch (case true) because a positive number evaluates to true!!!
$aExtraParams['auto_reload'] = 'standard';
}
switch($aExtraParams['auto_reload'])
{
case 'fast':
$bAutoReload = true;
$iReloadInterval = MetaModel::GetConfig()->GetFastReloadInterval()*1000;
break;
case 'standard':
case 'true':
$bAutoReload = true;
$iReloadInterval = MetaModel::GetConfig()->GetStandardReloadInterval()*1000;
break;
default:
if (is_numeric($aExtraParams['auto_reload']) && ($aExtraParams['auto_reload'] > 0))
{
$bAutoReload = true;
$iReloadInterval = max(MetaModel::GetConfig()->Get('min_reload_interval'), $aExtraParams['auto_reload'])*1000;
}
else
{
// incorrect config, ignore it
$bAutoReload = false;
}
}
}
if (($bAutoReload) && ($this->m_sStyle != 'search')) // Search form do NOT auto-reload
{
$sFilter = $this->m_oFilter->serialize(); // Used either for asynchronous or auto_reload
$sExtraParams = addslashes(str_replace('"', "'", json_encode($aExtraParams))); // JSON encode, change the style of the quotes and escape them
$oPage->add_script('if (typeof window.oAutoReloadBlock == "undefined") {
window.oAutoReloadBlock = {};
}
if (typeof window.oAutoReloadBlock[\''.$sId.'\'] != "undefined") {
clearInterval(window.oAutoReloadBlock[\''.$sId.'\']);
}
window.oAutoReloadBlock[\''.$sId.'\'] = setInterval("ReloadBlock(\''.$sId.'\', \''.$this->m_sStyle.'\', \''.$sFilter.'\', \"'.$sExtraParams.'\")", '.$iReloadInterval.');');
}
return $sHtml;
}

View File

@@ -111,8 +111,13 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
$sSearchAny = addslashes(Dict::S('UI:SearchValue:Any'));
$sSearchNbSelected = addslashes(Dict::S('UI:SearchValue:NbSelected'));
$this->add_dict_entry('UI:FillAllMandatoryFields');
$this->add_dict_entry('UI:Button:Cancel');
$this->add_dict_entry('UI:Button:Done');
$this->add_dict_entries('Error:');
$this->add_dict_entries('UI:Button:');
$this->add_dict_entries('UI:Search:');
$this->add_dict_entry('UI:UndefinedObject');
$this->add_dict_entries('Enum:Undefined');
if (!$this->IsPrintableVersion())
{

View File

@@ -41,6 +41,7 @@ class NiceWebPage extends WebPage
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery-migrate-1.4.1.min.js'); // Needed since many other plugins still rely on oldies like $.browser
$this->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/ui-lightness/jquery-ui-1.11.4.custom.css');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery-ui-1.11.4.custom.min.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/utils.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/hovertip.js');
// table sorting
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.tablesorter.js');
@@ -50,7 +51,24 @@ class NiceWebPage extends WebPage
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/datatable.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.positionBy.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.popupmenu.js');
$this->add_ready_script(
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/searchformforeignkeys.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_handler.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_handler_history.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_raw.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_string.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_external_field.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_numeric.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_enum.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_external_key.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_hierarchical_key.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_date_abstract.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_date.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_date_time.js');
$this->add_dict_entries('UI:Combo');
$this->add_ready_script(
<<< EOF
//add new widget called TruncatedList to properly display truncated lists when they are sorted
$.tablesorter.addWidget({

View File

@@ -19,15 +19,15 @@
* Class UIExtKeyWidget
* UI wdiget for displaying and editing external keys when
* A simple drop-down list is not enough...
*
*
* The layout is the following
*
*
* +-- #label_<id> (input)-------+ +-----------+
* | | | Browse... |
* +-----------------------------+ +-----------+
*
*
* And the popup dialog has the following layout:
*
*
* +------------------- ac_dlg_<id> (div)-----------+
* + +--- ds_<id> (div)---------------------------+ |
* | | +------------- fs_<id> (form)------------+ | |
@@ -61,13 +61,16 @@
require_once(APPROOT.'/application/webpage.class.inc.php');
require_once(APPROOT.'/application/displayblock.class.inc.php');
class UIExtKeyWidget
class UIExtKeyWidget
{
const ENUM_OUTPUT_FORMAT_CSV = 'csv';
const ENUM_OUTPUT_FORMAT_JSON = 'json';
protected $iId;
protected $sTargetClass;
protected $sAttCode;
protected $bSearchMode;
//public function __construct($sAttCode, $sClass, $sTitle, $oAllowedValues, $value, $iInputId, $bMandatory, $sNameSuffix = '', $sFieldPrefix = '', $sFormPrefix = '')
static public function DisplayFromAttCode($oPage, $sAttCode, $sClass, $sTitle, $oAllowedValues, $value, $iInputId, $bMandatory, $sFieldName = '', $sFormPrefix = '', $aArgs, $bSearchMode = false)
{
@@ -94,7 +97,7 @@ class UIExtKeyWidget
$this->sAttCode = $sAttCode;
$this->bSearchMode = $bSearchMode;
}
/**
* Get the HTML fragment corresponding to the ext key editing widget
* @param WebPage $oP The web page used for all the output
@@ -107,10 +110,10 @@ class UIExtKeyWidget
{
$this->bSearchMode = $bSearchMode;
}
$sTitle = addslashes($sTitle);
$sTitle = addslashes($sTitle);
$oPage->add_linked_script('../js/extkeywidget.js');
$oPage->add_linked_script('../js/forms-json-utils.js');
$bCreate = (!$this->bSearchMode) && (UserRights::IsActionAllowed($this->sTargetClass, UR_ACTION_BULK_MODIFY) && $bAllowTargetCreation);
$bExtensions = true;
$sMessage = Dict::S('UI:Message:EmptyList:UseSearchForm');
@@ -123,7 +126,7 @@ class UIExtKeyWidget
$sWizHelper = 'null';
$sWizHelperJSON = "''";
$sJSSearchMode = 'true';
}
}
else
{
if (isset($aArgs['wizHelper']))
@@ -159,7 +162,7 @@ class UIExtKeyWidget
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';
break;
@@ -168,7 +171,7 @@ class UIExtKeyWidget
case 'list':
default:
$sSelectMode = 'true';
$sHelpText = ''; //$this->oAttDef->GetHelpOnEdition();
$sHTMLValue .= "<div class=\"field_select_wrapper\">\n";
@@ -196,7 +199,7 @@ class UIExtKeyWidget
{
$key = $oObj->GetKey();
$display_value = $oObj->GetName();
if (($oAllowedValues->Count() == 1) && ($bMandatory == 'true') )
{
// When there is only once choice, select it by default
@@ -239,7 +242,7 @@ EOF
{
// Too many choices, use an autocomplete
$sSelectMode = 'false';
// Check that the given value is allowed
$oSearch = $oAllowedValues->GetFilter();
$oSearch->AddCondition('id', $value);
@@ -259,15 +262,15 @@ EOF
}
$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 .= "<input class=\"field_autocomplete\" count=\"".$oAllowedValues->Count()."\" type=\"text\" id=\"label_$this->iId\" value=\"$sDisplayValue\"/>";
$sHTMLValue .= "<span class=\"field_input_btn\"><img id=\"mini_search_{$this->iId}\" style=\"border:0;vertical-align:middle;cursor:pointer;\" src=\"../images/mini_search.gif?itopversion=".ITOP_VERSION."\" onClick=\"oACWidget_{$this->iId}.Search();\"/></span>";
// another hidden input to store & pass the object's Id
$sHTMLValue .= "<input type=\"hidden\" id=\"$this->iId\" name=\"{$sAttrFieldPrefix}{$sFieldName}\" value=\"".htmlentities($value, ENT_QUOTES, 'UTF-8')."\" />\n";
$JSSearchMode = $this->bSearchMode ? 'true' : 'false';
$JSSearchMode = $this->bSearchMode ? 'true' : 'false';
// Scripts to start the autocomplete and bind some events to it
$oPage->add_ready_script(
<<<EOF
@@ -320,7 +323,7 @@ EOF
return $sHTMLValue;
}
public function GetSearchDialog(WebPage $oPage, $sTitle, $oCurrObject = null)
{
$sHTML = '<div class="wizContainer" style="vertical-align:top;"><div id="dc_'.$this->iId.'">';
@@ -341,7 +344,17 @@ EOF
$bOpen = MetaModel::GetConfig()->Get('legacy_search_drawer_open');
$oFilter->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', $this->bSearchMode);
$oBlock = new DisplayBlock($oFilter, 'search', false, $aParams);
$sHTML .= $oBlock->GetDisplay($oPage, $this->iId, array('open' => $bOpen, 'currentId' => $this->iId));
$sHTML .= $oBlock->GetDisplay($oPage, $this->iId,
array(
'menu' => false,
'open' => $bOpen,
'currentId' => $this->iId,
'table_id' => "dr_{$this->iId}",
'table_inner_id' => "{$this->iId}_results",
'selection_mode' => true,
'selection_type' => 'single',
'cssCount' => '#count_'.$this->iId)
);
$sHTML .= "<form id=\"fr_{$this->iId}\" OnSubmit=\"return oACWidget_{$this->iId}.DoOk();\">\n";
$sHTML .= "<div id=\"dr_{$this->iId}\" style=\"vertical-align:top;background: #fff;height:100%;overflow:auto;padding:0;border:0;\">\n";
$sHTML .= "<div style=\"background: #fff; border:0; text-align:center; vertical-align:middle;\"><p>".Dict::S('UI:Message:EmptyList:UseSearchForm')."</p></div>\n";
@@ -375,7 +388,7 @@ EOF
{
throw new Exception('Implementation: null value for allowed values definition');
}
$oFilter = DBObjectSearch::FromOQL($sFilter);
if (strlen($sRemoteClass) > 0)
{
@@ -389,7 +402,7 @@ EOF
$oBlock = new DisplayBlock($oFilter, 'list', false, array('query_params' => array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId)));
$oBlock->Display($oP, $this->iId.'_results', array('this' => $oObj, 'cssCount'=> '#count_'.$this->iId, 'menu' => false, 'selection_mode' => true, 'selection_type' => 'single', 'table_id' => 'select_'.$this->sAttCode)); // Don't display the 'Actions' menu on the results
}
/**
* Search for objects to be selected
* @param WebPage $oP The page used for the output (usually an AjaxWebPage)
@@ -397,7 +410,7 @@ EOF
* @param DBObject $oObj The current object for the OQL context
* @param string $sContains The text of the autocomplete to filter the results
*/
public function AutoComplete(WebPage $oP, $sFilter, $oObj = null, $sContains)
public function AutoComplete(WebPage $oP, $sFilter, $oObj = null, $sContains, $sOutputFormat = self::ENUM_OUTPUT_FORMAT_CSV)
{
if (is_null($sFilter))
{
@@ -410,12 +423,27 @@ EOF
$oValuesSet = new ValueSetObjects($sFilter, 'friendlyname'); // Bypass GetName() to avoid the encoding by htmlentities
$oValuesSet->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', $this->bSearchMode);
$aValues = $oValuesSet->GetValues(array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId), $sContains);
foreach($aValues as $sKey => $sFriendlyName)
switch($sOutputFormat)
{
$oP->add(trim($sFriendlyName)."\t".$sKey."\n");
case static::ENUM_OUTPUT_FORMAT_JSON:
// Array flip to preserve values order on the label, otherwise the JS will re-order regarding the keys.
$oP->SetContentType('application/json');
$oP->add(json_encode(array_flip($aValues)));
break;
case static::ENUM_OUTPUT_FORMAT_CSV:
foreach($aValues as $sKey => $sFriendlyName)
{
$oP->add(trim($sFriendlyName)."\t".$sKey."\n");
}
break;
default:
throw new Exception('Invalid output format, "'.$sOutputFormat.'" given.');
break;
}
}
/**
* Get the display name of the selected object, to fill back the autocomplete
*/
@@ -505,7 +533,7 @@ EOF
}
}
}
// 3rd - set values from the page argument 'default'
$oNewObj->UpdateObjectFromArg('default');
@@ -522,7 +550,7 @@ EOF
$aFieldsComments[$sAttCode] = '&nbsp;<img src="../images/transp-lock.png" style="vertical-align:middle" title="'.htmlentities(Dict::S('UI:UploadNotSupportedInThisMode')).'"/>';
}
}
cmdbAbstractObject::DisplayCreationForm($oPage, $this->sTargetClass, $oNewObj, array(), array('formPrefix' => $this->iId, 'noRelations' => true, 'fieldsFlags' => $aFieldsFlags, 'fieldsComments' => $aFieldsComments));
cmdbAbstractObject::DisplayCreationForm($oPage, $this->sTargetClass, $oNewObj, array(), array('formPrefix' => $this->iId, 'noRelations' => true, 'fieldsFlags' => $aFieldsFlags, 'fieldsComments' => $aFieldsComments));
$oPage->add('</div></div></div>');
// $oPage->add_ready_script("\$('#ac_create_$this->iId').dialog({ width: $(window).width()*0.8, height: 'auto', autoOpen: false, modal: true, title: '$sDialogTitle'});\n");
$oPage->add_ready_script("\$('#ac_create_$this->iId').dialog({ width: 'auto', height: 'auto', maxHeight: $(window).height() - 50, autoOpen: false, modal: true, title: '$sDialogTitle'});\n");
@@ -566,7 +594,7 @@ EOF
$oPage->add('</div>');
$oPage->add("<input type=\"button\" id=\"btn_cancel_{$this->iId}\" value=\"".Dict::S('UI:Button:Cancel')."\" onClick=\"$('#dlg_tree_{$this->iId}').dialog('close');\">&nbsp;&nbsp;");
$oPage->add("<input type=\"button\" id=\"btn_ok_{$this->iId}\" value=\"".Dict::S('UI:Button:Ok')."\" onClick=\"oACWidget_{$this->iId}.DoHKOk();\">");
$oPage->add('</div></div>');
$oPage->add_ready_script("\$('#tree_$this->iId ul').treeview();\n");
$oPage->add_ready_script("\$('#dlg_tree_$this->iId').dialog({ width: 'auto', height: 'auto', autoOpen: true, modal: true, title: '$sDialogTitle', resizeStop: oACWidget_{$this->iId}.OnHKResize, close: oACWidget_{$this->iId}.OnHKClose });\n");
@@ -588,7 +616,7 @@ EOF
}
else
{
return array('error' => implode(' ', $aErrors), 'id' => 0);
return array('error' => implode(' ', $aErrors), 'id' => 0);
}
}
catch(Exception $e)
@@ -611,7 +639,7 @@ EOF
$aTree[$iParentId][$oObj->GetKey()] = $oObj->GetName();
$aNodes[$oObj->GetKey()] = $oObj;
}
$aParents = array_keys($aTree);
$aRoots = array();
foreach($aParents as $id)
@@ -626,7 +654,7 @@ EOF
$this->DumpNodes($oP, $iRootId, $aTree, $aNodes, $currValue);
}
}
function DumpNodes($oP, $iRootId, $aTree, $aNodes, $currValue)
{
$bSelect = true;

View File

@@ -278,14 +278,36 @@ class UILinksWidgetDirect
/**
* @param WebPage $oPage
* @param DBObject $oCurrentObj
* @throws Exception
* @param $aAlreadyLinked
*
* @throws \CoreException
* @throws \Exception
* @throws \OQLException
*/
public function GetObjectsSelectionDlg($oPage, $oCurrentObj)
public function GetObjectsSelectionDlg($oPage, $oCurrentObj, $aAlreadyLinked)
{
$sHtml = "<div class=\"wizContainer\" style=\"vertical-align:top;\">\n";
$oLinksetDef = MetaModel::GetAttributeDef($this->sClass, $this->sAttCode);
$valuesDef = $oLinksetDef->GetValuesDef();
$oHiddenFilter = new DBObjectSearch($this->sLinkedClass);
if (($oCurrentObj != null) && MetaModel::IsSameFamilyBranch($this->sLinkedClass, $this->sClass))
{
// Prevent linking to self if the linked object is of the same family
// and already present in the database
if (!$oCurrentObj->IsNew())
{
$oHiddenFilter->AddCondition('id', $oCurrentObj->GetKey(), '!=');
}
}
if (count($aAlreadyLinked) > 0)
{
$oHiddenFilter->AddCondition('id', $aAlreadyLinked, 'NOTIN');
}
$oHiddenCriteria = $oHiddenFilter->GetCriteria();
$aArgs = $oHiddenFilter->GetInternalParams();
$sHiddenCriteria = $oHiddenCriteria->Render($aArgs);
$oLinkSetDef = MetaModel::GetAttributeDef($this->sClass, $this->sAttCode);
$valuesDef = $oLinkSetDef->GetValuesDef();
if ($valuesDef === null)
{
$oFilter = new DBObjectSearch($this->sLinkedClass);
@@ -298,13 +320,27 @@ class UILinksWidgetDirect
}
$oFilter = DBObjectSearch::FromOQL($valuesDef->GetFilterExpression());
}
if ($oCurrentObj != null)
{
$this->SetSearchDefaultFromContext($oCurrentObj, $oFilter);
$aArgs = array_merge($oCurrentObj->ToArgs('this'), $oFilter->GetInternalParams());
$oFilter->SetInternalParams($aArgs);
}
$bOpen = MetaModel::GetConfig()->Get('legacy_search_drawer_open');
$oBlock = new DisplayBlock($oFilter, 'search', false);
$sHtml .= $oBlock->GetDisplay($oPage, "SearchFormToAdd_{$this->sInputid}", array('open' => $bOpen));
$sHtml .= $oBlock->GetDisplay($oPage, "SearchFormToAdd_{$this->sInputid}",
array(
'open' => $bOpen,
'result_list_outer_selector' => "SearchResultsToAdd_{$this->sInputid}",
'table_id' => "add_{$this->sInputid}",
'table_inner_id' => "ResultsToAdd_{$this->sInputid}",
'cssCount' => "#count_{$this->sInputid}",
'query_params' => $oFilter->GetInternalParams(),
'hidden_criteria' => $sHiddenCriteria,
)
);
$sHtml .= "<form id=\"ObjectsAddForm_{$this->sInputid}\">\n";
$sHtml .= "<div id=\"SearchResultsToAdd_{$this->sInputid}\" style=\"vertical-align:top;background: #fff;height:100%;overflow:auto;padding:0;border:0;\">\n";
$sHtml .= "<div style=\"background: #fff; border:0; text-align:center; vertical-align:middle;\"><p>".Dict::S('UI:Message:EmptyList:UseSearchForm')."</p></div>\n";
@@ -330,8 +366,8 @@ class UILinksWidgetDirect
{
$sRemoteClass = $this->sLinkedClass;
}
$oLinksetDef = MetaModel::GetAttributeDef($this->sClass, $this->sAttCode);
$valuesDef = $oLinksetDef->GetValuesDef();
$oLinkSetDef = MetaModel::GetAttributeDef($this->sClass, $this->sAttCode);
$valuesDef = $oLinkSetDef->GetValuesDef();
if ($valuesDef === null)
{
$oFilter = new DBObjectSearch($sRemoteClass);
@@ -348,7 +384,7 @@ class UILinksWidgetDirect
if (($oCurrentObj != null) && MetaModel::IsSameFamilyBranch($sRemoteClass, $this->sClass))
{
// Prevent linking to self if the linked object is of the same family
// and laready present in the database
// and already present in the database
if (!$oCurrentObj->IsNew())
{
$oFilter->AddCondition('id', $oCurrentObj->GetKey(), '!=');
@@ -360,6 +396,8 @@ class UILinksWidgetDirect
}
if ($oCurrentObj != null)
{
$this->SetSearchDefaultFromContext($oCurrentObj, $oFilter);
$aArgs = array_merge($oCurrentObj->ToArgs('this'), $oFilter->GetInternalParams());
$oFilter->SetInternalParams($aArgs);
}

View File

@@ -386,21 +386,55 @@ EOF
/**
* @param WebPage $oPage
* @param DBObject $oCurrentObj
* @param $sJson
* @param array $aAlreadyLinkedIds
*
* @throws \CoreException
* @throws \DictExceptionMissingString
* @throws \Exception
*/
public function GetObjectPickerDialog($oPage, $oCurrentObj)
public function GetObjectPickerDialog($oPage, $oCurrentObj, $sJson, $aAlreadyLinkedIds = array())
{
$bOpen = MetaModel::GetConfig()->Get('legacy_search_drawer_open');
$sHtml = "<div class=\"wizContainer\" style=\"vertical-align:top;\">\n";
$oAlreadyLinkedFilter = new DBObjectSearch($this->m_sRemoteClass);
if (!$this->m_bDuplicatesAllowed && count($aAlreadyLinkedIds) > 0)
{
$oAlreadyLinkedFilter->AddCondition('id', $aAlreadyLinkedIds, 'NOTIN');
$oAlreadyLinkedExpression = $oAlreadyLinkedFilter->GetCriteria();
$sAlreadyLinkedExpression = $oAlreadyLinkedExpression->Render();
}
else
{
$sAlreadyLinkedExpression = '';
}
$oFilter = new DBObjectSearch($this->m_sRemoteClass);
$this->SetSearchDefaultFromContext($oCurrentObj, $oFilter);
if ($oCurrentObj != null)
{
$this->SetSearchDefaultFromContext($oCurrentObj, $oFilter);
}
$oBlock = new DisplayBlock($oFilter, 'search', false);
$sHtml .= $oBlock->GetDisplay($oPage, "SearchFormToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}", array('open' => $bOpen));
$sHtml .= "<form id=\"ObjectsAddForm_{$this->m_sAttCode}{$this->m_sNameSuffix}\" OnSubmit=\"return oWidget{$this->m_iInputId}.DoAddObjects(this.id);\">\n";
$sHtml .= $oBlock->GetDisplay($oPage, "SearchFormToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}",
array(
'open' => $bOpen,
'menu' => false,
'result_list_outer_selector' => "SearchResultsToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}",
'table_id' => 'add_'.$this->m_sAttCode,
'table_inner_id' => "ResultsToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}",
'selection_mode' => true,
'json' => $sJson,
'cssCount' => '#count_'.$this->m_sAttCode.$this->m_sNameSuffix,
'query_params' => $oFilter->GetInternalParams(),
'hidden_criteria' => $sAlreadyLinkedExpression,
));
$sHtml .= "<form id=\"ObjectsAddForm_{$this->m_sAttCode}{$this->m_sNameSuffix}\">\n";
$sHtml .= "<div id=\"SearchResultsToAdd_{$this->m_sAttCode}{$this->m_sNameSuffix}\" style=\"vertical-align:top;background: #fff;height:100%;overflow:auto;padding:0;border:0;\">\n";
$sHtml .= "<div style=\"background: #fff; border:0; text-align:center; vertical-align:middle;\"><p>".Dict::S('UI:Message:EmptyList:UseSearchForm')."</p></div>\n";
$sHtml .= "</div>\n";
$sHtml .= "<input type=\"hidden\" id=\"count_{$this->m_sAttCode}{$this->m_sNameSuffix}\" value=\"0\"/>";
$sHtml .= "<input type=\"button\" value=\"".Dict::S('UI:Button:Cancel')."\" onClick=\"$('#dlg_{$this->m_sAttCode}{$this->m_sNameSuffix}').dialog('close');\">&nbsp;&nbsp;<input id=\"btn_ok_{$this->m_sAttCode}{$this->m_sNameSuffix}\" disabled=\"disabled\" type=\"submit\" value=\"".Dict::S('UI:Button:Add')."\">";
$sHtml .= "<input type=\"button\" value=\"".Dict::S('UI:Button:Cancel')."\" onClick=\"$('#dlg_{$this->m_sAttCode}{$this->m_sNameSuffix}').dialog('close');\">&nbsp;&nbsp;<input id=\"btn_ok_{$this->m_sAttCode}{$this->m_sNameSuffix}\" disabled=\"disabled\" type=\"button\" onclick=\"return oWidget{$this->m_iInputId}.DoAddObjects(this.id);\" value=\"".Dict::S('UI:Button:Add')."\">";
$sHtml .= "</div>\n";
$sHtml .= "</form>\n";
$oPage->add($sHtml);
@@ -416,7 +450,7 @@ EOF
* @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
*/
public function SearchObjectsToAdd(WebPage $oP, $sRemoteClass = '', $aAlreadyLinkedIds = array())
public function SearchObjectsToAdd(WebPage $oP, $sRemoteClass = '', $aAlreadyLinkedIds = array(), $oCurrentObj = null)
{
if ($sRemoteClass != '')
{
@@ -432,6 +466,7 @@ EOF
{
$oFilter->AddCondition('id', $aAlreadyLinkedIds, 'NOTIN');
}
$this->SetSearchDefaultFromContext($oCurrentObj, $oFilter);
$oBlock = new DisplayBlock($oFilter, 'list', false);
$oBlock->Display($oP, "ResultsToAdd_{$this->m_sAttCode}", array('menu' => false, 'cssCount'=> '#count_'.$this->m_sAttCode.$this->m_sNameSuffix , 'selection_mode' => true, 'table_id' => 'add_'.$this->m_sAttCode)); // Don't display the 'Actions' menu on the results
}

View File

@@ -0,0 +1,117 @@
<?php
/**
*
* Copyright (C) 2010-2018 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/>
*
*/
require_once(APPROOT.'/application/webpage.class.inc.php');
require_once(APPROOT.'/application/displayblock.class.inc.php');
class UISearchFormForeignKeys
{
public function __construct($sTargetClass, $iInputId = null)
{
$this->m_sRemoteClass = $sTargetClass;
$this->m_iInputId = $iInputId;
}
/**
* @param WebPage $oPage
*
* @param $sTitle
*
* @throws \Exception
*/
public function ShowModalSearchForeignKeys($oPage, $sTitle)
{
$bOpen = MetaModel::GetConfig()->Get('legacy_search_drawer_open');
$sHtml = "<div class=\"wizContainer\" style=\"vertical-align:top;\">\n";
$oFilter = new DBObjectSearch($this->m_sRemoteClass);
$oBlock = new DisplayBlock($oFilter, 'search', false);
$sHtml .= $oBlock->GetDisplay($oPage, "SearchFormToAdd_{$this->m_iInputId}",
array(
'open' => $bOpen,
'menu' => false,
'result_list_outer_selector' => "SearchResultsToAdd_{$this->m_iInputId}",
'table_id' => "add_{$this->m_iInputId}",
'table_inner_id' => "ResultsToAdd_{$this->m_iInputId}",
'selection_mode' => true,
'cssCount' => "#count_{$this->m_iInputId}",
'query_params' => $oFilter->GetInternalParams(),
));
$sHtml .= "<form id=\"ObjectsAddForm_{$this->m_iInputId}\">\n";
$sHtml .= "<div id=\"SearchResultsToAdd_{$this->m_iInputId}\" style=\"vertical-align:top;background: #fff;height:100%;overflow:auto;padding:0;border:0;\">\n";
$sHtml .= "<div style=\"background: #fff; border:0; text-align:center; vertical-align:middle;\"><p>".Dict::S('UI:Message:EmptyList:UseSearchForm')."</p></div>\n";
$sHtml .= "</div>\n";
$sHtml .= "<input type=\"hidden\" id=\"count_{$this->m_iInputId}\" value=\"0\"/>";
$sHtml .= "<input type=\"button\" value=\"".Dict::S('UI:Button:Cancel')."\" onClick=\"$('#dlg_{$this->m_iInputId}').dialog('close');\">&nbsp;&nbsp;<input id=\"btn_ok_{$this->m_iInputId}\" disabled=\"disabled\" type=\"button\" onclick=\"return oForeignKeysWidget{$this->m_iInputId}.DoAddObjects(this.id);\" value=\"".Dict::S('UI:Button:Add')."\">";
$sHtml .= "</div>\n";
$sHtml .= "</form>\n";
$oPage->add($sHtml);
$oPage->add_ready_script("$('#dlg_{$this->m_iInputId}').dialog({ width: $(window).width()*0.8, height: $(window).height()*0.8, autoOpen: false, modal: true, resizeStop: oForeignKeysWidget{$this->m_iInputId}.UpdateSizes });");
$oPage->add_ready_script("$('#dlg_{$this->m_iInputId}').dialog('option', {title:'$sTitle'});");
$oPage->add_ready_script("$('#SearchFormToAdd_{$this->m_iInputId} form').bind('submit.uilinksWizard', oForeignKeysWidget{$this->m_iInputId}.SearchObjectsToAdd);");
$oPage->add_ready_script("$('#SearchFormToAdd_{$this->m_iInputId}').resize(oForeignKeysWidget{$this->m_iInputId}.UpdateSizes);");
}
public function GetFullListForeignKeysFromSelection($oPage, $oFullSetFilter)
{
try
{
$aLinkedObjects = utils::ReadMultipleSelectionWithFriendlyname($oFullSetFilter);
$oPage->add(json_encode($aLinkedObjects));
}
catch (CoreException $e)
{
http_response_code(500);
$oPage->add(json_encode(array('error' => $e->GetMessage())));
IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString());
}
}
/**
* Search for objects to be linked to the current object (i.e "remote" objects)
*
* @param WebPage $oP The page used for the output (usually an AjaxWebPage)
* @param string $sRemoteClass Name of the "remote" class to perform the search on, must be a derived class of m_sRemoteClass
*
* @throws \Exception
*/
public function ListResultsSearchForeignKeys(WebPage $oP, $sRemoteClass = '')
{
if ($sRemoteClass != '')
{
// assert(MetaModel::IsParentClass($this->m_sRemoteClass, $sRemoteClass));
$oFilter = new DBObjectSearch($sRemoteClass);
}
else
{
// No remote class specified use the one defined in the linkedset
$oFilter = new DBObjectSearch($this->m_sRemoteClass);
}
$oBlock = new DisplayBlock($oFilter, 'list', false);
$oBlock->Display($oP, "ResultsToAdd_{$this->m_iInputId}",
array('menu' => false, 'cssCount' => "#count_{$this->m_iInputId}", 'selection_mode' => true, 'table_id' => "add_{$this->m_iInputId}"));
}
}

View File

@@ -413,8 +413,10 @@ class utils
/**
* Interprets the results posted by a normal or paginated list (in multiple selection mode)
*
* @param $oFullSetFilter DBSearch The criteria defining the whole sets of objects being selected
* @return Array An arry of object IDs corresponding to the objects selected in the set
*
* @return Array An array of object IDs corresponding to the objects selected in the set
*/
public static function ReadMultipleSelection($oFullSetFilter)
{
@@ -448,6 +450,47 @@ class utils
return $aSelectedObj;
}
/**
* Interprets the results posted by a normal or paginated list (in multiple selection mode)
*
* @param DBSearch $oFullSetFilter The criteria defining the whole sets of objects being selected
*
* @return Array An array of object IDs:friendlyname corresponding to the objects selected in the set
* @throws \CoreException
*/
public static function ReadMultipleSelectionWithFriendlyname($oFullSetFilter)
{
$sSelectionMode = utils::ReadParam('selectionMode', '');
if ($sSelectionMode === '')
{
throw new CoreException('selectionMode is mandatory');
}
// Paginated selection
$aSelectedIds = utils::ReadParam('storedSelection', array());
if ($sSelectionMode == 'positive')
{
// Only the explicitly listed items are selected
$oFullSetFilter->AddCondition('id', $aSelectedIds, 'IN');
}
else
{
// All items of the set are selected, except the one explicitly listed
$oFullSetFilter->AddCondition('id', $aSelectedIds, 'NOTIN');
}
$aSelectedObj = array();
$oFullSet = new DBObjectSet($oFullSetFilter);
$sClassAlias = $oFullSetFilter->GetClassAlias();
$oFullSet->OptimizeColumnLoad(array($sClassAlias => array('friendlyname'))); // We really need only the IDs but it does not work since id is not a real field
while ($oObj = $oFullSet->Fetch())
{
$aSelectedObj[$oObj->GetKey()] = $oObj->Get('friendlyname');
}
return $aSelectedObj;
}
public static function GetNewTransactionId()
{
return privUITransaction::GetNewTransactionId();

View File

@@ -58,6 +58,7 @@ class WebPage implements Page
protected $s_deferred_content;
protected $a_scripts;
protected $a_dict_entries;
protected $a_dict_entries_prefixes;
protected $a_styles;
protected $a_include_scripts;
protected $a_include_stylesheets;
@@ -80,6 +81,7 @@ class WebPage implements Page
$this->s_deferred_content = '';
$this->a_scripts = array();
$this->a_dict_entries = array();
$this->a_dict_entries_prefixes = array();
$this->a_styles = array();
$this->a_linked_scripts = array();
$this->a_linked_stylesheets = array();
@@ -243,10 +245,39 @@ class WebPage implements Page
/**
* Add a dictionary entry for the Javascript side
*/
public function add_dict_entry($s_entryId)
{
$this->a_dict_entries[$s_entryId] = Dict::S($s_entryId);
}
public function add_dict_entry($s_entryId)
{
$this->a_dict_entries[] = $s_entryId;
}
/**
* Add a set of dictionary entries (based on the given prefix) for the Javascript side
*/
public function add_dict_entries($s_entriesPrefix)
{
$this->a_dict_entries_prefixes[] = $s_entriesPrefix;
}
protected function get_dict_signature()
{
return str_replace('_', '', Dict::GetUserLanguage()).'-'.md5(implode(',', $this->a_dict_entries).'|'.implode(',', $this->a_dict_entries_prefixes));
}
protected function get_dict_file_content()
{
$aEntries = array();
foreach($this->a_dict_entries as $sCode)
{
$aEntries[$sCode] = Dict::S($sCode);
}
foreach($this->a_dict_entries_prefixes as $sPrefix)
{
$aEntries = array_merge($aEntries, Dict::ExportEntries($sPrefix));
}
$sJSFile = 'var aDictEntries = '.json_encode($aEntries);
return $sJSFile;
}
/**
@@ -347,7 +378,7 @@ class WebPage implements Page
*/
public function GetDetails($aFields)
{
$sHtml = "<div class=\"details\">\n";
$sHtml = "<div class=\"details\" id='search-widget-results-outer'>\n";
foreach($aFields as $aAttrib)
{
$sDataAttCode = isset($aAttrib['attcode']) ? "data-attcode=\"{$aAttrib['attcode']}\"" : '';
@@ -501,6 +532,9 @@ class WebPage implements Page
echo "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, shrink-to-fit=no\" />";
echo "<title>".htmlentities($this->s_title, ENT_QUOTES, 'UTF-8')."</title>\n";
echo $this->get_base_tag();
$this->output_dict_entries();
foreach($this->a_linked_scripts as $s_script)
{
// Make sure that the URL to the script contains the application's version number
@@ -524,7 +558,6 @@ class WebPage implements Page
}
echo "</script>\n";
}
$this->output_dict_entries();
foreach($this->a_linked_stylesheets as $a_stylesheet)
{
if (strpos($a_stylesheet['link'], '?') === false)
@@ -778,36 +811,21 @@ class WebPage implements Page
protected function output_dict_entries($bReturnOutput = false)
{
$sHtml = '';
if (count($this->a_dict_entries)>0)
if ((count($this->a_dict_entries) > 0) || (count($this->a_dict_entries_prefixes) > 0))
{
$sHtml .= "<script type=\"text/javascript\">\n";
$sHtml .= "var Dict = {};\n";
$sHtml .= "Dict._entries = {};\n";
$sHtml .= "Dict.S = function(sEntry) {\n";
$sHtml .= " if (sEntry in Dict._entries)\n";
$sHtml .= " {\n";
$sHtml .= " return Dict._entries[sEntry];\n";
$sHtml .= " }\n";
$sHtml .= " else\n";
$sHtml .= " {\n";
$sHtml .= " return sEntry;\n";
$sHtml .= " }\n";
$sHtml .= "};\n";
foreach($this->a_dict_entries as $s_entry => $s_value)
if (class_exists('Dict'))
{
$sHtml .= "Dict._entries['$s_entry'] = '".addslashes($s_value)."';\n";
// The dictionary may not be available for example during the setup...
// Create a specific dictionary file and load it as a JS script
$sSignature = $this->get_dict_signature();
$sJSFileName = utils::GetCachePath().$sSignature.'.js';
if (!file_exists($sJSFileName) && is_writable(utils::GetCachePath()))
{
file_put_contents($sJSFileName, $this->get_dict_file_content());
}
// Load the dictionary as the first javascript file, so that other JS file benefit from the translations
array_unshift($this->a_linked_scripts, utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php/?operation=dict&s='.$sSignature);
}
$sHtml .= "</script>\n";
}
if ($bReturnOutput)
{
return $sHtml;
}
else
{
echo $sHtml;
}
}
}

View File

@@ -111,6 +111,18 @@ define('LINKSET_EDITMODE_ADDREMOVE', 4); // The "linked" objects can be added/re
*/
abstract class AttributeDefinition
{
const SEARCH_WIDGET_TYPE_RAW = 'raw';
const SEARCH_WIDGET_TYPE_STRING = 'string';
const SEARCH_WIDGET_TYPE_NUMERIC = 'numeric';
const SEARCH_WIDGET_TYPE_ENUM = 'enum';
const SEARCH_WIDGET_TYPE_EXTERNAL_KEY = 'external_key';
const SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY = 'hierarchical_key';
const SEARCH_WIDGET_TYPE_EXTERNAL_FIELD = 'external_field';
const SEARCH_WIDGET_TYPE_DATE_TIME = 'date_time';
const SEARCH_WIDGET_TYPE_DATE = 'date';
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
public function GetType()
{
return Dict::S('Core:'.get_class($this));
@@ -122,6 +134,24 @@ abstract class AttributeDefinition
abstract public function GetEditClass();
/**
* Return the search widget type corresponding to this attribute
*
* @return string
*/
public function GetSearchType()
{
return static::SEARCH_WIDGET_TYPE;
}
/**
* @return bool
*/
public function IsSearchable()
{
return static::SEARCH_WIDGET_TYPE != static::SEARCH_WIDGET_TYPE_RAW;
}
protected $m_sCode;
private $m_aParams = array();
protected $m_sHostClass = '!undefined!';
@@ -1617,6 +1647,8 @@ class AttributeDBField extends AttributeDBFieldVoid
*/
class AttributeInteger extends AttributeDBField
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC;
static public function ListExpectedParams()
{
return parent::ListExpectedParams();
@@ -1710,6 +1742,8 @@ class AttributeInteger extends AttributeDBField
*/
class AttributeObjectKey extends AttributeDBFieldVoid
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY;
static public function ListExpectedParams()
{
return array_merge(parent::ListExpectedParams(), array('class_attcode', 'is_null_allowed'));
@@ -1765,6 +1799,8 @@ class AttributeObjectKey extends AttributeDBFieldVoid
*/
class AttributePercentage extends AttributeInteger
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC;
public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
{
$iWidth = 5; // Total width of the percentage bar graph, in em...
@@ -1804,6 +1840,8 @@ class AttributePercentage extends AttributeInteger
*/
class AttributeDecimal extends AttributeDBField
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC;
static public function ListExpectedParams()
{
return array_merge(parent::ListExpectedParams(), array('digits', 'decimals' /* including precision */));
@@ -1902,6 +1940,8 @@ class AttributeDecimal extends AttributeDBField
*/
class AttributeBoolean extends AttributeInteger
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
static public function ListExpectedParams()
{
return parent::ListExpectedParams();
@@ -2098,6 +2138,8 @@ class AttributeBoolean extends AttributeInteger
*/
class AttributeString extends AttributeDBField
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
static public function ListExpectedParams()
{
return parent::ListExpectedParams();
@@ -2248,6 +2290,8 @@ class AttributeString extends AttributeDBField
*/
class AttributeClass extends AttributeString
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
static public function ListExpectedParams()
{
return array_merge(parent::ListExpectedParams(), array("class_category", "more_values"));
@@ -2300,6 +2344,8 @@ class AttributeClass extends AttributeString
*/
class AttributeApplicationLanguage extends AttributeString
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
static public function ListExpectedParams()
{
return parent::ListExpectedParams();
@@ -2336,6 +2382,8 @@ class AttributeApplicationLanguage extends AttributeString
*/
class AttributeFinalClass extends AttributeString
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
public function __construct($sCode, $aParams)
{
$this->m_sCode = $sCode;
@@ -2490,6 +2538,8 @@ class AttributeFinalClass extends AttributeString
*/
class AttributePassword extends AttributeString
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
static public function ListExpectedParams()
{
return parent::ListExpectedParams();
@@ -2542,6 +2592,8 @@ class AttributePassword extends AttributeString
*/
class AttributeEncryptedString extends AttributeString
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
static $sKey = null; // Encryption key used for all encrypted fields
public function __construct($sCode, $aParams)
@@ -2984,6 +3036,8 @@ class AttributeLongText extends AttributeText
*/
class AttributeCaseLog extends AttributeLongText
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
public function GetNullValue()
{
return '';
@@ -3403,6 +3457,8 @@ class AttributeIPAddress extends AttributeString
*/
class AttributeOQL extends AttributeText
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
public function GetEditClass() {return "OQLExpression";}
}
@@ -3413,6 +3469,7 @@ class AttributeOQL extends AttributeText
*/
class AttributeTemplateString extends AttributeString
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
}
/**
@@ -3422,6 +3479,7 @@ class AttributeTemplateString extends AttributeString
*/
class AttributeTemplateText extends AttributeText
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
}
/**
@@ -3431,6 +3489,8 @@ class AttributeTemplateText extends AttributeText
*/
class AttributeTemplateHTML extends AttributeText
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
public function GetSQLColumns($bFullSpec = false)
{
$aColumns = array();
@@ -3465,6 +3525,8 @@ class AttributeTemplateHTML extends AttributeText
*/
class AttributeEnum extends AttributeString
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_ENUM;
static public function ListExpectedParams()
{
return parent::ListExpectedParams();
@@ -3892,6 +3954,8 @@ class AttributeMetaEnum extends AttributeEnum
*/
class AttributeDateTime extends AttributeDBField
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_DATE_TIME;
static $oFormat = null;
static public function GetFormat()
@@ -4418,6 +4482,8 @@ class AttributeDuration extends AttributeInteger
*/
class AttributeDate extends AttributeDateTime
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_DATE;
static $oDateFormat = null;
static public function GetFormat()
@@ -4562,6 +4628,8 @@ class AttributeDeadline extends AttributeDateTime
*/
class AttributeExternalKey extends AttributeDBFieldVoid
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY;
static public function ListExpectedParams()
{
return array_merge(parent::ListExpectedParams(), array("targetclass", "is_null_allowed", "on_target_delete"));
@@ -4767,6 +4835,8 @@ class AttributeExternalKey extends AttributeDBFieldVoid
*/
class AttributeHierarchicalKey extends AttributeExternalKey
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY;
protected $m_sTargetClass;
static public function ListExpectedParams()
@@ -4914,6 +4984,35 @@ class AttributeHierarchicalKey extends AttributeExternalKey
*/
class AttributeExternalField extends AttributeDefinition
{
/**
* Return the search widget type corresponding to this attribute
*
* @return string
*/
public function GetSearchType()
{
// Not necessary the external key is already present
if ($this->IsFriendlyName())
{
return self::SEARCH_WIDGET_TYPE_RAW;
}
try
{
$oRemoteAtt = $this->GetExtAttDef();
if ($oRemoteAtt instanceof AttributeString)
{
return self::SEARCH_WIDGET_TYPE_EXTERNAL_FIELD;
}
}
catch (CoreException $e)
{
}
return self::SEARCH_WIDGET_TYPE_RAW;
}
static public function ListExpectedParams()
{
return array_merge(parent::ListExpectedParams(), array("extkey_attcode", "target_attcode"));
@@ -4965,6 +5064,23 @@ class AttributeExternalField extends AttributeDefinition
}
return $sLabel;
}
public function GetLabelForSearchField()
{
$sLabel = parent::GetLabel('');
if (strlen($sLabel) == 0)
{
$sKeyAttCode = $this->Get("extkey_attcode");
$oExtKeyAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $sKeyAttCode);
$sLabel = $oExtKeyAttDef->GetLabel($this->m_sCode);
$oRemoteAtt = $this->GetExtAttDef();
$sLabel .= '->'.$oRemoteAtt->GetLabel($this->m_sCode);
}
return $sLabel;
}
public function GetDescription($sDefault = null)
{
$sLabel = parent::GetDescription('');
@@ -5285,6 +5401,8 @@ class AttributeURL extends AttributeString
*/
class AttributeBlob extends AttributeDefinition
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
static public function ListExpectedParams()
{
return array_merge(parent::ListExpectedParams(), array("depends_on"));
@@ -5612,6 +5730,8 @@ class AttributeImage extends AttributeBlob
*/
class AttributeStopWatch extends AttributeDefinition
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
static public function ListExpectedParams()
{
// The list of thresholds must be an array of iPercent => array of 'option' => value
@@ -6280,6 +6400,8 @@ class AttributeStopWatch extends AttributeDefinition
*/
class AttributeSubItem extends AttributeDefinition
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
static public function ListExpectedParams()
{
return array_merge(parent::ListExpectedParams(), array('target_attcode', 'item_code'));
@@ -6446,6 +6568,8 @@ class AttributeSubItem extends AttributeDefinition
*/
class AttributeOneWayPassword extends AttributeDefinition
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
static public function ListExpectedParams()
{
return array_merge(parent::ListExpectedParams(), array("depends_on"));
@@ -6609,6 +6733,8 @@ class AttributeOneWayPassword extends AttributeDefinition
// Indexed array having two dimensions
class AttributeTable extends AttributeDBField
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
public function GetEditClass() {return "Table";}
protected function GetSQLCol($bFullSpec = false)
@@ -6838,6 +6964,8 @@ class AttributePropertySet extends AttributeTable
*/
class AttributeFriendlyName extends AttributeDefinition
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
public function __construct($sCode)
{
$this->m_sCode = $sCode;
@@ -7004,6 +7132,8 @@ class AttributeFriendlyName extends AttributeDefinition
*/
class AttributeRedundancySettings extends AttributeDBField
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
static public function ListExpectedParams()
{
return array('sql', 'relation_code', 'from_class', 'neighbour_id', 'enabled', 'enabled_mode', 'min_up', 'min_up_type', 'min_up_mode');
@@ -7394,6 +7524,8 @@ class AttributeRedundancySettings extends AttributeDBField
*/
class AttributeCustomFields extends AttributeDefinition
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING;
static public function ListExpectedParams()
{
return array_merge(parent::ListExpectedParams(), array("handler_class"));

View File

@@ -241,8 +241,7 @@ class CMDBSource
}
catch(mysqli_sql_exception $e)
{
throw new MySQLException('Could not connect to the DB server',
array('host' => $sServer, 'user' => $sUser), $e);
throw new MySQLException('Could not connect to the DB server', array('host' => $sServer, 'user' => $sUser), $e);
}
if ($bCheckTlsAfterConnection

View File

@@ -20,7 +20,7 @@
* Class Dict
* Management of localizable strings
*
* @copyright Copyright (C) 2010-2012 Combodo SARL
* @copyright Copyright (C) 2010-2018 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
@@ -65,6 +65,11 @@ class Dict
protected static $m_aData = array();
protected static $m_sApplicationPrefix = null;
/**
* @param $sLanguageCode
*
* @throws \DictExceptionUnknownLanguage
*/
public static function SetDefaultLanguage($sLanguageCode)
{
if (!array_key_exists($sLanguageCode, self::$m_aLanguages))
@@ -74,6 +79,11 @@ class Dict
self::$m_sDefaultLanguage = $sLanguageCode;
}
/**
* @param $sLanguageCode
*
* @throws \DictExceptionUnknownLanguage
*/
public static function SetUserLanguage($sLanguageCode)
{
if (!array_key_exists($sLanguageCode, self::$m_aLanguages))
@@ -113,7 +123,6 @@ class Dict
* @param string $sDefault Default value if there is no match in the dictionary
* @param bool $bUserLanguageOnly True to allow the use of the default language as a fallback, false otherwise
*
* @throws DictExceptionMissingString
* @return string
*/
public static function S($sStringCode, $sDefault = null, $bUserLanguageOnly = false)
@@ -124,7 +133,7 @@ class Dict
if (!array_key_exists(self::GetUserLanguage(), self::$m_aData))
{
// It may happen, when something happens before the dictionnaries get loaded
// It may happen, when something happens before the dictionaries get loaded
return $sStringCode;
}
$aCurrentDictionary = self::$m_aData[self::GetUserLanguage()];
@@ -155,25 +164,12 @@ class Dict
}
// Could not find the string...
//
switch (self::$m_iErrorMode)
if (is_null($sDefault))
{
case DICT_ERR_STRING:
if (is_null($sDefault))
{
return $sStringCode;
}
else
{
return $sDefault;
}
break;
case DICT_ERR_EXCEPTION:
default:
throw new DictExceptionMissingString(self::$m_sCurrentLanguage, $sStringCode);
break;
return $sStringCode;
}
return 'bug!';
return $sDefault;
}
@@ -285,6 +281,9 @@ class Dict
/**
* Clone a string in every language (if it exists in that language)
*
* @param $sSourceCode
* @param $sDestCode
*/
public static function CloneString($sSourceCode, $sDestCode)
{
@@ -357,5 +356,38 @@ class Dict
// No need to actually load the strings since it's only used to know the list of languages
// at setup time !!
}
/**
* Export all the dictionary entries - of the given language - whose code matches the given prefix
* missing entries in the current language will be replaced by entries in the default language
* @param string $sStartingWith
* @return string[]
*/
public static function ExportEntries($sStartingWith)
{
self::InitLangIfNeeded(self::GetUserLanguage());
self::InitLangIfNeeded(self::$m_sDefaultLanguage);
$aEntries = array();
$iLength = strlen($sStartingWith);
// First prefill the array with entries from the default language
foreach(self::$m_aData[self::$m_sDefaultLanguage] as $sCode => $sEntry)
{
if (substr($sCode, 0, $iLength) == $sStartingWith)
{
$aEntries[$sCode] = $sEntry;
}
}
// Now put (overwrite) the entries for the user language
foreach(self::$m_aData[self::GetUserLanguage()] as $sCode => $sEntry)
{
if (substr($sCode, 0, $iLength) == $sStartingWith)
{
$aEntries[$sCode] = $sEntry;
}
}
return $aEntries;
}
}
?>

View File

@@ -1439,7 +1439,7 @@ class DisplayableGraph extends SimpleGraph
$aExcludedByClass[get_class($oObj)][] = $oObj->GetKey();
}
$oP->add("<div class=\"not-printable\">\n");
$oP->add("<div id=\"ds_flash\" class=\"SearchDrawer\" style=\"display:none;\">\n");
$oP->add("<div id=\"ds_flash\" class=\"search_box\" style=\"display:none;\">\n");
if (!$oP->IsPrintableVersion())
{
$oP->add_ready_script(

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (c) 2010-2017 Combodo SARL
// Copyright (c) 2010-2018 Combodo SARL
//
// This file is part of iTop.
//
@@ -21,11 +21,12 @@ class MissingQueryArgument extends CoreException
{
}
abstract class Expression
{
/**
* Perform a deep clone (as opposed to "clone" which does copy a reference to the underlying objects)
**/
**/
public function DeepClone()
{
return unserialize(serialize($this));
@@ -33,11 +34,36 @@ abstract class Expression
// recursive translation of identifiers
abstract public function GetUnresolvedFields($sAlias, &$aUnresolved);
/**
* @param array $aTranslationData
* @param bool $bMatchAll
* @param bool $bMarkFieldsAsResolved
*
* @return Expression Translated expression
*/
abstract public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true);
// recursive rendering (aArgs used as input by default, or used as output if bRetrofitParams set to True
abstract public function Render(&$aArgs = null, $bRetrofitParams = false);
/**
* @param DBObjectSearch $oSearch
* @param array $aArgs
* @param AttributeDefinition $oAttDef
*
* @return array parameters for the search form
*/
public function Display($oSearch, &$aArgs = null, $oAttDef = null)
{
return $this->Render($aArgs);
}
public function GetAttDef($aClasses = array())
{
return null;
}
/**
* Recursively browse the expression tree
* @param Closure $callback
@@ -46,7 +72,7 @@ abstract class Expression
abstract public function Browse(Closure $callback);
abstract public function ApplyParameters($aArgs);
// recursively builds an array of class => fieldname
abstract public function ListRequiredFields();
@@ -54,10 +80,10 @@ abstract class Expression
abstract public function CollectUsedParents(&$aTable);
abstract public function IsTrue();
// recursively builds an array of [classAlias][fieldName] => value
abstract public function ListConstantFields();
public function RequiresField($sClass, $sFieldName)
{
// #@# todo - optimize : this is called quite often when building a single query !
@@ -71,6 +97,12 @@ abstract class Expression
return base64_encode($this->Render());
}
/**
* @param $sValue
*
* @return Expression
* @throws OQLException
*/
static public function unserialize($sValue)
{
return self::FromOQL(base64_decode($sValue));
@@ -119,22 +151,57 @@ abstract class Expression
{
return new BinaryExpression($this, 'OR', $oExpr);
}
abstract public function RenameParam($sOldName, $sNewName);
abstract public function RenameAlias($sOldName, $sNewName);
/**
* Make the most relevant label, given the value of the expression
*
* @param DBSearch oFilter The context in which this expression has been used
* @param string sValue The value returned by the query, for this expression
* @param string sDefault The default value if no relevant label could be computed
*
* @param DBSearch oFilter The context in which this expression has been used
* @param string sValue The value returned by the query, for this expression
* @param string sDefault The default value if no relevant label could be computed
*
* @return The label
*/
*/
public function MakeValueLabel($oFilter, $sValue, $sDefault)
{
return $sDefault;
}
public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null)
{
return array(
'widget' => AttributeDefinition::SEARCH_WIDGET_TYPE_RAW,
'oql' => $this->Render($aArgs, $bRetrofitParams),
'label' => $this->Display($oSearch, $aArgs, $oAttDef),
'source' => get_class($this),
);
}
/**
* Split binary expression on given operator
*
* @param Expression $oExpr
* @param string $sOperator
* @param array $aAndExpr
*
* @return array of expressions
*/
public static function Split($oExpr, $sOperator = 'AND', &$aAndExpr = array())
{
if (($oExpr instanceof BinaryExpression) && ($oExpr->GetOperator() == $sOperator))
{
static::Split($oExpr->GetLeftExpr(), $sOperator, $aAndExpr);
static::Split($oExpr->GetRightExpr(), $sOperator, $aAndExpr);
}
else
{
$aAndExpr[] = $oExpr;
}
return $aAndExpr;
}
}
class SQLExpression extends Expression
@@ -165,7 +232,7 @@ class SQLExpression extends Expression
public function ApplyParameters($aArgs)
{
}
public function GetUnresolvedFields($sAlias, &$aUnresolved)
{
}
@@ -183,12 +250,12 @@ class SQLExpression extends Expression
public function CollectUsedParents(&$aTable)
{
}
public function ListConstantFields()
{
return array();
}
public function RenameParam($sOldName, $sNewName)
{
// Do nothing, since there is nothing to rename
@@ -244,7 +311,7 @@ class BinaryExpression extends Expression
}
return false;
}
public function GetLeftExpr()
{
return $this->m_oLeftExpr;
@@ -295,7 +362,7 @@ class BinaryExpression extends Expression
$this->m_oRightExpr->ApplyParameters($aArgs);
}
}
public function GetUnresolvedFields($sAlias, &$aUnresolved)
{
$this->GetLeftExpr()->GetUnresolvedFields($sAlias, $aUnresolved);
@@ -321,7 +388,15 @@ class BinaryExpression extends Expression
$this->GetLeftExpr()->CollectUsedParents($aTable);
$this->GetRightExpr()->CollectUsedParents($aTable);
}
public function GetAttDef($aClasses = array())
{
$oAttDef = $this->GetLeftExpr()->GetAttDef($aClasses);
if (!is_null($oAttDef)) return $oAttDef;
return $this->GetRightExpr()->GetAttDef($aClasses);
}
/**
* List all constant expression of the form <field> = <scalar> or <field> = :<variable>
* Could be extended to support <field> = <function><constant_expression>
@@ -355,7 +430,7 @@ class BinaryExpression extends Expression
}
return $aResult;
}
public function RenameParam($sOldName, $sNewName)
{
$this->GetLeftExpr()->RenameParam($sOldName, $sNewName);
@@ -367,6 +442,131 @@ class BinaryExpression extends Expression
$this->GetLeftExpr()->RenameAlias($sOldName, $sNewName);
$this->GetRightExpr()->RenameAlias($sOldName, $sNewName);
}
// recursive rendering
public function Display($oSearch, &$aArgs = null, $oAttDef = null)
{
$bReverseOperator = false;
$oLeftExpr = $this->GetLeftExpr();
if ($oLeftExpr instanceof FieldExpression)
{
$oAttDef = $oLeftExpr->GetAttDef($oSearch->GetJoinedClasses());
}
$oRightExpr = $this->GetRightExpr();
if ($oRightExpr instanceof FieldExpression)
{
$oAttDef = $oRightExpr->GetAttDef($oSearch->GetJoinedClasses());
$bReverseOperator = true;
}
$sLeft = $oLeftExpr->Display($oSearch, $aArgs, $oAttDef);
$sRight = $oRightExpr->Display($oSearch, $aArgs, $oAttDef);
if ($bReverseOperator)
{
// switch left and right expressions so reverse the operator
// Note that the operation is the same so < becomes > and not >=
switch ($this->GetOperator())
{
case '>':
$sOperator = '<';
break;
case '<':
$sOperator = '>';
break;
case '>=':
$sOperator = '<=';
break;
case '<=':
$sOperator = '>=';
break;
default:
$sOperator = $this->GetOperator();
break;
}
$sOperator = $this->OperatorToNaturalLanguage($sOperator, $oAttDef);
return "({$sRight}{$sOperator}{$sLeft})";
}
$sOperator = $this->GetOperator();
$sOperator = $this->OperatorToNaturalLanguage($sOperator, $oAttDef);
return "({$sLeft}{$sOperator}{$sRight})";
}
private function OperatorToNaturalLanguage($sOperator, $oAttDef)
{
if ($oAttDef instanceof AttributeDateTime)
{
return Dict::S('Expression:Operator:Date:'.$sOperator, " $sOperator ");
}
return Dict::S('Expression:Operator:'.$sOperator, " $sOperator ");
}
public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null)
{
$bReverseOperator = false;
$oLeftExpr = $this->GetLeftExpr();
$oRightExpr = $this->GetRightExpr();
$oAttDef = $oLeftExpr->GetAttDef($oSearch->GetJoinedClasses());
if (is_null($oAttDef))
{
$oAttDef = $oRightExpr->GetAttDef($oSearch->GetJoinedClasses());
$bReverseOperator = true;
}
if (is_null($oAttDef))
{
return parent::GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef);
}
$aCriteriaLeft = $oLeftExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef);
$aCriteriaRight = $oRightExpr->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef);
if ($bReverseOperator)
{
$aCriteria = array_merge($aCriteriaRight, $aCriteriaLeft);
// switch left and right expressions so reverse the operator
// Note that the operation is the same so < becomes > and not >=
switch ($this->GetOperator())
{
case '>':
$aCriteria['operator'] = '<';
break;
case '<':
$aCriteria['operator'] = '>';
break;
case '>=':
$aCriteria['operator'] = '<=';
break;
case '<=':
$aCriteria['operator'] = '>=';
break;
default:
$aCriteria['operator'] = $this->GetOperator();
break;
}
}
else
{
$aCriteria = array_merge($aCriteriaLeft, $aCriteriaRight);
$aCriteria['operator'] = $this->GetOperator();
}
$aCriteria['oql'] = $this->Render($aArgs, $bRetrofitParams);
$aCriteria['label'] = $this->Display($oSearch, $aArgs, $oAttDef);
if (isset($aCriteriaLeft['ref']) && isset($aCriteriaRight['ref']) && ($aCriteriaLeft['ref'] != $aCriteriaRight['ref']))
{
// Only one Field is supported in the expressions
$aCriteria['widget'] = AttributeDefinition::SEARCH_WIDGET_TYPE_RAW;
}
return $aCriteria;
}
}
@@ -404,7 +604,7 @@ class UnaryExpression extends Expression
public function ApplyParameters($aArgs)
{
}
public function GetUnresolvedFields($sAlias, &$aUnresolved)
{
}
@@ -427,7 +627,7 @@ class UnaryExpression extends Expression
{
return array();
}
public function RenameParam($sOldName, $sNewName)
{
// Do nothing
@@ -451,6 +651,46 @@ class ScalarExpression extends UnaryExpression
parent::__construct($value);
}
/**
* @param array $oSearch
* @param array $aArgs
* @param AttributeDefinition $oAttDef
*
* @return array|string
* @throws \Exception
*/
public function Display($oSearch, &$aArgs = null, $oAttDef = null)
{
if (!is_null($oAttDef))
{
if ($oAttDef->IsExternalKey())
{
try
{
/** @var AttributeExternalKey $oAttDef */
$sTarget = $oAttDef->GetTargetClass();
$oObj = MetaModel::GetObject($sTarget, $this->m_value);
return $oObj->Get("friendlyname");
} catch (CoreException $e)
{
}
}
if (!($oAttDef instanceof AttributeDateTime))
{
return $oAttDef->GetAsPlainText($this->m_value);
}
}
if (strpos($this->m_value, '%') === 0)
{
return '';
}
return $this->Render($aArgs);
}
// recursive rendering
public function Render(&$aArgs = null, $bRetrofitParams = false)
{
@@ -469,6 +709,54 @@ class ScalarExpression extends UnaryExpression
{
return clone $this;
}
public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null)
{
$aCriteria = array();
switch ((string)($this->m_value))
{
case '%Y-%m-%d':
$aCriteria['date_type'] = 'd';
break;
case '%Y-%m':
$aCriteria['date_type'] = 'm';
break;
case '%w':
$aCriteria['date_type'] = 'w';
break;
default:
$aValue = array('value' => $this->GetValue());
if (!is_null($oAttDef))
{
switch (true)
{
case $oAttDef->IsExternalKey():
try
{
/** @var AttributeExternalKey $oAttDef */
$sTarget = $oAttDef->GetTargetClass();
$oObj = MetaModel::GetObject($sTarget, $this->GetValue());
$aValue['label'] = $oObj->Get("friendlyname");
}
catch (Exception $e)
{
IssueLog::Error($e->getMessage());
}
break;
default:
$aValue['label'] = $oAttDef->GetAsPlainText($this->GetValue());
break;
}
}
$aCriteria['values'] = array($aValue);
break;
}
return $aCriteria;
}
}
class TrueExpression extends ScalarExpression
@@ -525,6 +813,44 @@ class FieldExpression extends UnaryExpression
$this->m_value = $sParent.'.'.$this->m_sName;
}
private function GetClassName($aClasses = array())
{
if (isset($aClasses[$this->m_sParent]))
{
return $aClasses[$this->m_sParent];
}
else
{
return $this->m_sParent;
}
}
/**
* @param DBObjectSearch $oSearch
* @param array $aArgs
* @param AttributeDefinition $oAttDef
*
* @return array|string
* @throws \CoreException
* @throws \DictExceptionMissingString
* @throws \Exception
*/
public function Display($oSearch, &$aArgs = null, $oAttDef = null)
{
if (empty($this->m_sParent))
{
return "`{$this->m_sName}`";
}
$sClass = $this->GetClassName($oSearch->GetJoinedClasses());
$sAttName = MetaModel::GetLabel($sClass, $this->m_sName);
if ($sClass != $oSearch->GetClass())
{
$sAttName = MetaModel::GetName($sClass).':'.$sAttName;
}
return $sAttName;
}
// recursive rendering
public function Render(&$aArgs = null, $bRetrofitParams = false)
{
@@ -535,6 +861,37 @@ class FieldExpression extends UnaryExpression
return "`{$this->m_sParent}`.`{$this->m_sName}`";
}
public function GetAttDef($aClasses = array())
{
if (!empty($this->m_sParent))
{
$sClass = $this->GetClassName($aClasses);
$aAttDefs = MetaModel::ListAttributeDefs($sClass);
if (isset($aAttDefs[$this->m_sName]))
{
return $aAttDefs[$this->m_sName];
}
else
{
if ($this->m_sName == 'id')
{
$aParams = array(
'default_value' => 0,
'is_null_allowed' => false,
'allowed_values' => null,
'depends_on' => null,
'sql' => 'id',
);
return new AttributeInteger($this->m_sName, $aParams);
}
}
}
return null;
}
public function ListRequiredFields()
{
return array($this->m_sParent.'.'.$this->m_sName);
@@ -565,8 +922,9 @@ class FieldExpression extends UnaryExpression
if (!array_key_exists($this->m_sParent, $aTranslationData))
{
if ($bMatchAll) throw new CoreException('Unknown parent id in translation table', array('parent_id' => $this->m_sParent, 'translation_table' => array_keys($aTranslationData)));
return clone $this;
}
}
if (!array_key_exists($this->m_sName, $aTranslationData[$this->m_sParent]))
{
if (!array_key_exists('*', $aTranslationData[$this->m_sParent]))
@@ -595,12 +953,13 @@ class FieldExpression extends UnaryExpression
/**
* Make the most relevant label, given the value of the expression
*
* @param DBSearch oFilter The context in which this expression has been used
* @param string sValue The value returned by the query, for this expression
* @param string sDefault The default value if no relevant label could be computed
*
* @param DBSearch oFilter The context in which this expression has been used
* @param string sValue The value returned by the query, for this expression
* @param string sDefault The default value if no relevant label could be computed
*
* @return The label
*/
*/
public function MakeValueLabel($oFilter, $sValue, $sDefault)
{
$sAttCode = $this->GetName();
@@ -646,6 +1005,47 @@ class FieldExpression extends UnaryExpression
$this->m_sParent = $sNewName;
}
}
/**
* @param $oSearch
* @param null $aArgs
* @param bool $bRetrofitParams
* @param AttributeDefinition $oAttDef
*
* @return array
*/
public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null)
{
$oAttDef = $this->GetAttDef($oSearch->GetJoinedClasses());
if (!is_null($oAttDef))
{
$sSearchType = $oAttDef->GetSearchType();
try
{
if ($sSearchType == AttributeDefinition::SEARCH_WIDGET_TYPE_EXTERNAL_KEY)
{
// TODO Check the type of external key ? (EXTKEY_ABSOLUTE or EXTKEY_RELATIVE)
if (MetaModel::IsHierarchicalClass($oAttDef->GetTargetClass()))
{
$sSearchType = AttributeDefinition::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY;
}
}
}
catch (CoreException $e)
{
}
}
else
{
$sSearchType = AttributeDefinition::SEARCH_WIDGET_TYPE;
}
return array(
'widget' => $sSearchType,
'ref' => $this->GetParent().'.'.$this->GetName(),
'class_alias' => $this->GetParent(),
);
}
}
// Has been resolved into an SQL expression
@@ -678,7 +1078,68 @@ class VariableExpression extends UnaryExpression
return false;
}
public function GetName() {return $this->m_sName;}
public function GetName()
{
return $this->m_sName;
}
public function Display($oSearch, &$aArgs = null, $oAttDef = null)
{
$sValue = $this->m_value;
if (!is_null($aArgs) && (array_key_exists($this->m_sName, $aArgs)))
{
$sValue = $aArgs[$this->m_sName];
}
elseif (($iPos = strpos($this->m_sName, '->')) !== false)
{
$sParamName = substr($this->m_sName, 0, $iPos);
$oObj = null;
$sAttCode = 'id';
if (array_key_exists($sParamName.'->object()', $aArgs))
{
$sAttCode = substr($this->m_sName, $iPos + 2);
$oObj = $aArgs[$sParamName.'->object()'];
}
elseif (array_key_exists($sParamName, $aArgs))
{
$sAttCode = substr($this->m_sName, $iPos + 2);
$oObj = $aArgs[$sParamName];
}
if (!is_null($oObj))
{
if ($sAttCode == 'id')
{
$sValue = $oObj->Get("friendlyname");
}
else
{
$sValue = $oObj->Get($sAttCode);
}
return $sValue;
}
}
if (!is_null($oAttDef))
{
if ($oAttDef->IsExternalKey())
{
try
{
/** @var AttributeExternalKey $oAttDef */
$sTarget = $oAttDef->GetTargetClass();
$oObj = MetaModel::GetObject($sTarget, $sValue);
return $oObj->Get("friendlyname");
} catch (CoreException $e)
{
}
}
return $oAttDef->GetAsPlainText($sValue);
}
return $this->Render($aArgs);
}
// recursive rendering
public function Render(&$aArgs = null, $bRetrofitParams = false)
@@ -729,7 +1190,7 @@ class VariableExpression extends UnaryExpression
$this->m_sName = $sNewName;
}
}
public function GetAsScalar($aArgs)
{
$oRet = null;
@@ -833,7 +1294,7 @@ class ListExpression extends Expression
}
}
}
public function GetUnresolvedFields($sAlias, &$aUnresolved)
{
foreach ($this->m_aExpressions as $oExpr)
@@ -861,7 +1322,7 @@ class ListExpression extends Expression
}
return $aRes;
}
public function CollectUsedParents(&$aTable)
{
foreach ($this->m_aExpressions as $oExpr)
@@ -879,7 +1340,7 @@ class ListExpression extends Expression
}
return $aRes;
}
public function RenameParam($sOldName, $sNewName)
{
$aRes = array();
@@ -887,7 +1348,7 @@ class ListExpression extends Expression
{
$this->m_aExpressions[$key] = $oExpr->RenameParam($sOldName, $sNewName);
}
}
}
public function RenameAlias($sOldName, $sNewName)
{
@@ -896,7 +1357,34 @@ class ListExpression extends Expression
{
$oExpr->RenameAlias($sOldName, $sNewName);
}
}
}
public function GetAttDef($aClasses = array())
{
foreach($this->m_aExpressions as $oExpression)
{
$oAttDef = $oExpression->GetAttDef($aClasses);
if (!is_null($oAttDef)) return $oAttDef;
}
return null;
}
public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null)
{
$aValues = array();
foreach($this->m_aExpressions as $oExpression)
{
$aCrit = $oExpression->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef);
if (array_key_exists('values', $aCrit))
{
$aValues = array_merge($aValues, $aCrit['values']);
}
}
return array('values' => $aValues);
}
}
@@ -990,7 +1478,7 @@ class FunctionExpression extends Expression
}
return $aRes;
}
public function CollectUsedParents(&$aTable)
{
foreach ($this->m_aArgs as $oExpr)
@@ -1008,7 +1496,7 @@ class FunctionExpression extends Expression
}
return $aRes;
}
public function RenameParam($sOldName, $sNewName)
{
foreach ($this->m_aArgs as $key => $oExpr)
@@ -1025,14 +1513,26 @@ class FunctionExpression extends Expression
}
}
public function GetAttDef($aClasses = array())
{
foreach($this->m_aArgs as $oExpression)
{
$oAttDef = $oExpression->GetAttDef($aClasses);
if (!is_null($oAttDef)) return $oAttDef;
}
return null;
}
/**
* Make the most relevant label, given the value of the expression
*
* @param DBSearch oFilter The context in which this expression has been used
* @param string sValue The value returned by the query, for this expression
* @param string sDefault The default value if no relevant label could be computed
*
* @param DBSearch oFilter The context in which this expression has been used
* @param string sValue The value returned by the query, for this expression
* @param string sDefault The default value if no relevant label could be computed
*
* @return The label
*/
*/
public function MakeValueLabel($oFilter, $sValue, $sDefault)
{
static $aWeekDayToString = null;
@@ -1095,6 +1595,87 @@ class FunctionExpression extends Expression
}
return $sRes;
}
public function Display($oSearch, &$aArgs = null, $oAttDef = null)
{
$sOperation = '';
$sVerb = '';
switch ($this->m_sVerb)
{
case 'NOW':
$sVerb = $this->VerbToNaturalLanguage();
break;
case 'DATE_SUB':
$sVerb = ' -';
break;
case 'DATE_ADD':
$sVerb = ' +';
break;
case 'DATE_FORMAT':
break;
default:
return $this->Render($aArgs);
}
foreach($this->m_aArgs as $oExpression)
{
if ($oExpression instanceof IntervalExpression)
{
$sOperation .= $sVerb;
$sVerb = '';
}
$sOperation .= $oExpression->Display($oSearch, $aArgs, $oAttDef);
}
if (!empty($sVerb))
{
$sOperation .= $sVerb;
}
return $sOperation;
}
private function VerbToNaturalLanguage()
{
return Dict::S('Expression:Verb:'.$this->m_sVerb, " {$this->m_sVerb} ");
}
public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null)
{
$aCriteria = array();
switch ($this->m_sVerb)
{
case 'ISNULL':
$aCriteria['operator'] = $this->m_sVerb;
foreach($this->m_aArgs as $oExpression)
{
$aCriteria = array_merge($oExpression->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef), $aCriteria);
}
$aCriteria['has_undefined'] = true;
break;
case 'NOW':
$aCriteria = array('widget' => 'date_time');
$aCriteria['is_relative'] = true;
$aCriteria['verb'] = $this->m_sVerb;
break;
case 'DATE_ADD':
case 'DATE_SUB':
case 'DATE_FORMAT':
$aCriteria = array('widget' => 'date_time');
foreach($this->m_aArgs as $oExpression)
{
$aCriteria = array_merge($oExpression->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef), $aCriteria);
}
$aCriteria['verb'] = $this->m_sVerb;
break;
default:
return parent::GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef);
}
return $aCriteria;
}
}
class IntervalExpression extends Expression
@@ -1147,7 +1728,7 @@ class IntervalExpression extends Expression
$this->m_oValue->ApplyParameters($aArgs);
}
}
public function GetUnresolvedFields($sAlias, &$aUnresolved)
{
$this->m_oValue->GetUnresolvedFields($sAlias, $aUnresolved);
@@ -1171,16 +1752,29 @@ class IntervalExpression extends Expression
{
return array();
}
public function RenameParam($sOldName, $sNewName)
{
$this->m_oValue->RenameParam($sOldName, $sNewName);
}
}
public function RenameAlias($sOldName, $sNewName)
{
$this->m_oValue->RenameAlias($sOldName, $sNewName);
}
}
public function GetCriterion($oSearch, &$aArgs = null, $bRetrofitParams = false, $oAttDef = null)
{
$aCriteria = $this->m_oValue->GetCriterion($oSearch, $aArgs, $bRetrofitParams, $oAttDef);
$aCriteria['unit'] = $this->m_sUnit;
return $aCriteria;
}
public function Display($oSearch, &$aArgs = null, $oAttDef = null)
{
return $this->m_oValue->Render($aArgs).' '.Dict::S('Expression:Unit:Long:'.$this->m_sUnit, $this->m_sUnit);
}
}
class CharConcatExpression extends Expression
@@ -1210,7 +1804,7 @@ class CharConcatExpression extends Expression
foreach ($this->m_aExpressions as $oExpr)
{
$sCol = $oExpr->Render($aArgs, $bRetrofitParams);
// Concat will be globally NULL if one single argument is null !
// Concat will be globally NULL if one single argument is null !
$aRes[] = "COALESCE($sCol, '')";
}
return "CAST(CONCAT(".implode(', ', $aRes).") AS CHAR)";
@@ -1293,7 +1887,7 @@ class CharConcatExpression extends Expression
{
$this->m_aExpressions[$key] = $oExpr->RenameParam($sOldName, $sNewName);
}
}
}
public function RenameAlias($sOldName, $sNewName)
{
@@ -1301,7 +1895,7 @@ class CharConcatExpression extends Expression
{
$oExpr->RenameAlias($sOldName, $sNewName);
}
}
}
}
@@ -1322,7 +1916,7 @@ class CharConcatWSExpression extends CharConcatExpression
foreach ($this->m_aExpressions as $oExpr)
{
$sCol = $oExpr->Render($aArgs, $bRetrofitParams);
// Concat will be globally NULL if one single argument is null !
// Concat will be globally NULL if one single argument is null !
$aRes[] = "COALESCE($sCol, '')";
}
$sSep = CMDBSource::Quote($this->m_separator);
@@ -1502,6 +2096,7 @@ class QueryBuilderExpressions
{
$this->m_aJoinFields[$index] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
}
foreach($this->m_aClassIds as $sClass => $oExpression)
{
$this->m_aClassIds[$sClass] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);

View File

@@ -1,8 +1,49 @@
$highlight-color: #E87C1E;
// Base colors
$gray-base: #000 !default;
$gray-darker: lighten($gray-base, 13.5%) !default; // #222
$gray-dark: #444 !default;
$gray: #777 !default;
$gray-light: #808080 !default;
$gray-lighter: #ddd !default;
$gray-extra-light: #F1F1F1 !default;
$white: #FFFFFF !default;
$combodo-orange: #EA7D1E !default;
$combodo-dark-gray: #585653 !default;
$combodo-orange-dark: darken($combodo-orange, 13.8%) !default;
$combodo-orange-darker: darken($combodo-orange, 18%) !default;
$combodo-dark-gray-dark: darken($combodo-dark-gray, 13.5%) !default;
$combodo-dark-gray-darker: darken($combodo-dark-gray, 18%) !default;
// Vars
$highlight-color: $combodo-orange;
$grey-color: #555555;
$complement-color: #1c94c4;
$complement-light: #d6e8ef;
$frame-background-color: #F1F1F1;
$frame-background-color: $gray-extra-light;
$text-color: #000;
$box-radius: 0px;
$box-shadow-regular: 0 1px 1px rgba(0, 0, 0, 0.15);
// - Boxes
//$search-criteria-box-color: #2D2D2D;
//$search-criteria-box-bg-color: #f0f3f5;
//$search-criteria-box-border-color: #3f7294;
//$search-criteria-box-border: 1px solid $search-criteria-box-border-color;
//$search-criteria-box-radius: 1px;
//
$search-criteria-box-color: #2D2D2D;
$search-criteria-box-picto-color: #E87C1E;
$search-criteria-box-bg-color: #EEEEEE;
$search-criteria-box-hover-color: $white;
$search-criteria-box-border-color: #CCCCCC;
$search-criteria-box-border: 1px solid $search-criteria-box-border-color;
$search-criteria-box-radius: 1px;
//
$search-add-criteria-box-color: $search-criteria-box-color;
$search-add-criteria-box-bg-color: $white;
$search-add-criteria-box-hover-color: $gray-extra-light;
// Beware the version number MUST be enclosed with quotes otherwise v2.3.0 becomes v2 0.3 .0
$version: "v2.4.0";
$version: "v2.4.0";

Binary file not shown.

View File

@@ -21,7 +21,7 @@ OS2Version: 0
OS2_WeightWidthSlopeOnly: 0
OS2_UseTypoMetrics: 1
CreationTime: 1463745065
ModificationTime: 1506001058
ModificationTime: 1522325525
OS2TypoAscent: 0
OS2TypoAOffset: 1
OS2TypoDescent: 0
@@ -35,6 +35,7 @@ HheadAscent: 0
HheadAOffset: 1
HheadDescent: 0
HheadDOffset: 1
OS2Vendor: 'PfEd'
MarkAttachClasses: 1
DEI: 91125
Encoding: ISO8859-1
@@ -46,7 +47,7 @@ FitToEm: 0
WinInfo: 0 31 10
BeginPrivate: 0
EndPrivate
BeginChars: 256 11
BeginChars: 256 13
StartChar: zero
Encoding: 48 48 0
@@ -940,7 +941,135 @@ SplineSet
815.574 32.7998 814.774 33.5996 813.175 33.5996 c 2
810.774 33.5996 l 1
EndSplineSet
Validated: 524321
Validated: 33
EndChar
StartChar: B
Encoding: 66 66 11
Width: 1024
VWidth: 0
HStem: 0 59<210.859 353.111 670.997 813.141> 103 28<483.475 540.525> 326 280<245 398 626 779>
VStem: 87 62<121.402 265.716> 415 60<134.336 199.804> 549 60<134.336 199.804> 875 62<121.402 265.716>
LayerCount: 3
Fore
SplineSet
376 606 m 0
420 606 456 571 456 527 c 0
456 514 l 1
458 444 l 1
566 444 l 1
568 514 l 1
568 514 568 524 568 527 c 0
568 571 604 606 648 606 c 0
677 606 704 590 718 564 c 1
719 564 l 1
754 502 l 1
781 491 803 472 817 446 c 1
818 446 l 1
914 283 l 1
929 255 937 224 937 192 c 0
937 87 852 0 747 0 c 0
666 0 594 54 568 131 c 1
555 114 534 103 512 103 c 0
490 103 469 114 456 132 c 1
431 55 358 0 277 0 c 0
172 0 87 87 87 192 c 0
87 224 95 255 110 283 c 1
206 446 l 1
207 446 l 1
221 472 243 491 270 502 c 1
306 564 l 1
320 590 347 606 376 606 c 0
282 326 m 0
208 326 149 266 149 193 c 0
149 119 208 59 282 59 c 0
355 59 415 119 415 193 c 0
415 266 355 326 282 326 c 0
742 326 m 0
669 326 609 266 609 193 c 0
609 119 669 59 742 59 c 0
816 59 875 119 875 193 c 0
875 266 816 326 742 326 c 0
512 204 m 0
492 204 475 188 475 168 c 0
475 148 492 131 512 131 c 0
532 131 549 148 549 168 c 0
549 188 532 204 512 204 c 0
EndSplineSet
EndChar
StartChar: b
Encoding: 98 98 12
Width: 1024
VWidth: 0
Flags: H
HStem: -1 35<183.649 347.721 676.937 838.822> 198 25<478.803 548.794> 294 234<470.756 555.897>
VStem: 75 44<83.375 145> 414 42<83.6665 369.146> 432 24<255 473> 569 42<83.6665 369.146> 569 24<255 473> 906 43<84.3258 145>
LayerCount: 3
Fore
SplineSet
352 616 m 0xf080
390 616 423 593 420 563 c 1
422 488 l 1
452 468 454 441 454 441 c 1
455 305 456 159 456 12 c 1
453 -62 369 -121 266 -121 c 0
161 -121 74 -59 74 17 c 0
74 20 76 22 76 25 c 1
74 25 l 1
106 196 l 1
106 196 l 1
113 235 143 270 188 292 c 1
186 292 l 1
206 408 l 1
206 408 228 457 274 487 c 1
282 570 l 1
284 570 l 1
287 596 316 616 352 616 c 0xf080
266 114 m 0
185 114 118 69 118 14 c 0
118 -41 185 -86 266 -86 c 0
347 -86 414 -41 414 14 c 0
414 69 347 114 266 114 c 0
514 408 m 0
554 408 589 384 592 353 c 1
592 353 l 1
592 135 l 1
592 135 l 1
590 103 555 78 514 78 c 0
473 78 436 103 434 135 c 1
432 135 l 1
432 353 l 1
434 353 l 1
437 384 474 408 514 408 c 0
466 138 m 0
466 118 489 103 514 103 c 2
539 103 560 118 560 138 c 0
560 158 539 174 514 174 c 0
489 174 466 158 466 138 c 0
672 616 m 0
708 616 737 596 740 570 c 1
742 570 l 1
750 487 l 1
796 457 818 408 818 408 c 1
838 292 l 1
836 292 l 1
881 270 911 235 918 196 c 1
950 25 l 1
948 25 l 1
948 22 948 20 948 17 c 0
948 -59 863 -121 758 -121 c 0
655 -121 571 -62 568 12 c 1
568 159 569 305 570 441 c 1
570 441 572 468 602 488 c 1
604 563 l 1
601 593 634 616 672 616 c 0
758 114 m 0
677 114 610 69 610 14 c 0
610 -41 677 -86 758 -86 c 0
839 -86 906 -41 906 14 c 0
906 69 839 114 758 114 c 0
EndSplineSet
EndChar
EndChars
EndSplineFont

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: 'CombodoRegular';
src: url('combodo-webfont.woff2?v=2.0') format('woff2'),
url('combodo-webfont.woff?v=2.0') format('woff'),
url('combodo-webfont.ttf?v=2.0') format('truetype');
src: url('combodo-webfont.woff2?v=2.1') format('woff2'),
url('combodo-webfont.woff?v=2.1') format('woff'),
url('combodo-webfont.ttf?v=2.1') format('truetype');
font-weight: normal;
font-style: normal;
@@ -187,6 +187,12 @@
.fc-closed-request:before {
content: "3";
}
.fc-binoculars:before {
content: "B";
}
.fc-binoculars-alt:before {
content: "b";
}
.fc-combodo-icon-o:before {
content: "C";
}

35
css/font-combodo/glyphs/B.svg Executable file
View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8"
version="1.1"
viewBox="0 0 264.58333 264.58333"
height="1000"
width="1000">
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer2">
<path
id="path4516"
transform="scale(0.26458333)"
d="m 363.57227,214.44727 c -28.72645,0.0164 -55.09978,15.88172 -68.57422,41.25195 l -0.53907,-0.18164 -34.7832,60.49414 c -26.17161,10.74972 -47.91781,30.06712 -61.67773,54.78906 l -0.51367,-0.125 -93.94336,159.60352 0.33984,0.0918 c -14.914544,27.26255 -22.732489,57.83851 -22.734376,88.91407 -3.76e-4,102.32848 82.952766,185.2825 185.281246,185.2832 79.57685,-0.18509 150.14836,-51.16271 175.33008,-126.65039 12.75761,17.44216 33.06009,27.76494 54.66992,27.79687 21.38684,-0.0322 41.50695,-10.14508 54.29297,-27.28906 25.33697,75.22798 95.78021,125.95807 175.16016,126.14258 102.32848,-7e-4 185.28163,-82.95472 185.28125,-185.2832 -0.002,-31.07556 -7.81983,-61.65152 -22.73438,-88.91407 l 0.33985,-0.0918 -93.94336,-159.60352 -0.51367,0.125 c -13.75992,-24.72194 -35.50612,-44.03935 -61.67774,-54.78906 l -34.7832,-60.49414 -0.53906,0.18164 c -13.47444,-25.37023 -39.84777,-41.23555 -68.57422,-41.25195 -42.90975,4.5e-4 -77.69486,34.78556 -77.69531,77.69531 -0.7247,3.23852 -0.16993,12.87304 -0.16993,12.87304 l -2.25,67.8418 H 443.6875 l -2.25,-67.8418 c 0,0 -0.20029,-9.98923 -0.16992,-12.87304 -4.5e-4,-42.90975 -34.78556,-77.69486 -77.69531,-77.69531 z m -92.14454,274.125 c 71.79702,0 130,58.20298 130,130 0,71.79702 -58.20298,130 -130,130 -71.79702,0 -130,-58.20298 -130,-130 0,-71.79702 58.20298,-130 130,-130 z m 449.45313,0 c 71.79702,0 130,58.20298 130,130 0,71.79702 -58.20298,130 -130,130 -71.79702,0 -130,-58.20298 -130,-130 0,-71.79702 58.20298,-130 130,-130 z M 496.1543,607.14258 c 19.72489,-3.1e-4 35.71515,15.98995 35.71484,35.71484 3.1e-4,19.72489 -15.98995,35.71516 -35.71484,35.71485 -19.7249,3.1e-4 -35.71516,-15.98995 -35.71485,-35.71485 -3.1e-4,-19.72489 15.98996,-35.71515 35.71485,-35.71484 z"
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:3.77952766;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,280 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
id="svg8"
version="1.1"
viewBox="0 0 264.58333 264.58334"
height="1000"
width="1000">
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
style="display:none"
transform="translate(0,-32.41665)"
id="layer1">
<image
width="262.41626"
height="211.46167"
preserveAspectRatio="none"
style="image-rendering:optimizeQuality"
xlink:href="data:image/png;base64,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
AElFTkSuQmCC
"
id="image823"
x="2.9734159"
y="52.273003" />
</g>
<g
id="layer2">
<path
id="path827-9"
d="m 97.624888,39.796021 c -8.178314,0.006 -14.961537,4.568212 -15.54272,10.453627 l -0.226343,-0.0021 -1.86862,19.061368 c -10.48507,6.763065 -15.77113,18.088851 -15.77113,18.088851 l -4.276224,26.459893 0.258381,0.0661 C 50.006857,118.97418 43.274297,126.9138 41.68205,135.75964 l -0.183967,0.014 -7.216098,39.0214 0.316778,0.0109 c -0.07327,0.65029 -0.11842,1.30208 -0.135393,1.9544 -5e-6,17.33952 19.483317,31.39599 43.51724,31.39602 23.49434,-0.0182 42.72694,-13.48746 43.45006,-30.42967 0.0382,-33.56054 -0.12647,-67.01884 -0.28628,-98.079355 0,0 -0.58301,-6.16341 -7.34633,-10.666015 l -0.6258,-17.202566 c 0.0278,-0.242592 0.0447,-0.485771 0.0506,-0.729154 1.9e-4,-6.215145 -6.98334,-11.253585 -15.598012,-11.253579 z M 78.113934,154.49394 c 18.592694,-9e-5 33.665126,10.22596 33.665136,22.84046 0,12.61451 -15.072436,22.84056 -33.665136,22.84047 -18.592498,-1e-4 -33.664614,-10.2261 -33.664612,-22.84047 6e-6,-12.61437 15.07212,-22.84036 33.664612,-22.84046 z"
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.36251795;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
id="path898"
d="m 134.63546,87.230775 a 17.907058,13.764381 0 0 0 -17.83715,12.695354 h -0.87127 v 49.712211 h 0.83251 a 17.907058,13.76438 0 0 0 17.87591,13.09635 17.907058,13.76438 0 0 0 17.86403,-13.09635 h 0.31057 V 99.926129 h -0.32711 A 17.907058,13.764381 0 0 0 134.63546,87.230775 Z m 0,53.624635 a 10.557147,8.1148223 0 0 1 10.55749,8.11475 10.557147,8.1148223 0 0 1 -10.55749,8.11475 10.557147,8.1148223 0 0 1 -10.55698,-8.11475 10.557147,8.1148223 0 0 1 10.55698,-8.11475 z"
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00007248;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
id="path827-9-6"
d="m 171.1122,39.796021 c 8.17832,0.006 14.96154,4.568212 15.54273,10.453627 l 0.22634,-0.0021 1.86862,19.061369 c 10.48507,6.763074 15.77113,18.08886 15.77113,18.08886 l 4.27623,26.459883 -0.25839,0.0661 c 10.19138,5.05042 16.92394,12.99004 18.51619,21.83588 l 0.18396,0.014 7.2161,39.0214 -0.31678,0.0109 c 0.0733,0.65029 0.11842,1.30208 0.1354,1.9544 0,17.33952 -19.48332,31.39599 -43.51725,31.39602 -23.49434,-0.0182 -42.72693,-13.48746 -43.45005,-30.42967 -0.0382,-33.56054 0.12647,-67.01884 0.28628,-98.079352 0,0 0.58301,-6.163411 7.34633,-10.666016 l 0.6258,-17.202565 c -0.0278,-0.242592 -0.0447,-0.485771 -0.0506,-0.729154 -1.9e-4,-6.215148 6.98334,-11.253588 15.598,-11.253582 z m 19.51096,114.697919 c -18.59269,-9e-5 -33.66512,10.22596 -33.66513,22.84046 0,12.61451 15.07243,22.84056 33.66513,22.84047 18.5925,-1e-4 33.66462,-10.2261 33.66461,-22.84047 0,-12.61437 -15.07212,-22.84036 -33.66461,-22.84046 z"
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.36251807;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -58,7 +58,7 @@ label {
cursor: pointer;
}
.hilite, .hilite a, .hilite a:visited {
color: #e87c1e;
color: #ea7d1e;
text-decoration: none;
}
table.datatable {
@@ -114,7 +114,7 @@ table.listResults td .view-image img {
cursor: pointer;
margin-bottom: 3px;
padding: 2px;
background-color: #e87c1e;
background-color: #ea7d1e;
}
.edit-image .edit-buttons .button.disabled {
cursor: default;
@@ -275,7 +275,7 @@ legend.transparent {
}
.ui-widget-content td a:hover, p a:hover, td a:hover {
text-decoration: underline;
color: #e87c1e;
color: #ea7d1e;
}
.cke_reset_all *:hover {
text-decoration: none;
@@ -283,8 +283,8 @@ legend.transparent {
}
table.cke_dialog_contents a.cke_dialog_ui_button_ok {
color: #000;
border-color: #e87c1e;
background: #e87c1e;
border-color: #ea7d1e;
background: #ea7d1e;
}
.cke_notifications_area {
display: none;
@@ -309,7 +309,7 @@ td a.mailto, td a.mailto:visited {
}
td a.mailto:hover {
text-decoration: underline;
color: #e87c1e;
color: #ea7d1e;
padding-left: 20px;
background: url(../images/mail.png?v=v2.4.0) no-repeat left;
}
@@ -329,7 +329,7 @@ a.small_action {
padding-left: 5px;
padding-top: 2px;
padding-bottom: 2px;
background: #e87c1e url(../images/actions_left.png?v=v2.4.0) no-repeat left;
background: #ea7d1e url(../images/actions_left.png?v=v2.4.0) no-repeat left;
}
.actions_details span {
background: url(../images/actions_right.png?v=v2.4.0) no-repeat right;
@@ -424,7 +424,7 @@ div.ui-accordion-content {
text-decoration: none;
}
.ui-accordion-content a:hover {
color: #e87c1e;
color: #ea7d1e;
text-decoration: none;
}
.ui-accordion-content ul {
@@ -455,7 +455,7 @@ a.CollapsibleLabel.open, td a.CollapsibleLabel.open {
padding: 0px 0pt 0px 16px;
font-size: 8pt;
text-decoration: none;
color: #e87c1e;
color: #ea7d1e;
background: url(../images/mini-arrow-orange-open.gif) no-repeat left;
}
.page_header {
@@ -506,7 +506,7 @@ div.actions_menu > ul {
nowidth: 70px;
padding-left: 5px;
/* Nasty work-around for IE... en attendant mieux */
background: #e87c1e url(../images/actions_left.png?v=v2.4.0) no-repeat top left;
background: #ea7d1e url(../images/actions_left.png?v=v2.4.0) no-repeat top left;
cursor: pointer;
margin: 0;
}
@@ -577,7 +577,7 @@ div.actions_menu > ul > li {
position: absolute;
display: none;
border-top: 1px solid white;
z-index: 999;
z-index: 1500;
}
.itop_popup li ul li, #logOffBtn li ul li {
float: none;
@@ -588,7 +588,7 @@ div.actions_menu > ul > li {
text-align: left;
}
.itop_popup li ul li a:hover, #logOffBtn li ul li a:hover {
background: #e87c1e;
background: #ea7d1e;
color: #fff;
font-weight: bold;
}
@@ -675,60 +675,6 @@ input.dp-applied {
float: left;
}
/* For search forms */
.SearchDrawer {
border-top: 5px solid #1c94c4;
border-left: 5px solid #1c94c4;
border-right: 5px solid #1c94c4;
border-bottom: 0;
background: #d6e8ef;
color: #000;
padding: 10px;
margin: 0;
font-size: 12px;
}
.SearchDrawer label {
background: #d6e8ef;
color: #000;
text-align: right;
}
.SearchDrawer h1 {
color: #000;
}
.SearchDrawer .SearchAttribute > .field_input_zone {
display: inline-block;
}
.SearchDrawer .SearchAttribute > .field_input_zone > .field_select_wrapper {
display: inline-block;
}
.DrawerClosed {
display: none;
}
.DrawerHandle {
margin: 0;
padding: 5px;
background: url(../images/drawer-handle.gif) bottom no-repeat transparent;
color: #fff;
cursor: pointer;
text-align: center;
/* center the block */
width: 100px;
margin-left: auto;
margin-right: auto;
margin-top: 0;
margin-bottom: 0;
display: block;
font-size: 12px;
}
div.HRDrawer {
height: 5px;
width: 100%;
margin: 0;
background-color: #1c94c4;
margin: 0;
padding: 0;
border: 0;
display: block;
}
.mini_tabs a {
text-decoration: none;
font-weight: bold;
@@ -753,6 +699,506 @@ div.HRDrawer {
nopadding-right: 1em;
margin-top: 0;
}
/* Search forms v2 */
.search_box {
box-sizing: border-box;
position: relative;
z-index: 1100;
/* To be over the table block/unblock UI. Not very sure about this. */
/* Sizing reset */
}
.search_box * {
box-sizing: border-box;
}
.search_form_handler {
position: relative;
z-index: 10;
font-size: 12px;
border: 1px solid #3f7294;
/* Sizing reset */
/* Hyperlink reset */
/* Input reset */
/* List helpers */
}
.search_form_handler * {
box-sizing: border-box;
}
.search_form_handler a {
color: inherit;
text-decoration: none;
}
.search_form_handler input[type="text"], .search_form_handler select {
padding: 1px 2px;
}
.search_form_handler.opened .sf_title .sft_toggler {
transform: rotateX(180deg);
}
.search_form_handler.opened .sf_criterion_area {
/*display: inherit;*/
}
.search_form_handler .sf_title {
padding: 8px 10px;
margin: 0;
color: #fff;
background-color: #3f7294;
cursor: pointer;
/* Pictogram */
}
.search_form_handler .sf_title .sft_picto {
display: none;
/* TODO: Remove this class and the correspondig DOM element if this option is kept. */
margin-right: 10px;
}
.search_form_handler .sf_title .sft_refresh, .search_form_handler .sf_title .sft_toggler {
transition: color 0.2s ease-in-out, transform 0.4s ease-in-out;
}
.search_form_handler .sf_title .sft_refresh:hover, .search_form_handler .sf_title .sft_toggler:hover {
color: #f1f1f1;
}
.search_form_handler .sf_title .sft_refresh {
font-size: 10pt;
line-height: 13pt;
}
.search_form_handler .sf_title .sft_toggler {
margin-left: 0.7em;
}
.search_form_handler .sf_message {
display: none;
margin: 8px 8px 0px 8px;
border-radius: 0px;
}
.search_form_handler .sf_criterion_area {
/*display: none;*/
padding: 8px 8px 3px 8px;
/* padding-bottom must equals to padding-top - .search_form_criteria:margin-bottom */
background-color: #fff;
/* Common style between criterion and more criterion */
/* Criteria tags */
/* More criterion */
}
.search_form_handler .sf_criterion_area .search_form_criteria, .search_form_handler .sf_criterion_area .sf_more_criterion {
position: relative;
display: inline-block;
margin-bottom: 5px;
vertical-align: top;
}
.search_form_handler .sf_criterion_area .search_form_criteria.opened, .search_form_handler .sf_criterion_area .sf_more_criterion.opened {
margin-bottom: 0px;
/* To compensate the .sfc/.sfm_header:padding-bottom: 13px */
}
.search_form_handler .sf_criterion_area .search_form_criteria.opened .sfc_header, .search_form_handler .sf_criterion_area .sf_more_criterion.opened .sfc_header, .search_form_handler .sf_criterion_area .search_form_criteria.opened .sfm_header, .search_form_handler .sf_criterion_area .sf_more_criterion.opened .sfm_header {
border-bottom: none !important;
box-shadow: none !important;
padding-bottom: 13px;
/* Must be equal to .search_form_criteria:margin-bottom + this:padding-bottom */
}
.search_form_handler .sf_criterion_area .search_form_criteria > *, .search_form_handler .sf_criterion_area .sf_more_criterion > * {
padding: 7px 8px;
vertical-align: top;
border: 1px solid #ccc;
border-radius: 1px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group, .search_form_handler .sf_criterion_area .sf_more_criterion .sfc_form_group, .search_form_handler .sf_criterion_area .search_form_criteria .sfm_content, .search_form_handler .sf_criterion_area .sf_more_criterion .sfm_content {
position: absolute;
z-index: -1;
min-width: 100%;
left: 0px;
margin-top: -1px;
}
.search_form_handler .sf_criterion_area .search_form_criteria {
margin-right: 30px;
/* Non editable criteria */
/* Draft criteria (modifications not applied) */
/* Opened criteria (form group displayed) */
/* Top left corner icons */
/* Special criterion processing */
}
.search_form_handler .sf_criterion_area .search_form_criteria.locked {
background-color: #f1f1f1;
}
.search_form_handler .sf_criterion_area .search_form_criteria.locked .sfc_title {
user-select: none;
cursor: initial;
}
.search_form_handler .sf_criterion_area .search_form_criteria.draft .sfc_header, .search_form_handler .sf_criterion_area .search_form_criteria.draft .sfc_form_group {
border-style: dashed;
}
.search_form_handler .sf_criterion_area .search_form_criteria.draft .sfc_title {
font-style: italic;
}
.search_form_handler .sf_criterion_area .search_form_criteria.opened {
z-index: 1;
/* To be over other criterion */
}
.search_form_handler .sf_criterion_area .search_form_criteria.opened .sfc_toggle {
transform: rotateX(-180deg);
}
.search_form_handler .sf_criterion_area .search_form_criteria.opened .sfc_form_group {
display: block;
}
.search_form_handler .sf_criterion_area .search_form_criteria.opened_left .sfc_form_group {
left: auto;
right: 0px;
}
.search_form_handler .sf_criterion_area .search_form_criteria:not(:last-of-type)::after {
/* TODO: Find an elegant way to do this, without hardcoding the content (could be a <style> in the markup) and margin. Note, only a few languages (hawaiian and stuff like this have more than 4 letters in for "and" word. */
content: "and";
position: absolute;
top: 8px;
left: calc(100% + 0px);
margin-right: 5px;
margin-left: 5px;
text-align: center;
color: #808080;
}
.search_form_handler .sf_criterion_area .search_form_criteria > * {
background-color: #eee;
color: #2d2d2d;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_toggle, .search_form_handler .sf_criterion_area .search_form_criteria .sfc_close {
position: absolute;
top: 7px;
color: #e87c1e;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_locked {
position: absolute;
top: 9px;
color: #808080;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_toggle {
display: inline-block;
right: 23px;
transition: all 0.3s ease-in-out;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_close, .search_form_handler .sf_criterion_area .search_form_criteria .sfc_locked {
right: 7px;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_title {
max-width: 240px;
padding-right: 30px;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group {
/* Form group (operators) is displayed only when the criteria is toggled to opened state */
display: none;
max-height: 520px;
overflow: auto;
/* Show only first operator in simple mode */
/* Show all operators in advanced mode */
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators {
font-size: 12px;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator.force_hide {
display: none !important;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator > label {
line-height: 20px;
white-space: nowrap;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator > label > * {
display: inline-block;
vertical-align: middle;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator .sfc_op_radio {
width: 12px;
margin: 0px;
margin-right: 7px;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator .sfc_op_name {
width: 90px;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator .sfc_op_content input[type="text"] {
width: 160px;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator .sfc_opc_multichoices label > input {
vertical-align: middle;
margin-left: 0px;
margin-right: 8px;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator .sfc_opc_multichoices .sfc_opc_mc_items {
max-height: 445px;
/* Must be less than .sfc_form_group:max-height - .sfc_opc_mc_toggler:height - .sfc_opc_mc_filter:height */
overflow-x: hidden;
overflow-y: auto;
margin: 0px -8px;
/* Compensate .sfc_opc_multichoices side padding so the hover style can take the full with */
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator .sfc_opc_multichoices .sfc_opc_mc_items .sfc_opc_mc_items_list.sfc_opc_mc_items_selected {
position: relative;
padding-top: 5px;
margin-top: 5px;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator .sfc_opc_multichoices .sfc_opc_mc_items .sfc_opc_mc_items_list.sfc_opc_mc_items_selected::before {
content: "";
position: absolute;
border-top: 1px solid #ddd;
width: calc(100% - 12px);
/* minus margin-left x2 */
margin-left: 6px;
top: 0px;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator .sfc_opc_multichoices .sfc_opc_mc_items .sfc_opc_mc_items_list .sfc_opc_mc_placeholder {
padding: 15px 8px;
font-style: italic;
text-align: center;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator .sfc_opc_multichoices .sfc_opc_mc_items .sfc_opc_mc_items_list .sfc_opc_mc_item {
padding: 4px 8px;
/* Putting back the padding remove by .sfc_opc_mc_items */
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator .sfc_opc_multichoices .sfc_opc_mc_items .sfc_opc_mc_items_list .sfc_opc_mc_item:hover {
background-color: #fff;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator .sfc_opc_multichoices .sfc_opc_mc_items .sfc_opc_mc_items_list .sfc_opc_mc_item label {
display: inline-block;
width: 100%;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_apply, .search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_cancel {
margin-top: 8px;
padding: 3px 6px;
font-size: 11px;
/* Not bold, so it looks like 10px/bold of more/less buttons */
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_apply {
margin-right: 5px;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_more, .search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_less {
position: absolute;
bottom: 4px;
right: 0px;
cursor: pointer;
color: #3f7294;
font-size: 10px;
font-weight: bold;
border: none;
background-color: transparent;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_more > span, .search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_less > span {
margin-left: 3px;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operator:not(:first-of-type), .search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operator:first-of-type .sfc_op_radio {
display: none;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_less {
display: none;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_more {
display: inline-block;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group.advanced .sfc_fg_operator {
margin-bottom: 3px;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group.advanced .sfc_fg_operator:last-of-type {
margin-bottom: 0px;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group.advanced .sfc_fg_operator:not(:first-of-type), .search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group.advanced .sfc_fg_operator:first-of-type .sfc_op_radio {
display: inherit;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group.advanced .sfc_fg_less {
display: inline-block;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group.advanced .sfc_fg_more {
display: none;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group.advanced .hide_on_advanced {
display: none;
}
.search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group:not(.advanced) .hide_on_less {
display: none;
}
.search_form_handler .sf_criterion_area .search_form_criteria.search_form_criteria_raw > * {
border-color: transparent;
}
.search_form_handler .sf_criterion_area .search_form_criteria.search_form_criteria_raw .sfc_title {
cursor: initial;
padding-right: 20px;
/* Less than regular as there is no toggle icon */
}
.search_form_handler .sf_criterion_area .search_form_criteria.search_form_criteria_raw .sfc_form_group {
display: none;
}
.search_form_handler .sf_criterion_area .search_form_criteria.search_form_criteria_enum .sfc_form_group .sfc_fg_operator_in > label {
display: inline-block;
width: 100%;
line-height: initial;
white-space: nowrap;
}
.search_form_handler .sf_criterion_area .search_form_criteria.search_form_criteria_enum .sfc_form_group .sfc_fg_operator_in > label .sfc_op_content {
width: 100%;
}
.search_form_handler .sf_criterion_area .search_form_criteria.search_form_criteria_numeric .sfc_form_group.advanced .sfc_fg_operator_between {
margin-top: 5px;
margin-bottom: 5px;
}
.search_form_handler .sf_criterion_area .search_form_criteria.search_form_criteria_numeric .sfc_fg_operator_between label.sfc_op_content_from_label, .search_form_handler .sf_criterion_area .search_form_criteria.search_form_criteria_numeric .sfc_fg_operator_between label.sfc_op_content_until_label {
width: 90px;
display: inline-block;
}
.search_form_handler .sf_criterion_area .search_form_criteria.search_form_criteria_date_time .sfc_form_group.advanced .sfc_fg_operator_between, .search_form_handler .sf_criterion_area .search_form_criteria.search_form_criteria_date .sfc_form_group.advanced .sfc_fg_operator_between {
margin-bottom: 5px;
}
.search_form_handler .sf_criterion_area .search_form_criteria.search_form_criteria_date_time .sfc_fg_operator_between_days input, .search_form_handler .sf_criterion_area .search_form_criteria.search_form_criteria_date .sfc_fg_operator_between_days input {
width: 135px;
}
.search_form_handler .sf_criterion_area .search_form_criteria.search_form_criteria_date_time button.ui-datepicker-trigger, .search_form_handler .sf_criterion_area .search_form_criteria.search_form_criteria_date button.ui-datepicker-trigger {
background: none;
border: none;
height: 100%;
padding: 2px;
}
.search_form_handler .sf_criterion_area .search_form_criteria.search_form_criteria_date_time button.ui-datepicker-trigger img, .search_form_handler .sf_criterion_area .search_form_criteria.search_form_criteria_date button.ui-datepicker-trigger img {
vertical-align: middle;
}
.search_form_handler .sf_criterion_area .sf_more_criterion {
margin-right: 10px;
}
.search_form_handler .sf_criterion_area .sf_more_criterion.opened {
z-index: 2;
/* To be over criterion */
}
.search_form_handler .sf_criterion_area .sf_more_criterion.opened .sfm_content {
display: inherit;
}
.search_form_handler .sf_criterion_area .sf_more_criterion.opened_left .sfm_content {
left: auto;
right: 0px;
}
.search_form_handler .sf_criterion_area .sf_more_criterion > * {
background-color: #fff;
color: #2d2d2d;
}
.search_form_handler .sf_criterion_area .sf_more_criterion .sfm_toggler .sfm_tg_title {
margin-right: 7px;
}
.search_form_handler .sf_criterion_area .sf_more_criterion .sfm_toggler .sfm_tg_icon {
color: #e87c1e;
}
.search_form_handler .sf_criterion_area .sf_more_criterion .sfm_content {
display: none;
min-width: 200px;
/* To avoid element going to thin on filter, not very slick */
}
.search_form_handler .sf_criterion_area .sf_more_criterion .sfm_content .sfm_lists {
margin: 0px -8px;
padding: 0px 8px;
max-height: 400px;
overflow-x: hidden;
overflow-y: auto;
}
.search_form_handler .sf_criterion_area .sf_more_criterion .sfm_content .sfm_lists .sfl_items > li:hover {
background-color: #f1f1f1;
}
.search_form_handler .sf_criterion_area .sf_more_criterion .sfm_content .sfm_buttons {
display: none;
}
.search_form_handler .sf_criterion_area .sf_more_criterion .sfm_content .sfm_buttons button {
margin-top: 8px;
margin-right: 5px;
padding: 3px 6px;
font-size: 11px;
}
.search_form_handler .sf_criterion_area .sf_more_criterion .sfm_content .sfm_buttons button:last-of-type {
margin-right: 0px;
}
.search_form_handler .sf_list:not(:first-of-type) .sfl_title {
border-top: 1px solid #ddd;
padding-top: 8px;
margin-top: 5px;
}
.search_form_handler .sf_list .sfl_title {
font-weight: bold;
}
.search_form_handler .sf_list .sfl_items {
margin: 5px -8px 0px -8px;
padding: 0px;
}
.search_form_handler .sf_list .sfl_items > li {
padding: 4px 8px;
list-style: none;
white-space: nowrap;
}
.search_form_handler .sf_list .sfl_items > li:hover {
background-color: #fff;
}
.search_form_handler .sf_list .sfl_items > li.sfl_i_placeholder {
font-style: italic;
opacity: 0.8;
}
.search_form_handler .sf_list .sfl_items > li > label {
display: inline-block;
width: 100%;
}
.search_form_handler .sf_list .sfl_items > li > label > * {
vertical-align: middle;
}
.search_form_handler .sf_list .sfl_items > li > label > input[type="checkbox"] {
margin-left: 0px;
margin-right: 8px;
}
.search_form_handler .sf_filter {
position: relative;
margin-top: 8px;
margin-bottom: 8px;
}
.search_form_handler .sf_filter input, .search_form_handler .sf_filter button, .search_form_handler .sf_filter .sff_picto {
vertical-align: middle;
height: 22px;
}
.search_form_handler .sf_filter input, .search_form_handler .sf_filter button {
border: 1px solid #ababab;
}
.search_form_handler .sf_filter input {
width: 100%;
}
.search_form_handler .sf_filter button {
width: 23px;
/* Must be equals to .sf_filter > *:height */
background-color: #fff;
color: #ea7d1e;
font-size: 10px;
}
.search_form_handler .sf_filter button:first-of-type {
margin-left: 5px;
}
.search_form_handler .sf_filter button:not(:first-of-type) {
border-left: transparent;
}
.search_form_handler .sf_filter .sff_input_wrapper {
position: relative;
}
.search_form_handler .sf_filter .sff_input_wrapper .sff_picto {
position: absolute;
right: 5px;
top: 2px;
opacity: 0.6;
user-select: none;
}
.search_form_handler .sf_filter .sff_input_wrapper .sff_reset {
display: none;
}
.search_form_handler .sf_filter.sf_with_buttons input {
width: calc(100% - 28px) !important;
/* Minus button outter width (only one for now) */
min-width: 120px;
}
.sf_results_area .sf_results_placeholder {
margin-top: 100px;
text-align: center;
}
.sf_results_area .sf_results_placeholder button > span {
margin-left: 0.5em;
}
.hidden {
display: none !important;
}
.visible {
display: initial !important;
}
.mandatory {
border: 1px solid #f00;
}
@@ -892,7 +1338,7 @@ div#logo div {
margin-top: 2px;
margin-right: 4px;
padding: 6px 9px;
background-color: #e87c1e;
background-color: #ea7d1e;
color: white;
border-radius: 6px;
text-align: left;
@@ -1499,7 +1945,7 @@ img.prev, img.first, img.next, img.last {
}
div.actions_button {
float: right;
background: #e87c1e url("../images/actions_left.png?v=v2.4.0") no-repeat scroll left top;
background: #ea7d1e url("../images/actions_left.png?v=v2.4.0") no-repeat scroll left top;
padding-left: 5px;
margin-top: 0;
margin-right: 10px;
@@ -1507,7 +1953,7 @@ div.actions_button {
vertical-align: middle;
}
div.actions_button a, .actions_button a:hover, .actions_button a:visited {
background: #e87c1e url(../images/actions_bkg.png?v=v2.4.0) no-repeat scroll right top;
background: #ea7d1e url(../images/actions_bkg.png?v=v2.4.0) no-repeat scroll right top;
color: #fff;
padding-right: 8px;
cursor: pointer;
@@ -1540,7 +1986,7 @@ select#org_id {
position: relative;
}
.edit_mode .dashlet-selected {
background: #e87c1e !important;
background: #ea7d1e !important;
padding: 5px;
margin: 0;
}
@@ -1703,10 +2149,10 @@ a.summary, a.summary:hover {
position: absolute;
display: none;
border-top: 1px solid white;
z-index: 999;
z-index: 1500;
}
#DashboardMenu li ul li a:hover {
background: #e87c1e;
background: #ea7d1e;
color: #fff;
font-weight: bold;
list-style: none;
@@ -2063,7 +2509,7 @@ span.refresh-button {
}
#itop-breadcrumb .breadcrumb-item a:hover {
text-decoration: none;
color: #e87c1e;
color: #ea7d1e;
}
#itop-breadcrumb .breadcrumb-item a::after {
content: '';
@@ -2127,7 +2573,7 @@ span.refresh-button {
margin-bottom: 1px;
}
.object-ref-icon.fa {
color: #e87c1e;
color: #ea7d1e;
font-size: smaller;
vertical-align: 1px;
margin-right: 1px;

View File

@@ -646,7 +646,7 @@ div.actions_menu > ul > li {
position: absolute;
display: none;
border-top: 1px solid white;
z-index: 999;
z-index: 1500;
}
.itop_popup li ul li, #logOffBtn li ul li {
@@ -756,65 +756,6 @@ input.dp-applied {
}
/* For search forms */
.SearchDrawer {
//background: $complement-color url(../images/search-top-left-corner.png?v=#{$version}) top left no-repeat;
border-top: 5px solid $complement-color;
border-left: 5px solid $complement-color;
border-right: 5px solid $complement-color;
border-bottom: 0;
background: $complement-light;
color: #000;
padding: 10px;
margin: 0;
font-size: 12px;
}
.SearchDrawer label {
background: $complement-light;
color: #000;
text-align: right;
}
.SearchDrawer h1 {
color: #000;
}
.SearchDrawer .SearchAttribute{
> .field_input_zone{
display: inline-block;
> .field_select_wrapper{
display: inline-block;
}
}
}
.DrawerClosed {
display: none;
}
.DrawerHandle {
margin: 0;
padding: 5px;
background: url(../images/drawer-handle.gif) bottom no-repeat transparent;
color: #fff;
cursor: pointer;
text-align: center;
/* center the block */
width: 100px;
margin-left: auto;
margin-right: auto;
margin-top: 0;
margin-bottom: 0;
display: block;
font-size: 12px;
}
div.HRDrawer {
height: 5px;
width: 100%;
margin: 0;
background-color: $complement-color;
margin: 0;
padding: 0;
border: 0;
display: block;
}
.mini_tabs a {
text-decoration: none;
font-weight:bold;
@@ -839,6 +780,634 @@ div.HRDrawer {
nopadding-right: 1em;
margin-top: 0;
}
/* Search forms v2 */
.search_box{
box-sizing: border-box;
position: relative;
z-index: 1100; /* To be over the table block/unblock UI. Not very sure about this. */
/* Sizing reset */
*{
box-sizing: border-box;
}
}
.search_form_handler{
position: relative;
z-index: 10;
font-size: 12px;
border: 1px solid #3F7294;
/* Sizing reset */
*{
box-sizing: border-box;
}
/* Hyperlink reset */
a{
color: inherit;
text-decoration: none;
}
/* Input reset */
input[type="text"],
select{
padding: 1px 2px;
}
&.opened{
.sf_title{
.sft_toggler{
transform: rotateX(180deg);
}
}
.sf_criterion_area{
/*display: inherit;*/
}
}
.sf_title{
padding: 8px 10px;
margin: 0;
color: #ffffff;
background-color: #3F7294;
cursor: pointer;
/* Pictogram */
.sft_picto{
display: none; /* TODO: Remove this class and the correspondig DOM element if this option is kept. */
margin-right: 10px;
}
.sft_refresh,
.sft_toggler{
transition: color 0.2s ease-in-out, transform 0.4s ease-in-out;
&:hover{
color: $gray-extra-light;
}
}
.sft_refresh{
font-size: 10pt;
line-height: 13pt;
}
.sft_toggler{
margin-left: 0.7em;
}
}
.sf_message{
display: none;
margin: 8px 8px 0px 8px;
border-radius: 0px;
}
.sf_criterion_area{
/*display: none;*/
padding: 8px 8px 3px 8px; /* padding-bottom must equals to padding-top - .search_form_criteria:margin-bottom */
background-color: $white;
/* Common style between criterion and more criterion */
.search_form_criteria,
.sf_more_criterion {
position: relative;
display: inline-block;
margin-bottom: 5px;
vertical-align: top;
&.opened{
margin-bottom: 0px; /* To compensate the .sfc/.sfm_header:padding-bottom: 13px */
.sfc_header,
.sfm_header{
border-bottom: none !important;
box-shadow: none !important;
padding-bottom: 13px; /* Must be equal to .search_form_criteria:margin-bottom + this:padding-bottom */
}
}
> *{
padding: 7px 8px;
vertical-align: top;
border: $search-criteria-box-border;
border-radius: $search-criteria-box-radius;
box-shadow: $box-shadow-regular;
}
.sfc_form_group,
.sfm_content{
position: absolute;
z-index: -1;
min-width: 100%;
left: 0px;
margin-top: -1px;
}
}
/* Criteria tags */
.search_form_criteria{
margin-right: 30px;
/* Non editable criteria */
&.locked{
background-color: $gray-extra-light;
.sfc_title{
user-select: none;
cursor: initial;
}
}
/* Draft criteria (modifications not applied) */
&.draft{
.sfc_header,
.sfc_form_group{
border-style: dashed;
}
.sfc_title{
font-style: italic;
}
}
/* Opened criteria (form group displayed) */
&.opened{
z-index: 1; /* To be over other criterion */
.sfc_toggle{
transform: rotateX(-180deg);
}
.sfc_form_group{
display: block;
}
}
&.opened_left{
.sfc_form_group{
left: auto;
right: 0px;
}
}
&:not(:last-of-type)::after{
/* TODO: Find an elegant way to do this, without hardcoding the content (could be a <style> in the markup) and margin. Note, only a few languages (hawaiian and stuff like this have more than 4 letters in for "and" word. */
content: "and";
position: absolute;
top: 8px;
left: calc(100% + 0px);
margin-right: 5px;
margin-left: 5px;
text-align: center;
color: $gray-light;
}
> *{
background-color: $search-criteria-box-bg-color;
color: $search-criteria-box-color;
}
/* Top left corner icons */
.sfc_toggle,
.sfc_close{
position: absolute;
top: 7px;
color: $search-criteria-box-picto-color;
}
.sfc_locked{
position: absolute;
top: 9px;
color: $gray-light;
}
.sfc_toggle{
display: inline-block;
right: 23px;
transition: all 0.3s ease-in-out;
}
.sfc_close,
.sfc_locked{
right: 7px;
}
.sfc_title{
max-width: 240px;
padding-right: 30px;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.sfc_form_group{
/* Form group (operators) is displayed only when the criteria is toggled to opened state */
display: none;
max-height: 520px;
overflow: auto;
.sfc_fg_operators{
font-size: 12px;
.sfc_fg_operator{
&.force_hide {
display: none !important;
}
> label{
line-height: 20px;
white-space: nowrap;
> *{
display: inline-block;
vertical-align: middle;
}
}
.sfc_op_radio{
width: 12px;
margin: 0px;
margin-right: 7px;
}
.sfc_op_name{
width: 90px;
}
.sfc_op_content{
input[type="text"]{
width: 160px;
}
}
.sfc_opc_multichoices{
label > input{
vertical-align: middle;
margin-left: 0px;
margin-right: 8px;
}
.sfc_opc_mc_toggler{
}
.sfc_opc_mc_items{
max-height: 445px; /* Must be less than .sfc_form_group:max-height - .sfc_opc_mc_toggler:height - .sfc_opc_mc_filter:height */
overflow-x: hidden;
overflow-y: auto;
margin: 0px -8px; /* Compensate .sfc_opc_multichoices side padding so the hover style can take the full with */
.sfc_opc_mc_items_list{
&.sfc_opc_mc_items_selected{
position: relative;
padding-top: 5px;
margin-top: 5px;
&::before{
content: "";
position: absolute;
border-top: 1px solid $gray-lighter;
width: calc(100% - 12px); /* minus margin-left x2 */
margin-left: 6px;
top: 0px;
}
}
.sfc_opc_mc_placeholder{
padding: 15px 8px;
font-style: italic;
text-align: center;
}
.sfc_opc_mc_item{
padding: 4px 8px; /* Putting back the padding remove by .sfc_opc_mc_items */
&:hover{
background-color: $search-criteria-box-hover-color;
}
label{
display: inline-block;
width: 100%;
}
}
}
}
}
}
}
.sfc_fg_apply,
.sfc_fg_cancel{
margin-top: 8px;
padding: 3px 6px;
font-size: 11px; /* Not bold, so it looks like 10px/bold of more/less buttons */
}
.sfc_fg_apply{
margin-right: 5px;
}
.sfc_fg_more,
.sfc_fg_less{
position: absolute;
bottom: 4px;
right: 0px;
cursor: pointer;
color: #3F7294;
font-size: 10px;
font-weight: bold;
border: none;
background-color: transparent;
> span{
margin-left: 3px;
}
}
/* Show only first operator in simple mode */
.sfc_fg_operator:not(:first-of-type),
.sfc_fg_operator:first-of-type .sfc_op_radio{
display: none;
}
.sfc_fg_less{
display: none;
}
.sfc_fg_more{
display: inline-block;
}
/* Show all operators in advanced mode */
&.advanced{
.sfc_fg_operator{
margin-bottom: 3px;
&:last-of-type{
margin-bottom: 0px;
}
}
.sfc_fg_operator:not(:first-of-type),
.sfc_fg_operator:first-of-type .sfc_op_radio{
display: inherit;
}
.sfc_fg_less{
display: inline-block;
}
.sfc_fg_more{
display: none;
}
.hide_on_advanced {
display: none;
}
}
&:not(.advanced) {
.hide_on_less {
display: none;
}
}
}
/* Special criterion processing */
&.search_form_criteria_raw{
> *{
border-color: transparent;
}
.sfc_title{
cursor: initial;
padding-right: 20px; /* Less than regular as there is no toggle icon */
}
.sfc_form_group{
display: none;
}
}
&.search_form_criteria_enum{
.sfc_form_group{
.sfc_fg_operator_in{
> label{
display: inline-block;
width: 100%;
line-height: initial;
white-space: nowrap;
.sfc_op_content{
width: 100%;
}
}
}
}
}
&.search_form_criteria_numeric {
.sfc_form_group.advanced {
.sfc_fg_operator_between {
margin-top: 5px;
margin-bottom: 5px;
}
}
.sfc_fg_operator_between {
label {
&.sfc_op_content_from_label, &.sfc_op_content_until_label {
width: 90px;
display: inline-block;
}
}
}
}
&.search_form_criteria_date_time,
&.search_form_criteria_date {
.sfc_form_group.advanced {
.sfc_fg_operator_between {
margin-bottom: 5px;
}
}
.sfc_fg_operator_between_days {
input {
width: 135px;
}
}
button.ui-datepicker-trigger {
background: none;
border: none;
height: 100%;
padding: 2px;
img {
vertical-align: middle;
}
}
}
}
/* More criterion */
.sf_more_criterion{
margin-right: 10px;
&.opened{
z-index: 2; /* To be over criterion */
.sfm_content{
display: inherit;
}
}
&.opened_left{
.sfm_content{
left: auto;
right: 0px;
}
}
> *{
background-color: $search-add-criteria-box-bg-color;
color: $search-add-criteria-box-color;
}
.sfm_toggler{
.sfm_tg_title{
margin-right: 7px;
}
.sfm_tg_icon{
color: $search-criteria-box-picto-color;
}
}
.sfm_content{
display: none;
min-width: 200px; /* To avoid element going to thin on filter, not very slick */
.sfm_lists{
margin: 0px -8px;
padding: 0px 8px;
max-height: 400px;
overflow-x: hidden;
overflow-y: auto;
.sfl_items{
> li{
&:hover{
background-color: $search-add-criteria-box-hover-color;
}
}
}
}
.sfm_buttons{
display: none;
button{
margin-top: 8px;
margin-right: 5px;
padding: 3px 6px;
font-size: 11px;
&:last-of-type{
margin-right: 0px;
}
}
}
}
}
}
/* List helpers */
.sf_list{
&:not(:first-of-type){
.sfl_title{
border-top: 1px solid $gray-lighter;
padding-top: 8px;
margin-top: 5px;
}
}
.sfl_title{
font-weight: bold;
}
.sfl_items{
margin: 5px -8px 0px -8px;
padding: 0px;
> li{
padding: 4px 8px;
list-style: none;
white-space: nowrap;
&:hover{
background-color: $white;
}
&.sfl_i_placeholder{
font-style: italic;
opacity: 0.8;
}
> label{
display: inline-block;
width: 100%;
> *{
vertical-align: middle;
}
> input[type="checkbox"]{
margin-left: 0px;
margin-right: 8px;
}
}
}
}
}
.sf_filter{
position: relative;
margin-top: 8px;
margin-bottom: 8px;
input,
button,
.sff_picto{
vertical-align: middle;
height: 22px;
}
input,
button{
border: 1px solid #ABABAB;
}
input{
width: 100%;
}
button{
width: 23px; /* Must be equals to .sf_filter > *:height */
background-color: $white;
color: $combodo-orange;
font-size: 10px;
&:first-of-type{
margin-left: 5px;
}
&:not(:first-of-type){
border-left: transparent;
}
}
.sff_input_wrapper{
position: relative;
.sff_picto{
position: absolute;
right: 5px;
top: 2px;
opacity: 0.6;
user-select: none;
}
.sff_reset{
display: none;
}
}
&.sf_with_buttons{
input{
width: calc(100% - 28px) !important; /* Minus button outter width (only one for now) */
min-width: 120px;
}
}
}
}
.sf_results_area{
.sf_results_placeholder{
margin-top: 100px;
text-align: center;
button > span{
margin-left: 0.5em;
}
}
}
.hidden{
display: none !important;
}
.visible{
display: initial !important;
}
.mandatory {
border: 1px solid #f00;
}
@@ -1881,7 +2450,7 @@ a.summary, a.summary:hover {
position: absolute;
display: none;
border-top: 1px solid white;
z-index: 999;
z-index: 1500;
}
#DashboardMenu li ul li a:hover{
background: $popup-menu-highlight-color;

View File

@@ -311,6 +311,17 @@ Dict::Add('EN US', 'English', 'English', array(
'Class:URP_AttributeGrant/Attribute:attcode+' => 'attribute code',
));
//
// Expression to Natural language
//
Dict::Add('EN US', 'English', 'English', array(
'Expression:Unit:Short:DAY' => 'd',
'Expression:Unit:Short:WEEK' => 'w',
'Expression:Unit:Short:MONTH' => 'm',
'Expression:Unit:Short:YEAR' => 'y',
));
//
// String from the User Interface: menu, messages, buttons, etc...
//
@@ -367,6 +378,7 @@ Dict::Add('EN US', 'English', 'English', array(
'UI:Button:Ok' => 'Ok',
'UI:Button:Save' => 'Save',
'UI:Button:Cancel' => 'Cancel',
'UI:Button:Close' => 'Close',
'UI:Button:Apply' => 'Apply',
'UI:Button:Back' => ' << Back ',
'UI:Button:Restart' => ' |<< Restart ',
@@ -390,6 +402,8 @@ Dict::Add('EN US', 'English', 'English', array(
'UI:Button:ChangePassword' => ' Change Password ',
'UI:Button:ResetPassword' => ' Reset Password ',
'UI:Button:Insert' => 'Insert',
'UI:Button:More' => 'More',
'UI:Button:Less' => 'Less',
'UI:SearchToggle' => 'Search',
'UI:ClickToCreateNew' => 'Create a new %1$s',
@@ -1368,4 +1382,116 @@ When associated with a trigger, each action is given an "order" number, specifyi
'UI:Button:ResetImage' => 'Recover the previous image',
'UI:Button:RemoveImage' => 'Remove the image',
'UI:UploadNotSupportedInThisMode' => 'The modification of images or files is not supported in this mode.',
// TODO: Reorganize those entries with other search entries and make entries for other languages.
// Search form
'UI:Search:Toggle' => 'Minimize / Expand',
'UI:Search:Criterion:MoreMenu:AddCriteria' => 'Add new criteria',
// - Add new criteria button
'UI:Search:AddCriteria:List:RecentlyUsed:Title' => 'Recently used',
'UI:Search:AddCriteria:List:MostPopular:Title' => 'Most popular',
'UI:Search:AddCriteria:List:Others:Title' => 'Others',
'UI:Search:AddCriteria:List:RecentlyUsed:Placeholder' => 'None yet.',
// - Criteria titles
// - Default widget
'UI:Search:Criteria:Title:Default:Empty' => '%1$s is empty',
'UI:Search:Criteria:Title:Default:NotEmpty' => '%1$s is not empty',
'UI:Search:Criteria:Title:Default:Equals' => '%1$s equals %2$s',
'UI:Search:Criteria:Title:Default:Contains' => '%1$s contains %2$s',
'UI:Search:Criteria:Title:Default:StartsWith' => '%1$s starts with %2$s',
'UI:Search:Criteria:Title:Default:EndsWith' => '%1$s ends with %2$s',
'UI:Search:Criteria:Title:Default:RegExp' => '%1$s matches %2$s',
'UI:Search:Criteria:Title:Default:GreaterThan' => '%1$s > %2$s',
'UI:Search:Criteria:Title:Default:GreaterThanOrEquals' => '%1$s >= %2$s',
'UI:Search:Criteria:Title:Default:LessThan' => '%1$s < %2$s',
'UI:Search:Criteria:Title:Default:LessThanOrEquals' => '%1$s <= %2$s',
'UI:Search:Criteria:Title:Default:Different' => '%1$s ≠ %2$s',
'UI:Search:Criteria:Title:Default:Between' => '%1$s between [%2$s]',
'UI:Search:Criteria:Title:Default:BetweenDates:All' => '%1$s: Any',
'UI:Search:Criteria:Title:Default:BetweenDates:From' => '%1$s from %2$s',
'UI:Search:Criteria:Title:Default:BetweenDates:Until' => '%1$s until %2$s',
'UI:Search:Criteria:Title:Default:Between:All' => '%1$s: Any',
'UI:Search:Criteria:Title:Default:Between:From' => '%1$s from %2$s',
'UI:Search:Criteria:Title:Default:Between:Until' => '%1$s until %2$s',
'UI:Search:Criteria:Title:Default:BetweenDays' => '%1$s [%2$s]',
// - Numeric widget
// None yet
// - DateTime widget
'UI:Search:Criteria:Title:DateTime:Between' => '%2$s <= 1$s <= %3$s',
// - Enum widget
'UI:Search:Criteria:Title:Enum:In' => '%1$s: %2$s',
'UI:Search:Criteria:Title:Enum:In:Many' => '%1$s: %2$s and %3$s others',
'UI:Search:Criteria:Title:Enum:In:All' => '%1$s: Any',
// - External key widget
'UI:Search:Criteria:Title:ExternalKey:Empty' => '%1$s is defined',
'UI:Search:Criteria:Title:ExternalKey:NotEmpty' => '%1$s is not defined',
'UI:Search:Criteria:Title:ExternalKey:Equals' => '%1$s %2$s',
'UI:Search:Criteria:Title:ExternalKey:In' => '%1$s: %2$s',
'UI:Search:Criteria:Title:ExternalKey:In:Many' => '%1$s: %2$s and %3$s others',
'UI:Search:Criteria:Title:ExternalKey:In:All' => '%1$s: Any',
// - Criteria operators
// - Default widget
'UI:Search:Criteria:Operator:Default:Empty' => 'Is empty',
'UI:Search:Criteria:Operator:Default:NotEmpty' => 'Is not empty',
'UI:Search:Criteria:Operator:Default:Equals' => 'Equals',
'UI:Search:Criteria:Operator:Default:Between' => 'Between',
// - String widget
'UI:Search:Criteria:Operator:String:Contains' => 'Contains',
'UI:Search:Criteria:Operator:String:StartsWith' => 'Starts with',
'UI:Search:Criteria:Operator:String:EndsWith' => 'Ends with',
'UI:Search:Criteria:Operator:String:RegExp' => 'Regular exp.',
// - Numeric widget
'UI:Search:Criteria:Operator:Numeric:Equals' => 'Equals', // => '=',
'UI:Search:Criteria:Operator:Numeric:GreaterThan' => 'Greater', // => '>',
'UI:Search:Criteria:Operator:Numeric:GreaterThanOrEquals' => 'Greater / equals', // > '>=',
'UI:Search:Criteria:Operator:Numeric:LessThan' => 'Less', // => '<',
'UI:Search:Criteria:Operator:Numeric:LessThanOrEquals' => 'Less / equals', // > '<=',
'UI:Search:Criteria:Operator:Numeric:Different' => 'Different', // => '≠',
// - Other translations
'UI:Search:Value:Filter:Placeholder' => 'Filter...',
'UI:Search:Value:Search:Placeholder' => 'Search...',
'UI:Search:Value:Autocomplete:StartTyping' => 'Start typing for possible values.',
'UI:Search:Value:Autocomplete:Wait' => 'Please wait...',
'UI:Search:Value:Autocomplete:NoResult' => 'No result.',
'UI:Search:Value:Toggler:CheckAllNone' => 'Check all / none',
'UI:Search:Value:Toggler:CheckAllNoneFiltered' => 'Check all / none filtered',
// - Widget other translations
'UI:Search:Criteria:Numeric:From' => 'From',
'UI:Search:Criteria:Numeric:Until' => 'Until',
'UI:Search:Criteria:Numeric:PlaceholderFrom' => 'Any',
'UI:Search:Criteria:Numeric:PlaceholderUntil' => 'Any',
'UI:Search:Criteria:DateTime:From' => 'From',
'UI:Search:Criteria:DateTime:FromTime' => 'From',
'UI:Search:Criteria:DateTime:Until' => 'until',
'UI:Search:Criteria:DateTime:UntilTime' => 'until',
'UI:Search:Criteria:DateTime:PlaceholderFrom' => 'Any date',
'UI:Search:Criteria:DateTime:PlaceholderFromTime' => 'Any date',
'UI:Search:Criteria:DateTime:PlaceholderUntil' => 'Any date',
'UI:Search:Criteria:DateTime:PlaceholderUntilTime' => 'Any date',
'UI:Search:Criteria:Raw:Filtered' => 'Filtered',
'UI:Search:Criteria:Raw:FilteredOn' => 'Filtered on %1$s',
));
//
// Expression to Natural language
//
Dict::Add('EN US', 'English', 'English', array(
'Expression:Operator:AND' => ' AND ',
'Expression:Operator:OR' => ' OR ',
'Expression:Unit:Short:DAY' => 'd',
'Expression:Unit:Short:WEEK' => 'w',
'Expression:Unit:Short:MONTH' => 'm',
'Expression:Unit:Short:YEAR' => 'y',
'Expression:Unit:Long:DAY' => 'day(s)',
'Expression:Unit:Long:HOUR' => 'hour(s)',
'Expression:Unit:Long:MINUTE' => 'minute(s)',
'Expression:Verb:NOW' => 'now',
));

View File

@@ -1200,4 +1200,116 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
'UI:Button:ResetImage' => 'Récupérer l\'image initiale',
'UI:Button:RemoveImage' => 'Supprimer l\'image',
'UI:UploadNotSupportedInThisMode' => 'La modification d\'images ou de fichiers n\'est pas supportée dans ce mode.',
));
// TODO: Reorganize those entries with other search entries and make entries for other languages.
// Search form
'UI:Search:Toggle' => 'Réduire / Ouvrir',
'UI:Search:Criterion:MoreMenu:AddCriteria' => 'Ajouter un critère',
// - Add new criteria button
'UI:Search:AddCriteria:List:RecentlyUsed:Title' => 'Récents',
'UI:Search:AddCriteria:List:MostPopular:Title' => 'Populaires',
'UI:Search:AddCriteria:List:Others:Title' => 'Autres',
'UI:Search:AddCriteria:List:RecentlyUsed:Placeholder' => 'Aucun.',
// - Criteria titles
// - Default widget
'UI:Search:Criteria:Title:Default:Empty' => '%1$s vide',
'UI:Search:Criteria:Title:Default:NotEmpty' => '%1$s non vide',
'UI:Search:Criteria:Title:Default:Equals' => '%1$s égal %2$s',
'UI:Search:Criteria:Title:Default:Contains' => '%1$s contient %2$s',
'UI:Search:Criteria:Title:Default:StartsWith' => '%1$s commence par %2$s',
'UI:Search:Criteria:Title:Default:EndsWith' => '%1$s fini par %2$s',
'UI:Search:Criteria:Title:Default:RegExp' => '%1$s correspond à %2$s',
'UI:Search:Criteria:Title:Default:GreaterThan' => '%1$s > %2$s',
'UI:Search:Criteria:Title:Default:GreaterThanOrEquals' => '%1$s >= %2$s',
'UI:Search:Criteria:Title:Default:LessThan' => '%1$s < %2$s',
'UI:Search:Criteria:Title:Default:LessThanOrEquals' => '%1$s <= %2$s',
'UI:Search:Criteria:Title:Default:Different' => '%1$s ≠ %2$s',
'UI:Search:Criteria:Title:Default:Between' => '%1$s entre [%2$s]',
'UI:Search:Criteria:Title:Default:BetweenDates:All' => '%1$s : Indifférent',
'UI:Search:Criteria:Title:Default:BetweenDates:From' => '%1$s depuis %2$s',
'UI:Search:Criteria:Title:Default:BetweenDates:Until' => '%1$s jusqu\'à %2$s',
'UI:Search:Criteria:Title:Default:Between:All' => '%1$s : Indifférent',
'UI:Search:Criteria:Title:Default:Between:From' => '%1$s à partir de %2$s',
'UI:Search:Criteria:Title:Default:Between:Until' => '%1$s jusqu\'à %2$s',
'UI:Search:Criteria:Title:Default:BetweenDays' => '%1$s [%2$s]',
// - Numeric widget
// None yet
// - DateTime widget
'UI:Search:Criteria:Title:DateTime:Between' => '%2$s <= 1$s <= %3$s',
// - Enum widget
'UI:Search:Criteria:Title:Enum:In' => '%1$s : %2$s',
'UI:Search:Criteria:Title:Enum:In:Many' => '%1$s : %2$s et %3$s autres',
'UI:Search:Criteria:Title:Enum:In:All' => '%1$s : Indifférent',
// - External key widget
'UI:Search:Criteria:Title:ExternalKey:Empty' => '%1$s est renseigné',
'UI:Search:Criteria:Title:ExternalKey:NotEmpty' => '%1$s n\'est pas renseigné',
'UI:Search:Criteria:Title:ExternalKey:Equals' => '%1$s %2$s',
'UI:Search:Criteria:Title:ExternalKey:In' => '%1$s : %2$s',
'UI:Search:Criteria:Title:ExternalKey:In:Many' => '%1$s : %2$s et %3$s autres',
'UI:Search:Criteria:Title:ExternalKey:In:All' => '%1$s : Indifférent',
/// - Criteria operators
// - Default widget
'UI:Search:Criteria:Operator:Default:Empty' => 'Vide',
'UI:Search:Criteria:Operator:Default:NotEmpty' => 'Non vide',
'UI:Search:Criteria:Operator:Default:Equals' => 'Egal',
// - String widget
'UI:Search:Criteria:Operator:String:Contains' => 'Contient',
'UI:Search:Criteria:Operator:String:StartsWith' => 'Commence par',
'UI:Search:Criteria:Operator:String:EndsWith' => 'Fini par',
'UI:Search:Criteria:Operator:String:RegExp' => 'Exp. rég.',
// - Numeric widget
'UI:Search:Criteria:Operator:Numeric:Equals' => 'Egal', // => '=',
'UI:Search:Criteria:Operator:Numeric:GreaterThan' => 'Supérieur', // => '>',
'UI:Search:Criteria:Operator:Numeric:GreaterThanOrEquals' => 'Sup. / égal', // > '>=',
'UI:Search:Criteria:Operator:Numeric:LessThan' => 'Inférieur', // => '<',
'UI:Search:Criteria:Operator:Numeric:LessThanOrEquals' => 'Inf. / égal', // > '<=',
'UI:Search:Criteria:Operator:Numeric:Different' => 'Différent', // => '≠',
// - Other translations
'UI:Search:Value:Filter:Placeholder' => 'Filtrez...',
'UI:Search:Value:Search:Placeholder' => 'Recherchez...',
'UI:Search:Value:Autocomplete:StartTyping' => 'Commencez à taper pour voir les valeurs possibles.',
'UI:Search:Value:Autocomplete:Wait' => 'Patientez ...',
'UI:Search:Value:Autocomplete:NoResult' => 'Aucun résultat.',
'UI:Search:Value:Toggler:CheckAllNone' => 'Cocher tout / aucun',
'UI:Search:Value:Toggler:CheckAllNoneFiltered' => 'Cocher tout / aucun filtrés',
// - Widget other translations
'UI:Search:Criteria:Numeric:From' => 'Depuis',
'UI:Search:Criteria:Numeric:Until' => 'Jusqu\'à',
'UI:Search:Criteria:Numeric:PlaceholderFrom' => 'Indifférent',
'UI:Search:Criteria:Numeric:PlaceholderUntil' => 'Indifférent',
'UI:Search:Criteria:DateTime:From' => 'Depuis',
'UI:Search:Criteria:DateTime:FromTime' => 'Depuis',
'UI:Search:Criteria:DateTime:Until' => 'jusqu\'à',
'UI:Search:Criteria:DateTime:UntilTime' => 'jusqu\'à',
'UI:Search:Criteria:DateTime:PlaceholderFrom' => 'Indifférent',
'UI:Search:Criteria:DateTime:PlaceholderFromTime' => 'Indifférent',
'UI:Search:Criteria:DateTime:PlaceholderUntil' => 'Indifférent',
'UI:Search:Criteria:DateTime:PlaceholderUntilTime' => 'Indifférent',
'UI:Search:Criteria:Raw:Filtered' => 'Filtré',
'UI:Search:Criteria:Raw:FilteredOn' => 'Filtré sur %1$s',
));
//
// Expression to Natural language
//
Dict::Add('FR FR', 'French', 'Français', array(
'Expression:Operator:AND' => ' ET ',
'Expression:Operator:OR' => ' OU ',
'Expression:Unit:Short:DAY' => 'j',
'Expression:Unit:Short:WEEK' => 's',
'Expression:Unit:Short:MONTH' => 'm',
'Expression:Unit:Short:YEAR' => 'a',
'Expression:Unit:Long:DAY' => 'jour(s)',
'Expression:Unit:Long:HOUR' => 'heure(s)',
'Expression:Unit:Long:MINUTE' => 'minute(s)',
'Expression:Verb:NOW' => 'maintenant',
));

View File

@@ -184,6 +184,15 @@ $(function()
oParams.real_class = '';
oParams.att_code = this.options.att_code;
oParams.iInputId = this.id;
// Gather the already linked target objects
oParams.aAlreadyLinked = new Array();
$('#'+this.id+' .listResults td input:checkbox').each(function () {
iKey = parseInt(this.value, 10); // Numbers are in base 10
oParams.aAlreadyLinked.push(iKey);
}
);
if (this.options.oWizardHelper)
{
this.options.oWizardHelper.UpdateWizard();

View File

@@ -1,3 +1,20 @@
// Copyright (C) 2010-2018 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/>
// JavaScript Document
function LinksWidget(id, sClass, sAttCode, iInputId, sSuffix, bDuplicates, oWizHelper, sExtKeyToRemote)
{
@@ -101,21 +118,43 @@ function LinksWidget(id, sClass, sAttCode, iInputId, sSuffix, bDuplicates, oWizH
operation: 'addObjects',
json: me.oWizardHelper.ToJSON()
};
$.post( GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', theMap,
function(data)
{
$('#dlg_'+me.id).html(data);
$('#dlg_'+me.id).dialog('open');
me.UpdateSizes(null, null);
me.SearchObjectsToAdd();
$('#'+me.id+'_indicatorAdd').html('');
},
'html'
);
// Gather the already linked target objects
theMap.aAlreadyLinked = [];
$('#linkedset_'+me.id+' .selection:input').each(function (i) {
var iRemote = $(this).attr('data-remote-id');
theMap.aAlreadyLinked.push(iRemote);
});
$.ajax({
"url": GetAbsoluteUrlAppRoot()+'pages/ajax.render.php',
"method": "POST",
"data": theMap,
"dataType": "html"
})
.done(function (data) {
$('#dlg_'+me.id).html(data);
$('#dlg_'+me.id).dialog('open');
me.UpdateSizes(null, null);
me.SearchObjectsToAdd();
$('#'+me.id+'_indicatorAdd').html('');
})
;
};
this.SearchObjectsToAdd = function()
{
$('#count_'+me.id).change(function () {
var c = this.value;
me.UpdateButtons(c);
});
FixSearchFormsDisposition();
me.UpdateSizes(null, null);
$("#fs_SearchFormToAdd_"+me.id).trigger('itop.search.form.submit');
return false; // Don't submit the form, stay in the current page !
var theMap = { sAttCode: me.sAttCode,
iInputId: me.iInputId,
sSuffix: me.sSuffix,

View File

@@ -0,0 +1,729 @@
//iTop Search form criteria
;
$(function()
{
// the widget definition, where 'itop' is the namespace,
// 'search_form_criteria' the widget name
$.widget( 'itop.search_form_criteria',
{
// default options
options:
{
// Default values for the criteria
'ref': '',
'operator': '=',
'values': [],
'oql': '',
'is_removable': true, // Not used for now. If we come to show locked criterion they will need to have this flag set to false.
'field': {
'label': '',
'allowed_values': null,
'is_null_allowed': false,
},
// Available operators. They can be extended or restricted by derivated widgets (see this._initOperators() for more informations)
'available_operators': {
'=': {
'label': Dict.S('UI:Search:Criteria:Operator:Default:Equals'),
'code': 'equals',
'rank': 10,
},
'empty': {
'label': Dict.S('UI:Search:Criteria:Operator:Default:Empty'),
'code': 'empty',
'rank': 90,
},
'not_empty': {
'label': Dict.S('UI:Search:Criteria:Operator:Default:NotEmpty'),
'code': 'not_empty',
'rank': 100,
},
},
'init_opened': false,
'is_modified': false, // TODO: change this on value change and remove oql property value
},
// Operators
operators: null,
// Form handler
handler: null,
// Keys that should not trigger an event in filter/autocomplete inputs
filtered_keys: [9, 16, 17, 18, 19, 27, 33, 34, 35, 36, 37, 38, 39, 40], // Tab, Shift, Ctrl, Alt, Pause, Esc, Page Up/Down, Home, End, Left/Up/Right/Down arrows
// the constructor
_create: function()
{
var me = this;
this.element.addClass('search_form_criteria');
// Init properties (complexe type properties would be static if not initialized with a simple type variable...)
this.operators = {};
// Init operators
this._initOperators();
// Link search form handler
this.handler = this.element.closest('.search_form_handler');
// Bind events
this._bindEvents();
this._prepareElement();
},
// called when created, and later when changing options
_refresh: function()
{
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function()
{
this.element.removeClass('search_form_criteria');
},
// _setOptions is called with a hash of all options that are changing
// always refresh when changing options
_setOptions: function()
{
this._superApply(arguments);
},
// _setOption is called for each individual option that is changing
_setOption: function( key, value )
{
this._super( key, value );
},
// Protected methods
// - Init operators by cleaning up available operators and ordering them.
// Note: A null operator or an operator with a rank "false" will be removed.
_initOperators: function()
{
// Reset operators
this.operators = {};
// Cancel empty/not_empty operators if field can't be null
if(this.options.field.is_null_allowed === false)
{
this.options.available_operators.empty = null;
this.options.available_operators.not_empty = null;
}
// Temp array to sort operators
var aSortable = [];
for(var sOpIdx in this.options.available_operators)
{
var oOp = this.options.available_operators[sOpIdx];
// Some operator can be disabled by the derivated widget, so we check it.
if(oOp !== null && oOp.rank !== false)
{
aSortable.push([sOpIdx, oOp.rank]);
}
}
// Sort the array
aSortable.sort(function(a, b){
return a[1] - b[1];
})
// Populate this.operators
for(var iIdx in aSortable)
{
var sOpIdx = aSortable[iIdx][0];
this.operators[sOpIdx] = this.options.available_operators[sOpIdx];
}
// Fallback operator in case the current operator is not available. Should not happen.
if(this.operators[this.options.operator] === undefined)
{
this.options.operator = Object.keys(this.operators)[0];
}
},
// - Bind external events
_bindEvents: function()
{
var me = this;
// Get criteria data
this.element.on('itop.search.criteria.get_data', function(oEvent, oData){
return me._onGetData(oData);
});
// Get/SetCurrentValues callbacks handler
this.element.on('itop.search.criteria.get_current_values itop.search.criteria.set_current_values', function(oEvent, oData){
oEvent.stopPropagation();
var callback = me.options[oEvent.type+'_callback'];
if(typeof callback === 'string')
{
return me[callback](oEvent, oData);
}
else if(typeof callback === 'function')
{
return callback(me, oEvent, oData);
}
else
{
console.log('search form criteria: callback type must be a function or a existing function name of the widget');
return false;
}
});
// Close criteria
this.element.on('itop.search.criteria.close', function(){
return me._close();
});
},
// - Cinematic
// - Open / Close criteria
_open: function()
{
// Inform handler that a criteria is opening
this.handler.triggerHandler('itop.search.criteria.opening');
// Open criteria
this._resetOperators();
// - Open it first
this.element.addClass('opened');
// - Then only check if more menu is to close to the right side (otherwise we might not have the right element's position)
var iFormWidth = this.element.closest('.search_form_handler').outerWidth();
var iFormLeftPos = this.element.closest('.search_form_handler').offset().left;
var iContentWidth = this.element.find('.sfc_form_group').outerWidth();
var iContentLeftPos = this.element.find('.sfc_form_group').offset().left;
if( (iContentWidth + iContentLeftPos) > (iFormWidth + iFormLeftPos - 10 /* Security margin */) )
{
this.element.addClass('opened_left');
}
// Focus on right input
var oOpElemRadioChecked = this.element.find('.sfc_fg_operator .sfc_op_radio:checked');
var oOpElemInputFirst = oOpElemRadioChecked.closest('.sfc_fg_operator').find('.sfc_op_content input[type="text"]:first');
oOpElemInputFirst.filter(':not([data-no-auto-focus])').trigger('click').trigger('focus');
this.element.find('.sfc_form_group').removeClass('advanced');
if (!oOpElemInputFirst.is(':visible'))
{
this.element.find('.sfc_form_group').addClass('advanced');
}
},
_close: function()
{
this.element.removeClass('opened_left');
this.element.removeClass('opened');
this._unmarkAsDraft();
},
_closeAll: function()
{
this.element.closest('.search_form_handler').find('.search_form_criteria').each(function(){
$(this).triggerHandler('itop.search.criteria.close');
});
},
_remove: function()
{
this.element.remove();
this.handler.triggerHandler('itop.search.criteria.removed');
},
// - Mark / Unmark criteria as draft (new value not applied)
_markAsDraft: function()
{
this.element.addClass('draft');
},
_unmarkAsDraft: function()
{
this.element.removeClass('draft');
},
// - Apply / Cancel new value
_apply: function()
{
// Find active operator
var oActiveOpElem = this.element.find('.sfc_op_radio:checked').closest('.sfc_fg_operator');
if(oActiveOpElem.length === 0)
{
this._trace('Could not apply new value as there seems to be no active operator.');
return false;
}
// Get value from operator (polymorphic method)
var sCallback = '_get' + this._toCamelCase(oActiveOpElem.attr('data-operator-code')) + 'OperatorValues';
if(this[sCallback] === undefined)
{
this._trace('Callback ' + sCallback + ' is undefined, using _getOperatorValues instead.');
sCallback = '_getOperatorValues';
}
var aValues = this[sCallback](oActiveOpElem);
// Update widget
this.options.operator = oActiveOpElem.find('.sfc_op_radio').val();
// TODO: Better modification check. The previous if has been removed as it caused a regression.
this.is_modified = true;
this.options.oql = '';
this.options.values = aValues;
this._setTitle();
this._unmarkAsDraft();
// Trigger event to handler
this.handler.triggerHandler('itop.search.criteria.value_changed');
},
// Event callbacks
// - Internal events
_onButtonApply: function()
{
this._apply();
this._close();
},
_onButtonCancel: function()
{
this._close();
},
_onButtonMore: function()
{
this.element.find('.sfc_form_group').addClass('advanced');
},
_onButtonLess: function()
{
this.element.find('.sfc_form_group').removeClass('advanced');
},
// - External events
_onGetData: function(oData)
{
var oCriteriaData = {
'ref': this.options.ref,
'operator': this.options.operator,
'values': this.options.values,
'is_removable': this.options.is_removable,
'oql': this.options.oql,
// Field data
'class': this.options.field.class,
'class_alias': this.options.field.class_alias,
'code': this.options.field.code,
'widget': this.options.field.widget,
};
return oCriteriaData;
},
// DOM element helpers
// - Prepare element DOM structure
_prepareElement: function()
{
var me = this;
// Prepare base DOM structure
this.element
.append('<div class="sfc_header"><div class="sfc_title"></div><span class="sfc_toggle"><a class="fa fa-caret-down" href="#"></a></span></div>')
.append('<div class="sfc_form_group"><div class="sfc_fg_operators"></div><div class="sfc_fg_buttons"></div></div>');
// Bind events
// Note: No event to handle criteria closing when clicking outside of it as it is already handle by the form handler.
// - Toggler
this.element.find('.sfc_toggle, .sfc_title').on('click', function(oEvent){
// Prevent anchor
oEvent.preventDefault();
// First memorize if current criteria is close
var bOpen = !me.element.hasClass('opened');
// Then close every criterion
me._closeAll();
// Finally open current criteria if necessary
if(bOpen === true)
{
me._open();
}
});
this.element.on('keydown', function(oEvent){
// Apply if "enter" key
if(oEvent.key === 'Enter')
{
me._apply();
// Keep criteria open only on Ctrl + Enter.
if(oEvent.ctrlKey === false)
{
me._close();
}
}
// Close if "escape" key
else if(oEvent.key === 'Escape')
{
me._close();
}
});
// Removable / locked decoration
if(this.options.is_removable === true)
{
this.element.find('.sfc_header').append('<span class="sfc_close"><a class="fa fa-times" href="#"></a></span>');
this.element.find('.sfc_close').on('click', function(oEvent){
// Prevent anchor
oEvent.preventDefault();
me._remove();
});
}
else
{
this.element.addClass('locked');
this.element.find('.sfc_header').append('<span class="sfc_locked"><span class="fa fa-lock"></span></span>');
}
// Form group
this._prepareOperators();
this._prepareButtons();
// Fill criteria
// - Title
this._setTitle();
// Init opened to improve UX (toggle & focus in main operator's input)
if(this.options.init_opened === true)
{
this._closeAll();
this._open();
}
},
// - Prepare the available operators for the criteria
// Meant for overloading.
_prepareOperators: function()
{
for(var sOpIdx in this.operators)
{
var oOp = this.operators[sOpIdx];
var sMethod = '_prepare' + this._toCamelCase(oOp.code) + 'Operator';
// Create DOM element from template
var oOpElem = $(this._getOperatorTemplate())
.uniqueId()
.appendTo(this.element.find('.sfc_fg_operators'));
// Prepare operator's base elements
this._prepareOperator(oOpElem, sOpIdx, oOp);
// Prepare operator's specific elements
if(this[sMethod] !== undefined)
{
this[sMethod](oOpElem, sOpIdx, oOp);
}
else
{
this._prepareDefaultOperator(oOpElem, sOpIdx, oOp);
}
}
},
// - Prepare the buttons (DOM and events) for a criteria
_prepareButtons: function()
{
var me = this;
// DOM elements
this.element.find('.sfc_fg_buttons')
.append('<button type="button" name="apply" class="sfc_fg_button sfc_fg_apply">' + Dict.S('UI:Button:Apply') + '</button>')
.append('<button type="button" name="cancel" class="sfc_fg_button sfc_fg_cancel">' + Dict.S('UI:Button:Close') + '</button>')
.append('<button type="button" name="more" class="sfc_fg_button sfc_fg_more">' + Dict.S('UI:Button:More') + '<span class="fa fa-angle-double-down"></span></button>')
.append('<button type="button" name="less" class="sfc_fg_button sfc_fg_less">' + Dict.S('UI:Button:Less') + '<span class="fa fa-angle-double-up"></span></button>');
// Events
this.element.find('.sfc_fg_button').on('click', function(oEvent){
oEvent.preventDefault();
oEvent.stopPropagation();
var sCallback = '_onButton' + me._toCamelCase($(this).attr('name'));
me[sCallback]();
});
},
// - Reset all operators but active one
_resetOperators: function()
{
var me = this;
// Reset all operators
this.element.find('.sfc_fg_operator').each(function(){
var sCallback = '_reset' + me._toCamelCase($(this).attr('data-operator-code')) + 'Operator';
if(me[sCallback] === undefined)
{
sCallback = '_resetOperator';
}
me[sCallback]($(this));
});
// Set value on current operator
var sCurrentOpCode = this.operators[this.options.operator].code;
this.element.find('.sfc_fg_operator[data-operator-code="' + sCurrentOpCode + '"]').each(function(){
// Check radio (we don't use .trigger('click'), otherwise the criteria will be seen as draft.
$(this).find('.sfc_op_radio').prop('checked', true);
// Reset values
var sCallback = '_set' + me._toCamelCase(sCurrentOpCode) + 'OperatorValues';
if(me[sCallback] === undefined)
{
sCallback = '_setOperatorValues';
}
me[sCallback]($(this), me.options.values);
});
},
// - Set the title element
_setTitle: function(sTitle)
{
if(sTitle === undefined)
{
var sOperator = this.operators[this.options.operator].code;
var sDictEntry = 'UI:Search:Criteria:Title:' + this._toCamelCase(this.options.field.widget) + ':' + this._toCamelCase(sOperator);
// Fallback to default widget dict entry if none exists for the current widget
if(Dict.S(sDictEntry) === sDictEntry)
{
sDictEntry = 'UI:Search:Criteria:Title:Default:' + this._toCamelCase(sOperator);
}
sTitle = Dict.Format(sDictEntry, this.options.field.label, this._getValuesAsText());
// Last chande fallback
if(sTitle === sDictEntry)
{
sTitle = this.options.label;
}
}
this.element.find('.sfc_title')
.html(sTitle)
.attr('title', sTitle);
},
// Operators helpers
// - Return a HTML template for operators
_getOperatorTemplate: function()
{
return '<div class="sfc_fg_operator"><label><input type="radio" class="sfc_op_radio" name="operator" /><span class="sfc_op_name"></span><span class="sfc_op_content"></span></label></div>';
},
// Prepare operator's DOM element
// - Base preparation, always called
_prepareOperator: function(oOpElem, sOpIdx, oOp)
{
var me = this;
var sInputId = oOp.code + '_' + oOpElem.attr('id');
// Set radio
oOpElem.find('.sfc_op_radio').val(sOpIdx);
oOpElem.find('.sfc_op_radio').attr('id', sInputId);
// Set label
oOpElem.find('.sfc_op_name').text(oOp.label);
oOpElem.find('> label').attr('for', sInputId);
// Set helper classes
oOpElem.addClass('sfc_fg_operator_' + oOp.code)
.attr('data-operator-code', oOp.code);
// Bind events
// - Check radio button on click and mark criteria as draft
oOpElem.on('click focusin', function(){
var bIsChecked = oOpElem.find('.sfc_op_radio').prop('checked');
if(bIsChecked === false)
{
oOpElem.find('.sfc_op_radio').prop('checked', true);
me._markAsDraft();
}
});
},
// - Fallback for operator that has no dedicated callback
_prepareDefaultOperator: function(oOpElem, sOpIdx, oOp)
{
var me = this;
// DOM element
var oOpContentElem = $('<input type="text" />');
oOpContentElem.val(this._getValuesAsText());
// Events
// - Focus input on click (radio, label, ...)
oOpElem.on('click', ':not(input[type="text"], select)', function(oEvent) {
// Stopping propagation like this instead of oEvent.stopPropagation() as the event could be used by something.
if ($(oEvent.target).is('input[type="text"], select')) {
return;
}
oOpContentElem.focus();
});
// - Mark as draft on key typing
oOpContentElem.on('keydown', function(oEvent){
me._markAsDraft();
});
oOpElem.find('.sfc_op_content').append(oOpContentElem);
},
_prepareEmptyOperator: function(oOpElem, sOpIdx, oOp)
{
// Do nothing as only the label is necessary
},
_prepareNotEmptyOperator: function(oOpElem, sOpIdx, oOp)
{
// Do nothing as only the label is necessary
},
// Reset operator's state
// - Fallback for operator that has no dedicated callback
_resetOperator: function(oOpElem)
{
oOpElem.find('.sfc_op_content input').val('');
},
// Get operator's values
// - Fallback for operators without a specific callback
_getOperatorValues: function(oOpElem)
{
var aValues = [];
oOpElem.find('.sfc_op_content input').each(function(){
var sValue = $(this).val();
aValues.push({value: sValue, label: sValue});
});
return aValues;
},
// Set operator's values
// - Fallback for operators without a specific callback
_setOperatorValues: function(oOpElem, aValues)
{
if(aValues.length === 0)
{
return false;
}
oOpElem.find('.sfc_op_content input').each(function(){
$(this).val(aValues[0].value);
});
return true;
},
// Values helpers
// - Check if criteria has allowed values either preloaded or through autocomplete
_hasAllowedValues: function()
{
return ( (this.options.field.allowed_values !== undefined) && (this.options.field.allowed_values !== null) );
},
// - Check if criteria has preloaded allowed values (as opposed to autocomplete)
_hasPreloadedAllowedValues: function()
{
if(this._hasAllowedValues() && (this.options.field.allowed_values.values !== undefined) && (this.options.field.allowed_values.values !== null))
{
return true;
}
return false;
},
// - Return the preloaded allowed values (not coming from autocomplete)
_getPreloadedAllowedValues: function()
{
return (this._hasPreloadedAllowedValues()) ? this.options.field.allowed_values.values : {};
},
// - Check if criteria has allowed values that should be loaded through autocomplete
_hasAutocompleteAllowedValues: function()
{
if(this._hasAllowedValues() && (this.options.field.allowed_values.autocomplete === true) )
{
return true;
}
return false;
},
// - Return the allowed values from the autocomplete
_getAutocompleteAllowedValues: function()
{
// Meant for overloading.
},
// - Return current values
_getValues: function()
{
return this.options.values;
},
// - Convert values to a standard string
_getValuesAsText: function(aRawValues)
{
if (aRawValues == undefined)
{
aRawValues = this._getValues();
}
var aValues = [];
for(var iValueIdx in aRawValues)
{
aValues.push(aRawValues[iValueIdx].label);
}
return aValues.join(', ');
},
// - Make an OQL expression from the criteria values and operator
_makeOQLExpression: function()
{
var aValues = [];
var sOQL = '';
for(var iValueIdx in this.options.values)
{
aValues.push( '\'' + this.options.values[iValueIdx].value + '\'' );
}
sOQL += '(`' + this.options.ref + '`) ' + this.options.operator + ' ' + aValues.join(', ') + ')';
return sOQL;
},
// Global helpers
// - Converts a snake_case string to CamelCase
_toCamelCase: function(sString)
{
if( (sString === undefined) || (sString === null) )
{
return sString;
}
var aParts = sString.split('_');
for(var i in aParts)
{
aParts[i] = aParts[i].charAt(0).toUpperCase() + aParts[i].substr(1);
}
return aParts.join('');
},
// - Return if the given keycode is among filtered
_isFilteredKey: function(iKeyCode)
{
return (this.filtered_keys.indexOf(iKeyCode) >= 0);
},
// Debug helpers
// - Show a trace in the javascript console
_trace: function(sMessage, oData)
{
if(window.console)
{
if(oData !== undefined)
{
console.log('Search form criteria: ' + sMessage, oData);
}
else
{
console.log('Search form criteria: ' + sMessage);
}
}
},
// - Show current options
showOptions: function()
{
this._trace('Options', this.options);
}
});
});

View File

@@ -0,0 +1,77 @@
//iTop Search form criteria date
;
$(function()
{
// the widget definition, where 'itop' is the namespace,
// 'search_form_criteria_date' the widget name
$.widget( 'itop.search_form_criteria_date', $.itop.search_form_criteria_date_abstract,
{
// default options
options:
{
// // Overload default operator
// 'operator': 'between_dates',
// // Available operators
// 'available_operators': {
//
// },
aInputsParam: [
{
"code": "from",
"code_uc_first":"From",
"x_picker" : 'datepicker',
"value_index": 0,
"onclose_show" : "until",
},
{
"code": "until",
"code_uc_first":"Until",
"x_picker" : 'datepicker',
"value_index": 1,
}
]
},
// the constructor
_create: function()
{
var me = this;
this._super();
this.element.addClass('search_form_criteria_date');
},
// called when created, and later when changing options
_refresh: function()
{
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function()
{
this.element.removeClass('search_form_criteria_date');
this._super();
},
// _setOptions is called with a hash of all options that are changing
// always refresh when changing options
_setOptions: function()
{
this._superApply(arguments);
},
// _setOption is called for each individual option that is changing
_setOption: function( key, value )
{
this._super( key, value );
},
//------------------
// Inherited methods
//------------------
});
});

View File

@@ -0,0 +1,436 @@
//iTop Search form criteria date_abstract
;
$(function()
{
// the widget definition, where 'itop' is the namespace,
// 'search_form_criteria_date_abstract' the widget name
$.widget( 'itop.search_form_criteria_date_abstract', $.itop.search_form_criteria,
{
// default options
options:
{
// Overload default operator
'operator': 'between_dates',
// Available operators
'available_operators': {
'between_dates': {
'label': Dict.S('UI:Search:Criteria:Operator:Default:Between'),
'code': 'between_days',
'rank': 1,
},
'empty': {
'rank': 700,//pre-existing, reordered
},
'not_empty': {
'rank': 800,//pre-existing, reordered
},
'=': null
},
aInputsParam: [
{
// Common settings :
// "code": "from", => the code used in the HTML
// "code_uc_first":"From", => the code used in the translations
// "onclose_show" : "until", => on x_picker close, should we open another one (on "from" close shall we show "until")
// "value_index": 0, => the widget communicate with an array of values, the index 0 is "from" the index 1 is "until"
// Date_time widget specifi settings :
// "x_picker" : 'datetimepicker', => the plugin used either datepicker or datetimepicker
// "default_time_add": false, => either false to disable it or number of second to add (used by the datetimepicker to choose the right time on synched datepicker change, its value change from 0 for "from" to +1d-1s for "until"
// "show_on_advanced": true, => is the input displaye on "more" or "less" mode advanced is an lais for "more" in the css
// "synced_with": "from_time", => from and until has both two input (datepicker and datetimepicker). each time one input change, the other one has to change
// "getter_code":"from_time" => iTop expect datetime to always provide time, so we must always use the datetimepicker in order to compute the value to be sent to iTop event if whe are in the display mode "less" with datepicker visible
// "getter_suffix": " 00:00:00", => iTop expect datetime to always provide time, so either we use the "getter_code" or we add a suffix to force the time
// "title_getter_code":"from", => if present, the title will be computed base on the given input code. Because the datetime widget title area is not large enought, so we remove the time info, in order to do so we use this.
}
]
},
// Prepare operator's DOM element
_prepareBetweenDaysOperator: function(oOpElem, sOpIdx, oOp)
{
var me = this;
var aInputsParam = me.options.aInputsParam;
var aInputsParamLength = aInputsParam.length;
var aValues = me._getValues();//TODO : tenir compte du refactoring de la structure
var oInputsParamIndexByCode = {};
for (var i = 0; i < aInputsParamLength; i++) {
oInputsParamIndexByCode[aInputsParam[i].code] = i;
}
oContentElem = $(); //will be populated on each loop
for (var i = 0; i < aInputsParamLength; i++) {
var oInputParam = aInputsParam[i];
var oOpContentElem = $('<span class="sfc_op_content_'+oInputParam.code+'_outer '+(oInputParam.show_on_advanced ? 'hide_on_less' : 'hide_on_advanced')+'"><label class="sfc_op_content_'+oInputParam.code+'_label" for=""> '+Dict.S('UI:Search:Criteria:DateTime:'+oInputParam.code_uc_first)+' </label><input type="text" name="'+oInputParam.code+'" placeholder="'+Dict.S('UI:Search:Criteria:DateTime:Placeholder'+oInputParam.code_uc_first)+'"/></span>');
var oInputElem = oOpContentElem
.find('input')
.uniqueId()
//.attr('data-no-auto-focus', true)
;
oOpContentElem
.find('label')
.attr('for', oInputElem.attr('id'))
;
if (oInputParam.value_index in aValues && typeof aValues[oInputParam.value_index].value != 'undefined')
{
oInputElem.val(aValues[oInputParam.value_index].value);
}
oContentElem = oContentElem.add(oOpContentElem);
}
// - Apply on "enter" key hit
//todo: this could be refactored
oOpContentElem.on('keyup', function(oEvent){
// Check operator's radio if not already (typically when focusing in input via "tab" key)
if(oOpElem.find('.sfc_op_radio').prop('checked') === false)
{
oOpElem.find('.sfc_op_radio').prop('checked', true)
}
me._markAsDraft();
// Apply if enter key
if(oEvent.key === 'Enter')
{
me.val(me.val().trim());
me._apply();
}
});
oOpElem
.find('.sfc_op_name')
.remove()
.end()
.find('.sfc_op_content')
.append(oContentElem)
;
// once the inputs are appended into the DOM we can safely use jQuery UI
var fHandleSynchCallback = function(select, bSetDate) {
var selectElem = $(select);
var sSyncedWith = selectElem.data('synced_with');
var oInputParam = selectElem.data('oInputParam');
if (bSetDate)
{
var sDate = selectElem.val().trim();
if ('' == sDate)
{
selectElem[oInputParam.x_picker]('setDate', null);
} else
{
selectElem[oInputParam.x_picker]('setDate', sDate);
}
}
if (sSyncedWith != undefined)
{
var sCode = selectElem.data('code');
var oInputParam = aInputsParam[oInputsParamIndexByCode[sCode]];
var oSyncedInputParam = aInputsParam[oInputsParamIndexByCode[sSyncedWith]];
var oSyncedInputElem = oOpElem.find('input[name="'+sSyncedWith+'"]');
var dSyncedDate = selectElem[oInputParam.x_picker]('getDate');
if (null == dSyncedDate)
{
// oSyncedInputElem.val('');
oSyncedInputElem[oSyncedInputParam.x_picker]('setDate', null);
}
else
{
if (typeof oSyncedInputParam.default_time_add != 'undefined' && oSyncedInputParam.default_time_add)
{
dSyncedDate.setSeconds(dSyncedDate.getSeconds() + oSyncedInputParam.default_time_add);
}
oSyncedInputElem[oSyncedInputParam.x_picker]('setDate', dSyncedDate);
}
}
};
var odatetimepickerOptionsDefault = {
dateFormat: 'yy-mm-dd',
timeFormat: 'HH:mm:ss',
buttonImage: GetAbsoluteUrlAppRoot()+"/images/calendar.png",
// buttonImageOnly: true,
buttonText: "",
showOn:'button',
changeMonth:true,
changeYear:true
};
for (var i = 0; i < aInputsParamLength; i++) {
var oInputParam = aInputsParam[i];
var odatetimepickerOptions = $.extend({}, odatetimepickerOptionsDefault, me.options.datepicker, {
onSelect: function() {
fHandleSynchCallback(this, false);
$(this).focus();
}
});
var oInputElem = oOpElem.find('input[name="'+oInputParam.code+'"]');
oInputElem.data('code', oInputParam.code);
oInputElem.data('onclose_show', oInputParam.onclose_show);
oInputElem.data('oInputParam', oInputParam);
oInputElem.on('keydown', function(oEvent) {
// Synch if "enter" key
if(oEvent.key === 'Enter')
{
fHandleSynchCallback(this, true);
}
});
oInputElem[oInputParam.x_picker](odatetimepickerOptions);
if (typeof aInputsParam[oInputsParamIndexByCode[oInputParam.synced_with]] != 'undefined')
{
var oSyncedInputParam = aInputsParam[oInputsParamIndexByCode[oInputParam.synced_with]];
oInputElem.data('synced_with', oSyncedInputParam.code);
}
}
},
_getBetweenDaysOperatorValues: function(oOpElem)
{
var me = this;
var aValues = [];
var aInputsParam = me.options.aInputsParam;
var aInputsParamLength = aInputsParam.length;
var bAdvancedMode = (me.element.find('.sfc_form_group.advanced').length > 0);
var sValue = '';
var sLabel = '';
for (var i = 0; i < aInputsParamLength; i++) {
var oInputParam = aInputsParam[i];
sLabel = oOpElem.find('input[name="'+oInputParam.code+'"]').val();
if (typeof oInputParam.show_on_advanced == 'undefined' || bAdvancedMode == oInputParam.show_on_advanced)
{
if (typeof oInputParam.getter_code != 'undefined')
{
sValue = oOpElem.find('input[name="'+oInputParam.getter_code+'"]').val();
}
else if (sLabel != "" && typeof oInputParam.getter_suffix != 'undefined')
{
sValue = sLabel + oInputParam.getter_suffix;
}
else
{
sValue = sLabel;
}
aValues[oInputParam.value_index] = {value: sValue, label: sLabel};
}
}
return aValues;
},
_setBetweenDaysOperatorValues: function(oOpElem, aValues)
{
var me = this;
var aInputsParam = me.options.aInputsParam;
var aInputsParamLength = aInputsParam.length;
for (var i = 0; i < aInputsParamLength; i++) {
var oInputParam = aInputsParam[i];
var oInputElem = oOpElem.find('input[name="'+oInputParam.code+'"]');
// oInputElem.val(aValues[0].value);
if (typeof aValues[oInputParam.value_index] != 'undefined' && typeof aValues[oInputParam.value_index].value != 'undefined')
{
var sDate = aValues[oInputParam.value_index].value;
if (sDate.trim() != '')
{
var oDate = new Date(sDate);
oInputElem[oInputParam.x_picker]('setDate', oDate);
}
else
{
oInputElem[oInputParam.x_picker]('setDate', sDate);
}
}
}
},
_resetBetweenDaysOperator: function(oOpElem)
{
this._resetOperator(oOpElem);
},
//------------------
// Inherited methods
//------------------
_setTitle: function(sTitle)
{
var me = this;
if (sTitle === undefined && me.options.operator == 'between_dates')
{
var aValues = me._getValues();
switch (true)
{
case (typeof aValues[0] == 'undefined' && typeof aValues[1] == 'undefined'):
case (typeof aValues[0].label == 'undefined' && typeof aValues[1].label == 'undefined'):
case (aValues[0].label.trim() == '' && aValues[1].label.trim() == ''):
var sDictEntrySuffix = ':All';
break;
case (typeof aValues[0] == 'undefined' ):
case (typeof aValues[0].label == 'undefined' ):
case (aValues[0].label.trim() == '' ):
var sDictEntrySuffix = ':Until';
break;
case (typeof aValues[1] == 'undefined'):
case (typeof aValues[1].label == 'undefined' ):
case (aValues[1].label.trim() == ''):
var sDictEntrySuffix = ':From';
break;
default:
var sDictEntrySuffix = undefined;
break;
}
if (sDictEntrySuffix != undefined)
{
var sDictEntry = 'UI:Search:Criteria:Title:' + this._toCamelCase(this.options.field.widget) + ':' + this._toCamelCase(me.options.operator) + sDictEntrySuffix ;
// Fallback to default widget dict entry if none exists for the current widget
if(Dict.S(sDictEntry) === sDictEntry)
{
sDictEntry = 'UI:Search:Criteria:Title:Default:' + this._toCamelCase(me.options.operator) + sDictEntrySuffix;
}
sTitle = Dict.Format(sDictEntry, this.options.field.label, this._getValuesAsText());
}
}
return me._super(sTitle);
},
// - Convert values to a standard string
_getValuesAsText: function(aRawValues)
{
var me = this;
if (aRawValues == undefined)
{
aRawValues = me._getValues();
}
if (me.options.operator == 'between_dates')
{
aRawValues = aRawValues.slice();//clone
if (typeof aRawValues[1] == 'undefined' || typeof aRawValues[1].label == 'undefined' || aRawValues[1].label == '')
{
aRawValues.splice(1, 1);
}
else
{
aRawValues[1].label = aRawValues[1].label.replace(/(\s\d{2}:\d{2}:\d{2})/, '');
}
if (typeof aRawValues[0] == 'undefined' || typeof aRawValues[0].label == 'undefined' || aRawValues[0].label == '')
{
aRawValues.splice(0, 1);
}
else
{
aRawValues[0].label = aRawValues[0].label.replace(/(\s\d{2}:\d{2}:\d{2})/, '');
}
}
return me._super(aRawValues);
},
// Prepare operator's DOM element
// - Base preparation, always called
_prepareOperator: function(oOpElem, sOpIdx, oOp)
{
var me = this;
if (typeof oOp.dropdown_group == 'undefined')
{
return this._super(oOpElem, sOpIdx, oOp);
}
this._super(oOpElem, sOpIdx, oOp);
oOpElem.addClass('force_hide')
//TODO: move this into the abstract widget
// DOM element
oDropdownElem = this.element.find('select.dropdown_group_'+oOp.dropdown_group);
if (oDropdownElem.length == 0)
{
oDropdownElem = $('<select class="dropdown_group_'+oOp.dropdown_group+'" data-dropdown-group="'+oOp.dropdown_group+'"></select>');
oDropdownElem.on('change', function(){
// $option = $(this);
me.element.find('.sfc_fg_operator_dropdown_group .sfc_op_radio').val(oDropdownElem.val());
oOptionOp = oDropdownElem.data('oOp');
oDropdownElem.attr('data-operator-code', oOptionOp.code);
});
// Create DOM element from template
var oOpElemDropdown = $(this._getOperatorTemplate()).uniqueId();
//todo : if this code is keeped, the radio mustr have an id and the label need to point to it
oOpElemDropdown
.addClass('sfc_fg_operator_dropdown_group')
.attr('data-operator-code', 'dropdown_group')
.find('.sfc_op_name')
.append(oDropdownElem)
.end()
.find('.sfc_op_radio')
.val(sOpIdx)
.end()
.on('click', function(oEvent){
var bIsChecked = oOpElemDropdown.find('.sfc_op_radio').prop('checked');
if(bIsChecked === false)
{
oOpElemDropdown.find('.sfc_op_radio').prop('checked', true);
me._markAsDraft();
}
oOpElemDropdown.find('input[type="text"]:first').focus();
})
.appendTo(this.element.find('.sfc_fg_operators'))
;
this._prepareDefaultOperator(oOpElemDropdown, sOpIdx, oOp);
}
oDropdownElem
.append('<option value="'+sOpIdx+'" >'+oOp.label+'</option>')
.data('oOp', oOp)
;
},
});
});

View File

@@ -0,0 +1,125 @@
//iTop Search form criteria date_time
;
$(function()
{
// the widget definition, where 'itop' is the namespace,
// 'search_form_criteria_date_time' the widget name
$.widget( 'itop.search_form_criteria_date_time', $.itop.search_form_criteria_date_abstract,
{
// default options
options:
{
// // Overload default operator
// 'operator': 'between_dates',
// // Available operators
// 'available_operators': {
//
// },
aInputsParam: [
{
"code": "from",
"code_uc_first":"From",
"x_picker" : 'datepicker',
"default_time_add": false,
"show_on_advanced": false,
"value_index": 0,
"onclose_show" : "until",
"synced_with": "from_time",
//"getter_code":"from_time",
"getter_suffix":" 00:00:00",
},
{
"code": "from_time",
"code_uc_first":"FromTime",
"x_picker" : 'datetimepicker',
"default_time_add": 0,
"show_on_advanced": true,
"value_index": 0,
"onclose_show" : "until_time",
"synced_with": "from",
"title_getter_code":"from",
},
{
"code": "until",
"code_uc_first":"Until",
"x_picker" : 'datepicker',
"default_time_add": false,
"show_on_advanced": false,
"value_index": 1,
"synced_with": "until_time",
//"getter_code":"until_time",
"getter_suffix":" 23:59:59",
},
{
"code": "until_time",
"code_uc_first":"UntilTime",
"x_picker" : 'datetimepicker',
"default_time_add": 86399, // 24 * 60 * 60 - 1
"show_on_advanced": true,
"value_index": 1,
"synced_with": "until",
"title_getter_code":"until",
}
]
},
//------------------
// Inherited methods
//------------------
// the constructor
_create: function()
{
var me = this;
me._super();
me.element.addClass('search_form_criteria_date_time');
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function()
{
var me = this;
me.element.removeClass('search_form_criteria_date_time');
me._super();
},
_prepareBetweenDaysOperator: function(oOpElem, sOpIdx, oOp)
{
var me = this;
var showAvancedOnInit = false;
me._super(oOpElem, sOpIdx, oOp);
for (i = 0; i <= 1; i++) {
if (typeof me.options.values[i] != 'undefined' && typeof me.options.values[i].value != 'undefined')
{
if (me.options.values[i].value.length > 10)
{
if (i == 0 && me.options.values[i].value.indexOf(' 00:00:00') == 10)
{
continue;
}
if (i == 1 && me.options.values[i].value.indexOf(' 23:59:59') == 10)
{
continue;
}
showAvancedOnInit = true;
}
}
}
if (showAvancedOnInit)
{
this.element.find('.sfc_form_group').addClass('advanced');
}
}
});
});

View File

@@ -0,0 +1,707 @@
//iTop Search form criteria enum
;
$(function()
{
// the widget definition, where 'itop' is the namespace,
// 'search_form_criteria_enum' the widget name
$.widget( 'itop.search_form_criteria_enum', $.itop.search_form_criteria,
{
// default options
options:
{
// Overload default operator
'operator': 'IN',
// Available operators
'available_operators': {
'IN': {
'label': Dict.S('UI:Search:Criteria:Operator:Enum:In'),
'code': 'in',
'rank': 10,
},
'=': null, // Remove this one from enum widget.
'empty': null, // Remove as it will be handle by the "null" value in the "IN" operator
'not_empty': null, // Remove as it will be handle by the "null" value in the "IN" operator
},
// Null value
'null_value': {
'code': null,
'label': Dict.S('Enum:Undefined'),
},
// Autocomplete
'autocomplete': {
'xhr_throttle': 200,
'min_autocomplete_chars': 3, // TODO: Pass this through widget instanciation.
},
},
// the constructor
_create: function()
{
var me = this;
this._super();
this.element.addClass('search_form_criteria_enum');
},
// called when created, and later when changing options
_refresh: function()
{
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function()
{
this.element.removeClass('search_form_criteria_enum');
this._super();
},
// _setOptions is called with a hash of all options that are changing
// always refresh when changing options
_setOptions: function()
{
this._superApply(arguments);
},
// _setOption is called for each individual option that is changing
_setOption: function( key, value )
{
this._super( key, value );
},
//------------------
// Inherited methods
//------------------
// - Bind external events
_bindEvents: function()
{
var me = this;
this._super();
// Add selected data
this.element.on('itop.search.criteria_enum.add_selected_values', function(oEvent, oData){
return me._onAddSelectedValues(oData);
});
},
// Events callbacks
_onAddSelectedValues: function(oData)
{
this._addSelectedValues(oData);
//this._apply();
},
// DOM element helpers
// - Prepare element DOM structure
_prepareElement: function()
{
this._super();
// Remove more/less buttons
this.element.find('.sfc_fg_buttons .sfc_fg_more, .sfc_fg_buttons .sfc_fg_less').remove();
},
_prepareInOperator: function(oOpElem, sOpIdx, oOp)
{
var me = this;
// Hide radio & name for now, until there is more than one operator
oOpElem.find('.sfc_op_radio, .sfc_op_name').hide();
// DOM elements
var sOpId = oOpElem.attr('id');
var oOpContentElem = $('<div></div>')
.addClass('sfc_opc_multichoices')
.appendTo(oOpElem.find('.sfc_op_content'));
// - Check / Uncheck all togglers
var sTogglerId = 'toggle_' + sOpId;
var oTogglerElem = $('<div></div>')
.addClass('sfc_opc_mc_toggler')
.append('<label for="' + sTogglerId + '"><input type="checkbox" id="' + sTogglerId + '" />' + Dict.S('UI:Search:Value:Toggler:CheckAllNone') + '</label>')
.appendTo(oOpContentElem);
// - Filter
var sFilterId = 'filter_' + sOpId;
var sFilterPlaceholder = (this._hasAutocompleteAllowedValues()) ? Dict.S('UI:Search:Value:Search:Placeholder') : Dict.S('UI:Search:Value:Filter:Placeholder');
var oFilterElem = $('<div></div>')
.addClass('sf_filter')
.append('<span class="sff_input_wrapper"><input type="text" id="' + sFilterId + '" placeholder="' + sFilterPlaceholder + '" autocomplete="off" /><span class="sff_picto sff_reset fa fa-times"></span></span>')
.appendTo(oOpContentElem);
// - Allowed values
var oAllowedValuesElem = $('<div></div>')
.addClass('sfc_opc_mc_items')
.appendTo(oOpContentElem);
// - Static values: Always there no matter the field constraints
var oStaticListElem = $('<div></div>')
.addClass('sfc_opc_mc_items_list')
.addClass('sfc_opc_mc_items_static')
.appendTo(oAllowedValuesElem);
// - Dynamic values: Depends on the field constraints
var oDynamicListElem = $('<div></div>')
.addClass('sfc_opc_mc_items_list')
.addClass('sfc_opc_mc_items_dynamic')
.appendTo(oAllowedValuesElem);
// - Null value if allowed
// Note: null values is NOT put among the allowed values for two reasons:
// - It must be the first value of the list
// - It is not give by neither the autocomplete or the pre-filled values, so we would need to manually add it in both cases, all operations.
if(this.options.field.is_null_allowed === true)
{
var sValCode = this.options.null_value.code;
var sValLabel = this.options.null_value.label;
var oValueElem = this._makeListItemElement(sValLabel, sValCode);
oValueElem.appendTo(oStaticListElem);
}
// Events
// - Filter
oFilterElem.find('.sff_reset').on('click', function(){
oFilterElem.find('input')
.val('')
// Focus the input for improved UX
.trigger('focus')
// Submit autocomplete with new value
.trigger('itop.search.criteria_enum.autocomplete.submit');
});
// - Check / Uncheck all toggler
oTogglerElem.on('click', function(oEvent){
// Check / uncheck all allowed values
var bChecked = $(this).closest('.sfc_opc_mc_toggler').find('input:checkbox').prop('checked');
oOpContentElem.find('.sfc_opc_mc_item input:checkbox').prop('checked', bChecked);
// Apply criteria
//me._apply();
});
if(this._hasAutocompleteAllowedValues())
{
this._prepareInOperatorWithAutocomplete(oOpElem, sOpIdx, oOp);
}
else
{
this._prepareInOperatorWithoutAutocomplete(oOpElem, sOpIdx, oOp);
}
},
_prepareInOperatorWithoutAutocomplete: function(oOpElem, sOpIdx, oOp)
{
var me = this;
var oOpContentElem = oOpElem.find('.sfc_opc_multichoices');
var oTogglerElem = oOpContentElem.find('.sfc_opc_mc_toggler');
var oFilterElem = oOpContentElem.find('.sf_filter');
var oAllowedValuesElem = oOpContentElem.find('.sfc_opc_mc_items');
var oDynamicListElem = oOpContentElem.find('.sfc_opc_mc_items_dynamic');
// DOM elements
// - Filter
oFilterElem.find('.sff_input_wrapper')
.append('<span class="sff_picto sff_filter fa fa-filter"></span>');
// - Allowed values
var aSortedValues = this._sortValuesByLabel(this._getPreloadedAllowedValues());
for (var i in aSortedValues)
{
var sValCode = aSortedValues[i][0];
var sValLabel = aSortedValues[i][1];
var oValueElem = this._makeListItemElement(sValLabel, sValCode);
oValueElem.appendTo(oDynamicListElem);
if (this._isSelectedValues(sValCode))
{
oValueElem.find(':checkbox').prop('checked', true);
}
}
// Events
// - Filter
// Note: "keyup" event is use instead of "keydown", otherwise, the input value would not be set yet.
oFilterElem.find('input').on('keyup focus', function(oEvent){
// TODO: Move on values with up and down arrow keys; select with space or enter.
var sQuery = $(this).val();
if(sQuery === '')
{
oOpContentElem.find('.sfc_opc_mc_item').show();
oFilterElem.find('.sff_filter').show();
oFilterElem.find('.sff_reset').hide();
}
else
{
oOpContentElem.find('.sfc_opc_mc_item').each(function(){
var oRegExp = new RegExp(sQuery, 'ig');
var sValue = $(this).find('input').val();
var sLabel = $(this).text();
if( (sValue.match(oRegExp) !== null) || (sLabel.match(oRegExp) !== null) )
{
$(this).show();
}
else
{
$(this).hide();
}
});
oFilterElem.find('.sff_filter').hide();
oFilterElem.find('.sff_reset').show();
}
});
oFilterElem.find('.sff_filter').on('click', function(){
oFilterElem.find('input').trigger('focus');
});
// - Apply on check
oAllowedValuesElem.on('click', '.sfc_opc_mc_item input', function(oEvent){
// Prevent propagation, otherwise there will be multiple "_apply()"
oEvent.stopPropagation();
// Uncheck toggler
oTogglerElem.find('input:checkbox').prop('checked', false);
// Apply criteria
//me._apply();
});
},
_prepareInOperatorWithAutocomplete: function(oOpElem, sOpIdx, oOp)
{
var me = this;
var oOpContentElem = oOpElem.find('.sfc_opc_multichoices');
var oTogglerElem = oOpContentElem.find('.sfc_opc_mc_toggler');
var oFilterElem = oOpContentElem.find('.sf_filter');
var oAllowedValuesElem = oOpContentElem.find('.sfc_opc_mc_items');
// DOM
// - Hide toggler for now
oTogglerElem.hide();
// - Set typing hint
this._setACTypingHint();
// - Add search dialog button
oFilterElem
.append('<button type="button" class="sff_search_dialog"><span class=" fa fa-search"></span></button>')
.addClass('sf_with_buttons');
// - Prepare "selected" values area
var oSelectedValuesElem = $('<div></div>')
.addClass('sfc_opc_mc_items')
.append('<div class="sfc_opc_mc_items_list sfc_opc_mc_items_selected"></div>')
.appendTo(oOpContentElem);
this._refreshSelectedValues();
// - External classes
var oFilterIconElem = oFilterElem.find('.sff_search_dialog').uniqueId();
oFilterIconElem.attr('id', oFilterIconElem.attr('id').replace(/-/g, '_'));
var oForeignKeysWidgetCurrent = new SearchFormForeignKeys(
oFilterIconElem.attr('id'), // id
me.options.field.target_class, // sTargetClass
me.options.field.code, // sAttCode
me.element, // oSearchWidgetElmt
'', // sFilter //TODO
me.options.field.label // sTitle
);
window['oForeignKeysWidget'+oFilterIconElem.attr('id')] = oForeignKeysWidgetCurrent;
oForeignKeysWidgetCurrent.Init();
// Events
// - Autocomplete
var oACXHR = null;
var oACTimeout = null;
oFilterElem.find('input').on('keyup itop.search.criteria_enum.autocomplete.submit', function(oEvent){
// TODO: Move on values with up and down arrow keys; select with space or enter.
if(me._isFilteredKey(oEvent.keyCode))
{
return false;
}
var sQuery = $(this).val();
if( (sQuery === '') ) // TODO: Put this back after tests || (sQuery.length < me.options.autocomplete.min_autocomplete_chars) )
{
me._setACTypingHint();
oFilterElem.find('.sff_reset').hide();
}
else
{
// Show loader
me._setACWaitHint();
clearTimeout(oACTimeout);
oACTimeout = setTimeout(function(){
if(oACXHR !== null)
{
oACXHR.abort();
}
oACXHR = $.post(
AddAppContext(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php'),
{
sTargetClass: me.options.field.target_class,
sFilter: 'SELECT ' + me.options.field.target_class,
q: sQuery,
bSearchMode: 'true',
sOutputFormat: 'json',
operation: 'ac_extkey',
}
)
.done(function(oResponse, sStatus, oXHR){ me._onACSearchSuccess(oResponse); })
.fail(function(oResponse, sStatus, oXHR){ me._onACSearchFail(oResponse, sStatus); })
.always(function(oResponse, sStatus, oXHR){ me._onACSearchAlways(); });
oFilterElem.find('.sff_reset').show();
}, me.options.autocomplete.xhr_throttle);
}
});
// - Apply on check
oAllowedValuesElem.on('click', '.sfc_opc_mc_item input', function(oEvent){
// Prevent propagation, otherwise there will be multiple "_apply()"
oEvent.stopPropagation();
var oItemElem = $(this).closest('.sfc_opc_mc_item');
// Hide item
oItemElem.hide();
// Copy item to selected items list
var oValues = {};
oValues[oItemElem.find('input:checkbox').val()] = oItemElem.text();
me._addSelectedValues(oValues);
// Apply criteria
//me._apply();
});
// - Apply on uncheck
oSelectedValuesElem.on('click', '.sfc_opc_mc_item', function(oEvent){
// Prevent propagation, otherwise there will be multiple "_apply()"
oEvent.stopPropagation();
// Show item among allowed values (if still there, could have been removed bu another search needle)
var oAllowedValueElem = oAllowedValuesElem.find('.sfc_opc_mc_item[data-value-code="' + $(this).attr('data-value-code') + '"]');
if(oAllowedValueElem.length > 0)
{
oAllowedValuesElem.find('.sfc_opc_mc_item[data-value-code="' + $(this).attr('data-value-code') + '"]')
.show()
.find('input:checkbox')
.prop('checked', false);
}
// Remove item from selected values
$(this).remove();
me._refreshSelectedValues();
// Apply criteria
//me._apply();
});
// - Open search dialog
oFilterElem.find('.sff_search_dialog').on('click', function(){
oForeignKeysWidgetCurrent.ShowModalSearchForeignKeys();
});
},
_setTitle: function(sTitle)
{
var iValLimit = 3;
var iValCount = Object.keys(this.options.values).length;
var iAllowedValuesCount = Object.keys(this._getPreloadedAllowedValues()).length;
// Manually increase allowed values count if null is allowed
if( (this.options.field.is_null_allowed === true) && (this._hasAutocompleteAllowedValues() === false) )
{
iAllowedValuesCount++;
}
// Making right tite regarding the number of selected values
if( (iValCount === 0) || (iValCount === iAllowedValuesCount) )
{
sTitle = Dict.Format('UI:Search:Criteria:Title:Enum:In:All', this.options.field.label);
}
else if(iValCount > iValLimit)
{
var aFirstValues = [];
for(var i=0; i<iValLimit-1; i++)
{
aFirstValues.push(this.options.values[i].label);
}
sTitle = Dict.Format('UI:Search:Criteria:Title:Enum:In:Many', this.options.field.label, aFirstValues.join(', '), (iValCount - iValLimit+1));
}
this._super(sTitle);
},
// Operators helpers
// Reset operator's state
_resetInOperator: function(oOpElem)
{
// Uncheck toggler
oOpElem.find('sfc_opc_mc_toggler input').prop('checked', false);
// Clear filter
oOpElem.find('sfc_opc_mc_filter input').val('');
},
// Get operator's values
_getInOperatorValues: function(oOpElem)
{
var aValues = [];
var sValuesSelector = this._getSelectedValuesWrapperSelector();
oOpElem.find(sValuesSelector).find('.sfc_opc_mc_item input:checked').each(function(iIdx, oElem){
var sValue = $(oElem).val();
var sLabel = $(oElem).parent().text();
aValues.push({value: sValue, label: sLabel});
});
return aValues;
},
// Set operator's values
_setInOperatorValues: function(oOpElem, aValues)
{
if(aValues.length === 0)
{
return false;
}
// Uncheck all allowed values
oOpElem.find('.sfc_opc_mc_item input').prop('checked', false);
// Re-check allowed values from param
for(var iIdx in aValues)
{
if(oOpElem.find('.sfc_opc_mc_item[data-value-code="' + aValues[iIdx].value + '"]').length > 0)
{
oOpElem.find('.sfc_opc_mc_item[data-value-code="' + aValues[iIdx].value + '"] input')
.prop('checked', true);
}
else
{
var oItemElem = this._makeListItemElement(aValues[iIdx].label, aValues[iIdx].value, true);
oItemElem.appendTo(this._getSelectedValuesWrapperSelector());
}
}
this._refreshSelectedValues();
return true;
},
// Autocomplete helpers
_setACHint: function(sHint)
{;
this.element.find('.sfc_opc_mc_items_dynamic').html('<div class="sfc_opc_mc_placeholder">' + sHint + '</div>');
},
_setACTypingHint: function()
{
this._setACHint(Dict.S('UI:Search:Value:Autocomplete:StartTyping'));
},
_setACWaitHint: function()
{
this._setACHint(Dict.S('UI:Search:Value:Autocomplete:Wait'));
},
_setACNoResultHint: function()
{
this._setACHint(Dict.S('UI:Search:Value:Autocomplete:NoResult'));
},
// Autocomplete callbacks
_onACSearchSuccess: function(oResponse)
{
if(typeof oResponse !== 'object')
{
this._onACSearchFail(oResponse, 'unexcepted');
return false;
}
var oDynamicListElem = this.element.find('.sfc_opc_mc_items_dynamic')
.html('');
if(Object.keys(oResponse).length > 0)
{
// Note: Response is indexed by labels from server so the JSON is always ordered on decoding.
for(var sLabel in oResponse)
{
var sValue = oResponse[sLabel];
var bInitChecked = this._isSelectedValues(sValue);
var bInitHidden = this._isSelectedValues(sValue);
var oValueElem = this._makeListItemElement(sLabel, sValue, bInitChecked, bInitHidden);
oValueElem.appendTo(oDynamicListElem);
}
}
else
{
this._setACNoResultHint();
}
},
_onACSearchFail: function(oResponse, sStatus)
{
if(sStatus !== 'abort')
{
var sErrorMessage = Dict.Format('Error:XHR:Fail', '');
this._setACHint('=/');
this.handler.triggerHandler('itop.search.criteria.error_occured', sErrorMessage);
}
},
_onACSearchAlways: function()
{
},
// Value helpers
// - Return the selector fo the element containing the selected values
_getSelectedValuesWrapperSelector: function()
{
return (this._hasAutocompleteAllowedValues()) ? '.sfc_opc_mc_items_selected' : '.sfc_opc_mc_items_static, .sfc_opc_mc_items_dynamic';
},
// - Return true if sValue is among the selected values "codes"
_isSelectedValues: function(sValue)
{
var bFound = false;
for(var iValIdx in this.options.values)
{
// Note: Mind the double "=" instead of triple as the .value can be either string or integer whereas the sValue is always string
if(this.options.values[iValIdx].value == sValue)
{
bFound = true;
break;
}
}
return bFound;
},
// - Return true if sLabel is among the selected values "labels"
_isSelectedLabels: function(sLabel)
{
var bFound = false;
for(var iValIdx in this.options.values)
{
// Note: Mind the double "=" instead of triple as the .label can be either string or integer whereas the sLabel is always string
if(this.options.values[iValIdx].label == sLabel)
{
bFound = true;
break;
}
}
return bFound;
},
// - Add oValues to the list of selected values
_addSelectedValues: function(oValues)
{
var oSelectedValuesElem = this.element.find(this._getSelectedValuesWrapperSelector());
for(var sValue in oValues)
{
// Add only if not already selected
if(this._isSelectedValues(sValue))
{
continue;
}
if(oSelectedValuesElem.find('.sfc_opc_mc_item[data-value-code="' + sValue + '"]').length > 0)
{
oSelectedValuesElem.find('.sfc_opc_mc_item[data-value-code="' + sValue + '"] input')
.prop('checked', true);
}
else
{
var oItemElem = this._makeListItemElement(oValues[sValue], sValue, true);
oItemElem
.appendTo(oSelectedValuesElem)
.effect('highlight', {}, 500);
}
this.options.values.push({
value: sValue,
label: oValues[sValue]
});
}
this._refreshSelectedValues();
},
_refreshSelectedValues: function()
{
// No sense to make this in preloaded mode
if(this._hasAutocompleteAllowedValues() === false)
{
return false;
}
// Show / hide
var oSelectedValuesElem = this.element.find(this._getSelectedValuesWrapperSelector());
if(oSelectedValuesElem.find('.sfc_opc_mc_item').length > 0)
{
oSelectedValuesElem.show();
}
else
{
oSelectedValuesElem.hide();
}
// TODO: Reorder
// oSelectedValuesElem.html('');
// var aSortedValues = this._sortValuesByLabel(this.options.values);
// for(var iIdx in aSortedValues)
// {
// var oItemElem = this._makeListItemElement(aSortedValues[iIdx][1], aSortedValues[iIdx][0], true);
// oItemElem.appendTo(oSelectedValuesElem);
// }
},
// - Return an array of allowed values sorted by labels
_sortValuesByLabel: function(oSource)
{
var aSortable = [];
var bSourceAsObject = $.isPlainObject(oSource);
for (var sKey in oSource) {
// eg. [{value: "3", label: "Demo"}, {value: "2", label: "IT Department"}] in autocomplete mode
if(bSourceAsObject === false)
{
aSortable.push([oSource[sKey].value, oSource[sKey].label]);
}
// eg. {2: "IT Department", 3: "Demo"} in regular mode
else
{
aSortable.push([sKey, oSource[sKey]]);
}
}
aSortable.sort(function(a, b) {
if(a[1] < b[1])
{
return -1;
}
else if(a[1] > b[1])
{
return 1;
}
return 0;
});
return aSortable;
},
// - Make a jQuery element for a list item
_makeListItemElement: function(sLabel, sValue, bInitChecked, bInitHidden)
{
var oItemElem = $('<div></div>')
.addClass('sfc_opc_mc_item')
.attr('data-value-code', sValue)
.append('<label><input type="checkbox" value="'+sValue+'"/>'+sLabel+'</label>');
if(bInitChecked === true)
{
oItemElem.find('input:checkbox').prop('checked', true);
}
if(bInitHidden === true)
{
oItemElem.hide();
}
return oItemElem;
},
});
});

View File

@@ -0,0 +1,25 @@
//iTop Search form criteria external_field
;
$(function () {
// the widget definition, where 'itop' is the namespace,
// 'search_form_criteria_external_field' the widget name
$.widget('itop.search_form_criteria_external_field', $.itop.search_form_criteria_string,
{
// the constructor
_create: function () {
this._super();
this.element.addClass('search_form_criteria_external_field');
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function () {
this.element.removeClass('search_form_criteria_external_field');
this._super();
},
//------------------
// Inherited methods
//------------------
});
});

View File

@@ -0,0 +1,56 @@
//iTop Search form criteria external key
;
$(function()
{
// the widget definition, where 'itop' is the namespace,
// 'search_form_criteria_external_key' the widget name
$.widget( 'itop.search_form_criteria_external_key', $.itop.search_form_criteria_enum,
{
// default options
options:
{
// Null value
'null_value': {
'code': 0,
'label': Dict.S('UI:UndefinedObject'),
},
},
// the constructor
_create: function()
{
var me = this;
this._super();
this.element.addClass('search_form_criteria_external_key');
},
// called when created, and later when changing options
_refresh: function()
{
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function()
{
this.element.removeClass('search_form_criteria_external_key');
this._super();
},
// _setOptions is called with a hash of all options that are changing
// always refresh when changing options
_setOptions: function()
{
this._superApply(arguments);
},
// _setOption is called for each individual option that is changing
_setOption: function( key, value )
{
this._super( key, value );
},
//------------------
// Inherited methods
//------------------
});
});

View File

@@ -0,0 +1,73 @@
//iTop Search form criteria hierarchical key
;
$(function()
{
// the widget definition, where 'itop' is the namespace,
// 'search_form_criteria_hierarchical_key' the widget name
$.widget( 'itop.search_form_criteria_hierarchical_key', $.itop.search_form_criteria_enum,
{
// default options
options:
{
},
// the constructor
_create: function()
{
var me = this;
this._super();
this.element.addClass('search_form_criteria_hierarchical_key');
},
// called when created, and later when changing options
_refresh: function()
{
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function()
{
this.element.removeClass('search_form_criteria_hierarchical_key');
this._super();
},
// _setOptions is called with a hash of all options that are changing
// always refresh when changing options
_setOptions: function()
{
this._superApply(arguments);
},
// _setOption is called for each individual option that is changing
_setOption: function( key, value )
{
this._super( key, value );
},
//------------------
// Inherited methods
//------------------
// DOM element helpers
prepareInOperator: function(oOpElem, sOpIdx, oOp)
{
var me = this;
this._super();
// Note: Hierarchical key is on stand by for now.
// // DOM elements
// // - Add search dialog button
// this.element.find('.sf_filter')
// .append('<button type="button" class="sff_hierarchy_dialog"><span class=" fa fa-sitemap"></span></button>')
// .addClass('sf_with_buttons');
//
// // Events
// // - Open hierarchy dialog
// this.element.find('.sff_hierarchy_dialog').on('click', function(){
// // TODO: Open hierarchy dialog with right params
// alert('Not implemented yet');
// });
},
});
});

View File

@@ -0,0 +1,336 @@
//iTop Search form criteria numeric
;
$(function()
{
// the widget definition, where 'itop' is the namespace,
// 'search_form_criteria_numeric' the widget name
$.widget( 'itop.search_form_criteria_numeric', $.itop.search_form_criteria,
{
// default options
options:
{
// Overload default operator
'operator': '=',
// Available operators
'available_operators': {
'=': {
'label': Dict.S('UI:Search:Criteria:Operator:Numeric:Equals'),//pre-existing, label changed
// 'dropdown_group':1,
},
'>': {
'label': Dict.S('UI:Search:Criteria:Operator:Numeric:GreaterThan'),
'code': 'greater_than',
'rank': 100,
// 'dropdown_group':1,
},
'>=': {
'label': Dict.S('UI:Search:Criteria:Operator:Numeric:GreaterThanOrEquals'),
'code': 'greater_than_or_equals',
'rank': 200,
// 'dropdown_group':1,
},
'<': {
'label': Dict.S('UI:Search:Criteria:Operator:Numeric:LessThan'),
'code': 'less_than',
'rank': 300,
// 'dropdown_group':1,
},
'<=': {
'label': Dict.S('UI:Search:Criteria:Operator:Numeric:LessThanOrEquals'),
'code': 'less_than_or_equals',
'rank': 400,
// 'dropdown_group':1,
},
'!=': {
'label': Dict.S('UI:Search:Criteria:Operator:Numeric:Different'),
'code': 'different',
'rank': 500,
// 'dropdown_group':1,
},
'between': {
'label': Dict.S('UI:Search:Criteria:Operator:Numeric:Between'),
'code': 'between',
'rank': 600,
},
'empty': {
'rank': 700,//pre-existing, reordered
},
'not_empty': {
'rank': 800,//pre-existing, reordered
},
},
},
// the constructor
_create: function()
{
var me = this;
this._super();
this.element.addClass('search_form_criteria_numeric');
},
// called when created, and later when changing options
_refresh: function()
{
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function()
{
this.element.removeClass('search_form_criteria_numeric');
this._super();
},
// _setOptions is called with a hash of all options that are changing
// always refresh when changing options
_setOptions: function()
{
this._superApply(arguments);
},
// _setOption is called for each individual option that is changing
_setOption: function( key, value )
{
this._super( key, value );
},
// Prepare operator's DOM element
_prepareBetweenOperator: function(oOpElem, sOpIdx, oOp)
{
var me = this;
aValues = me._getValues();//TODO : tenir compte du refactoring de la structure
// DOM elements
var oOpContentOuterElemFrom = $('<div class="sfc_op_content_from_outer"><label class="sfc_op_content_from_label" for=""> '+Dict.S('UI:Search:Criteria:Numeric:From')+' </label><input type="text" name="from" placeholder="'+Dict.S('UI:Search:Criteria:Numeric:PlaceholderFrom')+'"/></div>');
var oOpContentElemFrom = oOpContentOuterElemFrom.find('input').uniqueId();
oOpContentOuterElemFrom.find('label').attr('for', oOpContentElemFrom.attr('id'));
if (typeof aValues[0] != 'undefined' && typeof aValues[0].value != 'undefined')
{
oOpContentElemFrom.val(aValues[0].value);
}
var oOpContentOuterElemUntil = $('<div class="sfc_op_content_until_outer"><label class="sfc_op_content_until_label" for=""> '+Dict.S('UI:Search:Criteria:Numeric:Until')+' </label><input type="text" name="until" placeholder="'+Dict.S('UI:Search:Criteria:Numeric:PlaceholderUntil')+'"/></div>');
var oOpContentElemUntil = oOpContentOuterElemUntil.find('input').uniqueId();
oOpContentOuterElemUntil.find('label').attr('for', oOpContentElemUntil.attr('id'));
if (typeof aValues[1] != 'undefined' && typeof aValues[1].value != 'undefined')
{
oOpContentElemUntil.val(aValues[1].value);
}
oOpContentElem = $().add(oOpContentOuterElemFrom).add(oOpContentOuterElemUntil);
// Events
// - Focus input on click (radio, label, ...)
oOpElem.on('click', function(oEvent){
if ($(oEvent.target).is('input[type="text"], select')) {
return;
}
oOpContentElemFrom.focus();
});
// - Apply on "enter" key hit
// TODO: this could be refactored
oOpContentElem.on('keydown', function(oEvent){
me._markAsDraft();
});
oOpElem
.find('.sfc_op_name')
.remove()
.end()
.find('.sfc_op_content')
.append(oOpContentElem)
;
},
_getBetweenOperatorValues: function(oOpElem)
{
var aValues = [];
var sValueFrom = oOpElem.find('.sfc_op_content input[name="from"]').val();
var sValueUntil = oOpElem.find('.sfc_op_content input[name="until"]').val();
aValues.push({value: sValueFrom, label: sValueFrom});
aValues.push({value: sValueUntil, label: sValueUntil});
return aValues;
},
_setBetweenOperatorValues: function(oOpElem, aValues)
{
switch (aValues.length)
{
case 2:
oOpElem.find('.sfc_op_content input[name="until"]').val(aValues[1].value);
//NO BREAK!!!
case 1:
oOpElem.find('.sfc_op_content input[name="from"]').val(aValues[0].value);
break;
default:
return false;
}
return true;
},
_resetBetweenOperator: function(oOpElem)
{
this._resetOperator(oOpElem);
},
//------------------
// Inherited methods
//------------------
_setTitle: function(sTitle)
{
var me = this;
if (sTitle === undefined && me.options.operator == 'between')
{
var aValues = me._getValues();
switch (true)
{
case (typeof aValues[0] == 'undefined' && typeof aValues[1] == 'undefined'):
var sDictEntrySuffix = ':All';
break;
case (typeof aValues[0] == 'undefined' ):
var sDictEntrySuffix = ':Until';
break;
case (typeof aValues[1] == 'undefined'):
var sDictEntrySuffix = ':From';
break;
case (typeof aValues[0].label == 'undefined' && typeof aValues[1].label == 'undefined'):
var sDictEntrySuffix = ':All';
break;
case (typeof aValues[0].label == 'undefined' ):
var sDictEntrySuffix = ':Until';
break;
case (typeof aValues[1].label == 'undefined' ):
var sDictEntrySuffix = ':From';
break;
case ((typeof aValues[0].label == 'string' && aValues[0].label.trim() == '') && (typeof aValues[1].label == 'string' && aValues[1].label.trim() == '')):
var sDictEntrySuffix = ':All';
break;
case (typeof aValues[0].label == 'string' && aValues[0].label.trim() == ''):
var sDictEntrySuffix = ':Until';
break;
case (typeof aValues[1].label == 'string' && aValues[1].label.trim() == ''):
var sDictEntrySuffix = ':From';
break;
default:
var sDictEntrySuffix = undefined;
break;
}
if (sDictEntrySuffix != undefined)
{
var sDictEntry = 'UI:Search:Criteria:Title:' + this._toCamelCase(this.options.field.widget) + ':' + this._toCamelCase(me.options.operator) + sDictEntrySuffix ;
// Fallback to default widget dict entry if none exists for the current widget
if(Dict.S(sDictEntry) === sDictEntry)
{
sDictEntry = 'UI:Search:Criteria:Title:Default:' + this._toCamelCase(me.options.operator) + sDictEntrySuffix;
}
sTitle = Dict.Format(sDictEntry, this.options.field.label, this._getValuesAsText());
}
}
return me._super(sTitle);
},
// - Convert values to a standard string
_getValuesAsText: function(aRawValues)
{
var me = this;
if (aRawValues == undefined)
{
aRawValues = me._getValues();
}
if (me.options.operator == 'between')
{
aRawValues = aRawValues.slice(); //clone
if (typeof aRawValues[1] == 'undefined' || typeof aRawValues[1].label == 'undefined' || (typeof aRawValues[1].label == 'string' && aRawValues[1].label.trim() == ''))
{
aRawValues.splice(1, 1);
}
if (typeof aRawValues[0] == 'undefined' || typeof aRawValues[0].label == 'undefined' || (typeof aRawValues[0].label == 'string' && aRawValues[0].label.trim() == ''))
{
aRawValues.splice(0, 1);
}
}
return me._super(aRawValues);
},
// Prepare operator's DOM element
// - Base preparation, always called
_prepareOperator: function(oOpElem, sOpIdx, oOp)
{
var me = this;
if (typeof oOp.dropdown_group == 'undefined')
{
return this._super(oOpElem, sOpIdx, oOp);
}
this._super(oOpElem, sOpIdx, oOp);
oOpElem.addClass('force_hide')
//TODO: Move this into the abstract widget
// DOM element
oDropdownElem = this.element.find('select.dropdown_group_'+oOp.dropdown_group);
if (oDropdownElem.length == 0)
{
oDropdownElem = $('<select class="dropdown_group_'+oOp.dropdown_group+'" data-dropdown-group="'+oOp.dropdown_group+'"></select>');
oDropdownElem.on('change', function(){
// $option = $(this);
me.element.find('.sfc_fg_operator_dropdown_group .sfc_op_radio').val(oDropdownElem.val());
oOptionOp = oDropdownElem.data('oOp');
oDropdownElem.attr('data-operator-code', oOptionOp.code);
});
// Create DOM element from template
var oOpElemDropdown = $(this._getOperatorTemplate()).uniqueId();
// TODO: If this code is keeped, the radio must have an id and the label need to point to it
oOpElemDropdown
.addClass('sfc_fg_operator_dropdown_group')
.attr('data-operator-code', 'dropdown_group')
.find('.sfc_op_name')
.append(oDropdownElem)
.end()
.find('.sfc_op_radio')
.val(sOpIdx)
.end()
.on('click', function(oEvent){
//oOpElemDropdown.find('input[type="text"]:first').focus();
})
.appendTo(this.element.find('.sfc_fg_operators'))
;
this._prepareDefaultOperator(oOpElemDropdown, sOpIdx, oOp);
}
oDropdownElem
.append('<option value="'+sOpIdx+'" >'+oOp.label+'</option>')
.data('oOp', oOp)
;
},
});
});

View File

@@ -0,0 +1,80 @@
//iTop Search form criteria raw
;
$(function()
{
// the widget definition, where 'itop' is the namespace,
// 'search_form_criteria_raw' the widget name
$.widget( 'itop.search_form_criteria_raw', $.itop.search_form_criteria,
{
// default options
options:
{
'label': '', // Computed by server
},
// the constructor
_create: function()
{
var me = this;
this._super();
this.element.addClass('search_form_criteria_raw');
},
// called when created, and later when changing options
_refresh: function()
{
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function()
{
this.element.removeClass('search_form_criteria_raw');
this._super();
},
// _setOptions is called with a hash of all options that are changing
// always refresh when changing options
_setOptions: function()
{
this._superApply(arguments);
},
// _setOption is called for each individual option that is changing
_setOption: function( key, value )
{
this._super( key, value );
},
//------------------
// Inherited methods
//------------------
// DOM element helpers
_prepareElement: function()
{
this._super();
// Remove toggler as it's a non sense here
this.element.find('.sfc_toggle').remove();
this.element.find('.sfc_toggle, .sfc_title').off('click');
// Force close as it has no sense either
this._close();
},
_prepareOperators: function()
{
// Overloading function and doing nothing for this special kind of criteria.
},
_prepareButtons: function()
{
// Overloading function and doing nothing for this special kind of criteria.
},
_setTitle: function(sTitle)
{
if(sTitle === undefined)
{
sTitle = this.options.label;
}
this._super(sTitle);
},
});
});

View File

@@ -0,0 +1,77 @@
//iTop Search form criteria string
;
$(function()
{
// the widget definition, where 'itop' is the namespace,
// 'search_form_criteria_string' the widget name
$.widget( 'itop.search_form_criteria_string', $.itop.search_form_criteria,
{
// default options
options:
{
// Overload default operator
'operator': 'contains',
// Available operators
'available_operators': {
'contains': {
'label': Dict.S('UI:Search:Criteria:Operator:String:Contains'),
'code': 'contains',
'rank': 10,
},
'starts_with': {
'label': Dict.S('UI:Search:Criteria:Operator:String:StartsWith'),
'code': 'starts_with',
'rank': 20,
},
'ends_with': {
'label': Dict.S('UI:Search:Criteria:Operator:String:EndsWith'),
'code': 'ends_with',
'rank': 30,
},
'REGEXP': {
'label': Dict.S('UI:Search:Criteria:Operator:String:RegExp'),
'code': 'reg_exp',
'rank': 40,
},
'=': null, // Remove this one from string widget.
},
},
// the constructor
_create: function()
{
var me = this;
this._super();
this.element.addClass('search_form_criteria_string');
},
// called when created, and later when changing options
_refresh: function()
{
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function()
{
this.element.removeClass('search_form_criteria_string');
this._super();
},
// _setOptions is called with a hash of all options that are changing
// always refresh when changing options
_setOptions: function()
{
this._superApply(arguments);
},
// _setOption is called for each individual option that is changing
_setOption: function( key, value )
{
this._super( key, value );
},
//------------------
// Inherited methods
//------------------
});
});

View File

@@ -0,0 +1,963 @@
//iTop Search form handler
;
$(function()
{
// the widget definition, where 'itop' is the namespace,
// 'search_form_handler' the widget name
$.widget( 'itop.search_form_handler',
{
// default options
options:
{
'criterion_outer_selector': null,
'result_list_outer_selector': null,
'data_config_list_selector': null,
'submit_button_selector': null,
'hide_initial_criterion': false, // TODO: What is that?
'endpoint': null,
'init_opened': false,
"datepicker":
{
"dayNamesMin": ["Su","Mo","Tu","We","Th","Fr","Sa"],
"monthNamesShort": ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],
"firstDay": 0
},
'search': {
'base_oql': '',
'class_name': null,
'criterion': [
// Structure
// {
// 'or': [
// {
// 'and': [
// {
// 'ref': 'alias.code',
// 'operator': 'contains',
// 'values': [
// {
// 'value': 'foo',
// 'label': 'bar',
// }
// ],
// 'is_removable': true,
// 'oql': '',
// },
// ]
// },
// ]
// },
],
'fields': [
// Structure
// 'zlist': {
// 'alias.code': {
// 'class_alias': '',
// 'class': '',
// 'code': '',
// 'label': '',
// 'type': '',
// 'allowed_values': {...},
// },
// },
// 'others': {
// 'alias.code': {
// 'class_alias': '',
// 'class': '',
// 'code': '',
// 'label': '',
// 'type': '',
// 'allowed_values': {...},
// },
// },
],
},
'default_criteria_type': 'raw',
},
// jQuery elements
elements: null,
// Submit properties (XHR, throttle, ...)
submit: null,
// the constructor
_create: function()
{
var me = this;
this.element.addClass('search_form_handler');
// Init properties (complexe type properties would be static if not initialized with a simple type variable...)
this.elements = {
message_area: null,
criterion_area: null,
more_criterion: null,
results_area: null,
};
this.submit = {
xhr: null,
};
//init others widgets :
this.element.search_form_handler_history({"itop_root_class":me.options.search.class_name});
// Prepare DOM elements
this._prepareFormArea();
this._prepareCriterionArea();
this._prepareResultsArea();
// Binding buttons
if(this.options.submit_button_selector !== null)
{
$(this.options.submit_button_selector).off('click').on('click', function(oEvent){ me._onSubmitClick(oEvent); });
}
// Binding events (eg. from search_form_criteria widgets)
this._bindEvents();
},
// called when created, and later when changing options
_refresh: function()
{
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function()
{
this.element
.removeClass('search_form_handler');
},
// _setOptions is called with a hash of all options that are changing
// always refresh when changing options
_setOptions: function()
{
this._superApply(arguments);
},
// _setOption is called for each individual option that is changing
_setOption: function( key, value )
{
this._super( key, value );
},
//
_bindEvents: function()
{
var me = this;
// Form events
// - Prevent regular form submission (eg. hitting "Enter" in inputs)
this.element.on('submit', function(oEvent){
oEvent.preventDefault();
});
// - Submit the search form
this.element.on('itop.search.form.submit', function(oEvent, oData){
me._onSubmit();
});
// Criteria events
this.element.on('itop.search.criteria.value_changed', function(oEvent, oData){
me._onCriteriaValueChanged(oData);
});
this.element.on('itop.search.criteria.removed', function(oEvent, oData){
me._onCriteriaRemoved(oData);
});
this.element.on('itop.search.criteria.error_occured', function(oEvent, oData){
me._onCriteriaErrorOccured(oData);
});
},
// - Update search option of the widget
_updateSearch: function()
{
var me = this;
// Criterion
// - Note: As of today, only a "or" level with a "and" is supported, so the following part
// will need some refactoring when introducing new stuff.
var oCriterion = {
'or': [{
'and': []
}]
};
// - Retrieve criterion
this.elements.criterion_area.find('.search_form_criteria').each(function(){
var oCriteriaData = $(this).triggerHandler('itop.search.criteria.get_data');
oCriterion['or'][0]['and'].push(oCriteriaData);
});
// - Update search
this.options.search.criterion = oCriterion;
// No need to update base OQL and fields
},
// - Open / Close more criterion menu
_openMoreCriterion: function()
{
// Open more criterion menu
// - Open it first
this.elements.more_criterion.addClass('opened');
// - Focus filter
this.elements.more_criterion.find('.sf_filter:first input[type="text"]')
.val('')
.focus();
// - Then only check if more menu is to close to the right side (otherwise we might not have the right element's position)
var iFormWidth = this.element.outerWidth();
var iFormLeftPos = this.element.offset().left;
var iMenuWidth = this.elements.more_criterion.find('.sfm_content').outerWidth();
var iMenuLeftPos = this.elements.more_criterion.find('.sfm_content').offset().left;
if( (iMenuWidth + iMenuLeftPos) > (iFormWidth + iFormLeftPos - 10 /* Security margin */) )
{
this.elements.more_criterion.addClass('opened_left');
}
},
_closeMoreCriterion: function()
{
this.elements.more_criterion.removeClass('opened_left');
this.elements.more_criterion.removeClass('opened');
},
_toggleMoreCriterion: function()
{
// Calling methods instead of toggling the class so additional processing are done.
if(this.elements.more_criterion.hasClass('opened'))
{
this._closeMoreCriterion();
}
else
{
this._openMoreCriterion();
}
},
// - Close all criterion
_closeAllCriterion: function()
{
this.elements.criterion_area.find('.search_form_criteria').each(function(){
$(this).triggerHandler('itop.search.criteria.close');
});
},
// DOM helpers
// - Prepare form area
_prepareFormArea: function()
{
var me = this;
// Build DOM elements
// - Message area
this.elements.message_area = this.element.find('.sf_message');
this._cleanMessageArea();
// Events
// - Refresh icon
this.element.find('.sft_refresh').on('click', function(oEvent){
// Prevent anchor
oEvent.preventDefault();
// Prevent form toggling
oEvent.stopPropagation();
me._submit();
});
// - Toggle icon
// TODO: UX Improvment
// Note: Would be better to toggle by clicking on the whole title, but we have an issue with <select> on abstract classes.
this.element.find('.sf_title').on('click', function(oEvent){
// Prevent anchors
oEvent.preventDefault();
// Prevent toggle on <select>
if(oEvent.target.nodeName.toLowerCase() !== 'select')
{
me.element.find('.sf_criterion_area').slideToggle('fast');
me.element.toggleClass('opened');
}
});
},
// - Prepare criterion area
_prepareCriterionArea: function()
{
var oCriterionAreaElem;
// Build area element
if(this.options.criterion_outer_selector !== null && $(this.options.criterion_outer_selector).length > 0)
{
oCriterionAreaElem = $(this.options.criterion_outer_selector);
}
else
{
oCriterionAreaElem = $('<div></div>').appendTo(this.element);
}
oCriterionAreaElem.addClass('sf_criterion_area');
this.elements.criterion_area = oCriterionAreaElem;
// Clean area
oCriterionAreaElem
.html('')
.append('<div class="sf_more_criterion"></div>');
this.elements.more_criterion = oCriterionAreaElem.find('.sf_more_criterion');
// Prepare content
this._prepareExistingCriterion();
this._prepareMoreCriterionMenu();
},
// - Prepare existing criterion
_prepareExistingCriterion: function()
{
// - OR conditions
var aORs = (this.options.search.criterion['or'] !== undefined) ? this.options.search.criterion['or'] : [];
for(var iORIdx in aORs)
{
// Note: We might want to create a OR container here when handling several OR conditions.
var aANDs = (aORs[iORIdx]['and'] !== undefined) ? aORs[iORIdx]['and'] : [];
for(var iANDIdx in aANDs)
{
var oCriteriaData = aANDs[iANDIdx];
this._addCriteria(oCriteriaData);
}
}
},
// - Prepare "more" button
_prepareMoreCriterionMenu: function()
{
var me = this;
// Header part
var oHeaderElem = $('<div class="sfm_header"></div>')
.append('<a class="sfm_toggler" href="#"><span class="sfm_tg_title">' + Dict.S('UI:Search:Criterion:MoreMenu:AddCriteria') + '</span><span class="sfm_tg_icon fa fa-plus"></span></a>')
.appendTo(this.elements.more_criterion);
// Content part
var oContentElem = $('<div class="sfm_content"></div>')
.appendTo(this.elements.more_criterion);
// - Filter
var oFilterElem = $('<div></div>')
.addClass('sf_filter')
.addClass('sfm_filter')
.append('<span class="sff_input_wrapper"><input type="text" placeholder="' + Dict.S('UI:Search:Value:Filter:Placeholder') + '" /><span class="sff_picto sff_filter fa fa-filter"></span><span class="sff_picto sff_reset fa fa-times"></span></span>')
.appendTo(oContentElem);
// - Lists container
var oListsElem = $('<div></div>')
.addClass('sfm_lists')
.appendTo(oContentElem);
// - Recently used list
var oRecentsElem = $('<div></div>')
.addClass('sf_list')
.addClass('sf_list_recents')
.appendTo(oListsElem);
$('<div class="sfl_title"></div>')
.text(Dict.S('UI:Search:AddCriteria:List:RecentlyUsed:Title'))
.appendTo(oRecentsElem);
var oRecentsItemsElem = $('<ul class="sfl_items"></ul>')
.append('<li class="sfl_i_placeholder">' + Dict.S('UI:Search:AddCriteria:List:RecentlyUsed:Placeholder') + '</li>')
.appendTo(oRecentsElem);
me._refreshRecentlyUsed();
// - Search zlist list
var oZlistElem = $('<div></div>')
.addClass('sf_list')
.addClass('sf_list_zlist')
.appendTo(oListsElem);
$('<div class="sfl_title"></div>')
.text(Dict.S('UI:Search:AddCriteria:List:MostPopular:Title'))
.appendTo(oZlistElem);
var oZListItemsElem = $('<ul class="sfl_items"></ul>')
.appendTo(oZlistElem);
for(var sFieldRef in this.options.search.fields.zlist)
{
var oFieldElem = me._getHtmlLiFromFieldRef(me, sFieldRef, ['zlist']);
oFieldElem.appendTo(oZListItemsElem);
}
// - Remaining fields list
if(this.options.search.fields.others !== undefined)
{
var oOthersElem = $('<div></div>')
.addClass('sf_list')
.addClass('sf_list_others')
.appendTo(oListsElem);
$('<div class="sfl_title"></div>')
.text(Dict.S('UI:Search:AddCriteria:List:Others:Title'))
.appendTo(oOthersElem);
var oOthersItemsElem = $('<ul class="sfl_items"></ul>')
.appendTo(oOthersElem);
for(var sFieldRef in this.options.search.fields.others)
{
var oFieldElem = me._getHtmlLiFromFieldRef(me, sFieldRef, ['others']);
oFieldElem.appendTo(oOthersItemsElem);
}
}
// - Buttons
var oButtonsElem = $('<div></div>')
.addClass('sfm_buttons')
.append('<button type="button" name="apply">' + Dict.S('UI:Button:Apply') + '</button>')
.append('<button type="button" name="cancel">' + Dict.S('UI:Button:Cancel') + '</button>')
.appendTo(oContentElem);
// Bind events
// - Close menu on click anywhere else
// - Intercept click to avoid propagation (mostly used for closing it when clicking outside of it)
$('body').on('click', function(oEvent){
// Prevent propagation to parents and therefore multiple attempts to close it
oEvent.stopPropagation();
oEventTargetElem = $(oEvent.target);
// If not more menu, close all criterion
if(oEventTargetElem.closest('.sf_more_criterion').length > 0)
{
me._closeAllCriterion();
}
else
{
// TODO: Try to put this back in the date widget as it introduced a non necessary coupling.
// If using the datetimepicker, do not close anything
if (oEventTargetElem.closest('#ui-datepicker-div, .ui-datepicker-prev, .ui-datepicker-next').length > 0 )
{
// No closing in this case
}
// //if the context is not document, then we have encountered a bug : the css ::after elements do have a context on click and thus, we cannot check if they are inside a #ui-datepicker-div
// else if (typeof oEventTargetElem.context != 'undefined' && $(oEventTargetElem.context).is('.ui-icon'))
// {
// //no closing in this case (bugfix)
// }
// If criteria, close more menu & all criterion but me
else if(oEventTargetElem.closest('.search_form_criteria').length > 0)
{
me._closeMoreCriterion();
// All criterion but me is already handle by the criterion, no callback needed.
}
// If not criteria, close more menu & all criterion
else
{
me._closeMoreCriterion();
me._closeAllCriterion();
}
}
});
// - More criteria toggling
this.elements.more_criterion.find('.sfm_header').on('click', function(oEvent){
// Prevent anchor
oEvent.preventDefault();
me._toggleMoreCriterion();
});
// - Filter
// Note: "keyup" event is use instead of "keydown", otherwise, the inpu value would not be set yet.
oFilterElem.find('input').on('keyup focus', function(oEvent){
// TODO: Move on values with up and down arrow keys; select with space or enter.
// TODO: Hide list if no result on filter.
var sFilter = $(this).val();
// Show / hide items
if(sFilter === '')
{
oListsElem.find('.sfl_items > li').show();
oFilterElem.find('.sff_filter').show();
oFilterElem.find('.sff_reset').hide();
}
else
{
oListsElem.find('.sfl_items > li:not(.sfl_i_placeholder)').each(function(){
var oRegExp = new RegExp(sFilter, 'ig');
var sValue = $(this).find('input').val();
var sLabel = $(this).text();
// We don't check the sValue as it contains the class alias.
if(sLabel.match(oRegExp) !== null)
{
$(this).show();
}
else
{
$(this).hide();
}
});
oFilterElem.find('.sff_filter').hide();
oFilterElem.find('.sff_reset').show();
}
// Show / hide lists with no visible items
oListsElem.find('.sf_list').each(function(){
$(this).show();
if($(this).find('.sfl_items > li:visible').length === 0)
{
$(this).hide();
}
});
});
oFilterElem.find('.sff_filter').on('click', function(){
oFilterElem.find('input').trigger('focus');
});
oFilterElem.find('.sff_reset').on('click', function(){
oFilterElem.find('input')
.val('')
.trigger('focus');
});
// - Add one criteria
this.elements.more_criterion.on('click', '.sfm_field', function(oEvent){
// Prevent anchor
oEvent.preventDefault();
// Prevent propagation to not close the opening criteria
oEvent.stopPropagation();
// If no checkbox checked, add criteria right away, otherwise we "queue" it we other checkboxed.
if(me.elements.more_criterion.find('.sfm_field input[type="checkbox"]:checked').length === 0)
{
var sFieldRef = $(this).attr('data-field-ref');
// Prepare new criterion data (as already opened to increase UX)
var oData = {
'ref': sFieldRef,
'init_opened': (oEvent.ctrlKey) ? false : true,
};
// Add criteria but don't submit form as the user has not specified the value yet.
me.element.search_form_handler_history('setLatest', sFieldRef);
me._refreshRecentlyUsed();
me._addCriteria(oData);
}
else
{
$(this).find('input[type="checkbox"]').prop('checked', !$(this).find('input[type="checkbox"]').prop('checked'));
}
});
// - Add several criterion
this.elements.more_criterion.on('click', '.sfm_field input[type="checkbox"]', function(oEvent){
// Prevent propagation to field and instant add of the criteria
oEvent.stopPropagation();
if(me.elements.more_criterion.find('.sfm_field input[type="checkbox"]:checked').length === 0)
{
oButtonsElem.hide();
}
else
{
oButtonsElem.show();
}
// Put focus back to filter to improve UX.
oFilterElem.find('input').trigger('focus');
});
oButtonsElem.find('button').on('click', function(){
// Add criterion on apply
if($(this).attr('name') === 'apply')
{
me.elements.more_criterion.find('.sfm_field input[type="checkbox"]:checked').each(function(iIdx, oElem){
var sFieldRef = $(oElem).closest('.sfm_field').attr('data-field-ref');
var oData = {
'ref': sFieldRef,
'init_opened': false,
};
me.element.search_form_handler_history('setLatest', sFieldRef);
me._addCriteria(oData);
});
me._refreshRecentlyUsed();
me._closeMoreCriterion();
}
// Clear all
// - Checkboxes
me.elements.more_criterion.find('.sfm_field input[type="checkbox"]:checked').prop('checked', false);
// - Filter
oFilterElem.find('input')
.val('')
.trigger('focus');
// Hide buttons
oButtonsElem.hide();
});
},
// - Prepare results area
_prepareResultsArea: function()
{
var me = this;
var oResultAreaElem;
// Build area element
if(this.options.result_list_outer_selector !== null && $(this.options.result_list_outer_selector).length > 0)
{
oResultAreaElem = $(this.options.result_list_outer_selector);
}
else
{
// Reusing previously created DOM element
if(this.element.closest('.display_block').next('.sf_results_area').length > 0)
{
oResultAreaElem = this.element.closest('.display_block').next('.sf_results_area');
}
else
{
oResultAreaElem = $('<div class="display_block"></div>').insertAfter(this.element.closest('.display_block'));
}
}
oResultAreaElem.addClass('sf_results_area');
// Make placeholder if nothing yet
if(oResultAreaElem.html() === '')
{
// TODO: Make a good UI for this POC.
// TODO: Translate sentence.
oResultAreaElem.html('<div class="sf_results_placeholder"><p>Add some criterion on the search box or click the search button to view the objects.</p><p><button type="button">Search<span class="fa fa-search"></span></button></p></div>');
oResultAreaElem.find('button').on('click', function(){
// TODO: Bug: Open "Search for CI", change child classe in the dropdown, click the search button. It submit the search for the original child classe, not the current one; whereas a click on the upper right pictogram does. This might be due to the form reloading.
me._onSubmitClick();
});
}
this.elements.results_area = oResultAreaElem;
},
/**
* "add new criteria" <li /> markup
* - with checkbox, label, data-* ...
* - without event binding
*
* @private
*
* @param sFieldRef
* @param aFieldCollectionsEligible
*
* @return jQuery detached <li />
*/
_getHtmlLiFromFieldRef: function(me, sFieldRef, aFieldCollectionsEligible) {
var oFieldElem = undefined;
aFieldCollectionsEligible.forEach(function (sFieldCollection) {
if (typeof me.options.search.fields[sFieldCollection][sFieldRef] == 'undefined')
{
return true;//if this field is not present in the Collection, let's try the next
}
var oField = me.options.search.fields[sFieldCollection][sFieldRef];
var sFieldTitleAttr = (oField.description !== undefined) ? 'title="' + oField.description + '"' : '';
oFieldElem = $('<li></li>')
.addClass('sfm_field')
.attr('data-field-ref', sFieldRef)
.append('<label ' + sFieldTitleAttr + '><input type="checkbox" value="' + sFieldRef + '" />' + oField.label + '</label>')
});
if (undefined == oFieldElem)
{
me._trace('no sFieldRef in given collection', {"sFieldRef":sFieldRef, "aFieldCollectionsEligible":aFieldCollectionsEligible});
return $('<!-- no sFieldRef in given collection -->');
}
return oFieldElem;
},
// Criteria helpers
// - Add a criteria to the form
_addCriteria: function(oData)
{
var sRef = oData.ref;
var sType = sType = (oData.widget !== undefined) ? oData.widget : this._getCriteriaTypeFromFieldRef(sRef);
// Force to raw for non removable criteria
if( (oData.is_removable !== undefined) && (oData.is_removable === false) )
{
sType = 'raw';
}
// Protection against bad initialization data
if(sType === null)
{
this._trace('Could not add criteria as we could not retrieve type for ref "'+sRef+'".');
return false;
}
// Retrieve widget class
var sWidgetName = this._getCriteriaWidgetNameFromType(sType);
// Add some informations from the field
if(this._hasFieldDefinition(sRef))
{
var oFieldDef = this._getFieldDefinition(sRef);
oData.field = {
label: oFieldDef.label,
class: oFieldDef.class,
class_alias: oFieldDef.class_alias,
code: oFieldDef.code,
target_class: oFieldDef.target_class,
widget: oFieldDef.widget,
allowed_values: oFieldDef.allowed_values,
is_null_allowed: oFieldDef.is_null_allowed,
};
}
if ('date' == sType || 'date_time' == sType)
{
oData.datepicker = this.options.datepicker;
}
// Create DOM element
var oCriteriaElem = $('<div></div>')
.addClass('sf_criteria')
//.insertBefore(this.elements.more_criterion);
.appendTo(this.elements.criterion_area);
// Instanciate widget
$.itop[sWidgetName](oData, oCriteriaElem);
return true;
},
// - Find a criteria's type from a field's ref (usually <CLASS_ALIAS>.<ATT_CODE>)
_getCriteriaTypeFromFieldRef: function(sRef)
{
// Fallback for unknown widget types or unknown field refs
var sType = this.options.default_criteria_type;
for(var sListIdx in this.options.search.fields)
{
if(this.options.search.fields[sListIdx][sRef] !== undefined)
{
sType = this.options.search.fields[sListIdx][sRef].widget.toLowerCase();
break;
}
}
return sType;
},
// - Find a criteria's widget name from a criteria's type
_getCriteriaWidgetNameFromType: function(sType)
{
return 'search_form_criteria' + '_' + (($.itop['search_form_criteria_'+sType] !== undefined) ? sType : 'raw');
},
// Criteria handlers
_onCriteriaValueChanged: function(oData)
{
this._updateSearch();
this._submit();
},
_onCriteriaRemoved: function(oData)
{
this._updateSearch();
this._submit();
},
_onCriteriaErrorOccured: function(oData)
{
this._setErrorMessage(oData);
},
// Field helpers
_hasFieldDefinition: function(sRef)
{
var bFound = false;
for(var sListIdx in this.options.search.fields)
{
if(this.options.search.fields[sListIdx][sRef] !== undefined)
{
bFound = true;
break;
}
}
return bFound;
},
_getFieldDefinition: function(sRef)
{
var oFieldDef = null;
for(var sListIdx in this.options.search.fields)
{
if(this.options.search.fields[sListIdx][sRef] !== undefined)
{
oFieldDef = this.options.search.fields[sListIdx][sRef];
break;
}
}
return oFieldDef;
},
// Message helper
_cleanMessageArea: function()
{
this.elements.message_area
.hide()
.html('')
.removeClass('message_error');
},
_setErrorMessage: function(sMessage)
{
this.elements.message_area
.addClass('message_error')
.html(sMessage)
.show();
},
// Button handlers
_onSubmitClick: function(oEvent)
{
// Assertion: the search is already up to date
this._submit();
},
// Submit handlers
// - External event callback
_onSubmit: function()
{
this._submit();
},
// - Do the submit
_submit: function()
{
var me = this;
// Data
// - Regular params
var oData = {
'params': JSON.stringify({
'base_oql': this.options.search.base_oql,
'criterion': this.options.search.criterion,
}),
};
// - List params (pass through for the server), merge data_config with list_params if present.
var oListParams = {};
if(this.options.data_config_list_selector !== null)
{
var sExtraParams = $(this.options.data_config_list_selector).data('sExtraParams');
if(sExtraParams !== undefined)
{
oListParams = JSON.parse(sExtraParams);
}
}
$.extend(oListParams, this.options.list_params);
oData.list_params = JSON.stringify(oListParams);
// Abort pending request
if(this.submit.xhr !== null)
{
this.submit.xhr.abort();
}
// Show loader
this._showLoader();
this._cleanMessageArea();
// Do submit
this.submit.xhr = $.post(
this.options.endpoint,
oData
)
.done(function(oResponse, sStatus, oXHR){ me._onSubmitSuccess(oResponse); })
.fail(function(oResponse, sStatus, oXHR){ me._onSubmitFailure(oResponse, sStatus); })
.always(function(oResponse, sStatus, oXHR){ me._onSubmitAlways(oResponse); });
},
// - Called on form submit successes
_onSubmitSuccess: function(oData)
{
this.elements.results_area.html(oData);
},
// - Called on form submit failures
_onSubmitFailure: function(oData, sStatus)
{
if(sStatus === 'abort')
{
return false;
}
// Fallback message in case the server send back only HTML markup.
var oErrorElem = $(oData.responseText);
var sErrorMessage = (oErrorElem.text() !== '') ? oErrorElem.text() : Dict.Format('Error:XHR:Fail', '');
this._setErrorMessage(sErrorMessage);
},
// - Called after form submits
_onSubmitAlways: function(oData)
{
this._hideLoader();
},
// Global helpers
_refreshRecentlyUsed: function()
{
me = this;
var aHistory = me.element.search_form_handler_history("getHistory");
var oRecentsItemsElem = me.element.find('.sf_list_recents .sfl_items');
if (aHistory.length == 0)
{
return;
}
oRecentsItemsElem.empty();
aHistory.forEach(function(sFieldRef) {
var oFieldElem = me._getHtmlLiFromFieldRef(me, sFieldRef, ['zlist', 'others']);
oRecentsItemsElem.append(oFieldElem);
});
},
// - Show loader
_showLoader: function()
{
this.elements.results_area.block();
},
// - Hide loader
_hideLoader: function()
{
this.elements.results_area.unblock();
},
// - Converts a snake_case string to CamelCase
_toCamelCase: function(sString)
{
var aParts = sString.split('_');
for(var i in aParts)
{
aParts[i] = aParts[i].charAt(0).toUpperCase() + aParts[i].substr(1);
}
return aParts.join('');
},
// Debug helpers
// - Show a trace in the javascript console
_trace: function(sMessage, oData)
{
if(window.console)
{
if(oData !== undefined)
{
console.log('Search form handler: ' + sMessage, oData);
}
else
{
console.log('Search form handler: ' + sMessage);
}
}
},
// - Show current options
showOptions: function()
{
this._trace('Options', this.options);
}
});
});

View File

@@ -0,0 +1,132 @@
/**
*
* iTop Search form history handler
*
* the widget exposes
*
* $(cssSelector).search_form_handler_history({"itop_root_class":"FooBarClass"}) : constructor
* $(cssSelector).search_form_handler_history("getHistory") : history array getter
* $(cssSelector).search_form_handler_history("setLatest", "field.ref") : prepend the field ref to the beginning of the history array
*
*
* please take a look at the options for custom constructor values
*
*
* The persistence layer rely on these two core iTop's JS functions :
* - GetUserPreference(sPreferenceCode, sDefaultValue)
* - SetUserPreference(sPreferenceCode, sPrefValue, bPersistent)
*
*/
;
$(function()
{
// the widget definition, where 'itop' is the namespace,
// 'search_form_handler_history' the widget name
$.widget( 'itop.search_form_handler_history',
{
// default options
options:
{
"itop_root_class": null,
"preference_code": undefined, // if undefined, it is computed on _create
"history_max_length": 10, // if the history is longer, the older entries are removed
"history_backend_store_timeout": 5000, // wait for this time before storing the new history in the backend
},
aHistory : [],
iStoreHistoryTimeoutHandler : undefined,
/**
* the constructor, called by the widget factory
* @private
*/
_create: function()
{
var me = this;
if (me.options.preference_code == undefined)
{
me.options.preference_code = me.options.itop_root_class + '__search_history';
}
me.aHistory = JSON.parse(GetUserPreference(me._getPreferenceCode(), "[]"));
},
getHistory: function()
{
var me = this;
return me.aHistory;
},
setLatest: function(sFieldRef)
{
var me = this;
//if present, delete
var iIndexInHistory = me.aHistory.indexOf(sFieldRef);
if (iIndexInHistory > -1)
{
me.aHistory.splice(iIndexInHistory, 1);
}
//add to the begining
me.aHistory.unshift(sFieldRef);
//restrain the length to me.options.history_max_length
var iDeleteCount = me.aHistory.length - me.options.history_max_length;
if (iDeleteCount > 0)
{
me.aHistory.splice(-iDeleteCount, iDeleteCount);
}
//store it in the backend (with a delay in the hope to wait long enough to make bulk modifications
me._storeHistory();
//setter should never return a value!
return;
},
/**
* @returns String
* @private
*/
_getPreferenceCode: function()
{
return this.options.preference_code;
},
/**
* should only be called by setLatest in order to store the updated history
* @private
*/
_storeHistory: function()
{
var me = this;
if (undefined != me.iStoreHistoryTimeoutHandler)
{
clearTimeout(me.iStoreHistoryTimeoutHandler);
}
me.iStoreHistoryTimeoutHandler = setTimeout(me._storeHistoryTimeoutFunction(me), me.options.history_backend_store_timeout);
},
/**
* should only be called by _storeHistory using a timeout
* @private
*/
_storeHistoryTimeoutFunction: function(me)
{
SetUserPreference(me._getPreferenceCode(), JSON.stringify(me.getHistory()), true);
me.iStoreHistoryTimeoutHandler = undefined;
}
});
});

343
js/searchformforeignkeys.js Normal file
View File

@@ -0,0 +1,343 @@
// Copyright (C) 2010-2018 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/>
/**
*
* @param id String the dom identifier of the source input
* @param sTargetClass
* @param sAttCode
* @param oSearchWidgetElmt
* @param sFilter
* @param sTitle
* @constructor
*/
function SearchFormForeignKeys(id, sTargetClass, sAttCode, oSearchWidgetElmt, sFilter, sTitle)
{
this.id = id;
//this.sOriginalTargetClass = sTargetClass;
this.sTargetClass = sTargetClass;
this.sFilter = sFilter;
this.sTitle = sTitle;
this.sAttCode = sAttCode;
this.oSearchWidgetElmt = oSearchWidgetElmt;
this.emptyHtml = ''; // content to be displayed when the search results are empty (when opening the dialog)
this.emptyOnClose = true; // Workaround for the JQuery dialog being very slow when opening and closing if the content contains many INPUT tags
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
var me = this;
this.Init = function()
{
// make sure that the form is clean
$('#linkedset_'+this.id+' .selection').each( function() { this.checked = false; });
$('#'+this.id+'_btnRemove').prop('disabled', false);
$('<div id="dlg_'+me.id+'"></div>').appendTo(document.body);
// me.trace(dialog);
//TODO : check and remove all unneded code bellow this line!!
$('#'+this.id+'_linksToRemove').val('');
$('#linkedset_'+me.id).on('remove', function() {
// prevent having the dlg div twice
$('#dlg_'+me.id).remove();
});
$('#'+this.iInputId).closest('form').submit(function() {
return me.OnFormSubmit();
});
};
this.StopPendingRequest = function()
{
if (me.ajax_request)
{
me.ajax_request.abort();
me.ajax_request = null;
}
};
this.ShowModalSearchForeignKeys = function()
{
// // Query the server to get the form to search for target objects
// if (me.bSelectMode)
// {
// $('#fstatus_'+me.id).html('<img src="../images/indicator.gif" />');
// }
// else
// {
// $('#label_'+me.id).addClass('dlg_loading');
// }
$('#label_'+me.id).addClass('dlg_loading');
var theMap = {
sAttCode: me.sAttCode,
iInputId: me.id,
sTitle: me.sTitle,
sTargetClass: me.sTargetClass,
// bSearchMode: me.bSearchMode,
operation: 'ShowModalSearchForeignKeys'
};
// Make sure that we cancel any pending request before issuing another
// since responses may arrive in arbitrary order
me.StopPendingRequest();
// Run the query and get the result back directly in HTML
me.ajax_request = $.post( AddAppContext(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php'), theMap,
function(data)
{
// $('#dlg_'+me.id).html(data);
$('#dlg_'+me.id).empty().append($(data)); // $(data).filter(':not(script)'));
$('#dlg_'+me.id).dialog('open');
me.UpdateSizes();
me.UpdateButtons();
me.ajax_request = null;
FixSearchFormsDisposition();
me.ListResultsSearchForeignKeys();
},
'html'
);
};
this.UpdateSizes = function()
{
var dlg = $('#dlg_'+me.id);
// Adjust the dialog's size to fit into the screen
if (dlg.width() > ($(window).width()-40))
{
dlg.width($(window).width()-40);
}
if (dlg.height() > ($(window).height()-70))
{
dlg.height($(window).height()-70);
}
var searchForm = dlg.find('div.display_block:first'); // Top search form, enclosing display_block
var results = $('#SearchResultsToAdd_'+me.id);
var oPadding = {};
var aKeys = ['top', 'right', 'bottom', 'left'];
for(k in aKeys)
{
oPadding[aKeys[k]] = 0;
if (dlg.css('padding-'+aKeys[k]))
{
oPadding[aKeys[k]] = parseInt(dlg.css('padding-'+aKeys[k]).replace('px', ''));
}
}
//var width = dlg.innerWidth() - oPadding['right'] - oPadding['left'] - 22; // 5 (margin-left) + 5 (padding-left) + 5 (padding-right) + 5 (margin-right) + 2 for rounding !
var height = dlg.innerHeight()-oPadding['top']-oPadding['bottom']-22;
var form_height = searchForm.outerHeight();
results.height(height - form_height - 40); // Leave some space for the buttons
};
this.UpdateButtons = function()
{
var okBtn = $('#btn_ok_'+me.id);
if ($('#count_'+me.id).val() > 0)
{
okBtn.removeAttr('disabled');
}
else
{
okBtn.prop('disabled', 'disabled');
}
};
/**
* @return {boolean}
*/
this.ListResultsSearchForeignKeys = function ()
{
var theMap = {
sTargetClass: me.sTargetClass,
iInputId: me.id,
sFilter: me.sFilter
// bSearchMode: me.bSearchMode
};
// Gather the parameters from the search form
$('#fs_'+me.id+' :input').each( function() {
if (this.name !== '')
{
var val = $(this).val(); // supports multiselect as well
if (val !== null)
{
theMap[this.name] = val;
}
}
});
theMap['sRemoteClass'] = theMap['class']; // swap 'class' (defined in the form) and 'remoteClass'
theMap.operation = 'ListResultsSearchForeignKeys'; // Override what is defined in the form itself
theMap.sAttCode = me.sAttCode;
var sSearchAreaId = '#SearchResultsToAdd_'+me.id;
//$(sSearchAreaId).html('<div style="text-align:center;width:100%;height:24px;vertical-align:middle;"><img src="../images/indicator.gif" /></div>');
$(sSearchAreaId).block();
me.UpdateButtons();
// Make sure that we cancel any pending request before issuing another
// since responses may arrive in arbitrary order
me.StopPendingRequest();
// Run the query and display the results
me.ajax_request = $.post(AddAppContext(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php'), theMap,
function(data)
{
$(sSearchAreaId).html(data);
$(sSearchAreaId+' .listResults').tableHover();
$('#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'
);
return false; // Don't submit the form, stay in the current page !
};
/**
* @return {boolean}
*/
this.DoAddObjects = function () {
// Gather the parameters from the search form
var theMap = {};
var context = $('#SearchResultsToAdd_'+me.id);
var selectionMode = $(':input[name="selectionMode"]', context);
if (selectionMode.length > 0) {
// Paginated table retrieve the mode and the exceptions
theMap['selectionMode'] = selectionMode.val();
$('#fs_SearchFormToAdd_'+me.id+' :input').each(function () {
theMap[this.name] = this.value;
});
$(':input[name="storedSelection[]"]', context).each(function () {
if (typeof theMap[this.name] === "undefined") {
theMap[this.name] = [];
}
theMap[this.name].push(this.value);
$(this).remove(); // Remove the selection for the next time the dialog re-opens
});
}
// Normal table, retrieve all the checked check-boxes
$(':checked[name="selectObject[]"]', context).each(
function () {
if ((this.name !== '') && ((this.type !== 'checkbox') || (this.checked))) {
var arrayExpr = /\[\]$/;
if (arrayExpr.test(this.name)) {
// Array
if (typeof theMap[this.name] === "undefined") {
theMap[this.name] = [];
}
theMap[this.name].push(this.value);
}
else {
theMap[this.name] = this.value;
}
}
$(this).parents('tr:first').remove(); // Remove the whole line, so that, next time the dialog gets displayed it's no longer there
}
);
theMap["class"] = me.sTargetClass;
theMap['operation'] = 'GetFullListForeignKeysFromSelection';
$('#busy_'+me.iInputId).html('&nbsp;<img src="../images/indicator.gif"/>');
// Run the query and display the results
$.ajax(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', {
"data": theMap,
"method": "POST"
})
.done(function (data) {
if (Object.keys(data).length > 0) {
me.oSearchWidgetElmt.trigger("itop.search.criteria_enum.add_selected_values", data);
}
})
.fail(function (data) {
try {
console.error(data);
} catch (e) {
}
})
;
$('#dlg_'+me.id).dialog('close');
return false;
};
// Workaround for a ui.jquery limitation: if the content of
// the dialog contains many INPUTs, closing and opening the
// dialog is very slow. So empty it each time.
this.OnClose = function()
{
me.StopPendingRequest();
// called by the dialog, so in the context 'this' points to the jQueryObject
if (me.emptyOnClose)
{
$('#SearchResultsToAdd_'+me.id).html(me.emptyHtml);
}
$('#label_'+me.id).removeClass('dlg_loading');
$('#label_'+me.id).focus();
me.ajax_request = null;
};
this.DoSelectObjectClass = function()
{
// Retrieving selected value
var oSelectedClass = $('#ac_create_'+me.id+' select');
if(oSelectedClass.length !== 1) return;
// Setting new target class
me.sTargetClass = oSelectedClass.val();
// Opening real creation form
$('#ac_create_'+me.id).dialog('close');
me.CreateObject();
};
this.Update = function()
{
if ($('#'+me.id).prop('disabled'))
{
$('#v_'+me.id).html('');
$('#label_'+me.id).prop('disabled', 'disabled');
$('#label_'+me.id).css({'background': 'transparent'});
$('#mini_add_'+me.id).hide();
$('#mini_tree_'+me.id).hide();
$('#mini_search_'+me.id).hide();
}
else
{
$('#label_'+me.id).removeAttr('disabled');
$('#label_'+me.id).css({'background': '#fff url(../images/ac-background.gif) no-repeat right'});
$('#mini_add_'+me.id).show();
$('#mini_tree_'+me.id).show();
$('#mini_search_'+me.id).show();
}
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -130,7 +130,7 @@ function DisplayDetails($oP, $sClass, $oObj, $id)
$sClassLabel = MetaModel::GetName($sClass);
$oSearch = new DBObjectSearch($sClass);
$oBlock = new DisplayBlock($oSearch, 'search', false);
$oBlock->Display($oP, 0);
$oBlock->Display($oP, 0, array('table_id' => 'search-widget-results-outer'));
// The object could be listed, check if it is actually allowed to view it
$oSet = CMDBObjectSet::FromObject($oObj);
@@ -198,7 +198,7 @@ function DisplaySearchSet($oP, $oFilter, $bSearchForm = true, $sBaseClass = '',
{
if ($bSearchForm)
{
$aParams = array('open' => $bSearchFormOpen);
$aParams = array('open' => $bSearchFormOpen, 'table_id' => '1');
if (!empty($sBaseClass))
{
$aParams['baseClass'] = $sBaseClass;
@@ -484,9 +484,9 @@ try
if ($bSearchForm)
{
$oBlock = new DisplayBlock($oFilter, 'search', false);
$oBlock->Display($oP, 0);
$oBlock->Display($oP, 0, array('table_id' => 'search-widget-result-outer'));
}
$oP->P('<b>'.Dict::Format('UI:Error:IncorrectOQLQuery_Message', $e->getHtmlDesc()).'</b>');
$oP->add('<div id="search-widget-result-outer"><p><b>'.Dict::Format('UI:Error:IncorrectOQLQuery_Message', $e->getHtmlDesc()).'</b></p></div>');
}
catch(Exception $e)
{

View File

@@ -110,6 +110,7 @@ if ($oFilter != null)
$aExtraParams['open'] = true;
$aExtraParams['baseClass'] = $sBaseClass;
$aExtraParams['action'] = utils::GetAbsoluteUrlAppRoot().'pages/UniversalSearch.php';
$aExtraParams['table_id'] = '1';
//$aExtraParams['class'] = $sClassName;
$oBlock->Display($oP, 0, $aExtraParams);

File diff suppressed because it is too large Load Diff

139
pages/ajax.searchform.php Normal file
View File

@@ -0,0 +1,139 @@
<?php
/**
* Copyright (C) 2010-2018 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/>
*
*/
use Combodo\iTop\Application\Search\AjaxSearchException;
use Combodo\iTop\Application\Search\CriterionParser;
require_once('../approot.inc.php');
require_once(APPROOT.'/application/application.inc.php');
require_once(APPROOT.'/application/webpage.class.inc.php');
require_once(APPROOT.'/application/ajaxwebpage.class.inc.php');
require_once(APPROOT.'/application/startup.inc.php');
require_once(APPROOT.'/application/user.preferences.class.inc.php');
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
require_once(APPROOT.'/sources/application/search/ajaxsearchexception.class.inc.php');
require_once(APPROOT.'/sources/application/search/criterionparser.class.inc.php');
require_once(APPROOT.'/application/wizardhelper.class.inc.php');
try
{
if (LoginWebPage::EXIT_CODE_OK != LoginWebPage::DoLoginEx(null /* any portal */, false, LoginWebPage::EXIT_RETURN))
{
throw new SecurityException('You must be logged in');
}
$sParams = utils::ReadParam('params', '', false, 'raw_data');
if (!$sParams)
{
throw new AjaxSearchException("Invalid query (empty filter)", 400);
}
$oPage = new ajax_page("");
$oPage->no_cache();
$oPage->SetContentType('text/html');
$sListParams = utils::ReadParam('list_params', '{}', false, 'raw_data');
$aListParams = (array)json_decode($sListParams, true);
$aParams = json_decode($sParams, true);
if (array_key_exists('hidden_criteria', $aListParams))
{
$sHiddenCriteria = $aListParams['hidden_criteria'];
}
else
{
$sHiddenCriteria = '';
}
$oFilter = CriterionParser::Parse($aParams['base_oql'], $aParams['criterion'], $sHiddenCriteria);
if (isset($aListParams['debug']))
{
$sOQL = $oFilter->ToOQL();
$oPage->add("<div class=\"header_message message_info\">$sOQL</div>\n");
}
//IssueLog::Info('Search OQL: "'.$oFilter->ToOQL().'"');
$oDisplayBlock = new DisplayBlock($oFilter, 'list', false);
foreach($aListParams as $key => $value)
{
$aExtraParams[$key] = $value;
}
if (array_key_exists('table_inner_id', $aListParams))
{
$sListId = $aListParams['table_inner_id'];
}
if (array_key_exists('json', $aListParams))
{
$aJson = $aListParams['json'];
$sJson = json_encode($aJson);
$oWizardHelper = WizardHelper::FromJSON($sJson);
$oObj = $oWizardHelper->GetTargetObject();
if (array_key_exists('query_params', $aExtraParams))
{
$aExtraParams['query_params']['this'] = $oObj;
}
else
{
$aExtraParams['query_params'] = array('this' => $oObj);
}
// // Current extkey value, so we can display event if it is not available anymore (eg. archived).
// $iCurrentExtKeyId = (is_null($oObj)) ? 0 : $oObj->Get($this->sAttCode);
// $aExtraParams['current_extkey_id'] = $iCurrentExtKeyId;
}
$aExtraParams['display_limit'] = true;
$aExtraParams['truncated'] = true;
if (isset($sListId))
{
$oDisplayBlock->Display($oPage, $sListId, $aExtraParams);
}
else
{
$oDisplayBlock->RenderContent($oPage, $aExtraParams);
}
$oPage->output();
} catch (AjaxSearchException $e)
{
http_response_code($e->getCode());
// note: transform to cope with XSS attacks
echo '<html><head></head><body><div>' . htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8') . '</div></body></html>';
IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString());
} catch (SecurityException $e)
{
http_response_code(403);
// note: transform to cope with XSS attacks
echo '<html><head></head><body><div>' . htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8') . '</div></body></html>';
IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString());
} catch (Exception $e)
{
http_response_code(500);
// note: transform to cope with XSS attacks
echo '<html><head></head><body><div>' . htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8') . '</div></body></html>';
IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString());
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Copyright (C) 2010-2018 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/>
*
*/
/**
* Created by PhpStorm.
* User: Eric
* Date: 08/03/2018
* Time: 11:18
*/
namespace Combodo\iTop\Application\Search;
class AjaxSearchException extends \Exception
{
}

View File

@@ -0,0 +1,356 @@
<?php
/**
* Copyright (C) 2010-2018 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\Application\Search\CriterionConversion;
use AttributeDate;
use AttributeDateTime;
use AttributeDefinition;
use AttributeEnum;
use Combodo\iTop\Application\Search\AjaxSearchException;
use Combodo\iTop\Application\Search\CriterionConversionAbstract;
use Combodo\iTop\Application\Search\SearchForm;
class CriterionToOQL extends CriterionConversionAbstract
{
public static function Convert($aCriteria)
{
if (!empty($aCriteria['oql']))
{
return $aCriteria['oql'];
}
$aRef = explode('.', $aCriteria['ref']);
for($i = 0; $i < count($aRef); $i++)
{
$aRef[$i] = '`'.$aRef[$i].'`';
}
$sRef = implode('.', $aRef);
$sOperator = $aCriteria['operator'];
$aMappedOperators = array(
self::OP_CONTAINS => 'ContainsToOql',
self::OP_STARTS_WITH => 'StartsWithToOql',
self::OP_ENDS_WITH => 'EndsWithToOql',
self::OP_EMPTY => 'EmptyToOql',
self::OP_NOT_EMPTY => 'NotEmptyToOql',
self::OP_BETWEEN_DATES => 'BetweenDatesToOql',
self::OP_BETWEEN => 'BetweenToOql',
self::OP_IN => 'InToOql',
self::OP_ALL => 'AllToOql',
);
if (array_key_exists($sOperator, $aMappedOperators))
{
$sFct = $aMappedOperators[$sOperator];
return self::$sFct($sRef, $aCriteria);
}
$sValue = self::GetValue(self::GetValues($aCriteria), 0);
return "({$sRef} {$sOperator} '{$sValue}')";
}
private static function GetValues($aCriteria)
{
if (!array_key_exists('values', $aCriteria))
{
return array();
}
return $aCriteria['values'];
}
private static function GetValue($aValues, $iIndex)
{
if (!array_key_exists($iIndex, $aValues))
{
return null;
}
if (!array_key_exists('value', $aValues[$iIndex]))
{
return null;
}
return $aValues[$iIndex]['value'];
}
protected static function ContainsToOql($sRef, $aCriteria)
{
$aValues = self::GetValues($aCriteria);
$sValue = self::GetValue($aValues, 0);
return "({$sRef} LIKE '%{$sValue}%')";
}
protected static function StartsWithToOql($sRef, $aCriteria)
{
$aValues = self::GetValues($aCriteria);
$sValue = self::GetValue($aValues, 0);
return "({$sRef} LIKE '{$sValue}%')";
}
protected static function EndsWithToOql($sRef, $aCriteria)
{
$aValues = self::GetValues($aCriteria);
$sValue = self::GetValue($aValues, 0);
return "({$sRef} LIKE '%{$sValue}')";
}
protected static function EmptyToOql($sRef, $aCriteria)
{
if (isset($aCriteria['widget']))
{
switch ($aCriteria['widget'])
{
case AttributeDefinition::SEARCH_WIDGET_TYPE_NUMERIC:
case AttributeDefinition::SEARCH_WIDGET_TYPE_EXTERNAL_FIELD:
return "ISNULL({$sRef})";
}
}
return "({$sRef} = '')";
}
protected static function NotEmptyToOql($sRef, $aCriteria)
{
return "({$sRef} != '')";
}
protected static function InToOql($sRef, $aCriteria)
{
$sAttCode = $aCriteria['code'];
$sClass = $aCriteria['class'];
$aValues = self::GetValues($aCriteria);
if (count($aValues) == 0)
{
// Ignore when nothing is selected
return "1";
}
$bFilterOnUndefined = false;
try
{
$aAttributeDefs = \MetaModel::ListAttributeDefs($sClass);
if (array_key_exists($sAttCode, $aAttributeDefs))
{
$oAttDef = $aAttributeDefs[$sAttCode];
if ($oAttDef instanceof AttributeEnum)
{
$aAllowedValues = SearchForm::GetFieldAllowedValues($oAttDef);
if (array_key_exists('values', $aAllowedValues))
{
// Can't invert the test if NULL is allowed
if (!$oAttDef->IsNullAllowed())
{
$aAllowedValues = $aAllowedValues['values'];
if (count($aValues) == count($aAllowedValues))
{
// All entries are selected
return "1";
}
// more selected values than remaining so use NOT IN
else
{
if (count($aValues) > (count($aAllowedValues) / 2))
{
foreach($aValues as $aValue)
{
unset($aAllowedValues[$aValue['value']]);
}
$sInList = implode("','", array_keys($aAllowedValues));
return "({$sRef} NOT IN ('$sInList'))";
}
}
}
// search for "undefined"
for($i = 0; $i < count($aValues); $i++)
{
$aValue = $aValues[$i];
if (isset($aValue['value']) && ($aValue['value'] === 'null'))
{
$bFilterOnUndefined = true;
unset($aValues[$i]);
break;
}
}
}
}
}
} catch (\CoreException $e)
{
}
$aInValues = array();
foreach($aValues as $aValue)
{
$aInValues[] = $aValue['value'];
}
$sInList = implode("','", $aInValues);
if ($bFilterOnUndefined)
{
$sFilterOnUndefined = "ISNULL({$sRef})";
if (count($aValues) === 0)
{
return $sFilterOnUndefined;
}
if (count($aInValues) == 1)
{
// Add 'AND 1' to group the 'OR' inside an AND list for OQL parsing
return "((({$sRef} = '$sInList') OR {$sFilterOnUndefined}) AND 1)";
}
// Add 'AND 1' to group the 'OR' inside an AND list for OQL parsing
return "(({$sRef} IN ('$sInList') OR {$sFilterOnUndefined}) AND 1)";
}
if (count($aInValues) == 1)
{
return "({$sRef} = '$sInList')";
}
return "({$sRef} IN ('$sInList'))";
}
protected static function BetweenDatesToOql($sRef, $aCriteria)
{
$aOQL = array();
$aValues = self::GetValues($aCriteria);
if (count($aValues) != 2)
{
return "1";
}
$sWidget = $aCriteria['widget'];
if ($sWidget == AttributeDefinition::SEARCH_WIDGET_TYPE_DATE_TIME)
{
$sAttributeClass = AttributeDateTime::class;
}
else
{
$sAttributeClass = AttributeDate::class;
}
$oFormat = $sAttributeClass::GetFormat();
$sStartDate = $aValues[0]['value'];
if (!empty($sStartDate))
{
$oDate = $oFormat->parse($sStartDate);
$sStartDate = $oDate->format($sAttributeClass::GetSQLFormat());
$aOQL[] = "({$sRef} >= '$sStartDate')";
}
$sEndDate = $aValues[1]['value'];
if (!empty($sEndDate))
{
$oDate = $oFormat->parse($sEndDate);
$sEndDate = $oDate->format($sAttributeClass::GetSQLFormat());
$aOQL[] = "({$sRef} <= '$sEndDate')";
}
$sOQL = implode(' AND ', $aOQL);
if (empty($sOQL))
{
$sOQL = "1";
}
return $sOQL;
}
/**
* @param $sRef
* @param $aCriteria
*
* @return string
* @throws \Combodo\iTop\Application\Search\AjaxSearchException
*/
protected static function BetweenToOql($sRef, $aCriteria)
{
$aOQL = array();
$aValues = self::GetValues($aCriteria);
if (count($aValues) != 2)
{
return "1";
}
if (isset($aValues[0]['value']))
{
$sStartNum = trim($aValues[0]['value']);
if (is_numeric($sStartNum))
{
$aOQL[] = "({$sRef} >= '$sStartNum')";
}
else
{
if (!empty($sStartNum))
{
throw new AjaxSearchException("'$sStartNum' is not a numeric value", 400);
}
}
}
if (isset($aValues[1]['value']))
{
$sEndNum = trim($aValues[1]['value']);
if (is_numeric($sEndNum))
{
$aOQL[] = "({$sRef} <= '$sEndNum')";
}
else
{
if (!empty($sEndNum))
{
throw new AjaxSearchException("'$sEndNum' is not a numeric value", 400);
}
}
}
$sOQL = implode(' AND ', $aOQL);
if (empty($sOQL))
{
$sOQL = "1";
}
return $sOQL;
}
protected static function AllToOql($sRef, $aCriteria)
{
return "1";
}
}

View File

@@ -0,0 +1,644 @@
<?php
/**
* Copyright (C) 2010-2018 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/>
*
*/
/**
* Convert structures from OQL expressions into structure for the search form
*/
namespace Combodo\iTop\Application\Search\CriterionConversion;
use AttributeDate;
use AttributeDateTime;
use AttributeDefinition;
use Combodo\iTop\Application\Search\CriterionConversionAbstract;
use DateInterval;
use DateTime;
use Dict;
use Exception;
use MetaModel;
class CriterionToSearchForm extends CriterionConversionAbstract
{
/**
* @param array $aAndCriterionRaw
* @param array $aFieldsByCategory
*
* @param array $aClasses all the classes of the filter
*
* @param bool $bIsRemovable
*
* @return array
*/
public static function Convert($aAndCriterionRaw, $aFieldsByCategory, $aClasses, $bIsRemovable = true)
{
$aAllFields = array();
foreach($aFieldsByCategory as $aFields)
{
foreach($aFields as $aField)
{
$sAlias = $aField['class_alias'];
$sCode = $aField['code'];
$aAllFields["$sAlias.$sCode"] = $aField;
}
}
$aAndCriterion = array();
$aMappingOperatorToFunction = array(
AttributeDefinition::SEARCH_WIDGET_TYPE_STRING => 'TextToSearchForm',
AttributeDefinition::SEARCH_WIDGET_TYPE_EXTERNAL_FIELD => 'ExternalFieldToSearchForm',
AttributeDefinition::SEARCH_WIDGET_TYPE_DATE => 'DateTimeToSearchForm',
AttributeDefinition::SEARCH_WIDGET_TYPE_DATE_TIME => 'DateTimeToSearchForm',
AttributeDefinition::SEARCH_WIDGET_TYPE_NUMERIC => 'NumericToSearchForm',
AttributeDefinition::SEARCH_WIDGET_TYPE_EXTERNAL_KEY => 'ExternalKeyToSearchForm',
AttributeDefinition::SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY => 'ExternalKeyToSearchForm',
AttributeDefinition::SEARCH_WIDGET_TYPE_ENUM => 'EnumToSearchForm',
);
foreach($aAndCriterionRaw as $aCriteria)
{
if (isset($aCriteria['label']))
{
$aCriteria['label'] = preg_replace("@\)$@", '', $aCriteria['label']);
$aCriteria['label'] = preg_replace("@^\(@", '', $aCriteria['label']);
}
$aCriteria['is_removable'] = $bIsRemovable;
$sClass = '';
if (isset($aCriteria['ref']))
{
$aRef = explode('.', $aCriteria['ref']);
if (isset($aClasses[$aRef[0]]))
{
$sClass = $aClasses[$aRef[0]];
$aCriteria['class'] = $sClass;
}
}
// Check criteria validity
if (!isset($aCriteria['ref']) || !isset($aAllFields[$aCriteria['ref']]))
{
$aCriteria['widget'] = AttributeDefinition::SEARCH_WIDGET_TYPE_RAW;
$aCriteria['label'] = Dict::S('UI:Search:Criteria:Raw:Filtered');
if (isset($aCriteria['ref']))
{
try
{
$aCriteria['label'] = Dict::Format('UI:Search:Criteria:Raw:FilteredOn', MetaModel::GetName($sClass));
}
catch (Exception $e)
{
}
}
}
if (array_key_exists('widget', $aCriteria))
{
if (array_key_exists($aCriteria['widget'], $aMappingOperatorToFunction))
{
$sFct = $aMappingOperatorToFunction[$aCriteria['widget']];
$aAndCriterion[] = self::$sFct($aCriteria, $aAllFields);
}
else
{
$aAndCriterion[] = $aCriteria;
}
}
}
// Regroup criterion by variable name (no ref first)
usort($aAndCriterion, function ($a, $b) {
if (array_key_exists('ref', $a) || array_key_exists('ref', $b))
{
if (array_key_exists('ref', $a) && array_key_exists('ref', $b))
{
$iRefCmp = strcmp($a['ref'], $b['ref']);
if ($iRefCmp != 0) return $iRefCmp;
return strcmp($a['operator'], $b['operator']);
}
if (array_key_exists('ref', $a))
{
return 1;
}
return -1;
}
if (array_key_exists('oql', $a) && array_key_exists('oql', $b))
{
return strcmp($a['oql'], $b['oql']);
}
return 0;
});
$aMergeFctByWidget = array(
AttributeDefinition::SEARCH_WIDGET_TYPE_DATE => 'MergeDate',
AttributeDefinition::SEARCH_WIDGET_TYPE_DATE_TIME => 'MergeDateTime',
AttributeDefinition::SEARCH_WIDGET_TYPE_NUMERIC => 'MergeNumeric',
AttributeDefinition::SEARCH_WIDGET_TYPE_ENUM => 'MergeEnumExtKeys',
AttributeDefinition::SEARCH_WIDGET_TYPE_EXTERNAL_KEY => 'MergeEnumExtKeys',
);
$aPrevCriterion = null;
$aMergedCriterion = array();
foreach($aAndCriterion as $aCurrCriterion)
{
if (!is_null($aPrevCriterion))
{
if (array_key_exists('ref', $aPrevCriterion))
{
// If previous has ref, the current has ref as the array is sorted with all without ref first
if (strcmp($aPrevCriterion['ref'], $aCurrCriterion['ref']) == 0)
{
// Same attribute, try to merge
if (array_key_exists('widget', $aCurrCriterion))
{
if (array_key_exists($aCurrCriterion['widget'], $aMergeFctByWidget))
{
$sFct = $aMergeFctByWidget[$aCurrCriterion['widget']];
$aPrevCriterion = self::$sFct($aPrevCriterion, $aCurrCriterion, $aMergedCriterion);
continue;
}
}
}
}
$aMergedCriterion[] = $aPrevCriterion;
}
$aPrevCriterion = $aCurrCriterion;
}
if (!is_null($aPrevCriterion))
{
$aMergedCriterion[] = $aPrevCriterion;
}
// Sort by label criterion by variable name (no ref first)
usort($aMergedCriterion, function ($a, $b) {
if (($a['widget'] === AttributeDefinition::SEARCH_WIDGET_TYPE_RAW) ||
($b['widget'] === AttributeDefinition::SEARCH_WIDGET_TYPE_RAW))
{
if (($a['widget'] === AttributeDefinition::SEARCH_WIDGET_TYPE_RAW) &&
($b['widget'] === AttributeDefinition::SEARCH_WIDGET_TYPE_RAW))
{
return strcmp($a['label'], $b['label']);
}
if ($a['widget'] === AttributeDefinition::SEARCH_WIDGET_TYPE_RAW)
{
return -1;
}
return 1;
}
return strcmp($a['label'], $b['label']);
});
return $aMergedCriterion;
}
/**
* @param $aPrevCriterion
* @param $aCurrCriterion
* @param $aMergedCriterion
*
* @return Current criteria or null if merged
* @throws \Exception
*/
protected static function MergeDate($aPrevCriterion, $aCurrCriterion, &$aMergedCriterion)
{
$sPrevOperator = $aPrevCriterion['operator'];
$sCurrOperator = $aCurrCriterion['operator'];
if ((($sPrevOperator != '<') && ($sPrevOperator != '<=')) || (($sCurrOperator != '>') && ($sCurrOperator != '>=')))
{
$aMergedCriterion[] = $aPrevCriterion;
return $aCurrCriterion;
}
// Merge into 'between' operation.
// The ends of the interval are included
$aCurrCriterion['operator'] = 'between_dates';
$oFormat = AttributeDate::GetFormat();
$sLastDate = $aPrevCriterion['values'][0]['value'];
$oDate = new DateTime($sLastDate);
if ($sPrevOperator == '<')
{
// previous day to include ends
$oDate->sub(DateInterval::createFromDateString('1 day'));
}
$sLastDateValue = $oDate->format(AttributeDate::GetSQLFormat());
$sLastDateLabel = $oFormat->format($oDate);
$sFirstDate = $aCurrCriterion['values'][0]['value'];
$oDate = new DateTime($sFirstDate);
if ($sCurrOperator == '>')
{
// next day to include ends
$oDate->add(DateInterval::createFromDateString('1 day'));
}
$sFirstDateValue = $oDate->format(AttributeDate::GetSQLFormat());
$sFirstDateLabel = $oFormat->format($oDate);
$aCurrCriterion['values'] = array();
$aCurrCriterion['values'][] = array('value' => $sFirstDateValue, 'label' => $sFirstDateLabel);
$aCurrCriterion['values'][] = array('value' => $sLastDateValue, 'label' => $sLastDateLabel);
$aCurrCriterion['oql'] = "({$aPrevCriterion['oql']} AND {$aCurrCriterion['oql']})";
$aCurrCriterion['label'] = $aPrevCriterion['label'].' '.Dict::S('Expression:Operator:AND', 'AND').' '.$aCurrCriterion['label'];
$aMergedCriterion[] = $aCurrCriterion;
return null;
}
/**
* @param $aPrevCriterion
* @param $aCurrCriterion
* @param $aMergedCriterion
*
* @return Current criteria or null if merged
* @throws \Exception
*/
protected static function MergeDateTime($aPrevCriterion, $aCurrCriterion, &$aMergedCriterion)
{
$sPrevOperator = $aPrevCriterion['operator'];
$sCurrOperator = $aCurrCriterion['operator'];
if ((($sPrevOperator != '<') && ($sPrevOperator != '<=')) || (($sCurrOperator != '>') && ($sCurrOperator != '>=')))
{
$aMergedCriterion[] = $aPrevCriterion;
return $aCurrCriterion;
}
// Merge into 'between' operation.
// The ends of the interval are included
$sLastDate = $aPrevCriterion['values'][0]['value'];
$sFirstDate = $aCurrCriterion['values'][0]['value'];
$oDate = new DateTime($sLastDate);
$aCurrCriterion['operator'] = 'between_dates';
$sInterval = '1 second';
if ($sPrevOperator == '<')
{
// previous day/second to include ends
$oDate->sub(DateInterval::createFromDateString($sInterval));
}
$sLastDateValue = $oDate->format(AttributeDateTime::GetSQLFormat());
$sLastDateLabel = AttributeDateTime::GetFormat()->Format($sLastDateValue);
$oDate = new DateTime($sFirstDate);
if ($sCurrOperator == '>')
{
// next day/second to include ends
$oDate->add(DateInterval::createFromDateString($sInterval));
}
$sFirstDateValue = $oDate->format(AttributeDateTime::GetSQLFormat());
$sFirstDateLabel = AttributeDateTime::GetFormat()->Format($sFirstDateValue);
$aCurrCriterion['values'] = array();
$aCurrCriterion['values'][] = array('value' => $sFirstDateValue, 'label' => $sFirstDateLabel);
$aCurrCriterion['values'][] = array('value' => $sLastDateValue, 'label' => $sLastDateLabel);
$aCurrCriterion['oql'] = "({$aPrevCriterion['oql']} AND {$aCurrCriterion['oql']})";
$aCurrCriterion['label'] = $aPrevCriterion['label'].' '.Dict::S('Expression:Operator:AND',
'AND').' '.$aCurrCriterion['label'];
$aMergedCriterion[] = $aCurrCriterion;
return null;
}
/**
* @param $aPrevCriterion
* @param $aCurrCriterion
* @param $aMergedCriterion
*
* @return Current criteria or null if merged
* @throws \Exception
*/
protected static function MergeNumeric($aPrevCriterion, $aCurrCriterion, &$aMergedCriterion)
{
$sPrevOperator = $aPrevCriterion['operator'];
$sCurrOperator = $aCurrCriterion['operator'];
if (($sPrevOperator != '<=') || ($sCurrOperator != '>='))
{
$aMergedCriterion[] = $aPrevCriterion;
return $aCurrCriterion;
}
// Merge into 'between' operation.
$sLastNum = $aPrevCriterion['values'][0]['value'];
$sFirstNum = $aCurrCriterion['values'][0]['value'];
$aCurrCriterion['values'] = array();
$aCurrCriterion['values'][] = array('value' => $sFirstNum, 'label' => "$sFirstNum");
$aCurrCriterion['values'][] = array('value' => $sLastNum, 'label' => "$sLastNum");
$aCurrCriterion['oql'] = "({$aPrevCriterion['oql']} AND {$aCurrCriterion['oql']})";
$aCurrCriterion['label'] = $aPrevCriterion['label'].' '.Dict::S('Expression:Operator:AND', 'AND').' '.$aCurrCriterion['label'];
$aCurrCriterion['operator'] = 'between';
$aMergedCriterion[] = $aCurrCriterion;
return null;
}
private static function SerializeValues($aValues)
{
$aSerializedValues = array();
foreach($aValues as $aValue)
{
$aSerializedValues[] = serialize($aValue);
}
return $aSerializedValues;
}
protected static function MergeEnumExtKeys($aPrevCriterion, $aCurrCriterion, &$aMergedCriterion)
{
$aFirstValues = self::SerializeValues($aPrevCriterion['values']);
$aNextValues = self::SerializeValues($aCurrCriterion['values']);
// Keep only the common values
$aCurrCriterion['values'] = array_map("unserialize", array_intersect($aFirstValues, $aNextValues));
$aMergedCriterion[] = $aCurrCriterion;
return null;
}
protected static function TextToSearchForm($aCriteria, $aFields)
{
$sOperator = $aCriteria['operator'];
$sValue = $aCriteria['values'][0]['value'];
$bStartWithPercent = substr($sValue, 0, 1) == '%' ? true : false;
$bEndWithPercent = substr($sValue, -1) == '%' ? true : false;
switch (true)
{
case ('' == $sValue and ($sOperator == '=' or $sOperator == 'LIKE')):
$aCriteria['operator'] = CriterionConversionAbstract::OP_EMPTY;
break;
case ('' == $sValue and $sOperator == '!='):
$aCriteria['operator'] = CriterionConversionAbstract::OP_NOT_EMPTY;
break;
case ($sOperator == 'LIKE' && $bStartWithPercent && $bEndWithPercent):
$aCriteria['operator'] = CriterionConversionAbstract::OP_CONTAINS;
$sValue = substr($sValue, 1, -1);
$aCriteria['values'][0]['value'] = $sValue;
$aCriteria['values'][0]['label'] = "$sValue";
break;
case ($sOperator == 'LIKE' && $bStartWithPercent):
$aCriteria['operator'] = CriterionConversionAbstract::OP_ENDS_WITH;
$sValue = substr($sValue, 1);
$aCriteria['values'][0]['value'] = $sValue;
$aCriteria['values'][0]['label'] = "$sValue";
break;
case ($sOperator == 'LIKE' && $bEndWithPercent):
$aCriteria['operator'] = CriterionConversionAbstract::OP_STARTS_WITH;
$sValue = substr($sValue, 0, -1);
$aCriteria['values'][0]['value'] = $sValue;
$aCriteria['values'][0]['label'] = "$sValue";
break;
}
return $aCriteria;
}
protected static function ExternalFieldToSearchForm($aCriteria, $aFields)
{
$sOperator = $aCriteria['operator'];
$sValue = $aCriteria['values'][0]['value'];
$bStartWithPercent = substr($sValue, 0, 1) == '%' ? true : false;
$bEndWithPercent = substr($sValue, -1) == '%' ? true : false;
switch (true)
{
case ($sOperator == 'ISNULL'):
case ('' == $sValue and ($sOperator == 'LIKE')):
$aCriteria['operator'] = CriterionConversionAbstract::OP_EMPTY;
break;
case ('' == $sValue and $sOperator == '!='):
$aCriteria['operator'] = CriterionConversionAbstract::OP_NOT_EMPTY;
break;
case ($sOperator == 'LIKE' && $bStartWithPercent && $bEndWithPercent):
$aCriteria['operator'] = CriterionConversionAbstract::OP_CONTAINS;
$sValue = substr($sValue, 1, -1);
$aCriteria['values'][0]['value'] = $sValue;
$aCriteria['values'][0]['label'] = "$sValue";
break;
case ($sOperator == 'LIKE' && $bStartWithPercent):
$aCriteria['operator'] = CriterionConversionAbstract::OP_ENDS_WITH;
$sValue = substr($sValue, 1);
$aCriteria['values'][0]['value'] = $sValue;
$aCriteria['values'][0]['label'] = "$sValue";
break;
case ($sOperator == 'LIKE' && $bEndWithPercent):
$aCriteria['operator'] = CriterionConversionAbstract::OP_STARTS_WITH;
$sValue = substr($sValue, 0, -1);
$aCriteria['values'][0]['value'] = $sValue;
$aCriteria['values'][0]['label'] = "$sValue";
break;
}
return $aCriteria;
}
protected static function DateTimeToSearchForm($aCriteria, $aFields)
{
if (!array_key_exists('is_relative', $aCriteria) || !$aCriteria['is_relative'])
{
// Convert '=' in 'between'
if (isset($aCriteria['operator']) && ($aCriteria['operator'] === '='))
{
$aCriteria['operator'] = CriterionConversionAbstract::OP_BETWEEN_DATES;
$sWidget = $aCriteria['widget'];
if ($sWidget == AttributeDefinition::SEARCH_WIDGET_TYPE_DATE)
{
$aCriteria['values'][1] = $aCriteria['values'][0];
}
else
{
$sDate = $aCriteria['values'][0]['value'];
$oDate = new DateTime($sDate);
$sFirstDateValue = $oDate->format(AttributeDateTime::GetSQLFormat());
$sFirstDateLabel = AttributeDateTime::GetFormat()->Format($sFirstDateValue);
$aCriteria['values'][0] = array('value' => $sFirstDateValue, 'label' => "$sFirstDateLabel");
$oDate->add(DateInterval::createFromDateString('1 day'));
$oDate->sub(DateInterval::createFromDateString('1 second'));
$sLastDateValue = $oDate->format(AttributeDateTime::GetSQLFormat());
$sLastDateLabel = AttributeDateTime::GetFormat()->Format($sLastDateValue);
$aCriteria['values'][1] = array('value' => $sLastDateValue, 'label' => "$sLastDateLabel");
}
}
return $aCriteria;
}
if (isset($aCriteria['values'][0]['value']))
{
$sLabel = $aCriteria['values'][0]['value'];
if (isset($aCriteria['verb']))
{
switch ($aCriteria['verb'])
{
case 'DATE_SUB':
$sLabel = '-'.$sLabel;
break;
case 'DATE_ADD':
$sLabel = '+'.$sLabel;
break;
}
}
if (isset($aCriteria['unit']))
{
$sLabel .= Dict::S('Expression:Unit:Short:'.$aCriteria['unit'], $aCriteria['unit']);
}
$aCriteria['values'][0]['label'] = "$sLabel";
}
// Temporary until the JS widget support relative dates
$aCriteria['widget'] = AttributeDefinition::SEARCH_WIDGET_TYPE_RAW;
return $aCriteria;
}
protected static function NumericToSearchForm($aCriteria, $aFields)
{
if ($aCriteria['operator'] == 'ISNULL')
{
$aCriteria['operator'] = CriterionConversionAbstract::OP_EMPTY;
}
return $aCriteria;
}
protected static function EnumToSearchForm($aCriteria, $aFields)
{
$sOperator = $aCriteria['operator'];
switch ($sOperator)
{
case '=':
// Same as IN
$aCriteria['operator'] = CriterionConversionAbstract::OP_IN;
break;
case 'NOT IN':
case 'NOTIN':
case '!=':
// Same as NOT IN
$aCriteria = self::RevertValues($aCriteria, $aFields);
break;
case 'IN':
// Nothing special to do
break;
case 'OR':
case 'ISNULL':
// Special case when undefined and/or other values are selected
$aCriteria['operator'] = CriterionConversionAbstract::OP_IN;
if (isset($aCriteria['has_undefined']) && $aCriteria['has_undefined'])
{
if (!isset($aCriteria['values']))
{
$aCriteria['values'] = array();
}
// Convention for 'undefined' enums
$aCriteria['values'][] = array('value' => 'null', 'label' => 'null');
}
break;
default:
// Unknown operator
$aCriteria['widget'] = AttributeDefinition::SEARCH_WIDGET_TYPE_RAW;
break;
}
return $aCriteria;
}
protected static function ExternalKeyToSearchForm($aCriteria, $aFields)
{
$sOperator = $aCriteria['operator'];
switch ($sOperator)
{
case '=':
// Same as IN
$aCriteria['operator'] = CriterionConversionAbstract::OP_IN;
break;
case 'IN':
// Nothing special to do
break;
default:
// Unknown operator
$aCriteria['widget'] = AttributeDefinition::SEARCH_WIDGET_TYPE_RAW;
break;
}
return $aCriteria;
}
/**
* @param $aCriteria
* @param $aFields
*
* @return mixed
*/
protected static function RevertValues($aCriteria, $aFields)
{
$sRef = $aCriteria['ref'];
$aValues = $aCriteria['values'];
if (array_key_exists($sRef, $aFields))
{
$aField = $aFields[$sRef];
if (array_key_exists('allowed_values', $aField) && array_key_exists('values', $aField['allowed_values']))
{
$aAllowedValues = $aField['allowed_values']['values'];
}
else
{
// Can't obtain the list of allowed values, just set as unknown
$aCriteria['widget'] = AttributeDefinition::SEARCH_WIDGET_TYPE_RAW;
}
}
if (isset($aAllowedValues))
{
foreach($aValues as $aValue)
{
$sValue = $aValue['value'];
unset($aAllowedValues[$sValue]);
}
$aCriteria['values'] = array();
foreach($aAllowedValues as $sValue => $sLabel)
{
$aValue = array('value' => $sValue, 'label' => "$sLabel");
$aCriteria['values'][] = $aValue;
}
$aCriteria['operator'] = 'IN';
}
return $aCriteria;
}
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* Copyright (C) 2010-2018 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\Application\Search;
abstract class CriterionConversionAbstract
{
const OP_CONTAINS = 'contains';
const OP_STARTS_WITH = 'starts_with';
const OP_ENDS_WITH = 'ends_with';
const OP_EMPTY = 'empty';
const OP_NOT_EMPTY = 'not_empty';
const OP_IN = 'IN';
const OP_BETWEEN_DATES = 'between_dates';
const OP_BETWEEN = 'between';
const OP_ALL = 'all';
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* Copyright (C) 2010-2018 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/>
*
*/
/**
* Created by PhpStorm.
* User: Eric
* Date: 08/03/2018
* Time: 11:25
*/
namespace Combodo\iTop\Application\Search;
use Combodo\iTop\Application\Search\CriterionConversion\CriterionToOQL;
use DBObjectSearch;
use Expression;
use IssueLog;
use OQLException;
class CriterionParser
{
/**
* @param $sBaseOql
* @param $aCriterion
* @param $sHiddenCriteria
*
* @return \DBSearch
*/
public static function Parse($sBaseOql, $aCriterion, $sHiddenCriteria = null)
{
$aExpression = array();
$aOr = $aCriterion['or'];
foreach($aOr as $aAndList)
{
$sExpression = self::ParseAndList($aAndList['and']);
if (!empty($sExpression))
{
$aExpression[] = $sExpression;
}
}
try
{
$oSearch = DBObjectSearch::FromOQL($sBaseOql);
if (!empty($sHiddenCriteria))
{
$oHiddenCriteriaExpression = Expression::FromOQL($sHiddenCriteria);
$oSearch->AddConditionExpression($oHiddenCriteriaExpression);
}
if (empty($aExpression))
{
return $oSearch;
}
$oExpression = Expression::FromOQL(implode(" OR ", $aExpression));
$oSearch->AddConditionExpression($oExpression);
return $oSearch;
} catch (OQLException $e)
{
IssueLog::Error($e->getMessage());
}
return null;
}
private static function ParseAndList($aAnd)
{
$aExpression = array();
foreach($aAnd as $aCriteria)
{
$aExpression[] = CriterionToOQL::Convert($aCriteria);
}
if (empty($aExpression))
{
return '';
}
return '('.implode(" AND ", $aExpression).')';
}
}

View File

@@ -0,0 +1,483 @@
<?php
/**
* Copyright (C) 2010-2018 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\Application\Search;
use ApplicationContext;
use AttributeDefinition;
use CMDBObjectSet;
use Combodo\iTop\Application\Search\CriterionConversion\CriterionToSearchForm;
use CoreException;
use DBObjectSearch;
use DBObjectSet;
use Dict;
use Exception;
use Expression;
use IssueLog;
use MetaModel;
use TrueExpression;
use utils;
use WebPage;
class SearchForm
{
/**
* @param \WebPage $oPage
* @param \CMDBObjectSet $oSet
* @param array $aExtraParams
*
* @return string
* @throws \CoreException
* @throws \DictExceptionMissingString
*/
public function GetSearchForm(WebPage $oPage, CMDBObjectSet $oSet, $aExtraParams = array())
{
$sHtml = '';
$oAppContext = new ApplicationContext();
$sClassName = $oSet->GetFilter()->GetClass();
$aListParams = array();
foreach($aExtraParams as $key => $value)
{
$aListParams[$key] = $value;
}
// Simple search form
if (isset($aExtraParams['currentId']))
{
$sSearchFormId = $aExtraParams['currentId'];
}
else
{
$iSearchFormId = $oPage->GetUniqueId();
$sSearchFormId = 'SimpleSearchForm'.$iSearchFormId;
$sHtml .= "<div id=\"ds_$sSearchFormId\" class=\"mini_tab{$iSearchFormId}\">\n";
$aListParams['currentId'] = "$iSearchFormId";
}
// Check if the current class has some sub-classes
if (isset($aExtraParams['baseClass']))
{
$sRootClass = $aExtraParams['baseClass'];
}
else
{
$sRootClass = $sClassName;
}
$sJson = utils::ReadParam('json', '', false, 'raw_data');
if (!empty($sJson))
{
$aListParams['json'] = json_decode($sJson, true);
}
if (!isset($aExtraParams['result_list_outer_selector']))
{
if (isset($aExtraParams['table_id']))
{
$aExtraParams['result_list_outer_selector'] = $aExtraParams['table_id'];
}
else
{
$aExtraParams['result_list_outer_selector'] = "search_form_result_{$sSearchFormId}";
}
}
$aSubClasses = MetaModel::GetSubclasses($sRootClass);
if (count($aSubClasses) > 0)
{
$aOptions = array();
$aOptions[MetaModel::GetName($sRootClass)] = "<option value=\"$sRootClass\">".MetaModel::GetName($sRootClass)."</options>\n";
foreach($aSubClasses as $sSubclassName)
{
$aOptions[MetaModel::GetName($sSubclassName)] = "<option value=\"$sSubclassName\">".MetaModel::GetName($sSubclassName)."</options>\n";
}
$aOptions[MetaModel::GetName($sClassName)] = "<option selected value=\"$sClassName\">".MetaModel::GetName($sClassName)."</options>\n";
ksort($aOptions);
$sContext = $oAppContext->GetForLink();
$sClassesCombo = "<select name=\"class\" onChange=\"ReloadSearchForm('$sSearchFormId', this.value, '$sRootClass', '$sContext', '{$aExtraParams['result_list_outer_selector']}')\">\n".implode('',
$aOptions)."</select>\n";
}
else
{
$sClassesCombo = MetaModel::GetName($sClassName);
}
$sAction = (isset($aExtraParams['action'])) ? $aExtraParams['action'] : utils::GetAbsoluteUrlAppRoot().'pages/UI.php';
$sStyle = (isset($aExtraParams['open']) && ($aExtraParams['open'] == 'true')) ? 'opened' : '';
$sHtml .= "<form id=\"fs_{$sSearchFormId}\" action=\"{$sAction}\" class=\"{$sStyle}\">\n"; // Don't use $_SERVER['SCRIPT_NAME'] since the form may be called asynchronously (from ajax.php)
$sHtml .= "<h2 class=\"sf_title\"><span class=\"sft_picto fa fa-search\"></span>" . Dict::Format('UI:SearchFor_Class_Objects', $sClassesCombo) . "<a class=\"sft_toggler fa fa-caret-down pull-right\" href=\"#\" title=\"" . Dict::S('UI:Search:Toggle') . "\"></a><a class=\"sft_refresh fa fa-search pull-right\" href=\"#\" title=\"" . Dict::S('UI:Button:Refresh') . "\"></a></h2>\n";
$sHtml .= "<div id=\"fs_{$sSearchFormId}_message\" class=\"sf_message header_message\"></div>\n";
$sHtml .= "<div id=\"fs_{$sSearchFormId}_criterion_outer\">\n</div>\n";
$sHtml .= "</form>\n";
if (isset($aExtraParams['query_params']))
{
$aArgs = $aExtraParams['query_params'];
}
else
{
$aArgs = array();
}
$bIsRemovable = true;
if (isset($aExtraParams['selection_mode']) && $aExtraParams['selection_mode'])
{
// Mark all criterion as read-only and non-removable
$bIsRemovable = false;
}
$aFields = $this->GetFields($oSet);
$oSearch = $oSet->GetFilter();
$aCriterion = $this->GetCriterion($oSearch, $aFields, $aArgs, $bIsRemovable);
$aClasses = $oSearch->GetSelectedClasses();
$sClassAlias = '';
foreach($aClasses as $sAlias => $sClass)
{
$sClassAlias = $sAlias;
}
$oBaseSearch = $oSearch->DeepClone();
$oBaseSearch->ResetCondition();
$sBaseOQL = str_replace(' WHERE 1', '', $oBaseSearch->ToOQL());
if (isset($aExtraParams['table_inner_id']))
{
$sDataConfigListSelector = $aExtraParams['table_inner_id'];
}
else
{
$sDataConfigListSelector = $aExtraParams['result_list_outer_selector'];
}
if (!isset($aExtraParams['table_inner_id']))
{
$aListParams['table_inner_id'] = "table_inner_id_{$sSearchFormId}";
}
$bOpen = false;
if (isset($aExtraParams['open']))
{
$bOpen = $aExtraParams['open'];
}
$sDebug = utils::ReadParam('debug', 'false', false, 'parameter');
if ($sDebug == 'true')
{
$aListParams['debug'] = 'true';
}
$aDaysMin = array(Dict::S('DayOfWeek-Sunday-Min'), Dict::S('DayOfWeek-Monday-Min'), Dict::S('DayOfWeek-Tuesday-Min'), Dict::S('DayOfWeek-Wednesday-Min'),
Dict::S('DayOfWeek-Thursday-Min'), Dict::S('DayOfWeek-Friday-Min'), Dict::S('DayOfWeek-Saturday-Min'));
$aMonthsShort = array(Dict::S('Month-01-Short'), Dict::S('Month-02-Short'), Dict::S('Month-03-Short'), Dict::S('Month-04-Short'), Dict::S('Month-05-Short'), Dict::S('Month-06-Short'),
Dict::S('Month-07-Short'), Dict::S('Month-08-Short'), Dict::S('Month-09-Short'), Dict::S('Month-10-Short'), Dict::S('Month-11-Short'), Dict::S('Month-12-Short'));
$aSearchParams = array(
'criterion_outer_selector' => "#fs_{$sSearchFormId}_criterion_outer",
'result_list_outer_selector' => "#{$aExtraParams['result_list_outer_selector']}",
'data_config_list_selector' => "#{$sDataConfigListSelector}",
'endpoint' => utils::GetAbsoluteUrlAppRoot().'pages/ajax.searchform.php',
'init_opened' => $bOpen,
'datepicker' => array(
'dayNamesMin' => $aDaysMin,
'monthNamesShort' => $aMonthsShort,
'firstDay' => (int) Dict::S('Calendar-FirstDayOfWeek'),
// 'date_format' => AttributeDate::GetFormat()->ToDatePicker(),
// 'date_time_format' => AttributeDateTime::GetFormat()->ToMomentJS(),
),
'list_params' => $aListParams,
'search' => array(
'has_hidden_criteria' => (array_key_exists('hidden_criteria', $aListParams) && !empty($aListParams['hidden_criteria'])),
'fields' => $aFields,
'criterion' => $aCriterion,
'class_name' => $sClassName,
'class_alias' => $sClassAlias,
'base_oql' => $sBaseOQL,
),
);
$oPage->add_ready_script('$("#fs_'.$sSearchFormId.'").search_form_handler('.json_encode($aSearchParams).');');
return $sHtml;
}
/**
* @param DBObjectSet $oSet
*
* @return array
*/
public function GetFields($oSet)
{
$oSearch = $oSet->GetFilter();
$aAllClasses = $oSearch->GetSelectedClasses();
$aAuthorizedClasses = array();
foreach($aAllClasses as $sAlias => $sClassName)
{
if (\UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $oSet) != UR_ALLOWED_NO)
{
$aAuthorizedClasses[$sAlias] = $sClassName;
}
}
$aAllFields = array('zlist' => array(), 'others' => array());
try
{
foreach($aAuthorizedClasses as $sAlias => $sClass)
{
$aZList = array();
$aOthers = array();
$this->PopulateFiledList($sClass, $sAlias, $aZList, $aOthers);
$aAllFields[$sAlias.'_zlist'] = $aZList;
$aAllFields[$sAlias.'_others'] = $aOthers;
}
}
catch (CoreException $e)
{
IssueLog::Error($e->getMessage());
}
$aSelectedClasses = $oSearch->GetSelectedClasses();
foreach($aSelectedClasses as $sAlias => $sClassName)
{
$aAllFields['zlist'] = array_merge($aAllFields['zlist'], $aAllFields[$sAlias.'_zlist']);
unset($aAllFields[$sAlias.'_zlist']);
$aAllFields['others'] = array_merge($aAllFields['others'], $aAllFields[$sAlias.'_others']);
unset($aAllFields[$sAlias.'_others']);
}
return $aAllFields;
}
/**
* @param $sClass
* @param $sAlias
* @param $aZList
* @param $aOthers
*
* @throws \CoreException
*/
protected function PopulateFiledList($sClass, $sAlias, &$aZList, &$aOthers)
{
$aAttributeDefs = MetaModel::ListAttributeDefs($sClass);
$aList = MetaModel::GetZListItems($sClass, 'standard_search');
foreach($aList as $sAttCode)
{
if (array_key_exists($sAttCode, $aAttributeDefs))
{
$oAttDef = $aAttributeDefs[$sAttCode];
$aZList = $this->AppendField($sClass, $sAlias, $sAttCode, $oAttDef, $aZList);
unset($aAttributeDefs[$sAttCode]);
}
}
uasort($aZList, function ($aItem1, $aItem2) {
return strcmp($aItem1['label'], $aItem2['label']);
});
$aZList = $this->AppendId($sClass, $sAlias, $aZList);
foreach($aAttributeDefs as $sAttCode => $oAttDef)
{
if ($this->IsSubAttribute($oAttDef)) continue;
$aOthers = $this->AppendField($sClass, $sAlias, $sAttCode, $oAttDef, $aOthers);
}
uasort($aOthers, function ($aItem1, $aItem2) {
return strcmp($aItem1['label'], $aItem2['label']);
});
}
protected function IsSubAttribute($oAttDef)
{
return (($oAttDef instanceof AttributeFriendlyName) || ($oAttDef instanceof AttributeExternalField) || ($oAttDef instanceof AttributeSubItem));
}
/**
* @param $oAttrDef
*
* @return array
*/
public static function GetFieldAllowedValues($oAttrDef)
{
if ($oAttrDef->IsExternalKey(EXTKEY_ABSOLUTE))
{
$sTargetClass = $oAttrDef->GetTargetClass();
try
{
$oSearch = new DBObjectSearch($sTargetClass);
} catch (Exception $e)
{
IssueLog::Error($e->getMessage());
return array('values' => array());
}
$oSearch->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', true);
$oSet = new DBObjectSet($oSearch);
$iCount = $oSet->Count();
if ($iCount > MetaModel::GetConfig()->Get('max_combo_length'))
{
return array('autocomplete' => true, 'count' => $iCount);
}
}
else
{
if (method_exists($oAttrDef, 'GetAllowedValuesAsObjectSet'))
{
$oSet = $oAttrDef->GetAllowedValuesAsObjectSet();
$iCount = $oSet->Count();
if ($iCount > MetaModel::GetConfig()->Get('max_combo_length'))
{
return array('autocomplete' => true, 'count' => $iCount);
}
}
}
$aAllowedValues = $oAttrDef->GetAllowedValues();
return array('values' => $aAllowedValues, 'count' => count($aAllowedValues));
}
/**
* @param \DBObjectSearch $oSearch
* @param array $aFields
*
* @param array $aArgs
*
* @param bool $bIsRemovable
*
* @return array
*/
public function GetCriterion($oSearch, $aFields, $aArgs = array(), $bIsRemovable = true)
{
$oExpression = $oSearch->GetCriteria();
if (!empty($aArgs))
{
$aArgs = MetaModel::PrepareQueryArguments($aArgs);
$sOQL = $oExpression->Render($aArgs);
$oExpression = Expression::FromOQL($sOQL);
}
$aOrCriterion = array();
$aORExpressions = Expression::Split($oExpression, 'OR');
foreach($aORExpressions as $oORSubExpr)
{
$aAndCriterion = array();
$aAndExpressions = Expression::Split($oORSubExpr, 'AND');
foreach($aAndExpressions as $oAndSubExpr)
{
if (($oAndSubExpr instanceof TrueExpression) || ($oAndSubExpr->Render() == 1))
{
continue;
}
$aAndCriterion[] = $oAndSubExpr->GetCriterion($oSearch);
}
$aAndCriterion = CriterionToSearchForm::Convert($aAndCriterion, $aFields, $oSearch->GetJoinedClasses(), $bIsRemovable);
$aOrCriterion[] = array('and' => $aAndCriterion);
}
return array('or' => $aOrCriterion);
}
/**
* @param $sClass
* @param $sClassAlias
* @param $aFields
*
* @return mixed
*/
private function AppendId($sClass, $sClassAlias, $aFields)
{
$aField = array();
$aField['code'] = 'id';
$aField['class'] = $sClass;
$aField['class_alias'] = $sClassAlias;
$aField['label'] = 'Id';
$aField['widget'] = AttributeDefinition::SEARCH_WIDGET_TYPE_NUMERIC;
$aField['is_null_allowed'] = false;
$aNewFields = array($sClassAlias.'.id' => $aField);
$aFields = array_merge($aNewFields, $aFields);
return $aFields;
}
/**
* @param $sClass
* @param $sClassAlias
* @param $sFilterCode
* @param $oAttDef
* @param $aFields
*
* @return mixed
*/
private function AppendField($sClass, $sClassAlias, $sFilterCode, $oAttDef, $aFields)
{
if (!is_null($oAttDef) && ($oAttDef->GetSearchType() != AttributeDefinition::SEARCH_WIDGET_TYPE_RAW))
{
if (method_exists($oAttDef, 'GetLabelForSearchField'))
{
$sLabel = $oAttDef->GetLabelForSearchField();
}
else
{
$sLabel = $oAttDef->GetLabel();
}
if (method_exists($oAttDef, 'GetTargetClass'))
{
$sTargetClass = $oAttDef->GetTargetClass();
}
else
{
$sTargetClass = $oAttDef->GetHostClass();
}
$aField = array();
$aField['code'] = $sFilterCode;
$aField['class'] = $sClass;
$aField['class_alias'] = $sClassAlias;
$aField['target_class'] = $sTargetClass;
$aField['label'] = $sLabel;
$aField['widget'] = $oAttDef->GetSearchType();
$aField['allowed_values'] = self::GetFieldAllowedValues($oAttDef);
$aField['is_null_allowed'] = $oAttDef->IsNullAllowed();
$aFields[$sClassAlias.'.'.$sFilterCode] = $aField;
// Sub items
//
// if ($oAttDef->IsSearchable())
// {
// $sShortLabel = $oAttDef->GetLabel();
// $sLabel = $sShortAlias.$oAttDef->GetLabel();
// $aSubAttr = $this->GetSubAttributes($sClass, $sFilterCode, $oAttDef);
// $aValidSubAttr = array();
// foreach($aSubAttr as $aSubAttDef)
// {
// $aValidSubAttr[] = array('attcodeex' => $aSubAttDef['code'], 'code' => $sShortAlias.$aSubAttDef['code'], 'label' => $aSubAttDef['label'], 'unique_label' => $sShortAlias.$aSubAttDef['unique_label']);
// }
// $aAllFields[] = array('attcodeex' => $sFilterCode, 'code' => $sShortAlias.$sFilterCode, 'label' => $sShortLabel, 'unique_label' => $sLabel, 'subattr' => $aValidSubAttr);
// }
}
return $aFields;
}
}

View File

@@ -19,9 +19,6 @@
namespace Combodo\iTop\Form\Field;
use \AttributeDatetime;
use \Combodo\iTop\Form\Field\StringField;
/**
* A field for Dates and Date & Times, supporting custom formats
*/
@@ -30,7 +27,7 @@ class DateTimeField extends StringField
protected $sJSDateTimeFormat;
protected $sPHPDateTimeFormat;
protected $bDateOnly;
/**
* Overloaded constructor
*
@@ -42,9 +39,10 @@ class DateTimeField extends StringField
parent::__construct($sId, $onFinalizeCallback);
$this->bDateOnly = false;
}
/**
* Get the PHP format string
*
* @return string
*/
public function GetPHPDateTimeFormat()
@@ -55,14 +53,16 @@ class DateTimeField extends StringField
/**
*
* @param string $sFormat
*
* @return \Combodo\iTop\Form\Field\DateTimeField
*/
public function SetPHPDateTimeFormat($sDateTimeFormat)
{
$this->sPHPDateTimeFormat = $sDateTimeFormat;
return $this;
}
/**
* @return string
*/
@@ -74,23 +74,26 @@ class DateTimeField extends StringField
/**
*
* @param string $sFormat
*
* @return \Combodo\iTop\Form\Field\DateTimeField
*/
public function SetJSDateTimeFormat($sDateTimeFormat)
{
$this->sDateTimeFormat = $sDateTimeFormat;
return $this;
}
/**
* Set the DateOnly flag
*
* @return \Combodo\iTop\Form\Field\DateTimeField
*/
public function SetDateOnly($bDateOnly)
{
return $this->bDateOnly = $bDateOnly;
return $this;
}
/**
* @return bool
*/

View File

@@ -18,16 +18,16 @@
namespace Combodo\iTop\Renderer\Console\FieldRenderer;
use \Dict;
use AttributeDate;
use AttributeDateTime;
use AttributeDuration;
use Combodo\iTop\Form\Field\TextAreaField;
use Combodo\iTop\Renderer\FieldRenderer;
use Combodo\iTop\Renderer\RenderingOutput;
use Combodo\iTop\Form\Field\TextAreaField;
use \InlineImage;
use \UserRights;
use \AttributeDuration;
use \DateTimeFormat;
use \AttributeDateTime;
use \AttributeDate;
use DateTimeFormat;
use Dict;
use InlineImage;
use UserRights;
class ConsoleSimpleFieldRenderer extends FieldRenderer
{
@@ -217,7 +217,6 @@ EOF
switch ($sFieldClass)
{
case 'Combodo\\iTop\Form\\Field\\DateTimeField':
case 'Combodo\\iTop\\Form\\Field\\DateTimeField':
$sDateTimeFormat = $this->oField->GetPHPDateTimeFormat();
$oFormat = new DateTimeFormat($sDateTimeFormat);

View File

@@ -59,7 +59,7 @@ try
throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'oql'));
}
$oFilter = DBObjectSearch::FromOQL($sOQL);
$oBlock1 = new DisplayBlock($oFilter, 'search', false, array('menu'=>false));
$oBlock1 = new DisplayBlock($oFilter, 'search', false, array('menu' => false, 'table_id' => '1'));
$oBlock1->Display($oP, 0);
$oP->add('<p class="page-header">'.MetaModel::GetClassIcon('SynchroReplica').Dict::S('Core:SynchroReplica:ListOfReplicas').'</p>');
$iSourceId = utils::ReadParam('datasource', null);

View File

@@ -37,7 +37,6 @@ use Hypervisor;
use lnkContactToFunctionalCI;
use MetaModel;
use Person;
use PHPUnit\Framework\TestCase;
use Server;
use Ticket;
use URP_UserProfile;
@@ -92,14 +91,6 @@ class ItopDataTestCase extends ItopTestCase
return $this->testOrgId;
}
/**
* PHPUnit complains that there is no test... so here is a dummy one.
*/
public function testDummy()
{
$this->assertTrue(true);
}
/////////////////////////////////////////////////////////////////////////////
/// Database Utilities
/////////////////////////////////////////////////////////////////////////////
@@ -152,7 +143,7 @@ class ItopDataTestCase extends ItopTestCase
$oTicket = self::createObject('UserRequest', array(
'ref' => 'Ticket_'.$iNum,
'title' => 'BUG 789_'.$iNum,
'request_type' => 'incident',
//'request_type' => 'incident',
'description' => 'method UpdateImpactedItems() reconstruit le lnkContactToTicket donc impossible de rajouter des champs dans cette classe',
'org_id' => $this->getTestOrgId(),
));
@@ -176,7 +167,7 @@ class ItopDataTestCase extends ItopTestCase
$oTicket = self::createObject('UserRequest', array(
'ref' => 'Ticket_'.$iNum,
'title' => 'BUG 1161_'.$iNum,
'request_type' => 'incident',
//'request_type' => 'incident',
'description' => 'Add aggregate functions',
'time_spent' => $iTimeSpent,
'caller_id' => $iCallerId,
@@ -188,16 +179,20 @@ class ItopDataTestCase extends ItopTestCase
/**
* Create a Server in database
*
* @param int $iNum
* @param null $iRackUnit
*
* @return Server
* @throws Exception
* @throws \Exception
*/
protected function CreateServer($iNum)
protected function CreateServer($iNum, $iRackUnit = null)
{
/** @var Server $oServer */
$oServer = self::createObject('Server', array(
'name' => 'Server_'.$iNum,
'org_id' => $this->getTestOrgId(),
'nb_u' => $iRackUnit,
));
$this->debug("Created {$oServer->GetName()} ({$oServer->GetKey()})");
return $oServer;

View File

@@ -47,6 +47,9 @@
<testsuite name="Tickets">
<directory>itop-tickets</directory>
</testsuite>
<testsuite name="Application">
<directory>application</directory>
</testsuite>
</testsuites>
<!-- Code coverage white list -->

View File

@@ -0,0 +1,570 @@
<?php
/**
* Copyright (C) 2010-2018 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/>
*
*/
/**
* Created by PhpStorm.
* User: Eric
* Date: 08/03/2018
* Time: 16:46
*/
namespace Combodo\iTop\Test\UnitTest\Application\Search;
use Combodo\iTop\Application\Search\CriterionConversion\CriterionToOQL;
use Combodo\iTop\Application\Search\CriterionConversion\CriterionToSearchForm;
use Combodo\iTop\Application\Search\CriterionParser;
use Combodo\iTop\Application\Search\SearchForm;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
class CriterionConversionTest extends ItopDataTestCase
{
/**
* @throws \Exception
*/
protected function setUp()
{
parent::setUp();
require_once(APPROOT."sources/application/search/criterionconversionabstract.class.inc.php");
}
/**
* @dataProvider ToOqlProvider
*
* @param $sJSONCriterion
* @param $sExpectedOQL
*/
public function testToOql($sJSONCriterion, $sExpectedOQL)
{
$sOql = CriterionToOQL::Convert(
json_decode($sJSONCriterion, true)
);
$this->debug($sOql);
$this->assertEquals($sExpectedOQL, $sOql);
}
public function ToOqlProvider()
{
return array(
'>' => array(
'{
"ref": "UserRequest.start_date",
"values": [
{
"value": "2017-01-01",
"label": "2017-01-01 00:00:00"
}
],
"operator": ">",
"oql": ""
}',
"(`UserRequest`.`start_date` > '2017-01-01')"
),
'contains' => array(
'{
"ref": "Contact.name",
"values": [
{
"value": "toto",
"label": "toto"
}
],
"operator": "contains",
"oql": ""
}',
"(`Contact`.`name` LIKE '%toto%')"
),
'starts_with' => array(
'{
"ref": "Contact.name",
"values": [
{
"value": "toto",
"label": "toto"
}
],
"operator": "starts_with",
"oql": ""
}',
"(`Contact`.`name` LIKE 'toto%')"
),
'ends_with' => array(
'{
"ref": "Contact.name",
"values": [
{
"value": "toto",
"label": "toto"
}
],
"operator": "ends_with",
"oql": ""
}',
"(`Contact`.`name` LIKE '%toto')"
),
'empty' => array(
'{
"ref": "Contact.name",
"values": [
{
"value": "",
"label": ""
}
],
"operator": "empty",
"oql": ""
}',
"(`Contact`.`name` = '')"
),
'not_empty' => array(
'{
"ref": "Contact.name",
"values": [
{
"value": "",
"label": ""
}
],
"operator": "not_empty",
"oql": ""
}',
"(`Contact`.`name` != '')"
),
);
}
/**
* @dataProvider ToSearchFormProvider
*
* @param $aCriterion
* @param $sExpectedOperator
*
* @throws \OQLException
*/
function testToSearchForm($aCriterion, $sExpectedOperator)
{
$oSearchForm = new SearchForm();
$oSearch = \DBSearch::FromOQL("SELECT Contact");
$aFields = $oSearchForm->GetFields(new \DBObjectSet($oSearch));
$aRes = CriterionToSearchForm::Convert($aCriterion, $aFields, $oSearch->GetJoinedClasses());
$this->debug($aRes);
$this->assertEquals($sExpectedOperator, $aRes[0]['operator']);
}
function ToSearchFormProvider()
{
return array(
'=' => array(
json_decode('[
{
"ref": "Contact.name",
"widget": "string",
"values": [
{
"value": "toto",
"label": "toto"
}
],
"operator": "=",
"oql": "(`Contact`.`name` = \'toto\')"
}
]', true),
'='
),
'starts_with' => array(
json_decode('[
{
"ref": "Contact.name",
"widget": "string",
"values": [
{
"value": "toto%",
"label": "toto%"
}
],
"operator": "LIKE",
"oql": "(`Contact`.`name` LIKE \'toto%\')"
}
]', true),
'starts_with'
),
'ends_with' => array(
json_decode('[
{
"ref": "Contact.name",
"widget": "string",
"values": [
{
"value": "%toto",
"label": "%toto"
}
],
"operator": "LIKE",
"oql": "(`Contact`.`name` LIKE \'%toto\')"
}
]', true),
'ends_with'
),
'contains' => array(
json_decode('[
{
"widget": "string",
"ref": "Contact.name",
"values": [
{
"value": "%toto%",
"label": "%toto%"
}
],
"operator": "LIKE",
"oql": "(`Contact`.`name` LIKE \'%toto%\')"
}
]', true),
'contains'
),
'empty1' => array(
json_decode('[
{
"widget": "string",
"ref": "Contact.name",
"values": [
{
"value": "",
"label": ""
}
],
"operator": "LIKE",
"oql": "(`Contact`.`name` LIKE \'\')"
}
]', true),
'empty'
),
'empty2' => array(
json_decode('[
{
"widget": "string",
"ref": "Contact.name",
"values": [
{
"value": "",
"label": ""
}
],
"operator": "=",
"oql": "(`Contact`.`name` = \'\')"
}
]', true),
'empty'
),
'not_empty' => array(
json_decode('[
{
"widget": "string",
"ref": "Contact.name",
"values": [
{
"value": "",
"label": ""
}
],
"operator": "!=",
"oql": "(`Contact`.`name` != \'\')"
}
]', true),
'not_empty'
),
);
}
/**
* @dataProvider OqlProvider
*
* @param $sOQL
*
* @param $sExpectedOQL
*
* @param $aExpectedCriterion
*
* @throws \OQLException
*/
function testOqlToForSearchToOql($sOQL, $sExpectedOQL, $aExpectedCriterion)
{
$this->debug($sOQL);
$oSearchForm = new SearchForm();
$oSearch = \DBSearch::FromOQL($sOQL);
$aFields = $oSearchForm->GetFields(new \DBObjectSet($oSearch));
/** @var \DBObjectSearch $oSearch */
$aCriterion = $oSearchForm->GetCriterion($oSearch, $aFields);
$aAndCriterion = $aCriterion['or'][0]['and'];
$aNewCriterion = array();
foreach($aAndCriterion as $aCriteria)
{
if ($aCriteria['widget'] != \AttributeDefinition::SEARCH_WIDGET_TYPE_RAW)
{
unset($aCriteria['oql']);
foreach($aFields as $aCatFields)
{
if (isset($aCatFields[$aCriteria['ref']]))
{
$aField = $aCatFields[$aCriteria['ref']];
break;
}
}
if (isset($aField))
{
$aCriteria['code'] = $aField['code'];
$aCriteria['class'] = $aField['class'];
}
}
$aNewCriterion[] = $aCriteria;
}
$this->debug($aNewCriterion);
$this->assertFalse($this->array_diff_assoc_recursive($aExpectedCriterion, $aNewCriterion));
$aCriterion['or'][0]['and'] = $aNewCriterion;
$oSearch->ResetCondition();
$oFilter = CriterionParser::Parse($oSearch->ToOQL(), $aCriterion);
$sResultOQL = $oFilter->ToOQL();
$this->debug($sResultOQL);
$this->assertEquals($sExpectedOQL, $sResultOQL);
}
function OqlProvider()
{
return array(
'no criteria' => array(
'OQL' => 'SELECT WebApplication',
'ExpectedOQL' => "SELECT `WebApplication` FROM WebApplication AS `WebApplication` WHERE 1",
'ExpectedCriterion' => array(),
),
'string starts' => array(
'OQL' => "SELECT Contact WHERE name LIKE 'toto%'",
'ExpectedOQL' => "SELECT `Contact` FROM Contact AS `Contact` WHERE (`Contact`.`name` LIKE 'toto%')",
'ExpectedCriterion' => array(array('widget' => 'string', 'operator' => 'starts_with', 'values' => array(array('value' => 'toto')))),
),
'string ends' => array(
'OQL' => "SELECT Contact WHERE name LIKE '%toto'",
'ExpectedOQL' => "SELECT `Contact` FROM Contact AS `Contact` WHERE (`Contact`.`name` LIKE '%toto')",
'ExpectedCriterion' => array(array('widget' => 'string', 'operator' => 'ends_with', 'values' => array(array('value' => 'toto')))),
),
'string contains 1' => array(
'OQL' => "SELECT Contact WHERE name LIKE '%toto%'",
'ExpectedOQL' => "SELECT `Contact` FROM Contact AS `Contact` WHERE (`Contact`.`name` LIKE '%toto%')",
'ExpectedCriterion' => array(array('widget' => 'string', 'operator' => 'contains', 'values' => array(array('value' => 'toto')))),
),
'string contains 2' => array(
'OQL' => "SELECT Person AS B WHERE B.name LIKE '%A%'",
'ExpectedOQL' => "SELECT `B` FROM Person AS `B` WHERE (`B`.`name` LIKE '%A%')",
'ExpectedCriterion' => array(array('widget' => 'string', 'operator' => 'contains', 'values' => array(array('value' => 'A')))),
),
'string regexp' => array(
'OQL' => "SELECT Server WHERE name REGEXP '^dbserver[0-9]+\\\\\\\\..+\\\\\\\\.[a-z]{2,3}$'",
'ExpectedOQL' => "SELECT `Server` FROM Server AS `Server` WHERE (`Server`.`name` REGEXP '^dbserver[0-9]+\\\\..+\\\\.[a-z]{2,3}$')",
'ExpectedCriterion' => array(array('widget' => 'string', 'operator' => 'REGEXP')),
),
'enum + key =' => array(
'OQL' => "SELECT Contact WHERE status = 'active' AND org_id = 3",
'ExpectedOQL' => "SELECT `Contact` FROM Contact AS `Contact` WHERE ((`Contact`.`org_id` = '3') AND (`Contact`.`status` = 'active'))",
'ExpectedCriterion' => array(array('widget' => 'hierarchical_key', 'operator' => 'IN'), array('widget' => 'enum', 'operator' => 'IN')),
),
'enum =' => array(
'OQL' => "SELECT Contact WHERE status = 'active'",
'ExpectedOQL' => "SELECT `Contact` FROM Contact AS `Contact` WHERE (`Contact`.`status` = 'active')",
'ExpectedCriterion' => array(array('widget' => 'enum', 'operator' => 'IN', 'values' => array(array('value' => 'active')))),
),
'enum IN' => array(
'OQL' => "SELECT Contact WHERE status IN ('active', 'inactive')",
'ExpectedOQL' => "SELECT `Contact` FROM Contact AS `Contact` WHERE 1",
'ExpectedCriterion' => array(array('widget' => 'enum', 'operator' => 'IN', 'values' => array(array('value' => 'active'), array('value' => 'inactive')))),
),
'enum NOT IN 1' => array(
'OQL' => "SELECT Contact WHERE status NOT IN ('active')",
'ExpectedOQL' => "SELECT `Contact` FROM Contact AS `Contact` WHERE (`Contact`.`status` = 'inactive')",
'ExpectedCriterion' => array(array('widget' => 'enum', 'operator' => 'IN', 'values' => array(array('value' => 'inactive')))),
),
'enum NOT IN 2' => array(
'OQL' => "SELECT Person AS p JOIN UserRequest AS u ON u.agent_id = p.id WHERE u.status != 'closed'",
'ExpectedOQL' => "SELECT `p` FROM Person AS `p` JOIN UserRequest AS `u` ON `u`.agent_id = `p`.id WHERE (`u`.`status` != 'closed')",
'ExpectedCriterion' => array(array('widget' => 'raw')),
),
'enum undefined 1' => array(
'OQL' => "SELECT FunctionalCI WHERE ((business_criticity = 'high') OR ISNULL(business_criticity)) AND 1",
'ExpectedOQL' => "SELECT `FunctionalCI` FROM FunctionalCI AS `FunctionalCI` WHERE (((`FunctionalCI`.`business_criticity` = 'high') OR ISNULL(`FunctionalCI`.`business_criticity`)) AND 1)",
'ExpectedCriterion' => array(array('widget' => 'enum', 'has_undefined' => true, 'operator' => 'IN', 'values' => array(array('value' => 'high'), array('value' => 'null')))),
),
'enum undefined 2' => array(
'OQL' => "SELECT FunctionalCI WHERE ((business_criticity IN ('high', 'medium')) OR ISNULL(business_criticity)) AND 1",
'ExpectedOQL' => "SELECT `FunctionalCI` FROM FunctionalCI AS `FunctionalCI` WHERE (((`FunctionalCI`.`business_criticity` IN ('high', 'medium')) OR ISNULL(`FunctionalCI`.`business_criticity`)) AND 1)",
'ExpectedCriterion' => array(array('widget' => 'enum', 'has_undefined' => true, 'operator' => 'IN', 'values' => array(array('value' => 'high'), array('value' => 'medium'), array('value' => 'null')))),
),
'enum undefined 3' => array(
'OQL' => "SELECT FunctionalCI WHERE ISNULL(business_criticity)",
'ExpectedOQL' => "SELECT `FunctionalCI` FROM FunctionalCI AS `FunctionalCI` WHERE ISNULL(`FunctionalCI`.`business_criticity`)",
'ExpectedCriterion' => array(array('widget' => 'enum', 'has_undefined' => true, 'operator' => 'IN', 'values' => array(array('value' => 'null')))),
),
'key NOT IN' => array(
'OQL' => "SELECT Contact WHERE org_id NOT IN ('1')",
'ExpectedOQL' => "SELECT `Contact` FROM Contact AS `Contact` WHERE (`Contact`.`org_id` NOT IN ('1'))",
'ExpectedCriterion' => array(array('widget' => 'raw', 'operator' => 'NOT IN')),
),
'key IN' => array(
'OQL' => "SELECT Contact WHERE org_id IN ('1')",
'ExpectedOQL' => "SELECT `Contact` FROM Contact AS `Contact` WHERE (`Contact`.`org_id` = '1')",
'ExpectedCriterion' => array(array('widget' => 'hierarchical_key', 'operator' => 'IN')),
),
'key empty' => array(
'OQL' => "SELECT Person WHERE location_id = '0'",
'ExpectedOQL' => "SELECT `Person` FROM Person AS `Person` WHERE (`Person`.`location_id` = '0')",
'ExpectedCriterion' => array(array('widget' => 'external_key', 'operator' => 'IN', 'values' => array(array('value' => '0')))),
),
'Double field' => array(
'OQL' => "SELECT UserRequest AS u WHERE u.close_date > u.start_date",
'ExpectedOQL' => "SELECT `u` FROM UserRequest AS `u` WHERE (`u`.`close_date` > `u`.`start_date`)",
'ExpectedCriterion' => array(array('widget' => 'raw')),
),
'Date relative 1' => array(
'OQL' => "SELECT UserRequest WHERE DATE_SUB(NOW(), INTERVAL 14 DAY) < start_date",
'ExpectedOQL' => "SELECT `UserRequest` FROM UserRequest AS `UserRequest` WHERE (DATE_SUB(NOW(), INTERVAL 14 DAY) < `UserRequest`.`start_date`)",
'ExpectedCriterion' => array(array('widget' => 'raw')),
),
'Date relative 2' => array(
'OQL' => "SELECT Contract AS c WHERE c.end_date > NOW() AND c.end_date < DATE_ADD(NOW(), INTERVAL 30 DAY)",
'ExpectedOQL' => "SELECT `c` FROM Contract AS `c` WHERE ((`c`.`end_date` < DATE_ADD(NOW(), INTERVAL 30 DAY)) AND (`c`.`end_date` > NOW()))",
'ExpectedCriterion' => array(array('widget' => 'raw'), array('widget' => 'raw')),
),
'Date relative 3' => array(
'OQL' => "SELECT UserRequest AS u WHERE u.close_date > DATE_ADD(u.start_date, INTERVAL 8 HOUR)",
'ExpectedOQL' => "SELECT `u` FROM UserRequest AS `u` WHERE (`u`.`close_date` > DATE_ADD(`u`.`start_date`, INTERVAL 8 HOUR))",
'ExpectedCriterion' => array(),
),
'Date relative 4' => array(
'OQL' => "SELECT UserRequest AS u WHERE u.start_date < DATE_SUB(NOW(), INTERVAL 60 MINUTE) AND u.status = 'new'",
'ExpectedOQL' => "SELECT `u` FROM UserRequest AS `u` WHERE ((`u`.`start_date` < DATE_SUB(NOW(), INTERVAL 60 MINUTE)) AND (`u`.`status` = 'new'))",
'ExpectedCriterion' => array(array('widget' => 'raw')),
),
'Date between 1' => array(
'OQL' => "SELECT UserRequest WHERE start_date > '2017-01-01 00:00:00' AND '2018-01-01 00:00:00' >= start_date",
'ExpectedOQL' => "SELECT `UserRequest` FROM UserRequest AS `UserRequest` WHERE ((`UserRequest`.`start_date` >= '2017-01-01 00:00:01') AND (`UserRequest`.`start_date` <= '2018-01-01 00:00:00'))",
'ExpectedCriterion' => array(array('widget' => 'date_time', 'operator' => 'between_dates')),
),
'Date between 2' => array(
'OQL' => "SELECT UserRequest WHERE start_date > '2017-01-01 00:00:00' AND status = 'active' AND org_id = 3 AND '2018-01-01 00:00:00' >= start_date",
'ExpectedOQL' => "SELECT `UserRequest` FROM UserRequest AS `UserRequest` WHERE ((((`UserRequest`.`org_id` = '3') AND (`UserRequest`.`start_date` >= '2017-01-01 00:00:01')) AND (`UserRequest`.`start_date` <= '2018-01-01 00:00:00')) AND (`UserRequest`.`status` = 'active'))",
'ExpectedCriterion' => array(array('widget' => 'hierarchical_key', 'operator' => 'IN'), array('widget' => 'date_time', 'operator' => 'between_dates'), array('widget' => 'enum', 'operator' => 'IN')),
),
'Date between 3' => array(
'OQL' => "SELECT UserRequest WHERE start_date >= '2017-01-01 00:00:00' AND '2017-01-01 00:00:00' >= start_date",
'ExpectedOQL' => "SELECT `UserRequest` FROM UserRequest AS `UserRequest` WHERE ((`UserRequest`.`start_date` >= '2017-01-01 00:00:00') AND (`UserRequest`.`start_date` <= '2017-01-01 00:00:00'))",
'ExpectedCriterion' => array(array('widget' => 'date_time', 'operator' => 'between_dates')),
),
'Date between 4' => array(
'OQL' => "SELECT UserRequest WHERE start_date >= '2017-01-01 00:00:00' AND '2017-01-01 01:00:00' > start_date",
'ExpectedOQL' => "SELECT `UserRequest` FROM UserRequest AS `UserRequest` WHERE ((`UserRequest`.`start_date` >= '2017-01-01 00:00:00') AND (`UserRequest`.`start_date` <= '2017-01-01 00:59:59'))",
'ExpectedCriterion' => array(array('widget' => 'date_time', 'operator' => 'between_dates')),
),
'Date between 5' => array(
'OQL' => "SELECT UserRequest WHERE start_date >= '2017-01-01 00:00:00' AND '2017-01-02 00:00:00' > start_date",
'ExpectedOQL' => "SELECT `UserRequest` FROM UserRequest AS `UserRequest` WHERE ((`UserRequest`.`start_date` >= '2017-01-01 00:00:00') AND (`UserRequest`.`start_date` <= '2017-01-01 23:59:59'))",
'ExpectedCriterion' => array(array('widget' => 'date_time', 'operator' => 'between_dates')),
),
'Date between 6' => array(
'OQL' => "SELECT UserRequest WHERE start_date >= '2017-01-01' AND '2017-01-02' >= start_date",
'ExpectedOQL' => "SELECT `UserRequest` FROM UserRequest AS `UserRequest` WHERE ((`UserRequest`.`start_date` >= '2017-01-01 00:00:00') AND (`UserRequest`.`start_date` <= '2017-01-02 00:00:00'))",
'ExpectedCriterion' => array(array('widget' => 'date_time', 'operator' => 'between_dates')),
),
'Date between 7' => array(
'OQL' => "SELECT CustomerContract WHERE ((start_date >= '2018-03-01') AND (start_date < '2018-04-01'))",
'ExpectedOQL' => "SELECT `CustomerContract` FROM CustomerContract AS `CustomerContract` WHERE ((`CustomerContract`.`start_date` >= '2018-03-01') AND (`CustomerContract`.`start_date` <= '2018-03-31'))",
'ExpectedCriterion' => array(array('widget' => 'date', 'operator' => 'between_dates')),
),
'Date =' => array(
'OQL' => "SELECT CustomerContract WHERE (start_date = '2018-03-01')",
'ExpectedOQL' => "SELECT `CustomerContract` FROM CustomerContract AS `CustomerContract` WHERE ((`CustomerContract`.`start_date` >= '2018-03-01') AND (`CustomerContract`.`start_date` <= '2018-03-01'))",
'ExpectedCriterion' => array(array('widget' => 'date', 'operator' => 'between_dates')),
),
'Date =2' => array(
'OQL' => "SELECT UserRequest WHERE (DATE_FORMAT(start_date, '%Y-%m-%d') = '2018-03-21')",
'ExpectedOQL' => "SELECT `UserRequest` FROM UserRequest AS `UserRequest` WHERE ((`UserRequest`.`start_date` >= '2018-03-21 00:00:00') AND (`UserRequest`.`start_date` <= '2018-03-21 23:59:59'))",
'ExpectedCriterion' => array(array('widget' => 'date_time', 'operator' => 'between_dates')),
),
'Num between 1' => array(
'OQL' => "SELECT Server WHERE nb_u >= 0 AND 1 >= nb_u",
'ExpectedOQL' => "SELECT `Server` FROM Server AS `Server` WHERE ((`Server`.`nb_u` >= '0') AND (`Server`.`nb_u` <= '1'))",
'ExpectedCriterion' => array(array('widget' => 'numeric', 'operator' => 'between')),
),
'Num ISNULL' => array(
'OQL' => "SELECT Server WHERE ISNULL(nb_u)",
'ExpectedOQL' => "SELECT `Server` FROM Server AS `Server` WHERE ISNULL(`Server`.`nb_u`)",
'ExpectedCriterion' => array(array('widget' => 'numeric', 'operator' => 'empty')),
),
'Hierarchical below' => array(
'OQL' => "SELECT Person AS P JOIN Organization AS Node ON P.org_id = Node.id JOIN Organization AS Root ON Node.parent_id BELOW Root.id WHERE Root.id=1",
'ExpectedOQL' => "SELECT `P` FROM Person AS `P` JOIN Organization AS `Node` ON `P`.org_id = `Node`.id JOIN Organization AS `Root` ON `Node`.parent_id BELOW `Root`.id WHERE (`Root`.`id` = 1)",
'ExpectedCriterion' => array(array('widget' => 'raw')),
),
'IP range' => array(
'OQL' => "SELECT DatacenterDevice AS dev WHERE INET_ATON(dev.managementip) > INET_ATON('10.22.32.224') AND INET_ATON(dev.managementip) < INET_ATON('10.22.32.255')",
'ExpectedOQL' => "SELECT `dev` FROM DatacenterDevice AS `dev` WHERE ((INET_ATON(`dev`.`managementip`) < INET_ATON('10.22.32.255')) AND (INET_ATON(`dev`.`managementip`) > INET_ATON('10.22.32.224')))",
'ExpectedCriterion' => array(array('widget' => 'raw')),
),
);
}
function array_diff_assoc_recursive($array1, $array2)
{
foreach($array1 as $key => $value)
{
if (is_array($value))
{
if (!isset($array2[$key]))
{
$difference[$key] = $value;
}
elseif (!is_array($array2[$key]))
{
$difference[$key] = $value;
}
else
{
$new_diff = $this->array_diff_assoc_recursive($value, $array2[$key]);
if ($new_diff !== false)
{
$difference[$key] = $new_diff;
}
}
}
elseif (!array_key_exists($key, $array2) || $array2[$key] != $value)
{
$difference[$key] = $value;
}
}
return !isset($difference) ? false : $difference;
}
}

View File

@@ -0,0 +1,87 @@
<?php
/**
* Copyright (C) 2010-2018 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/>
*
*/
/**
* Created by PhpStorm.
* User: Eric
* Date: 08/03/2018
* Time: 11:28
*/
namespace Combodo\iTop\Test\UnitTest\Application\Search;
use Combodo\iTop\Application\Search\CriterionParser;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
class CriterionParserTest extends ItopDataTestCase
{
/**
* @throws Exception
*/
protected function setUp()
{
parent::setUp();
require_once(APPROOT."sources/application/search/criterionparser.class.inc.php");
}
public function testParse()
{
$sBaseOql = 'SELECT UserRequest';
$aCriterion = json_decode('{
"or": [
{
"and": [
{
"ref": "UserRequest.start_date",
"values": [
{
"value": "2017-01-01",
"label": "2017-01-01 00:00:00"
}
],
"operator": ">",
"oql": ""
},
{
"ref": "UserRequest.start_date",
"values": [
{
"value": "2018-01-01",
"label": "2018-01-01 00:00:00"
}
],
"operator": "<",
"oql": "(`UserRequest`.`start_date` < \'2018-01-01\')"
}
]
}
]
}
', true);
$oSearch = CriterionParser::Parse($sBaseOql, $aCriterion);
//$this->debug($oSearch);
$this->assertEquals("SELECT `UserRequest` FROM UserRequest AS `UserRequest` WHERE ((`UserRequest`.`start_date` > '2017-01-01') AND (`UserRequest`.`start_date` < '2018-01-01'))", $oSearch->ToOQL());
}
}

View File

@@ -0,0 +1,125 @@
<?php
/**
* Copyright (C) 2010-2018 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\Test\UnitTest\Application\Search;
use Combodo\iTop\Application\Search\SearchForm;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Exception;
class SearchFormTest extends ItopDataTestCase
{
/**
* @throws Exception
*/
protected function setUp()
{
parent::setUp();
require_once(APPROOT."sources/application/search/searchform.class.inc.php");
}
/**
* @dataProvider GetFieldsProvider
* @throws \OQLException
*/
public function testGetFields($sOQL, $iNum, $sList)
{
$oSearchForm = new SearchForm();
$oSearch = \DBSearch::FromOQL($sOQL);
$aFields = $oSearchForm->GetFields(new \DBObjectSet($oSearch));
$this->debug($sOQL);
$this->debug(json_encode($aFields, JSON_PRETTY_PRINT));
$this->assertCount($iNum, $aFields[$sList]);
}
public function GetFieldsProvider()
{
return array(
array("SELECT Contact", 8, 'zlist'),
array("SELECT Contact AS C WHERE C.status = 'active'", 3, 'others'),
array("SELECT Person", 12, 'zlist'),
array(
"SELECT Person AS p JOIN UserRequest AS u ON u.agent_id = p.id WHERE u.status != 'closed'",
12,
'zlist'
),
);
}
/**
* @dataProvider GetCriterionProvider
*
* @param $sOQL
* @param $iOrCount
*
*/
public function testGetCriterion($sOQL, $iOrCount)
{
$oSearchForm = new SearchForm();
try
{
$oSearch = \DBSearch::FromOQL($sOQL);
$aFields = $oSearchForm->GetFields(new \DBObjectSet($oSearch));
$aCriterion = $oSearchForm->GetCriterion($oSearch, $aFields);
} catch (\OQLException $e)
{
$this->assertTrue(false);
return;
}
$aRes = array('base_oql' => $sOQL, 'criterion' => $aCriterion);
$this->debug(json_encode($aRes));
$this->debug($sOQL);
$this->debug(json_encode($aCriterion, JSON_PRETTY_PRINT));
$this->assertCount($iOrCount, $aCriterion['or']);
}
public function GetCriterionProvider()
{
return array(
array('OQL' => "SELECT Contact", 1),
array('OQL' => "SELECT Contact WHERE status = 'active'", 1),
array('OQL' => "SELECT Contact AS C WHERE C.status = 'active'", 1),
array('OQL' => "SELECT Contact WHERE status = 'active' AND name LIKE 'toto%'", 1),
array('OQL' => "SELECT Contact WHERE status = 'active' AND org_id = 3", 1),
array('OQL' => "SELECT Contact WHERE status IN ('active', 'inactive')", 1),
array('OQL' => "SELECT Contact WHERE status NOT IN ('active')", 1),
array('OQL' => "SELECT Contact WHERE status NOT IN ('active', 'inactive')", 1),
array('OQL' => "SELECT Contact WHERE status = 'active' OR name LIKE 'toto%'", 2),
array('OQL' => "SELECT UserRequest WHERE DATE_SUB(NOW(), INTERVAL 14 DAY) < start_date", 1),
array('OQL' => "SELECT UserRequest WHERE start_date > '2017-01-01 00:00:00' AND '2018-01-01 00:00:00' >= start_date", 1),
array('OQL' => "SELECT UserRequest WHERE start_date > '2017-01-01 00:00:00' AND status = 'active' AND org_id = 3 AND '2018-01-01 00:00:00' >= start_date", 1),
array('OQL' => "SELECT UserRequest WHERE start_date >= '2017-01-01 00:00:00' AND '2017-01-01 00:00:00' >= start_date", 1),
array('OQL' => "SELECT UserRequest WHERE start_date >= '2017-01-01 00:00:00' AND '2017-01-01 01:00:00' > start_date", 1),
array('OQL' => "SELECT UserRequest WHERE start_date >= '2017-01-01 00:00:00' AND '2017-01-02 00:00:00' > start_date", 1),
array(
'OQL' => "SELECT FunctionalCI WHERE ((business_criticity IN ('high', 'medium')) OR ISNULL(business_criticity)) AND 1",
1
),
);
}
}

View File

@@ -28,7 +28,6 @@ namespace Combodo\iTop\Test\UnitTest\iTopTickets;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Exception;
use PHPUnit\Framework\TestCase;
/**
@@ -799,7 +798,7 @@ class ItopTicketTest extends ItopDataTestCase
$oServer2->GetKey() => 'manual');
$this->CheckFunctionalCIList($oTicket2, $aWaitedCIList);
$this->CheckContactList($oTicket2);
$this->assertEquals(5, $oTicket2->Get('functionalcis_list')->Count());
$this->assertEquals(2, $oTicket2->Get('functionalcis_list')->Count());
$this->assertEquals(1, $oTicket2->Get('contacts_list')->Count());
// The first ticket is not impacted
@@ -910,7 +909,7 @@ class ItopTicketTest extends ItopDataTestCase
$oServer2->GetKey() => 'manual');
$this->CheckFunctionalCIList($oTicket2, $aWaitedCIList);
$this->CheckContactList($oTicket2);
$this->assertEquals(6, $oTicket2->Get('functionalcis_list')->Count());
$this->assertEquals(3, $oTicket2->Get('functionalcis_list')->Count());
$this->assertEquals(1, $oTicket2->Get('contacts_list')->Count());
// The first ticket is not impacted