mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-28 04:58:46 +02:00
Compare commits
59 Commits
feature/79
...
saas/3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d6f4569ef | ||
|
|
1e41e805a2 | ||
|
|
bd1e4389f7 | ||
|
|
b059fb72a2 | ||
|
|
1b3b2e8a69 | ||
|
|
6b448e29f5 | ||
|
|
7cb6af0a2b | ||
|
|
ddc9952ec1 | ||
|
|
a1a9ffe192 | ||
|
|
001194835f | ||
|
|
7728082c00 | ||
|
|
955aefc05b | ||
|
|
970183ef45 | ||
|
|
6c2db1e687 | ||
|
|
034ca26d01 | ||
|
|
32d74fbc8e | ||
|
|
477f2f51e9 | ||
|
|
94ea8e60e8 | ||
|
|
b9a00b15f5 | ||
|
|
e87f5af465 | ||
|
|
97d717b016 | ||
|
|
d03bd706e2 | ||
|
|
251fd3c67b | ||
|
|
7b0a569c64 | ||
|
|
7176bc8686 | ||
|
|
9c0b906ded | ||
|
|
0533916dad | ||
|
|
60b08586c2 | ||
|
|
28df2942e4 | ||
|
|
cd48d2ad37 | ||
|
|
9db2205241 | ||
|
|
045985cd5b | ||
|
|
809b371520 | ||
|
|
9bbc7342b8 | ||
|
|
973c435138 | ||
|
|
1c3dfd6491 | ||
|
|
9400b697eb | ||
|
|
49748a0374 | ||
|
|
163276a6c2 | ||
|
|
9b0c2f7324 | ||
|
|
e1807f598f | ||
|
|
02e63fff64 | ||
|
|
0864f05d9f | ||
|
|
1bbcd9656a | ||
|
|
34d8e52c22 | ||
|
|
ac7309e48c | ||
|
|
c8fade6013 | ||
|
|
ad052dd861 | ||
|
|
a5ea868609 | ||
|
|
0b03b3ef4d | ||
|
|
174cace20a | ||
|
|
e3f5dbfc80 | ||
|
|
40e24c25a2 | ||
|
|
d3f8e1c472 | ||
|
|
647b669eb9 | ||
|
|
c6e4466c53 | ||
|
|
6638eb4adc | ||
|
|
eb40968e34 | ||
|
|
766c9f0e7e |
@@ -2184,5 +2184,50 @@ class RestUtils
|
|||||||
*/
|
*/
|
||||||
interface iModuleExtension
|
interface iModuleExtension
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
public function __construct();
|
public function __construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to provide messages to be displayed in the "Welcome Popup"
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @private
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
interface iWelcomePopup
|
||||||
|
{
|
||||||
|
// Importance for ordering messages
|
||||||
|
// Just two levels since less important messages have nothing to do in the welcome popup
|
||||||
|
const IMPORTANCE_CRITICAL = 0;
|
||||||
|
const IMPORTANCE_HIGH = 1;
|
||||||
|
/**
|
||||||
|
* @return [['importance' => IMPORTANCE_CRITICAL|IMPORTANCE_HIGH, 'id' => '...', 'title' => '', 'html' => '', 'twig' => '']]
|
||||||
|
*/
|
||||||
|
public function GetMessages();
|
||||||
|
/**
|
||||||
|
* The message specified by the given Id has been acknowledged by the current user
|
||||||
|
* @param string $sMessageId
|
||||||
|
*/
|
||||||
|
public function AcknowledgeMessage(string $sMessageId): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inherit from this class to provide messages to be displayed in the "Welcome Popup"
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
abstract class AbstractWelcomePopup implements iWelcomePopup
|
||||||
|
{
|
||||||
|
public function GetMessages()
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
public function AcknowledgeMessage(string $sMessageId): void
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,36 @@
|
|||||||
<presentation/>
|
<presentation/>
|
||||||
<methods/>
|
<methods/>
|
||||||
</class>
|
</class>
|
||||||
|
<class id="WelcomePopupAcknowledge" _delta="define">
|
||||||
|
<parent>DBObject</parent>
|
||||||
|
<properties>
|
||||||
|
<comment>/* Acknowledge welcome popup messages */</comment>
|
||||||
|
<abstract>false</abstract>
|
||||||
|
<category></category>
|
||||||
|
<key_type>autoincrement</key_type>
|
||||||
|
<db_table>priv_welcome_popup_acknowledge</db_table>
|
||||||
|
</properties>
|
||||||
|
<fields>
|
||||||
|
<field id="message_uuid" xsi:type="AttributeString">
|
||||||
|
<sql>message_uuid</sql>
|
||||||
|
<default_value/>
|
||||||
|
<is_null_allowed>false</is_null_allowed>
|
||||||
|
</field>
|
||||||
|
<field id="user_id" xsi:type="AttributeExternalKey">
|
||||||
|
<sql>user_id</sql>
|
||||||
|
<target_class>User</target_class>
|
||||||
|
<is_null_allowed>false</is_null_allowed>
|
||||||
|
<on_target_delete>DEL_SILENT</on_target_delete>
|
||||||
|
</field>
|
||||||
|
<field id="acknowledge_date" xsi:type="AttributeDateTime">
|
||||||
|
<sql>acknowledge_date</sql>
|
||||||
|
<default_value/>
|
||||||
|
<is_null_allowed>false</is_null_allowed>
|
||||||
|
</field>
|
||||||
|
</fields>
|
||||||
|
<presentation/>
|
||||||
|
<methods/>
|
||||||
|
</class>
|
||||||
</classes>
|
</classes>
|
||||||
<portals>
|
<portals>
|
||||||
<portal id="backoffice" _delta="define">
|
<portal id="backoffice" _delta="define">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Combodo\iTop\Application\Helper\WebResourcesHelper;
|
|||||||
require_once(APPROOT.'/application/utils.inc.php');
|
require_once(APPROOT.'/application/utils.inc.php');
|
||||||
require_once(APPROOT.'/application/template.class.inc.php');
|
require_once(APPROOT.'/application/template.class.inc.php');
|
||||||
require_once(APPROOT."/application/user.dashboard.class.inc.php");
|
require_once(APPROOT."/application/user.dashboard.class.inc.php");
|
||||||
|
require_once(APPROOT."/setup/parentmenunodecompiler.class.inc.php");
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -273,12 +274,23 @@ class ApplicationMenu
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$aSubMenuNodes = static::GetSubMenuNodes($sMenuGroupIdx, $aExtraParams);
|
||||||
|
if (! ParentMenuNodeCompiler::$bUseLegacyMenuCompilation && !($oMenuNode instanceof ShortcutMenuNode)){
|
||||||
|
if (is_array($aSubMenuNodes) && 0 === sizeof($aSubMenuNodes)){
|
||||||
|
IssueLog::Error('Empty menu node not displayed', LogChannels::CONSOLE, [
|
||||||
|
'menu_node_class' => get_class($oMenuNode),
|
||||||
|
'menu_node_label' => $oMenuNode->GetLabel(),
|
||||||
|
]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$aMenuGroups[] = [
|
$aMenuGroups[] = [
|
||||||
'sId' => $oMenuNode->GetMenuID(),
|
'sId' => $oMenuNode->GetMenuID(),
|
||||||
'sIconCssClasses' => $oMenuNode->GetDecorationClasses(),
|
'sIconCssClasses' => $oMenuNode->GetDecorationClasses(),
|
||||||
'sInitials' => $oMenuNode->GetInitials(),
|
'sInitials' => $oMenuNode->GetInitials(),
|
||||||
'sTitle' => $oMenuNode->GetTitle(),
|
'sTitle' => $oMenuNode->GetTitle(),
|
||||||
'aSubMenuNodes' => static::GetSubMenuNodes($sMenuGroupIdx, $aExtraParams),
|
'aSubMenuNodes' => $aSubMenuNodes,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
<div style="width:100%;background: #fff url(../images/welcome.jpg) top left no-repeat;">
|
|
||||||
<style>
|
|
||||||
.welcome_popup_cell {
|
|
||||||
vertical-align:top;
|
|
||||||
width:50%;
|
|
||||||
border:0px solid #000;
|
|
||||||
-moz-border-radius:10px;
|
|
||||||
padding:5px;
|
|
||||||
text-align:left;
|
|
||||||
}
|
|
||||||
tr td.welcome_popup_cell, tr td.welcome_popup_cell ul {
|
|
||||||
font-size:10pt;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<p></p>
|
|
||||||
<p></p>
|
|
||||||
<p style="text-align:left; font-size:32px;padding-left:400px;padding-top:40px;margin-bottom:30px;margin-top:0;color:#FFFFFF;"><itopstring>UI:WelcomeMenu:Title</itopstring></p>
|
|
||||||
<p></p>
|
|
||||||
<table border="0" style="padding:10px;border-spacing: 10px;width:100%">
|
|
||||||
<tr>
|
|
||||||
<td class="welcome_popup_cell">
|
|
||||||
<itopstring>UI:WelcomeMenu:LeftBlock</itopstring>
|
|
||||||
</td>
|
|
||||||
<td class="welcome_popup_cell">
|
|
||||||
<itopstring>UI:WelcomeMenu:RightBlock</itopstring>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
@@ -42,6 +42,17 @@ abstract class CellChangeSpec
|
|||||||
return $this->m_sOql;
|
return $this->m_sOql;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 3.1.0 N°5305
|
||||||
|
*/
|
||||||
|
public function GetDisplayableValueAndDescription(): string
|
||||||
|
{
|
||||||
|
return sprintf("%s%s",
|
||||||
|
$this->GetDisplayableValue(),
|
||||||
|
$this->GetDescription()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
abstract public function GetDescription();
|
abstract public function GetDescription();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,26 +97,90 @@ class CellStatus_Issue extends CellStatus_Modify
|
|||||||
parent::__construct($proposedValue, $previousValue);
|
parent::__construct($proposedValue, $previousValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function GetDescription()
|
public function GetDisplayableValue()
|
||||||
{
|
{
|
||||||
if (is_null($this->m_proposedValue))
|
if (is_null($this->m_proposedValue))
|
||||||
{
|
{
|
||||||
return Dict::Format('UI:CSVReport-Value-SetIssue', $this->m_sReason);
|
return Dict::Format('UI:CSVReport-Value-SetIssue');
|
||||||
}
|
}
|
||||||
return Dict::Format('UI:CSVReport-Value-ChangeIssue', $this->m_proposedValue, $this->m_sReason);
|
return Dict::Format('UI:CSVReport-Value-ChangeIssue', \utils::EscapeHtml($this->m_proposedValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function GetDescription()
|
||||||
|
{
|
||||||
|
return $this->m_sReason;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* @since 3.1.0 N°5305
|
||||||
|
*/
|
||||||
|
public function GetDisplayableValueAndDescription(): string
|
||||||
|
{
|
||||||
|
return sprintf("%s. %s",
|
||||||
|
$this->GetDisplayableValue(),
|
||||||
|
$this->GetDescription()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CellStatus_SearchIssue extends CellStatus_Issue
|
class CellStatus_SearchIssue extends CellStatus_Issue
|
||||||
{
|
{
|
||||||
public function __construct()
|
/** @var string|null $m_sAllowedValues */
|
||||||
|
private $m_sAllowedValues;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 3.1.0 N°5305
|
||||||
|
* @var string $sSerializedSearch
|
||||||
|
*/
|
||||||
|
private $sSerializedSearch;
|
||||||
|
|
||||||
|
/** @var string|null $m_sTargetClass */
|
||||||
|
private $m_sTargetClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CellStatus_SearchIssue constructor.
|
||||||
|
* @since 3.1.0 N°5305
|
||||||
|
*
|
||||||
|
* @param string $sOql : main message
|
||||||
|
* @param string $sReason : main message
|
||||||
|
* @param null $sClass : used for additional message that provides allowed values for current class $sClass
|
||||||
|
* @param null $sAllowedValues : used for additional message that provides allowed values $sAllowedValues for current class
|
||||||
|
*/
|
||||||
|
public function __construct($sSerializedSearch, $sReason, $sClass=null, $sAllowedValues=null)
|
||||||
{
|
{
|
||||||
parent::__construct(null, null, null);
|
parent::__construct(null, null, $sReason);
|
||||||
|
$this->sSerializedSearch = $sSerializedSearch;
|
||||||
|
$this->m_sAllowedValues = $sAllowedValues;
|
||||||
|
$this->m_sTargetClass = $sClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function GetDisplayableValue()
|
||||||
|
{
|
||||||
|
if (null === $this->m_sReason) {
|
||||||
|
return Dict::Format('UI:CSVReport-Value-NoMatch', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->m_sReason;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function GetDescription()
|
public function GetDescription()
|
||||||
{
|
{
|
||||||
return Dict::S('UI:CSVReport-Value-NoMatch');
|
if (\utils::IsNullOrEmptyString($this->m_sAllowedValues) ||
|
||||||
|
\utils::IsNullOrEmptyString($this->m_sTargetClass)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Dict::Format('UI:CSVReport-Value-NoMatch-PossibleValues', $this->m_sTargetClass, $this->m_sAllowedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 3.1.0 N°5305
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function GetSearchLinkUrl()
|
||||||
|
{
|
||||||
|
return sprintf("UI.php?operation=search&filter=%s",
|
||||||
|
rawurlencode($this->sSerializedSearch)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,11 +201,24 @@ class CellStatus_NullIssue extends CellStatus_Issue
|
|||||||
class CellStatus_Ambiguous extends CellStatus_Issue
|
class CellStatus_Ambiguous extends CellStatus_Issue
|
||||||
{
|
{
|
||||||
protected $m_iCount;
|
protected $m_iCount;
|
||||||
|
/**
|
||||||
|
* @since 3.1.0 N°5305
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $sSerializedSearch;
|
||||||
|
|
||||||
public function __construct($previousValue, $iCount, $sOql)
|
/**
|
||||||
|
* @since 3.1.0 N°5305
|
||||||
|
*
|
||||||
|
* @param $previousValue
|
||||||
|
* @param int $iCount
|
||||||
|
* @param string $sSerializedSearch
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function __construct($previousValue, $iCount, $sSerializedSearch)
|
||||||
{
|
{
|
||||||
$this->m_iCount = $iCount;
|
$this->m_iCount = $iCount;
|
||||||
$this->m_sQuery = $sOql;
|
$this->sSerializedSearch = $sSerializedSearch;
|
||||||
parent::__construct(null, $previousValue, '');
|
parent::__construct(null, $previousValue, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +227,17 @@ class CellStatus_Ambiguous extends CellStatus_Issue
|
|||||||
$sCount = $this->m_iCount;
|
$sCount = $this->m_iCount;
|
||||||
return Dict::Format('UI:CSVReport-Value-Ambiguous', $sCount);
|
return Dict::Format('UI:CSVReport-Value-Ambiguous', $sCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 3.1.0 N°5305
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function GetSearchLinkUrl()
|
||||||
|
{
|
||||||
|
return sprintf("UI.php?operation=search&filter=%s",
|
||||||
|
rawurlencode($this->sSerializedSearch)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -211,6 +310,26 @@ class RowStatus_Issue extends RowStatus
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* class dedicated to testability
|
||||||
|
* not used/ignored in csv imports UI/CLI
|
||||||
|
* @since 3.1.0 N°5305
|
||||||
|
*/
|
||||||
|
class RowStatus_Error extends RowStatus
|
||||||
|
{
|
||||||
|
/** @var string */
|
||||||
|
protected $m_sError;
|
||||||
|
|
||||||
|
public function __construct($sError)
|
||||||
|
{
|
||||||
|
$this->m_sError = $sError;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function GetDescription()
|
||||||
|
{
|
||||||
|
return $this->m_sError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BulkChange
|
* BulkChange
|
||||||
@@ -220,17 +339,35 @@ class RowStatus_Issue extends RowStatus
|
|||||||
*/
|
*/
|
||||||
class BulkChange
|
class BulkChange
|
||||||
{
|
{
|
||||||
|
/** @var string */
|
||||||
protected $m_sClass;
|
protected $m_sClass;
|
||||||
protected $m_aData; // Note: hereafter, iCol maybe actually be any acceptable key (string)
|
protected $m_aData; // Note: hereafter, iCol maybe actually be any acceptable key (string)
|
||||||
// #@# todo: rename the variables to sColIndex
|
// #@# todo: rename the variables to sColIndex
|
||||||
protected $m_aAttList; // attcode => iCol
|
/** @var array<string, string> attcode as key, iCol as value */
|
||||||
protected $m_aExtKeys; // aExtKeys[sExtKeyAttCode][sExtReconcKeyAttCode] = iCol;
|
protected $m_aAttList;
|
||||||
protected $m_aReconcilKeys; // attcode (attcode = 'id' for the pkey)
|
/** @var array<string, array<string, string>> sExtKeyAttCode as key, array of sExtReconcKeyAttCode/iCol as value */
|
||||||
protected $m_sSynchroScope; // OQL - if specified, then the missing items will be reported
|
protected $m_aExtKeys;
|
||||||
protected $m_aOnDisappear; // array of attcode => value, values to be set when an object gets out of scope (ignored if no scope has been defined)
|
/** @var string[] list of attcode (attcode = 'id' for the pkey) */
|
||||||
protected $m_sDateFormat; // Date format specification, see DateTime::createFromFormat
|
protected $m_aReconcilKeys;
|
||||||
protected $m_bLocalizedValues; // Values in the data set are localized (see AttributeEnum)
|
/** @var string OQL - if specified, then the missing items will be reported */
|
||||||
protected $m_aExtKeysMappingCache; // Cache for resolving external keys based on the given search criterias
|
protected $m_sSynchroScope;
|
||||||
|
/**
|
||||||
|
* @var array<string, mixed> attcode as key, attvalue as value. Values to be set when an object gets out of scope
|
||||||
|
* (ignored if no scope has been defined)
|
||||||
|
*/
|
||||||
|
protected $m_aOnDisappear;
|
||||||
|
/**
|
||||||
|
* @see DateTime::createFromFormat
|
||||||
|
* @var string Date format specification
|
||||||
|
*/
|
||||||
|
protected $m_sDateFormat;
|
||||||
|
/**
|
||||||
|
* @see AttributeEnum
|
||||||
|
* @var boolean true if Values in the data set are localized
|
||||||
|
*/
|
||||||
|
protected $m_bLocalizedValues;
|
||||||
|
/** @var array Cache for resolving external keys based on the given search criterias */
|
||||||
|
protected $m_aExtKeysMappingCache;
|
||||||
|
|
||||||
public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys, $sSynchroScope = null, $aOnDisappear = null, $sDateFormat = null, $bLocalize = false)
|
public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys, $sSynchroScope = null, $aOnDisappear = null, $sDateFormat = null, $bLocalize = false)
|
||||||
{
|
{
|
||||||
@@ -266,25 +403,25 @@ class BulkChange
|
|||||||
{
|
{
|
||||||
$oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
|
$oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
|
||||||
$oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass());
|
$oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass());
|
||||||
foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol)
|
foreach ($this->m_aExtKeys[$sAttCode] as $sReconKeyAttCode => $iCol)
|
||||||
{
|
{
|
||||||
if ($sForeignAttCode == 'id')
|
if ($sReconKeyAttCode == 'id')
|
||||||
{
|
{
|
||||||
$value = (int) $aRowData[$iCol];
|
$value = (int) $aRowData[$iCol];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// The foreign attribute is one of our reconciliation key
|
// The foreign attribute is one of our reconciliation key
|
||||||
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode);
|
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sReconKeyAttCode);
|
||||||
$value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
|
$value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
|
||||||
}
|
}
|
||||||
$oReconFilter->AddCondition($sForeignAttCode, $value, '=');
|
$oReconFilter->AddCondition($sReconKeyAttCode, $value, '=');
|
||||||
$aResults[$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol]));
|
$aResults[$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol]));
|
||||||
}
|
}
|
||||||
|
|
||||||
$oExtObjects = new CMDBObjectSet($oReconFilter);
|
$oExtObjects = new CMDBObjectSet($oReconFilter);
|
||||||
$aKeys = $oExtObjects->ToArray();
|
$aKeys = $oExtObjects->ToArray();
|
||||||
return array($oReconFilter->ToOql(), $aKeys);
|
return array($oReconFilter, $aKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns true if the CSV data specifies that the external key must be left undefined
|
// Returns true if the CSV data specifies that the external key must be left undefined
|
||||||
@@ -321,7 +458,7 @@ class BulkChange
|
|||||||
|
|
||||||
// External keys reconciliation
|
// External keys reconciliation
|
||||||
//
|
//
|
||||||
foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig)
|
foreach($this->m_aExtKeys as $sAttCode => $aReconKeys)
|
||||||
{
|
{
|
||||||
// Skip external keys used for the reconciliation process
|
// Skip external keys used for the reconciliation process
|
||||||
// if (!array_key_exists($sAttCode, $this->m_aAttList)) continue;
|
// if (!array_key_exists($sAttCode, $this->m_aAttList)) continue;
|
||||||
@@ -330,7 +467,7 @@ class BulkChange
|
|||||||
|
|
||||||
if ($this->IsNullExternalKeySpec($aRowData, $sAttCode))
|
if ($this->IsNullExternalKeySpec($aRowData, $sAttCode))
|
||||||
{
|
{
|
||||||
foreach ($aKeyConfig as $sForeignAttCode => $iCol)
|
foreach ($aReconKeys as $sReconKeyAttCode => $iCol)
|
||||||
{
|
{
|
||||||
// Default reporting
|
// Default reporting
|
||||||
// $aRowData[$iCol] is always null
|
// $aRowData[$iCol] is always null
|
||||||
@@ -352,25 +489,24 @@ class BulkChange
|
|||||||
$oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass());
|
$oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass());
|
||||||
|
|
||||||
$aCacheKeys = array();
|
$aCacheKeys = array();
|
||||||
foreach ($aKeyConfig as $sForeignAttCode => $iCol)
|
foreach ($aReconKeys as $sReconKeyAttCode => $iCol)
|
||||||
{
|
{
|
||||||
// The foreign attribute is one of our reconciliation key
|
// The foreign attribute is one of our reconciliation key
|
||||||
if ($sForeignAttCode == 'id')
|
if ($sReconKeyAttCode == 'id')
|
||||||
{
|
{
|
||||||
$value = $aRowData[$iCol];
|
$value = $aRowData[$iCol];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode);
|
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sReconKeyAttCode);
|
||||||
$value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
|
$value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
|
||||||
}
|
}
|
||||||
$aCacheKeys[] = $value;
|
$aCacheKeys[] = $value;
|
||||||
$oReconFilter->AddCondition($sForeignAttCode, $value, '=');
|
$oReconFilter->AddCondition($sReconKeyAttCode, $value, '=');
|
||||||
$aResults[$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol]));
|
$aResults[$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol]));
|
||||||
}
|
}
|
||||||
$sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query...
|
$sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query...
|
||||||
$iForeignKey = null;
|
$iForeignKey = null;
|
||||||
$sOQL = '';
|
|
||||||
// TODO: check if *too long* keys can lead to collisions... and skip the cache in such a case...
|
// TODO: check if *too long* keys can lead to collisions... and skip the cache in such a case...
|
||||||
if (!array_key_exists($sAttCode, $this->m_aExtKeysMappingCache))
|
if (!array_key_exists($sAttCode, $this->m_aExtKeysMappingCache))
|
||||||
{
|
{
|
||||||
@@ -379,9 +515,8 @@ class BulkChange
|
|||||||
if (array_key_exists($sCacheKey, $this->m_aExtKeysMappingCache[$sAttCode]))
|
if (array_key_exists($sCacheKey, $this->m_aExtKeysMappingCache[$sAttCode]))
|
||||||
{
|
{
|
||||||
// Cache hit
|
// Cache hit
|
||||||
$iCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c'];
|
$iObjectFoundCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c'];
|
||||||
$iForeignKey = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['k'];
|
$iForeignKey = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['k'];
|
||||||
$sOQL = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['oql'];
|
|
||||||
// Record the hit
|
// Record the hit
|
||||||
$this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['h']++;
|
$this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['h']++;
|
||||||
}
|
}
|
||||||
@@ -389,24 +524,25 @@ class BulkChange
|
|||||||
{
|
{
|
||||||
// Cache miss, let's initialize it
|
// Cache miss, let's initialize it
|
||||||
$oExtObjects = new CMDBObjectSet($oReconFilter);
|
$oExtObjects = new CMDBObjectSet($oReconFilter);
|
||||||
$iCount = $oExtObjects->Count();
|
$iObjectFoundCount = $oExtObjects->Count();
|
||||||
if ($iCount == 1)
|
if ($iObjectFoundCount == 1)
|
||||||
{
|
{
|
||||||
$oForeignObj = $oExtObjects->Fetch();
|
$oForeignObj = $oExtObjects->Fetch();
|
||||||
$iForeignKey = $oForeignObj->GetKey();
|
$iForeignKey = $oForeignObj->GetKey();
|
||||||
}
|
}
|
||||||
$this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey] = array(
|
$this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey] = array(
|
||||||
'c' => $iCount,
|
'c' => $iObjectFoundCount,
|
||||||
'k' => $iForeignKey,
|
'k' => $iForeignKey,
|
||||||
'oql' => $oReconFilter->ToOql(),
|
'oql' => $oReconFilter->ToOql(),
|
||||||
'h' => 0, // number of hits on this cache entry
|
'h' => 0, // number of hits on this cache entry
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
switch($iCount)
|
switch($iObjectFoundCount)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
|
$oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter);
|
||||||
|
$aResults[$sAttCode] = $oCellStatus_SearchIssue;
|
||||||
$aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound');
|
$aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound');
|
||||||
$aResults[$sAttCode]= new CellStatus_SearchIssue();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 1:
|
case 1:
|
||||||
@@ -415,8 +551,8 @@ class BulkChange
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $iCount);
|
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $iObjectFoundCount);
|
||||||
$aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iCount, $sOQL);
|
$aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iObjectFoundCount, $oReconFilter->serialize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,7 +569,7 @@ class BulkChange
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
$aResults[$sAttCode]= new CellStatus_Modify($iForeignObj, $oTargetObj->GetOriginal($sAttCode));
|
$aResults[$sAttCode]= new CellStatus_Modify($iForeignObj, $oTargetObj->GetOriginal($sAttCode));
|
||||||
foreach ($aKeyConfig as $sForeignAttCode => $iCol)
|
foreach ($aReconKeys as $sReconKeyAttCode => $iCol)
|
||||||
{
|
{
|
||||||
// Report the change on reconciliation values as well
|
// Report the change on reconciliation values as well
|
||||||
$aResults[$iCol] = new CellStatus_Modify(utils::HtmlEntities($aRowData[$iCol]));
|
$aResults[$iCol] = new CellStatus_Modify(utils::HtmlEntities($aRowData[$iCol]));
|
||||||
@@ -479,9 +615,18 @@ class BulkChange
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$value = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
|
$value = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
|
||||||
if (is_null($value) && (strlen($aRowData[$iCol]) > 0)) {
|
if (is_null($value) && (strlen($aRowData[$iCol]) > 0))
|
||||||
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode);
|
{
|
||||||
|
if ($oAttDef instanceof AttributeEnum || $oAttDef instanceof AttributeTagSet){
|
||||||
|
/** @var AttributeDefinition $oAttributeDefinition */
|
||||||
|
$oAttributeDefinition = $oAttDef;
|
||||||
|
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-AllowedValues', $sAttCode, implode(',', $oAttributeDefinition->GetAllowedValues()));
|
||||||
} else {
|
} else {
|
||||||
|
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
$res = $oTargetObj->CheckValue($sAttCode, $value);
|
$res = $oTargetObj->CheckValue($sAttCode, $value);
|
||||||
if ($res === true)
|
if ($res === true)
|
||||||
{
|
{
|
||||||
@@ -559,6 +704,95 @@ class BulkChange
|
|||||||
return $aResults;
|
return $aResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* search with current permissions did not match
|
||||||
|
* let's search why and give some more feedbacks to the user through proper labels
|
||||||
|
*
|
||||||
|
* @param DBObjectSearch $oDbSearchWithConditions search used to find external key
|
||||||
|
*
|
||||||
|
* @return \CellStatus_SearchIssue
|
||||||
|
* @throws \CoreException
|
||||||
|
* @throws \MissingQueryArgument
|
||||||
|
* @throws \MySQLException
|
||||||
|
* @throws \MySQLHasGoneAwayException
|
||||||
|
*
|
||||||
|
* @since 3.1.0 N°5305
|
||||||
|
*/
|
||||||
|
protected function GetCellSearchIssue($oDbSearchWithConditions) : CellStatus_SearchIssue {
|
||||||
|
//current search with current permissions did not match
|
||||||
|
//let's search why and give some more feedback to the user
|
||||||
|
|
||||||
|
$sSerializedSearch = $oDbSearchWithConditions->serialize();
|
||||||
|
|
||||||
|
// Count all objects with all permissions without any condition
|
||||||
|
$oDbSearchWithoutAnyCondition = new DBObjectSearch($oDbSearchWithConditions->GetClass());
|
||||||
|
$oDbSearchWithoutAnyCondition->AllowAllData(true);
|
||||||
|
$oExtObjectSet = new CMDBObjectSet($oDbSearchWithoutAnyCondition);
|
||||||
|
$iAllowAllDataObjectCount = $oExtObjectSet->Count();
|
||||||
|
|
||||||
|
if ($iAllowAllDataObjectCount === 0) {
|
||||||
|
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject', $oDbSearchWithConditions->GetClass());
|
||||||
|
return new CellStatus_SearchIssue($sSerializedSearch, $sReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count all objects with current user permissions
|
||||||
|
$oDbSearchWithoutAnyCondition->AllowAllData(false);
|
||||||
|
$oExtObjectSetWithCurrentUserPermissions = new CMDBObjectSet($oDbSearchWithoutAnyCondition);
|
||||||
|
$iCurrentUserRightsObjectCount = $oExtObjectSetWithCurrentUserPermissions->Count();
|
||||||
|
|
||||||
|
if ($iCurrentUserRightsObjectCount === 0){
|
||||||
|
// No objects visible by current user
|
||||||
|
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser', $oDbSearchWithConditions->GetClass());
|
||||||
|
return new CellStatus_SearchIssue($sSerializedSearch, $sReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
$aDisplayedAllowedValues = [];
|
||||||
|
// Possibles values are displayed to UI user. we have to limit the amount of displayed values
|
||||||
|
$oExtObjectSetWithCurrentUserPermissions->SetLimit(4);
|
||||||
|
for($i = 0; $i < 3; $i++){
|
||||||
|
/** @var \DBObject $oVisibleObject */
|
||||||
|
$oVisibleObject = $oExtObjectSetWithCurrentUserPermissions->Fetch();
|
||||||
|
if (is_null($oVisibleObject)){
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$aCurrentAllowedValueFields = [];
|
||||||
|
foreach ($oDbSearchWithConditions->GetInternalParams() as $sForeignAttCode => $sValue){
|
||||||
|
$aCurrentAllowedValueFields[] = $oVisibleObject->Get($sForeignAttCode);
|
||||||
|
}
|
||||||
|
$aDisplayedAllowedValues[] = implode(" ", $aCurrentAllowedValueFields);
|
||||||
|
|
||||||
|
}
|
||||||
|
$allowedValues = implode(", ", $aDisplayedAllowedValues);
|
||||||
|
if ($oExtObjectSetWithCurrentUserPermissions->Count() > 3){
|
||||||
|
$allowedValues .= "...";
|
||||||
|
}
|
||||||
|
} catch(Exception $e) {
|
||||||
|
IssueLog::Error("failure during CSV import when fetching few visible objects: ", null,
|
||||||
|
[ 'target_class' => $oDbSearchWithConditions->GetClass(), 'criteria' => $oDbSearchWithConditions->GetCriteria(), 'message' => $e->getMessage()]
|
||||||
|
);
|
||||||
|
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser', $oDbSearchWithConditions->GetClass());
|
||||||
|
return new CellStatus_SearchIssue($sSerializedSearch, $sReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($iAllowAllDataObjectCount != $iCurrentUserRightsObjectCount) {
|
||||||
|
// No match and some objects NOT visible by current user. including current search maybe...
|
||||||
|
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser', $oDbSearchWithConditions->GetClass());
|
||||||
|
return new CellStatus_SearchIssue($sSerializedSearch, $sReason, $oDbSearchWithConditions->GetClass(), $allowedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No match. This is not linked to any right issue
|
||||||
|
// Possible values: DD,DD
|
||||||
|
$aCurrentValueFields = [];
|
||||||
|
foreach ($oDbSearchWithConditions->GetInternalParams() as $sValue){
|
||||||
|
$aCurrentValueFields[] = $sValue;
|
||||||
|
}
|
||||||
|
$value =implode(" ", $aCurrentValueFields);
|
||||||
|
$sReason = Dict::Format('UI:CSVReport-Value-NoMatch', $value);
|
||||||
|
return new CellStatus_SearchIssue($sSerializedSearch, $sReason, $oDbSearchWithConditions->GetClass(), $allowedValues);
|
||||||
|
}
|
||||||
|
|
||||||
protected function PrepareMissingObject(&$oTargetObj, &$aErrors)
|
protected function PrepareMissingObject(&$oTargetObj, &$aErrors)
|
||||||
{
|
{
|
||||||
$aResults = array();
|
$aResults = array();
|
||||||
@@ -670,6 +904,8 @@ class BulkChange
|
|||||||
{
|
{
|
||||||
$sErrors = implode(', ', $aErrors);
|
$sErrors = implode(', ', $aErrors);
|
||||||
$aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
|
$aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
|
||||||
|
//__ERRORS__ used by tests only
|
||||||
|
$aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors);
|
||||||
return $oTargetObj;
|
return $oTargetObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -736,6 +972,8 @@ class BulkChange
|
|||||||
{
|
{
|
||||||
$sErrors = implode(', ', $aErrors);
|
$sErrors = implode(', ', $aErrors);
|
||||||
$aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
|
$aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
|
||||||
|
//__ERRORS__ used by tests only
|
||||||
|
$aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -785,6 +1023,8 @@ class BulkChange
|
|||||||
{
|
{
|
||||||
$sErrors = implode(', ', $aErrors);
|
$sErrors = implode(', ', $aErrors);
|
||||||
$aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
|
$aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Attribute'));
|
||||||
|
//__ERRORS__ used by tests only
|
||||||
|
$aResult[$iRow]["__ERRORS__"] = new RowStatus_Error($sErrors);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -872,14 +1112,18 @@ class BulkChange
|
|||||||
$sFormat = $sDateFormat;
|
$sFormat = $sDateFormat;
|
||||||
}
|
}
|
||||||
$oFormat = new DateTimeFormat($sFormat);
|
$oFormat = new DateTimeFormat($sFormat);
|
||||||
|
$sDateExample = $oFormat->Format(new DateTime('2022-10-23 16:25:33'));
|
||||||
$sRegExp = $oFormat->ToRegExpr('/');
|
$sRegExp = $oFormat->ToRegExpr('/');
|
||||||
if (!preg_match($sRegExp, $this->m_aData[$iRow][$iCol]))
|
$sErrorMsg = Dict::Format('UI:CSVReport-Row-Issue-ExpectedDateFormat', $sDateExample);
|
||||||
|
if (!preg_match($sRegExp, $sValue))
|
||||||
{
|
{
|
||||||
$aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat'));
|
$aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat'));
|
||||||
|
$aResult[$iRow][$iCol] = new CellStatus_Issue(utils::HtmlEntities($sValue), null, $sErrorMsg);
|
||||||
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$oDate = DateTime::createFromFormat($sFormat, $this->m_aData[$iRow][$iCol]);
|
$oDate = DateTime::createFromFormat($sFormat, $sValue);
|
||||||
if ($oDate !== false)
|
if ($oDate !== false)
|
||||||
{
|
{
|
||||||
$sNewDate = $oDate->format($oAttDef->GetInternalFormat());
|
$sNewDate = $oDate->format($oAttDef->GetInternalFormat());
|
||||||
@@ -889,7 +1133,7 @@ class BulkChange
|
|||||||
{
|
{
|
||||||
// Leave the cell unchanged
|
// Leave the cell unchanged
|
||||||
$aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat'));
|
$aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-DateFormat'));
|
||||||
$aResult[$iRow][$sAttCode] = new CellStatus_Issue(null, utils::HtmlEntities($this->m_aData[$iRow][$iCol]), Dict::S('UI:CSVReport-Row-Issue-DateFormat'));
|
$aResult[$iRow][$iCol] = new CellStatus_Issue($sValue, null, $sErrorMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -943,7 +1187,9 @@ class BulkChange
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// The value has to be found or verified
|
// The value has to be found or verified
|
||||||
list($sQuery, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]);
|
|
||||||
|
/** var DBObjectSearch $oReconFilter */
|
||||||
|
list($oReconFilter, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]);
|
||||||
|
|
||||||
if (count($aMatches) == 1)
|
if (count($aMatches) == 1)
|
||||||
{
|
{
|
||||||
@@ -953,11 +1199,12 @@ class BulkChange
|
|||||||
}
|
}
|
||||||
elseif (count($aMatches) == 0)
|
elseif (count($aMatches) == 0)
|
||||||
{
|
{
|
||||||
$aResult[$iRow][$sAttCode] = new CellStatus_SearchIssue();
|
$oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter);
|
||||||
|
$aResult[$iRow][$sAttCode] = $oCellStatus_SearchIssue;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $sQuery);
|
$aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $oReconFilter->serialize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1010,7 +1257,7 @@ class BulkChange
|
|||||||
default:
|
default:
|
||||||
// Found several matches, ambiguous
|
// Found several matches, ambiguous
|
||||||
$aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Ambiguous'));
|
$aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Ambiguous'));
|
||||||
$aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->ToOql());
|
$aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->serialize());
|
||||||
$aResult[$iRow]["finalclass"]= 'n/a';
|
$aResult[$iRow]["finalclass"]= 'n/a';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1257,6 +1257,22 @@ class Config
|
|||||||
'source_of_value' => '',
|
'source_of_value' => '',
|
||||||
'show_in_conf_sample' => false,
|
'show_in_conf_sample' => false,
|
||||||
],
|
],
|
||||||
|
'navigation_menu.show_organization_filter' => [
|
||||||
|
'type' => 'bool',
|
||||||
|
'description' => 'Display organization filter in menu',
|
||||||
|
'default' => true,
|
||||||
|
'value' => true,
|
||||||
|
'source_of_value' => '',
|
||||||
|
'show_in_conf_sample' => false,
|
||||||
|
],
|
||||||
|
'navigation_menu.sorted_popup_user_menu_items' => [
|
||||||
|
'type' => 'array',
|
||||||
|
'description' => 'Sort user menu items after setup on page load',
|
||||||
|
'default' => [],
|
||||||
|
'value' => false,
|
||||||
|
'source_of_value' => '',
|
||||||
|
'show_in_conf_sample' => false,
|
||||||
|
],
|
||||||
'quick_create.enabled' => [
|
'quick_create.enabled' => [
|
||||||
'type' => 'bool',
|
'type' => 'bool',
|
||||||
'description' => 'Whether or not the quick create is enabled',
|
'description' => 'Whether or not the quick create is enabled',
|
||||||
|
|||||||
5
css/backoffice/pages/_csv-import.scss
vendored
5
css/backoffice/pages/_csv-import.scss
vendored
@@ -34,11 +34,6 @@ tr.ibo-csv-import--row-unchanged td {
|
|||||||
border-bottom: 1px $ibo-csv-import--row--border-color solid;
|
border-bottom: 1px $ibo-csv-import--row--border-color solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wizContainer table tr.ibo-csv-import--row-error td {
|
|
||||||
border-bottom: 1px $ibo-csv-import--row--border-color solid;
|
|
||||||
background-color: $ibo-csv-import--row-error--background-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.ibo-csv-import--row-modified td {
|
tr.ibo-csv-import--row-modified td {
|
||||||
border-bottom: 1px $ibo-csv-import--row--border-color solid;
|
border-bottom: 1px $ibo-csv-import--row--border-color solid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ $ibo-welcome-popup--text--options--bottom: 10px !default;
|
|||||||
|
|
||||||
#welcome_popup{
|
#welcome_popup{
|
||||||
display: flex;
|
display: flex;
|
||||||
|
}
|
||||||
|
.ibo-welcome-popup--columns{
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
.ibo-welcome-popup--image{
|
.ibo-welcome-popup--image{
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -44,7 +46,39 @@ $ibo-welcome-popup--text--options--bottom: 10px !default;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ibo-welcome-popup--text--options{
|
.ibo-welcome-popup--dialog {
|
||||||
position: absolute;
|
width: 60rem;
|
||||||
bottom: $ibo-welcome-popup--text--options--bottom;
|
}
|
||||||
|
.ibo-welcome-popup--content {
|
||||||
|
width: 100%;
|
||||||
|
.ibo-welcome-popup--message {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 12rem;
|
||||||
|
}
|
||||||
|
.ibo-welcome-popup--button {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 1rem;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ibo-welcome-popup--indicators {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
padding-bottom: 0;
|
||||||
|
height: 3rem;
|
||||||
|
.ibo-welcome-popup--indicator {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background-color: $ibo-color-secondary-600;
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.ibo-welcome-popup--active {
|
||||||
|
background-color: $ibo-color-information-600 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,10 @@ $ibo-sticky-sentinel-bottom--height: $ibo-sticky-sentinel--height !default;
|
|||||||
opacity: 1 !important; /* Note: !important is necessary as it needs to overload any standard rules */
|
opacity: 1 !important; /* Note: !important is necessary as it needs to overload any standard rules */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ibo-is-disabled {
|
||||||
|
cursor: not-allowed !important; /* Note: !important is necessary as it needs to overload any standard rules */
|
||||||
|
}
|
||||||
|
|
||||||
/****************************/
|
/****************************/
|
||||||
/* Disposition / alignement */
|
/* Disposition / alignement */
|
||||||
/****************************/
|
/****************************/
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -188,6 +188,12 @@ class DBRestore extends DBBackup
|
|||||||
rename($sDataDir.'/config-itop.php', $sConfigFile);
|
rename($sDataDir.'/config-itop.php', $sConfigFile);
|
||||||
@chmod($sConfigFile, 0440); // Read-only
|
@chmod($sConfigFile, 0440); // Read-only
|
||||||
|
|
||||||
|
$aExtraFiles = $this->ListExtraFiles($sDataDir);
|
||||||
|
foreach($aExtraFiles as $sSourceFilePath => $sDestinationFilePath) {
|
||||||
|
SetupUtils::builddir(dirname($sDestinationFilePath));
|
||||||
|
rename($sSourceFilePath, $sDestinationFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SetupUtils::rrmdir($sDataDir);
|
SetupUtils::rrmdir($sDataDir);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
@@ -211,4 +217,27 @@ class DBRestore extends DBBackup
|
|||||||
$oRestoreMutex->Unlock();
|
$oRestoreMutex->Unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List the 'extra files' found in the decompressed archive
|
||||||
|
* (i.e. files other than config-itop.php, delta.xml, itop-dump.sql or production-modules/*
|
||||||
|
* @param string $sDataDir
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
protected function ListExtraFiles(string $sDataDir)
|
||||||
|
{
|
||||||
|
$aExtraFiles = [];
|
||||||
|
$aStandardFiles = ['config-itop.php', 'itop-dump.sql', 'production-modules', 'delta.xml'];
|
||||||
|
$oDirectoryIterator = new RecursiveDirectoryIterator($sDataDir, FilesystemIterator::CURRENT_AS_FILEINFO|FilesystemIterator::SKIP_DOTS);
|
||||||
|
$oIterator = new RecursiveIteratorIterator($oDirectoryIterator);
|
||||||
|
foreach ($oIterator as $oFileInfo)
|
||||||
|
{
|
||||||
|
if (in_array($oFileInfo->getFilename(), $aStandardFiles)) continue;
|
||||||
|
if (strncmp($oFileInfo->getPathname(), $sDataDir.'/production-modules', strlen($sDataDir.'/production-modules')) == 0) continue;
|
||||||
|
|
||||||
|
$aExtraFiles[$oFileInfo->getPathname()] = APPROOT.substr($oFileInfo->getPathname(), strlen($sDataDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $aExtraFiles;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -644,9 +644,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
|||||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Vyberte třídu pro hledání: ',
|
'UI:UniversalSearch:LabelSelectTheClass' => 'Vyberte třídu pro hledání: ',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Modified' => 'Upraveno',
|
'UI:CSVReport-Value-Modified' => 'Upraveno',
|
||||||
'UI:CSVReport-Value-SetIssue' => 'Nemůže být změněno - důvod: %1$s',
|
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||||
'UI:CSVReport-Value-ChangeIssue' => 'Nemůže být změněno na %1$s - důvod: %2$s',
|
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||||
'UI:CSVReport-Value-NoMatch' => 'Žádná shoda',
|
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||||
'UI:CSVReport-Value-Missing' => 'Chybí povinná hodnota',
|
'UI:CSVReport-Value-Missing' => 'Chybí povinná hodnota',
|
||||||
'UI:CSVReport-Value-Ambiguous' => 'Nejednoznačné: nalezeno %1$s objektů',
|
'UI:CSVReport-Value-Ambiguous' => 'Nejednoznačné: nalezeno %1$s objektů',
|
||||||
'UI:CSVReport-Row-Unchanged' => 'nezměněn',
|
'UI:CSVReport-Row-Unchanged' => 'nezměněn',
|
||||||
|
|||||||
@@ -633,9 +633,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
|||||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Vælg klasse at søge efter: ',
|
'UI:UniversalSearch:LabelSelectTheClass' => 'Vælg klasse at søge efter: ',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Modified' => 'Ændret',
|
'UI:CSVReport-Value-Modified' => 'Ændret',
|
||||||
'UI:CSVReport-Value-SetIssue' => 'Kunne ikke ændres - årsag: %1$s',
|
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||||
'UI:CSVReport-Value-ChangeIssue' => 'Kunne ikke ændres til %1$s - årsag: %2$s',
|
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||||
'UI:CSVReport-Value-NoMatch' => 'No match',
|
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||||
'UI:CSVReport-Value-Missing' => 'Mangler obligatorisk værdi',
|
'UI:CSVReport-Value-Missing' => 'Mangler obligatorisk værdi',
|
||||||
'UI:CSVReport-Value-Ambiguous' => 'Tvetydig: fandt %1$s objekter',
|
'UI:CSVReport-Value-Ambiguous' => 'Tvetydig: fandt %1$s objekter',
|
||||||
'UI:CSVReport-Row-Unchanged' => 'Uændret',
|
'UI:CSVReport-Row-Unchanged' => 'Uændret',
|
||||||
|
|||||||
@@ -365,6 +365,7 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
|
|||||||
We hope you’ll enjoy this version as much as we enjoyed imagining and creating it.</div>
|
We hope you’ll enjoy this version as much as we enjoyed imagining and creating it.</div>
|
||||||
|
|
||||||
<div>Customize your '.ITOP_APPLICATION.' preferences for a personalized experience.</div>~~',
|
<div>Customize your '.ITOP_APPLICATION.' preferences for a personalized experience.</div>~~',
|
||||||
|
'UI:WelcomePopup:Button:Acknowledge' => 'Ok, verwerfen Sie diese Nachricht',
|
||||||
'UI:WelcomeMenu:AllOpenRequests' => 'Offene Requests: %1$d',
|
'UI:WelcomeMenu:AllOpenRequests' => 'Offene Requests: %1$d',
|
||||||
'UI:WelcomeMenu:MyCalls' => 'An mich gestellte Benutzeranfragen',
|
'UI:WelcomeMenu:MyCalls' => 'An mich gestellte Benutzeranfragen',
|
||||||
'UI:WelcomeMenu:OpenIncidents' => 'Offene Incidents: %1$d',
|
'UI:WelcomeMenu:OpenIncidents' => 'Offene Incidents: %1$d',
|
||||||
@@ -633,9 +634,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
|||||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Wählen Sie für die Suche die Klasse aus: ',
|
'UI:UniversalSearch:LabelSelectTheClass' => 'Wählen Sie für die Suche die Klasse aus: ',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Modified' => 'Modifiziert',
|
'UI:CSVReport-Value-Modified' => 'Modifiziert',
|
||||||
'UI:CSVReport-Value-SetIssue' => 'Konnte nicht geändert werden - Grund: %1$s',
|
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||||
'UI:CSVReport-Value-ChangeIssue' => 'Konnte nicht zu %1$s geändert werden - Grund: %2$s',
|
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||||
'UI:CSVReport-Value-NoMatch' => 'Kein Treffer',
|
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||||
'UI:CSVReport-Value-Missing' => 'Pflichtfeld fehlt',
|
'UI:CSVReport-Value-Missing' => 'Pflichtfeld fehlt',
|
||||||
'UI:CSVReport-Value-Ambiguous' => 'Doppeldeutig: %1$s Objekte gefunden',
|
'UI:CSVReport-Value-Ambiguous' => 'Doppeldeutig: %1$s Objekte gefunden',
|
||||||
'UI:CSVReport-Row-Unchanged' => 'Unverändert',
|
'UI:CSVReport-Row-Unchanged' => 'Unverändert',
|
||||||
|
|||||||
@@ -381,6 +381,7 @@ Dict::Add('EN US', 'English', 'English', array(
|
|||||||
We hope you’ll enjoy this version as much as we enjoyed imagining and creating it.</div>
|
We hope you’ll enjoy this version as much as we enjoyed imagining and creating it.</div>
|
||||||
|
|
||||||
<div>Customize your '.ITOP_APPLICATION.' preferences for a personalized experience.</div>',
|
<div>Customize your '.ITOP_APPLICATION.' preferences for a personalized experience.</div>',
|
||||||
|
'UI:WelcomePopup:Button:Acknowledge' => 'Ok, discard this message',
|
||||||
'UI:WelcomeMenu:AllOpenRequests' => 'Open requests: %1$d',
|
'UI:WelcomeMenu:AllOpenRequests' => 'Open requests: %1$d',
|
||||||
'UI:WelcomeMenu:MyCalls' => 'My requests',
|
'UI:WelcomeMenu:MyCalls' => 'My requests',
|
||||||
'UI:WelcomeMenu:OpenIncidents' => 'Open incidents: %1$d',
|
'UI:WelcomeMenu:OpenIncidents' => 'Open incidents: %1$d',
|
||||||
@@ -650,9 +651,14 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
|||||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Select the class to search: ',
|
'UI:UniversalSearch:LabelSelectTheClass' => 'Select the class to search: ',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Modified' => 'Modified',
|
'UI:CSVReport-Value-Modified' => 'Modified',
|
||||||
'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s',
|
'UI:CSVReport-Value-SetIssue' => 'Invalid value for attribute',
|
||||||
'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s',
|
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value',
|
||||||
'UI:CSVReport-Value-NoMatch' => 'No match',
|
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'',
|
||||||
|
'UI:CSVReport-Value-NoMatch-PossibleValues' => 'Some possible \'%1$s\' value(s): %2$s',
|
||||||
|
'UI:CSVReport-Value-NoMatch-NoObject' => 'There are no \'%1$s\' objects',
|
||||||
|
'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => 'There are no \'%1$s\' objects found with your current profile',
|
||||||
|
'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => 'There are some \'%1$s\' objects not visible with your current profile',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Missing' => 'Missing mandatory value',
|
'UI:CSVReport-Value-Missing' => 'Missing mandatory value',
|
||||||
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects',
|
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects',
|
||||||
'UI:CSVReport-Row-Unchanged' => 'unchanged',
|
'UI:CSVReport-Row-Unchanged' => 'unchanged',
|
||||||
@@ -666,11 +672,13 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
|||||||
'UI:CSVReport-Value-Issue-Readonly' => 'The attribute \'%1$s\' is read-only and cannot be modified (current value: %2$s, proposed value: %3$s)',
|
'UI:CSVReport-Value-Issue-Readonly' => 'The attribute \'%1$s\' is read-only and cannot be modified (current value: %2$s, proposed value: %3$s)',
|
||||||
'UI:CSVReport-Value-Issue-Format' => 'Failed to process input: %1$s',
|
'UI:CSVReport-Value-Issue-Format' => 'Failed to process input: %1$s',
|
||||||
'UI:CSVReport-Value-Issue-NoMatch' => 'Unexpected value for attribute \'%1$s\': no match found, check spelling',
|
'UI:CSVReport-Value-Issue-NoMatch' => 'Unexpected value for attribute \'%1$s\': no match found, check spelling',
|
||||||
|
'UI:CSVReport-Value-Issue-AllowedValues' => 'Allowed \'%1$s\' value(s): %2$s',
|
||||||
'UI:CSVReport-Value-Issue-Unknown' => 'Unexpected value for attribute \'%1$s\': %2$s',
|
'UI:CSVReport-Value-Issue-Unknown' => 'Unexpected value for attribute \'%1$s\': %2$s',
|
||||||
'UI:CSVReport-Row-Issue-Inconsistent' => 'Attributes not consistent with each others: %1$s',
|
'UI:CSVReport-Row-Issue-Inconsistent' => 'Attributes not consistent with each others: %1$s',
|
||||||
'UI:CSVReport-Row-Issue-Attribute' => 'Unexpected attribute value(s)',
|
'UI:CSVReport-Row-Issue-Attribute' => 'Unexpected attribute value(s)',
|
||||||
'UI:CSVReport-Row-Issue-MissingExtKey' => 'Could not be created, due to missing external key(s): %1$s',
|
'UI:CSVReport-Row-Issue-MissingExtKey' => 'Could not be created, due to missing external key(s): %1$s',
|
||||||
'UI:CSVReport-Row-Issue-DateFormat' => 'wrong date format',
|
'UI:CSVReport-Row-Issue-DateFormat' => 'wrong date format',
|
||||||
|
'UI:CSVReport-Row-Issue-ExpectedDateFormat' => 'Expected format: %1$s',
|
||||||
'UI:CSVReport-Row-Issue-Reconciliation' => 'failed to reconcile',
|
'UI:CSVReport-Row-Issue-Reconciliation' => 'failed to reconcile',
|
||||||
'UI:CSVReport-Row-Issue-Ambiguous' => 'ambiguous reconciliation',
|
'UI:CSVReport-Row-Issue-Ambiguous' => 'ambiguous reconciliation',
|
||||||
'UI:CSVReport-Row-Issue-Internal' => 'Internal error: %1$s, %2$s',
|
'UI:CSVReport-Row-Issue-Internal' => 'Internal error: %1$s, %2$s',
|
||||||
|
|||||||
@@ -377,6 +377,7 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array(
|
|||||||
Esperamos distrute de esta versión tanto como nosotros la imaginamos y creamos.</div>
|
Esperamos distrute de esta versión tanto como nosotros la imaginamos y creamos.</div>
|
||||||
|
|
||||||
<div>Configure las preferencias de '.ITOP_APPLICATION.' para una experiencia personalizada.</div>',
|
<div>Configure las preferencias de '.ITOP_APPLICATION.' para una experiencia personalizada.</div>',
|
||||||
|
'UI:WelcomePopup:Button:Acknowledge' => 'Ok, descartar este mensaje',
|
||||||
'UI:WelcomeMenu:AllOpenRequests' => 'Requerimientos Abiertos: %1$d',
|
'UI:WelcomeMenu:AllOpenRequests' => 'Requerimientos Abiertos: %1$d',
|
||||||
'UI:WelcomeMenu:MyCalls' => 'Mis Requerimientos',
|
'UI:WelcomeMenu:MyCalls' => 'Mis Requerimientos',
|
||||||
'UI:WelcomeMenu:OpenIncidents' => 'Incidentes Abiertos: %1$d',
|
'UI:WelcomeMenu:OpenIncidents' => 'Incidentes Abiertos: %1$d',
|
||||||
@@ -644,9 +645,9 @@ Esperamos distrute de esta versión tanto como nosotros la imaginamos y creamos.
|
|||||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Seleccione la clase a buscar: ',
|
'UI:UniversalSearch:LabelSelectTheClass' => 'Seleccione la clase a buscar: ',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Modified' => 'Modificado',
|
'UI:CSVReport-Value-Modified' => 'Modificado',
|
||||||
'UI:CSVReport-Value-SetIssue' => 'No puede ser modificado - motivo: %1$s',
|
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||||
'UI:CSVReport-Value-ChangeIssue' => 'No puede ser cambiado a %1$s - motivo: %2$s',
|
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||||
'UI:CSVReport-Value-NoMatch' => 'No hay Coincidencias',
|
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||||
'UI:CSVReport-Value-Missing' => 'Falta valor obligatorio',
|
'UI:CSVReport-Value-Missing' => 'Falta valor obligatorio',
|
||||||
'UI:CSVReport-Value-Ambiguous' => 'Ambigüedad: encontrados %1$s objetos',
|
'UI:CSVReport-Value-Ambiguous' => 'Ambigüedad: encontrados %1$s objetos',
|
||||||
'UI:CSVReport-Row-Unchanged' => 'Sin Cambios',
|
'UI:CSVReport-Row-Unchanged' => 'Sin Cambios',
|
||||||
|
|||||||
@@ -365,6 +365,7 @@ Dict::Add('FR FR', 'French', 'Français', array(
|
|||||||
Nous espérons que vous aimerez cette version autant que nous avons eu du plaisir à l\'imaginer et à la créer.</div>
|
Nous espérons que vous aimerez cette version autant que nous avons eu du plaisir à l\'imaginer et à la créer.</div>
|
||||||
|
|
||||||
<div>Configurez vos préférences '.ITOP_APPLICATION.' pour une expérience personnalisée.</div>',
|
<div>Configurez vos préférences '.ITOP_APPLICATION.' pour une expérience personnalisée.</div>',
|
||||||
|
'UI:WelcomePopup:Button:Acknowledge' => 'Ok, supprimer ce message',
|
||||||
'UI:WelcomeMenu:AllOpenRequests' => 'Requêtes en cours: %1$d',
|
'UI:WelcomeMenu:AllOpenRequests' => 'Requêtes en cours: %1$d',
|
||||||
'UI:WelcomeMenu:MyCalls' => 'Mes Appels Support',
|
'UI:WelcomeMenu:MyCalls' => 'Mes Appels Support',
|
||||||
'UI:WelcomeMenu:OpenIncidents' => 'Incidents en cours: %1$d',
|
'UI:WelcomeMenu:OpenIncidents' => 'Incidents en cours: %1$d',
|
||||||
@@ -633,9 +634,14 @@ Nous espérons que vous aimerez cette version autant que nous avons eu du plaisi
|
|||||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Sélectionnez le type d\'objets à rechercher : ',
|
'UI:UniversalSearch:LabelSelectTheClass' => 'Sélectionnez le type d\'objets à rechercher : ',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Modified' => 'Modifié',
|
'UI:CSVReport-Value-Modified' => 'Modifié',
|
||||||
'UI:CSVReport-Value-SetIssue' => 'Modification impossible - cause : %1$s',
|
'UI:CSVReport-Value-SetIssue' => 'Valeur invalide',
|
||||||
'UI:CSVReport-Value-ChangeIssue' => 'Ne peut pas prendre la valeur \'%1$s\' - cause : %2$s',
|
'UI:CSVReport-Value-ChangeIssue' => 'Ne peut pas prendre la valeur \'%1$s\'',
|
||||||
'UI:CSVReport-Value-NoMatch' => 'Pas de correspondance',
|
'UI:CSVReport-Value-NoMatch' => 'Pas de correspondance avec \'%1$s\'',
|
||||||
|
'UI:CSVReport-Value-NoMatch-PossibleValues' => 'Valeur(s) possible(s) pour l\'objet \'%1$s\' : %2$s',
|
||||||
|
'UI:CSVReport-Value-NoMatch-NoObject' => 'Il n\'y a aucun objet \'%1$s\'',
|
||||||
|
'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => 'Il n\'y a aucun objet \'%1$s\' visible par votre utilisateur',
|
||||||
|
'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => 'Il existe des objet(s) \'%1$s\' non visible(s) par votre utilisateur',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Missing' => 'Absence de valeur obligatoire',
|
'UI:CSVReport-Value-Missing' => 'Absence de valeur obligatoire',
|
||||||
'UI:CSVReport-Value-Ambiguous' => 'Ambigüité: %1$d objets trouvés',
|
'UI:CSVReport-Value-Ambiguous' => 'Ambigüité: %1$d objets trouvés',
|
||||||
'UI:CSVReport-Row-Unchanged' => 'inchangé',
|
'UI:CSVReport-Row-Unchanged' => 'inchangé',
|
||||||
|
|||||||
@@ -633,9 +633,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
|||||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Keresendő osztály kiválasztása:',
|
'UI:UniversalSearch:LabelSelectTheClass' => 'Keresendő osztály kiválasztása:',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Modified' => 'Modified~~',
|
'UI:CSVReport-Value-Modified' => 'Modified~~',
|
||||||
'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s~~',
|
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||||
'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s~~',
|
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||||
'UI:CSVReport-Value-NoMatch' => 'No match~~',
|
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||||
'UI:CSVReport-Value-Missing' => 'Missing mandatory value~~',
|
'UI:CSVReport-Value-Missing' => 'Missing mandatory value~~',
|
||||||
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects~~',
|
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects~~',
|
||||||
'UI:CSVReport-Row-Unchanged' => 'unchanged~~',
|
'UI:CSVReport-Row-Unchanged' => 'unchanged~~',
|
||||||
|
|||||||
@@ -644,9 +644,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
|||||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Seleziona la classe per la ricerca: ',
|
'UI:UniversalSearch:LabelSelectTheClass' => 'Seleziona la classe per la ricerca: ',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Modified' => 'Modified~~',
|
'UI:CSVReport-Value-Modified' => 'Modified~~',
|
||||||
'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s~~',
|
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||||
'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s~~',
|
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||||
'UI:CSVReport-Value-NoMatch' => 'No match~~',
|
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||||
'UI:CSVReport-Value-Missing' => 'Missing mandatory value~~',
|
'UI:CSVReport-Value-Missing' => 'Missing mandatory value~~',
|
||||||
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects~~',
|
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects~~',
|
||||||
'UI:CSVReport-Row-Unchanged' => 'unchanged~~',
|
'UI:CSVReport-Row-Unchanged' => 'unchanged~~',
|
||||||
|
|||||||
@@ -633,9 +633,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
|||||||
'UI:UniversalSearch:LabelSelectTheClass' => '検索するクラスを選択してください。',
|
'UI:UniversalSearch:LabelSelectTheClass' => '検索するクラスを選択してください。',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Modified' => '修正済み',
|
'UI:CSVReport-Value-Modified' => '修正済み',
|
||||||
'UI:CSVReport-Value-SetIssue' => '変更出来ません - 理由: %1$s',
|
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||||
'UI:CSVReport-Value-ChangeIssue' => '%1$s へ変更出来ません - 理由: %2$s',
|
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||||
'UI:CSVReport-Value-NoMatch' => 'マッチしません',
|
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||||
'UI:CSVReport-Value-Missing' => '必須の値がありません',
|
'UI:CSVReport-Value-Missing' => '必須の値がありません',
|
||||||
'UI:CSVReport-Value-Ambiguous' => 'あいまいな値: %1$s オブジェクト',
|
'UI:CSVReport-Value-Ambiguous' => 'あいまいな値: %1$s オブジェクト',
|
||||||
'UI:CSVReport-Row-Unchanged' => '未変更',
|
'UI:CSVReport-Row-Unchanged' => '未変更',
|
||||||
|
|||||||
@@ -644,9 +644,9 @@ We hopen dat je even hard van deze versie geniet als dat we zelf ervan hebben ge
|
|||||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Selecteer de klasse om te zoeken: ',
|
'UI:UniversalSearch:LabelSelectTheClass' => 'Selecteer de klasse om te zoeken: ',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Modified' => 'Aangepast',
|
'UI:CSVReport-Value-Modified' => 'Aangepast',
|
||||||
'UI:CSVReport-Value-SetIssue' => 'Kon niet worden aangepast - reden: %1$s',
|
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||||
'UI:CSVReport-Value-ChangeIssue' => 'Kon niet worden aangepast naar %1$s - reden: %2$s',
|
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||||
'UI:CSVReport-Value-NoMatch' => 'Geen match',
|
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||||
'UI:CSVReport-Value-Missing' => 'Ontbrekende verplichte waarde',
|
'UI:CSVReport-Value-Missing' => 'Ontbrekende verplichte waarde',
|
||||||
'UI:CSVReport-Value-Ambiguous' => 'Onduidelijk: gevonden %1$s objecten',
|
'UI:CSVReport-Value-Ambiguous' => 'Onduidelijk: gevonden %1$s objecten',
|
||||||
'UI:CSVReport-Row-Unchanged' => 'onveranderd',
|
'UI:CSVReport-Row-Unchanged' => 'onveranderd',
|
||||||
|
|||||||
@@ -643,9 +643,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
|||||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Wybierz klasę do przeszukania: ',
|
'UI:UniversalSearch:LabelSelectTheClass' => 'Wybierz klasę do przeszukania: ',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Modified' => 'Zmodyfikowano',
|
'UI:CSVReport-Value-Modified' => 'Zmodyfikowano',
|
||||||
'UI:CSVReport-Value-SetIssue' => 'Nie można było zmienić - powód: %1$s',
|
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||||
'UI:CSVReport-Value-ChangeIssue' => 'Nie można zmienić na %1$s - powód: %2$s',
|
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||||
'UI:CSVReport-Value-NoMatch' => 'Nie pasuje',
|
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||||
'UI:CSVReport-Value-Missing' => 'Brak wymaganej wartości',
|
'UI:CSVReport-Value-Missing' => 'Brak wymaganej wartości',
|
||||||
'UI:CSVReport-Value-Ambiguous' => 'Uwaga: znaleziono %1$s obiektów',
|
'UI:CSVReport-Value-Ambiguous' => 'Uwaga: znaleziono %1$s obiektów',
|
||||||
'UI:CSVReport-Row-Unchanged' => 'niezmieniony',
|
'UI:CSVReport-Row-Unchanged' => 'niezmieniony',
|
||||||
|
|||||||
@@ -644,9 +644,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
|||||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Selecione a classe para pesquisar: ',
|
'UI:UniversalSearch:LabelSelectTheClass' => 'Selecione a classe para pesquisar: ',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Modified' => 'Modificado',
|
'UI:CSVReport-Value-Modified' => 'Modificado',
|
||||||
'UI:CSVReport-Value-SetIssue' => 'Não pode ser modificado - razão: %1$s',
|
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||||
'UI:CSVReport-Value-ChangeIssue' => 'Não pode ser modificado para %1$s - razão: %2$s',
|
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||||
'UI:CSVReport-Value-NoMatch' => 'Não combina',
|
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||||
'UI:CSVReport-Value-Missing' => 'Faltando valor obrigatório',
|
'UI:CSVReport-Value-Missing' => 'Faltando valor obrigatório',
|
||||||
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects',
|
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects',
|
||||||
'UI:CSVReport-Row-Unchanged' => 'unchanged',
|
'UI:CSVReport-Row-Unchanged' => 'unchanged',
|
||||||
|
|||||||
@@ -645,9 +645,9 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
|
|||||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Выбор класса для поиска: ',
|
'UI:UniversalSearch:LabelSelectTheClass' => 'Выбор класса для поиска: ',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Modified' => 'Изменен',
|
'UI:CSVReport-Value-Modified' => 'Изменен',
|
||||||
'UI:CSVReport-Value-SetIssue' => 'Не может быть изменен - причина: %1$s',
|
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||||
'UI:CSVReport-Value-ChangeIssue' => 'Не может быть изменен %1$s - причина: %2$s',
|
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||||
'UI:CSVReport-Value-NoMatch' => 'Нет совпадений',
|
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||||
'UI:CSVReport-Value-Missing' => 'Отсутствует обязательное значение',
|
'UI:CSVReport-Value-Missing' => 'Отсутствует обязательное значение',
|
||||||
'UI:CSVReport-Value-Ambiguous' => 'Неоднозначное сопоставление: найдено %1$s объектов',
|
'UI:CSVReport-Value-Ambiguous' => 'Неоднозначное сопоставление: найдено %1$s объектов',
|
||||||
'UI:CSVReport-Row-Unchanged' => 'без изменений',
|
'UI:CSVReport-Row-Unchanged' => 'без изменений',
|
||||||
|
|||||||
@@ -634,9 +634,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
|||||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Vyberte triedu na vyhľadávanie: ',
|
'UI:UniversalSearch:LabelSelectTheClass' => 'Vyberte triedu na vyhľadávanie: ',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Modified' => 'Upravený',
|
'UI:CSVReport-Value-Modified' => 'Upravený',
|
||||||
'UI:CSVReport-Value-SetIssue' => 'Nemožno zmeniť - dôvod: %1$s',
|
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||||
'UI:CSVReport-Value-ChangeIssue' => 'Nemožno zmeniť na %1$s - dôvod: %2$s',
|
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||||
'UI:CSVReport-Value-NoMatch' => 'Žiadna zhoda',
|
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||||
'UI:CSVReport-Value-Missing' => 'Chýbajúca povinná hodnota',
|
'UI:CSVReport-Value-Missing' => 'Chýbajúca povinná hodnota',
|
||||||
'UI:CSVReport-Value-Ambiguous' => 'Nejednoznačné: nájdených %1$s objektov',
|
'UI:CSVReport-Value-Ambiguous' => 'Nejednoznačné: nájdených %1$s objektov',
|
||||||
'UI:CSVReport-Row-Unchanged' => 'Nezmený',
|
'UI:CSVReport-Row-Unchanged' => 'Nezmený',
|
||||||
|
|||||||
@@ -661,9 +661,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
|||||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Aranacak sınıfı seçiniz: ',
|
'UI:UniversalSearch:LabelSelectTheClass' => 'Aranacak sınıfı seçiniz: ',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Modified' => 'Değiştiridi',
|
'UI:CSVReport-Value-Modified' => 'Değiştiridi',
|
||||||
'UI:CSVReport-Value-SetIssue' => 'Değiştirilemedi - Sebep: %1$s',
|
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||||
'UI:CSVReport-Value-ChangeIssue' => '%1$s olarak değiştirilemedi - Sebep: %2$s',
|
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||||
'UI:CSVReport-Value-NoMatch' => 'Eşleşme yok',
|
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||||
'UI:CSVReport-Value-Missing' => 'Eksik Zorunlu Değer',
|
'UI:CSVReport-Value-Missing' => 'Eksik Zorunlu Değer',
|
||||||
'UI:CSVReport-Value-Ambiguous' => 'Belirsiz: %1$s nesnelerini buldum',
|
'UI:CSVReport-Value-Ambiguous' => 'Belirsiz: %1$s nesnelerini buldum',
|
||||||
'UI:CSVReport-Row-Unchanged' => 'Değiştirilmedi',
|
'UI:CSVReport-Row-Unchanged' => 'Değiştirilmedi',
|
||||||
|
|||||||
@@ -649,9 +649,9 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating
|
|||||||
'UI:UniversalSearch:LabelSelectTheClass' => '选择要搜索的类别: ',
|
'UI:UniversalSearch:LabelSelectTheClass' => '选择要搜索的类别: ',
|
||||||
|
|
||||||
'UI:CSVReport-Value-Modified' => '已修改',
|
'UI:CSVReport-Value-Modified' => '已修改',
|
||||||
'UI:CSVReport-Value-SetIssue' => '无法修改 - 原因: %1$s',
|
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||||
'UI:CSVReport-Value-ChangeIssue' => '无法修改成 %1$s - 原因: %2$s',
|
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||||
'UI:CSVReport-Value-NoMatch' => '不匹配',
|
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||||
'UI:CSVReport-Value-Missing' => '缺少必填项',
|
'UI:CSVReport-Value-Missing' => '缺少必填项',
|
||||||
'UI:CSVReport-Value-Ambiguous' => '模糊匹配: 找到 %1$s 个对象',
|
'UI:CSVReport-Value-Ambiguous' => '模糊匹配: 找到 %1$s 个对象',
|
||||||
'UI:CSVReport-Row-Unchanged' => '保持不变',
|
'UI:CSVReport-Row-Unchanged' => '保持不变',
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
@@ -124,5 +124,16 @@ $.widget( "itop.regulartabs", $.ui.tabs, {
|
|||||||
this._off( prevPanels.not( this.panels ) );
|
this._off( prevPanels.not( this.panels ) );
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// JQuery UI overload
|
||||||
|
disable: function(index){
|
||||||
|
const panel = this._getPanelForTab( index );
|
||||||
|
panel.addClass('ibo-is-hidden'); // Do not use .hide() since it alters the tab state
|
||||||
|
this._super( index );
|
||||||
|
},
|
||||||
|
// JQuery UI overload
|
||||||
|
enable: function(index) {
|
||||||
|
const panel = this._getPanelForTab( index );
|
||||||
|
panel.removeClass('ibo-is-hidden'); // Do not use .show() since it alters the tab state
|
||||||
|
this._super( index );
|
||||||
|
},
|
||||||
});
|
});
|
||||||
@@ -377,4 +377,16 @@ $.widget( "itop.scrollabletabs", $.ui.tabs, {
|
|||||||
setTab : function(tab){
|
setTab : function(tab){
|
||||||
this.active = tab;
|
this.active = tab;
|
||||||
},
|
},
|
||||||
|
// JQuery UI overload
|
||||||
|
disable: function(index){
|
||||||
|
const panel = this._getPanelForTab( this.tabs[index] );
|
||||||
|
panel.addClass('ibo-is-hidden'); // Do not use .hide() since it alters the tab state
|
||||||
|
this._super( index );
|
||||||
|
},
|
||||||
|
// JQuery UI overload
|
||||||
|
enable: function(index) {
|
||||||
|
const panel = this._getPanelForTab( this.tabs[index] );
|
||||||
|
panel.removeClass('ibo-is-hidden'); // Do not use .show() since it alters the tab state
|
||||||
|
this._super( index );
|
||||||
|
},
|
||||||
});
|
});
|
||||||
@@ -15,6 +15,7 @@ $(function()
|
|||||||
css_classes:
|
css_classes:
|
||||||
{
|
{
|
||||||
is_hidden: 'ibo-is-hidden',
|
is_hidden: 'ibo-is-hidden',
|
||||||
|
is_disabled: 'ibo-is-disabled',
|
||||||
is_transparent: 'ibo-is-transparent',
|
is_transparent: 'ibo-is-transparent',
|
||||||
is_opaque: 'ibo-is-opaque',
|
is_opaque: 'ibo-is-opaque',
|
||||||
is_scrollable: 'ibo-is-scrollable',
|
is_scrollable: 'ibo-is-scrollable',
|
||||||
@@ -252,6 +253,11 @@ $(function()
|
|||||||
// Prevent anchor default behaviour
|
// Prevent anchor default behaviour
|
||||||
oEvent.preventDefault();
|
oEvent.preventDefault();
|
||||||
|
|
||||||
|
if (oExtraTabTogglerElem.attr('aria-disabled') === 'true') {
|
||||||
|
// Corresponding tab is disabled, do nothing
|
||||||
|
oEvent.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Trigger click event on real tab toggler (the hidden one)
|
// Trigger click event on real tab toggler (the hidden one)
|
||||||
const sTargetTabId = oExtraTabTogglerElem.attr('href').replace(/#/, '');
|
const sTargetTabId = oExtraTabTogglerElem.attr('href').replace(/#/, '');
|
||||||
this.element.find(this.js_selectors.tab_header+'[data-tab-id="'+sTargetTabId+'"] '+this.js_selectors.tab_toggler).trigger('click');
|
this.element.find(this.js_selectors.tab_header+'[data-tab-id="'+sTargetTabId+'"] '+this.js_selectors.tab_toggler).trigger('click');
|
||||||
@@ -297,6 +303,11 @@ $(function()
|
|||||||
const sTabId = oTabHeaderElem.attr('data-tab-id');
|
const sTabId = oTabHeaderElem.attr('data-tab-id');
|
||||||
const oMatchingExtraTabElem = this.element.find(this.js_selectors.extra_tab_toggler+'[href="#'+sTabId+'"]');
|
const oMatchingExtraTabElem = this.element.find(this.js_selectors.extra_tab_toggler+'[href="#'+sTabId+'"]');
|
||||||
|
|
||||||
|
// Disabled tabs should be disabled in the ExtraTabs list as well
|
||||||
|
let bIsDisabled = false;
|
||||||
|
if (oTabHeaderElem.attr('aria-disabled') === 'true') {
|
||||||
|
bIsDisabled = true;
|
||||||
|
}
|
||||||
// Manually check if the tab header is visible if the info isn't passed
|
// Manually check if the tab header is visible if the info isn't passed
|
||||||
if (bIsVisible === null) {
|
if (bIsVisible === null) {
|
||||||
bIsVisible = CombodoGlobalToolbox.IsElementVisibleToTheUser(oTabHeaderElem[0], true, 2);
|
bIsVisible = CombodoGlobalToolbox.IsElementVisibleToTheUser(oTabHeaderElem[0], true, 2);
|
||||||
@@ -308,6 +319,14 @@ $(function()
|
|||||||
} else {
|
} else {
|
||||||
oMatchingExtraTabElem.removeClass(this.css_classes.is_hidden);
|
oMatchingExtraTabElem.removeClass(this.css_classes.is_hidden);
|
||||||
}
|
}
|
||||||
|
// Enable/disable the corresponding extra tab element
|
||||||
|
if (bIsDisabled) {
|
||||||
|
oMatchingExtraTabElem.attr('aria-disabled', 'true');
|
||||||
|
oMatchingExtraTabElem.addClass(this.css_classes.is_disabled);
|
||||||
|
} else {
|
||||||
|
oMatchingExtraTabElem.attr('aria-disabled', 'false');
|
||||||
|
oMatchingExtraTabElem.removeClass(this.css_classes.is_disabled);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// - Update extra tabs list
|
// - Update extra tabs list
|
||||||
_updateExtraTabsList: function () {
|
_updateExtraTabsList: function () {
|
||||||
@@ -326,7 +345,7 @@ $(function()
|
|||||||
* @return {string} The [data-tab-id] of the iIdx-th tab (zero based). Can return undefined if it has not [data-tab-id] attribute
|
* @return {string} The [data-tab-id] of the iIdx-th tab (zero based). Can return undefined if it has not [data-tab-id] attribute
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_getTabIdFromTabIndex(iIdx) {
|
_getTabIdFromTabIndex: function(iIdx) {
|
||||||
return this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header).eq(iIdx).attr('data-tab-id');
|
return this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header).eq(iIdx).attr('data-tab-id');
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -334,10 +353,41 @@ $(function()
|
|||||||
* @return {number} The index (zero based) of the tab. If no matching tab, 0 will be returned.
|
* @return {number} The index (zero based) of the tab. If no matching tab, 0 will be returned.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_getTabIndexFromTabId(sId) {
|
_getTabIndexFromTabId: function(sId) {
|
||||||
const oTabElem = this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header+'[data-tab-id="'+sId+'"]');
|
const oTabElem = this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header+'[data-tab-id="'+sId+'"]');
|
||||||
|
|
||||||
return oTabElem.length === 0 ? 0 : oTabElem.prevAll().length;
|
return oTabElem.length === 0 ? 0 : oTabElem.prevAll().length;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param sId {string} The [data-tab-id] of the tab
|
||||||
|
* @return {Object} The jQuery object representing the tab element
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getTabElementFromTabId: function(sId) {
|
||||||
|
return this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header+'[data-tab-id="'+sId+'"]');
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param sId {string} The [data-tab-id] of the tab
|
||||||
|
* @return {Object} The jQuery object representing the tab element
|
||||||
|
*/
|
||||||
|
disableTab: function(sId){
|
||||||
|
const tabsWidget = this.GetTabsWidget();
|
||||||
|
const iIdx = this._getTabIndexFromTabId(sId);
|
||||||
|
tabsWidget.disable(iIdx);
|
||||||
|
const tabElement = this._getTabElementFromTabId(sId);
|
||||||
|
this._updateTabHeaderDisplay(tabElement);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param sId {string} The [data-tab-id] of the tab
|
||||||
|
* @return {Object} The jQuery object representing the tab element
|
||||||
|
*/
|
||||||
|
enableTab: function(sId){
|
||||||
|
const tabsWidget = this.GetTabsWidget();
|
||||||
|
const iIdx = this._getTabIndexFromTabId(sId);
|
||||||
|
tabsWidget.enable(iIdx);
|
||||||
|
const tabElement = this._getTabElementFromTabId(sId);
|
||||||
|
this._updateTabHeaderDisplay(tabElement);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ return array(
|
|||||||
'AbstractPortalUIExtension' => $baseDir . '/application/applicationextension.inc.php',
|
'AbstractPortalUIExtension' => $baseDir . '/application/applicationextension.inc.php',
|
||||||
'AbstractPreferencesExtension' => $baseDir . '/application/applicationextension.inc.php',
|
'AbstractPreferencesExtension' => $baseDir . '/application/applicationextension.inc.php',
|
||||||
'AbstractWeeklyScheduledProcess' => $baseDir . '/core/backgroundprocess.inc.php',
|
'AbstractWeeklyScheduledProcess' => $baseDir . '/core/backgroundprocess.inc.php',
|
||||||
|
'AbstractWelcomePopup' => $baseDir . '/application/applicationextension.inc.php',
|
||||||
'Action' => $baseDir . '/core/action.class.inc.php',
|
'Action' => $baseDir . '/core/action.class.inc.php',
|
||||||
'ActionChecker' => $baseDir . '/core/userrights.class.inc.php',
|
'ActionChecker' => $baseDir . '/core/userrights.class.inc.php',
|
||||||
'ActionEmail' => $baseDir . '/core/action.class.inc.php',
|
'ActionEmail' => $baseDir . '/core/action.class.inc.php',
|
||||||
@@ -142,6 +143,7 @@ return array(
|
|||||||
'CheckStopWatchThresholds' => $baseDir . '/core/ormstopwatch.class.inc.php',
|
'CheckStopWatchThresholds' => $baseDir . '/core/ormstopwatch.class.inc.php',
|
||||||
'CheckableExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php',
|
'CheckableExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php',
|
||||||
'Combodo\\iTop\\Application\\Branding' => $baseDir . '/sources/application/Branding.php',
|
'Combodo\\iTop\\Application\\Branding' => $baseDir . '/sources/application/Branding.php',
|
||||||
|
'Combodo\\iTop\\Application\\DatamodelDisplayService' => $baseDir . '/sources/application/DatamodelDisplayService.php',
|
||||||
'Combodo\\iTop\\Application\\Helper\\Session' => $baseDir . '/sources/application/Helper/Session.php',
|
'Combodo\\iTop\\Application\\Helper\\Session' => $baseDir . '/sources/application/Helper/Session.php',
|
||||||
'Combodo\\iTop\\Application\\Helper\\WebResourcesHelper' => $baseDir . '/sources/application/Helper/WebResourcesHelper.php',
|
'Combodo\\iTop\\Application\\Helper\\WebResourcesHelper' => $baseDir . '/sources/application/Helper/WebResourcesHelper.php',
|
||||||
'Combodo\\iTop\\Application\\Search\\AjaxSearchException' => $baseDir . '/sources/application/search/ajaxsearchexception.class.inc.php',
|
'Combodo\\iTop\\Application\\Search\\AjaxSearchException' => $baseDir . '/sources/application/search/ajaxsearchexception.class.inc.php',
|
||||||
@@ -296,11 +298,14 @@ return array(
|
|||||||
'Combodo\\iTop\\Application\\UI\\Links\\Indirect\\BlockObjectPickerDialog\\BlockObjectPickerDialog' => $baseDir . '/sources/application/UI/Links/Indirect/BlockObjectPickerDialog/BlockObjectPickerDialog.php',
|
'Combodo\\iTop\\Application\\UI\\Links\\Indirect\\BlockObjectPickerDialog\\BlockObjectPickerDialog' => $baseDir . '/sources/application/UI/Links/Indirect/BlockObjectPickerDialog/BlockObjectPickerDialog.php',
|
||||||
'Combodo\\iTop\\Application\\UI\\Preferences\\BlockShortcuts\\BlockShortcuts' => $baseDir . '/sources/application/UI/Preferences/BlockShortcuts/BlockShortcuts.php',
|
'Combodo\\iTop\\Application\\UI\\Preferences\\BlockShortcuts\\BlockShortcuts' => $baseDir . '/sources/application/UI/Preferences/BlockShortcuts/BlockShortcuts.php',
|
||||||
'Combodo\\iTop\\Application\\UI\\Printable\\BlockPrintHeader\\BlockPrintHeader' => $baseDir . '/sources/application/UI/Printable/BlockPrintHeader/BlockPrintHeader.php',
|
'Combodo\\iTop\\Application\\UI\\Printable\\BlockPrintHeader\\BlockPrintHeader' => $baseDir . '/sources/application/UI/Printable/BlockPrintHeader/BlockPrintHeader.php',
|
||||||
|
'Combodo\\iTop\\Application\\WelcomePopup\\DefaultWelcomePopup' => $baseDir . '/sources/Application/WelcomePopup/DefaultWelcomePopup.php',
|
||||||
|
'Combodo\\iTop\\Application\\WelcomePopup\\WelcomePopupService' => $baseDir . '/sources/Application/WelcomePopup/WelcomePopupService.php',
|
||||||
'Combodo\\iTop\\Composer\\iTopComposer' => $baseDir . '/sources/Composer/iTopComposer.php',
|
'Combodo\\iTop\\Composer\\iTopComposer' => $baseDir . '/sources/Composer/iTopComposer.php',
|
||||||
'Combodo\\iTop\\Controller\\AjaxRenderController' => $baseDir . '/sources/Controller/AjaxRenderController.php',
|
'Combodo\\iTop\\Controller\\AjaxRenderController' => $baseDir . '/sources/Controller/AjaxRenderController.php',
|
||||||
'Combodo\\iTop\\Controller\\Base\\Layout\\ActivityPanelController' => $baseDir . '/sources/Controller/Base/Layout/ActivityPanelController.php',
|
'Combodo\\iTop\\Controller\\Base\\Layout\\ActivityPanelController' => $baseDir . '/sources/Controller/Base/Layout/ActivityPanelController.php',
|
||||||
'Combodo\\iTop\\Controller\\OAuth\\OAuthLandingController' => $baseDir . '/sources/Controller/OAuth/OAuthLandingController.php',
|
'Combodo\\iTop\\Controller\\OAuth\\OAuthLandingController' => $baseDir . '/sources/Controller/OAuth/OAuthLandingController.php',
|
||||||
'Combodo\\iTop\\Controller\\PreferencesController' => $baseDir . '/sources/Controller/PreferencesController.php',
|
'Combodo\\iTop\\Controller\\PreferencesController' => $baseDir . '/sources/Controller/PreferencesController.php',
|
||||||
|
'Combodo\\iTop\\Controller\\WelcomePopupController' => $baseDir . '/sources/Controller/WelcomePopupController.php',
|
||||||
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\IOAuthClientProvider' => $baseDir . '/sources/Core/Authentication/Client/OAuth/IOAuthClientProvider.php',
|
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\IOAuthClientProvider' => $baseDir . '/sources/Core/Authentication/Client/OAuth/IOAuthClientProvider.php',
|
||||||
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAbstract' => $baseDir . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAbstract.php',
|
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAbstract' => $baseDir . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAbstract.php',
|
||||||
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAzure' => $baseDir . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAzure.php',
|
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAzure' => $baseDir . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAzure.php',
|
||||||
@@ -1398,6 +1403,7 @@ return array(
|
|||||||
'RotatingLogFileNameBuilder' => $baseDir . '/core/log.class.inc.php',
|
'RotatingLogFileNameBuilder' => $baseDir . '/core/log.class.inc.php',
|
||||||
'RowStatus' => $baseDir . '/core/bulkchange.class.inc.php',
|
'RowStatus' => $baseDir . '/core/bulkchange.class.inc.php',
|
||||||
'RowStatus_Disappeared' => $baseDir . '/core/bulkchange.class.inc.php',
|
'RowStatus_Disappeared' => $baseDir . '/core/bulkchange.class.inc.php',
|
||||||
|
'RowStatus_Error' => $baseDir . '/core/bulkchange.class.inc.php',
|
||||||
'RowStatus_Issue' => $baseDir . '/core/bulkchange.class.inc.php',
|
'RowStatus_Issue' => $baseDir . '/core/bulkchange.class.inc.php',
|
||||||
'RowStatus_Modify' => $baseDir . '/core/bulkchange.class.inc.php',
|
'RowStatus_Modify' => $baseDir . '/core/bulkchange.class.inc.php',
|
||||||
'RowStatus_NewObj' => $baseDir . '/core/bulkchange.class.inc.php',
|
'RowStatus_NewObj' => $baseDir . '/core/bulkchange.class.inc.php',
|
||||||
@@ -2973,6 +2979,7 @@ return array(
|
|||||||
'iTopStandardURLMaker' => $baseDir . '/application/applicationcontext.class.inc.php',
|
'iTopStandardURLMaker' => $baseDir . '/application/applicationcontext.class.inc.php',
|
||||||
'iTopWebPage' => $baseDir . '/sources/application/WebPage/iTopWebPage.php',
|
'iTopWebPage' => $baseDir . '/sources/application/WebPage/iTopWebPage.php',
|
||||||
'iTopWizardWebPage' => $baseDir . '/sources/application/WebPage/iTopWizardWebPage.php',
|
'iTopWizardWebPage' => $baseDir . '/sources/application/WebPage/iTopWizardWebPage.php',
|
||||||
|
'iWelcomePopup' => $baseDir . '/application/applicationextension.inc.php',
|
||||||
'iWorkingTimeComputer' => $baseDir . '/core/computing.inc.php',
|
'iWorkingTimeComputer' => $baseDir . '/core/computing.inc.php',
|
||||||
'lnkTriggerAction' => $baseDir . '/core/trigger.class.inc.php',
|
'lnkTriggerAction' => $baseDir . '/core/trigger.class.inc.php',
|
||||||
'ormCaseLog' => $baseDir . '/core/ormcaselog.class.inc.php',
|
'ormCaseLog' => $baseDir . '/core/ormcaselog.class.inc.php',
|
||||||
|
|||||||
@@ -382,6 +382,7 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239
|
|||||||
'AbstractPortalUIExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
'AbstractPortalUIExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||||
'AbstractPreferencesExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
'AbstractPreferencesExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||||
'AbstractWeeklyScheduledProcess' => __DIR__ . '/../..' . '/core/backgroundprocess.inc.php',
|
'AbstractWeeklyScheduledProcess' => __DIR__ . '/../..' . '/core/backgroundprocess.inc.php',
|
||||||
|
'AbstractWelcomePopup' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||||
'Action' => __DIR__ . '/../..' . '/core/action.class.inc.php',
|
'Action' => __DIR__ . '/../..' . '/core/action.class.inc.php',
|
||||||
'ActionChecker' => __DIR__ . '/../..' . '/core/userrights.class.inc.php',
|
'ActionChecker' => __DIR__ . '/../..' . '/core/userrights.class.inc.php',
|
||||||
'ActionEmail' => __DIR__ . '/../..' . '/core/action.class.inc.php',
|
'ActionEmail' => __DIR__ . '/../..' . '/core/action.class.inc.php',
|
||||||
@@ -510,6 +511,7 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239
|
|||||||
'CheckStopWatchThresholds' => __DIR__ . '/../..' . '/core/ormstopwatch.class.inc.php',
|
'CheckStopWatchThresholds' => __DIR__ . '/../..' . '/core/ormstopwatch.class.inc.php',
|
||||||
'CheckableExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php',
|
'CheckableExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php',
|
||||||
'Combodo\\iTop\\Application\\Branding' => __DIR__ . '/../..' . '/sources/application/Branding.php',
|
'Combodo\\iTop\\Application\\Branding' => __DIR__ . '/../..' . '/sources/application/Branding.php',
|
||||||
|
'Combodo\\iTop\\Application\\DatamodelDisplayService' => __DIR__ . '/../..' . '/sources/application/DatamodelDisplayService.php',
|
||||||
'Combodo\\iTop\\Application\\Helper\\Session' => __DIR__ . '/../..' . '/sources/application/Helper/Session.php',
|
'Combodo\\iTop\\Application\\Helper\\Session' => __DIR__ . '/../..' . '/sources/application/Helper/Session.php',
|
||||||
'Combodo\\iTop\\Application\\Helper\\WebResourcesHelper' => __DIR__ . '/../..' . '/sources/application/Helper/WebResourcesHelper.php',
|
'Combodo\\iTop\\Application\\Helper\\WebResourcesHelper' => __DIR__ . '/../..' . '/sources/application/Helper/WebResourcesHelper.php',
|
||||||
'Combodo\\iTop\\Application\\Search\\AjaxSearchException' => __DIR__ . '/../..' . '/sources/application/search/ajaxsearchexception.class.inc.php',
|
'Combodo\\iTop\\Application\\Search\\AjaxSearchException' => __DIR__ . '/../..' . '/sources/application/search/ajaxsearchexception.class.inc.php',
|
||||||
@@ -664,11 +666,14 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239
|
|||||||
'Combodo\\iTop\\Application\\UI\\Links\\Indirect\\BlockObjectPickerDialog\\BlockObjectPickerDialog' => __DIR__ . '/../..' . '/sources/application/UI/Links/Indirect/BlockObjectPickerDialog/BlockObjectPickerDialog.php',
|
'Combodo\\iTop\\Application\\UI\\Links\\Indirect\\BlockObjectPickerDialog\\BlockObjectPickerDialog' => __DIR__ . '/../..' . '/sources/application/UI/Links/Indirect/BlockObjectPickerDialog/BlockObjectPickerDialog.php',
|
||||||
'Combodo\\iTop\\Application\\UI\\Preferences\\BlockShortcuts\\BlockShortcuts' => __DIR__ . '/../..' . '/sources/application/UI/Preferences/BlockShortcuts/BlockShortcuts.php',
|
'Combodo\\iTop\\Application\\UI\\Preferences\\BlockShortcuts\\BlockShortcuts' => __DIR__ . '/../..' . '/sources/application/UI/Preferences/BlockShortcuts/BlockShortcuts.php',
|
||||||
'Combodo\\iTop\\Application\\UI\\Printable\\BlockPrintHeader\\BlockPrintHeader' => __DIR__ . '/../..' . '/sources/application/UI/Printable/BlockPrintHeader/BlockPrintHeader.php',
|
'Combodo\\iTop\\Application\\UI\\Printable\\BlockPrintHeader\\BlockPrintHeader' => __DIR__ . '/../..' . '/sources/application/UI/Printable/BlockPrintHeader/BlockPrintHeader.php',
|
||||||
|
'Combodo\\iTop\\Application\\WelcomePopup\\DefaultWelcomePopup' => __DIR__ . '/../..' . '/sources/Application/WelcomePopup/DefaultWelcomePopup.php',
|
||||||
|
'Combodo\\iTop\\Application\\WelcomePopup\\WelcomePopupService' => __DIR__ . '/../..' . '/sources/Application/WelcomePopup/WelcomePopupService.php',
|
||||||
'Combodo\\iTop\\Composer\\iTopComposer' => __DIR__ . '/../..' . '/sources/Composer/iTopComposer.php',
|
'Combodo\\iTop\\Composer\\iTopComposer' => __DIR__ . '/../..' . '/sources/Composer/iTopComposer.php',
|
||||||
'Combodo\\iTop\\Controller\\AjaxRenderController' => __DIR__ . '/../..' . '/sources/Controller/AjaxRenderController.php',
|
'Combodo\\iTop\\Controller\\AjaxRenderController' => __DIR__ . '/../..' . '/sources/Controller/AjaxRenderController.php',
|
||||||
'Combodo\\iTop\\Controller\\Base\\Layout\\ActivityPanelController' => __DIR__ . '/../..' . '/sources/Controller/Base/Layout/ActivityPanelController.php',
|
'Combodo\\iTop\\Controller\\Base\\Layout\\ActivityPanelController' => __DIR__ . '/../..' . '/sources/Controller/Base/Layout/ActivityPanelController.php',
|
||||||
'Combodo\\iTop\\Controller\\OAuth\\OAuthLandingController' => __DIR__ . '/../..' . '/sources/Controller/OAuth/OAuthLandingController.php',
|
'Combodo\\iTop\\Controller\\OAuth\\OAuthLandingController' => __DIR__ . '/../..' . '/sources/Controller/OAuth/OAuthLandingController.php',
|
||||||
'Combodo\\iTop\\Controller\\PreferencesController' => __DIR__ . '/../..' . '/sources/Controller/PreferencesController.php',
|
'Combodo\\iTop\\Controller\\PreferencesController' => __DIR__ . '/../..' . '/sources/Controller/PreferencesController.php',
|
||||||
|
'Combodo\\iTop\\Controller\\WelcomePopupController' => __DIR__ . '/../..' . '/sources/Controller/WelcomePopupController.php',
|
||||||
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\IOAuthClientProvider' => __DIR__ . '/../..' . '/sources/Core/Authentication/Client/OAuth/IOAuthClientProvider.php',
|
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\IOAuthClientProvider' => __DIR__ . '/../..' . '/sources/Core/Authentication/Client/OAuth/IOAuthClientProvider.php',
|
||||||
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAbstract' => __DIR__ . '/../..' . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAbstract.php',
|
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAbstract' => __DIR__ . '/../..' . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAbstract.php',
|
||||||
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAzure' => __DIR__ . '/../..' . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAzure.php',
|
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAzure' => __DIR__ . '/../..' . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAzure.php',
|
||||||
@@ -1766,6 +1771,7 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239
|
|||||||
'RotatingLogFileNameBuilder' => __DIR__ . '/../..' . '/core/log.class.inc.php',
|
'RotatingLogFileNameBuilder' => __DIR__ . '/../..' . '/core/log.class.inc.php',
|
||||||
'RowStatus' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
|
'RowStatus' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
|
||||||
'RowStatus_Disappeared' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
|
'RowStatus_Disappeared' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
|
||||||
|
'RowStatus_Error' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
|
||||||
'RowStatus_Issue' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
|
'RowStatus_Issue' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
|
||||||
'RowStatus_Modify' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
|
'RowStatus_Modify' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
|
||||||
'RowStatus_NewObj' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
|
'RowStatus_NewObj' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
|
||||||
@@ -3341,6 +3347,7 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239
|
|||||||
'iTopStandardURLMaker' => __DIR__ . '/../..' . '/application/applicationcontext.class.inc.php',
|
'iTopStandardURLMaker' => __DIR__ . '/../..' . '/application/applicationcontext.class.inc.php',
|
||||||
'iTopWebPage' => __DIR__ . '/../..' . '/sources/application/WebPage/iTopWebPage.php',
|
'iTopWebPage' => __DIR__ . '/../..' . '/sources/application/WebPage/iTopWebPage.php',
|
||||||
'iTopWizardWebPage' => __DIR__ . '/../..' . '/sources/application/WebPage/iTopWizardWebPage.php',
|
'iTopWizardWebPage' => __DIR__ . '/../..' . '/sources/application/WebPage/iTopWizardWebPage.php',
|
||||||
|
'iWelcomePopup' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||||
'iWorkingTimeComputer' => __DIR__ . '/../..' . '/core/computing.inc.php',
|
'iWorkingTimeComputer' => __DIR__ . '/../..' . '/core/computing.inc.php',
|
||||||
'lnkTriggerAction' => __DIR__ . '/../..' . '/core/trigger.class.inc.php',
|
'lnkTriggerAction' => __DIR__ . '/../..' . '/core/trigger.class.inc.php',
|
||||||
'ormCaseLog' => __DIR__ . '/../..' . '/core/ormcaselog.class.inc.php',
|
'ormCaseLog' => __DIR__ . '/../..' . '/core/ormcaselog.class.inc.php',
|
||||||
|
|||||||
14
pages/UI.php
14
pages/UI.php
@@ -19,6 +19,7 @@ use Combodo\iTop\Application\UI\Base\Component\Toolbar\ToolbarUIBlockFactory;
|
|||||||
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory;
|
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory;
|
||||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
|
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
|
||||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
|
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
|
||||||
|
use Combodo\iTop\Application\WelcomePopup\WelcomePopupService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a popup welcome message, once per session at maximum
|
* Displays a popup welcome message, once per session at maximum
|
||||||
@@ -28,18 +29,17 @@ use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
|
|||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
function DisplayWelcomePopup(WebPage $oP)
|
function DisplayWelcomePopup(WebPage $oP): void
|
||||||
{
|
{
|
||||||
if (!Session::IsSet('welcome'))
|
if (!Session::IsSet('welcome'))
|
||||||
{
|
{
|
||||||
// Check, only once per session, if the popup should be displayed...
|
$oWelcomePopupService = new WelcomePopupService();
|
||||||
// If the user did not already ask for hiding it forever
|
$aMessages = $oWelcomePopupService->GetMessages();
|
||||||
$bPopup = appUserPreferences::GetPref('welcome_popup', true);
|
if (count($aMessages) > 0)
|
||||||
if ($bPopup)
|
|
||||||
{
|
{
|
||||||
TwigHelper::RenderIntoPage($oP, APPROOT.'/', 'templates/pages/backoffice/welcome_popup/welcome_popup');
|
TwigHelper::RenderIntoPage($oP, APPROOT.'/', 'templates/pages/backoffice/welcome_popup/welcome_popup', ['messages' => $aMessages]);
|
||||||
Session::Set('welcome', 'ok');
|
|
||||||
}
|
}
|
||||||
|
Session::Set('welcome', 'ok'); // Try just once per session
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use Combodo\iTop\Controller\Base\Layout\ActivityPanelController;
|
|||||||
use Combodo\iTop\Controller\PreferencesController;
|
use Combodo\iTop\Controller\PreferencesController;
|
||||||
use Combodo\iTop\Renderer\Console\ConsoleBlockRenderer;
|
use Combodo\iTop\Renderer\Console\ConsoleBlockRenderer;
|
||||||
use Combodo\iTop\Renderer\Console\ConsoleFormRenderer;
|
use Combodo\iTop\Renderer\Console\ConsoleFormRenderer;
|
||||||
|
use Combodo\iTop\Controller\WelcomePopupController;
|
||||||
|
|
||||||
require_once('../approot.inc.php');
|
require_once('../approot.inc.php');
|
||||||
|
|
||||||
@@ -2690,13 +2691,31 @@ EOF
|
|||||||
$oAjaxRenderController->GetMenusCount($oPage);
|
$oAjaxRenderController->GetMenusCount($oPage);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
//--------------------------------
|
||||||
|
// WelcomePopupMenu
|
||||||
|
//--------------------------------
|
||||||
|
case 'welcome_popup_acknowledge_message':
|
||||||
|
$oPage = new JsonPage();
|
||||||
|
try {
|
||||||
|
$oController = new WelcomePopupController();
|
||||||
|
$oController->AcknowledgeMessage();
|
||||||
|
$aResult = ['success' => true];
|
||||||
|
}
|
||||||
|
catch (Exception $oException) {
|
||||||
|
$aResult = [
|
||||||
|
'success' => false,
|
||||||
|
'error_message' => $oException->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$oPage->SetData($aResult);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
$oPage->p("Invalid query.");
|
$oPage->p("Invalid query.");
|
||||||
}
|
}
|
||||||
$oKPI->ComputeAndReport('Data fetch and format');
|
$oKPI->ComputeAndReport('Data fetch and format');
|
||||||
$oPage->output();
|
$oPage->output();
|
||||||
} catch (Exception $e)
|
} catch (Exception $e) {
|
||||||
{
|
|
||||||
// note: transform to cope with XSS attacks
|
// note: transform to cope with XSS attacks
|
||||||
echo htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8');
|
echo htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8');
|
||||||
IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString());
|
IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString());
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ try {
|
|||||||
* Add a paragraph to the body of the page
|
* Add a paragraph to the body of the page
|
||||||
*
|
*
|
||||||
* @param string $s_html
|
* @param string $s_html
|
||||||
|
* @param ?string $sLinkUrl
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
@@ -362,8 +363,6 @@ try {
|
|||||||
CMDBObject::SetTrackOrigin(CMDBChangeOrigin::CSV_INTERACTIVE);
|
CMDBObject::SetTrackOrigin(CMDBChangeOrigin::CSV_INTERACTIVE);
|
||||||
$oMyChange = CMDBObject::GetCurrentChange();
|
$oMyChange = CMDBObject::GetCurrentChange();
|
||||||
}
|
}
|
||||||
CMDBObject::SetTrackOrigin('csv-interactive');
|
|
||||||
|
|
||||||
$oBulk = new BulkChange(
|
$oBulk = new BulkChange(
|
||||||
$sClassName,
|
$sClassName,
|
||||||
$aData,
|
$aData,
|
||||||
@@ -440,7 +439,6 @@ try {
|
|||||||
|
|
||||||
case 'RowStatus_NewObj':
|
case 'RowStatus_NewObj':
|
||||||
$iCreated++;
|
$iCreated++;
|
||||||
$sFinalClass = $aResRow['finalclass'];
|
|
||||||
$sStatus = '<img src="../images/added.png" title="'.Dict::S('UI:CSVReport-Icon-Created').'">';
|
$sStatus = '<img src="../images/added.png" title="'.Dict::S('UI:CSVReport-Icon-Created').'">';
|
||||||
$sCSSRowClass = 'ibo-csv-import--row-added';
|
$sCSSRowClass = 'ibo-csv-import--row-added';
|
||||||
if ($bSimulate) {
|
if ($bSimulate) {
|
||||||
@@ -456,7 +454,7 @@ try {
|
|||||||
case 'RowStatus_Issue':
|
case 'RowStatus_Issue':
|
||||||
$iErrors++;
|
$iErrors++;
|
||||||
$sMessage .= GetDivAlert($oStatus->GetDescription());
|
$sMessage .= GetDivAlert($oStatus->GetDescription());
|
||||||
$sStatus = '<img src="../images/error.png" title="'.Dict::S('UI:CSVReport-Icon-Error').'">';//translate
|
$sStatus = '<div class="ibo-csv-import--cell-error"><i class="fas fa-exclamation-triangle" title="'.Dict::S('UI:CSVReport-Icon-Error').'" /></div>';//translate
|
||||||
$sCSSMessageClass = 'ibo-csv-import--cell-error';
|
$sCSSMessageClass = 'ibo-csv-import--cell-error';
|
||||||
$sCSSRowClass = 'ibo-csv-import--row-error';
|
$sCSSRowClass = 'ibo-csv-import--row-error';
|
||||||
if (array_key_exists($iLine, $aData)) {
|
if (array_key_exists($iLine, $aData)) {
|
||||||
@@ -477,33 +475,36 @@ try {
|
|||||||
if (isset($aExternalKeysByColumn[$iNumber - 1])) {
|
if (isset($aExternalKeysByColumn[$iNumber - 1])) {
|
||||||
$sExtKeyName = $aExternalKeysByColumn[$iNumber - 1];
|
$sExtKeyName = $aExternalKeysByColumn[$iNumber - 1];
|
||||||
$oExtKeyCellStatus = $aResRow[$sExtKeyName];
|
$oExtKeyCellStatus = $aResRow[$sExtKeyName];
|
||||||
switch (get_class($oExtKeyCellStatus)) {
|
$oCellStatus = $oExtKeyCellStatus;
|
||||||
case 'CellStatus_Issue':
|
|
||||||
case 'CellStatus_SearchIssue':
|
|
||||||
case 'CellStatus_NullIssue':
|
|
||||||
case 'CellStatus_Ambiguous':
|
|
||||||
$sCellMessage .= GetDivAlert($oExtKeyCellStatus->GetDescription());
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$sHtmlValue = $oCellStatus->GetDisplayableValue();
|
$sHtmlValue = $oCellStatus->GetDisplayableValue();
|
||||||
switch (get_class($oCellStatus)) {
|
switch (get_class($oCellStatus)) {
|
||||||
case 'CellStatus_Issue':
|
case 'CellStatus_Issue':
|
||||||
|
case 'CellStatus_NullIssue':
|
||||||
$sCellMessage .= GetDivAlert($oCellStatus->GetDescription());
|
$sCellMessage .= GetDivAlert($oCellStatus->GetDescription());
|
||||||
$aTableRow[$sClassName.'/'.$sAttCode] = '<div class="ibo-csv-import--cell-error">'.Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue).$sCellMessage.'</div>';
|
$aTableRow[$sClassName.'/'.$sAttCode] = '<div class="ibo-csv-import--cell-error">'.Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue).$sCellMessage.'</div>';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'CellStatus_SearchIssue':
|
case 'CellStatus_SearchIssue':
|
||||||
$sCellMessage .= GetDivAlert($oCellStatus->GetDescription());
|
$aTableRow[$sClassName.'/'.$sAttCode] = sprintf("%s%s%s%s%s%s",
|
||||||
$aTableRow[$sClassName.'/'.$sAttCode] = '<div class="ibo-csv-import--cell-error">ERROR: '.$sHtmlValue.$sCellMessage.'</div>';
|
'<a href="',
|
||||||
|
$oCellStatus->GetSearchLinkUrl(),
|
||||||
|
'"><div class="ibo-csv-import--cell-error">',
|
||||||
|
Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue),
|
||||||
|
GetDivAlert($oCellStatus->GetDescription()),
|
||||||
|
'<i class="fas fa-search"></i></div><a/>'
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'CellStatus_Ambiguous':
|
case 'CellStatus_Ambiguous':
|
||||||
$sCellMessage .= GetDivAlert($oCellStatus->GetDescription());
|
$aTableRow[$sClassName.'/'.$sAttCode] = sprintf("%s%s%s%s%s%s",
|
||||||
$aTableRow[$sClassName.'/'.$sAttCode] = '<div class="ibo-csv-import--cell-error" >'.Dict::Format('UI:CSVReport-Object-Ambiguous', $sHtmlValue).$sCellMessage.'</div>';
|
'<a href="',
|
||||||
|
$oCellStatus->GetSearchLinkUrl(),
|
||||||
|
'"><i class="fas fa-search"/><div class="ibo-csv-import--cell-error">',
|
||||||
|
Dict::Format('UI:CSVReport-Object-Ambiguous', $sHtmlValue),
|
||||||
|
GetDivAlert($oCellStatus->GetDescription()),
|
||||||
|
'<i class="fas fa-search"></i></div><a/>'
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'CellStatus_Modify':
|
case 'CellStatus_Modify':
|
||||||
@@ -592,7 +593,7 @@ try {
|
|||||||
$oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxUnchanged));
|
$oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxUnchanged));
|
||||||
$oPage->add_ready_script("$('#show_created').on('click', function(){ToggleRows('ibo-csv-import--row-added')})");
|
$oPage->add_ready_script("$('#show_created').on('click', function(){ToggleRows('ibo-csv-import--row-added')})");
|
||||||
|
|
||||||
$oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel('<img src="../images/error.png"> '.sprintf($aDisplayFilters['errors'], $iErrors), '', "1", "show_errors", "checkbox");
|
$oCheckBoxUnchanged = InputUIBlockFactory::MakeForInputWithLabel('<i class="fas fa-exclamation-triangle" style="color:#A33; background-color: #FFF0F0;"> '.sprintf($aDisplayFilters['errors'], $iErrors).'</i></i>', '', "1", "show_errors", "checkbox");
|
||||||
$oCheckBoxUnchanged->GetInput()->SetIsChecked(true);
|
$oCheckBoxUnchanged->GetInput()->SetIsChecked(true);
|
||||||
$oCheckBoxUnchanged->SetBeforeInput(false);
|
$oCheckBoxUnchanged->SetBeforeInput(false);
|
||||||
$oCheckBoxUnchanged->GetInput()->AddCSSClass('ibo-input-checkbox');
|
$oCheckBoxUnchanged->GetInput()->AddCSSClass('ibo-input-checkbox');
|
||||||
|
|||||||
@@ -139,6 +139,9 @@ JS
|
|||||||
//
|
//
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
$bIsSiloSelectionEnabled = MetaModel::GetConfig()->Get('navigation_menu.show_organization_filter');
|
||||||
|
if ($bIsSiloSelectionEnabled)
|
||||||
|
{
|
||||||
$oFavoriteOrganizationsBlock = new Panel(Dict::S('UI:FavoriteOrganizations'), array(), 'grey', 'ibo-favorite-organizations');
|
$oFavoriteOrganizationsBlock = new Panel(Dict::S('UI:FavoriteOrganizations'), array(), 'grey', 'ibo-favorite-organizations');
|
||||||
$oFavoriteOrganizationsBlock->SetSubTitle(Dict::S('UI:FavoriteOrganizations+'));
|
$oFavoriteOrganizationsBlock->SetSubTitle(Dict::S('UI:FavoriteOrganizations+'));
|
||||||
$oFavoriteOrganizationsBlock->AddCSSClass('ibo-datatable-panel');
|
$oFavoriteOrganizationsBlock->AddCSSClass('ibo-datatable-panel');
|
||||||
@@ -187,7 +190,7 @@ JS
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
$oContentLayout->AddMainBlock($oFavoriteOrganizationsBlock);
|
$oContentLayout->AddMainBlock($oFavoriteOrganizationsBlock);
|
||||||
|
}
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Shortcuts
|
// Shortcuts
|
||||||
|
|||||||
@@ -205,11 +205,12 @@ class DBBackup
|
|||||||
*
|
*
|
||||||
* @param string $sSourceConfigFile
|
* @param string $sSourceConfigFile
|
||||||
* @param string $sTmpFolder
|
* @param string $sTmpFolder
|
||||||
|
* @param bool $bSkipSQLDumpForTesting
|
||||||
*
|
*
|
||||||
* @return array list of files to archive
|
* @return array list of files to archive
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
protected function PrepareFilesToBackup($sSourceConfigFile, $sTmpFolder)
|
protected function PrepareFilesToBackup($sSourceConfigFile, $sTmpFolder, $bSkipSQLDumpForTesting = false)
|
||||||
{
|
{
|
||||||
$aRet = array();
|
$aRet = array();
|
||||||
if (is_dir($sTmpFolder))
|
if (is_dir($sTmpFolder))
|
||||||
@@ -226,7 +227,7 @@ class DBBackup
|
|||||||
{
|
{
|
||||||
$sFile = $sTmpFolder.'/config-itop.php';
|
$sFile = $sTmpFolder.'/config-itop.php';
|
||||||
$this->LogInfo("backup: adding resource '$sSourceConfigFile'");
|
$this->LogInfo("backup: adding resource '$sSourceConfigFile'");
|
||||||
copy($sSourceConfigFile, $sFile);
|
@copy($sSourceConfigFile, $sFile); // During unattended install config file may be absent
|
||||||
$aRet[] = $sFile;
|
$aRet[] = $sFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,9 +248,41 @@ class DBBackup
|
|||||||
SetupUtils::copydir($sExtraDir, $sFile);
|
SetupUtils::copydir($sExtraDir, $sFile);
|
||||||
$aRet[] = $sFile;
|
$aRet[] = $sFile;
|
||||||
}
|
}
|
||||||
|
if (MetaModel::GetConfig() !== null) // During unattended install config file may be absent
|
||||||
|
{
|
||||||
|
$aExtraFiles = MetaModel::GetModuleSetting('itop-backup', 'extra_files', []);
|
||||||
|
foreach($aExtraFiles as $sExtraFileOrDir)
|
||||||
|
{
|
||||||
|
if(!file_exists(APPROOT.'/'.$sExtraFileOrDir)) continue; // Ignore non-existing files
|
||||||
|
|
||||||
|
$sExtraFullPath = realpath(APPROOT.'/'.$sExtraFileOrDir);
|
||||||
|
if (strncmp(APPROOT, $sExtraFullPath, strlen(APPROOT)) !== 0)
|
||||||
|
{
|
||||||
|
throw new Exception("Backup: Aborting, resource '$sExtraFileOrDir'. Considered as UNSAFE because not inside the iTop directory.");
|
||||||
|
}
|
||||||
|
if (is_dir($sExtraFullPath))
|
||||||
|
{
|
||||||
|
$sFile = $sTmpFolder.'/'.$sExtraFileOrDir;
|
||||||
|
$this->LogInfo("backup: adding directory '$sExtraFileOrDir'");
|
||||||
|
SetupUtils::copydir($sExtraFullPath, $sFile);
|
||||||
|
$aRet[] = $sFile;
|
||||||
|
}
|
||||||
|
elseif (file_exists($sExtraFullPath))
|
||||||
|
{
|
||||||
|
$sFile = $sTmpFolder.'/'.$sExtraFileOrDir;
|
||||||
|
$this->LogInfo("backup: adding file '$sExtraFileOrDir'");
|
||||||
|
@mkdir(dirname($sFile), 0755, true);
|
||||||
|
copy($sExtraFullPath, $sFile);
|
||||||
|
$aRet[] = $sFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$bSkipSQLDumpForTesting)
|
||||||
|
{
|
||||||
$sDataFile = $sTmpFolder.'/itop-dump.sql';
|
$sDataFile = $sTmpFolder.'/itop-dump.sql';
|
||||||
$this->DoBackup($sDataFile);
|
$this->DoBackup($sDataFile);
|
||||||
$aRet[] = $sDataFile;
|
$aRet[] = $sDataFile;
|
||||||
|
}
|
||||||
|
|
||||||
return $aRet;
|
return $aRet;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ use Combodo\iTop\DesignElement;
|
|||||||
|
|
||||||
require_once(APPROOT.'setup/setuputils.class.inc.php');
|
require_once(APPROOT.'setup/setuputils.class.inc.php');
|
||||||
require_once(APPROOT.'setup/modelfactory.class.inc.php');
|
require_once(APPROOT.'setup/modelfactory.class.inc.php');
|
||||||
|
require_once(APPROOT.'setup/parentmenunodecompiler.class.inc.php');
|
||||||
require_once(APPROOT.'core/moduledesign.class.inc.php');
|
require_once(APPROOT.'core/moduledesign.class.inc.php');
|
||||||
|
|
||||||
|
|
||||||
class DOMFormatException extends Exception
|
class DOMFormatException extends Exception
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@@ -327,21 +327,16 @@ class MFCompiler
|
|||||||
*/
|
*/
|
||||||
protected function DoCompile($sTempTargetDir, $sFinalTargetDir, $oP = null, $bUseSymbolicLinks = false)
|
protected function DoCompile($sTempTargetDir, $sFinalTargetDir, $oP = null, $bUseSymbolicLinks = false)
|
||||||
{
|
{
|
||||||
$aAllClasses = array(); // flat list of classes
|
$aAllClasses = []; // flat list of classes
|
||||||
$aModulesInfo = array(); // Hash array of module_name => array('version' => string, 'root_dir' => string)
|
$aModulesInfo = []; // Hash array of module_name => array('version' => string, 'root_dir' => string)
|
||||||
|
|
||||||
// Determine the target modules for the MENUS
|
// Determine the target modules for the MENUS
|
||||||
//
|
|
||||||
$aMenuNodes = array();
|
|
||||||
$aMenusByModule = array();
|
|
||||||
foreach ($this->oFactory->GetNodes('menus/menu') as $oMenuNode)
|
|
||||||
{
|
|
||||||
$sMenuId = $oMenuNode->getAttribute('id');
|
|
||||||
$aMenuNodes[$sMenuId] = $oMenuNode;
|
|
||||||
|
|
||||||
$sModuleMenu = $oMenuNode->getAttribute('_created_in');
|
/**
|
||||||
$aMenusByModule[$sModuleMenu][] = $sMenuId;
|
* @since 3.1 N°4762
|
||||||
}
|
*/
|
||||||
|
$oParentMenuNodeCompiler = new ParentMenuNodeCompiler($this);
|
||||||
|
$oParentMenuNodeCompiler->LoadXmlMenus($this->oFactory);
|
||||||
|
|
||||||
// Determine the target module (exactly one!) for USER RIGHTS
|
// Determine the target module (exactly one!) for USER RIGHTS
|
||||||
// This used to be based solely on the module which created the user_rights node first
|
// This used to be based solely on the module which created the user_rights node first
|
||||||
@@ -386,6 +381,7 @@ class MFCompiler
|
|||||||
|
|
||||||
static::SetUseSymbolicLinksFlag($bUseSymbolicLinks);
|
static::SetUseSymbolicLinksFlag($bUseSymbolicLinks);
|
||||||
|
|
||||||
|
$oParentMenuNodeCompiler->LoadModuleMenuInfo($aModules);
|
||||||
foreach ($aModules as $foo => $oModule) {
|
foreach ($aModules as $foo => $oModule) {
|
||||||
$sModuleName = $oModule->GetName();
|
$sModuleName = $oModule->GetName();
|
||||||
$sModuleVersion = $oModule->GetVersion();
|
$sModuleVersion = $oModule->GetVersion();
|
||||||
@@ -461,7 +457,7 @@ class MFCompiler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!array_key_exists($sModuleName, $aMenusByModule))
|
if (is_null($oParentMenuNodeCompiler->GetMenusByModule($sModuleName)))
|
||||||
{
|
{
|
||||||
$this->Log("Found module without menus declared: $sModuleName");
|
$this->Log("Found module without menus declared: $sModuleName");
|
||||||
}
|
}
|
||||||
@@ -481,75 +477,19 @@ class $sMenuCreationClass extends ModuleHandlerAPI
|
|||||||
global \$__comp_menus__; // ensure that the global variable is indeed global !
|
global \$__comp_menus__; // ensure that the global variable is indeed global !
|
||||||
|
|
||||||
EOF;
|
EOF;
|
||||||
// Preliminary: determine parent menus not defined within the current module
|
|
||||||
$aMenusToLoad = array();
|
$oParentMenuNodeCompiler->CompileModuleMenus($oModule, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||||
$aParentMenus = array();
|
|
||||||
foreach($aMenusByModule[$sModuleName] as $sMenuId)
|
|
||||||
{
|
|
||||||
$oMenuNode = $aMenuNodes[$sMenuId];
|
|
||||||
if ($sParent = $oMenuNode->GetChildText('parent', null))
|
|
||||||
{
|
|
||||||
$aMenusToLoad[] = $sParent;
|
|
||||||
$aParentMenus[] = $sParent;
|
|
||||||
}
|
|
||||||
// Note: the order matters: the parents must be defined BEFORE
|
|
||||||
$aMenusToLoad[] = $sMenuId;
|
|
||||||
}
|
|
||||||
$aMenusToLoad = array_unique($aMenusToLoad);
|
|
||||||
$aMenuLinesForAll = array();
|
|
||||||
$aMenuLinesForAdmins = array();
|
|
||||||
$aAdminMenus = array();
|
|
||||||
foreach($aMenusToLoad as $sMenuId)
|
|
||||||
{
|
|
||||||
$oMenuNode = $aMenuNodes[$sMenuId];
|
|
||||||
if (is_null($oMenuNode))
|
|
||||||
{
|
|
||||||
throw new Exception("Module '{$oModule->GetId()}' (location : '$sModuleRootDir') contains an unknown menuId : '$sMenuId'");
|
|
||||||
}
|
|
||||||
if ($oMenuNode->getAttribute("xsi:type") == 'MenuGroup')
|
|
||||||
{
|
|
||||||
// Note: this algorithm is wrong
|
|
||||||
// 1 - the module may appear empty in the current module, while children are defined in other modules
|
|
||||||
// 2 - check recursively that child nodes are not empty themselves
|
|
||||||
// Future algorithm:
|
|
||||||
// a- browse the modules and build the menu tree
|
|
||||||
// b- browse the tree and blacklist empty menus
|
|
||||||
// c- before compiling, discard if blacklisted
|
|
||||||
if (!in_array($oMenuNode->getAttribute("id"), $aParentMenus))
|
|
||||||
{
|
|
||||||
// Discard empty menu groups
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
$aMenuLines = $this->CompileMenu($oMenuNode, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
|
||||||
}
|
|
||||||
catch (DOMFormatException $e)
|
|
||||||
{
|
|
||||||
throw new Exception("Failed to process menu '$sMenuId', from '$sModuleRootDir': ".$e->getMessage());
|
|
||||||
}
|
|
||||||
$sParent = $oMenuNode->GetChildText('parent', null);
|
|
||||||
if (($oMenuNode->GetChildText('enable_admin_only') == '1') || isset($aAdminMenus[$sParent]))
|
|
||||||
{
|
|
||||||
$aMenuLinesForAdmins = array_merge($aMenuLinesForAdmins, $aMenuLines);
|
|
||||||
$aAdminMenus[$oMenuNode->getAttribute("id")] = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$aMenuLinesForAll = array_merge($aMenuLinesForAll, $aMenuLines);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$sIndent = "\t\t";
|
$sIndent = "\t\t";
|
||||||
foreach ($aMenuLinesForAll as $sPHPLine)
|
foreach ($oParentMenuNodeCompiler->GetMenuLinesForAll() as $sPHPLine)
|
||||||
{
|
{
|
||||||
$sCompiledCode .= $sIndent.$sPHPLine."\n";
|
$sCompiledCode .= $sIndent.$sPHPLine."\n";
|
||||||
}
|
}
|
||||||
if (count($aMenuLinesForAdmins) > 0)
|
if (count($oParentMenuNodeCompiler->GetMenuLinesForAdmins()) > 0)
|
||||||
{
|
{
|
||||||
$sCompiledCode .= $sIndent."if (UserRights::IsAdministrator())\n";
|
$sCompiledCode .= $sIndent."if (UserRights::IsAdministrator())\n";
|
||||||
$sCompiledCode .= $sIndent."{\n";
|
$sCompiledCode .= $sIndent."{\n";
|
||||||
foreach ($aMenuLinesForAdmins as $sPHPLine)
|
foreach ($oParentMenuNodeCompiler->GetMenuLinesForAdmins() as $sPHPLine)
|
||||||
{
|
{
|
||||||
$sCompiledCode .= $sIndent."\t".$sPHPLine."\n";
|
$sCompiledCode .= $sIndent."\t".$sPHPLine."\n";
|
||||||
}
|
}
|
||||||
@@ -2416,7 +2356,7 @@ CSS;
|
|||||||
* @return array
|
* @return array
|
||||||
* @throws \DOMFormatException
|
* @throws \DOMFormatException
|
||||||
*/
|
*/
|
||||||
protected function CompileMenu($oMenu, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir, $oP)
|
public function CompileMenu($oMenu, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir, $oP)
|
||||||
{
|
{
|
||||||
$this->CompileFiles($oMenu, $sTempTargetDir.'/'.$sModuleRelativeDir, $sFinalTargetDir.'/'.$sModuleRelativeDir, $sModuleRelativeDir);
|
$this->CompileFiles($oMenu, $sTempTargetDir.'/'.$sModuleRelativeDir, $sFinalTargetDir.'/'.$sModuleRelativeDir, $sModuleRelativeDir);
|
||||||
|
|
||||||
|
|||||||
287
setup/parentmenunodecompiler.class.inc.php
Normal file
287
setup/parentmenunodecompiler.class.inc.php
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 3.1 N°4762
|
||||||
|
*/
|
||||||
|
class ParentMenuNodeCompiler
|
||||||
|
{
|
||||||
|
const COMPILED = 1;
|
||||||
|
const COMPILING = 2;
|
||||||
|
|
||||||
|
public static $bUseLegacyMenuCompilation = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var MFCompiler
|
||||||
|
*/
|
||||||
|
private $oMFCompiler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* admin menus declaration lines: result of module menu compilation
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $aMenuLinesForAdmins = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* non-admin menus declaration lines: result of module menu compilation
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $aMenuLinesForAll = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* use to handle menu group compilation recurring algorithm
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $aMenuProcessStatus = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $aMenuNodes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $aMenusByModule = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $aMenusToLoadByModule = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $aParentMenusByModule = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* used by overall algo
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $aParentMenuNodes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* used by new algo
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $aParentAdminMenus = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* used by overall algo
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $aParentModuleRootDirs = [];
|
||||||
|
|
||||||
|
public function __construct(MFCompiler $oMFCompiler) {
|
||||||
|
$this->oMFCompiler = $oMFCompiler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function UseLegacyMenuCompilation(){
|
||||||
|
self::$bUseLegacyMenuCompilation = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \ModelFactory $oFactory
|
||||||
|
* Initialize menu nodes arrays
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function LoadXmlMenus(\ModelFactory $oFactory) : void {
|
||||||
|
foreach ($oFactory->GetNodes('menus/menu') as $oMenuNode) {
|
||||||
|
$sMenuId = $oMenuNode->getAttribute('id');
|
||||||
|
$this->aMenuNodes[$sMenuId] = $oMenuNode;
|
||||||
|
|
||||||
|
$sModuleMenu = $oMenuNode->getAttribute('_created_in');
|
||||||
|
$this->aMenusByModule[$sModuleMenu][] = $sMenuId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $aModules
|
||||||
|
* Initialize arrays related to parent/child menus
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function LoadModuleMenuInfo($aModules) : void
|
||||||
|
{
|
||||||
|
foreach ($aModules as $foo => $oModule) {
|
||||||
|
$sModuleRootDir = $oModule->GetRootDir();
|
||||||
|
$sModuleName = $oModule->GetName();
|
||||||
|
|
||||||
|
if (array_key_exists($sModuleName, $this->aMenusByModule)) {
|
||||||
|
$aMenusToLoad = [];
|
||||||
|
$aParentMenus = [];
|
||||||
|
|
||||||
|
foreach ($this->aMenusByModule[$sModuleName] as $sMenuId) {
|
||||||
|
$oMenuNode = $this->aMenuNodes[$sMenuId];
|
||||||
|
|
||||||
|
if (self::$bUseLegacyMenuCompilation){
|
||||||
|
if ($sParent = $oMenuNode->GetChildText('parent', null)) {
|
||||||
|
$aMenusToLoad[] = $sParent;
|
||||||
|
$aParentMenus[] = $sParent;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($oMenuNode->getAttribute("xsi:type") == 'MenuGroup') {
|
||||||
|
$this->aParentModuleRootDirs[$sMenuId] = $sModuleRootDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sParent = $oMenuNode->GetChildText('parent', null)) {
|
||||||
|
$aMenusToLoad[] = $sParent;
|
||||||
|
$aParentMenus[] = $sParent;
|
||||||
|
|
||||||
|
$this->aParentModuleRootDirs[$sParent] = $sModuleRootDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists($sMenuId, $this->aParentModuleRootDirs)){
|
||||||
|
$this->aParentMenuNodes[$sMenuId] = $oMenuNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: the order matters: the parents must be defined BEFORE
|
||||||
|
$aMenusToLoad[] = $sMenuId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->aMenusToLoadByModule[$sModuleName] = array_unique($aMenusToLoad);
|
||||||
|
$this->aParentMenusByModule[$sModuleName] = array_unique($aParentMenus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the actual "Compilation" for one module at a time
|
||||||
|
* @param \MFModule $oModule
|
||||||
|
* @param string $sTempTargetDir
|
||||||
|
* @param string $sFinalTargetDir
|
||||||
|
* @param string $sRelativeDir
|
||||||
|
* @param Page $oP
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function CompileModuleMenus(MFModule $oModule, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP = null) : void
|
||||||
|
{
|
||||||
|
$this->aMenuLinesForAdmins = [];
|
||||||
|
$this->aMenuLinesForAll = [];
|
||||||
|
$aAdminMenus = [];
|
||||||
|
|
||||||
|
$sModuleRootDir = $oModule->GetRootDir();
|
||||||
|
$sModuleName = $oModule->GetName();
|
||||||
|
|
||||||
|
$aParentMenus = $this->aParentMenusByModule[$sModuleName];
|
||||||
|
foreach($this->aMenusToLoadByModule[$sModuleName] as $sMenuId)
|
||||||
|
{
|
||||||
|
$oMenuNode = $this->aMenuNodes[$sMenuId];
|
||||||
|
if (is_null($oMenuNode))
|
||||||
|
{
|
||||||
|
throw new Exception("Module '{$oModule->GetId()}' (location : '$sModuleRootDir') contains an unknown menuId : '$sMenuId'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::$bUseLegacyMenuCompilation) {
|
||||||
|
if ($oMenuNode->getAttribute("xsi:type") == 'MenuGroup') {
|
||||||
|
// Note: this algorithm is wrong
|
||||||
|
// 1 - the module may appear empty in the current module, while children are defined in other modules
|
||||||
|
// 2 - check recursively that child nodes are not empty themselves
|
||||||
|
// Future algorithm:
|
||||||
|
// a- browse the modules and build the menu tree
|
||||||
|
// b- browse the tree and blacklist empty menus
|
||||||
|
// c- before compiling, discard if blacklisted
|
||||||
|
if (! in_array($oMenuNode->getAttribute("id"), $aParentMenus)) {
|
||||||
|
// Discard empty menu groups
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (array_key_exists($sMenuId, $this->aParentMenuNodes)) {
|
||||||
|
// compile parent menus recursively
|
||||||
|
$this->CompileParentMenuNode($sMenuId, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//both new/legacy algo: compile leaf menu
|
||||||
|
$aMenuLines = $this->oMFCompiler->CompileMenu($oMenuNode, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||||
|
}
|
||||||
|
catch (DOMFormatException $e)
|
||||||
|
{
|
||||||
|
throw new Exception("Failed to process menu '$sMenuId', from '$sModuleRootDir': ".$e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$sParent = $oMenuNode->GetChildText('parent', null);
|
||||||
|
if (($oMenuNode->GetChildText('enable_admin_only') == '1') || isset($aAdminMenus[$sParent]) || isset($this->aParentAdminMenus[$sParent]))
|
||||||
|
{
|
||||||
|
$this->aMenuLinesForAdmins = array_merge($this->aMenuLinesForAdmins, $aMenuLines);
|
||||||
|
$aAdminMenus[$oMenuNode->getAttribute("id")] = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$this->aMenuLinesForAll = array_merge($this->aMenuLinesForAll, $aMenuLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform parent menu compilation including its ancestrors (recursively)
|
||||||
|
* @param string $sMenuId
|
||||||
|
* @param string $sTempTargetDir
|
||||||
|
* @param string $sFinalTargetDir
|
||||||
|
* @param string $sRelativeDir
|
||||||
|
* @param Page $oP
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function CompileParentMenuNode(string $sMenuId, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP = null) : void
|
||||||
|
{
|
||||||
|
$oMenuNode = $this->aParentMenuNodes[$sMenuId];
|
||||||
|
$sStatus = array_key_exists($sMenuId, $this->aMenuProcessStatus) ? $this->aMenuProcessStatus[$sMenuId] : null;
|
||||||
|
if ($sStatus === self::COMPILED){
|
||||||
|
//node already processed before
|
||||||
|
return;
|
||||||
|
} else if ($sStatus === self::COMPILING){
|
||||||
|
throw new \Exception("Cyclic dependency between parent menus ($sMenuId)");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->aMenuProcessStatus[$sMenuId] = self::COMPILING;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sParent = $oMenuNode->GetChildText('parent', null);
|
||||||
|
if (! empty($sParent)){
|
||||||
|
//compile parents before (even parent of parents ... recursively)
|
||||||
|
$this->CompileParentMenuNode($sParent, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! array_key_exists($sMenuId, $this->aParentModuleRootDirs)){
|
||||||
|
throw new Exception("Failed to process parent menu '$sMenuId' that is referenced by a child but not defined");
|
||||||
|
}
|
||||||
|
$sModuleRootDir = $this->aParentModuleRootDirs[$sMenuId];
|
||||||
|
$aMenuLines = $this->oMFCompiler->CompileMenu($oMenuNode, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||||
|
} catch (DOMFormatException $e) {
|
||||||
|
throw new Exception("Failed to process menu '$sMenuId', from '$sModuleRootDir': ".$e->getMessage());
|
||||||
|
}
|
||||||
|
$sParent = $oMenuNode->GetChildText('parent', null);
|
||||||
|
if (($oMenuNode->GetChildText('enable_admin_only') == '1') || isset($this->aParentAdminMenus[$sParent])) {
|
||||||
|
$this->aMenuLinesForAdmins = array_merge($this->aMenuLinesForAdmins, $aMenuLines);
|
||||||
|
$this->aParentAdminMenus[$oMenuNode->getAttribute("id")] = true;
|
||||||
|
} else {
|
||||||
|
$this->aMenuLinesForAll = array_merge($this->aMenuLinesForAll, $aMenuLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->aMenuProcessStatus[$sMenuId] = self::COMPILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function GetMenusByModule(string $sModuleName) : ?array
|
||||||
|
{
|
||||||
|
if (array_key_exists($sModuleName, $this->aMenusByModule)) {
|
||||||
|
return $this->aMenusByModule[$sModuleName];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function GetMenuLinesForAdmins(): array {
|
||||||
|
return $this->aMenuLinesForAdmins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function GetMenuLinesForAll(): array {
|
||||||
|
return $this->aMenuLinesForAll;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
sources/Application/WelcomePopup/DefaultWelcomePopup.php
Normal file
27
sources/Application/WelcomePopup/DefaultWelcomePopup.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
namespace Combodo\iTop\Application\WelcomePopup;
|
||||||
|
|
||||||
|
use Dict;
|
||||||
|
use AbstractWelcomePopup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the "default" Welcome Popup message
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
class DefaultWelcomePopup extends AbstractWelcomePopup
|
||||||
|
{
|
||||||
|
public function GetMessages()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
// Replacement of the welcome popup message which
|
||||||
|
// was hard-coded in the pages/UI.php
|
||||||
|
'id' => '0001',
|
||||||
|
'title' => Dict::S('UI:WelcomeMenu:Title'),
|
||||||
|
'twig' => '/templates/pages/backoffice/welcome_popup/default_welcome_popup',
|
||||||
|
'importance' => \iWelcomePopup::IMPORTANCE_HIGH,
|
||||||
|
'parameters' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
234
sources/Application/WelcomePopup/WelcomePopupService.php
Normal file
234
sources/Application/WelcomePopup/WelcomePopupService.php
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
<?php
|
||||||
|
namespace Combodo\iTop\Application\WelcomePopup;
|
||||||
|
use AttributeDateTime;
|
||||||
|
use DBObjectSearch;
|
||||||
|
use DBObjectSet;
|
||||||
|
use Exception;
|
||||||
|
use IssueLog;
|
||||||
|
use LogChannels;
|
||||||
|
use MetaModel;
|
||||||
|
use UserRights;
|
||||||
|
use WelcomePopupAcknowledge;
|
||||||
|
use iWelcomePopup;
|
||||||
|
use utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handling of the messages displayed in the "Welcome Popup"
|
||||||
|
* @since 3.1.0
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class WelcomePopupService
|
||||||
|
{
|
||||||
|
private const PROVIDER_KEY_LENGTH = 128;
|
||||||
|
/**
|
||||||
|
* Array of acknowledged messages for the current user
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
static $aAcknowledgedMessage = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of "providers" of welcome popup messages
|
||||||
|
* @var iWelcomePopup[]
|
||||||
|
*/
|
||||||
|
protected $aMessagesProviders = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of messages to display in the Welcome popup dialog
|
||||||
|
* @return string[][]
|
||||||
|
*/
|
||||||
|
public function GetMessages()
|
||||||
|
{
|
||||||
|
$this->LoadProviders();
|
||||||
|
return $this->ProcessMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the messages to display from a list of iWelcomePopup instances
|
||||||
|
* The messages are ordered by importance (CRITICAL first) then by ID
|
||||||
|
* Invalid messages or acknowledged messages are removed from the list
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function ProcessMessages(): array
|
||||||
|
{
|
||||||
|
$this->LoadProviders();
|
||||||
|
$aMessages = [];
|
||||||
|
foreach($this->aMessagesProviders as $oProvider) {
|
||||||
|
$aProviderMessages = $oProvider->GetMessages();
|
||||||
|
if (count($aProviderMessages) === 0) {
|
||||||
|
IssueLog::Debug('Empty list of messages for '.get_class($oProvider), LogChannels::CONSOLE);
|
||||||
|
}
|
||||||
|
foreach($aProviderMessages as $aMessage) {
|
||||||
|
$aReasons = [];
|
||||||
|
if (!$this->IsMessageValid($aMessage, $aReasons)) {
|
||||||
|
IssueLog::Error('Invalid structure returned by '.get_class($oProvider).'::GetMessages()', LogChannels::CONSOLE, $aReasons);
|
||||||
|
continue; // Fail silently
|
||||||
|
}
|
||||||
|
$sUUID = $this->MakeStringFitIn(get_class($oProvider), static::PROVIDER_KEY_LENGTH).'::'.$aMessage['id'];
|
||||||
|
$aMessage['uuid'] = $sUUID;
|
||||||
|
$aMessages[] = $aMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Filter the acknowledged messages AFTER getting all messages
|
||||||
|
// This allows for "replacing" a message (from another provider for example)
|
||||||
|
// by automatically acknowledging it when called in GetMessages()
|
||||||
|
foreach($aMessages as $key => $aMessage) {
|
||||||
|
if ($this->IsMessageAcknowledged($aMessage['uuid'])) {
|
||||||
|
IssueLog::Debug('Ignoring already acknowledged message '.$aMessage['uuid'], LogChannels::CONSOLE);
|
||||||
|
unset($aMessages[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usort($aMessages, array(get_class($this), 'SortOnImportance'));
|
||||||
|
return $aMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for usort to compare two items based on their 'importance' field
|
||||||
|
* @param string[] $aItem1
|
||||||
|
* @param string[] $aItem2
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public static function SortOnImportance($aItem1, $aItem2): int
|
||||||
|
{
|
||||||
|
if ($aItem1['importance'] === $aItem2['importance']) {
|
||||||
|
return strcmp($aItem1['id'], $aItem2['id']);
|
||||||
|
}
|
||||||
|
return ($aItem1['importance'] < $aItem2['importance']) ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function AcknowledgeMessage(string $sMessageUUID): void
|
||||||
|
{
|
||||||
|
$this->LoadProviders();
|
||||||
|
$oAcknowledge = MetaModel::NewObject(WelcomePopupAcknowledge::class, [
|
||||||
|
'message_uuid' => $sMessageUUID,
|
||||||
|
'acknowledge_date' => date(AttributeDateTime::GetSQLFormat()),
|
||||||
|
'user_id' => UserRights::GetConnectedUserId(),
|
||||||
|
]);
|
||||||
|
try {
|
||||||
|
$oAcknowledge->DBInsert();
|
||||||
|
$oProvider = $this->GetProviderByUUID($sMessageUUID);
|
||||||
|
if (static::$aAcknowledgedMessage !== null) {
|
||||||
|
static::$aAcknowledgedMessage[] = $sMessageUUID; // Update the cache
|
||||||
|
}
|
||||||
|
// Notify the provider of the message
|
||||||
|
$sMessageId = substr($sMessageUUID, strpos($sMessageUUID, '::')+2);
|
||||||
|
if ($oProvider !== null) {
|
||||||
|
$oProvider->AcknowledgeMessage($sMessageId);
|
||||||
|
}
|
||||||
|
} catch(Exception $e) {
|
||||||
|
IssueLog::Error("Failed to acknowledge the message $sMessageUUID for user ".UserRights::GetConnectedUserId().". Reason: ".$e->getMessage(), LogChannels::CONSOLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the provider of messages, decoupled from the constructor for testability
|
||||||
|
*/
|
||||||
|
protected function LoadProviders(): void
|
||||||
|
{
|
||||||
|
if ($this->aMessagesProviders !== null) return;
|
||||||
|
|
||||||
|
$aProviders = [];
|
||||||
|
$aProviderClasses = utils::GetClassesForInterface(iWelcomePopup::class, '', array('[\\\\/]lib[\\\\/]', '[\\\\/]node_modules[\\\\/]', '[\\\\/]test[\\\\/]', '[\\\\/]tests[\\\\/]'));
|
||||||
|
foreach($aProviderClasses as $sProviderClass) {
|
||||||
|
$aProviders[] = new $sProviderClass();
|
||||||
|
}
|
||||||
|
$this->SetMessagesProviders($aProviders);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given message was acknowledged by the current user
|
||||||
|
* @param string $sMessageId
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function IsMessageAcknowledged(string $sMessageUUID): bool
|
||||||
|
{
|
||||||
|
$iUserId = UserRights::GetConnectedUserId();
|
||||||
|
if (static::$aAcknowledgedMessage === null) {
|
||||||
|
|
||||||
|
$oSearch = new DBObjectSearch(WelcomePopupAcknowledge::class);
|
||||||
|
$oSearch->AddCondition('user_id', $iUserId);
|
||||||
|
$oSet = new DBObjectSet($oSearch);
|
||||||
|
$aAcknowledgedMessages = $oSet->GetColumnAsArray('message_uuid');
|
||||||
|
$this->SetAcknowledgedMessagesCache($aAcknowledgedMessages);
|
||||||
|
}
|
||||||
|
return in_array($sMessageUUID, static::$aAcknowledgedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the cache of acknowledged messages (useful for testing)
|
||||||
|
* @param array $aAcknowledgedMessages
|
||||||
|
*/
|
||||||
|
protected function SetAcknowledgedMessagesCache(array $aAcknowledgedMessages): void
|
||||||
|
{
|
||||||
|
static::$aAcknowledgedMessage = $aAcknowledgedMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the cache of welcome popup message providers (useful for testing)
|
||||||
|
* @param iWelcomePopup[] $aMessagesProviders
|
||||||
|
*/
|
||||||
|
protected function SetMessagesProviders(array $aMessagesProviders): void
|
||||||
|
{
|
||||||
|
$this->aMessagesProviders = $aMessagesProviders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the provider associated with a message
|
||||||
|
* @param string $sMessageUUID
|
||||||
|
* @return iWelcomePopup|NULL
|
||||||
|
*/
|
||||||
|
protected function GetProviderByUUID(string $sMessageUUID): ?iWelcomePopup
|
||||||
|
{
|
||||||
|
$this->LoadProviders();
|
||||||
|
$sProviderKey = substr($sMessageUUID, 0, strpos($sMessageUUID, '::'));
|
||||||
|
foreach($this->aMessagesProviders as $oProvider) {
|
||||||
|
if ($this->MakeStringFitIn(get_class($oProvider), static::PROVIDER_KEY_LENGTH) === $sProviderKey) {
|
||||||
|
return $oProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the structure of a given message is valid by checking
|
||||||
|
* all its mandatory elements
|
||||||
|
* @param string[] $aMessage
|
||||||
|
* @param string[] $aReasons
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function IsMessageValid($aMessage, array &$aReasons): bool
|
||||||
|
{
|
||||||
|
if (!is_array($aMessage)) {
|
||||||
|
$aReasons[] = 'GetMessage() must return an array of arrays.';
|
||||||
|
return false; // Stop checking immediately
|
||||||
|
}
|
||||||
|
$bRet = true;
|
||||||
|
foreach(['id', 'importance', 'title'] as $sKey) {
|
||||||
|
if (!array_key_exists($sKey, $aMessage)) {
|
||||||
|
$aReasons[] = "Field '$sKey' missing from the message structure.";
|
||||||
|
$bRet = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!array_key_exists('html', $aMessage) && !array_key_exists('twig', $aMessage)) {
|
||||||
|
$aReasons[] = "Message structure must contain either a field 'html' or a field 'twig'.";
|
||||||
|
$bRet = false;
|
||||||
|
}
|
||||||
|
return $bRet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorten the given string (if needed) but preserving its uniqueness
|
||||||
|
* @param string $sProviderClass
|
||||||
|
* @param int $iLengthLimit
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function MakeStringFitIn(string $sProviderClass, int $iLengthLimit): string
|
||||||
|
{
|
||||||
|
if(mb_strlen($sProviderClass) <= $iLengthLimit) {
|
||||||
|
return $sProviderClass;
|
||||||
|
}
|
||||||
|
// Truncate the string to $iLimitLength and replace the first carahcters with the MD5 of the complete string
|
||||||
|
$sMD5 = md5($sProviderClass, false);
|
||||||
|
return $sMD5.'-'.mb_substr($sProviderClass, -($iLengthLimit - strlen($sMD5) - 1)); // strlen is OK on the MD5 string, and '-' is not allowed in a class name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
24
sources/Controller/WelcomePopupController.php
Normal file
24
sources/Controller/WelcomePopupController.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
namespace Combodo\iTop\Controller;
|
||||||
|
|
||||||
|
use Combodo\iTop\Application\WelcomePopup\WelcomePopupService;
|
||||||
|
use utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple controller to acknowledge (via Ajax) welcome popup messages
|
||||||
|
* @since 3.1.0
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class WelcomePopupController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Operation: welcome_popup.acknowledge_message
|
||||||
|
*/
|
||||||
|
public function AcknowledgeMessage(): void
|
||||||
|
{
|
||||||
|
$oService = new WelcomePopupService();
|
||||||
|
$sMessageUUID = utils::ReadPostedParam('message_uuid', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
|
||||||
|
$oService->AcknowledgeMessage($sMessageUUID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ namespace Combodo\iTop\Application\UI\Base\Component\Field;
|
|||||||
|
|
||||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
|
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
|
||||||
use Combodo\iTop\Application\UI\Base\UIBlock;
|
use Combodo\iTop\Application\UI\Base\UIBlock;
|
||||||
|
use utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
@@ -45,6 +46,11 @@ class Field extends UIContentBlock
|
|||||||
protected $sValueRaw;
|
protected $sValueRaw;
|
||||||
/** @var string */
|
/** @var string */
|
||||||
protected $sLabel;
|
protected $sLabel;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
protected $sDescription = '';
|
||||||
/** @var string */
|
/** @var string */
|
||||||
protected $sValueId;
|
protected $sValueId;
|
||||||
|
|
||||||
@@ -354,4 +360,34 @@ class Field extends UIContentBlock
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public function GetDescription(): string
|
||||||
|
{
|
||||||
|
return $this->sDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $sDescription
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public function SetDescription(string $sDescription)
|
||||||
|
{
|
||||||
|
$this->sDescription = $sDescription;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @return bool
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public function HasDescription(): bool
|
||||||
|
{
|
||||||
|
return utils::IsNotNullOrEmptyString($this->GetDescription());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -105,12 +105,14 @@ class FieldUIBlockFactory extends AbstractUIBlockFactory
|
|||||||
* @api
|
* @api
|
||||||
* @param string $sLabel
|
* @param string $sLabel
|
||||||
* @param string $sValueHtml
|
* @param string $sValueHtml
|
||||||
|
* @param string $sDescription
|
||||||
*
|
*
|
||||||
* @return \Combodo\iTop\Application\UI\Base\Component\Field\Field
|
* @return \Combodo\iTop\Application\UI\Base\Component\Field\Field
|
||||||
*/
|
*/
|
||||||
public static function MakeLarge(string $sLabel, string $sValueHtml = '')
|
public static function MakeLarge(string $sLabel, string $sValueHtml = '', string $sDescription = '')
|
||||||
{
|
{
|
||||||
$oField = new Field($sLabel, new Html($sValueHtml));
|
$oField = new Field($sLabel, new Html($sValueHtml));
|
||||||
|
$oField->SetDescription($sDescription);
|
||||||
$oField->SetLayout(Field::ENUM_FIELD_LAYOUT_LARGE);
|
$oField->SetLayout(Field::ENUM_FIELD_LAYOUT_LARGE);
|
||||||
return $oField;
|
return $oField;
|
||||||
}
|
}
|
||||||
@@ -119,12 +121,14 @@ class FieldUIBlockFactory extends AbstractUIBlockFactory
|
|||||||
* @api
|
* @api
|
||||||
* @param string $sLabel
|
* @param string $sLabel
|
||||||
* @param string $sValueHtml
|
* @param string $sValueHtml
|
||||||
|
* @param string $sDescription
|
||||||
*
|
*
|
||||||
* @return \Combodo\iTop\Application\UI\Base\Component\Field\Field
|
* @return \Combodo\iTop\Application\UI\Base\Component\Field\Field
|
||||||
*/
|
*/
|
||||||
public static function MakeSmall(string $sLabel, string $sValueHtml = '')
|
public static function MakeSmall(string $sLabel, string $sValueHtml = '', string $sDescription = '')
|
||||||
{
|
{
|
||||||
$oField = new Field($sLabel, new Html($sValueHtml));
|
$oField = new Field($sLabel, new Html($sValueHtml));
|
||||||
|
$oField->SetDescription($sDescription);
|
||||||
$oField->SetLayout(Field::ENUM_FIELD_LAYOUT_SMALL);
|
$oField->SetLayout(Field::ENUM_FIELD_LAYOUT_SMALL);
|
||||||
return $oField;
|
return $oField;
|
||||||
}
|
}
|
||||||
@@ -134,12 +138,14 @@ class FieldUIBlockFactory extends AbstractUIBlockFactory
|
|||||||
* @param string $sLabel
|
* @param string $sLabel
|
||||||
* @param string $sLayout
|
* @param string $sLayout
|
||||||
* @param string|null $sId
|
* @param string|null $sId
|
||||||
|
* @param string $sDescription
|
||||||
*
|
*
|
||||||
* @return \Combodo\iTop\Application\UI\Base\Component\Field\Field
|
* @return \Combodo\iTop\Application\UI\Base\Component\Field\Field
|
||||||
*/
|
*/
|
||||||
public static function MakeStandard(string $sLabel = '', string $sLayout = Field::ENUM_FIELD_LAYOUT_SMALL, ?string $sId = null)
|
public static function MakeStandard(string $sLabel = '', string $sLayout = Field::ENUM_FIELD_LAYOUT_SMALL, ?string $sId = null, string $sDescription = '')
|
||||||
{
|
{
|
||||||
$oField = new Field($sLabel, null, $sId);
|
$oField = new Field($sLabel, null, $sId);
|
||||||
|
$oField->SetDescription($sDescription);
|
||||||
$oField->SetLayout($sLayout);
|
$oField->SetLayout($sLayout);
|
||||||
return $oField;
|
return $oField;
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,16 @@ class NewsroomMenuFactory
|
|||||||
return $oMenu;
|
return $oMenu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there is any Newsroom provider configured
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public static function HasProviders()
|
||||||
|
{
|
||||||
|
$aProviders = MetaModel::EnumPlugins('iNewsroomProvider');
|
||||||
|
return count($aProviders) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare parameters for the newsroom JS widget
|
* Prepare parameters for the newsroom JS widget
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -21,13 +21,14 @@ namespace Combodo\iTop\Application\UI\Base\Component\PopoverMenu;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenuItem\PopoverMenuItem;
|
||||||
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenuItem\PopoverMenuItemFactory;
|
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenuItem\PopoverMenuItemFactory;
|
||||||
use Dict;
|
use Dict;
|
||||||
|
use iPopupMenuExtension;
|
||||||
use JSPopupMenuItem;
|
use JSPopupMenuItem;
|
||||||
use MetaModel;
|
use MetaModel;
|
||||||
use SeparatorPopupMenuItem;
|
use SeparatorPopupMenuItem;
|
||||||
use URLPopupMenuItem;
|
use URLPopupMenuItem;
|
||||||
use iPopupMenuExtension;
|
|
||||||
use UserRights;
|
use UserRights;
|
||||||
use utils;
|
use utils;
|
||||||
|
|
||||||
@@ -56,31 +57,69 @@ class PopoverMenuFactory
|
|||||||
->SetHorizontalPosition(PopoverMenu::ENUM_HORIZONTAL_POSITION_ALIGN_OUTER_RIGHT)
|
->SetHorizontalPosition(PopoverMenu::ENUM_HORIZONTAL_POSITION_ALIGN_OUTER_RIGHT)
|
||||||
->SetVerticalPosition(PopoverMenu::ENUM_VERTICAL_POSITION_ABOVE);
|
->SetVerticalPosition(PopoverMenu::ENUM_VERTICAL_POSITION_ABOVE);
|
||||||
|
|
||||||
|
$aUserMenuItems = [];
|
||||||
|
|
||||||
// Allowed portals
|
// Allowed portals
|
||||||
$aAllowedPortalsItems = static::PrepareAllowedPortalsItemsForUserMenu();
|
$aAllowedPortalsItems = static::PrepareAllowedPortalsItemsForUserMenu();
|
||||||
if (!empty($aAllowedPortalsItems)) {
|
self::AddPopoverMenuItems($aAllowedPortalsItems, $aUserMenuItems);
|
||||||
$oMenu->AddSection('allowed_portals')
|
|
||||||
->SetItems('allowed_portals', $aAllowedPortalsItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
// User related pages
|
// User related pages
|
||||||
$oMenu->AddSection('user_related')
|
self::AddPopoverMenuItems(static::PrepareUserRelatedItemsForUserMenu(), $aUserMenuItems);
|
||||||
->SetItems('user_related', static::PrepareUserRelatedItemsForUserMenu());
|
|
||||||
|
|
||||||
// API: iPopupMenuExtension::MENU_USER_ACTIONS
|
// API: iPopupMenuExtension::MENU_USER_ACTIONS
|
||||||
$aAPIItems = static::PrepareAPIItemsForUserMenu($oMenu);
|
$aAPIItems = static::PrepareAPIItemsForUserMenu($oMenu);
|
||||||
if (count($aAPIItems) > 0) {
|
self::AddPopoverMenuItems($aAPIItems, $aUserMenuItems);
|
||||||
$oMenu->AddSection('popup_menu_extension-menu_user_actions')
|
|
||||||
->SetItems('popup_menu_extension-menu_user_actions', $aAPIItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Misc links
|
// Misc links
|
||||||
$oMenu->AddSection('misc')
|
/*$oMenu->AddSection('misc')
|
||||||
->SetItems('misc', static::PrepareMiscItemsForUserMenu());
|
->SetItems('misc', static::PrepareMiscItemsForUserMenu());*/
|
||||||
|
self::AddPopoverMenuItems(static::PrepareMiscItemsForUserMenu(), $aUserMenuItems);
|
||||||
|
|
||||||
|
self::SortPopoverMenuItems($aUserMenuItems);
|
||||||
|
|
||||||
|
$oMenu->AddSection('misc')
|
||||||
|
->AddItems('misc', $aUserMenuItems);
|
||||||
return $oMenu;
|
return $oMenu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param PopoverMenuItem[] $aPopoverMenuItem
|
||||||
|
* @param PopoverMenuItem[] $aUserMenuItems
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function AddPopoverMenuItems(array $aPopoverMenuItem, array &$aUserMenuItems) : void {
|
||||||
|
foreach ($aPopoverMenuItem as $oPopoverMenuItem){
|
||||||
|
$aUserMenuItems[$oPopoverMenuItem->GetUID()] = $oPopoverMenuItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param PopoverMenuItem[] $aPopoverMenuItem
|
||||||
|
* @param PopoverMenuItem[] $aUserMenuItems
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function SortPopoverMenuItems(array &$aUserMenuItems) : void {
|
||||||
|
$aSortedMenusFromConfig = MetaModel::GetConfig()->Get('navigation_menu.sorted_popup_user_menu_items');
|
||||||
|
if (!is_array($aSortedMenusFromConfig) || empty($aSortedMenusFromConfig)){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$aSortedMenus = [];
|
||||||
|
foreach ($aSortedMenusFromConfig as $sMenuUID){
|
||||||
|
if (array_key_exists($sMenuUID, $aUserMenuItems)){
|
||||||
|
$aSortedMenus[]=$aUserMenuItems[$sMenuUID];
|
||||||
|
unset($aUserMenuItems[$sMenuUID]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($aUserMenuItems as $oMenu){
|
||||||
|
$aSortedMenus[]=$oMenu;
|
||||||
|
}
|
||||||
|
$aUserMenuItems = $aSortedMenus;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the allowed portals items for the current user
|
* Return the allowed portals items for the current user
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
namespace Combodo\iTop\Application\UI\Base\Layout\NavigationMenu;
|
namespace Combodo\iTop\Application\UI\Base\Layout\NavigationMenu;
|
||||||
|
|
||||||
|
|
||||||
use ApplicationContext;
|
use ApplicationContext;
|
||||||
use ApplicationMenu;
|
use ApplicationMenu;
|
||||||
use appUserPreferences;
|
use appUserPreferences;
|
||||||
@@ -35,6 +34,7 @@ use MetaModel;
|
|||||||
use UIExtKeyWidget;
|
use UIExtKeyWidget;
|
||||||
use UserRights;
|
use UserRights;
|
||||||
use utils;
|
use utils;
|
||||||
|
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\NewsroomMenu\NewsroomMenuFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class NavigationMenu
|
* Class NavigationMenu
|
||||||
@@ -275,7 +275,7 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
|
|||||||
*/
|
*/
|
||||||
public function IsNewsroomEnabled(): bool
|
public function IsNewsroomEnabled(): bool
|
||||||
{
|
{
|
||||||
return MetaModel::GetConfig()->Get('newsroom_enabled');
|
return (MetaModel::GetConfig()->Get('newsroom_enabled') && NewsroomMenuFactory::HasProviders());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -296,6 +296,14 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return True if the silo selection is enabled, false otherwise
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public function IsSiloSelectionEnabled() : bool {
|
||||||
|
return MetaModel::GetConfig()->Get('navigation_menu.show_organization_filter');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
* @throws \CoreException
|
* @throws \CoreException
|
||||||
@@ -307,6 +315,10 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
|
|||||||
$this->bHasSiloSelected = false;
|
$this->bHasSiloSelected = false;
|
||||||
$this->sSiloLabel = null;
|
$this->sSiloLabel = null;
|
||||||
|
|
||||||
|
if (! $this->IsSiloSelectionEnabled()){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//TODO 3.0 Use components if we have the time to build select/autocomplete components before release
|
//TODO 3.0 Use components if we have the time to build select/autocomplete components before release
|
||||||
// List of visible Organizations
|
// List of visible Organizations
|
||||||
$iCount = 0;
|
$iCount = 0;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class NavigationMenuFactory
|
|||||||
{
|
{
|
||||||
|
|
||||||
$oNewsroomMenu = null;
|
$oNewsroomMenu = null;
|
||||||
if (MetaModel::GetConfig()->Get('newsroom_enabled'))
|
if (MetaModel::GetConfig()->Get('newsroom_enabled') && NewsroomMenuFactory::HasProviders())
|
||||||
{
|
{
|
||||||
$oNewsroomMenu = NewsroomMenuFactory::MakeNewsroomMenuForNavigationMenu();
|
$oNewsroomMenu = NewsroomMenuFactory::MakeNewsroomMenuForNavigationMenu();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
>
|
>
|
||||||
<div class="ibo-field--label">{{ oUIBlock.GetLabel()|raw }}
|
<div class="ibo-field--label">{{ oUIBlock.GetLabel()|raw }}
|
||||||
|
{% if oUIBlock.HasDescription() %}
|
||||||
|
<span class="ibo-has-description" data-tooltip-content="{{ oUIBlock.GetDescription() }}" data-tooltip-max-width="600px" ></span>
|
||||||
|
{% endif %}
|
||||||
{% if oUIBlock.GetLayout() == constant("Combodo\\iTop\\Application\\UI\\Base\\Component\\Field\\Field::ENUM_FIELD_LAYOUT_LARGE") %}
|
{% if oUIBlock.GetLayout() == constant("Combodo\\iTop\\Application\\UI\\Base\\Component\\Field\\Field::ENUM_FIELD_LAYOUT_LARGE") %}
|
||||||
{% if oUIBlock.GetComments() %}
|
{% if oUIBlock.GetComments() %}
|
||||||
<div class="ibo-field--comments">{{ oUIBlock.GetComments()|raw }}</div>
|
<div class="ibo-field--comments">{{ oUIBlock.GetComments()|raw }}</div>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="ibo-welcome-popup--columns">
|
||||||
|
<div class="ibo-welcome-popup--image ibo-svg-illustration--container">
|
||||||
|
{{ source("images/illustrations/undraw_relaunch_day.svg") }}
|
||||||
|
</div>
|
||||||
|
<div class="ibo-welcome-popup--text">
|
||||||
|
<div>
|
||||||
|
{{ 'UI:WelcomeMenu:Text'| dict_s|raw }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,14 +1,25 @@
|
|||||||
<div id="welcome_popup">
|
<div id="welcome_popup_dialog" class="ibo-welcome-popup--dialog ibo-is-hidden">
|
||||||
<div class="ibo-welcome-popup--image ibo-svg-illustration--container">
|
<div class="ibo-welcome-popup--content">
|
||||||
{{ source("images/illustrations/undraw_relaunch_day.svg") }}
|
{% for message in messages %}
|
||||||
|
<div class="ibo-welcome-popup--message {% if not loop.first %}ibo-is-hidden{% endif %}" data-message-uuid="{{ message.uuid }}" data_role="welcome-popup-title" data-title="{{ message.title }}">
|
||||||
|
{% if message.twig is defined %}
|
||||||
|
{{ include([message.twig ~ '.html.twig', message.twig ~ '.twig', message.twig], message.parameters ?? {}, sandboxed = true) }}
|
||||||
|
{% else %}
|
||||||
|
{{ message.html|raw }}
|
||||||
|
{% endif %}
|
||||||
|
<div class="ibo-welcome-popup--button" data-message-uuid="{{ message.uuid }}">
|
||||||
|
{% UIButton ForPrimaryAction{'sLabel':'UI:WelcomePopup:Button:Acknowledge'|dict_s, 'bIsSubmit': false } %}
|
||||||
</div>
|
</div>
|
||||||
<div class="ibo-welcome-popup--text">
|
|
||||||
<div>
|
|
||||||
{{ 'UI:WelcomeMenu:Text'| dict_s|raw }}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ibo-welcome-popup--text--options">
|
{% endfor %}
|
||||||
<input type="checkbox" checked id="display_welcome_popup"/><label for="display_welcome_popup">{{'UI:DisplayThisMessageAtStartup'| dict_s}}</label>
|
</div>
|
||||||
|
<div class="ibo-welcome-popup--indicators">
|
||||||
|
{% if messages|length > 1 %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<span class="ibo-welcome-popup--indicator {% if loop.first %}ibo-welcome-popup--active{% endif %}" data-message-uuid="{{ message.uuid }}"></span>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,14 +1,38 @@
|
|||||||
$('#welcome_popup').dialog( { width:'60%', height: 'auto', title: '{{ 'UI:WelcomeMenu:Title'|dict_s }}', autoOpen: true, modal:true,
|
$('#welcome_popup_dialog').removeClass('ibo-is-hidden');
|
||||||
close: function() {
|
$('#welcome_popup_dialog').dialog({
|
||||||
var bDisplay = $('#display_welcome_popup:checked').length;
|
modal: true,
|
||||||
SetUserPreference('welcome_popup', bDisplay, true);
|
width: '60%',
|
||||||
},
|
autoOpen: true,
|
||||||
buttons: [{
|
title: $('div[data_role=welcome-popup-title]').first().attr('data-title'),
|
||||||
text: "{{ 'UI:Button:Ok'|dict_s }}", click: function() {
|
close: function() { $('#welcome_popup_dialog').remove(); }
|
||||||
$(this).dialog( "close" ); $(this).remove();
|
|
||||||
}}],
|
|
||||||
});
|
});
|
||||||
if ($('#welcome_popup').height() > ($(window).height()-70))
|
$('.ui-widget-overlay').click(function() { $('#welcome_popup_dialog').dialog('close'); } );
|
||||||
{
|
$('.ibo-welcome-popup--indicator').click(function() {
|
||||||
$('#welcome_popup').height($(window).height()-70);
|
const id = $(this).attr('data-message-uuid');
|
||||||
|
const escaped_id = id.replace(/\\/g, '\\\\'); // All backslashes must be doubled in a jQuery selector
|
||||||
|
const new_title = $('.ibo-welcome-popup--message[data-message-uuid="'+escaped_id+'"]').attr('data-title');
|
||||||
|
$('.ibo-welcome-popup--message').addClass('ibo-is-hidden');
|
||||||
|
$('.ibo-welcome-popup--indicator').removeClass('ibo-welcome-popup--active');
|
||||||
|
$('.ibo-welcome-popup--message[data-message-uuid="'+escaped_id+'"]').removeClass('ibo-is-hidden');
|
||||||
|
$('.ibo-welcome-popup--indicator[data-message-uuid="'+escaped_id+'"]').addClass('ibo-welcome-popup--active');
|
||||||
|
$('#welcome_popup_dialog').dialog('option', 'title', new_title);
|
||||||
|
$('.ibo-welcome-popup--message[data-message-uuid="'+escaped_id+'"] button').focus();
|
||||||
|
});
|
||||||
|
$('.ibo-welcome-popup--button').click('button', function() {
|
||||||
|
const id = $(this).attr('data-message-uuid');
|
||||||
|
$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', {operation: 'welcome_popup_acknowledge_message', message_uuid: id});
|
||||||
|
const escaped_id = id.replace(/\\/g, '\\\\');; // All backslashes must be doubled in a jQuery selector
|
||||||
|
$('.ibo-welcome-popup--message[data-message-uuid="'+escaped_id+'"]').remove();
|
||||||
|
if($('.ibo-welcome-popup--message').length == 0) {
|
||||||
|
// Last message, close the dialog
|
||||||
|
$('#welcome_popup_dialog').dialog('close');
|
||||||
|
} else {
|
||||||
|
// Move the active state to the next message
|
||||||
|
$('.ibo-welcome-popup--indicator[data-message-uuid="'+escaped_id+'"]').siblings().first().trigger('click');
|
||||||
|
$('.ibo-welcome-popup--indicator[data-message-uuid="'+escaped_id+'"]').remove();
|
||||||
|
if ($('.ibo-welcome-popup--indicator').length == 1) {
|
||||||
|
// Last indicator, remove it
|
||||||
|
$('.ibo-welcome-popup--indicator').remove();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
67
test/application/UI/Base/Layout/NavigationMenuTest.php
Normal file
67
test/application/UI/Base/Layout/NavigationMenuTest.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace UI\Base\Layout;
|
||||||
|
|
||||||
|
use ApplicationContext;
|
||||||
|
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenu;
|
||||||
|
use Combodo\iTop\Application\UI\Base\Layout\NavigationMenu\NavigationMenu;
|
||||||
|
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @runTestsInSeparateProcesses
|
||||||
|
* @preserveGlobalState disabled
|
||||||
|
* @backupGlobals disabled
|
||||||
|
* Class NavigationMenuTest
|
||||||
|
*
|
||||||
|
* @package UI\Base\Layout
|
||||||
|
*/
|
||||||
|
class NavigationMenuTest extends ItopDataTestCase {
|
||||||
|
public function IsAllowedProvider(){
|
||||||
|
return [
|
||||||
|
'show menu' => [ true ],
|
||||||
|
'hide menu' => [ false ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider IsAllowedProvider
|
||||||
|
* test used to make sure backward compatibility is ensured
|
||||||
|
*/
|
||||||
|
public function testIsAllowed($bExpectedIsAllowed=true){
|
||||||
|
\MetaModel::GetConfig()->Set('navigation_menu.show_organization_filter', $bExpectedIsAllowed);
|
||||||
|
$oNavigationMenu = new NavigationMenu(
|
||||||
|
$this->createMock(ApplicationContext::class),
|
||||||
|
$this->createMock(PopoverMenu::class));
|
||||||
|
|
||||||
|
$isAllowed = $oNavigationMenu->IsSiloSelectionEnabled();
|
||||||
|
$this->assertEquals($bExpectedIsAllowed, $isAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsAllowed_BackwardCompatibility_NoVariableInConfFile(){
|
||||||
|
\MetaModel::GetConfig()->Set('navigation_menu.show_organization_filter', false);
|
||||||
|
|
||||||
|
$sTmpFilePath = tempnam(sys_get_temp_dir(), 'test_');
|
||||||
|
$oInitConfig = \MetaModel::GetConfig();
|
||||||
|
$oInitConfig->WriteToFile($sTmpFilePath);
|
||||||
|
|
||||||
|
//remove variable for the test
|
||||||
|
$aLines = file($sTmpFilePath);
|
||||||
|
|
||||||
|
$aRows = array();
|
||||||
|
|
||||||
|
foreach ($aLines as $key => $sLine) {
|
||||||
|
if (!preg_match('/navigation_menu.show_organization_filter/', $sLine)) {
|
||||||
|
$aRows[] = $sLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($sTmpFilePath, implode("\n", $aRows));
|
||||||
|
$oTempConfig = new \Config($sTmpFilePath);
|
||||||
|
|
||||||
|
$isAllowed = $oTempConfig->Get('navigation_menu.show_organization_filter');
|
||||||
|
|
||||||
|
$this->assertEquals(true, $isAllowed);
|
||||||
|
unlink($sTmpFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
test/core/AttributeDefTest.php
Normal file
0
test/core/AttributeDefTest.php
Normal file
344
test/core/BulkChangeExtKeyTest.php
Normal file
344
test/core/BulkChangeExtKeyTest.php
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Combodo\iTop\Test\UnitTest\Core;
|
||||||
|
|
||||||
|
use CMDBSource;
|
||||||
|
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||||
|
use MetaModel;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @runTestsInSeparateProcesses
|
||||||
|
* @preserveGlobalState disabled
|
||||||
|
* @backupGlobals disabled
|
||||||
|
*
|
||||||
|
* created a dedicated test for external keys imports.
|
||||||
|
*
|
||||||
|
* Class BulkChangeExtKeyTest
|
||||||
|
*
|
||||||
|
* @package Combodo\iTop\Test\UnitTest\Core
|
||||||
|
*/
|
||||||
|
class BulkChangeExtKeyTest extends ItopDataTestCase {
|
||||||
|
const CREATE_TEST_ORG = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this test may delete Person objects to cover all usecases
|
||||||
|
* DO NOT CHANGE USE_TRANSACTION value to avoid any DB loss!
|
||||||
|
*/
|
||||||
|
const USE_TRANSACTION = true;
|
||||||
|
|
||||||
|
private $sUid;
|
||||||
|
|
||||||
|
protected function setUp() : void {
|
||||||
|
parent::setUp();
|
||||||
|
require_once(APPROOT.'core/bulkchange.class.inc.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deleteAllRacks(){
|
||||||
|
$oSearch = \DBSearch::FromOQL("SELECT Rack");
|
||||||
|
$oSet = new \DBObjectSet($oSearch);
|
||||||
|
$iCount = $oSet->Count();
|
||||||
|
if ($iCount != 0){
|
||||||
|
while ($oRack = $oSet->Fetch()){
|
||||||
|
$oRack->DBDelete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider ReconciliationKeyProvider
|
||||||
|
*/
|
||||||
|
public function testExternalFieldIssueImportFail_NoObjectAtAll($bIsRackReconKey){
|
||||||
|
$this->deleteAllRacks();
|
||||||
|
|
||||||
|
$this->performBulkChangeTest(
|
||||||
|
'There are no \'Rack\' objects',
|
||||||
|
"",
|
||||||
|
null,
|
||||||
|
$bIsRackReconKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createRackObjects($aRackDict) {
|
||||||
|
foreach ($aRackDict as $iOrgId => $aRackNames) {
|
||||||
|
foreach ($aRackNames as $sRackName) {
|
||||||
|
$this->createObject('Rack', ['name' => $sRackName, 'description' => "${sRackName}Desc", 'org_id' => $iOrgId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createAnotherUserInAnotherOrg() {
|
||||||
|
$oOrg2 = $this->CreateOrganization('UnitTestOrganization2');
|
||||||
|
$oProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'Configuration Manager'), true);
|
||||||
|
|
||||||
|
$sUid = $this->GetUid();
|
||||||
|
|
||||||
|
$oUserProfile = new \URP_UserProfile();
|
||||||
|
$oUserProfile->Set('profileid', $oProfile->GetKey());
|
||||||
|
$oUserProfile->Set('reason', 'UNIT Tests');
|
||||||
|
$oSet = \DBObjectSet::FromObject($oUserProfile);
|
||||||
|
|
||||||
|
$oPerson = $this->CreatePerson('666', $oOrg2->GetKey());
|
||||||
|
$oUser = $this->createObject('UserLocal', array(
|
||||||
|
'contactid' => $oPerson->GetKey(),
|
||||||
|
'login' => $sUid,
|
||||||
|
'password' => "ABCdef$sUid@12345",
|
||||||
|
'language' => 'EN US',
|
||||||
|
'profile_list' => $oSet,
|
||||||
|
));
|
||||||
|
|
||||||
|
$oAllowedOrgList = $oUser->Get('allowed_org_list');
|
||||||
|
/** @var \URP_UserOrg $oUserOrg */
|
||||||
|
$oUserOrg = \MetaModel::NewObject('URP_UserOrg', ['allowed_org_id' => $oOrg2->GetKey(),]);
|
||||||
|
$oAllowedOrgList->AddItem($oUserOrg);
|
||||||
|
$oUser->Set('allowed_org_list', $oAllowedOrgList);
|
||||||
|
$oUser->DBWrite();
|
||||||
|
return [$oOrg2, $oUser];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ReconciliationKeyProvider(){
|
||||||
|
return [
|
||||||
|
'rack_id NOT a reconcilication key' => [ false ],
|
||||||
|
'rack_id reconcilication key' => [ true ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider ReconciliationKeyProvider
|
||||||
|
*/
|
||||||
|
public function testExternalFieldIssueImportFail_NoObjectVisibleByCurrentUser($bIsRackReconKey){
|
||||||
|
$this->deleteAllRacks();
|
||||||
|
$this->createRackObjects(
|
||||||
|
[
|
||||||
|
$this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4']
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
list($oOrg2, $oUser) = $this->createAnotherUserInAnotherOrg();
|
||||||
|
\UserRights::Login($oUser->Get('login'));
|
||||||
|
|
||||||
|
$this->performBulkChangeTest(
|
||||||
|
"There are no 'Rack' objects found with your current profile",
|
||||||
|
"",
|
||||||
|
$oOrg2,
|
||||||
|
$bIsRackReconKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider ReconciliationKeyProvider
|
||||||
|
*/
|
||||||
|
public function testExternalFieldIssueImportFail_SomeObjectVisibleByCurrentUser($bIsRackReconKey){
|
||||||
|
$this->deleteAllRacks();
|
||||||
|
list($oOrg2, $oUser) = $this->createAnotherUserInAnotherOrg();
|
||||||
|
$this->createRackObjects(
|
||||||
|
[
|
||||||
|
$this->getTestOrgId() => ['RackTest1', 'RackTest2'],
|
||||||
|
$oOrg2->GetKey() => ['RackTest3', 'RackTest4'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
\UserRights::Login($oUser->Get('login'));
|
||||||
|
|
||||||
|
$this->performBulkChangeTest(
|
||||||
|
"There are some 'Rack' objects not visible with your current profile",
|
||||||
|
"Some possible 'Rack' value(s): RackTest3, RackTest4",
|
||||||
|
$oOrg2,
|
||||||
|
$bIsRackReconKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider ReconciliationKeyProvider
|
||||||
|
*/
|
||||||
|
public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser($bIsRackReconKey){
|
||||||
|
$this->deleteAllRacks();
|
||||||
|
$this->createRackObjects(
|
||||||
|
[
|
||||||
|
$this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4']
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->performBulkChangeTest(
|
||||||
|
"No match for value 'UnexistingRack'",
|
||||||
|
"Some possible 'Rack' value(s): RackTest1, RackTest2, RackTest3...",
|
||||||
|
null,
|
||||||
|
$bIsRackReconKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider ReconciliationKeyProvider
|
||||||
|
*/
|
||||||
|
public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser_AmbigousMatch($bIsRackReconKey){
|
||||||
|
$this->deleteAllRacks();
|
||||||
|
$this->createRackObjects(
|
||||||
|
[
|
||||||
|
$this->getTestOrgId() => ['UnexistingRack', 'UnexistingRack']
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->performBulkChangeTest(
|
||||||
|
"Invalid value for attribute",
|
||||||
|
"Ambiguous: found 2 objects",
|
||||||
|
null,
|
||||||
|
$bIsRackReconKey,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
'Found 2 matches'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider ReconciliationKeyProvider
|
||||||
|
*/
|
||||||
|
public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser_FurtherExtKeyForRack($bIsRackReconKey){
|
||||||
|
$this->deleteAllRacks();
|
||||||
|
$this->createRackObjects(
|
||||||
|
[
|
||||||
|
$this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4']
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$aCsvData = [["UnexistingRackDescription"]];
|
||||||
|
$aExtKeys = ["org_id" => ["name" => 0], "rack_id" => ["name" => 1, "description" => 3]];
|
||||||
|
|
||||||
|
$sSearchLinkUrl = 'UI.php?operation=search&filter=%255B%2522SELECT%2B%2560Rack%2560%2BFROM%2BRack%2BAS%2B%2560Rack%2560%2BWHERE%2B%2528%2528%2560Rack%2560.%2560name%2560%2B%253D%2B%253Aname%2529%2BAND%2B%2528%2560Rack%2560.%2560description%2560%2B%253D%2B%253Adescription%2529%2529%2522%252C%257B%2522name%2522%253A%2522UnexistingRack%2522%252C%2522description%2522%253A%2522UnexistingRackDescription%2522%257D%252C%255B%255D%255D'
|
||||||
|
;
|
||||||
|
$this->performBulkChangeTest(
|
||||||
|
"No match for value 'UnexistingRack UnexistingRackDescription'",
|
||||||
|
"Some possible 'Rack' value(s): RackTest1 RackTest1Desc, RackTest2 RackTest2Desc, RackTest3 RackTest3Desc...",
|
||||||
|
null,
|
||||||
|
$bIsRackReconKey,
|
||||||
|
$aCsvData,
|
||||||
|
$aExtKeys,
|
||||||
|
$sSearchLinkUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function GetUid(){
|
||||||
|
if (is_null($this->sUid)){
|
||||||
|
$this->sUid = date('dmYHis');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->sUid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** *
|
||||||
|
* @param $aInitData
|
||||||
|
* @param $aCsvData
|
||||||
|
* @param $aAttributes
|
||||||
|
* @param $aExtKeys
|
||||||
|
* @param $aReconcilKeys
|
||||||
|
*/
|
||||||
|
public function performBulkChangeTest($sExpectedDisplayableValue, $sExpectedDescription, $oOrg, $bIsRackReconKey,
|
||||||
|
$aAdditionalCsvData=null, $aExtKeys=null, $sSearchLinkUrl=null, $sError="Object not found") {
|
||||||
|
if ($sSearchLinkUrl===null){
|
||||||
|
$sSearchLinkUrl = 'UI.php?operation=search&filter=%255B%2522SELECT%2B%2560Rack%2560%2BFROM%2BRack%2BAS%2B%2560Rack%2560%2BWHERE%2B%2528%2560Rack%2560.%2560name%2560%2B%253D%2B%253Aname%2529%2522%252C%257B%2522name%2522%253A%2522UnexistingRack%2522%257D%252C%255B%255D%255D';
|
||||||
|
}
|
||||||
|
if (is_null($oOrg)){
|
||||||
|
$iOrgId = $this->getTestOrgId();
|
||||||
|
$sOrgName = "UnitTestOrganization";
|
||||||
|
}else{
|
||||||
|
$iOrgId = $oOrg->GetKey();
|
||||||
|
$sOrgName = $oOrg->Get('name');
|
||||||
|
}
|
||||||
|
|
||||||
|
$sUid = $this->GetUid();
|
||||||
|
|
||||||
|
$aCsvData = [[$sOrgName, "UnexistingRack", "$sUid"]];
|
||||||
|
if ($aAdditionalCsvData !== null){
|
||||||
|
foreach ($aAdditionalCsvData as $i => $aData){
|
||||||
|
foreach ($aData as $sData){
|
||||||
|
$aCsvData[$i][] = $sData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$aAttributes = ["name" => 2];
|
||||||
|
if ($aExtKeys == null){
|
||||||
|
$aExtKeys = ["org_id" => ["name" => 0], "rack_id" => ["name" => 1]];
|
||||||
|
}
|
||||||
|
$aReconcilKeys = [ "name" ];
|
||||||
|
|
||||||
|
$aResult = [
|
||||||
|
0 => $sOrgName,
|
||||||
|
"org_id" => $iOrgId,
|
||||||
|
1 => "UnexistingRack",
|
||||||
|
2 => "\"$sUid\"",
|
||||||
|
"rack_id" => [
|
||||||
|
$sExpectedDisplayableValue,
|
||||||
|
$sExpectedDescription
|
||||||
|
],
|
||||||
|
"__STATUS__" => "Issue: Unexpected attribute value(s)",
|
||||||
|
"__ERRORS__" => $sError,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($bIsRackReconKey){
|
||||||
|
$aReconcilKeys[] = "rack_id";
|
||||||
|
$aResult[2] = $sUid;
|
||||||
|
$aResult["__STATUS__"] = "Issue: failed to reconcile";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
CMDBSource::Query('START TRANSACTION');
|
||||||
|
//change value during the test
|
||||||
|
$db_core_transactions_enabled=MetaModel::GetConfig()->Get('db_core_transactions_enabled');
|
||||||
|
MetaModel::GetConfig()->Set('db_core_transactions_enabled',false);
|
||||||
|
|
||||||
|
$this->debug("aCsvData:".json_encode($aCsvData[0]));
|
||||||
|
$this->debug("aReconcilKeys:". var_export($aReconcilKeys));
|
||||||
|
$oBulk = new \BulkChange(
|
||||||
|
"Server",
|
||||||
|
$aCsvData,
|
||||||
|
$aAttributes,
|
||||||
|
$aExtKeys,
|
||||||
|
$aReconcilKeys,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"Y-m-d H:i:s", // date format
|
||||||
|
true // localize
|
||||||
|
);
|
||||||
|
$this->debug("BulkChange:");
|
||||||
|
$oChange = \CMDBObject::GetCurrentChange();
|
||||||
|
$this->debug("GetCurrentChange:");
|
||||||
|
$aRes = $oBulk->Process($oChange);
|
||||||
|
$this->debug("Process:");
|
||||||
|
static::assertNotNull($aRes);
|
||||||
|
$this->debug("assertNotNull:");
|
||||||
|
var_dump($aRes);
|
||||||
|
foreach ($aRes as $aRow) {
|
||||||
|
if (array_key_exists('__STATUS__', $aRow)) {
|
||||||
|
$sStatus = $aRow['__STATUS__'];
|
||||||
|
$this->debug("sStatus:".$sStatus->GetDescription());
|
||||||
|
$this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription());
|
||||||
|
foreach ($aRow as $i => $oCell) {
|
||||||
|
if ($i != "finalclass" && $i != "__STATUS__" && $i != "__ERRORS__") {
|
||||||
|
$this->debug("i:".$i);
|
||||||
|
$this->debug('GetDisplayableValue:'.$oCell->GetDisplayableValue());
|
||||||
|
if (array_key_exists($i,$aResult)) {
|
||||||
|
$this->debug("aResult:".var_export($aResult[$i]));
|
||||||
|
if ($oCell instanceof \CellStatus_SearchIssue ||
|
||||||
|
$oCell instanceof \CellStatus_Ambiguous) {
|
||||||
|
$this->assertEquals($aResult[$i][0], $oCell->GetDisplayableValue(),
|
||||||
|
"failure on ".get_class($oCell).' cell type');
|
||||||
|
$this->assertEquals($sSearchLinkUrl, $oCell->GetSearchLinkUrl(),
|
||||||
|
"failure on ".get_class($oCell).' cell type');
|
||||||
|
$this->assertEquals($aResult[$i][1], $oCell->GetDescription(),
|
||||||
|
"failure on ".get_class($oCell).' cell type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ($i === "__ERRORS__") {
|
||||||
|
$sErrors = array_key_exists("__ERRORS__", $aResult) ? $aResult["__ERRORS__"] : "";
|
||||||
|
$this->assertEquals( $sErrors, $oCell->GetDescription());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assertEquals( $aResult[0], $aRow[0]->GetDisplayableValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MetaModel::GetConfig()->Set('db_core_transactions_enabled',$db_core_transactions_enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
119
test/setup/DBBackupDataTest.php
Normal file
119
test/setup/DBBackupDataTest.php
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Combodo\iTop\Test\UnitTest\Core;
|
||||||
|
|
||||||
|
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||||
|
use DBBackup;
|
||||||
|
use DBRestore;
|
||||||
|
use MetaModel;
|
||||||
|
use SetupUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @runTestsInSeparateProcesses
|
||||||
|
* @preserveGlobalState disabled
|
||||||
|
* @backupGlobals disabled
|
||||||
|
*/
|
||||||
|
class DBBackupDataTest extends ItopDataTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @dataProvider prepareFilesToBackupProvider
|
||||||
|
*/
|
||||||
|
public function testPrepareFilesToBackup(array $aExtraFiles, bool $bUnsafeFileException)
|
||||||
|
{
|
||||||
|
$sTmpDir = sys_get_temp_dir().'/testPrepareFilesToBackup-'.time();
|
||||||
|
$oBackup = new DBBackup(MetaModel::GetConfig());
|
||||||
|
MetaModel::GetConfig()->SetModuleSetting('itop-backup', 'extra_files', array_keys($aExtraFiles));
|
||||||
|
|
||||||
|
foreach($aExtraFiles as $sExtraFile => $bExists)
|
||||||
|
{
|
||||||
|
if ($bExists)
|
||||||
|
{
|
||||||
|
@mkdir(dirname(APPROOT.'/'.$sExtraFile), 0755, true);
|
||||||
|
file_put_contents(APPROOT.'/'.$sExtraFile, 'Hello World!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bUnsafeFileException)
|
||||||
|
{
|
||||||
|
$this->expectExceptionMessage("Backup: Aborting, resource '$sExtraFile'. Considered as UNSAFE because not inside the iTop directory.");
|
||||||
|
}
|
||||||
|
$aFiles = $this->InvokeNonPublicMethod('DBBackup', 'PrepareFilesToBackup', $oBackup, [APPROOT.'/conf/production/config-itop.php', $sTmpDir, true]);
|
||||||
|
SetupUtils::rrmdir($sTmpDir);
|
||||||
|
$aExpectedFiles = [
|
||||||
|
$sTmpDir.'/config-itop.php',
|
||||||
|
];
|
||||||
|
foreach($aExtraFiles as $sRelFile => $bExists)
|
||||||
|
{
|
||||||
|
if ($bExists)
|
||||||
|
{
|
||||||
|
$aExpectedFiles[] = $sTmpDir.'/'.$sRelFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort($aFiles);
|
||||||
|
sort($aExpectedFiles);
|
||||||
|
$this->assertEquals($aFiles, $aExpectedFiles);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
foreach($aExtraFiles as $sExtraFile => $bExists)
|
||||||
|
{
|
||||||
|
if ($bExists)
|
||||||
|
{
|
||||||
|
unlink(APPROOT.'/'.$sExtraFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareFilesToBackupProvider()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'no_extra_file' => ['aExtraFiles' => [], false],
|
||||||
|
'one_extra_file' => ['aExtraFiles' => ['foo.txt' => true], false],
|
||||||
|
'three_extra_file_and_dir' => ['aExtraFiles' => ['foo.txt' => true, 'gabu/zomeu.xml' => true, 'meuh.html' => true], false],
|
||||||
|
'two_extra_file_but_only_one_exists' => ['aExtraFiles' => ['foo.txt' => true, 'meuh.html' => false], false],
|
||||||
|
'one_unsafe_file' => ['aExtraFiles' => ['../foo.txt' => true], true],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider restoreListExtraFilesProvider
|
||||||
|
*/
|
||||||
|
function testRestoreListExtraFiles($aFilesToCreate, $aExpectedRelativeExtraFiles)
|
||||||
|
{
|
||||||
|
require_once(APPROOT.'/env-production/itop-backup/dbrestore.class.inc.php');
|
||||||
|
|
||||||
|
$sTmpDir = sys_get_temp_dir().'/testRestoreListExtraFiles-'.time();
|
||||||
|
|
||||||
|
foreach($aFilesToCreate as $sRelativeName)
|
||||||
|
{
|
||||||
|
$sDir = $sTmpDir.'/'.dirname($sRelativeName);
|
||||||
|
if(!is_dir($sDir))
|
||||||
|
{
|
||||||
|
mkdir($sDir, 0755, true);
|
||||||
|
}
|
||||||
|
file_put_contents($sTmpDir.'/'.$sRelativeName, 'Hello world.');
|
||||||
|
}
|
||||||
|
$aExpectedExtraFiles = [];
|
||||||
|
foreach($aExpectedRelativeExtraFiles as $sRelativeName)
|
||||||
|
{
|
||||||
|
$aExpectedExtraFiles[$sTmpDir.'/'.$sRelativeName] = APPROOT.'/'.$sRelativeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
$oRestore = new DBRestore(MetaModel::GetConfig());
|
||||||
|
$aExtraFiles = $this->InvokeNonPublicMethod('DBRestore', 'ListExtraFiles', $oRestore, [$sTmpDir]);
|
||||||
|
|
||||||
|
asort($aExtraFiles);
|
||||||
|
asort($aExpectedExtraFiles);
|
||||||
|
$this->assertEquals($aExpectedExtraFiles, $aExtraFiles);
|
||||||
|
SetupUtils::rrmdir($sTmpDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreListExtraFilesProvider()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'no extra file' => ['aFilesToCreate' => ['config-itop.php', 'itop-dump.sql', 'delta.xml'], 'aExpectedExtraFiles' => []],
|
||||||
|
'no extra file (2)' => ['aFilesToCreate' => ['config-itop.php', 'itop-dump.sql', 'delta.xml', 'production-modules/test/module.test.php'], 'aExpectedExtraFiles' => []],
|
||||||
|
'one extra file' => ['aFilesToCreate' => ['config-itop.php', 'itop-dump.sql', 'delta.xml', 'production-modules/test/module.test.php', 'collectors/ldap/conf/params.local.xml'], 'aExpectedExtraFiles' => ['collectors/ldap/conf/params.local.xml']],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
139
test/setup/MFCompilerMenuTest.php
Normal file
139
test/setup/MFCompilerMenuTest.php
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Combodo\iTop\Test\UnitTest\Setup;
|
||||||
|
|
||||||
|
use ApplicationMenu;
|
||||||
|
use Combodo\iTop\Test\UnitTest\ItopTestCase;
|
||||||
|
use Config;
|
||||||
|
use MetaModel;
|
||||||
|
use MFCompiler;
|
||||||
|
use ParentMenuNodeCompiler;
|
||||||
|
use RunTimeEnvironment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group menu_compilation
|
||||||
|
* @runTestsInSeparateProcesses
|
||||||
|
* @preserveGlobalState disabled
|
||||||
|
* @backupGlobals disabled
|
||||||
|
* @since 3.1 N°4762
|
||||||
|
* @covers \MFCompiler::DoCompile
|
||||||
|
*/
|
||||||
|
class MFCompilerMenuTest extends ItopTestCase {
|
||||||
|
private static $aPreviousEnvMenus;
|
||||||
|
private static $aPreviousEnvMenuCount;
|
||||||
|
|
||||||
|
public function setUp(): void {
|
||||||
|
parent::setUp();
|
||||||
|
require_once APPROOT . 'setup/compiler.class.inc.php';
|
||||||
|
require_once APPROOT . 'setup/modelfactory.class.inc.php';
|
||||||
|
require_once APPROOT . 'application/utils.inc.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tearDown(): void {
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function GetCurrentEnvDeltaXmlPath(string $sEnv) : string {
|
||||||
|
return APPROOT."data/$sEnv.delta.xml";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function CompileMenusProvider(){
|
||||||
|
return [
|
||||||
|
'legacy_algo' => [ 'sEnv' => 'legacy_algo', 'bLegacyMenuCompilation' => true ],
|
||||||
|
'menu_compilation_fix' => [ 'sEnv' => 'menu_compilation_fix', 'bLegacyMenuCompilation' => false ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider CompileMenusProvider
|
||||||
|
*/
|
||||||
|
public function testCompileMenus($sEnv, $bLegacyMenuCompilation){
|
||||||
|
$sConfigFilePath = \utils::GetConfigFilePath($sEnv);
|
||||||
|
|
||||||
|
//copy conf from production to phpunit context
|
||||||
|
$sDirPath = dirname($sConfigFilePath);
|
||||||
|
if (! is_dir($sDirPath)){
|
||||||
|
mkdir($sDirPath);
|
||||||
|
}
|
||||||
|
$oConfig = new Config(\utils::GetConfigFilePath());
|
||||||
|
$oConfig->WriteToFile($sConfigFilePath);
|
||||||
|
|
||||||
|
$oConfig = new Config($sConfigFilePath);
|
||||||
|
if ($bLegacyMenuCompilation){
|
||||||
|
ParentMenuNodeCompiler::UseLegacyMenuCompilation();
|
||||||
|
}
|
||||||
|
$oConfig->WriteToFile();
|
||||||
|
$oRunTimeEnvironment = new RunTimeEnvironment($sEnv);
|
||||||
|
$oRunTimeEnvironment->CompileFrom(\utils::GetCurrentEnvironment());
|
||||||
|
$oConfig->WriteToFile();
|
||||||
|
|
||||||
|
$sConfigFile = APPCONF.\utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE;
|
||||||
|
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
|
||||||
|
|
||||||
|
$aMenuGroups = ApplicationMenu::GetMenuGroups();
|
||||||
|
if (! is_null(static::$aPreviousEnvMenus)){
|
||||||
|
$this->assertEquals(static::$aPreviousEnvMenus, $aMenuGroups);
|
||||||
|
} else {
|
||||||
|
$this->assertNotEquals([], $aMenuGroups);
|
||||||
|
}
|
||||||
|
static::$aPreviousEnvMenus = $aMenuGroups;
|
||||||
|
|
||||||
|
$aMenuCount = ApplicationMenu::GetMenusCount();
|
||||||
|
|
||||||
|
if (! is_null(static::$aPreviousEnvMenuCount)){
|
||||||
|
$this->assertEquals(static::$aPreviousEnvMenuCount, $aMenuCount);
|
||||||
|
} else {
|
||||||
|
$this->assertNotEquals([], $aMenuCount);
|
||||||
|
}
|
||||||
|
static::$aPreviousEnvMenuCount = $aMenuCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function CompileMenusWithDeltaProvider(){
|
||||||
|
return [
|
||||||
|
'Menus are broken with specific delta XML using LEGACY algo' => [ 'sDeltaFile' => 'delta_broken_menus.xml', 'sEnv' => 'broken_menus', 'bLegacyMenuCompilation' => true ],
|
||||||
|
'Menus repaired using same delta XML with NEW algo' => [ 'sDeltaFile' => 'delta_broken_menus.xml', 'sEnv' => 'fixed_menus', 'bLegacyMenuCompilation' => false ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider CompileMenusWithDeltaProvider
|
||||||
|
*/
|
||||||
|
public function testCompileMenusWithDelta($sDeltaFile, $sEnv, $bLegacyMenuCompilation){
|
||||||
|
$sProvidedDeltaPath = __DIR__.'/ressources/datamodels/'.$sDeltaFile;
|
||||||
|
if (is_file($sProvidedDeltaPath)){
|
||||||
|
$sDeltaXmlPath = $this->GetCurrentEnvDeltaXmlPath($sEnv);
|
||||||
|
copy($sProvidedDeltaPath, $sDeltaXmlPath);
|
||||||
|
}
|
||||||
|
$sConfigFilePath = \utils::GetConfigFilePath($sEnv);
|
||||||
|
|
||||||
|
//copy conf from production to phpunit context
|
||||||
|
$sDirPath = dirname($sConfigFilePath);
|
||||||
|
if (! is_dir($sDirPath)){
|
||||||
|
mkdir($sDirPath);
|
||||||
|
}
|
||||||
|
$oConfig = new Config(\utils::GetConfigFilePath());
|
||||||
|
$oConfig->WriteToFile($sConfigFilePath);
|
||||||
|
|
||||||
|
$oConfig = new Config($sConfigFilePath);
|
||||||
|
if ($bLegacyMenuCompilation){
|
||||||
|
ParentMenuNodeCompiler::UseLegacyMenuCompilation();
|
||||||
|
}
|
||||||
|
$oConfig->WriteToFile();
|
||||||
|
$oRunTimeEnvironment = new RunTimeEnvironment($sEnv);
|
||||||
|
$oRunTimeEnvironment->CompileFrom(\utils::GetCurrentEnvironment());
|
||||||
|
$oConfig->WriteToFile();
|
||||||
|
|
||||||
|
if ($bLegacyMenuCompilation){
|
||||||
|
/**
|
||||||
|
* PHP Notice: Undefined index: ConfigManagement in /var/www/html/iTop/env-broken_menus/itop-structure/model.itop-structure.php on line 925
|
||||||
|
*/
|
||||||
|
error_reporting(E_ALL & ~E_NOTICE);
|
||||||
|
$this->expectErrorMessage("Call to a member function GetIndex() on null");
|
||||||
|
}
|
||||||
|
$sConfigFile = APPCONF.\utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE;
|
||||||
|
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, true /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv);
|
||||||
|
|
||||||
|
$this->assertNotEquals([], ApplicationMenu::GetMenuGroups());
|
||||||
|
$this->assertNotEquals([], ApplicationMenu::GetMenusCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
14
test/setup/ressources/datamodels/delta_broken_menus.xml
Normal file
14
test/setup/ressources/datamodels/delta_broken_menus.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0">
|
||||||
|
<menus>
|
||||||
|
<menu id="Contact" xsi:type="DashboardMenuNode" _created_in="itop-config-mgmt" _delta="must_exist">
|
||||||
|
<parent _delta="redefine">ConfigManagementOverview</parent>
|
||||||
|
</menu>
|
||||||
|
<menu id="Location" xsi:type="OQLMenuNode" _created_in="itop-config-mgmt" _delta="delete">
|
||||||
|
<parent _delta="redefine">ConfigManagementOverview</parent>
|
||||||
|
</menu>
|
||||||
|
<menu id="Document" xsi:type="OQLMenuNode" _created_in="itop-config-mgmt" _delta="must_exist">
|
||||||
|
<parent _delta="redefine">ConfigManagementOverview</parent>
|
||||||
|
</menu>
|
||||||
|
</menus>
|
||||||
|
</itop_design>
|
||||||
198
test/webservices/ImportTest.php
Normal file
198
test/webservices/ImportTest.php
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Combodo\iTop\Test\UnitTest\Webservices;
|
||||||
|
|
||||||
|
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||||
|
use MetaModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @runTestsInSeparateProcesses
|
||||||
|
* @preserveGlobalState disabled
|
||||||
|
* @backupGlobals disabled
|
||||||
|
*/
|
||||||
|
class ImportTest extends ItopDataTestCase {
|
||||||
|
const USE_TRANSACTION = false;
|
||||||
|
|
||||||
|
private $sUrl;
|
||||||
|
private $sUid;
|
||||||
|
private $sLogin;
|
||||||
|
private $sPassword = "abcDEF12345##";
|
||||||
|
private $sTmpFile = "";
|
||||||
|
private $oOrg;
|
||||||
|
|
||||||
|
protected function tearDown() : void{
|
||||||
|
parent::tearDown();
|
||||||
|
if (!empty($this->sTmpFile) && is_file($this->sTmpFile)){
|
||||||
|
unlink($this->sTmpFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setUp() : void{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->sTmpFile = tempnam(sys_get_temp_dir(), 'import_csv_');
|
||||||
|
|
||||||
|
require_once(APPROOT.'application/startup.inc.php');
|
||||||
|
$this->sUid = date('dmYHis');
|
||||||
|
$this->sLogin = "import-" .$this->sUid;
|
||||||
|
$this->oOrg = $this->CreateOrganization($this->sUid);
|
||||||
|
|
||||||
|
$sConfigFile = \utils::GetConfig()->GetLoadedFile();
|
||||||
|
@chmod($sConfigFile, 0770);
|
||||||
|
$this->sUrl = \MetaModel::GetConfig()->Get('app_root_url');
|
||||||
|
@chmod($sConfigFile, 0444); // Read-only
|
||||||
|
|
||||||
|
$oRestProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'REST Services User'), true);
|
||||||
|
$oAdminProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'Administrator'), true);
|
||||||
|
|
||||||
|
if (is_object($oRestProfile) && is_object($oAdminProfile))
|
||||||
|
{
|
||||||
|
$oUser = $this->CreateUser($this->sLogin, $oRestProfile->GetKey(), $this->sPassword);
|
||||||
|
$this->AddProfileToUser($oUser, $oAdminProfile->GetKey());
|
||||||
|
} else {
|
||||||
|
throw new \Exception("setup failed. test cannot work as usual");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ImportOkProvider(){
|
||||||
|
return [
|
||||||
|
'with reconciliation key' => [ "sReconciliationKeys" => "name,first_name,org_id->name" ],
|
||||||
|
'without reconciliation key' => [ "sReconciliationKeys" => null ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @dataProvider ImportOkProvider
|
||||||
|
*/
|
||||||
|
public function testImportOk($sReconciliationKeys){
|
||||||
|
$sFirstName = "firstname_UID";
|
||||||
|
$sLastName = "lastname_UID";
|
||||||
|
$sEmail = "email_UID@toto.fr";
|
||||||
|
|
||||||
|
$this->performImportTesting(
|
||||||
|
'"first_name","name", "email", "org_id->name"',
|
||||||
|
sprintf('"%s", "%s", "%s", UID', $sFirstName, $sLastName, $sEmail),
|
||||||
|
sprintf('ORGID;"%s";"%s";"%s"', $sFirstName, $sLastName, $sEmail),
|
||||||
|
$sReconciliationKeys,
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ImportFailProvider(){
|
||||||
|
return [
|
||||||
|
'without reconciliation key' => [
|
||||||
|
"sReconciliationKeys" => null,
|
||||||
|
"sExpectedLastLineNeedle" => 'Issue: Unexpected attribute value(s);n/a;n/a;No match for value \'gabuzomeu\'. Some possible \'Organization\' value(s): '
|
||||||
|
],
|
||||||
|
'with reconciliation key' => [
|
||||||
|
"sReconciliationKeys" => "name,first_name,org_id->name",
|
||||||
|
"sExpectedLastLineNeedle" => 'Issue: failed to reconcile;n/a;n/a;No match for value \'gabuzomeu\'. Some possible \'Organization\' value(s): '
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @dataProvider ImportFailProvider
|
||||||
|
*/
|
||||||
|
|
||||||
|
public function testImportFail_ExternalKey($sReconciliationKeys, $sExpectedLastLineNeedle){
|
||||||
|
$sFirstName = "firstname_UID";
|
||||||
|
$sLastName = "lastname_UID";
|
||||||
|
$sEmail = "email_UID@toto.fr";
|
||||||
|
|
||||||
|
$this->performImportTesting(
|
||||||
|
'"first_name","name", "email", "org_id->name"',
|
||||||
|
sprintf('"%s", "%s", "%s", gabuzomeu', $sFirstName, $sLastName, $sEmail),
|
||||||
|
$sExpectedLastLineNeedle,
|
||||||
|
$sReconciliationKeys,
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testImportFail_Enum(){
|
||||||
|
$sFirstName = "firstname_UID";
|
||||||
|
$sLastName = "lastname_UID";
|
||||||
|
$sEmail = "email_UID@toto.fr";
|
||||||
|
|
||||||
|
$this->performImportTesting(
|
||||||
|
'"first_name","name", "email", "org_id->name", status',
|
||||||
|
sprintf('"%s", "%s", "%s", UID, toto', $sFirstName, $sLastName, $sEmail),
|
||||||
|
sprintf(
|
||||||
|
'Issue: Unexpected attribute value(s);n/a;n/a;ORGID;"%s";"%s";"%s";\'toto\' is an invalid value. Unexpected value for attribute \'status\': Value not allowed [toto]', $sFirstName, $sLastName, $sEmail
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testImportFail_Date(){
|
||||||
|
$sFirstName = "firstname_UID";
|
||||||
|
$sLastName = "lastname_UID";
|
||||||
|
$sEmail = "email_UID@toto.fr";
|
||||||
|
|
||||||
|
$this->performImportTesting(
|
||||||
|
'"first_name","name", "email", "org_id->name", obsolescence_date',
|
||||||
|
sprintf('"%s", "%s", "%s", UID, toto', $sFirstName, $sLastName, $sEmail),
|
||||||
|
sprintf(
|
||||||
|
'Issue: Internal error: Exception, Wrong format for date attribute obsolescence_date, expecting "Y-m-d" and got "toto";n/a;n/a;n/a;%s;%s;%s;toto', $sFirstName, $sLastName, $sEmail
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function performImportTesting($sCsvHeaders, $sCsvFirstLineValues, $sExpectedLastLineNeedle, $sReconciliationKeys=null, $iExpectedIssue=1, $iExpectedCreated=0) {
|
||||||
|
$sContent = <<<CSVFILE
|
||||||
|
$sCsvHeaders
|
||||||
|
$sCsvFirstLineValues
|
||||||
|
CSVFILE;
|
||||||
|
file_put_contents($this->sTmpFile, str_replace("UID", $this->sUid, $sContent));
|
||||||
|
|
||||||
|
$aParams = [
|
||||||
|
'class' => 'Person',
|
||||||
|
'csvfile' => $this->sTmpFile,
|
||||||
|
'charset' => 'UTF-8',
|
||||||
|
'no_localize' => '1',
|
||||||
|
'output' => 'details',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (null != $sReconciliationKeys){
|
||||||
|
$aParams["reconciliationkeys"] = $sReconciliationKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
$aRes = \utils::ExecITopScript('webservices/import.php', $aParams, $this->sLogin, $this->sPassword);
|
||||||
|
$aOutput = $aRes[1];
|
||||||
|
$sOutput = implode("\n", $aOutput);
|
||||||
|
$sLastline = $aOutput[sizeof($aOutput) - 1];
|
||||||
|
$iRes = $aRes[0];
|
||||||
|
$this->assertEquals(0, $iRes, $sOutput);
|
||||||
|
$this->assertContains("#Issues: $iExpectedIssue", $sOutput, $sOutput);
|
||||||
|
$this->assertContains("#Warnings: 0", $sOutput, $sOutput);
|
||||||
|
$this->assertContains("#Created: $iExpectedCreated", $sOutput, $sOutput);
|
||||||
|
$this->assertContains("#Updated: 0", $sOutput, $sOutput);
|
||||||
|
var_dump($sLastline);
|
||||||
|
if ($iExpectedCreated === 1) {
|
||||||
|
$this->assertContains("created;Person", $sLastline, $sLastline);
|
||||||
|
}
|
||||||
|
|
||||||
|
$iOrgId = $this->oOrg->GetKey();
|
||||||
|
$sLastLineNeedle = $sExpectedLastLineNeedle;
|
||||||
|
foreach (["ORGID" => $iOrgId, "UID" => $this->sUid] as $sSearch => $sReplace){
|
||||||
|
$sLastLineNeedle = str_replace($sSearch, $sReplace, $sLastLineNeedle);
|
||||||
|
}
|
||||||
|
$this->assertContains($sLastLineNeedle, $sLastline, $sLastline);
|
||||||
|
|
||||||
|
$sPattern = "/Person;(\d+);/";
|
||||||
|
if (preg_match($sPattern,$sLastline,$aMatches)){
|
||||||
|
var_dump($aMatches);
|
||||||
|
$iObjId = $aMatches[1];
|
||||||
|
$oObj = MetaModel::GetObject("Person", $iObjId);
|
||||||
|
$oObj->DBDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
//date
|
||||||
|
//ext key
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,8 +27,8 @@ namespace Combodo\iTop\Test\UnitTest;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
use ArchivedObjectException;
|
use ArchivedObjectException;
|
||||||
use CMDBSource;
|
|
||||||
use CMDBObject;
|
use CMDBObject;
|
||||||
|
use CMDBSource;
|
||||||
use Contact;
|
use Contact;
|
||||||
use DBObject;
|
use DBObject;
|
||||||
use DBObjectSet;
|
use DBObjectSet;
|
||||||
@@ -70,6 +70,7 @@ define('TAG_ATTCODE', 'domains');
|
|||||||
class ItopDataTestCase extends ItopTestCase
|
class ItopDataTestCase extends ItopTestCase
|
||||||
{
|
{
|
||||||
private $iTestOrgId;
|
private $iTestOrgId;
|
||||||
|
|
||||||
// For cleanup
|
// For cleanup
|
||||||
private $aCreatedObjects = array();
|
private $aCreatedObjects = array();
|
||||||
|
|
||||||
@@ -416,10 +417,25 @@ class ItopDataTestCase extends ItopTestCase
|
|||||||
* @param string $sLogin
|
* @param string $sLogin
|
||||||
* @param int $iProfileId
|
* @param int $iProfileId
|
||||||
*
|
*
|
||||||
* @return \DBObject
|
* @return \UserLocal
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
protected function CreateUser($sLogin, $iProfileId, $sPassword=null, $iContactid=2)
|
protected function CreateUser($sLogin, $iProfileId, $sPassword=null, $iContactid=2)
|
||||||
|
{
|
||||||
|
$oUser = $this->CreateContactlessUser($sLogin, $iProfileId, $sPassword);
|
||||||
|
$oUser->Set('contactid', $iContactid);
|
||||||
|
$oUser->DBWrite();
|
||||||
|
return $oUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $sLogin
|
||||||
|
* @param int $iProfileId
|
||||||
|
*
|
||||||
|
* @return \UserLocal
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function CreateContactlessUser($sLogin, $iProfileId, $sPassword=null)
|
||||||
{
|
{
|
||||||
if (empty($sPassword)){
|
if (empty($sPassword)){
|
||||||
$sPassword = $sLogin;
|
$sPassword = $sLogin;
|
||||||
@@ -429,8 +445,8 @@ class ItopDataTestCase extends ItopTestCase
|
|||||||
$oUserProfile->Set('profileid', $iProfileId);
|
$oUserProfile->Set('profileid', $iProfileId);
|
||||||
$oUserProfile->Set('reason', 'UNIT Tests');
|
$oUserProfile->Set('reason', 'UNIT Tests');
|
||||||
$oSet = DBObjectSet::FromObject($oUserProfile);
|
$oSet = DBObjectSet::FromObject($oUserProfile);
|
||||||
|
/** @var \UserLocal $oUser */
|
||||||
$oUser = $this->createObject('UserLocal', array(
|
$oUser = $this->createObject('UserLocal', array(
|
||||||
'contactid' => $iContactid,
|
|
||||||
'login' => $sLogin,
|
'login' => $sLogin,
|
||||||
'password' => $sPassword,
|
'password' => $sPassword,
|
||||||
'language' => 'EN US',
|
'language' => 'EN US',
|
||||||
@@ -455,8 +471,8 @@ class ItopDataTestCase extends ItopTestCase
|
|||||||
$oUserProfile->Set('reason', 'UNIT Tests');
|
$oUserProfile->Set('reason', 'UNIT Tests');
|
||||||
/** @var DBObjectSet $oSet */
|
/** @var DBObjectSet $oSet */
|
||||||
$oSet = $oUser->Get('profile_list');
|
$oSet = $oUser->Get('profile_list');
|
||||||
$oSet->AddObject($oUserProfile);
|
$oSet->AddItem($oUserProfile);
|
||||||
$oUser = $this->updateObject('UserLocal', $oUser->GetKey(), array(
|
$oUser = $this->updateObject(\User::class, $oUser->GetKey(), array(
|
||||||
'profile_list' => $oSet,
|
'profile_list' => $oSet,
|
||||||
));
|
));
|
||||||
$this->debug("Updated {$oUser->GetName()} ({$oUser->GetKey()})");
|
$this->debug("Updated {$oUser->GetName()} ({$oUser->GetKey()})");
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ namespace Combodo\iTop\Test\UnitTest\Integration;
|
|||||||
|
|
||||||
use Combodo\iTop\Test\UnitTest\ItopTestCase;
|
use Combodo\iTop\Test\UnitTest\ItopTestCase;
|
||||||
|
|
||||||
|
use Dict;
|
||||||
|
|
||||||
class DictionariesConsistencyTest extends ItopTestCase
|
class DictionariesConsistencyTest extends ItopTestCase
|
||||||
{
|
{
|
||||||
@@ -148,4 +149,67 @@ class DictionariesConsistencyTest extends ItopTestCase
|
|||||||
$sMessage = "File `{$sDictFile}` syntax didn't matched expectations\nparsing results=".var_export($output, true);
|
$sMessage = "File `{$sDictFile}` syntax didn't matched expectations\nparsing results=".var_export($output, true);
|
||||||
self::assertEquals($bIsSyntaxValid, $bDictFileSyntaxOk, $sMessage);
|
self::assertEquals($bIsSyntaxValid, $bDictFileSyntaxOk, $sMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider ImBulChanportCsvMessageStillOkProvider
|
||||||
|
* make sure N°5305 dictionary changes are still here and UI remains unbroken for any lang
|
||||||
|
*/
|
||||||
|
public function testImportCsvMessageStillOk($sLangCode, $sDictFile)
|
||||||
|
{
|
||||||
|
$aFailedLabels = [];
|
||||||
|
$aLabelsToTest = [
|
||||||
|
'UI:CSVReport-Value-SetIssue' => [],
|
||||||
|
'UI:CSVReport-Value-ChangeIssue' => [ 'arg1' ],
|
||||||
|
'UI:CSVReport-Value-NoMatch' => [ 'arg1' ],
|
||||||
|
'UI:CSVReport-Value-NoMatch-PossibleValues' => [ 'arg1', 'arg2' ],
|
||||||
|
'UI:CSVReport-Value-NoMatch-NoObject' => [ 'arg1' ],
|
||||||
|
'UI:CSVReport-Value-NoMatch-NoObject-ForCurrentUser' => [ 'arg1' ],
|
||||||
|
'UI:CSVReport-Value-NoMatch-SomeObjectNotVisibleForCurrentUser' => [ 'arg1' ],
|
||||||
|
];
|
||||||
|
|
||||||
|
$sLanguageCode = strtoupper(str_replace('-', ' ', $sLangCode));
|
||||||
|
require_once(APPROOT.'env-'.\utils::GetCurrentEnvironment().'/dictionaries/languages.php');
|
||||||
|
Dict::SetUserLanguage($sLanguageCode);
|
||||||
|
foreach ($aLabelsToTest as $sLabelKey => $aLabelArgs){
|
||||||
|
try{
|
||||||
|
$sLabelValue = Dict::Format($sLabelKey, ...$aLabelArgs);
|
||||||
|
var_dump($sLabelValue);
|
||||||
|
} catch (\Exception $e){
|
||||||
|
$aFailedLabels[] = $sLabelKey;
|
||||||
|
|
||||||
|
var_dump([
|
||||||
|
'exception' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
'label_name' => $sLabelKey,
|
||||||
|
'label_args' =>$aLabelArgs,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assertEquals([], $aFailedLabels, "test fail for lang $sLangCode and labels (" . implode(",", $aFailedLabels) . ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ImportCsvMessageStillOkProvider(){
|
||||||
|
return $this->GetDictFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return a map linked to *.dict.php files that are generated after setup
|
||||||
|
* each entry key is lang code (example 'en')
|
||||||
|
* each value is an array with lang code (again) and dict file path
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function GetDictFiles() : array {
|
||||||
|
$aDictFiles = [];
|
||||||
|
|
||||||
|
foreach (glob(APPROOT.'env-'.\utils::GetCurrentEnvironment().'/dictionaries/*.dict.php') as $sDictFile){
|
||||||
|
if (preg_match('/.*\\/(.*).dict.php/', $sDictFile, $aMatches)){
|
||||||
|
$sLangCode = $aMatches[1];
|
||||||
|
$aDictFiles[$sLangCode] = [
|
||||||
|
'lang' => $sLangCode,
|
||||||
|
'file' => $sDictFile
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $aDictFiles;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -848,89 +848,6 @@ $oSearch->AddCondition_PointingTo($oOrgSearch, "org_id");
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
|
||||||
// Test bulk load API
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
class TestItopBulkLoad extends TestBizModel
|
|
||||||
{
|
|
||||||
static public function GetName()
|
|
||||||
{
|
|
||||||
return 'Itop - test BulkChange class';
|
|
||||||
}
|
|
||||||
|
|
||||||
static public function GetDescription()
|
|
||||||
{
|
|
||||||
return 'Execute a bulk change at the Core API level';
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function DoExecute()
|
|
||||||
{
|
|
||||||
$sLogin = 'testbulkload_'.time();
|
|
||||||
|
|
||||||
$oParser = new CSVParser("login,contactid->name,password,profile_list
|
|
||||||
_1_$sLogin,Picasso,secret1,profileid:10;reason:service manager|profileid->name:Problem Manager;'reason:toto;problem manager'
|
|
||||||
_2_$sLogin,Picasso,secret2,
|
|
||||||
", ',', '"');
|
|
||||||
$aData = $oParser->ToArray(1, array('_login', '_contact_name', '_password', '_profiles'));
|
|
||||||
self::DumpVariable($aData);
|
|
||||||
|
|
||||||
$oUser = new UserLocal();
|
|
||||||
$oUser->Set('login', 'patator');
|
|
||||||
$oUser->Set('password', 'patator');
|
|
||||||
//$oUser->Set('contactid', 0);
|
|
||||||
//$oUser->Set('language', $sLanguage);
|
|
||||||
|
|
||||||
$aProfiles = array(
|
|
||||||
array(
|
|
||||||
'profileid' => 10, // Service Manager
|
|
||||||
'reason' => 'service manager',
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'profileid->name' => 'Problem Manager',
|
|
||||||
'reason' => 'problem manager',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
$oBulk = new BulkChange(
|
|
||||||
'UserLocal',
|
|
||||||
$aData,
|
|
||||||
// attributes
|
|
||||||
array('login' => '_login', 'password' => '_password', 'profile_list' => '_profiles'),
|
|
||||||
// ext keys
|
|
||||||
array('contactid' => array('name' => '_contact_name')),
|
|
||||||
// reconciliation
|
|
||||||
array('login'),
|
|
||||||
// Synchro - scope
|
|
||||||
"SELECT UserLocal",
|
|
||||||
// Synchro - set attribute on missing objects
|
|
||||||
array ('password' => 'terminated', 'login' => 'terminated'.time())
|
|
||||||
);
|
|
||||||
|
|
||||||
if (false)
|
|
||||||
{
|
|
||||||
$oMyChange = MetaModel::NewObject("CMDBChange");
|
|
||||||
$oMyChange->Set("date", time());
|
|
||||||
$oMyChange->Set("userinfo", "Testor");
|
|
||||||
$iChangeId = $oMyChange->DBInsert();
|
|
||||||
// echo "Created new change: $iChangeId</br>";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "<h3>Planned for loading...</h3>";
|
|
||||||
$aRes = $oBulk->Process();
|
|
||||||
self::DumpVariable($aRes);
|
|
||||||
if (false)
|
|
||||||
{
|
|
||||||
echo "<h3>Go for loading...</h3>";
|
|
||||||
$aRes = $oBulk->Process($oMyChange);
|
|
||||||
self::DumpVariable($aRes);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// Test data load
|
// Test data load
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace UI\Base\Component\PopoverMenu;
|
||||||
|
|
||||||
|
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenuFactory;
|
||||||
|
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @runTestsInSeparateProcesses
|
||||||
|
* @preserveGlobalState disabled
|
||||||
|
* @backupGlobals disabled
|
||||||
|
*/
|
||||||
|
class PopoverMenuFactoryTest extends ItopDataTestCase {
|
||||||
|
|
||||||
|
public function MakeUserMenuForNavigationMenuProvider(){
|
||||||
|
$aNotSortedMenuUIDs = [
|
||||||
|
'portal_itop_portal',
|
||||||
|
'UI_Preferences',
|
||||||
|
'UI_Help',
|
||||||
|
'UI_AboutBox'
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'no conf' => [
|
||||||
|
'aConf' => null,
|
||||||
|
'aExpectedMenuUIDs' => $aNotSortedMenuUIDs
|
||||||
|
],
|
||||||
|
'not an array conf' => [
|
||||||
|
'aConf' => "wrong conf",
|
||||||
|
'aExpectedMenuUIDs' => $aNotSortedMenuUIDs
|
||||||
|
],
|
||||||
|
'default conf' => [
|
||||||
|
'aConf' => [],
|
||||||
|
'aExpectedMenuUIDs' => $aNotSortedMenuUIDs
|
||||||
|
],
|
||||||
|
'same order in conf' => [
|
||||||
|
'aConf' => [
|
||||||
|
'portal:itop-portal',
|
||||||
|
'UI:Preferences',
|
||||||
|
'UI:Help',
|
||||||
|
'UI:AboutBox',
|
||||||
|
],
|
||||||
|
'aExpectedMenuUIDs' => $aNotSortedMenuUIDs
|
||||||
|
],
|
||||||
|
'first menus sorted and last one missing in conf' => [
|
||||||
|
'aConf' => [
|
||||||
|
"portal:itop-portal",
|
||||||
|
"UI:Preferences",
|
||||||
|
],
|
||||||
|
'aExpectedMenuUIDs' => $aNotSortedMenuUIDs
|
||||||
|
],
|
||||||
|
'some menus but not all sorted' => [
|
||||||
|
'aConf' => [
|
||||||
|
'UI:Preferences',
|
||||||
|
'UI:AboutBox',
|
||||||
|
],
|
||||||
|
'aExpectedMenuUIDs' => [
|
||||||
|
'UI_Preferences',
|
||||||
|
'UI_AboutBox',
|
||||||
|
'portal_itop_portal',
|
||||||
|
'UI_Help',
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'all user menu sorted' => [
|
||||||
|
'aConf' => [
|
||||||
|
'UI:Preferences',
|
||||||
|
'UI:AboutBox',
|
||||||
|
'portal:itop-portal',
|
||||||
|
'UI:Help',
|
||||||
|
],
|
||||||
|
'aExpectedMenuUIDs' => [
|
||||||
|
'UI_Preferences',
|
||||||
|
'UI_AboutBox',
|
||||||
|
'portal_itop_portal',
|
||||||
|
'UI_Help',
|
||||||
|
]
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @dataProvider MakeUserMenuForNavigationMenuProvider
|
||||||
|
*/
|
||||||
|
public function testMakeUserMenuForNavigationMenu($aConf, $aExpectedMenuUIDs){
|
||||||
|
if (! is_null($aConf)){
|
||||||
|
\MetaModel::GetConfig()->Set('navigation_menu.sorted_popup_user_menu_items', $aConf);
|
||||||
|
}
|
||||||
|
|
||||||
|
$aRes = PopoverMenuFactory::MakeUserMenuForNavigationMenu()->GetSections();
|
||||||
|
$this->assertTrue(array_key_exists('misc', $aRes));
|
||||||
|
$aUIDsWithDummyRandoString = array_keys($aRes['misc']['aItems']);
|
||||||
|
//replace ibo-popover-menu--item-6464cdca5ecf4214716943--UI_AboutBox by UI_AboutBox (for ex)
|
||||||
|
$aUIDs = preg_replace('/ibo-popover-menu--item-([^\-]+)--/', '', $aUIDsWithDummyRandoString);
|
||||||
|
$this->assertEquals($aExpectedMenuUIDs, $aUIDs);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
<?php
|
||||||
|
use Combodo\iTop\Application\WelcomePopup\WelcomePopupService;
|
||||||
|
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @runTestsInSeparateProcesses
|
||||||
|
* @preserveGlobalState disabled
|
||||||
|
* @backupGlobals disabled
|
||||||
|
*/
|
||||||
|
class WelcomePopupTest extends ItopDataTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @dataProvider sortOnImportanceDataProvider
|
||||||
|
*/
|
||||||
|
public function testSortOnImportance($aToSort, $aExpected)
|
||||||
|
{
|
||||||
|
$bResult = usort($aToSort, [WelcomePopupService::class, 'SortOnImportance']);
|
||||||
|
$this->assertTrue($bResult);
|
||||||
|
$this->assertEquals($aExpected, $aToSort);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data provider for testSortOnImportance
|
||||||
|
* @return array[][]|string[][][][]|number[][][][]
|
||||||
|
*/
|
||||||
|
public function sortOnImportanceDataProvider()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'empty array' => [
|
||||||
|
'to-sort' => [],
|
||||||
|
'expected' => [],
|
||||||
|
],
|
||||||
|
'3-item array' => [
|
||||||
|
'to-sort' => [
|
||||||
|
['id' => 'aa1', 'title' => 'AA1', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
|
||||||
|
['id' => 'aa2', 'title' => 'AA2', 'importance' => 1 /*iWelcomePopup::IMPORTANCE_HIGH*/],
|
||||||
|
['id' => 'aa3', 'title' => 'AA3', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
|
||||||
|
],
|
||||||
|
'expected' => [
|
||||||
|
['id' => 'aa1', 'title' => 'AA1', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
|
||||||
|
['id' => 'aa3', 'title' => 'AA3', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
|
||||||
|
['id' => 'aa2', 'title' => 'AA2', 'importance' => 1 /*iWelcomePopup::IMPORTANCE_HIGH*/],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'5-item array' => [
|
||||||
|
'to-sort' => [
|
||||||
|
['id' => 'aa1', 'title' => 'AA1', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
|
||||||
|
['id' => 'aa2', 'title' => 'AA2', 'importance' => 1 /*iWelcomePopup::IMPORTANCE_HIGH*/],
|
||||||
|
['id' => 'aa3', 'title' => 'AA3', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
|
||||||
|
['id' => 'zz1', 'title' => 'ZZ1', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
|
||||||
|
['id' => 'zz2', 'title' => 'ZZ2', 'importance' => 1 /*iWelcomePopup::IMPORTANCE_HIGH*/],
|
||||||
|
],
|
||||||
|
'expected' => [
|
||||||
|
['id' => 'aa1', 'title' => 'AA1', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
|
||||||
|
['id' => 'aa3', 'title' => 'AA3', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
|
||||||
|
['id' => 'zz1', 'title' => 'ZZ1', 'importance' => 0 /*iWelcomePopup::IMPORTANCE_CRITICAL*/],
|
||||||
|
['id' => 'aa2', 'title' => 'AA2', 'importance' => 1 /*iWelcomePopup::IMPORTANCE_HIGH*/],
|
||||||
|
['id' => 'zz2', 'title' => 'ZZ2', 'importance' => 1 /*iWelcomePopup::IMPORTANCE_HIGH*/],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider isMessageAcknowledgedDataProvider
|
||||||
|
*/
|
||||||
|
public function testIsMessageAcknowledged($sMessageId, $aCache, $bExpected)
|
||||||
|
{
|
||||||
|
$oService = new WelcomePopupService();
|
||||||
|
$this->InvokeNonPublicMethod(WelcomePopupService::class, 'SetAcknowledgedMessagesCache', $oService, [$aCache]);
|
||||||
|
|
||||||
|
$this->assertEquals($bExpected, $this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, [$sMessageId]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isMessageAcknowledgedDataProvider()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'empty-cache' => [
|
||||||
|
'123', [], false,
|
||||||
|
],
|
||||||
|
'acknowledged' => [
|
||||||
|
'123', ['123'], true,
|
||||||
|
],
|
||||||
|
'non-acknowledged' => [
|
||||||
|
'456', ['123'], false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider isMessageValidDataProvider
|
||||||
|
*/
|
||||||
|
public function testIsMessageValid($aMessage, $bExpected)
|
||||||
|
{
|
||||||
|
$oService = new WelcomePopupService();
|
||||||
|
$aReasons = [];
|
||||||
|
$bResult = $this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageValid', $oService, [$aMessage, &$aReasons]);
|
||||||
|
if ($bResult !== $bExpected) {
|
||||||
|
print_r($aReasons);
|
||||||
|
}
|
||||||
|
$this->assertEquals($bExpected, $bResult);
|
||||||
|
if ($bResult) {
|
||||||
|
$this->assertEquals(0, count($aReasons));
|
||||||
|
} else {
|
||||||
|
$this->assertNotEquals(0, count($aReasons));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isMessageValidDataProvider()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'not an array' => [
|
||||||
|
'123', false,
|
||||||
|
],
|
||||||
|
'empty array' => [
|
||||||
|
[], false,
|
||||||
|
],
|
||||||
|
'missing id' => [
|
||||||
|
['title' => 'foo', 'importance' => 0, 'html' => '<p>Hello</p>'], false,
|
||||||
|
],
|
||||||
|
'message Ok (html)' => [
|
||||||
|
['id' => '123', 'title' => 'foo', 'importance' => 0, 'html' => '<p>Hello</p>'], true,
|
||||||
|
],
|
||||||
|
'message Ok (twig)' => [
|
||||||
|
['id' => '123', 'title' => 'foo', 'importance' => 0, 'twig' => '/some/path'], true,
|
||||||
|
],
|
||||||
|
'missing html and twig' => [
|
||||||
|
['id' => '123', 'title' => 'foo', 'importance' => 0], false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProcessMessages()
|
||||||
|
{
|
||||||
|
// Mock a WelcomePopup message provider, with a fixed class name
|
||||||
|
$oProvider1 = $this->getMockBuilder(iWelcomePopup::class)->setMockClassName('Provider1')->getMock();
|
||||||
|
$oProvider1->expects($this->once())->method('GetMessages')->willReturn([
|
||||||
|
['id' => '123', 'title' => 'foo', 'importance' => 0, 'html' => '<p>Hello Foo</p>'],
|
||||||
|
['id' => '456', 'title' => 'bar', 'importance' => 1, 'html' => '<p>Hello Bar</p>'], // Already acknowledged will be skipped
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mock another WelcomePopup message provider, with a different class name
|
||||||
|
$oProvider2 = $this->getMockBuilder(iWelcomePopup::class)->setMockClassName('Provider2')->getMock();
|
||||||
|
$oProvider2->expects($this->once())->method('GetMessages')->willReturn([
|
||||||
|
['id' => '789', 'title' => 'Ga', 'importance' => 1, 'html' => '<p>Hello Ga</p>'],
|
||||||
|
['id' => '012', 'title' => 'Bu', 'importance' => 0, 'twig' => 'ga/bu/zo'],
|
||||||
|
['id' => '000', 'title' => 'Bu', 'importance' => 0], // Invalid, will be ignored
|
||||||
|
]);
|
||||||
|
$oService = new WelcomePopupService();
|
||||||
|
$this->InvokeNonPublicMethod(WelcomePopupService::class, 'SetAcknowledgedMessagesCache', $oService, [[get_class($oProvider1).'::456']]);
|
||||||
|
$this->InvokeNonPublicMethod(WelcomePopupService::class, 'SetMessagesProviders', $oService, [[$oProvider1, $oProvider2]]);
|
||||||
|
|
||||||
|
$aMessages = $this->InvokeNonPublicMethod(WelcomePopupService::class, 'ProcessMessages', $oService, []);
|
||||||
|
$this->assertEquals(
|
||||||
|
[
|
||||||
|
['id' => '012', 'title' => 'Bu', 'importance' => 0, 'twig' => 'ga/bu/zo', 'uuid' => 'Provider2::012'],
|
||||||
|
['id' => '123', 'title' => 'foo', 'importance' => 0, 'html' => '<p>Hello Foo</p>', 'uuid' => 'Provider1::123'],
|
||||||
|
['id' => '789', 'title' => 'Ga', 'importance' => 1, 'html' => '<p>Hello Ga</p>', 'uuid' => 'Provider2::789'],
|
||||||
|
],
|
||||||
|
$aMessages
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function testAcknowledgeMessage()
|
||||||
|
{
|
||||||
|
self::CreateUser('admin-testAcknowledgeMessage', 1, '-Passw0rd!Complex-');
|
||||||
|
UserRights::Login('admin-testAcknowledgeMessage');
|
||||||
|
|
||||||
|
// Mock a WelcomePopup message provider, with a fixed class name
|
||||||
|
$oProvider1 = $this->getMockBuilder(iWelcomePopup::class)->setMockClassName('Provider1')->getMock();
|
||||||
|
$oProvider1->expects($this->exactly(2))->method('AcknowledgeMessage');
|
||||||
|
|
||||||
|
// Mock another WelcomePopup message provider, with a different class name
|
||||||
|
$oProvider2 = $this->getMockBuilder(iWelcomePopup::class)->setMockClassName('Provider2')->getMock();
|
||||||
|
$oProvider2->expects($this->exactly(1))->method('AcknowledgeMessage');
|
||||||
|
|
||||||
|
$sMessageUUID1 = get_class($oProvider1).'::0123456';
|
||||||
|
$sMessageUUID2 = get_class($oProvider1).'::456789';
|
||||||
|
$sMessageUUID3 = get_class($oProvider2).'::456789'; // Same message id but different provider / UUID
|
||||||
|
$oService = new WelcomePopupService();
|
||||||
|
|
||||||
|
$this->InvokeNonPublicMethod(WelcomePopupService::class, 'SetMessagesProviders', $oService, [[$oProvider1, $oProvider2]]);
|
||||||
|
|
||||||
|
$oService->AcknowledgeMessage($sMessageUUID1);
|
||||||
|
$this->assertTrue($this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, [$sMessageUUID1]));
|
||||||
|
$this->assertFalse($this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, ['-This-Message-Id-Is-Not-Ack0ledg3dged!']));
|
||||||
|
$this->assertFalse($this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, [$sMessageUUID3]));
|
||||||
|
|
||||||
|
$oService->AcknowledgeMessage($sMessageUUID2);
|
||||||
|
$this->assertTrue($this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, [$sMessageUUID1]));
|
||||||
|
$this->assertTrue($this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, [$sMessageUUID2]));
|
||||||
|
$this->assertFalse($this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, ['-This-Message-Id-Is-Not-Ack0ledg3dged!']));
|
||||||
|
$this->assertFalse($this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, [$sMessageUUID3]));
|
||||||
|
|
||||||
|
$oService->AcknowledgeMessage($sMessageUUID3);
|
||||||
|
$this->assertTrue($this->InvokeNonPublicMethod(WelcomePopupService::class, 'IsMessageAcknowledged', $oService, [$sMessageUUID3]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider makeStringFitInProvider
|
||||||
|
*/
|
||||||
|
public function testMakeStringFitIn($sInput, $iLimit, $sExpected)
|
||||||
|
{
|
||||||
|
$oService = new WelcomePopupService();
|
||||||
|
$sFitted = $this->InvokeNonPublicMethod(WelcomePopupService::class, 'MakeStringFitIn', $oService, [$sInput, $iLimit]);
|
||||||
|
$this->assertTrue(mb_strlen($sFitted) <= $iLimit);
|
||||||
|
$this->assertEquals($sExpected, $sFitted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function makeStringFitInProvider()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Simple (no truncation)' => ['/Some/Short/EnoughName', 50, '/Some/Short/EnoughName'],
|
||||||
|
'Very long (truncated)' => ['/Some/Very/Loooooooooooooooooooooooooooong/Naaaaaaaaaaaaaaaaaaaaaaaaaame', 50, '4769a98d57a0f2e9b99483f780833faf-aaaaaaaaaaaaaaame'],
|
||||||
|
'Long More aggressive truncation' => ['/Some/Very/Loooooooooooooooooooooooooooong/Naaaaaaaaaaaaaaaaaaaaaaaaaame', 45, '4769a98d57a0f2e9b99483f780833faf-aaaaaaaaaame'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -108,8 +108,8 @@ class CMDBObjectTest extends ItopDataTestCase
|
|||||||
$sAdminLogin = "admin-user-".$sUid;
|
$sAdminLogin = "admin-user-".$sUid;
|
||||||
$sImpersonatedLogin = "impersonated-user-".$sUid;
|
$sImpersonatedLogin = "impersonated-user-".$sUid;
|
||||||
|
|
||||||
$iAdminUserId = $this->CreateUserForImpersonation($sAdminLogin, 'Administrator', 'AdminName', 'AdminSurName');
|
$oAdminUser = $this->CreateUserForImpersonation($sAdminLogin, 'Administrator', 'AdminName', 'AdminSurName');
|
||||||
$this->CreateUserForImpersonation($sImpersonatedLogin, 'Configuration Manager', 'ImpersonatedName', 'ImpersonatedSurName');
|
$oImpersonatedUser = $this->CreateUserForImpersonation($sImpersonatedLogin, 'Configuration Manager', 'ImpersonatedName', 'ImpersonatedSurName');
|
||||||
|
|
||||||
$_SESSION = [];
|
$_SESSION = [];
|
||||||
\UserRights::Login($sAdminLogin);
|
\UserRights::Login($sAdminLogin);
|
||||||
@@ -124,28 +124,31 @@ class CMDBObjectTest extends ItopDataTestCase
|
|||||||
if (is_null($sTrackInfo)){
|
if (is_null($sTrackInfo)){
|
||||||
CMDBObject::SetTrackInfo(null);
|
CMDBObject::SetTrackInfo(null);
|
||||||
} else {
|
} else {
|
||||||
|
$sTrackInfo = $this->ReplaceByFriendlyNames($sTrackInfo, $oAdminUser, $oImpersonatedUser);
|
||||||
CMDBObject::SetTrackInfo($sTrackInfo);
|
CMDBObject::SetTrackInfo($sTrackInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->CreateSimpleObject();
|
$this->CreateSimpleObject();
|
||||||
if (is_null($sTrackInfo)){
|
if (is_null($sTrackInfo)){
|
||||||
self::assertEquals("AdminSurName AdminName", CMDBObject::GetCurrentChange()->Get('userinfo'),
|
self::assertEquals($oAdminUser->GetFriendlyName(), CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||||
'TrackInfo : no impersonation');
|
'TrackInfo : no impersonation');
|
||||||
} else {
|
} else {
|
||||||
self::assertEquals($sTrackInfo, CMDBObject::GetCurrentChange()->Get('userinfo'),
|
self::assertEquals($sTrackInfo, CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||||
'TrackInfo : no impersonation');
|
'TrackInfo : no impersonation');
|
||||||
}
|
}
|
||||||
self::assertEquals($iAdminUserId, CMDBObject::GetCurrentChange()->Get('user_id'),
|
self::assertEquals($oAdminUser->GetKey(), CMDBObject::GetCurrentChange()->Get('user_id'),
|
||||||
'TrackInfo : admin userid');
|
'TrackInfo : admin userid');
|
||||||
|
|
||||||
\UserRights::Impersonate($sImpersonatedLogin);
|
\UserRights::Impersonate($sImpersonatedLogin);
|
||||||
$this->CreateSimpleObject();
|
$this->CreateSimpleObject();
|
||||||
|
|
||||||
if (is_null($sExpectedChangeLogWhenImpersonation)){
|
if (is_null($sExpectedChangeLogWhenImpersonation)){
|
||||||
self::assertEquals("AdminSurName AdminName on behalf of ImpersonatedSurName ImpersonatedName", CMDBObject::GetCurrentChange()->Get('userinfo'),
|
$sExpectedMsg = $this->ReplaceByFriendlyNames("AdminSurName AdminName on behalf of ImpersonatedSurName ImpersonatedName", $oAdminUser, $oImpersonatedUser);
|
||||||
|
self::assertEquals($sExpectedMsg, CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||||
'TrackInfo : impersonation');
|
'TrackInfo : impersonation');
|
||||||
} else {
|
} else {
|
||||||
self::assertEquals($sExpectedChangeLogWhenImpersonation, CMDBObject::GetCurrentChange()->Get('userinfo'),
|
$sExpectedMsg = $this->ReplaceByFriendlyNames($sExpectedChangeLogWhenImpersonation, $oAdminUser, $oImpersonatedUser);
|
||||||
|
self::assertEquals($sExpectedMsg, CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||||
'TrackInfo : impersonation');
|
'TrackInfo : impersonation');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,13 +158,13 @@ class CMDBObjectTest extends ItopDataTestCase
|
|||||||
\UserRights::Deimpersonate();
|
\UserRights::Deimpersonate();
|
||||||
$this->CreateSimpleObject();
|
$this->CreateSimpleObject();
|
||||||
if (is_null($sTrackInfo)){
|
if (is_null($sTrackInfo)){
|
||||||
self::assertEquals("AdminSurName AdminName", CMDBObject::GetCurrentChange()->Get('userinfo'),
|
self::assertEquals($oAdminUser->GetFriendlyName(), CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||||
'TrackInfo : no impersonation');
|
'TrackInfo : no impersonation');
|
||||||
} else {
|
} else {
|
||||||
self::assertEquals($sTrackInfo, CMDBObject::GetCurrentChange()->Get('userinfo'),
|
self::assertEquals($sTrackInfo, CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||||
'TrackInfo : no impersonation');
|
'TrackInfo : no impersonation');
|
||||||
}
|
}
|
||||||
self::assertEquals($iAdminUserId, CMDBObject::GetCurrentChange()->Get('user_id'),
|
self::assertEquals($oAdminUser->GetKey(), CMDBObject::GetCurrentChange()->Get('user_id'),
|
||||||
'TrackInfo : admin userid');
|
'TrackInfo : admin userid');
|
||||||
|
|
||||||
// restore initial conditions
|
// restore initial conditions
|
||||||
@@ -169,6 +172,12 @@ class CMDBObjectTest extends ItopDataTestCase
|
|||||||
CMDBObject::SetTrackInfo($sInitialTrackInfo);
|
CMDBObject::SetTrackInfo($sInitialTrackInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function ReplaceByFriendlyNames($sMessage, $oAdminUser, $oImpersonatedUser) : string {
|
||||||
|
$sNewMessage = str_replace('AdminSurName AdminName', $oAdminUser->GetFriendlyName(), $sMessage);
|
||||||
|
$sNewMessage = str_replace('ImpersonatedSurName ImpersonatedName', $oImpersonatedUser->GetFriendlyName(), $sNewMessage);
|
||||||
|
return $sNewMessage;
|
||||||
|
}
|
||||||
|
|
||||||
private function CreateSimpleObject(){
|
private function CreateSimpleObject(){
|
||||||
/** @var \DocumentWeb $oTestObject */
|
/** @var \DocumentWeb $oTestObject */
|
||||||
$oTestObject = MetaModel::NewObject('DocumentWeb');
|
$oTestObject = MetaModel::NewObject('DocumentWeb');
|
||||||
@@ -178,7 +187,7 @@ class CMDBObjectTest extends ItopDataTestCase
|
|||||||
$oTestObject->DBWrite();
|
$oTestObject->DBWrite();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function CreateUserForImpersonation($sLogin, $sProfileName, $sName, $sSurname): int {
|
private function CreateUserForImpersonation($sLogin, $sProfileName, $sName, $sSurname): \UserLocal {
|
||||||
/** @var \Person $oPerson */
|
/** @var \Person $oPerson */
|
||||||
$oPerson = $this->createObject('Person', array(
|
$oPerson = $this->createObject('Person', array(
|
||||||
'name' => $sName,
|
'name' => $sName,
|
||||||
@@ -187,8 +196,9 @@ class CMDBObjectTest extends ItopDataTestCase
|
|||||||
));
|
));
|
||||||
|
|
||||||
$oProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => $sProfileName), true);
|
$oProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => $sProfileName), true);
|
||||||
|
/** @var \UserLocal $oUser */
|
||||||
$oUser = $this->CreateUser($sLogin, $oProfile->GetKey(), "1234567Azert@", $oPerson->GetKey());
|
$oUser = $this->CreateUser($sLogin, $oProfile->GetKey(), "1234567Azert@", $oPerson->GetKey());
|
||||||
|
|
||||||
return $oUser->GetKey();
|
return $oUser;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -866,12 +866,14 @@ try
|
|||||||
$sKey = (string) $key;
|
$sKey = (string) $key;
|
||||||
|
|
||||||
if ($sKey == '__STATUS__') continue;
|
if ($sKey == '__STATUS__') continue;
|
||||||
|
//__ERRORS__ used by tests only
|
||||||
|
if ($sKey == '__ERRORS__') continue;
|
||||||
if ($sKey == 'finalclass') continue;
|
if ($sKey == 'finalclass') continue;
|
||||||
if ($sKey == 'id') continue;
|
if ($sKey == 'id') continue;
|
||||||
|
|
||||||
if (is_object($value))
|
if (is_object($value))
|
||||||
{
|
{
|
||||||
$aRowDisp["$sKey"] = $value->GetDisplayableValue().$value->GetDescription();
|
$aRowDisp["$sKey"] = $value->GetDisplayableValueAndDescription();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user