diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index bf341e46d..06b3ffe1d 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -755,34 +755,23 @@ HTML } /** @var \iApplicationUIExtension $oExtensionInstance */ - foreach(MetaModel::EnumPlugins('iApplicationUIExtension') as $oExtensionInstance) - { + foreach (MetaModel::EnumPlugins('iApplicationUIExtension') as $oExtensionInstance) { $oExtensionInstance->OnDisplayRelations($this, $oPage, $bEditMode); } $oPage->SetCurrentTab(''); - + // Look for any trigger that considers this object as "In Scope" // If any trigger has been found then display a tab with notifications // - $oTriggerSet = new CMDBObjectSet(new DBObjectSearch('Trigger')); - $aTriggers = array(); - while ($oTrigger = $oTriggerSet->Fetch()) - { - if ($oTrigger->IsInScope($this)) - { - $aTriggers[] = $oTrigger->GetKey(); - } - } - if (count($aTriggers) > 0) - { + $aTriggers = $this->GetRelatedTriggersIDs(); + if (count($aTriggers) > 0) { $iId = $this->GetKey(); $aParams = array('triggers' => $aTriggers, 'id' => $iId); $aNotifSearches = array(); $iNotifsCount = 0; $aNotificationClasses = MetaModel::EnumChildClasses('EventNotification', ENUM_CHILD_CLASSES_EXCLUDETOP); - foreach($aNotificationClasses as $sNotifClass) - { + foreach ($aNotificationClasses as $sNotifClass) { $aNotifSearches[$sNotifClass] = DBObjectSearch::FromOQL("SELECT $sNotifClass AS Ev JOIN Trigger AS T ON Ev.trigger_id = T.id WHERE T.id IN (:triggers) AND Ev.object_id = :id"); $aNotifSearches[$sNotifClass]->SetInternalParams($aParams); $oNotifSet = new DBObjectSet($aNotifSearches[$sNotifClass], array()); @@ -792,18 +781,37 @@ HTML $sCount = ($iNotifsCount > 0) ? ' ('.$iNotifsCount.')' : ''; $oPage->SetCurrentTab('UI:NotificationsTab', Dict::S('UI:NotificationsTab').$sCount); - foreach($aNotificationClasses as $sNotifClass) - { + foreach($aNotificationClasses as $sNotifClass) { $oClassIcon = new MedallionIcon(MetaModel::GetClassIcon($sNotifClass, false)); $oClassIcon->SetDescription(MetaModel::GetName($sNotifClass))->AddCSSClass('ibo-blocklist--medallion'); $oPage->AddUiBlock($oClassIcon); - + $oBlock = new DisplayBlock($aNotifSearches[$sNotifClass], 'list', false); $oBlock->Display($oPage, 'notifications_'.$sNotifClass, array('menu' => false)); } } } + /** + * @return string[] IDs of the triggers that consider this object as "In Scope" + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \MySQLException + * @since 3.0.0 + */ + public function GetRelatedTriggersIDs(): array + { + $oTriggerSet = new CMDBObjectSet(new DBObjectSearch('Trigger')); + $aTriggers = []; + while ($oTrigger = $oTriggerSet->Fetch()) { + if ($oTrigger->IsInScope($this)) { + $aTriggers[] = $oTrigger->GetKey(); + } + } + + return $aTriggers; + } + /** * @param \WebPage $oPage * @param bool $bEditMode diff --git a/core/cmdbchangeop.class.inc.php b/core/cmdbchangeop.class.inc.php index 6aba76197..bb27036c5 100644 --- a/core/cmdbchangeop.class.inc.php +++ b/core/cmdbchangeop.class.inc.php @@ -39,7 +39,7 @@ interface iCMDBChangeOp { /** - * Describe (as a text string) the modifications corresponding to this change + * Describe (as an HTML string) the modifications corresponding to this change * * @return string */ diff --git a/core/event.class.inc.php b/core/event.class.inc.php index f85c356ec..f0d7e4a11 100644 --- a/core/event.class.inc.php +++ b/core/event.class.inc.php @@ -98,16 +98,16 @@ class Event extends DBObject implements iDisplay function DisplayBareProperties(WebPage $oPage, $bEditMode = false, $sPrefix = '', $aExtraParams = array()) { if ($bEditMode) return array(); // Not editable - + $aDetails = array(); $sClass = get_class($this); $aZList = MetaModel::FlattenZlist(MetaModel::GetZListItems($sClass, 'details')); - foreach( $aZList as $sAttCode) - { - $sDisplayValue = $this->GetAsHTML($sAttCode); + foreach ($aZList as $sAttCode) { + $sDisplayValue = $this->GetAsHTML($sAttCode); $aDetails[] = array('label' => ''.MetaModel::GetLabel($sClass, $sAttCode).'', 'value' => $sDisplayValue); } $oPage->Details($aDetails); + return array(); } } @@ -134,8 +134,8 @@ class EventNotification extends Event MetaModel::Init_Params($aParams); MetaModel::Init_InheritAttributes(); MetaModel::Init_AddAttribute(new AttributeExternalKey("trigger_id", array("targetclass"=>"Trigger", "jointype"=> "", "allowed_values"=>null, "sql"=>"trigger_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalKey("action_id", array("targetclass"=>"Action", "jointype"=> "", "allowed_values"=>null, "sql"=>"action_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeInteger("object_id", array("allowed_values"=>null, "sql"=>"object_id", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeExternalKey("action_id", array("targetclass" => "Action", "jointype" => "", "allowed_values" => null, "sql" => "action_id", "is_null_allowed" => false, "on_target_delete" => DEL_AUTO, "depends_on" => array()))); + MetaModel::Init_AddAttribute(new AttributeInteger("object_id", array("allowed_values" => null, "sql" => "object_id", "default_value" => 0, "is_null_allowed" => false, "depends_on" => array()))); // Display lists MetaModel::Init_SetZListItems('details', array('date', 'message', 'userinfo', 'trigger_id', 'action_id', 'object_id')); // Attributes to be displayed for the complete details @@ -144,7 +144,6 @@ class EventNotification extends Event // MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form } - } class EventNotificationEmail extends EventNotification @@ -181,7 +180,6 @@ class EventNotificationEmail extends EventNotification // MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form } - } class EventIssue extends Event diff --git a/css/backoffice/layout/_all.scss b/css/backoffice/layout/_all.scss index 9d3fe21c2..75ea63e17 100644 --- a/css/backoffice/layout/_all.scss +++ b/css/backoffice/layout/_all.scss @@ -26,10 +26,5 @@ @import "dashboard/all"; @import "wizard-container/wizard-container"; @import "object/object-details"; -@import "activity-panel/activity-panel"; -@import "activity-panel/activity-entry"; -@import "activity-panel/caselog-entry"; -@import "activity-panel/edits-entry"; -@import "activity-panel/transition-entry"; -@import "activity-panel/caselog-entry-form"; +@import "activity-panel/all"; @import "blocks-integrations/all"; diff --git a/css/backoffice/layout/activity-panel/_all.scss b/css/backoffice/layout/activity-panel/_all.scss new file mode 100644 index 000000000..fb9820188 --- /dev/null +++ b/css/backoffice/layout/activity-panel/_all.scss @@ -0,0 +1,12 @@ +/*! + * @copyright Copyright (C) 2010-2021 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +@import "activity-panel"; +@import "caselog-entry-form"; +@import "activity-entry"; +@import "caselog-entry"; +@import "transition-entry"; +@import "edits-entry"; +@import "notification-entry"; diff --git a/css/backoffice/layout/activity-panel/_notification-entry.scss b/css/backoffice/layout/activity-panel/_notification-entry.scss new file mode 100644 index 000000000..d26b454aa --- /dev/null +++ b/css/backoffice/layout/activity-panel/_notification-entry.scss @@ -0,0 +1,35 @@ +/*! + * @copyright Copyright (C) 2010-2021 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +/* SCSS variables */ +$ibo-notification-entry--short-description--text-color: inherit !default; +$ibo-notification-entry--long-description-toggler-icon--margin-left: 12px !default; +$ibo-notification-entry--long-description--margin-top: 8px !default; + +/* CSS rules */ +/* - Long description */ +a.ibo-notification-entry--short-description { + color: $ibo-notification-entry--short-description--text-color; +} +.ibo-notification-entry--long-description-toggler-icon{ + margin-left: $ibo-notification-entry--long-description-toggler-icon--margin-left; + transition: all 0.2s ease-in-out; +} +.ibo-notification-entry--long-description{ + display: none; + margin-top: $ibo-notification-entry--long-description--margin-top; + list-style: inside; +} +/* - Long desc. opened */ +.ibo-notification-entry{ + &.ibo-is-opened{ + .ibo-notification-entry--long-description-toggler-icon{ + transform: rotateX(180deg); + } + .ibo-notification-entry--long-description{ + display: block; + } + } +} diff --git a/dictionaries/ui/layouts/en.dictionary.itop.activity-panel.php b/dictionaries/ui/layouts/en.dictionary.itop.activity-panel.php index c63335e2f..c6aac2b71 100644 --- a/dictionaries/ui/layouts/en.dictionary.itop.activity-panel.php +++ b/dictionaries/ui/layouts/en.dictionary.itop.activity-panel.php @@ -44,6 +44,9 @@ Dict::Add('EN US', 'English', 'English', array( 'UI:Layout:ActivityPanel:MultipleEntriesSaveConfirmation:Title' => 'Multiple logs save', 'UI:Layout:ActivityPanel:MultipleEntriesSaveConfirmation:Explanation' => 'By pressing the "save" button, you will submit entries for all the edited logs at once.', + // Notification entry + 'UI:Layout:ActivityPanel:NotificationEntry:MessageLink:Tooltip' => 'Click to open the notifications tab and get more information', + // Placeholder 'UI:Layout:ActivityPanel:NoEntry:Placeholder:Hint' => 'It\'s calm up here, no activity yet', diff --git a/images/icons/icons8-music-robot.svg b/images/icons/icons8-music-robot.svg new file mode 100644 index 000000000..3547191cb --- /dev/null +++ b/images/icons/icons8-music-robot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/layouts/activity-panel/activity-panel.js b/js/layouts/activity-panel/activity-panel.js index e282925c5..31a2d7fbb 100644 --- a/js/layouts/activity-panel/activity-panel.js +++ b/js/layouts/activity-panel/activity-panel.js @@ -82,6 +82,8 @@ $(function() entry_datetime: '[data-role="ibo-activity-entry--datetime"]', edits_entry_long_description: '[data-role="ibo-edits-entry--long-description"]', edits_entry_long_description_toggler: '[data-role="ibo-edits-entry--long-description-toggler"]', + notification_entry_long_description: '[data-role="ibo-notification-entry--long-description"]', + notification_entry_long_description_toggler: '[data-role="ibo-notification-entry--long-description-toggler"]', }, enums: { tab_types: { @@ -206,7 +208,11 @@ $(function() }); // - Click on an edits entry's long description toggler this.element.find(this.js_selectors.edits_entry_long_description_toggler).on('click', function (oEvent) { - me._onEditsLongDescriptionTogglerClick(oEvent, $(this).closest(me.js_selectors.entry)); + me._onEntryLongDescriptionTogglerClick(oEvent, $(this).closest(me.js_selectors.entry)); + }); + // - Click on an notification entry's long description toggler + this.element.find(this.js_selectors.notification_entry_long_description_toggler).on('click', function (oEvent) { + me._onEntryLongDescriptionTogglerClick(oEvent, $(this).closest(me.js_selectors.entry)); }); // Page exit @@ -390,12 +396,10 @@ $(function() this._SendEntriesToServer(); } }, - _onCaseLogClosedMessageClick: function(oEntryElem) - { + _onCaseLogClosedMessageClick: function (oEntryElem) { this._OpenMessage(oEntryElem); }, - _onEditsLongDescriptionTogglerClick: function(oEvent, oEntryElem) - { + _onEntryLongDescriptionTogglerClick: function (oEvent, oEntryElem) { // Avoid anchor glitch oEvent.preventDefault(); diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 66d18fae1..09842e794 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -233,6 +233,9 @@ return array( 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\CMDBChangeOp\\CMDBChangeOpSetAttributeScalarFactory' => $baseDir . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpSetAttributeScalarFactory.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\CaseLogEntry' => $baseDir . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/CaseLogEntry.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\EditsEntry' => $baseDir . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/EditsEntry.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\EventNotification\\EventNotificationEmailFactory' => $baseDir . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/EventNotification/EventNotificationEmailFactory.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\EventNotification\\EventNotificationFactory' => $baseDir . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/EventNotification/EventNotificationFactory.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\NotificationEntry' => $baseDir . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/NotificationEntry.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\TransitionEntry' => $baseDir . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/TransitionEntry.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanel' => $baseDir . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityPanel.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanelFactory' => $baseDir . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityPanelFactory.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index bd5d78469..507a4989b 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -463,6 +463,9 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\CMDBChangeOp\\CMDBChangeOpSetAttributeScalarFactory' => __DIR__ . '/../..' . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/CMDBChangeOp/CMDBChangeOpSetAttributeScalarFactory.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\CaseLogEntry' => __DIR__ . '/../..' . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/CaseLogEntry.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\EditsEntry' => __DIR__ . '/../..' . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/EditsEntry.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\EventNotification\\EventNotificationEmailFactory' => __DIR__ . '/../..' . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/EventNotification/EventNotificationEmailFactory.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\EventNotification\\EventNotificationFactory' => __DIR__ . '/../..' . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/EventNotification/EventNotificationFactory.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\NotificationEntry' => __DIR__ . '/../..' . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/NotificationEntry.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityEntry\\TransitionEntry' => __DIR__ . '/../..' . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/TransitionEntry.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanel' => __DIR__ . '/../..' . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityPanel.php', 'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\ActivityPanelFactory' => __DIR__ . '/../..' . '/sources/application/UI/Base/Layout/ActivityPanel/ActivityPanelFactory.php', diff --git a/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/ActivityEntry.php b/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/ActivityEntry.php index 9b44fc3fd..f6d9377f5 100644 --- a/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/ActivityEntry.php +++ b/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/ActivityEntry.php @@ -24,6 +24,7 @@ use AttributeDateTime; use Combodo\iTop\Application\UI\Base\UIBlock; use DateTime; use UserRights; +use utils; /** * Class ActivityEntry @@ -46,6 +47,8 @@ class ActivityEntry extends UIBlock public const DEFAULT_TYPE = 'generic'; /** @var string DEFAULT_DECORATION_CLASSES */ public const DEFAULT_DECORATION_CLASSES = 'fas fa-fw fa-mortar-pestle'; + /** @var string Relative URL (from the app. root) to the default author picture URL */ + public const DEFAULT_AUTHOR_PICTURE_REL_URL = 'images/icons/icons8-music-robot.svg'; /** @var string $sType Type of entry, used for filtering (eg. case log, edits, transition, ...) */ protected $sType; @@ -209,17 +212,20 @@ class ActivityEntry extends UIBlock // Set friendlyname to whatever we have in case $sAuthorLogin is not a valid login (deleted user, cron, ...) $iAuthorId = UserRights::GetUserId($this->sAuthorLogin); - if(empty($iAuthorId) === true) - { + // - Friendlyname + if (true === empty($iAuthorId)) { $this->sAuthorFriendlyname = $this->sAuthorLogin; - } - else - { - // TODO 3.0.0: Check that this does not return '' when author is the CRON or an extension. + } else { $this->sAuthorFriendlyname = UserRights::GetUserFriendlyName($this->sAuthorLogin); } + // - Initials $this->sAuthorInitials = UserRights::GetUserInitials($this->sAuthorLogin); + // - Picture $this->sAuthorPictureAbsUrl = UserRights::GetContactPictureAbsUrl($this->sAuthorLogin, false); + if ((null === $this->sAuthorPictureAbsUrl) && (ITOP_APPLICATION_SHORT === $this->sAuthorLogin)) { + $this->sAuthorPictureAbsUrl = utils::GetAbsoluteUrlAppRoot().static::DEFAULT_AUTHOR_PICTURE_REL_URL; + } + $this->bIsFromCurrentUser = UserRights::GetUserId($this->sAuthorLogin) === UserRights::GetUserId(); return $this; diff --git a/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/ActivityEntryFactory.php b/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/ActivityEntryFactory.php index 9ca148b38..9ee48d434 100644 --- a/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/ActivityEntryFactory.php +++ b/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/ActivityEntryFactory.php @@ -23,6 +23,8 @@ namespace Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry; use AttributeDateTime; use CMDBChangeOp; use DateTime; +use DBObject; +use EventNotification; use Exception; use MetaModel; use ReflectionClass; @@ -47,11 +49,10 @@ class ActivityEntryFactory */ public static function MakeFromCmdbChangeOp(CMDBChangeOp $oChangeOp) { - $sFactoryFqcn = static::GetCmdbChangeOpFactoryClass($oChangeOp); + $sFactoryFqcn = static::GetFactoryClass($oChangeOp, 'CMDBChangeOp'); // If no factory found, throw an exception as the developer most likely forgot to create it - if(empty($sFactoryFqcn)) - { + if (empty($sFactoryFqcn)) { throw new Exception('No factory found for '.get_class($oChangeOp).', did you forgot to create one?'); } @@ -89,36 +90,57 @@ class ActivityEntryFactory } /** - * Return the FQCN of the best fitted factory for the $oChangeOp. If none found, null will be returned. + * Make an ActivityEntry entry (for ActivityPanel) based on the $oEvent * - * @param \CMDBChangeOp $oChangeOp + * @param \EventNotification $oEvent + * + * @return \Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\ActivityEntry + * @throws \ReflectionException + */ + public static function MakeFromEventNotification(EventNotification $oEvent) + { + $sFactoryFqcn = static::GetFactoryClass($oEvent, 'EventNotification'); + + // If no factory found, throw an exception as the developer most likely forgot to create it + if (empty($sFactoryFqcn)) { + throw new Exception('No factory found for '.get_class($oEvent).', did you forgot to create one?'); + } + + /** @var \Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\NotificationEntry $oEntry */ + /** @noinspection PhpUndefinedMethodInspection Call static method from the $sFactoryFqcn class */ + $oEntry = $sFactoryFqcn::MakeFromEventNotification($oEvent); + + return $oEntry; + } + + /** + * Return the FQCN of the best fitted factory for the $oObject / $sObjectType tuple. If none found, null will be returned. + * + * @param \DBObject $oObject * * @return string|null * @throws \ReflectionException */ - protected static function GetCmdbChangeOpFactoryClass(CMDBChangeOp $oChangeOp) + protected static function GetFactoryClass(DBObject $oObject, string $sObjectType) { // Classes to search a factory for - $aClassesTree = [get_class($oChangeOp)]; + $aClassesTree = [get_class($oObject)]; // Add parent classes to tree if not a root class - $aParentClasses = class_parents($oChangeOp); - if(is_array($aParentClasses)) - { + $aParentClasses = class_parents($oObject); + if (is_array($aParentClasses)) { $aClassesTree = array_merge($aClassesTree, array_values($aParentClasses)); } $sFactoryFqcn = null; - foreach($aClassesTree as $sClass) - { - // Warning: This will replace all occurrences of 'CMDBChangeOp' which can be an issue on classes using this + foreach ($aClassesTree as $sClass) { + // Warning: This will replace all occurrences of $sObjectType (eg. 'CMDBChangeOp', 'EventNotification', ...) which can be an issue on classes using this // We used the case sensitive search to limit this issue. $sSimplifiedClass = (new ReflectionClass($sClass))->getShortName(); - $sFactoryFqcnToTry = __NAMESPACE__ . '\\CMDBChangeOp\\' . $sSimplifiedClass . 'Factory'; + $sFactoryFqcnToTry = __NAMESPACE__.'\\'.$sObjectType.'\\'.$sSimplifiedClass.'Factory'; // Stop at the first factory found - if(class_exists($sFactoryFqcnToTry)) - { + if (class_exists($sFactoryFqcnToTry)) { $sFactoryFqcn = $sFactoryFqcnToTry; break; } diff --git a/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/EventNotification/EventNotificationEmailFactory.php b/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/EventNotification/EventNotificationEmailFactory.php new file mode 100644 index 000000000..c9c2bd78c --- /dev/null +++ b/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/EventNotification/EventNotificationEmailFactory.php @@ -0,0 +1,36 @@ + + * @package Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\EventNotification + * @since 3.0.0 + */ +class EventNotificationEmailFactory extends EventNotificationFactory +{ + /** @inheritDoc */ + public const DEFAULT_DECORATION_CLASSES = 'fas fa-fw fa-envelope'; +} \ No newline at end of file diff --git a/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/EventNotification/EventNotificationFactory.php b/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/EventNotification/EventNotificationFactory.php new file mode 100644 index 000000000..dc8c8fad0 --- /dev/null +++ b/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/EventNotification/EventNotificationFactory.php @@ -0,0 +1,67 @@ + + * @package Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\EventNotification + * @since 3.0.0 + */ +class EventNotificationFactory +{ + /** @var string Used to overload the type from the NotificationEntry */ + public const DEFAULT_TYPE = NotificationEntry::DEFAULT_TYPE; + /** @var string Used to overload the decoration classes from the NotificationEntry */ + public const DEFAULT_DECORATION_CLASSES = NotificationEntry::DEFAULT_DECORATION_CLASSES; + + /** + * Make an ActivityEntry from the iEventNotification $oEvent + * + * @param \EventNotification $oEvent + * + * @return \Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\NotificationEntry + * @throws \OQLException + */ + public static function MakeFromEventNotification(EventNotification $oEvent) + { + $oDateTime = DateTime::createFromFormat(AttributeDateTime::GetInternalFormat(), $oEvent->Get('date')); + + // Author login is hardcoded: + // - Adding a user_id column to the event tables like for the CMDBChangeOp could be erro prone during migration as those tables are huge. + // - Marking events as created by the app. is good enough as the user triggering it as no power over it, it cannot avoid it. + $sAuthorLogin = ITOP_APPLICATION_SHORT; + + $oEntry = new NotificationEntry($oDateTime, $sAuthorLogin, $oEvent->Get('action_id_friendlyname'), $oEvent->Get('message')); + $oEntry->SetType(static::DEFAULT_TYPE) + ->SetDecorationClasses(static::DEFAULT_DECORATION_CLASSES); + + return $oEntry; + } +} \ No newline at end of file diff --git a/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/NotificationEntry.php b/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/NotificationEntry.php new file mode 100644 index 000000000..d0aea700b --- /dev/null +++ b/sources/application/UI/Base/Layout/ActivityPanel/ActivityEntry/NotificationEntry.php @@ -0,0 +1,111 @@ + + * @package Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry + * @since 3.0.0 + */ +class NotificationEntry extends ActivityEntry +{ + // Overloaded constants + public const BLOCK_CODE = 'ibo-notification-entry'; + public const DEFAULT_HTML_TEMPLATE_REL_PATH = 'base/layouts/activity-panel/activity-entry/notification-entry'; + + public const DEFAULT_TYPE = 'edits'; + public const DEFAULT_DECORATION_CLASSES = 'fas fa-fw fa-bell'; + + /** @var string Title of the entry, usually the linked action title */ + protected $sTitle; + /** @var string Message of the entry, usually it's status */ + protected $sMessage; + + /** + * NotificationEntry constructor. + * + * @param \DateTime $oDateTime + * @param string $sAuthorLogin + * @param string $sTitle + * @param string $sMessage + * @param string|null $sId + * + * @throws \OQLException + */ + public function __construct(DateTime $oDateTime, string $sAuthorLogin, string $sTitle, string $sMessage, ?string $sId = null) + { + parent::__construct($oDateTime, $sAuthorLogin, null, $sId); + + $this->SetTitle($sTitle); + $this->SetMessage($sMessage); + } + + /** + * @see static::$sTitle + * + * @param string $sTitle + * + * @return $this + */ + public function SetTitle(string $sTitle) + { + $this->sTitle = $sTitle; + + return $this; + } + + /** + * @see static::$sTitle + * @return string + */ + public function GetTitle(): string + { + return $this->sTitle; + } + + /** + * @see static::$sMessage + * + * @param string $sMessage + * + * @return $this + */ + public function SetMessage(string $sMessage) + { + $this->sMessage = $sMessage; + + return $this; + } + + /** + * @see static::$sMessage + * @return string + */ + public function GetMessage(): string + { + return $this->sMessage; + } +} \ No newline at end of file diff --git a/sources/application/UI/Base/Layout/ActivityPanel/ActivityPanelFactory.php b/sources/application/UI/Base/Layout/ActivityPanel/ActivityPanelFactory.php index 2b21d4399..59bf4e4cc 100644 --- a/sources/application/UI/Base/Layout/ActivityPanel/ActivityPanelFactory.php +++ b/sources/application/UI/Base/Layout/ActivityPanel/ActivityPanelFactory.php @@ -45,11 +45,11 @@ class ActivityPanelFactory /** * Make an activity panel for an object details layout, meaning that it should contain the case logs and the activity. * - * @param \DBObject $oObject - * @param string $sMode Mode the object is being displayed (view, edit, create, ...), default is view. - * * @see cmdbAbstractObject::ENUM_OBJECT_MODE_XXX * + * @param \DBObject $oObject + * @param string $sMode Mode the object is being displayed (view, edit, create, ...), default is view. + * * @return \Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityPanel * @throws \ArchivedObjectException * @throws \CoreException @@ -124,14 +124,41 @@ class ActivityPanelFactory $oActivityPanel->AddEntry($oEntry); // Set previous edits entry - if($oEntry instanceof EditsEntry) - { + if ($oEntry instanceof EditsEntry) { $oPreviousEditsEntry = $oEntry; } } $iPreviousChangeId = $iChangeId; } + unset($oChangesSet); + + // Retrieving notification events for cmdbAbstractObject only + if ($oObject instanceof cmdbAbstractObject) { + $aRelatedTriggersIDs = $oObject->GetRelatedTriggersIDs(); + + // Protection for classes which have no related trigger + if (false === empty($aRelatedTriggersIDs)) { + // - Prepare query to retrieve events + $oNotifEventsSearch = DBObjectSearch::FromOQL('SELECT EN FROM EventNotification AS EN JOIN Action AS A ON EN.action_id = A.id WHERE EN.trigger_id IN (:triggers_ids) AND EN.object_id = :object_id'); + $oNotifEventsSet = new DBObjectSet($oNotifEventsSearch, ['id' => false], ['triggers_ids' => $aRelatedTriggersIDs, 'object_id' => $iObjId]); + $oNotifEventsSet->SetLimit(MetaModel::GetConfig()->Get('max_history_length')); + + /** @var \EventNotification $oNotifEvent */ + while ($oNotifEvent = $oNotifEventsSet->Fetch()) { + try { + $oEntry = ActivityEntryFactory::MakeFromEventNotification($oNotifEvent); + } + catch (Exception $oException) { + IssueLog::Debug(static::class.': Could not create entry from EventNotification: '.$oException->getMessage()); + continue; + } + + $oActivityPanel->AddEntry($oEntry); + } + unset($oNotifEventsSet); + } + } return $oActivityPanel; } diff --git a/templates/base/layouts/activity-panel/activity-entry/notification-entry.html.twig b/templates/base/layouts/activity-panel/activity-entry/notification-entry.html.twig new file mode 100644 index 000000000..b859b523d --- /dev/null +++ b/templates/base/layouts/activity-panel/activity-entry/notification-entry.html.twig @@ -0,0 +1,14 @@ +{% extends 'base/layouts/activity-panel/activity-entry/layout.html.twig' %} + +{% block iboActivityEntryExtraClasses %}ibo-notification-entry{% endblock %} + +{% block iboActivityEntryMainInformationContent %} + + {{ oUIBlock.GetTitle() }} + + +
+ {# Note: The #tab_UINotificationsTab is hardcoded for now but we should find a way to make the connection with how it is prepared in the object's details #} + {{ oUIBlock.GetMessage() }} +
+{% endblock %} \ No newline at end of file