mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-14 16:04:10 +01:00
Compare commits
59 Commits
feature/ph
...
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
|
||||
{
|
||||
/**
|
||||
* @api
|
||||
*/
|
||||
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/>
|
||||
<methods/>
|
||||
</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>
|
||||
<portals>
|
||||
<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/template.class.inc.php');
|
||||
require_once(APPROOT."/application/user.dashboard.class.inc.php");
|
||||
require_once(APPROOT."/setup/parentmenunodecompiler.class.inc.php");
|
||||
|
||||
|
||||
/**
|
||||
@@ -103,7 +104,7 @@ class ApplicationMenu
|
||||
{
|
||||
self::$sFavoriteSiloQuery = $sOQL;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the query used to limit the list of displayed organizations in the drop-down menu
|
||||
* @return string The OQL query returning a list of Organization objects
|
||||
@@ -273,12 +274,23 @@ class ApplicationMenu
|
||||
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[] = [
|
||||
'sId' => $oMenuNode->GetMenuID(),
|
||||
'sIconCssClasses' => $oMenuNode->GetDecorationClasses(),
|
||||
'sInitials' => $oMenuNode->GetInitials(),
|
||||
'sTitle' => $oMenuNode->GetTitle(),
|
||||
'aSubMenuNodes' => static::GetSubMenuNodes($sMenuGroupIdx, $aExtraParams),
|
||||
'aSubMenuNodes' => $aSubMenuNodes,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -525,7 +537,7 @@ EOF
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the currently active menu (if any, otherwise the first menu is the default)
|
||||
* @return string The Id of the currently active menu
|
||||
@@ -533,7 +545,7 @@ EOF
|
||||
public static function GetActiveNodeId()
|
||||
{
|
||||
$oAppContext = new ApplicationContext();
|
||||
$sMenuId = $oAppContext->GetCurrentValue('menu', null);
|
||||
$sMenuId = $oAppContext->GetCurrentValue('menu', null);
|
||||
if ($sMenuId === null)
|
||||
{
|
||||
$sMenuId = self::GetDefaultMenuId();
|
||||
@@ -643,7 +655,7 @@ abstract class MenuNode
|
||||
|
||||
/**
|
||||
* Stimulus to check: if the user can 'apply' this stimulus, then she/he can see this menu
|
||||
*/
|
||||
*/
|
||||
protected $m_aEnableStimuli;
|
||||
|
||||
/**
|
||||
@@ -804,7 +816,7 @@ abstract class MenuNode
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a limiting display condition for the same menu node. The conditions will be combined with a AND
|
||||
* @param $oMenuNode MenuNode Another definition of the same menu node, with potentially different access restriction
|
||||
@@ -977,7 +989,7 @@ class TemplateMenuNode extends MenuNode
|
||||
* @var string
|
||||
*/
|
||||
protected $sTemplateFile;
|
||||
|
||||
|
||||
/**
|
||||
* Create a menu item based on a custom template and inserts it into the application's main menu
|
||||
* @param string $sMenuId Unique identifier of the menu (used to identify the menu for bookmarking, and for getting the labels from the dictionary)
|
||||
@@ -1048,7 +1060,7 @@ class OQLMenuNode extends MenuNode
|
||||
* @var bool|null
|
||||
*/
|
||||
protected $bSearchFormOpen;
|
||||
|
||||
|
||||
/**
|
||||
* Extra parameters to be passed to the display block to fine tune its appearence
|
||||
*/
|
||||
@@ -1081,7 +1093,7 @@ class OQLMenuNode extends MenuNode
|
||||
// Enhancement: we could set as the "enable" condition that the user has enough rights to "read" the objects
|
||||
// of the class specified by the OQL...
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set some extra parameters to be passed to the display block to fine tune its appearence
|
||||
* @param array $aParams paramCode => value. See DisplayBlock::GetDisplay for the meaning of the parameters
|
||||
@@ -1109,7 +1121,7 @@ class OQLMenuNode extends MenuNode
|
||||
'Menu_'.$this->GetMenuId(),
|
||||
$this->bSearch, // Search pane
|
||||
$this->bSearchFormOpen, // Search open
|
||||
$oPage,
|
||||
$oPage,
|
||||
array_merge($this->m_aParams, $aExtraParams),
|
||||
true
|
||||
);
|
||||
@@ -1343,10 +1355,10 @@ class NewObjectMenuNode extends MenuNode
|
||||
{
|
||||
// Enable this menu, only if the current user has enough rights to create such an object, or an object of
|
||||
// any child class
|
||||
|
||||
|
||||
$aSubClasses = MetaModel::EnumChildClasses($this->sClass, ENUM_CHILD_CLASSES_ALL); // Including the specified class itself
|
||||
$bActionIsAllowed = false;
|
||||
|
||||
|
||||
foreach($aSubClasses as $sCandidateClass)
|
||||
{
|
||||
if (!MetaModel::IsAbstract($sCandidateClass) && (UserRights::IsActionAllowed($sCandidateClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES))
|
||||
@@ -1355,7 +1367,7 @@ class NewObjectMenuNode extends MenuNode
|
||||
break; // Enough for now
|
||||
}
|
||||
}
|
||||
return $bActionIsAllowed;
|
||||
return $bActionIsAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1497,7 +1509,7 @@ class DashboardMenuNode extends MenuNode
|
||||
throw new Exception("Error: failed to load dashboard file: '{$this->sDashboardFile}'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1538,7 +1550,7 @@ class ShortcutContainerMenuNode extends MenuNode
|
||||
$sName = $this->GetMenuId().'_'.$oShortcut->GetKey();
|
||||
new ShortcutMenuNode($sName, $oShortcut, $this->GetIndex(), $fRank++);
|
||||
}
|
||||
|
||||
|
||||
// Complete the tree
|
||||
//
|
||||
parent::PopulateChildMenus();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.1.0 N°5305
|
||||
*/
|
||||
public function GetDisplayableValueAndDescription(): string
|
||||
{
|
||||
return sprintf("%s%s",
|
||||
$this->GetDisplayableValue(),
|
||||
$this->GetDescription()
|
||||
);
|
||||
}
|
||||
|
||||
abstract public function GetDescription();
|
||||
}
|
||||
|
||||
@@ -86,26 +97,90 @@ class CellStatus_Issue extends CellStatus_Modify
|
||||
parent::__construct($proposedValue, $previousValue);
|
||||
}
|
||||
|
||||
public function GetDescription()
|
||||
public function GetDisplayableValue()
|
||||
{
|
||||
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
|
||||
{
|
||||
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()
|
||||
{
|
||||
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
|
||||
{
|
||||
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_sQuery = $sOql;
|
||||
$this->sSerializedSearch = $sSerializedSearch;
|
||||
parent::__construct(null, $previousValue, '');
|
||||
}
|
||||
|
||||
@@ -139,6 +227,17 @@ class CellStatus_Ambiguous extends CellStatus_Issue
|
||||
$sCount = $this->m_iCount;
|
||||
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
|
||||
@@ -220,17 +339,35 @@ class RowStatus_Issue extends RowStatus
|
||||
*/
|
||||
class BulkChange
|
||||
{
|
||||
/** @var string */
|
||||
protected $m_sClass;
|
||||
protected $m_aData; // Note: hereafter, iCol maybe actually be any acceptable key (string)
|
||||
// #@# todo: rename the variables to sColIndex
|
||||
protected $m_aAttList; // attcode => iCol
|
||||
protected $m_aExtKeys; // aExtKeys[sExtKeyAttCode][sExtReconcKeyAttCode] = iCol;
|
||||
protected $m_aReconcilKeys; // attcode (attcode = 'id' for the pkey)
|
||||
protected $m_sSynchroScope; // OQL - if specified, then the missing items will be reported
|
||||
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)
|
||||
protected $m_sDateFormat; // Date format specification, see DateTime::createFromFormat
|
||||
protected $m_bLocalizedValues; // Values in the data set are localized (see AttributeEnum)
|
||||
protected $m_aExtKeysMappingCache; // Cache for resolving external keys based on the given search criterias
|
||||
/** @var array<string, string> attcode as key, iCol as value */
|
||||
protected $m_aAttList;
|
||||
/** @var array<string, array<string, string>> sExtKeyAttCode as key, array of sExtReconcKeyAttCode/iCol as value */
|
||||
protected $m_aExtKeys;
|
||||
/** @var string[] list of attcode (attcode = 'id' for the pkey) */
|
||||
protected $m_aReconcilKeys;
|
||||
/** @var string OQL - if specified, then the missing items will be reported */
|
||||
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)
|
||||
{
|
||||
@@ -266,25 +403,25 @@ class BulkChange
|
||||
{
|
||||
$oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
|
||||
$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];
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
$oReconFilter->AddCondition($sForeignAttCode, $value, '=');
|
||||
$oReconFilter->AddCondition($sReconKeyAttCode, $value, '=');
|
||||
$aResults[$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol]));
|
||||
}
|
||||
|
||||
$oExtObjects = new CMDBObjectSet($oReconFilter);
|
||||
$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
|
||||
@@ -321,7 +458,7 @@ class BulkChange
|
||||
|
||||
// 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
|
||||
// if (!array_key_exists($sAttCode, $this->m_aAttList)) continue;
|
||||
@@ -330,7 +467,7 @@ class BulkChange
|
||||
|
||||
if ($this->IsNullExternalKeySpec($aRowData, $sAttCode))
|
||||
{
|
||||
foreach ($aKeyConfig as $sForeignAttCode => $iCol)
|
||||
foreach ($aReconKeys as $sReconKeyAttCode => $iCol)
|
||||
{
|
||||
// Default reporting
|
||||
// $aRowData[$iCol] is always null
|
||||
@@ -352,25 +489,24 @@ class BulkChange
|
||||
$oReconFilter = new DBObjectSearch($oExtKey->GetTargetClass());
|
||||
|
||||
$aCacheKeys = array();
|
||||
foreach ($aKeyConfig as $sForeignAttCode => $iCol)
|
||||
foreach ($aReconKeys as $sReconKeyAttCode => $iCol)
|
||||
{
|
||||
// The foreign attribute is one of our reconciliation key
|
||||
if ($sForeignAttCode == 'id')
|
||||
if ($sReconKeyAttCode == 'id')
|
||||
{
|
||||
$value = $aRowData[$iCol];
|
||||
}
|
||||
else
|
||||
{
|
||||
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sForeignAttCode);
|
||||
$oForeignAtt = MetaModel::GetAttributeDef($oExtKey->GetTargetClass(), $sReconKeyAttCode);
|
||||
$value = $oForeignAtt->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
|
||||
}
|
||||
$aCacheKeys[] = $value;
|
||||
$oReconFilter->AddCondition($sForeignAttCode, $value, '=');
|
||||
$oReconFilter->AddCondition($sReconKeyAttCode, $value, '=');
|
||||
$aResults[$iCol] = new CellStatus_Void(utils::HtmlEntities($aRowData[$iCol]));
|
||||
}
|
||||
$sCacheKey = implode('_|_', $aCacheKeys); // Unique key for this query...
|
||||
$iForeignKey = null;
|
||||
$sOQL = '';
|
||||
// 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))
|
||||
{
|
||||
@@ -379,9 +515,8 @@ class BulkChange
|
||||
if (array_key_exists($sCacheKey, $this->m_aExtKeysMappingCache[$sAttCode]))
|
||||
{
|
||||
// Cache hit
|
||||
$iCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c'];
|
||||
$iObjectFoundCount = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['c'];
|
||||
$iForeignKey = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['k'];
|
||||
$sOQL = $this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['oql'];
|
||||
// Record the hit
|
||||
$this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey]['h']++;
|
||||
}
|
||||
@@ -389,34 +524,35 @@ class BulkChange
|
||||
{
|
||||
// Cache miss, let's initialize it
|
||||
$oExtObjects = new CMDBObjectSet($oReconFilter);
|
||||
$iCount = $oExtObjects->Count();
|
||||
if ($iCount == 1)
|
||||
$iObjectFoundCount = $oExtObjects->Count();
|
||||
if ($iObjectFoundCount == 1)
|
||||
{
|
||||
$oForeignObj = $oExtObjects->Fetch();
|
||||
$iForeignKey = $oForeignObj->GetKey();
|
||||
}
|
||||
$this->m_aExtKeysMappingCache[$sAttCode][$sCacheKey] = array(
|
||||
'c' => $iCount,
|
||||
'c' => $iObjectFoundCount,
|
||||
'k' => $iForeignKey,
|
||||
'oql' => $oReconFilter->ToOql(),
|
||||
'h' => 0, // number of hits on this cache entry
|
||||
);
|
||||
}
|
||||
switch($iCount)
|
||||
switch($iObjectFoundCount)
|
||||
{
|
||||
case 0:
|
||||
$aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound');
|
||||
$aResults[$sAttCode]= new CellStatus_SearchIssue();
|
||||
break;
|
||||
$oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter);
|
||||
$aResults[$sAttCode] = $oCellStatus_SearchIssue;
|
||||
$aErrors[$sAttCode] = Dict::S('UI:CSVReport-Value-Issue-NotFound');
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// Do change the external key attribute
|
||||
$oTargetObj->Set($sAttCode, $iForeignKey);
|
||||
break;
|
||||
// Do change the external key attribute
|
||||
$oTargetObj->Set($sAttCode, $iForeignKey);
|
||||
break;
|
||||
|
||||
default:
|
||||
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $iCount);
|
||||
$aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iCount, $sOQL);
|
||||
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-FoundMany', $iObjectFoundCount);
|
||||
$aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $iObjectFoundCount, $oReconFilter->serialize());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,7 +569,7 @@ class BulkChange
|
||||
else
|
||||
{
|
||||
$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
|
||||
$aResults[$iCol] = new CellStatus_Modify(utils::HtmlEntities($aRowData[$iCol]));
|
||||
@@ -479,9 +615,18 @@ class BulkChange
|
||||
}
|
||||
} else {
|
||||
$value = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
|
||||
if (is_null($value) && (strlen($aRowData[$iCol]) > 0)) {
|
||||
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode);
|
||||
} else {
|
||||
if (is_null($value) && (strlen($aRowData[$iCol]) > 0))
|
||||
{
|
||||
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 {
|
||||
$aErrors[$sAttCode] = Dict::Format('UI:CSVReport-Value-Issue-NoMatch', $sAttCode);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$res = $oTargetObj->CheckValue($sAttCode, $value);
|
||||
if ($res === true)
|
||||
{
|
||||
@@ -559,6 +704,95 @@ class BulkChange
|
||||
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)
|
||||
{
|
||||
$aResults = array();
|
||||
@@ -670,6 +904,8 @@ class BulkChange
|
||||
{
|
||||
$sErrors = implode(', ', $aErrors);
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -736,6 +972,8 @@ class BulkChange
|
||||
{
|
||||
$sErrors = implode(', ', $aErrors);
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -785,6 +1023,8 @@ class BulkChange
|
||||
{
|
||||
$sErrors = implode(', ', $aErrors);
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -872,14 +1112,18 @@ class BulkChange
|
||||
$sFormat = $sDateFormat;
|
||||
}
|
||||
$oFormat = new DateTimeFormat($sFormat);
|
||||
$sDateExample = $oFormat->Format(new DateTime('2022-10-23 16:25:33'));
|
||||
$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][$iCol] = new CellStatus_Issue(utils::HtmlEntities($sValue), null, $sErrorMsg);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
$oDate = DateTime::createFromFormat($sFormat, $this->m_aData[$iRow][$iCol]);
|
||||
$oDate = DateTime::createFromFormat($sFormat, $sValue);
|
||||
if ($oDate !== false)
|
||||
{
|
||||
$sNewDate = $oDate->format($oAttDef->GetInternalFormat());
|
||||
@@ -889,7 +1133,7 @@ class BulkChange
|
||||
{
|
||||
// Leave the cell unchanged
|
||||
$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
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
@@ -953,11 +1199,12 @@ class BulkChange
|
||||
}
|
||||
elseif (count($aMatches) == 0)
|
||||
{
|
||||
$aResult[$iRow][$sAttCode] = new CellStatus_SearchIssue();
|
||||
$oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter);
|
||||
$aResult[$iRow][$sAttCode] = $oCellStatus_SearchIssue;
|
||||
}
|
||||
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:
|
||||
// Found several matches, 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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1257,6 +1257,22 @@ class Config
|
||||
'source_of_value' => '',
|
||||
'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' => [
|
||||
'type' => 'bool',
|
||||
'description' => 'Whether or not the quick create is enabled',
|
||||
@@ -1888,7 +1904,7 @@ class Config
|
||||
}
|
||||
if (strlen($sNoise) > 0)
|
||||
{
|
||||
// Note: sNoise is an html output, but so far it was ok for me (e.g. showing the entire call stack)
|
||||
// Note: sNoise is an html output, but so far it was ok for me (e.g. showing the entire call stack)
|
||||
throw new ConfigException('Syntax error in configuration file',
|
||||
array('file' => $sConfigFile, 'error' => '<tt>'.htmlentities($sNoise, ENT_QUOTES, 'UTF-8').'</tt>'));
|
||||
}
|
||||
@@ -2698,7 +2714,7 @@ class ConfigPlaceholdersResolver
|
||||
}
|
||||
|
||||
$sPattern = '/\%(env|server)\((\w+)\)(?:\?:(\w*))?\%/'; //3 capturing groups, ie `%env(HTTP_PORT)?:8080%` produce: `env` `HTTP_PORT` and `8080`.
|
||||
|
||||
|
||||
if (! preg_match_all($sPattern, $rawValue, $aMatchesCollection, PREG_SET_ORDER))
|
||||
{
|
||||
return $rawValue;
|
||||
|
||||
7
css/backoffice/pages/_csv-import.scss
vendored
7
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
border-bottom: 1px $ibo-csv-import--row--border-color solid;
|
||||
}
|
||||
@@ -51,4 +46,4 @@ tr.ibo-csv-import--row-added td {
|
||||
font-size: $ibo-csv-import--download-file--font-size;
|
||||
color: $ibo-csv-import--download-file--color;
|
||||
margin: $ibo-csv-import--download-file--margin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ $ibo-welcome-popup--text--options--bottom: 10px !default;
|
||||
|
||||
#welcome_popup{
|
||||
display: flex;
|
||||
|
||||
}
|
||||
.ibo-welcome-popup--columns{
|
||||
display: flex;
|
||||
}
|
||||
.ibo-welcome-popup--image{
|
||||
display: flex;
|
||||
@@ -44,7 +46,39 @@ $ibo-welcome-popup--text--options--bottom: 10px !default;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ibo-welcome-popup--text--options{
|
||||
position: absolute;
|
||||
bottom: $ibo-welcome-popup--text--options--bottom;
|
||||
.ibo-welcome-popup--dialog {
|
||||
width: 60rem;
|
||||
}
|
||||
.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 */
|
||||
}
|
||||
|
||||
.ibo-is-disabled {
|
||||
cursor: not-allowed !important; /* Note: !important is necessary as it needs to overload any standard rules */
|
||||
}
|
||||
|
||||
/****************************/
|
||||
/* Disposition / alignement */
|
||||
/****************************/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -187,6 +187,12 @@ class DBRestore extends DBBackup
|
||||
@chmod($sConfigFile, 0770); // Allow overwriting the file
|
||||
rename($sDataDir.'/config-itop.php', $sConfigFile);
|
||||
@chmod($sConfigFile, 0440); // Read-only
|
||||
|
||||
$aExtraFiles = $this->ListExtraFiles($sDataDir);
|
||||
foreach($aExtraFiles as $sSourceFilePath => $sDestinationFilePath) {
|
||||
SetupUtils::builddir(dirname($sDestinationFilePath));
|
||||
rename($sSourceFilePath, $sDestinationFilePath);
|
||||
}
|
||||
|
||||
try {
|
||||
SetupUtils::rrmdir($sDataDir);
|
||||
@@ -211,4 +217,27 @@ class DBRestore extends DBBackup
|
||||
$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:CSVReport-Value-Modified' => 'Upraveno',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Nemůže být změněno - důvod: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Nemůže být změněno na %1$s - důvod: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Žádná shoda',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Chybí povinná hodnota',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Nejednoznačné: nalezeno %1$s objektů',
|
||||
'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:CSVReport-Value-Modified' => 'Ændret',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Kunne ikke ændres - årsag: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Kunne ikke ændres til %1$s - årsag: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Mangler obligatorisk værdi',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Tvetydig: fandt %1$s objekter',
|
||||
'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>
|
||||
|
||||
<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:MyCalls' => 'An mich gestellte Benutzeranfragen',
|
||||
'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:CSVReport-Value-Modified' => 'Modifiziert',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Konnte nicht geändert werden - Grund: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Konnte nicht zu %1$s geändert werden - Grund: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Kein Treffer',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Pflichtfeld fehlt',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Doppeldeutig: %1$s Objekte gefunden',
|
||||
'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>
|
||||
|
||||
<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:MyCalls' => 'My requests',
|
||||
'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:CSVReport-Value-Modified' => 'Modified',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Invalid value for attribute',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value',
|
||||
'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-Ambiguous' => 'Ambiguous: found %1$s objects',
|
||||
'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-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-AllowedValues' => 'Allowed \'%1$s\' value(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-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-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-Ambiguous' => 'ambiguous reconciliation',
|
||||
'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>
|
||||
|
||||
<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:MyCalls' => 'Mis Requerimientos',
|
||||
'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:CSVReport-Value-Modified' => 'Modificado',
|
||||
'UI:CSVReport-Value-SetIssue' => 'No puede ser modificado - motivo: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'No puede ser cambiado a %1$s - motivo: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No hay Coincidencias',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Falta valor obligatorio',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Ambigüedad: encontrados %1$s objetos',
|
||||
'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>
|
||||
|
||||
<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:MyCalls' => 'Mes Appels Support',
|
||||
'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:CSVReport-Value-Modified' => 'Modifié',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Modification impossible - cause : %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Ne peut pas prendre la valeur \'%1$s\' - cause : %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Pas de correspondance',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Valeur invalide',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Ne peut pas prendre la valeur \'%1$s\'',
|
||||
'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-Ambiguous' => 'Ambigüité: %1$d objets trouvés',
|
||||
'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:CSVReport-Value-Modified' => 'Modified~~',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match~~',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Missing mandatory value~~',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects~~',
|
||||
'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:CSVReport-Value-Modified' => 'Modified~~',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Could not be changed - reason: %1$s~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Could not be changed to %1$s - reason: %2$s~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match~~',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Missing mandatory value~~',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects~~',
|
||||
'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:CSVReport-Value-Modified' => '修正済み',
|
||||
'UI:CSVReport-Value-SetIssue' => '変更出来ません - 理由: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '%1$s へ変更出来ません - 理由: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'マッチしません',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => '必須の値がありません',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'あいまいな値: %1$s オブジェクト',
|
||||
'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:CSVReport-Value-Modified' => 'Aangepast',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Kon niet worden aangepast - reden: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Kon niet worden aangepast naar %1$s - reden: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Geen match',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Ontbrekende verplichte waarde',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Onduidelijk: gevonden %1$s objecten',
|
||||
'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:CSVReport-Value-Modified' => 'Zmodyfikowano',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Nie można było zmienić - powód: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Nie można zmienić na %1$s - powód: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Nie pasuje',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Brak wymaganej wartości',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Uwaga: znaleziono %1$s obiektów',
|
||||
'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:CSVReport-Value-Modified' => 'Modificado',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Não pode ser modificado - razão: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Não pode ser modificado para %1$s - razão: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Não combina',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Faltando valor obrigatório',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Ambiguous: found %1$s objects',
|
||||
'UI:CSVReport-Row-Unchanged' => 'unchanged',
|
||||
|
||||
@@ -645,9 +645,9 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
|
||||
'UI:UniversalSearch:LabelSelectTheClass' => 'Выбор класса для поиска: ',
|
||||
|
||||
'UI:CSVReport-Value-Modified' => 'Изменен',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Не может быть изменен - причина: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Не может быть изменен %1$s - причина: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Нет совпадений',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Отсутствует обязательное значение',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Неоднозначное сопоставление: найдено %1$s объектов',
|
||||
'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:CSVReport-Value-Modified' => 'Upravený',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Nemožno zmeniť - dôvod: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => 'Nemožno zmeniť na %1$s - dôvod: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Žiadna zhoda',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Chýbajúca povinná hodnota',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Nejednoznačné: nájdených %1$s objektov',
|
||||
'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:CSVReport-Value-Modified' => 'Değiştiridi',
|
||||
'UI:CSVReport-Value-SetIssue' => 'Değiştirilemedi - Sebep: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '%1$s olarak değiştirilemedi - Sebep: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => 'Eşleşme yok',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => 'Eksik Zorunlu Değer',
|
||||
'UI:CSVReport-Value-Ambiguous' => 'Belirsiz: %1$s nesnelerini buldum',
|
||||
'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:CSVReport-Value-Modified' => '已修改',
|
||||
'UI:CSVReport-Value-SetIssue' => '无法修改 - 原因: %1$s',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '无法修改成 %1$s - 原因: %2$s',
|
||||
'UI:CSVReport-Value-NoMatch' => '不匹配',
|
||||
'UI:CSVReport-Value-SetIssue' => 'invalid value for attribute~~',
|
||||
'UI:CSVReport-Value-ChangeIssue' => '\'%1$s\' is an invalid value~~',
|
||||
'UI:CSVReport-Value-NoMatch' => 'No match for value \'%1$s\'~~',
|
||||
'UI:CSVReport-Value-Missing' => '缺少必填项',
|
||||
'UI:CSVReport-Value-Ambiguous' => '模糊匹配: 找到 %1$s 个对象',
|
||||
'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 ) );
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
// 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){
|
||||
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:
|
||||
{
|
||||
is_hidden: 'ibo-is-hidden',
|
||||
is_disabled: 'ibo-is-disabled',
|
||||
is_transparent: 'ibo-is-transparent',
|
||||
is_opaque: 'ibo-is-opaque',
|
||||
is_scrollable: 'ibo-is-scrollable',
|
||||
@@ -252,6 +253,11 @@ $(function()
|
||||
// Prevent anchor default behaviour
|
||||
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)
|
||||
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');
|
||||
@@ -297,17 +303,30 @@ $(function()
|
||||
const sTabId = oTabHeaderElem.attr('data-tab-id');
|
||||
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
|
||||
if (bIsVisible === null) {
|
||||
bIsVisible = CombodoGlobalToolbox.IsElementVisibleToTheUser(oTabHeaderElem[0], true, 2);
|
||||
}
|
||||
bIsVisible = CombodoGlobalToolbox.IsElementVisibleToTheUser(oTabHeaderElem[0], true, 2);
|
||||
}
|
||||
|
||||
// Hide/show the corresponding extra tab element
|
||||
if (bIsVisible) {
|
||||
oMatchingExtraTabElem.addClass(this.css_classes.is_hidden);
|
||||
} else {
|
||||
oMatchingExtraTabElem.removeClass(this.css_classes.is_hidden);
|
||||
}
|
||||
if (bIsVisible) {
|
||||
oMatchingExtraTabElem.addClass(this.css_classes.is_hidden);
|
||||
} else {
|
||||
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
|
||||
_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
|
||||
* @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');
|
||||
},
|
||||
/**
|
||||
@@ -334,10 +353,41 @@ $(function()
|
||||
* @return {number} The index (zero based) of the tab. If no matching tab, 0 will be returned.
|
||||
* @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+'"]');
|
||||
|
||||
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',
|
||||
'AbstractPreferencesExtension' => $baseDir . '/application/applicationextension.inc.php',
|
||||
'AbstractWeeklyScheduledProcess' => $baseDir . '/core/backgroundprocess.inc.php',
|
||||
'AbstractWelcomePopup' => $baseDir . '/application/applicationextension.inc.php',
|
||||
'Action' => $baseDir . '/core/action.class.inc.php',
|
||||
'ActionChecker' => $baseDir . '/core/userrights.class.inc.php',
|
||||
'ActionEmail' => $baseDir . '/core/action.class.inc.php',
|
||||
@@ -142,6 +143,7 @@ return array(
|
||||
'CheckStopWatchThresholds' => $baseDir . '/core/ormstopwatch.class.inc.php',
|
||||
'CheckableExpression' => $baseDir . '/core/oql/oqlquery.class.inc.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\\WebResourcesHelper' => $baseDir . '/sources/application/Helper/WebResourcesHelper.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\\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\\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\\Controller\\AjaxRenderController' => $baseDir . '/sources/Controller/AjaxRenderController.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\\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\\OAuthClientProviderAbstract' => $baseDir . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAbstract.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',
|
||||
'RowStatus' => $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_Modify' => $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',
|
||||
'iTopWebPage' => $baseDir . '/sources/application/WebPage/iTopWebPage.php',
|
||||
'iTopWizardWebPage' => $baseDir . '/sources/application/WebPage/iTopWizardWebPage.php',
|
||||
'iWelcomePopup' => $baseDir . '/application/applicationextension.inc.php',
|
||||
'iWorkingTimeComputer' => $baseDir . '/core/computing.inc.php',
|
||||
'lnkTriggerAction' => $baseDir . '/core/trigger.class.inc.php',
|
||||
'ormCaseLog' => $baseDir . '/core/ormcaselog.class.inc.php',
|
||||
|
||||
@@ -382,6 +382,7 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239
|
||||
'AbstractPortalUIExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||
'AbstractPreferencesExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||
'AbstractWeeklyScheduledProcess' => __DIR__ . '/../..' . '/core/backgroundprocess.inc.php',
|
||||
'AbstractWelcomePopup' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||
'Action' => __DIR__ . '/../..' . '/core/action.class.inc.php',
|
||||
'ActionChecker' => __DIR__ . '/../..' . '/core/userrights.class.inc.php',
|
||||
'ActionEmail' => __DIR__ . '/../..' . '/core/action.class.inc.php',
|
||||
@@ -510,6 +511,7 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239
|
||||
'CheckStopWatchThresholds' => __DIR__ . '/../..' . '/core/ormstopwatch.class.inc.php',
|
||||
'CheckableExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.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\\WebResourcesHelper' => __DIR__ . '/../..' . '/sources/application/Helper/WebResourcesHelper.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\\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\\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\\Controller\\AjaxRenderController' => __DIR__ . '/../..' . '/sources/Controller/AjaxRenderController.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\\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\\OAuthClientProviderAbstract' => __DIR__ . '/../..' . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAbstract.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',
|
||||
'RowStatus' => __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_Modify' => __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',
|
||||
'iTopWebPage' => __DIR__ . '/../..' . '/sources/application/WebPage/iTopWebPage.php',
|
||||
'iTopWizardWebPage' => __DIR__ . '/../..' . '/sources/application/WebPage/iTopWizardWebPage.php',
|
||||
'iWelcomePopup' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||
'iWorkingTimeComputer' => __DIR__ . '/../..' . '/core/computing.inc.php',
|
||||
'lnkTriggerAction' => __DIR__ . '/../..' . '/core/trigger.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\UIContentBlock;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
|
||||
use Combodo\iTop\Application\WelcomePopup\WelcomePopupService;
|
||||
|
||||
/**
|
||||
* Displays a popup welcome message, once per session at maximum
|
||||
@@ -28,18 +29,17 @@ use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function DisplayWelcomePopup(WebPage $oP)
|
||||
function DisplayWelcomePopup(WebPage $oP): void
|
||||
{
|
||||
if (!Session::IsSet('welcome'))
|
||||
{
|
||||
// Check, only once per session, if the popup should be displayed...
|
||||
// If the user did not already ask for hiding it forever
|
||||
$bPopup = appUserPreferences::GetPref('welcome_popup', true);
|
||||
if ($bPopup)
|
||||
$oWelcomePopupService = new WelcomePopupService();
|
||||
$aMessages = $oWelcomePopupService->GetMessages();
|
||||
if (count($aMessages) > 0)
|
||||
{
|
||||
TwigHelper::RenderIntoPage($oP, APPROOT.'/', 'templates/pages/backoffice/welcome_popup/welcome_popup');
|
||||
Session::Set('welcome', 'ok');
|
||||
TwigHelper::RenderIntoPage($oP, APPROOT.'/', 'templates/pages/backoffice/welcome_popup/welcome_popup', ['messages' => $aMessages]);
|
||||
}
|
||||
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\Renderer\Console\ConsoleBlockRenderer;
|
||||
use Combodo\iTop\Renderer\Console\ConsoleFormRenderer;
|
||||
use Combodo\iTop\Controller\WelcomePopupController;
|
||||
|
||||
require_once('../approot.inc.php');
|
||||
|
||||
@@ -2690,13 +2691,31 @@ EOF
|
||||
$oAjaxRenderController->GetMenusCount($oPage);
|
||||
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:
|
||||
$oPage->p("Invalid query.");
|
||||
}
|
||||
$oKPI->ComputeAndReport('Data fetch and format');
|
||||
$oPage->output();
|
||||
} catch (Exception $e)
|
||||
{
|
||||
} catch (Exception $e) {
|
||||
// note: transform to cope with XSS attacks
|
||||
echo htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8');
|
||||
IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString());
|
||||
|
||||
@@ -139,7 +139,7 @@ try {
|
||||
}
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the most frequent (and regularly occuring) character among the given set, in the specified lines
|
||||
* @param array $aCSVData The input data, one entry per line
|
||||
@@ -175,7 +175,7 @@ try {
|
||||
}
|
||||
$iLine++;
|
||||
}
|
||||
|
||||
|
||||
$aScores = array();
|
||||
foreach($aGuesses as $sSep => $aData)
|
||||
{
|
||||
@@ -186,7 +186,7 @@ try {
|
||||
$sSeparator = $aKeys[0]; // Take the first key, the one with the best score
|
||||
return $sSeparator;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Try to predict the CSV parameters based on the input data
|
||||
* @param string $sCSVData The input data
|
||||
@@ -197,10 +197,10 @@ try {
|
||||
$aData = explode("\n", $sCSVData);
|
||||
$sSeparator = GuessFromFrequency($aData, array("\t", ',', ';', '|')); // Guess the most frequent (and regular) character on each line
|
||||
$sQualifier = GuessFromFrequency($aData, array('"', "'")); // Guess the most frequent (and regular) character on each line
|
||||
|
||||
|
||||
return array('separator' => $sSeparator, 'qualifier' => $sQualifier);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display a banner for the special "synchro" mode
|
||||
* @param WebPage $oP The Page for the output
|
||||
@@ -216,6 +216,7 @@ try {
|
||||
* Add a paragraph to the body of the page
|
||||
*
|
||||
* @param string $s_html
|
||||
* @param ?string $sLinkUrl
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
@@ -260,9 +261,9 @@ try {
|
||||
$sSynchroScope = utils::ReadParam('synchro_scope', '', false, 'raw_data');
|
||||
$sDateTimeFormat = utils::ReadParam('date_time_format', 'default');
|
||||
$sCustomDateTimeFormat = utils::ReadParam('custom_date_time_format', (string)AttributeDateTime::GetFormat(), false, 'raw_data');
|
||||
|
||||
|
||||
$sChosenDateFormat = ($sDateTimeFormat == 'default') ? (string)AttributeDateTime::GetFormat() : $sCustomDateTimeFormat;
|
||||
|
||||
|
||||
if (!empty($sSynchroScope))
|
||||
{
|
||||
$oSearch = DBObjectSearch::FromOQL($sSynchroScope);
|
||||
@@ -277,7 +278,7 @@ try {
|
||||
$sSynchroScope = '';
|
||||
$aSynchroUpdate = null;
|
||||
}
|
||||
|
||||
|
||||
// Parse the data set
|
||||
$oCSVParser = new CSVParser($sCSVData, $sSeparator, $sTextQualifier, MetaModel::GetConfig()->Get('max_execution_time_per_loop'));
|
||||
$aData = $oCSVParser->ToArray($iSkippedLines);
|
||||
@@ -287,10 +288,10 @@ try {
|
||||
$aResult[] = $sTextQualifier.implode($sTextQualifier.$sSeparator.$sTextQualifier, array_shift($aData)).$sTextQualifier; // Remove the first line and store it in case of error
|
||||
$iRealSkippedLines++;
|
||||
}
|
||||
|
||||
|
||||
// Format for the line numbers
|
||||
$sMaxLen = (strlen(''.count($aData)) < 3) ? 3 : strlen(''.count($aData)); // Pad line numbers to the appropriate number of chars, but at least 3
|
||||
|
||||
|
||||
// Compute the list of search/reconciliation criteria
|
||||
$aSearchKeys = array();
|
||||
foreach($aSearchFields as $index => $sDummy)
|
||||
@@ -304,16 +305,16 @@ try {
|
||||
}
|
||||
else
|
||||
{
|
||||
$aSearchKeys[$sSearchField] = '';
|
||||
$aSearchKeys[$sSearchField] = '';
|
||||
}
|
||||
if (!MetaModel::IsValidFilterCode($sClassName, $sSearchField))
|
||||
{
|
||||
// Remove invalid or unmapped search fields
|
||||
$aSearchFields[$index] = null;
|
||||
unset($aSearchKeys[$sSearchField]);
|
||||
unset($aSearchKeys[$sSearchField]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Compute the list of fields and external keys to process
|
||||
$aExtKeys = array();
|
||||
$aAttributes = array();
|
||||
@@ -346,13 +347,13 @@ try {
|
||||
}
|
||||
else
|
||||
{
|
||||
$aAttributes[$sAttCode] = $iIndex;
|
||||
$aAttributes[$sAttCode] = $iIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$oMyChange = null;
|
||||
if (!$bSimulate)
|
||||
{
|
||||
@@ -362,8 +363,6 @@ try {
|
||||
CMDBObject::SetTrackOrigin(CMDBChangeOrigin::CSV_INTERACTIVE);
|
||||
$oMyChange = CMDBObject::GetCurrentChange();
|
||||
}
|
||||
CMDBObject::SetTrackOrigin('csv-interactive');
|
||||
|
||||
$oBulk = new BulkChange(
|
||||
$sClassName,
|
||||
$aData,
|
||||
@@ -373,7 +372,7 @@ try {
|
||||
empty($sSynchroScope) ? null : $sSynchroScope,
|
||||
$aSynchroUpdate,
|
||||
$sChosenDateFormat, // date format
|
||||
true // localize
|
||||
true // localize
|
||||
);
|
||||
$oBulk->SetReportHtml();
|
||||
|
||||
@@ -440,7 +439,6 @@ try {
|
||||
|
||||
case 'RowStatus_NewObj':
|
||||
$iCreated++;
|
||||
$sFinalClass = $aResRow['finalclass'];
|
||||
$sStatus = '<img src="../images/added.png" title="'.Dict::S('UI:CSVReport-Icon-Created').'">';
|
||||
$sCSSRowClass = 'ibo-csv-import--row-added';
|
||||
if ($bSimulate) {
|
||||
@@ -456,7 +454,7 @@ try {
|
||||
case 'RowStatus_Issue':
|
||||
$iErrors++;
|
||||
$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';
|
||||
$sCSSRowClass = 'ibo-csv-import--row-error';
|
||||
if (array_key_exists($iLine, $aData)) {
|
||||
@@ -477,33 +475,36 @@ try {
|
||||
if (isset($aExternalKeysByColumn[$iNumber - 1])) {
|
||||
$sExtKeyName = $aExternalKeysByColumn[$iNumber - 1];
|
||||
$oExtKeyCellStatus = $aResRow[$sExtKeyName];
|
||||
switch (get_class($oExtKeyCellStatus)) {
|
||||
case 'CellStatus_Issue':
|
||||
case 'CellStatus_SearchIssue':
|
||||
case 'CellStatus_NullIssue':
|
||||
case 'CellStatus_Ambiguous':
|
||||
$sCellMessage .= GetDivAlert($oExtKeyCellStatus->GetDescription());
|
||||
break;
|
||||
|
||||
default:
|
||||
// Do nothing
|
||||
}
|
||||
$oCellStatus = $oExtKeyCellStatus;
|
||||
}
|
||||
$sHtmlValue = $oCellStatus->GetDisplayableValue();
|
||||
switch (get_class($oCellStatus)) {
|
||||
case 'CellStatus_Issue':
|
||||
case 'CellStatus_NullIssue':
|
||||
$sCellMessage .= GetDivAlert($oCellStatus->GetDescription());
|
||||
$aTableRow[$sClassName.'/'.$sAttCode] = '<div class="ibo-csv-import--cell-error">'.Dict::Format('UI:CSVReport-Object-Error', $sHtmlValue).$sCellMessage.'</div>';
|
||||
break;
|
||||
|
||||
case 'CellStatus_SearchIssue':
|
||||
$sCellMessage .= GetDivAlert($oCellStatus->GetDescription());
|
||||
$aTableRow[$sClassName.'/'.$sAttCode] = '<div class="ibo-csv-import--cell-error">ERROR: '.$sHtmlValue.$sCellMessage.'</div>';
|
||||
$aTableRow[$sClassName.'/'.$sAttCode] = sprintf("%s%s%s%s%s%s",
|
||||
'<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;
|
||||
|
||||
case 'CellStatus_Ambiguous':
|
||||
$sCellMessage .= GetDivAlert($oCellStatus->GetDescription());
|
||||
$aTableRow[$sClassName.'/'.$sAttCode] = '<div class="ibo-csv-import--cell-error" >'.Dict::Format('UI:CSVReport-Object-Ambiguous', $sHtmlValue).$sCellMessage.'</div>';
|
||||
$aTableRow[$sClassName.'/'.$sAttCode] = sprintf("%s%s%s%s%s%s",
|
||||
'<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;
|
||||
|
||||
case 'CellStatus_Modify':
|
||||
@@ -592,7 +593,7 @@ try {
|
||||
$oMulticolumn->AddColumn(ColumnUIBlockFactory::MakeForBlock($oCheckBoxUnchanged));
|
||||
$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->SetBeforeInput(false);
|
||||
$oCheckBoxUnchanged->GetInput()->AddCSSClass('ibo-input-checkbox');
|
||||
@@ -679,7 +680,7 @@ try {
|
||||
EOF
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
$sErrors = json_encode(Dict::Format('UI:CSVImportError_items', $iErrors));
|
||||
$sCreated = json_encode(Dict::Format('UI:CSVImportCreated_items', $iCreated));
|
||||
$sModified = json_encode(Dict::Format('UI:CSVImportModified_items', $iModified));
|
||||
@@ -774,7 +775,7 @@ EOF
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
/**
|
||||
* Perform the actual load of the CSV data and display the results
|
||||
@@ -798,7 +799,7 @@ EOF
|
||||
$oField->AddSubBlock($oText);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Simulate the load of the CSV data and display the results
|
||||
* @param WebPage $oPage The web page to display the wizard
|
||||
@@ -810,7 +811,7 @@ EOF
|
||||
$oPage->AddSubBlock($oPanel);
|
||||
ProcessCSVData($oPage, true /* simulate */);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Select the mapping between the CSV column and the fields of the objects
|
||||
* @param WebPage $oPage The web page to display the wizard
|
||||
@@ -923,10 +924,10 @@ EOF
|
||||
$aSearchFields = utils::ReadParam('search_field', array(), false, 'field_name');
|
||||
$sFieldsMapping = addslashes(json_encode($aFieldsMapping));
|
||||
$sSearchFields = addslashes(json_encode($aSearchFields));
|
||||
|
||||
|
||||
$oPage->add_ready_script("DoMapping('$sFieldsMapping', '$sSearchFields');"); // There is already a class selected, run the mapping
|
||||
}
|
||||
|
||||
|
||||
$oPage->add_script(
|
||||
<<<EOF
|
||||
var aDefaultKeys = new Array();
|
||||
@@ -1142,7 +1143,7 @@ EOF
|
||||
EOF
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Select the options of the CSV load and check for CSV parsing errors
|
||||
* @param WebPage $oPage The current web page
|
||||
@@ -1166,7 +1167,7 @@ EOF
|
||||
$sCSVData = utils::ReadPostedParam('csvdata', '', 'raw_data');
|
||||
}
|
||||
$sEncoding = utils::ReadParam('encoding', 'UTF-8');
|
||||
|
||||
|
||||
// Compute a subset of the data set, now that we know the charset
|
||||
if ($sEncoding == 'UTF-8')
|
||||
{
|
||||
@@ -1183,7 +1184,7 @@ EOF
|
||||
{
|
||||
$sUTF8Data = iconv($sEncoding, 'UTF-8//IGNORE//TRANSLIT', $sCSVData);
|
||||
}
|
||||
|
||||
|
||||
$aGuesses = GuessParameters($sUTF8Data); // Try to predict the parameters, based on the input data
|
||||
|
||||
$iSkippedLines = utils::ReadParam('nb_skipped_lines', '');
|
||||
@@ -1191,7 +1192,7 @@ EOF
|
||||
$sTextQualifier = utils::ReadParam('text_qualifier', '', false, 'raw_data');
|
||||
if ($sTextQualifier == '') // May be set to an empty value by the previous page
|
||||
{
|
||||
$sTextQualifier = $aGuesses['qualifier'];
|
||||
$sTextQualifier = $aGuesses['qualifier'];
|
||||
}
|
||||
$sOtherTextQualifier = in_array($sTextQualifier, array('"', "'")) ? '' : $sTextQualifier;
|
||||
$bHeaderLine = utils::ReadParam('header_line', 0);
|
||||
@@ -1610,7 +1611,7 @@ EOF
|
||||
null, AjaxTab::ENUM_TAB_PLACEHOLDER_MISC);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
switch($iStep)
|
||||
{
|
||||
case 11:
|
||||
@@ -1618,45 +1619,45 @@ EOF
|
||||
$oPage = new AjaxPage('');
|
||||
BulkChange::DisplayImportHistory($oPage);
|
||||
$oPage->add_ready_script('$("#CSVImportHistory table.listResults").tableHover();');
|
||||
$oPage->add_ready_script('$("#CSVImportHistory table.listResults").tablesorter( { widgets: ["myZebra", "truncatedList"]} );');
|
||||
$oPage->add_ready_script('$("#CSVImportHistory table.listResults").tablesorter( { widgets: ["myZebra", "truncatedList"]} );');
|
||||
break;
|
||||
|
||||
|
||||
case 10:
|
||||
// Case generated by BulkChange::DisplayImportHistory
|
||||
$iChange = (int)utils::ReadParam('changeid', 0);
|
||||
BulkChange::DisplayImportHistoryDetails($oPage, $iChange);
|
||||
break;
|
||||
|
||||
|
||||
case 5:
|
||||
LoadData($oPage);
|
||||
break;
|
||||
|
||||
|
||||
case 4:
|
||||
Preview($oPage);
|
||||
break;
|
||||
|
||||
|
||||
case 3:
|
||||
SelectMapping($oPage);
|
||||
break;
|
||||
|
||||
|
||||
case 2:
|
||||
SelectOptions($oPage);
|
||||
break;
|
||||
|
||||
|
||||
case 1:
|
||||
case 6: // Loop back here when we are done
|
||||
default:
|
||||
Welcome($oPage);
|
||||
}
|
||||
|
||||
|
||||
$oPage->output();
|
||||
}
|
||||
catch(CoreException $e)
|
||||
{
|
||||
require_once(APPROOT.'/setup/setuppage.class.inc.php');
|
||||
$oP = new ErrorPage(Dict::S('UI:PageTitle:FatalError'));
|
||||
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
|
||||
$oP->error(Dict::Format('UI:Error_Details', $e->getHtmlDesc()));
|
||||
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
|
||||
$oP->error(Dict::Format('UI:Error_Details', $e->getHtmlDesc()));
|
||||
$oP->output();
|
||||
|
||||
if (MetaModel::IsLogEnabledIssue())
|
||||
@@ -1684,8 +1685,8 @@ catch(Exception $e)
|
||||
{
|
||||
require_once(APPROOT.'/setup/setuppage.class.inc.php');
|
||||
$oP = new ErrorPage(Dict::S('UI:PageTitle:FatalError'));
|
||||
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
|
||||
$oP->error(Dict::Format('UI:Error_Details', $e->getMessage()));
|
||||
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
|
||||
$oP->error(Dict::Format('UI:Error_Details', $e->getMessage()));
|
||||
$oP->output();
|
||||
|
||||
if (MetaModel::IsLogEnabledIssue())
|
||||
@@ -1705,4 +1706,4 @@ catch(Exception $e)
|
||||
|
||||
IssueLog::Error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,55 +139,58 @@ JS
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
$oFavoriteOrganizationsBlock = new Panel(Dict::S('UI:FavoriteOrganizations'), array(), 'grey', 'ibo-favorite-organizations');
|
||||
$oFavoriteOrganizationsBlock->SetSubTitle(Dict::S('UI:FavoriteOrganizations+'));
|
||||
$oFavoriteOrganizationsBlock->AddCSSClass('ibo-datatable-panel');
|
||||
$oFavoriteOrganizationsForm = new Form();
|
||||
$oFavoriteOrganizationsBlock->AddSubBlock($oFavoriteOrganizationsForm);
|
||||
// Favorite organizations: the organizations listed in the drop-down menu
|
||||
$sOQL = ApplicationMenu::GetFavoriteSiloQuery();
|
||||
$oFilter = DBObjectSearch::FromOQL($sOQL);
|
||||
$oBlock = new DisplayBlock($oFilter, 'list', false);
|
||||
$bIsSiloSelectionEnabled = MetaModel::GetConfig()->Get('navigation_menu.show_organization_filter');
|
||||
if ($bIsSiloSelectionEnabled)
|
||||
{
|
||||
$oFavoriteOrganizationsBlock = new Panel(Dict::S('UI:FavoriteOrganizations'), array(), 'grey', 'ibo-favorite-organizations');
|
||||
$oFavoriteOrganizationsBlock->SetSubTitle(Dict::S('UI:FavoriteOrganizations+'));
|
||||
$oFavoriteOrganizationsBlock->AddCSSClass('ibo-datatable-panel');
|
||||
$oFavoriteOrganizationsForm = new Form();
|
||||
$oFavoriteOrganizationsBlock->AddSubBlock($oFavoriteOrganizationsForm);
|
||||
// Favorite organizations: the organizations listed in the drop-down menu
|
||||
$sOQL = ApplicationMenu::GetFavoriteSiloQuery();
|
||||
$oFilter = DBObjectSearch::FromOQL($sOQL);
|
||||
$oBlock = new DisplayBlock($oFilter, 'list', false);
|
||||
|
||||
$aFavoriteOrgs = appUserPreferences::GetPref('favorite_orgs', null);
|
||||
$aFavoriteOrgs = appUserPreferences::GetPref('favorite_orgs', null);
|
||||
|
||||
$sIdFavoriteOrganizations = 1;
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oBlock->GetDisplay($oP, $sIdFavoriteOrganizations, [
|
||||
'menu' => false,
|
||||
'selection_mode' => true,
|
||||
'selection_type' => 'multiple',
|
||||
'table_id' => 'user_prefs',
|
||||
'surround_with_panel' => false,
|
||||
'selected_rows' => $aFavoriteOrgs,
|
||||
]));
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oAppContext->GetForFormBlock());
|
||||
$sIdFavoriteOrganizations = 1;
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oBlock->GetDisplay($oP, $sIdFavoriteOrganizations, [
|
||||
'menu' => false,
|
||||
'selection_mode' => true,
|
||||
'selection_type' => 'multiple',
|
||||
'table_id' => 'user_prefs',
|
||||
'surround_with_panel' => false,
|
||||
'selected_rows' => $aFavoriteOrgs,
|
||||
]));
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oAppContext->GetForFormBlock());
|
||||
|
||||
// Button toolbar
|
||||
$oFavoriteOrganizationsToolBar = ToolbarUIBlockFactory::MakeForButton(null, ['ibo-is-fullwidth']);
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oFavoriteOrganizationsToolBar);
|
||||
// Button toolbar
|
||||
$oFavoriteOrganizationsToolBar = ToolbarUIBlockFactory::MakeForButton(null, ['ibo-is-fullwidth']);
|
||||
$oFavoriteOrganizationsForm->AddSubBlock($oFavoriteOrganizationsToolBar);
|
||||
|
||||
// - Cancel button
|
||||
$oFavoriteOrganizationsCancelButton = ButtonUIBlockFactory::MakeForCancel(Dict::S('UI:Button:Cancel'));
|
||||
$oFavoriteOrganizationsToolBar->AddSubBlock($oFavoriteOrganizationsCancelButton);
|
||||
$oFavoriteOrganizationsCancelButton->SetOnClickJsCode("window.location.href = '$sURL'");
|
||||
// - Submit button
|
||||
$oFavoriteOrganizationsSubmitButton = ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('UI:Button:Apply'), 'operation', 'apply', true);
|
||||
$oFavoriteOrganizationsToolBar->AddSubBlock($oFavoriteOrganizationsSubmitButton);
|
||||
// - Cancel button
|
||||
$oFavoriteOrganizationsCancelButton = ButtonUIBlockFactory::MakeForCancel(Dict::S('UI:Button:Cancel'));
|
||||
$oFavoriteOrganizationsToolBar->AddSubBlock($oFavoriteOrganizationsCancelButton);
|
||||
$oFavoriteOrganizationsCancelButton->SetOnClickJsCode("window.location.href = '$sURL'");
|
||||
// - Submit button
|
||||
$oFavoriteOrganizationsSubmitButton = ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('UI:Button:Apply'), 'operation', 'apply', true);
|
||||
$oFavoriteOrganizationsToolBar->AddSubBlock($oFavoriteOrganizationsSubmitButton);
|
||||
|
||||
// TODO 3.0 have this code work again, currently it prevents the display of favorite organizations and shortcuts.
|
||||
// if ($aFavoriteOrgs == null) {
|
||||
// // All checked
|
||||
// $oP->add_ready_script(
|
||||
// <<<JS
|
||||
// $('#$sIdFavoriteOrganizations.checkAll').prop('checked', true);
|
||||
// checkAllDataTable('datatable_$sIdFavoriteOrganizations',true,'$sIdFavoriteOrganizations');
|
||||
//JS
|
||||
// );
|
||||
//
|
||||
// }
|
||||
|
||||
$oContentLayout->AddMainBlock($oFavoriteOrganizationsBlock);
|
||||
// TODO 3.0 have this code work again, currently it prevents the display of favorite organizations and shortcuts.
|
||||
// if ($aFavoriteOrgs == null) {
|
||||
// // All checked
|
||||
// $oP->add_ready_script(
|
||||
// <<<JS
|
||||
// $('#$sIdFavoriteOrganizations.checkAll').prop('checked', true);
|
||||
// checkAllDataTable('datatable_$sIdFavoriteOrganizations',true,'$sIdFavoriteOrganizations');
|
||||
//JS
|
||||
// );
|
||||
//
|
||||
// }
|
||||
|
||||
$oContentLayout->AddMainBlock($oFavoriteOrganizationsBlock);
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Shortcuts
|
||||
|
||||
@@ -205,11 +205,12 @@ class DBBackup
|
||||
*
|
||||
* @param string $sSourceConfigFile
|
||||
* @param string $sTmpFolder
|
||||
* @param bool $bSkipSQLDumpForTesting
|
||||
*
|
||||
* @return array list of files to archive
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function PrepareFilesToBackup($sSourceConfigFile, $sTmpFolder)
|
||||
protected function PrepareFilesToBackup($sSourceConfigFile, $sTmpFolder, $bSkipSQLDumpForTesting = false)
|
||||
{
|
||||
$aRet = array();
|
||||
if (is_dir($sTmpFolder))
|
||||
@@ -226,7 +227,7 @@ class DBBackup
|
||||
{
|
||||
$sFile = $sTmpFolder.'/config-itop.php';
|
||||
$this->LogInfo("backup: adding resource '$sSourceConfigFile'");
|
||||
copy($sSourceConfigFile, $sFile);
|
||||
@copy($sSourceConfigFile, $sFile); // During unattended install config file may be absent
|
||||
$aRet[] = $sFile;
|
||||
}
|
||||
|
||||
@@ -247,9 +248,41 @@ class DBBackup
|
||||
SetupUtils::copydir($sExtraDir, $sFile);
|
||||
$aRet[] = $sFile;
|
||||
}
|
||||
$sDataFile = $sTmpFolder.'/itop-dump.sql';
|
||||
$this->DoBackup($sDataFile);
|
||||
$aRet[] = $sDataFile;
|
||||
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';
|
||||
$this->DoBackup($sDataFile);
|
||||
$aRet[] = $sDataFile;
|
||||
}
|
||||
|
||||
return $aRet;
|
||||
}
|
||||
|
||||
@@ -23,15 +23,15 @@ use Combodo\iTop\DesignElement;
|
||||
|
||||
require_once(APPROOT.'setup/setuputils.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');
|
||||
|
||||
|
||||
class DOMFormatException extends Exception
|
||||
{
|
||||
/**
|
||||
* Overrides the Exception default constructor to automatically add informations about the concerned node (path and
|
||||
* line number)
|
||||
*
|
||||
*
|
||||
* @param string $message
|
||||
* @param $code
|
||||
* @param $previous
|
||||
@@ -49,7 +49,7 @@ class DOMFormatException extends Exception
|
||||
|
||||
/**
|
||||
* Compiler class
|
||||
*/
|
||||
*/
|
||||
class MFCompiler
|
||||
{
|
||||
const DATA_PRECOMPILED_FOLDER = 'data'.DIRECTORY_SEPARATOR.'precompiled_styles'.DIRECTORY_SEPARATOR;
|
||||
@@ -315,7 +315,7 @@ class MFCompiler
|
||||
apc_clear_cache();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Perform the actual "Compilation" of all modules
|
||||
@@ -327,21 +327,16 @@ class MFCompiler
|
||||
*/
|
||||
protected function DoCompile($sTempTargetDir, $sFinalTargetDir, $oP = null, $bUseSymbolicLinks = false)
|
||||
{
|
||||
$aAllClasses = array(); // flat list of classes
|
||||
$aModulesInfo = array(); // Hash array of module_name => array('version' => string, 'root_dir' => string)
|
||||
$aAllClasses = []; // flat list of classes
|
||||
$aModulesInfo = []; // Hash array of module_name => array('version' => string, 'root_dir' => string)
|
||||
|
||||
// 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
|
||||
// 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);
|
||||
|
||||
$oParentMenuNodeCompiler->LoadModuleMenuInfo($aModules);
|
||||
foreach ($aModules as $foo => $oModule) {
|
||||
$sModuleName = $oModule->GetName();
|
||||
$sModuleVersion = $oModule->GetVersion();
|
||||
@@ -418,7 +414,7 @@ class MFCompiler
|
||||
$sCompiledCode .= $this->CompileConstant($oConstant)."\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (array_key_exists($sModuleName, $this->aSnippets))
|
||||
{
|
||||
foreach( $this->aSnippets[$sModuleName]['before'] as $aSnippet)
|
||||
@@ -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");
|
||||
}
|
||||
@@ -481,75 +477,19 @@ class $sMenuCreationClass extends ModuleHandlerAPI
|
||||
global \$__comp_menus__; // ensure that the global variable is indeed global !
|
||||
|
||||
EOF;
|
||||
// Preliminary: determine parent menus not defined within the current module
|
||||
$aMenusToLoad = array();
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
$oParentMenuNodeCompiler->CompileModuleMenus($oModule, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP);
|
||||
|
||||
$sIndent = "\t\t";
|
||||
foreach ($aMenuLinesForAll as $sPHPLine)
|
||||
foreach ($oParentMenuNodeCompiler->GetMenuLinesForAll() as $sPHPLine)
|
||||
{
|
||||
$sCompiledCode .= $sIndent.$sPHPLine."\n";
|
||||
}
|
||||
if (count($aMenuLinesForAdmins) > 0)
|
||||
if (count($oParentMenuNodeCompiler->GetMenuLinesForAdmins()) > 0)
|
||||
{
|
||||
$sCompiledCode .= $sIndent."if (UserRights::IsAdministrator())\n";
|
||||
$sCompiledCode .= $sIndent."{\n";
|
||||
foreach ($aMenuLinesForAdmins as $sPHPLine)
|
||||
foreach ($oParentMenuNodeCompiler->GetMenuLinesForAdmins() as $sPHPLine)
|
||||
{
|
||||
$sCompiledCode .= $sIndent."\t".$sPHPLine."\n";
|
||||
}
|
||||
@@ -581,7 +521,7 @@ EOF;
|
||||
$sCompiledCode .= $aSnippet['content']."\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create (overwrite if existing) the compiled file
|
||||
//
|
||||
if (strlen($sCompiledCode) > 0)
|
||||
@@ -625,7 +565,7 @@ EOF;
|
||||
$aWebservicesFiles[] = "MetaModel::IncludeModule(MODULESROOT.'/$sRelativeDir/$sRelFileName');";
|
||||
}
|
||||
} // foreach module
|
||||
|
||||
|
||||
// Compile the dictionaries -out of the modules
|
||||
//
|
||||
$sDictDir = $sTempTargetDir.'/dictionaries';
|
||||
@@ -654,7 +594,7 @@ EOF;
|
||||
$this->sMainPHPCode .= $aSnippet['content']."\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Compile the portals
|
||||
$oPortalsNode = $this->oFactory->GetNodes('/itop_design/portals')->item(0);
|
||||
$this->CompilePortals($oPortalsNode, $sTempTargetDir, $sFinalTargetDir);
|
||||
@@ -666,7 +606,7 @@ EOF;
|
||||
// Compile the XML parameters
|
||||
$oParametersNode = $this->oFactory->GetNodes('/itop_design/module_parameters')->item(0);
|
||||
$this->CompileParameters($oParametersNode, $sTempTargetDir, $sFinalTargetDir);
|
||||
|
||||
|
||||
if (array_key_exists('_core_', $this->aSnippets))
|
||||
{
|
||||
foreach( $this->aSnippets['_core_']['after'] as $aSnippet)
|
||||
@@ -700,7 +640,7 @@ EOF;
|
||||
$sCurrDate = date(DATE_ISO8601);
|
||||
// Autoload
|
||||
$sPHPFile = $sTempTargetDir.'/autoload.php';
|
||||
$sPHPFileContent =
|
||||
$sPHPFileContent =
|
||||
<<<EOF
|
||||
<?php
|
||||
//
|
||||
@@ -709,7 +649,7 @@ EOF;
|
||||
//
|
||||
EOF
|
||||
;
|
||||
|
||||
|
||||
$sPHPFileContent .= "\nMetaModel::IncludeModule(MODULESROOT.'/core/main.php');\n";
|
||||
$sPHPFileContent .= implode("\n", $aDataModelFiles);
|
||||
$sPHPFileContent .= implode("\n", $aWebservicesFiles);
|
||||
@@ -717,14 +657,14 @@ EOF
|
||||
$sModulesInfo = str_replace("'".$sRelFinalTargetDir."/", "\$sCurrEnv.'/", $sModulesInfo);
|
||||
$sPHPFileContent .= "\nfunction GetModulesInfo()\n{\n\$sCurrEnv = 'env-'.utils::GetCurrentEnvironment();\nreturn ".$sModulesInfo.";\n}\n";
|
||||
file_put_contents($sPHPFile, $sPHPFileContent);
|
||||
|
||||
|
||||
} // DoCompile()
|
||||
|
||||
/**
|
||||
* Helper to form a valid ZList from the array built by GetNodeAsArrayOfItems()
|
||||
*
|
||||
* @param array $aItems
|
||||
*/
|
||||
*/
|
||||
protected function ArrayOfItemsToZList(&$aItems)
|
||||
{
|
||||
// Note: $aItems can be null in some cases so we have to protect it otherwise a PHP warning will be thrown during the foreach
|
||||
@@ -756,7 +696,7 @@ EOF
|
||||
* Helper to format the flags for an attribute, in a given state
|
||||
* @param object $oAttNode DOM node containing the information to build the flags
|
||||
* Returns string PHP flags, based on the OPT_ATT_ constants, or empty (meaning 0, can be omitted)
|
||||
*/
|
||||
*/
|
||||
protected function FlagsToPHP($oAttNode)
|
||||
{
|
||||
static $aNodeAttributeToFlag = array(
|
||||
@@ -766,7 +706,7 @@ EOF
|
||||
'must_change' => 'OPT_ATT_MUSTCHANGE',
|
||||
'hidden' => 'OPT_ATT_HIDDEN',
|
||||
);
|
||||
|
||||
|
||||
$aFlags = array();
|
||||
foreach ($aNodeAttributeToFlag as $sNodeAttribute => $sFlag)
|
||||
{
|
||||
@@ -778,7 +718,7 @@ EOF
|
||||
}
|
||||
if (empty($aFlags))
|
||||
{
|
||||
$aFlags[] = 'OPT_ATT_NORMAL'; // When no flag is defined, reset the state to "normal"
|
||||
$aFlags[] = 'OPT_ATT_NORMAL'; // When no flag is defined, reset the state to "normal"
|
||||
}
|
||||
$sRes = implode(' | ', $aFlags);
|
||||
return $sRes;
|
||||
@@ -800,7 +740,7 @@ EOF
|
||||
'details' => 'LINKSET_TRACKING_DETAILS',
|
||||
'all' => 'LINKSET_TRACKING_ALL',
|
||||
);
|
||||
|
||||
|
||||
static $aXmlToPHP_Others = array(
|
||||
'none' => 'ATTRIBUTE_TRACKING_NONE',
|
||||
'all' => 'ATTRIBUTE_TRACKING_ALL',
|
||||
@@ -841,7 +781,7 @@ EOF
|
||||
'in_place' => 'LINKSET_EDITMODE_INPLACE',
|
||||
'add_remove' => 'LINKSET_EDITMODE_ADDREMOVE',
|
||||
);
|
||||
|
||||
|
||||
if (!array_key_exists($sEditMode, $aXmlToPHP))
|
||||
{
|
||||
throw new DOMFormatException("Edit mode: unknown value '$sEditMode'");
|
||||
@@ -849,10 +789,10 @@ EOF
|
||||
return $aXmlToPHP[$sEditMode];
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Format a path (file or url) as an absolute path or relative to the module or the app
|
||||
*/
|
||||
*/
|
||||
protected function PathToPHP($sPath, $sModuleRelativeDir, $bIsUrl = false)
|
||||
{
|
||||
if ($sPath == '')
|
||||
@@ -945,7 +885,7 @@ EOF
|
||||
else
|
||||
{
|
||||
throw new DOMFormatException("missing (or empty) mandatory tag '$sTag' under the tag '".$oNode->nodeName."'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1049,7 +989,7 @@ EOF
|
||||
|
||||
/**
|
||||
* Adds quotes and escape characters
|
||||
*/
|
||||
*/
|
||||
protected function QuoteForPHP($sStr, $bSimpleQuotes = false)
|
||||
{
|
||||
if ($bSimpleQuotes)
|
||||
@@ -1084,7 +1024,7 @@ EOF
|
||||
$sScalar = (string)(int)$sText;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'float':
|
||||
if (is_null($sText))
|
||||
{
|
||||
@@ -1096,7 +1036,7 @@ EOF
|
||||
$sScalar = (string)(float)$sText;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'bool':
|
||||
if (is_null($sText))
|
||||
{
|
||||
@@ -1328,7 +1268,7 @@ EOF
|
||||
// $oField
|
||||
$sAttCode = $oField->getAttribute('id');
|
||||
$sAttType = $oField->getAttribute('xsi:type');
|
||||
|
||||
|
||||
$aDependencies = array();
|
||||
$oDependencies = $oField->GetOptionalElement('dependencies');
|
||||
if (!is_null($oDependencies))
|
||||
@@ -1340,9 +1280,9 @@ EOF
|
||||
}
|
||||
}
|
||||
$sDependencies = 'array('.implode(', ', $aDependencies).')';
|
||||
|
||||
|
||||
$aParameters = array();
|
||||
|
||||
|
||||
if ($sAttType == 'AttributeLinkedSetIndirect')
|
||||
{
|
||||
$aParameters['linked_class'] = $this->GetMandatoryPropString($oField, 'linked_class');
|
||||
@@ -2416,7 +2356,7 @@ CSS;
|
||||
* @return array
|
||||
* @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);
|
||||
|
||||
@@ -2512,11 +2452,11 @@ CSS;
|
||||
case '1':
|
||||
$sSearchFormOpen = 'true';
|
||||
break;
|
||||
|
||||
|
||||
case '0':
|
||||
$sSearchFormOpen = 'false';
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
$sSearchFormOpen = 'true';
|
||||
}
|
||||
@@ -2605,7 +2545,7 @@ CSS;
|
||||
foreach($this->oFactory->ListFields($oClass) as $oField)
|
||||
{
|
||||
$sAttType = $oField->getAttribute('xsi:type');
|
||||
|
||||
|
||||
if (($sAttType == 'AttributeExternalKey') || ($sAttType == 'AttributeHierarchicalKey'))
|
||||
{
|
||||
$sOnTargetDel = $oField->GetChildText('on_target_delete');
|
||||
@@ -2631,7 +2571,7 @@ CSS;
|
||||
$oClasses = $oGroup->GetUniqueElement('classes');
|
||||
foreach($oClasses->getElementsByTagName('class') as $oClass)
|
||||
{
|
||||
|
||||
|
||||
$sClass = $oClass->getAttribute("id");
|
||||
$aClasses[] = $sClass;
|
||||
|
||||
@@ -2649,7 +2589,7 @@ CSS;
|
||||
$aProfiles[1] = array(
|
||||
'name' => 'Administrator',
|
||||
'description' => 'Has the rights on everything (bypassing any control)',
|
||||
);
|
||||
);
|
||||
|
||||
$aGrants = array();
|
||||
$oProfiles = $oUserRightsNode->GetUniqueElement('profiles');
|
||||
@@ -2681,7 +2621,7 @@ CSS;
|
||||
}
|
||||
$sGrant = $oAction->GetText();
|
||||
$bGrant = ($sGrant == 'allow');
|
||||
|
||||
|
||||
if ($sGroupId == '*')
|
||||
{
|
||||
$aGrantClasses = array('*');
|
||||
@@ -2920,7 +2860,7 @@ Dict::SetLanguagesList(
|
||||
$sLanguagesDump
|
||||
);
|
||||
EOF;
|
||||
|
||||
|
||||
file_put_contents($sLanguagesFile, $sLanguagesFileContent);
|
||||
}
|
||||
|
||||
@@ -2957,7 +2897,7 @@ EOF;
|
||||
{
|
||||
throw new DOMFormatException('Could not find the file with ref '.$sFileId);
|
||||
}
|
||||
|
||||
|
||||
$sName = $oNodes->item(0)->GetChildText('name');
|
||||
$sData = base64_decode($oNodes->item(0)->GetChildText('data'));
|
||||
$aPathInfo = pathinfo($sName);
|
||||
@@ -2971,7 +2911,7 @@ EOF;
|
||||
}
|
||||
$oParentNode = $oFileRef->parentNode;
|
||||
$oParentNode->removeChild($oFileRef);
|
||||
|
||||
|
||||
$oTextNode = $oParentNode->ownerDocument->createTextNode($sRelativePath.'/images/'.$sFile);
|
||||
$oParentNode->appendChild($oTextNode);
|
||||
}
|
||||
@@ -3284,8 +3224,8 @@ EOF;
|
||||
{
|
||||
SetupUtils::rrmdir($sTempTargetDir.'/branding/images');
|
||||
}
|
||||
|
||||
// Compile themes
|
||||
|
||||
// Compile themes
|
||||
$this->CompileThemes($oBrandingNode, $sTempTargetDir);
|
||||
}
|
||||
}
|
||||
@@ -3326,11 +3266,11 @@ EOF;
|
||||
{
|
||||
$aPortalsConfig[$sPortalId]['deny'][] = $oProfile->getAttribute('id');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
uasort($aPortalsConfig, array(get_class($this), 'SortOnRank'));
|
||||
|
||||
|
||||
$this->sMainPHPCode .= "\n";
|
||||
$this->sMainPHPCode .= "/**\n";
|
||||
$this->sMainPHPCode .= " * Portal(s) definition(s) extracted from the XML definition at compile time\n";
|
||||
@@ -3373,7 +3313,7 @@ EOF;
|
||||
$oParamsReader = new MFParameters($oParams);
|
||||
$aParametersConfig[$sModuleId] = $oParamsReader->GetAll();
|
||||
}
|
||||
|
||||
|
||||
$this->sMainPHPCode .= "\n";
|
||||
$this->sMainPHPCode .= "/**\n";
|
||||
$this->sMainPHPCode .= " * Modules parameters extracted from the XML definition at compile time\n";
|
||||
|
||||
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\UIBlock;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
@@ -45,6 +46,11 @@ class Field extends UIContentBlock
|
||||
protected $sValueRaw;
|
||||
/** @var string */
|
||||
protected $sLabel;
|
||||
/**
|
||||
* @var string
|
||||
* @since 3.1.0
|
||||
*/
|
||||
protected $sDescription = '';
|
||||
/** @var string */
|
||||
protected $sValueId;
|
||||
|
||||
@@ -354,4 +360,34 @@ class Field extends UIContentBlock
|
||||
|
||||
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
|
||||
* @param string $sLabel
|
||||
* @param string $sValueHtml
|
||||
* @param string $sDescription
|
||||
*
|
||||
* @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->SetDescription($sDescription);
|
||||
$oField->SetLayout(Field::ENUM_FIELD_LAYOUT_LARGE);
|
||||
return $oField;
|
||||
}
|
||||
@@ -119,12 +121,14 @@ class FieldUIBlockFactory extends AbstractUIBlockFactory
|
||||
* @api
|
||||
* @param string $sLabel
|
||||
* @param string $sValueHtml
|
||||
* @param string $sDescription
|
||||
*
|
||||
* @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->SetDescription($sDescription);
|
||||
$oField->SetLayout(Field::ENUM_FIELD_LAYOUT_SMALL);
|
||||
return $oField;
|
||||
}
|
||||
@@ -134,14 +138,16 @@ class FieldUIBlockFactory extends AbstractUIBlockFactory
|
||||
* @param string $sLabel
|
||||
* @param string $sLayout
|
||||
* @param string|null $sId
|
||||
* @param string $sDescription
|
||||
*
|
||||
* @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->SetDescription($sDescription);
|
||||
$oField->SetLayout($sLayout);
|
||||
return $oField;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,16 @@ class NewsroomMenuFactory
|
||||
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
|
||||
*
|
||||
|
||||
@@ -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 Dict;
|
||||
use iPopupMenuExtension;
|
||||
use JSPopupMenuItem;
|
||||
use MetaModel;
|
||||
use SeparatorPopupMenuItem;
|
||||
use URLPopupMenuItem;
|
||||
use iPopupMenuExtension;
|
||||
use UserRights;
|
||||
use utils;
|
||||
|
||||
@@ -56,30 +57,68 @@ class PopoverMenuFactory
|
||||
->SetHorizontalPosition(PopoverMenu::ENUM_HORIZONTAL_POSITION_ALIGN_OUTER_RIGHT)
|
||||
->SetVerticalPosition(PopoverMenu::ENUM_VERTICAL_POSITION_ABOVE);
|
||||
|
||||
$aUserMenuItems = [];
|
||||
|
||||
// Allowed portals
|
||||
$aAllowedPortalsItems = static::PrepareAllowedPortalsItemsForUserMenu();
|
||||
if (!empty($aAllowedPortalsItems)) {
|
||||
$oMenu->AddSection('allowed_portals')
|
||||
->SetItems('allowed_portals', $aAllowedPortalsItems);
|
||||
}
|
||||
self::AddPopoverMenuItems($aAllowedPortalsItems, $aUserMenuItems);
|
||||
|
||||
// User related pages
|
||||
$oMenu->AddSection('user_related')
|
||||
->SetItems('user_related', static::PrepareUserRelatedItemsForUserMenu());
|
||||
self::AddPopoverMenuItems(static::PrepareUserRelatedItemsForUserMenu(), $aUserMenuItems);
|
||||
|
||||
// API: iPopupMenuExtension::MENU_USER_ACTIONS
|
||||
$aAPIItems = static::PrepareAPIItemsForUserMenu($oMenu);
|
||||
if (count($aAPIItems) > 0) {
|
||||
$oMenu->AddSection('popup_menu_extension-menu_user_actions')
|
||||
->SetItems('popup_menu_extension-menu_user_actions', $aAPIItems);
|
||||
}
|
||||
self::AddPopoverMenuItems($aAPIItems, $aUserMenuItems);
|
||||
|
||||
// Misc links
|
||||
$oMenu->AddSection('misc')
|
||||
->SetItems('misc', static::PrepareMiscItemsForUserMenu());
|
||||
/*$oMenu->AddSection('misc')
|
||||
->SetItems('misc', static::PrepareMiscItemsForUserMenu());*/
|
||||
self::AddPopoverMenuItems(static::PrepareMiscItemsForUserMenu(), $aUserMenuItems);
|
||||
|
||||
self::SortPopoverMenuItems($aUserMenuItems);
|
||||
|
||||
$oMenu->AddSection('misc')
|
||||
->AddItems('misc', $aUserMenuItems);
|
||||
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
|
||||
@@ -273,4 +312,4 @@ class PopoverMenuFactory
|
||||
|
||||
return $oMenu;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
namespace Combodo\iTop\Application\UI\Base\Layout\NavigationMenu;
|
||||
|
||||
|
||||
use ApplicationContext;
|
||||
use ApplicationMenu;
|
||||
use appUserPreferences;
|
||||
@@ -35,6 +34,7 @@ use MetaModel;
|
||||
use UIExtKeyWidget;
|
||||
use UserRights;
|
||||
use utils;
|
||||
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\NewsroomMenu\NewsroomMenuFactory;
|
||||
|
||||
/**
|
||||
* Class NavigationMenu
|
||||
@@ -202,7 +202,7 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
@@ -275,7 +275,7 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
|
||||
*/
|
||||
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
|
||||
* @throws \CoreException
|
||||
@@ -307,6 +315,10 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
|
||||
$this->bHasSiloSelected = false;
|
||||
$this->sSiloLabel = null;
|
||||
|
||||
if (! $this->IsSiloSelectionEnabled()){
|
||||
return;
|
||||
}
|
||||
|
||||
//TODO 3.0 Use components if we have the time to build select/autocomplete components before release
|
||||
// List of visible Organizations
|
||||
$iCount = 0;
|
||||
@@ -349,7 +361,7 @@ class NavigationMenu extends UIBlock implements iKeyboardShortcut
|
||||
$this->aSiloSelection['html'] = '<form data-role="ibo-navigation-menu--silo-selection--form" action="'.utils::GetAbsoluteUrlAppRoot().'pages/UI.php">'; //<select class="org_combo" name="c[org_id]" title="Pick an organization" onChange="this.form.submit();">';
|
||||
|
||||
$oPage = new \CaptureWebPage();
|
||||
|
||||
|
||||
$oWidget = new UIExtKeyWidget('Organization', 'org_id', '', true /* search mode */);
|
||||
$iMaxComboLength = MetaModel::GetConfig()->Get('max_combo_length');
|
||||
$this->aSiloSelection['html'] .= $oWidget->DisplaySelect($oPage, $iMaxComboLength, false, Dict::S('UI:Layout:NavigationMenu:Silo:Label'), $oSet, $iCurrentOrganization, false, 'c[org_id]', '',
|
||||
@@ -382,7 +394,7 @@ $sAddClearButton
|
||||
JS;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compute if the menu is expanded or collapsed
|
||||
*
|
||||
@@ -502,4 +514,4 @@ JS;
|
||||
$this->bShowMenusCount = $bShowMenusCount;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class NavigationMenuFactory
|
||||
{
|
||||
|
||||
$oNewsroomMenu = null;
|
||||
if (MetaModel::GetConfig()->Get('newsroom_enabled'))
|
||||
if (MetaModel::GetConfig()->Get('newsroom_enabled') && NewsroomMenuFactory::HasProviders())
|
||||
{
|
||||
$oNewsroomMenu = NewsroomMenuFactory::MakeNewsroomMenuForNavigationMenu();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
{% endif %}
|
||||
>
|
||||
<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.GetComments() %}
|
||||
<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 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 class="ibo-welcome-popup--text--options">
|
||||
<input type="checkbox" checked id="display_welcome_popup"/><label for="display_welcome_popup">{{'UI:DisplayThisMessageAtStartup'| dict_s}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="welcome_popup_dialog" class="ibo-welcome-popup--dialog ibo-is-hidden">
|
||||
<div class="ibo-welcome-popup--content">
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
</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>
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,38 @@
|
||||
$('#welcome_popup').dialog( { width:'60%', height: 'auto', title: '{{ 'UI:WelcomeMenu:Title'|dict_s }}', autoOpen: true, modal:true,
|
||||
close: function() {
|
||||
var bDisplay = $('#display_welcome_popup:checked').length;
|
||||
SetUserPreference('welcome_popup', bDisplay, true);
|
||||
},
|
||||
buttons: [{
|
||||
text: "{{ 'UI:Button:Ok'|dict_s }}", click: function() {
|
||||
$(this).dialog( "close" ); $(this).remove();
|
||||
}}],
|
||||
$('#welcome_popup_dialog').removeClass('ibo-is-hidden');
|
||||
$('#welcome_popup_dialog').dialog({
|
||||
modal: true,
|
||||
width: '60%',
|
||||
autoOpen: true,
|
||||
title: $('div[data_role=welcome-popup-title]').first().attr('data-title'),
|
||||
close: function() { $('#welcome_popup_dialog').remove(); }
|
||||
});
|
||||
$('.ui-widget-overlay').click(function() { $('#welcome_popup_dialog').dialog('close'); } );
|
||||
$('.ibo-welcome-popup--indicator').click(function() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
if ($('#welcome_popup').height() > ($(window).height()-70))
|
||||
{
|
||||
$('#welcome_popup').height($(window).height()-70);
|
||||
}
|
||||
|
||||
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 CMDBSource;
|
||||
use CMDBObject;
|
||||
use CMDBSource;
|
||||
use Contact;
|
||||
use DBObject;
|
||||
use DBObjectSet;
|
||||
@@ -70,6 +70,7 @@ define('TAG_ATTCODE', 'domains');
|
||||
class ItopDataTestCase extends ItopTestCase
|
||||
{
|
||||
private $iTestOrgId;
|
||||
|
||||
// For cleanup
|
||||
private $aCreatedObjects = array();
|
||||
|
||||
@@ -416,10 +417,25 @@ class ItopDataTestCase extends ItopTestCase
|
||||
* @param string $sLogin
|
||||
* @param int $iProfileId
|
||||
*
|
||||
* @return \DBObject
|
||||
* @return \UserLocal
|
||||
* @throws Exception
|
||||
*/
|
||||
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)){
|
||||
$sPassword = $sLogin;
|
||||
@@ -429,8 +445,8 @@ class ItopDataTestCase extends ItopTestCase
|
||||
$oUserProfile->Set('profileid', $iProfileId);
|
||||
$oUserProfile->Set('reason', 'UNIT Tests');
|
||||
$oSet = DBObjectSet::FromObject($oUserProfile);
|
||||
/** @var \UserLocal $oUser */
|
||||
$oUser = $this->createObject('UserLocal', array(
|
||||
'contactid' => $iContactid,
|
||||
'login' => $sLogin,
|
||||
'password' => $sPassword,
|
||||
'language' => 'EN US',
|
||||
@@ -455,8 +471,8 @@ class ItopDataTestCase extends ItopTestCase
|
||||
$oUserProfile->Set('reason', 'UNIT Tests');
|
||||
/** @var DBObjectSet $oSet */
|
||||
$oSet = $oUser->Get('profile_list');
|
||||
$oSet->AddObject($oUserProfile);
|
||||
$oUser = $this->updateObject('UserLocal', $oUser->GetKey(), array(
|
||||
$oSet->AddItem($oUserProfile);
|
||||
$oUser = $this->updateObject(\User::class, $oUser->GetKey(), array(
|
||||
'profile_list' => $oSet,
|
||||
));
|
||||
$this->debug("Updated {$oUser->GetName()} ({$oUser->GetKey()})");
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace Combodo\iTop\Test\UnitTest\Integration;
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopTestCase;
|
||||
|
||||
use Dict;
|
||||
|
||||
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);
|
||||
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
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -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;
|
||||
$sImpersonatedLogin = "impersonated-user-".$sUid;
|
||||
|
||||
$iAdminUserId = $this->CreateUserForImpersonation($sAdminLogin, 'Administrator', 'AdminName', 'AdminSurName');
|
||||
$this->CreateUserForImpersonation($sImpersonatedLogin, 'Configuration Manager', 'ImpersonatedName', 'ImpersonatedSurName');
|
||||
$oAdminUser = $this->CreateUserForImpersonation($sAdminLogin, 'Administrator', 'AdminName', 'AdminSurName');
|
||||
$oImpersonatedUser = $this->CreateUserForImpersonation($sImpersonatedLogin, 'Configuration Manager', 'ImpersonatedName', 'ImpersonatedSurName');
|
||||
|
||||
$_SESSION = [];
|
||||
\UserRights::Login($sAdminLogin);
|
||||
@@ -124,28 +124,31 @@ class CMDBObjectTest extends ItopDataTestCase
|
||||
if (is_null($sTrackInfo)){
|
||||
CMDBObject::SetTrackInfo(null);
|
||||
} else {
|
||||
$sTrackInfo = $this->ReplaceByFriendlyNames($sTrackInfo, $oAdminUser, $oImpersonatedUser);
|
||||
CMDBObject::SetTrackInfo($sTrackInfo);
|
||||
}
|
||||
|
||||
$this->CreateSimpleObject();
|
||||
if (is_null($sTrackInfo)){
|
||||
self::assertEquals("AdminSurName AdminName", CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
self::assertEquals($oAdminUser->GetFriendlyName(), CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
'TrackInfo : no impersonation');
|
||||
} else {
|
||||
self::assertEquals($sTrackInfo, CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
'TrackInfo : no impersonation');
|
||||
}
|
||||
self::assertEquals($iAdminUserId, CMDBObject::GetCurrentChange()->Get('user_id'),
|
||||
self::assertEquals($oAdminUser->GetKey(), CMDBObject::GetCurrentChange()->Get('user_id'),
|
||||
'TrackInfo : admin userid');
|
||||
|
||||
\UserRights::Impersonate($sImpersonatedLogin);
|
||||
$this->CreateSimpleObject();
|
||||
|
||||
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');
|
||||
} else {
|
||||
self::assertEquals($sExpectedChangeLogWhenImpersonation, CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
$sExpectedMsg = $this->ReplaceByFriendlyNames($sExpectedChangeLogWhenImpersonation, $oAdminUser, $oImpersonatedUser);
|
||||
self::assertEquals($sExpectedMsg, CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
'TrackInfo : impersonation');
|
||||
}
|
||||
|
||||
@@ -155,13 +158,13 @@ class CMDBObjectTest extends ItopDataTestCase
|
||||
\UserRights::Deimpersonate();
|
||||
$this->CreateSimpleObject();
|
||||
if (is_null($sTrackInfo)){
|
||||
self::assertEquals("AdminSurName AdminName", CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
self::assertEquals($oAdminUser->GetFriendlyName(), CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
'TrackInfo : no impersonation');
|
||||
} else {
|
||||
self::assertEquals($sTrackInfo, CMDBObject::GetCurrentChange()->Get('userinfo'),
|
||||
'TrackInfo : no impersonation');
|
||||
}
|
||||
self::assertEquals($iAdminUserId, CMDBObject::GetCurrentChange()->Get('user_id'),
|
||||
self::assertEquals($oAdminUser->GetKey(), CMDBObject::GetCurrentChange()->Get('user_id'),
|
||||
'TrackInfo : admin userid');
|
||||
|
||||
// restore initial conditions
|
||||
@@ -169,6 +172,12 @@ class CMDBObjectTest extends ItopDataTestCase
|
||||
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(){
|
||||
/** @var \DocumentWeb $oTestObject */
|
||||
$oTestObject = MetaModel::NewObject('DocumentWeb');
|
||||
@@ -178,7 +187,7 @@ class CMDBObjectTest extends ItopDataTestCase
|
||||
$oTestObject->DBWrite();
|
||||
}
|
||||
|
||||
private function CreateUserForImpersonation($sLogin, $sProfileName, $sName, $sSurname): int {
|
||||
private function CreateUserForImpersonation($sLogin, $sProfileName, $sName, $sSurname): \UserLocal {
|
||||
/** @var \Person $oPerson */
|
||||
$oPerson = $this->createObject('Person', array(
|
||||
'name' => $sName,
|
||||
@@ -187,8 +196,9 @@ class CMDBObjectTest extends ItopDataTestCase
|
||||
));
|
||||
|
||||
$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());
|
||||
|
||||
return $oUser->GetKey();
|
||||
return $oUser;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ if (utils::IsModeCLI())
|
||||
{
|
||||
// Next steps:
|
||||
// specific arguments: 'csvfile'
|
||||
//
|
||||
//
|
||||
$sAuthUser = ReadMandatoryParam($oP, 'auth_user', 'raw_data');
|
||||
$sAuthPwd = ReadMandatoryParam($oP, 'auth_pwd', 'raw_data');
|
||||
$sCsvFile = ReadMandatoryParam($oP, 'csvfile', 'raw_data');
|
||||
@@ -273,7 +273,7 @@ try
|
||||
//
|
||||
// Read parameters
|
||||
//
|
||||
$sClass = ReadMandatoryParam($oP, 'class', 'raw_data'); // do not filter as a valid class, we want to produce the report "wrong class" ourselves
|
||||
$sClass = ReadMandatoryParam($oP, 'class', 'raw_data'); // do not filter as a valid class, we want to produce the report "wrong class" ourselves
|
||||
$sSep = ReadParam($oP, 'separator', 'raw_data');
|
||||
$sQualifier = ReadParam($oP, 'qualifier', 'raw_data');
|
||||
$sCharSet = ReadParam($oP, 'charset', 'raw_data');
|
||||
@@ -326,7 +326,7 @@ try
|
||||
{
|
||||
$sDateFormat = null;
|
||||
}
|
||||
|
||||
|
||||
if ($sCharSet == '')
|
||||
{
|
||||
$sCharSet = MetaModel::GetConfig()->Get('csv_file_default_charset');
|
||||
@@ -444,7 +444,7 @@ try
|
||||
{
|
||||
$sUTF8Data = iconv($sCharSet, 'UTF-8//IGNORE//TRANSLIT', $sCSVData);
|
||||
}
|
||||
$oCSVParser = new CSVParser($sUTF8Data, $sSep, $sQualifier);
|
||||
$oCSVParser = new CSVParser($sUTF8Data, $sSep, $sQualifier);
|
||||
|
||||
// Limitation: as the attribute list is in the first line, we can not match external key by a third-party attribute
|
||||
$aRawFieldList = $oCSVParser->ListFields();
|
||||
@@ -466,7 +466,7 @@ try
|
||||
// Remove any trailing "star" character before the arrow (->)
|
||||
// A star character at the end can be used to indicate a mandatory field
|
||||
$sFieldName = $aMatches[1].'->'.$aMatches[2];
|
||||
}
|
||||
}
|
||||
if (array_key_exists(strtolower($sFieldName), $aKnownColumnNames))
|
||||
{
|
||||
$aColumns = $aKnownColumnNames[strtolower($sFieldName)];
|
||||
@@ -488,7 +488,7 @@ try
|
||||
throw new BulkLoadException("Unknown column: '$sSafeName'. Possible columns: ".implode(', ', array_keys($aKnownColumnNames)));
|
||||
}
|
||||
}
|
||||
// Note: at this stage the list of fields is supposed to be made of attcodes (and the symbol '->')
|
||||
// Note: at this stage the list of fields is supposed to be made of attcodes (and the symbol '->')
|
||||
|
||||
$aAttList = array();
|
||||
$aExtKeys = array();
|
||||
@@ -723,7 +723,7 @@ try
|
||||
}
|
||||
CMDBObject::SetTrackInfo($sMoreInfo);
|
||||
CMDBObject::SetTrackOrigin('csv-import.php');
|
||||
|
||||
|
||||
$oMyChange = CMDBObject::GetCurrentChange();
|
||||
}
|
||||
|
||||
@@ -758,7 +758,7 @@ try
|
||||
break;
|
||||
case 'RowStatus_Issue':
|
||||
$iCountErrors++;
|
||||
break;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($bWritten)
|
||||
@@ -837,7 +837,7 @@ try
|
||||
$aDisplayConfig["$iCol"] = array("label"=>$sAttCode, "description"=>$sLabel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$aResultDisp = array(); // to be displayed
|
||||
foreach($aRes as $iRow => $aRowData)
|
||||
{
|
||||
@@ -864,14 +864,16 @@ try
|
||||
foreach($aRowData as $key => $value)
|
||||
{
|
||||
$sKey = (string) $key;
|
||||
|
||||
|
||||
if ($sKey == '__STATUS__') continue;
|
||||
//__ERRORS__ used by tests only
|
||||
if ($sKey == '__ERRORS__') continue;
|
||||
if ($sKey == 'finalclass') continue;
|
||||
if ($sKey == 'id') continue;
|
||||
|
||||
|
||||
if (is_object($value))
|
||||
{
|
||||
$aRowDisp["$sKey"] = $value->GetDisplayableValue().$value->GetDescription();
|
||||
$aRowDisp["$sKey"] = $value->GetDisplayableValueAndDescription();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -885,15 +887,15 @@ try
|
||||
}
|
||||
catch(BulkLoadException $e)
|
||||
{
|
||||
$oP->add_comment($e->getMessage());
|
||||
$oP->add_comment($e->getMessage());
|
||||
}
|
||||
catch(SecurityException $e)
|
||||
{
|
||||
$oP->add_comment($e->getMessage());
|
||||
$oP->add_comment($e->getMessage());
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
$oP->add_comment((string)$e);
|
||||
$oP->add_comment((string)$e);
|
||||
}
|
||||
|
||||
$oP->output();
|
||||
|
||||
Reference in New Issue
Block a user