mirror of
https://github.com/Combodo/iTop.git
synced 2026-05-18 23:08:46 +02:00
Merge branch 'saas/3.0' into saas/3.1.0
This commit is contained in:
@@ -2246,3 +2246,45 @@ interface iModuleExtension
|
||||
*/
|
||||
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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -536,7 +548,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
|
||||
@@ -544,7 +556,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();
|
||||
@@ -654,7 +666,7 @@ abstract class MenuNode
|
||||
|
||||
/**
|
||||
* Stimulus to check: if the user can 'apply' this stimulus, then she/he can see this menu
|
||||
*/
|
||||
*/
|
||||
protected $m_aEnableStimuli;
|
||||
|
||||
/**
|
||||
@@ -814,7 +826,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
|
||||
@@ -987,7 +999,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)
|
||||
@@ -1058,7 +1070,7 @@ class OQLMenuNode extends MenuNode
|
||||
* @var bool|null
|
||||
*/
|
||||
protected $bSearchFormOpen;
|
||||
|
||||
|
||||
/**
|
||||
* Extra parameters to be passed to the display block to fine tune its appearence
|
||||
*/
|
||||
@@ -1091,7 +1103,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
|
||||
@@ -1120,7 +1132,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
|
||||
);
|
||||
@@ -1354,10 +1366,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))
|
||||
@@ -1366,7 +1378,7 @@ class NewObjectMenuNode extends MenuNode
|
||||
break; // Enough for now
|
||||
}
|
||||
}
|
||||
return $bActionIsAllowed;
|
||||
return $bActionIsAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1508,7 +1520,7 @@ class DashboardMenuNode extends MenuNode
|
||||
throw new Exception("Error: failed to load dashboard file: '{$this->sDashboardFile}'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1549,7 +1561,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>
|
||||
@@ -1345,6 +1345,14 @@ class Config
|
||||
'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',
|
||||
|
||||
2
css/backoffice/pages/_csv-import.scss
vendored
2
css/backoffice/pages/_csv-import.scss
vendored
@@ -51,4 +51,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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -452,6 +452,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',
|
||||
|
||||
@@ -444,6 +444,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',
|
||||
|
||||
@@ -433,6 +433,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',
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
28
pages/UI.php
28
pages/UI.php
@@ -20,6 +20,7 @@ use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
|
||||
use Combodo\iTop\Controller\Base\Layout\ObjectController;
|
||||
use Combodo\iTop\Service\Router\Router;
|
||||
use Combodo\iTop\Application\WelcomePopup\WelcomePopupService;
|
||||
|
||||
/**
|
||||
* Displays a popup welcome message, once per session at maximum
|
||||
@@ -29,19 +30,18 @@ use Combodo\iTop\Service\Router\Router;
|
||||
*
|
||||
* @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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +66,7 @@ function ApplyNextAction(Webpage $oP, CMDBObject $oObj, $sNextAction)
|
||||
}
|
||||
// Get the list of missing mandatory fields for the target state, considering only the changes from the previous form (i.e don't prompt twice)
|
||||
$aExpectedAttributes = $oObj->GetTransitionAttributes($sNextAction);
|
||||
|
||||
|
||||
if (count($aExpectedAttributes) == 0)
|
||||
{
|
||||
// If all the mandatory fields are already present, just apply the transition silently...
|
||||
@@ -85,7 +85,7 @@ function ApplyNextAction(Webpage $oP, CMDBObject $oObj, $sNextAction)
|
||||
// redirect to the 'stimulus' action
|
||||
$oAppContext = new ApplicationContext();
|
||||
//echo "<p>Missing Attributes <pre>".print_r($aExpectedAttributes, true)."</pre></p>\n";
|
||||
|
||||
|
||||
$oP->add_header('Location: '.utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=stimulus&class='.get_class($oObj).'&stimulus='.$sNextAction.'&id='.$oObj->getKey().'&'.$oAppContext->GetForLink());
|
||||
}
|
||||
}
|
||||
@@ -243,7 +243,7 @@ function DisplayMultipleSelectionForm(WebPage $oP, DBSearch $oFilter, string $sN
|
||||
$aExtraParams['surround_with_panel'] = true;
|
||||
if(array_key_exists('icon', $aDisplayParams)){
|
||||
$aExtraParams['panel_icon'] = $aDisplayParams['icon'];
|
||||
}
|
||||
}
|
||||
if(array_key_exists('title', $aDisplayParams)){
|
||||
$aExtraParams['panel_title'] = $aDisplayParams['title'];
|
||||
}
|
||||
@@ -292,7 +292,7 @@ function DisplayNavigatorGroupTab($oP)
|
||||
}
|
||||
|
||||
/***********************************************************************************
|
||||
*
|
||||
*
|
||||
* Main user interface page starts here
|
||||
*
|
||||
***********************************************************************************/
|
||||
@@ -700,7 +700,7 @@ try
|
||||
break;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
/** @deprecated 3.1.0 Use the "object.new" route instead */
|
||||
// Kept for backward compatibility
|
||||
case 'new': // Form to create a new object
|
||||
@@ -1638,4 +1638,4 @@ class UI
|
||||
);
|
||||
cmdbAbstractObject::DoBulkModify($oP, $sClass, $aSelectedObj, 'preview_or_modify_all', $bPreview, $sCancelUrl, $aContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use Combodo\iTop\Renderer\Console\ConsoleBlockRenderer;
|
||||
use Combodo\iTop\Renderer\Console\ConsoleFormRenderer;
|
||||
use Combodo\iTop\Service\Router\Router;
|
||||
use Combodo\iTop\Service\TemporaryObjects\TemporaryObjectManager;
|
||||
use Combodo\iTop\Controller\WelcomePopupController;
|
||||
|
||||
require_once('../approot.inc.php');
|
||||
|
||||
@@ -2551,6 +2552,25 @@ 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;
|
||||
|
||||
//--------------------------------
|
||||
// Object
|
||||
//--------------------------------
|
||||
@@ -2560,9 +2580,8 @@ EOF
|
||||
$oPage = $oController->OperationModify();
|
||||
break;
|
||||
|
||||
default:
|
||||
$oPage->p("Invalid query.");
|
||||
}
|
||||
default:
|
||||
$oPage->p("Invalid query.");
|
||||
}
|
||||
$oKPI->ComputeAndReport('Data fetch and format');
|
||||
$oPage->output();
|
||||
|
||||
@@ -141,55 +141,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
|
||||
|
||||
@@ -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;
|
||||
@@ -356,7 +356,7 @@ class MFCompiler
|
||||
apc_clear_cache();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Perform the actual "Compilation" of all modules
|
||||
@@ -368,21 +368,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
|
||||
@@ -429,6 +424,7 @@ class MFCompiler
|
||||
|
||||
static::SetUseSymbolicLinksFlag($bUseSymbolicLinks);
|
||||
|
||||
$oParentMenuNodeCompiler->LoadModuleMenuInfo($aModules);
|
||||
foreach ($aModules as $foo => $oModule) {
|
||||
$sModuleName = $oModule->GetName();
|
||||
$sModuleVersion = $oModule->GetVersion();
|
||||
@@ -513,7 +509,7 @@ class MFCompiler
|
||||
}
|
||||
}
|
||||
|
||||
if (!array_key_exists($sModuleName, $aMenusByModule))
|
||||
if (is_null($oParentMenuNodeCompiler->GetMenusByModule($sModuleName)))
|
||||
{
|
||||
$this->Log("Found module without menus declared: $sModuleName");
|
||||
}
|
||||
@@ -533,79 +529,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];
|
||||
// compute parent hierarchy
|
||||
$aParentIdHierarchy = [];
|
||||
while ($sParent = $oMenuNode->GetChildText('parent', null)) {
|
||||
array_unshift($aParentIdHierarchy, $sParent);
|
||||
$oMenuNode = $aMenuNodes[$sParent];
|
||||
}
|
||||
$aMenusToLoad = array_merge($aMenusToLoad, $aParentIdHierarchy);
|
||||
$aParentMenus = array_merge($aParentMenus, $aParentIdHierarchy);
|
||||
// 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
|
||||
{
|
||||
/** @var \iTopWebPage $oP */
|
||||
$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";
|
||||
}
|
||||
@@ -637,7 +573,7 @@ EOF;
|
||||
$sCompiledCode .= $aSnippet['content']."\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create (overwrite if existing) the compiled file
|
||||
//
|
||||
if (strlen($sCompiledCode) > 0)
|
||||
@@ -741,7 +677,7 @@ PHP;
|
||||
$this->sMainPHPCode .= $aSnippet['content']."\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Compile the portals
|
||||
/** @var \MFElement $oPortalsNode */
|
||||
$oPortalsNode = $this->oFactory->GetNodes('/itop_design/portals')->item(0);
|
||||
@@ -755,7 +691,7 @@ PHP;
|
||||
/** @var \MFElement $oParametersNode */
|
||||
$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)
|
||||
@@ -789,7 +725,7 @@ PHP;
|
||||
$sCurrDate = date(DATE_ISO8601);
|
||||
// Autoload
|
||||
$sPHPFile = $sTempTargetDir.'/autoload.php';
|
||||
$sPHPFileContent =
|
||||
$sPHPFileContent =
|
||||
<<<EOF
|
||||
<?php
|
||||
//
|
||||
@@ -798,7 +734,7 @@ PHP;
|
||||
//
|
||||
EOF
|
||||
;
|
||||
|
||||
|
||||
$sPHPFileContent .= "\nMetaModel::IncludeModule(MODULESROOT.'/core/main.php');\n";
|
||||
$sPHPFileContent .= implode("\n", $aDataModelFiles);
|
||||
$sPHPFileContent .= implode("\n", $aWebservicesFiles);
|
||||
@@ -806,14 +742,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
|
||||
@@ -845,7 +781,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(
|
||||
@@ -855,7 +791,7 @@ EOF
|
||||
'must_change' => 'OPT_ATT_MUSTCHANGE',
|
||||
'hidden' => 'OPT_ATT_HIDDEN',
|
||||
);
|
||||
|
||||
|
||||
$aFlags = array();
|
||||
foreach ($aNodeAttributeToFlag as $sNodeAttribute => $sFlag)
|
||||
{
|
||||
@@ -867,7 +803,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;
|
||||
@@ -889,7 +825,7 @@ EOF
|
||||
'details' => 'LINKSET_TRACKING_DETAILS',
|
||||
'all' => 'LINKSET_TRACKING_ALL',
|
||||
);
|
||||
|
||||
|
||||
static $aXmlToPHP_Others = array(
|
||||
'none' => 'ATTRIBUTE_TRACKING_NONE',
|
||||
'all' => 'ATTRIBUTE_TRACKING_ALL',
|
||||
@@ -930,7 +866,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'");
|
||||
@@ -938,10 +874,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 == '')
|
||||
@@ -1034,7 +970,7 @@ EOF
|
||||
else
|
||||
{
|
||||
throw new DOMFormatException("missing (or empty) mandatory tag '$sTag' under the tag '".$oNode->nodeName."'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1138,7 +1074,7 @@ EOF
|
||||
|
||||
/**
|
||||
* Adds quotes and escape characters
|
||||
*/
|
||||
*/
|
||||
protected function QuoteForPHP($sStr, $bSimpleQuotes = false)
|
||||
{
|
||||
if ($bSimpleQuotes)
|
||||
@@ -1245,7 +1181,7 @@ EOF
|
||||
$sScalar = (string)(int)$sText;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'float':
|
||||
if (is_null($sText))
|
||||
{
|
||||
@@ -1257,7 +1193,7 @@ EOF
|
||||
$sScalar = (string)(float)$sText;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'bool':
|
||||
if (is_null($sText))
|
||||
{
|
||||
@@ -2723,7 +2659,7 @@ CSS;
|
||||
* @throws \DOMException
|
||||
* @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);
|
||||
|
||||
@@ -2819,11 +2755,11 @@ CSS;
|
||||
case '1':
|
||||
$sSearchFormOpen = 'true';
|
||||
break;
|
||||
|
||||
|
||||
case '0':
|
||||
$sSearchFormOpen = 'false';
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
$sSearchFormOpen = 'true';
|
||||
}
|
||||
@@ -2912,7 +2848,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');
|
||||
@@ -2938,7 +2874,7 @@ CSS;
|
||||
$oClasses = $oGroup->GetUniqueElement('classes');
|
||||
foreach($oClasses->getElementsByTagName('class') as $oClass)
|
||||
{
|
||||
|
||||
|
||||
$sClass = $oClass->getAttribute("id");
|
||||
$aClasses[] = $sClass;
|
||||
|
||||
@@ -2956,7 +2892,7 @@ CSS;
|
||||
$aProfiles[1] = array(
|
||||
'name' => 'Administrator',
|
||||
'description' => 'Has the rights on everything (bypassing any control)',
|
||||
);
|
||||
);
|
||||
|
||||
$aGrants = array();
|
||||
$oProfiles = $oUserRightsNode->GetUniqueElement('profiles');
|
||||
@@ -2988,7 +2924,7 @@ CSS;
|
||||
}
|
||||
$sGrant = $oAction->GetText();
|
||||
$bGrant = ($sGrant == 'allow');
|
||||
|
||||
|
||||
if ($sGroupId == '*')
|
||||
{
|
||||
$aGrantClasses = array('*');
|
||||
@@ -3227,7 +3163,7 @@ Dict::SetLanguagesList(
|
||||
$sLanguagesDump
|
||||
);
|
||||
EOF;
|
||||
|
||||
|
||||
file_put_contents($sLanguagesFile, $sLanguagesFileContent);
|
||||
}
|
||||
|
||||
@@ -3264,7 +3200,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);
|
||||
@@ -3278,7 +3214,7 @@ EOF;
|
||||
}
|
||||
$oParentNode = $oFileRef->parentNode;
|
||||
$oParentNode->removeChild($oFileRef);
|
||||
|
||||
|
||||
$oTextNode = $oParentNode->ownerDocument->createTextNode($sRelativePath.'/images/'.$sFile);
|
||||
$oParentNode->appendChild($oTextNode);
|
||||
}
|
||||
@@ -3359,7 +3295,7 @@ EOF;
|
||||
'utility_imports' => array(),
|
||||
'stylesheets' => array(),
|
||||
);
|
||||
|
||||
|
||||
if($oThemesCommonNodes !== null) {
|
||||
/** @var \DOMNodeList $oThemesCommonVariables */
|
||||
$oThemesCommonVariables = $oThemesCommonNodes->GetNodes('variables/variable');
|
||||
@@ -3367,7 +3303,7 @@ EOF;
|
||||
$sVariableId = $oVariable->getAttribute('id');
|
||||
$aThemesCommonParameters['variables'][$sVariableId] = $oVariable->GetText();
|
||||
}
|
||||
|
||||
|
||||
/** @var \DOMNodeList $oThemesCommonImports */
|
||||
$oThemesCommonImports = $oThemesCommonNodes->GetNodes('imports/import');
|
||||
foreach ($oThemesCommonImports as $oImport) {
|
||||
@@ -3381,7 +3317,7 @@ EOF;
|
||||
SetupLog::Warning('CompileThemes: Theme common has an import (#'.$sImportId.') without explicit xsi:type, it will be ignored. Check Datamodel XML Reference to fix it.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Stylesheets
|
||||
// - Manually added in the XML
|
||||
/** @var \DOMNodeList $oThemesCommonStylesheets */
|
||||
@@ -3446,7 +3382,7 @@ EOF;
|
||||
$aThemeParameters[$sThemeParameterName] = array_merge($aThemeParameter, $aThemesCommonParameters[$sThemeParameterName]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$aThemes[$sThemeId] = [
|
||||
'theme_parameters' => $aThemeParameters,
|
||||
'precompiled_stylesheet' => $oTheme->GetChildText('precompiled_stylesheet', ''),
|
||||
@@ -3640,8 +3576,8 @@ EOF;
|
||||
{
|
||||
SetupUtils::rrmdir($sTempTargetDir.'/branding/images');
|
||||
}
|
||||
|
||||
// Compile themes
|
||||
|
||||
// Compile themes
|
||||
$this->CompileThemes($oBrandingNode, $sTempTargetDir);
|
||||
}
|
||||
}
|
||||
@@ -3682,11 +3618,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";
|
||||
@@ -3729,7 +3665,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
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>
|
||||
@@ -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'],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user