mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
Merge remote-tracking branch 'origin/support/3.2' into develop
This commit is contained in:
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -64,7 +64,7 @@ Don't remove these lines, check them once done.
|
||||
-->
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have tested all changes I made on an iTop instance
|
||||
- [ ] Would a unit test be relevant and have I added it?
|
||||
- [ ] I have added a unit test, otherwise I have explained why I couldn't
|
||||
- [ ] Is the PR clear and detailled enough so anyone can understand digging in the code?
|
||||
|
||||
## Checklist of things to do before PR is ready to merge
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
use Combodo\iTop\Application\TwigBase\Twig\TwigHelper;
|
||||
use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableUIBlockFactory;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
use Combodo\iTop\Service\Notification\NotificationsRepository;
|
||||
use Combodo\iTop\Service\Notification\NotificationsService;
|
||||
use Combodo\iTop\Service\Router\Router;
|
||||
|
||||
/**
|
||||
@@ -342,6 +344,7 @@ class ActionEmail extends ActionNotification
|
||||
"db_table" => "priv_action_email",
|
||||
"db_key_field" => "id",
|
||||
"db_finalclass_field" => "",
|
||||
'style' => new ormStyle(null, null, null, null, null, '../images/icons/icons8-mailing.svg'),
|
||||
);
|
||||
MetaModel::Init_Params($aParams);
|
||||
MetaModel::Init_InheritAttributes();
|
||||
@@ -412,18 +415,20 @@ class ActionEmail extends ActionNotification
|
||||
protected $m_aMailErrors; //array of strings explaining the issue
|
||||
|
||||
/**
|
||||
* Return a the list of emails as a string, or a detailed error description
|
||||
* Return the list of emails as a string, or a detailed error description
|
||||
*
|
||||
* @param string $sRecipAttCode
|
||||
* @param array $aArgs
|
||||
* @param \Trigger|null $oTrigger
|
||||
*
|
||||
* @return string
|
||||
* @since 3.2.0 $oTrigger parameter added
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
protected function FindRecipients($sRecipAttCode, $aArgs)
|
||||
protected function FindRecipients($sRecipAttCode, $aArgs, ?Trigger $oTrigger = null)
|
||||
{
|
||||
$sOQL = $this->Get($sRecipAttCode);
|
||||
if (utils::IsNullOrEmptyString($sOQL)) return '';
|
||||
@@ -432,7 +437,7 @@ class ActionEmail extends ActionNotification
|
||||
{
|
||||
$oSearch = DBObjectSearch::FromOQL($sOQL);
|
||||
if ($this->Get('ignore_notify') === 'no') {
|
||||
// In theory it is possible to notify *any* kind of object,
|
||||
// In theory, it is possible to notify *any* kind of object,
|
||||
// as long as there is an email attribute in the class
|
||||
// So let's not assume that the selected class is a Person
|
||||
$sFirstSelectedClass = $oSearch->GetClass();
|
||||
@@ -465,6 +470,16 @@ class ActionEmail extends ActionNotification
|
||||
return "The objects of the class '$sClass' do not have any email attribute";
|
||||
}
|
||||
|
||||
if(in_array('Contact', MetaModel::EnumParentClasses($sClass, ENUM_CHILD_CLASSES_ALL), true)) {
|
||||
$aArgs['trigger_id'] = $oTrigger->GetKey();
|
||||
$aArgs['action_id'] = $this->GetKey();
|
||||
|
||||
$sSubscribedContactsOQL = NotificationsRepository::GetInstance()->GetSearchOQLContactUnsubscribedByTriggerAndAction();
|
||||
$sSubscribedContactsOQL->ApplyParameters($aArgs);
|
||||
$sAlias = $oSearch->GetClassAlias();
|
||||
$oSearch->AddConditionExpression(Expression::FromOQL("`$sAlias`.id NOT IN ($sSubscribedContactsOQL)"));
|
||||
}
|
||||
|
||||
$oSet = new DBObjectSet($oSearch, array() /* order */, $aArgs);
|
||||
$aRecipients = array();
|
||||
while ($oObj = $oSet->Fetch())
|
||||
@@ -475,6 +490,9 @@ class ActionEmail extends ActionNotification
|
||||
$aRecipients[] = $sAddress;
|
||||
$this->m_iRecipients++;
|
||||
}
|
||||
if (in_array('Contact', MetaModel::EnumParentClasses($sClass, ENUM_CHILD_CLASSES_ALL), true)) {
|
||||
NotificationsService::GetInstance()->RegisterSubscription($oTrigger, $this, $oObj);
|
||||
}
|
||||
}
|
||||
return implode(', ', $aRecipients);
|
||||
}
|
||||
@@ -569,7 +587,7 @@ class ActionEmail extends ActionNotification
|
||||
|
||||
$oEmail = new EMail();
|
||||
|
||||
$aEmailContent = $this->PrepareMessageContent($aContextArgs, $oLog);
|
||||
$aEmailContent = $this->PrepareMessageContent($aContextArgs, $oLog, $oTrigger);
|
||||
$oEmail->SetSubject($aEmailContent['subject']);
|
||||
$oEmail->SetBody($aEmailContent['body'], 'text/html', $sStyles);
|
||||
$oEmail->SetRecipientTO($aEmailContent['to']);
|
||||
@@ -621,13 +639,19 @@ class ActionEmail extends ActionNotification
|
||||
/**
|
||||
* @param array $aContextArgs
|
||||
* @param \EventNotification $oLog
|
||||
* @param \Trigger|null $oTrigger
|
||||
*
|
||||
* @return array
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \Exception
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \DictExceptionMissingString
|
||||
* @throws \DictExceptionUnknownLanguage
|
||||
* @throws \MySQLException
|
||||
* @since 3.1.0 N°918
|
||||
* @since 3.2.0 Added $oTrigger parameter
|
||||
*/
|
||||
protected function PrepareMessageContent($aContextArgs, &$oLog): array
|
||||
protected function PrepareMessageContent($aContextArgs, &$oLog, ?Trigger $oTrigger = null): array
|
||||
{
|
||||
$aMessageContent = [
|
||||
'to' => '',
|
||||
@@ -654,9 +678,9 @@ class ActionEmail extends ActionNotification
|
||||
|
||||
// Determine recipients
|
||||
//
|
||||
$aMessageContent['to'] = $this->FindRecipients('to', $aContextArgs);
|
||||
$aMessageContent['cc'] = $this->FindRecipients('cc', $aContextArgs);
|
||||
$aMessageContent['bcc'] = $this->FindRecipients('bcc', $aContextArgs);
|
||||
$aMessageContent['to'] = $this->FindRecipients('to', $aContextArgs, $oTrigger);
|
||||
$aMessageContent['cc'] = $this->FindRecipients('cc', $aContextArgs, $oTrigger);
|
||||
$aMessageContent['bcc'] = $this->FindRecipients('bcc', $aContextArgs, $oTrigger);
|
||||
|
||||
$aMessageContent['from'] = MetaModel::ApplyParams($this->Get('from'), $aContextArgs);
|
||||
$aMessageContent['from_label'] = MetaModel::ApplyParams($this->Get('from_label'), $aContextArgs);
|
||||
|
||||
@@ -1,6 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.2">
|
||||
<classes>
|
||||
<class id="lnkActionNotificationToContact" _delta="define">
|
||||
<parent>cmdbAbstractObject</parent>
|
||||
<properties>
|
||||
<category>core/cmdb,application</category>
|
||||
<abstract>false</abstract>
|
||||
<key_type>autoincrement</key_type>
|
||||
<db_table>priv_lnk_action_notif_to_contact</db_table>
|
||||
<db_key_field>id</db_key_field>
|
||||
<db_final_class_field/>
|
||||
<naming>
|
||||
<attributes>
|
||||
<attribute id="action_id"/>
|
||||
<attribute id="contact_id"/>
|
||||
</attributes>
|
||||
</naming>
|
||||
<uniqueness_rules>
|
||||
<rule>
|
||||
<attributes>
|
||||
<attribute id="action_id"/>
|
||||
<attribute id="contact_id"/>
|
||||
<attribute id="trigger_id"/>
|
||||
</attributes>
|
||||
<filter/>
|
||||
<disabled>false</disabled>
|
||||
<is_blocking>true</is_blocking>
|
||||
</rule>
|
||||
</uniqueness_rules>
|
||||
</properties>
|
||||
<fields>
|
||||
<field id="action_id" xsi:type="AttributeExternalKey">
|
||||
<sql>action_id</sql>
|
||||
<target_class>ActionNotification</target_class>
|
||||
<default_value/>
|
||||
<is_null_allowed>false</is_null_allowed>
|
||||
</field>
|
||||
<field id="contact_id" xsi:type="AttributeExternalKey">
|
||||
<sql>contact_id</sql>
|
||||
<target_class>Contact</target_class>
|
||||
<default_value/>
|
||||
<is_null_allowed>false</is_null_allowed>
|
||||
</field>
|
||||
<field id="trigger_id" xsi:type="AttributeExternalKey">
|
||||
<sql>trigger_id</sql>
|
||||
<target_class>Trigger</target_class>
|
||||
<default_value/>
|
||||
<is_null_allowed>false</is_null_allowed>
|
||||
</field>
|
||||
<field id="subscribed" xsi:type="AttributeBoolean">
|
||||
<sql>subscribed</sql>
|
||||
<default_value>true</default_value>
|
||||
<is_null_allowed>false</is_null_allowed>
|
||||
</field>
|
||||
</fields>
|
||||
<presentation>
|
||||
<details>
|
||||
<items>
|
||||
<item id="col:col1">
|
||||
<items>
|
||||
<item id="fieldset:lnkActionNotificationToContact:content">
|
||||
<items>
|
||||
<item id="action_id">
|
||||
<rank>10</rank>
|
||||
</item>
|
||||
<item id="contact_id">
|
||||
<rank>20</rank>
|
||||
</item>
|
||||
<item id="title">
|
||||
<rank>30</rank>
|
||||
</item>
|
||||
</items>
|
||||
</item>
|
||||
</items>
|
||||
</item>
|
||||
</items>
|
||||
</details>
|
||||
<list>
|
||||
<items>
|
||||
<item id="action_id">
|
||||
<rank>10</rank>
|
||||
</item>
|
||||
<item id="contact_id">
|
||||
<rank>20</rank>
|
||||
</item>
|
||||
<item id="title">
|
||||
<rank>30</rank>
|
||||
</item>
|
||||
</items>
|
||||
</list>
|
||||
</presentation>
|
||||
<methods/>
|
||||
</class>
|
||||
<class id="ActioniTopNotification" _delta="define">
|
||||
<php_parent>
|
||||
<name>ActionNotification</name>
|
||||
@@ -18,6 +109,9 @@
|
||||
<attribute id="title"/>
|
||||
</attributes>
|
||||
</naming>
|
||||
<style>
|
||||
<icon>../../images/icons/icons8-notification.svg</icon>
|
||||
</style>
|
||||
</properties>
|
||||
<fields>
|
||||
<field id="title" xsi:type="AttributeString">
|
||||
@@ -94,6 +188,13 @@
|
||||
</item>
|
||||
</items>
|
||||
</item>
|
||||
<item id="fieldset:ActioniTopNotification:trigger">
|
||||
<items>
|
||||
<item id="trigger_list">
|
||||
<rank>10</rank>
|
||||
</item>
|
||||
</items>
|
||||
</item>
|
||||
</items>
|
||||
</item>
|
||||
<item id="col:col2">
|
||||
@@ -159,6 +260,13 @@
|
||||
$oRecipientsSet = new DBObjectSet($oRecipientsSearch);
|
||||
[$sPreviousLanguage, $aPreviousPluginProperties] = $this->SetNotificationLanguage();
|
||||
while ($oRecipient = $oRecipientsSet->Fetch()) {
|
||||
// Skip recipients that have no users
|
||||
if (get_class($oRecipient) === Person::class && UserRights::GetUserFromPerson($oRecipient) === null) {
|
||||
continue;
|
||||
}
|
||||
if (!\Combodo\iTop\Service\Notification\NotificationsService::GetInstance()->IsSubscribed($oTrigger, $this, $oRecipient)) {
|
||||
continue;
|
||||
}
|
||||
$oEvent = new EventiTopNotification();
|
||||
$oEvent->Set('title', MetaModel::ApplyParams($this->Get('title'), $aContextArgs));
|
||||
$oEvent->Set('message', MetaModel::ApplyParams($this->Get('message'), $aContextArgs));
|
||||
@@ -172,6 +280,8 @@
|
||||
$oEvent->Set('object_id', $iObjectId);
|
||||
$oEvent->Set('url', MetaModel::ApplyParams($this->Get('url'), $aContextArgs));
|
||||
$oEvent->DBInsertNoReload();
|
||||
|
||||
\Combodo\iTop\Service\Notification\NotificationsService::GetInstance()->RegisterSubscription($oTrigger, $this, $oRecipient);
|
||||
}
|
||||
$this->SetNotificationLanguage($sPreviousLanguage, $aPreviousPluginProperties['language_code'] ?? null);
|
||||
}
|
||||
|
||||
@@ -53,9 +53,10 @@ abstract class Trigger extends cmdbAbstractObject
|
||||
MetaModel::Init_AddAttribute(new AttributeEnumSet("context", array("allowed_values" => null, "possible_values" => new ValueSetEnumPadded($aTags, true), "sql" => "context", "depends_on" => array(), "is_null_allowed" => true, "max_items" => 12)));
|
||||
// "complement" is a computed field, fed by Trigger sub-classes, in general in ComputeValues method, for eg. the TriggerOnObject fed it with target_class info
|
||||
MetaModel::Init_AddAttribute(new AttributeString("complement", array("allowed_values" => null, "sql" => "complement", "default_value" => null, "is_null_allowed" => true, "depends_on" => array())));
|
||||
MetaModel::Init_AddAttribute(new AttributeEnum("subscription_policy", array("allowed_values" => new ValueSetEnum('allow_no_channel,force_at_least_one_channel,force_all_channels'), "sql" => "subscription_policy", "default_value" => 'allow_no_channel', "is_null_allowed" => false, "depends_on" => array())));
|
||||
|
||||
// Display lists
|
||||
MetaModel::Init_SetZListItems('details', array('finalclass', 'description', 'context', 'action_list', 'complement')); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('details', array('finalclass', 'description', 'context', 'subscription_policy', 'action_list', 'complement')); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('list', array('finalclass', 'complement')); // Attributes to be displayed for a list
|
||||
// Search criteria
|
||||
// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
|
||||
@@ -438,7 +439,7 @@ class TriggerOnStateEnter extends TriggerOnStateChange
|
||||
MetaModel::Init_InheritAttributes();
|
||||
|
||||
// Display lists
|
||||
MetaModel::Init_SetZListItems('details', array('description', 'context', 'target_class', 'filter', 'state', 'action_list')); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('details', array('description', 'context', 'target_class', 'filter', 'state', 'subscription_policy', 'action_list')); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('list', array('target_class', 'state')); // Attributes to be displayed for a list
|
||||
// Search criteria
|
||||
MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class', 'state')); // Criteria of the std search form
|
||||
@@ -471,7 +472,7 @@ class TriggerOnStateLeave extends TriggerOnStateChange
|
||||
MetaModel::Init_InheritAttributes();
|
||||
|
||||
// Display lists
|
||||
MetaModel::Init_SetZListItems('details', array('description', 'context', 'target_class', 'filter', 'state', 'action_list')); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('details', array('description', 'context', 'target_class', 'filter', 'state', 'subscription_policy', 'action_list')); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('list', array('target_class', 'state')); // Attributes to be displayed for a list
|
||||
// Search criteria
|
||||
MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class', 'state')); // Criteria of the std search form
|
||||
@@ -504,7 +505,7 @@ class TriggerOnObjectCreate extends TriggerOnObject
|
||||
MetaModel::Init_InheritAttributes();
|
||||
|
||||
// Display lists
|
||||
MetaModel::Init_SetZListItems('details', array('description', 'context', 'target_class', 'filter', 'action_list')); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('details', array('description', 'context', 'target_class', 'filter', 'subscription_policy', 'action_list')); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class')); // Attributes to be displayed for a list
|
||||
// Search criteria
|
||||
MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class')); // Criteria of the std search form
|
||||
@@ -537,7 +538,7 @@ class TriggerOnObjectDelete extends TriggerOnObject
|
||||
MetaModel::Init_InheritAttributes();
|
||||
|
||||
// Display lists
|
||||
MetaModel::Init_SetZListItems('details', array('description', 'context', 'target_class', 'filter', 'action_list')); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('details', array('description', 'context', 'target_class', 'filter', 'subscription_policy', 'action_list')); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class')); // Attributes to be displayed for a list
|
||||
// Search criteria
|
||||
MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class')); // Criteria of the std search form
|
||||
@@ -572,7 +573,7 @@ class TriggerOnObjectUpdate extends TriggerOnObject
|
||||
MetaModel::Init_AddAttribute(new AttributeClassAttCodeSet('target_attcodes', array("allowed_values" => null, "class_field" => "target_class", "sql" => "target_attcodes", "default_value" => null, "is_null_allowed" => true, "max_items" => 20, "min_items" => 0, "attribute_definition_exclusion_list" => "AttributeDashboard,AttributeExternalField,AttributeFinalClass,AttributeFriendlyName,AttributeObsolescenceDate,AttributeObsolescenceFlag,AttributeSubItem", "attribute_definition_list" => null, "depends_on" => array('target_class'))));
|
||||
|
||||
// Display lists
|
||||
MetaModel::Init_SetZListItems('details', array('description', 'context', 'target_class', 'filter', 'target_attcodes', 'action_list')); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('details', array('description', 'context', 'target_class', 'filter', 'target_attcodes', 'subscription_policy', 'action_list')); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class')); // Attributes to be displayed for a list
|
||||
// Search criteria
|
||||
MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class')); // Criteria of the std search form
|
||||
@@ -668,7 +669,7 @@ class TriggerOnObjectMention extends TriggerOnObject
|
||||
MetaModel::Init_AddAttribute(new AttributeOQL("mentioned_filter", array("allowed_values" => null, "sql" => "mentioned_filter", "default_value" => null, "is_null_allowed" => true, "depends_on" => array())));
|
||||
|
||||
// Display lists
|
||||
MetaModel::Init_SetZListItems('details', array('description', 'context', 'target_class', 'filter', 'mentioned_filter', 'action_list')); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('details', array('description', 'context', 'target_class', 'filter', 'mentioned_filter', 'subscription_policy', 'action_list')); // Attributes to be displayed for the complete details
|
||||
MetaModel::Init_SetZListItems('list', array('finalclass', 'target_class')); // Attributes to be displayed for a list
|
||||
// Search criteria
|
||||
MetaModel::Init_SetZListItems('standard_search', array('description', 'target_class')); // Criteria of the std search form
|
||||
|
||||
@@ -224,7 +224,11 @@ $ibo-input-select--autocomplete-item-image--border: 1px solid $ibo-color-grey-60
|
||||
margin-right: $ibo-input-select--autocomplete-item-image--margin-right;
|
||||
background-color: $ibo-input-select--autocomplete-item-image--background-color;
|
||||
border: $ibo-input-select--autocomplete-item-image--border;
|
||||
|
||||
&.ibo-is-not-medallion{
|
||||
border: unset;
|
||||
border-radius: 0;
|
||||
background-color: unset;
|
||||
}
|
||||
@extend %ibo-fully-centered-content;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,4 +15,5 @@
|
||||
@import "global-search";
|
||||
@import "run-query";
|
||||
@import "welcome-popup";
|
||||
@import "oauth.wizard";
|
||||
@import "oauth.wizard";
|
||||
@import "notifications-center";
|
||||
18
css/backoffice/pages/_notifications-center.scss
Normal file
18
css/backoffice/pages/_notifications-center.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
$ibo-input-select--notification-item--mixed-value--color: $ibo-color-primary-800 !default;
|
||||
$ibo-input-select--notification-item--mixed-value--margin-left: 4px !default;
|
||||
|
||||
.ibo-input-select--notification-item {
|
||||
display: flex !important; // override selectize default display with a stronger rule
|
||||
flex-direction: row;
|
||||
@extend .ibo-input-select--autocomplete-item
|
||||
}
|
||||
|
||||
.ibo-input-select--notification-item--mixed-value{
|
||||
font-size: $ibo-font-size-100;
|
||||
color: $ibo-input-select--notification-item--mixed-value--color;
|
||||
margin-left: $ibo-input-select--notification-item--mixed-value--margin-left;
|
||||
}
|
||||
@@ -1482,7 +1482,10 @@
|
||||
</reconciliation>
|
||||
</properties>
|
||||
<fields>
|
||||
<field id="file" xsi:type="AttributeBlob"/>
|
||||
<field id="file" xsi:type="AttributeBlob">
|
||||
<sql>file</sql>
|
||||
<is_null_allowed>true</is_null_allowed>
|
||||
</field>
|
||||
</fields>
|
||||
<methods>
|
||||
<method id="DisplayBareRelations">
|
||||
|
||||
@@ -140,6 +140,10 @@
|
||||
<on_target_delete>DEL_AUTO</on_target_delete>
|
||||
</field>
|
||||
<field id="team_name" xsi:type="AttributeExternalField">
|
||||
<extkey_attcode>team_id</extkey_attcode>
|
||||
<target_attcode>name</target_attcode>
|
||||
</field>
|
||||
<field id="team_email" xsi:type="AttributeExternalField">
|
||||
<extkey_attcode>team_id</extkey_attcode>
|
||||
<target_attcode>email</target_attcode>
|
||||
</field>
|
||||
|
||||
@@ -633,9 +633,10 @@ While editing, click on the magnifier to get pertinent examples',
|
||||
//
|
||||
|
||||
Dict::Add('EN US', 'English', 'English', array(
|
||||
'ActioniTopNotification:content' => 'Content',
|
||||
'ActioniTopNotification:trigger' => 'Trigger',
|
||||
'ActioniTopNotification:content' => 'Message',
|
||||
'ActioniTopNotification:settings' => 'Settings',
|
||||
'Class:ActioniTopNotification' => 'ActioniTopNotification',
|
||||
'Class:ActioniTopNotification' => ITOP_APPLICATION_SHORT.' Notification',
|
||||
'Class:ActioniTopNotification+' => '',
|
||||
'Class:ActioniTopNotification/Attribute:language' => 'Language',
|
||||
'Class:ActioniTopNotification/Attribute:language+' => '',
|
||||
@@ -679,6 +680,11 @@ Dict::Add('EN US', 'English', 'English', array(
|
||||
'Class:Trigger/Attribute:context+' => 'Context to allow the trigger to start',
|
||||
'Class:Trigger/Attribute:complement' => 'Additional information',
|
||||
'Class:Trigger/Attribute:complement+' => 'Further information as provided in english, by this trigger',
|
||||
'Class:Trigger/Attribute:subscription_policy' => 'Subscription policy',
|
||||
'Class:Trigger/Attribute:subscription_policy+' => 'Allows users to unsubscribe from the trigger',
|
||||
'Class:Trigger/Attribute:subscription_policy/Value:allow_no_channel' => 'Allow no channel',
|
||||
'Class:Trigger/Attribute:subscription_policy/Value:force_at_least_one_channel' => 'Force at least one channel',
|
||||
'Class:Trigger/Attribute:subscription_policy/Value:force_all_channels' => 'Force all channels',
|
||||
));
|
||||
|
||||
//
|
||||
|
||||
@@ -584,7 +584,10 @@ En édition, cliquez sur la loupe pour obtenir des exemples pertinents.',
|
||||
//
|
||||
|
||||
Dict::Add('FR FR', 'French', 'Français', array(
|
||||
'Class:ActioniTopNotification' => 'ActioniTopNotification',
|
||||
'ActioniTopNotification:trigger' => 'Conditions de déclenchement',
|
||||
'ActioniTopNotification:content' => 'Message',
|
||||
'ActioniTopNotification:settings' => 'Paramètres',
|
||||
'Class:ActioniTopNotification' => ITOP_APPLICATION_SHORT.' Notification',
|
||||
'Class:ActioniTopNotification+' => '',
|
||||
'Class:ActioniTopNotification/Attribute:title' => 'Titre',
|
||||
'Class:ActioniTopNotification/Attribute:title+' => '',
|
||||
|
||||
@@ -20,4 +20,5 @@
|
||||
// Input
|
||||
Dict::Add('EN US', 'English', 'English', array(
|
||||
'UI:Component:Input:Password:DoesNotMatch' => 'Passwords do not match',
|
||||
'UI:Component:Input:Set:MinimumItems' => 'Minimum %1$s items required',
|
||||
));
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2013-2024 Combodo SARL
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* iTop is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
|
||||
Dict::Add('EN US', 'English', 'English', array(
|
||||
'UI:NotificationsCenter:Page:Title' => 'Notifications center',
|
||||
'UI:NotificationsCenter:Panel:Title' => 'Notifications center',
|
||||
'UI:NotificationsCenter:Panel:SubTitle' => 'Manage your notifications subscriptions. For more granularity, you can also manage individual subscriptions using the <a href="%1$s">advanced mode</a>.',
|
||||
'UI:NotificationsCenter:Panel:Advanced:SubTitle' => 'Manage your notifications subscriptions individually. To manage your subscriptions by type, use the <a href="%1$s">standard mode</a>.',
|
||||
'UI:NotificationsCenter:Panel:Table:Channels' => 'Channels.',
|
||||
'UI:NotificationsCenter:Unsubscribe:Success' => 'You have been successfully unsubscribed from the selected notifications.',
|
||||
'UI:NotificationsCenter:Unsubscribe:Error' => 'An error occurred while unsubscribing from the selected notifications.',
|
||||
'UI:NotificationsCenter:Subscribe:Success' => 'You have been successfully subscribed to the selected notifications.',
|
||||
'UI:NotificationsCenter:Subscribe:Error' => 'An error occurred while subscribing to the selected notifications.',
|
||||
'UI:NotificationsCenter:Channel:OutOf:Text' => '%1$s out of %2$s',
|
||||
'UI:NotificationsCenter:Advanced:Input:Label' => '%1$s: %2$s',
|
||||
));
|
||||
@@ -51,4 +51,7 @@ Dict::Add('EN US', 'English', 'English', array(
|
||||
'UI:Preferences:General:Toasts:Top' => 'Top',
|
||||
'UI:Preferences:ChooseAPlaceholder' => 'User placeholder image',
|
||||
'UI:Preferences:ChooseAPlaceholder+' => 'Choose a placeholder image that will be displayed if the contact linked to your user doesn\'t have one',
|
||||
'UI:Preferences:Notifications' => 'Notifications',
|
||||
'UI:Preferences:Notifications+' => 'Configure the notifications you want to receive <a href="%1$s">on this page</a>.',
|
||||
|
||||
));
|
||||
|
||||
1
images/icons/icons8-mailing.svg
Normal file
1
images/icons/icons8-mailing.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"><linearGradient id="AY6Uvh9Cgiro1p2xH7xe2a" x1="25.633" x2="48.431" y1="17.912" y2="52.033" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#28afea"/><stop offset="1" stop-color="#0b88da"/></linearGradient><path fill="url(#AY6Uvh9Cgiro1p2xH7xe2a)" d="M13.714,25.875L48,12.461v24.664C48,38.161,47.148,39,46.095,39H13.714V25.875z"/><linearGradient id="AY6Uvh9Cgiro1p2xH7xe2b" x1="6.557" x2="39.664" y1="24.411" y2="45.033" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#28afea"/><stop offset="1" stop-color="#0b88da"/></linearGradient><path fill="url(#AY6Uvh9Cgiro1p2xH7xe2b)" d="M8,12.461v24.664C8,38.161,8.852,39,9.905,39h36.19c0.468,0,0.89-0.172,1.222-0.448L8,12.461 z"/><path d="M8,11.813h40v1.586L31.505,26.008c-2.062,1.576-4.948,1.576-7.01,0 L8,13.399V11.813z" opacity=".05"/><path d="M8,11.344h40v1.586L30.825,25.222c-1.678,1.223-3.971,1.223-5.65,0 L8,12.93V11.344z" opacity=".07"/><path fill="#50e6ff" d="M9.905,9h36.19C47.148,9,48,9.839,48,10.875v1.586L30.145,24.437c-1.294,0.868-2.996,0.868-4.29,0 L8,12.461v-1.586C8,9.839,8.852,9,9.905,9z"/><path d="M15.5,26H8v5h7.5c0.827,0,1.5-0.673,1.5-1.5v-2C17,26.673,16.327,26,15.5,26z" opacity=".05"/><path d="M15.5,31H8v5h7.5c0.827,0,1.5-0.673,1.5-1.5v-2C17,31.673,16.327,31,15.5,31z" opacity=".05"/><path d="M15.5,31.5H8v4h7.5c0.552,0,1-0.449,1-1v-2C16.5,31.949,16.052,31.5,15.5,31.5z" opacity=".07"/><path d="M15.5,26.5H8v4h7.5c0.552,0,1-0.449,1-1v-2C16.5,26.949,16.052,26.5,15.5,26.5z" opacity=".07"/><path d="M15.5,21H8v5h7.5c0.827,0,1.5-0.673,1.5-1.5v-2C17,21.673,16.327,21,15.5,21z" opacity=".05"/><linearGradient id="AY6Uvh9Cgiro1p2xH7xe2c" x1="0" x2="16" y1="33.5" y2="33.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#5961c3"/><stop offset="1" stop-color="#3a41ac"/></linearGradient><path fill="url(#AY6Uvh9Cgiro1p2xH7xe2c)" d="M15.5,35h-15C0.224,35,0,34.776,0,34.5v-2C0,32.224,0.224,32,0.5,32h15 c0.276,0,0.5,0.224,0.5,0.5v2C16,34.776,15.776,35,15.5,35z"/><path d="M15.5,21.5H8v4h7.5c0.552,0,1-0.449,1-1v-2C16.5,21.949,16.052,21.5,15.5,21.5z" opacity=".07"/><linearGradient id="AY6Uvh9Cgiro1p2xH7xe2d" x1="2" x2="16" y1="28.5" y2="28.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#5961c3"/><stop offset="1" stop-color="#3a41ac"/></linearGradient><path fill="url(#AY6Uvh9Cgiro1p2xH7xe2d)" d="M15.5,30h-13C2.224,30,2,29.776,2,29.5v-2C2,27.224,2.224,27,2.5,27h13 c0.276,0,0.5,0.224,0.5,0.5v2C16,29.776,15.776,30,15.5,30z"/><linearGradient id="AY6Uvh9Cgiro1p2xH7xe2e" x1="5" x2="16" y1="23.5" y2="23.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#5961c3"/><stop offset="1" stop-color="#3a41ac"/></linearGradient><path fill="url(#AY6Uvh9Cgiro1p2xH7xe2e)" d="M15.5,25h-10C5.224,25,5,24.776,5,24.5v-2C5,22.224,5.224,22,5.5,22h10 c0.276,0,0.5,0.224,0.5,0.5v2C16,24.776,15.776,25,15.5,25z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
1
images/icons/icons8-notification.svg
Normal file
1
images/icons/icons8-notification.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="48px" height="48px"><defs><linearGradient x1="24" y1="1.993" x2="24" y2="7.005" gradientUnits="userSpaceOnUse" id="color-1"><stop offset="0" stop-color="#fede00"></stop><stop offset="1" stop-color="#ffd000"></stop></linearGradient><linearGradient x1="24" y1="33.993" x2="24" y2="39.005" gradientUnits="userSpaceOnUse" id="color-2"><stop offset="0" stop-color="#fede00"></stop><stop offset="1" stop-color="#ffd000"></stop></linearGradient><linearGradient x1="24" y1="42.919" x2="24" y2="38.859" gradientUnits="userSpaceOnUse" id="color-3"><stop offset="0.486" stop-color="#fbc300"></stop><stop offset="1" stop-color="#dbaa00"></stop></linearGradient></defs><g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(5.33333,5.33333)"><path d="M27,7h-6v-2c0,-1.657 1.343,-3 3,-3v0c1.657,0 3,1.343 3,3z" fill="url(#color-1)"></path><path d="M39,21c0,-8.284 -6.716,-15 -15,-15c-8.284,0 -15,6.716 -15,15c0,0.39 0,13 0,13h30c0,0 0,-12.61 0,-13z" fill="#f5be00"></path><path d="M39,34h-30l-3.875,1.55c-0.68,0.272 -1.125,0.93 -1.125,1.661v0c0,0.988 0.801,1.789 1.789,1.789h36.422c0.988,0 1.789,-0.801 1.789,-1.789v0c0,-0.731 -0.445,-1.389 -1.125,-1.661z" fill="url(#color-2)"></path><path d="M28,39c0,2.209 -1.791,4 -4,4c-2.209,0 -4,-1.791 -4,-4z" fill="url(#color-3)"></path></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
26
js/selectize/plugin_combodo_min_items.js
Normal file
26
js/selectize/plugin_combodo_min_items.js
Normal file
@@ -0,0 +1,26 @@
|
||||
Selectize.define("combodo_min_items", function (aOptions) {
|
||||
|
||||
// Selectize instance
|
||||
let oSelf = this;
|
||||
|
||||
// Plugin options
|
||||
aOptions = $.extend({
|
||||
minItems: 0,
|
||||
errorMessage: 'Minimum ' + aOptions.minItems + ' item(s) required.'
|
||||
},
|
||||
aOptions
|
||||
);
|
||||
|
||||
// Override removeItem function
|
||||
oSelf.removeItem = (function () {
|
||||
let oOriginal = oSelf.removeItem;
|
||||
return function () {
|
||||
if(oSelf.items.length <= aOptions.minItems) {
|
||||
CombodoModal.OpenErrorModal(aOptions.errorMessage, []);
|
||||
return;
|
||||
}
|
||||
return oOriginal.apply(this, arguments);
|
||||
}
|
||||
})();
|
||||
|
||||
});
|
||||
@@ -57,7 +57,8 @@ Selectize.define("combodo_update_operations", function (aOptions) {
|
||||
let oOriginal = oSelf.disable;
|
||||
return function () {
|
||||
oOriginal.apply(oSelf, arguments);
|
||||
oSelf.$operationsInput.prop('disabled', true);
|
||||
if(oSelf.$operationsInput !== undefined)
|
||||
oSelf.$operationsInput.prop('disabled', true);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
@@ -797,7 +797,7 @@ const CombodoGlobalToolbox = {
|
||||
}
|
||||
|
||||
// Attribute replacement
|
||||
let aAttrElements = ['title', 'name', 'for'];
|
||||
let aAttrElements = ['title', 'name', 'for', 'src'];
|
||||
aAttrElements.forEach(function(e){
|
||||
$(`[data-template-attr-${e}]`, oElement).each(function(){
|
||||
$(this).attr(e, aData[$(this).attr(`data-template-attr-${e}`)]);
|
||||
|
||||
@@ -395,6 +395,7 @@ return array(
|
||||
'Combodo\\iTop\\Controller\\Links\\LinkSetController' => $baseDir . '/sources/Controller/Links/LinkSetController.php',
|
||||
'Combodo\\iTop\\Controller\\Newsroom\\iTopNewsroomController' => $baseDir . '/sources/Controller/Newsroom/iTopNewsroomController.php',
|
||||
'Combodo\\iTop\\Controller\\Notifications\\ActionController' => $baseDir . '/sources/Controller/Notifications/ActionController.php',
|
||||
'Combodo\\iTop\\Controller\\Notifications\\NotificationsCenterController' => $baseDir . '/sources/Controller/Notifications/NotificationsCenterController.php',
|
||||
'Combodo\\iTop\\Controller\\OAuth\\OAuthLandingController' => $baseDir . '/sources/Controller/OAuth/OAuthLandingController.php',
|
||||
'Combodo\\iTop\\Controller\\PreferencesController' => $baseDir . '/sources/Controller/PreferencesController.php',
|
||||
'Combodo\\iTop\\Controller\\TemporaryObjects\\TemporaryObjectController' => $baseDir . '/sources/Controller/TemporaryObjects/TemporaryObjectController.php',
|
||||
@@ -493,6 +494,8 @@ return array(
|
||||
'Combodo\\iTop\\Service\\Links\\LinksBulkDataPostProcessor' => $baseDir . '/sources/Service/Links/LinksBulkDataPostProcessor.php',
|
||||
'Combodo\\iTop\\Service\\Module\\ModuleService' => $baseDir . '/sources/Service/Module/ModuleService.php',
|
||||
'Combodo\\iTop\\Service\\Notification\\Event\\EventiTopNotificationGC' => $baseDir . '/sources/Service/Notification/Event/EventiTopNotificationGC.php',
|
||||
'Combodo\\iTop\\Service\\Notification\\NotificationsRepository' => $baseDir . '/sources/Service/Notification/NotificationsRepository.php',
|
||||
'Combodo\\iTop\\Service\\Notification\\NotificationsService' => $baseDir . '/sources/Service/Notification/NotificationsService.php',
|
||||
'Combodo\\iTop\\Service\\Router\\Exception\\RouteNotFoundException' => $baseDir . '/sources/Service/Router/Exception/RouteNotFoundException.php',
|
||||
'Combodo\\iTop\\Service\\Router\\Exception\\RouterException' => $baseDir . '/sources/Service/Router/Exception/RouterException.php',
|
||||
'Combodo\\iTop\\Service\\Router\\Router' => $baseDir . '/sources/Service/Router/Router.php',
|
||||
|
||||
@@ -770,6 +770,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\Controller\\Links\\LinkSetController' => __DIR__ . '/../..' . '/sources/Controller/Links/LinkSetController.php',
|
||||
'Combodo\\iTop\\Controller\\Newsroom\\iTopNewsroomController' => __DIR__ . '/../..' . '/sources/Controller/Newsroom/iTopNewsroomController.php',
|
||||
'Combodo\\iTop\\Controller\\Notifications\\ActionController' => __DIR__ . '/../..' . '/sources/Controller/Notifications/ActionController.php',
|
||||
'Combodo\\iTop\\Controller\\Notifications\\NotificationsCenterController' => __DIR__ . '/../..' . '/sources/Controller/Notifications/NotificationsCenterController.php',
|
||||
'Combodo\\iTop\\Controller\\OAuth\\OAuthLandingController' => __DIR__ . '/../..' . '/sources/Controller/OAuth/OAuthLandingController.php',
|
||||
'Combodo\\iTop\\Controller\\PreferencesController' => __DIR__ . '/../..' . '/sources/Controller/PreferencesController.php',
|
||||
'Combodo\\iTop\\Controller\\TemporaryObjects\\TemporaryObjectController' => __DIR__ . '/../..' . '/sources/Controller/TemporaryObjects/TemporaryObjectController.php',
|
||||
@@ -868,6 +869,8 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'Combodo\\iTop\\Service\\Links\\LinksBulkDataPostProcessor' => __DIR__ . '/../..' . '/sources/Service/Links/LinksBulkDataPostProcessor.php',
|
||||
'Combodo\\iTop\\Service\\Module\\ModuleService' => __DIR__ . '/../..' . '/sources/Service/Module/ModuleService.php',
|
||||
'Combodo\\iTop\\Service\\Notification\\Event\\EventiTopNotificationGC' => __DIR__ . '/../..' . '/sources/Service/Notification/Event/EventiTopNotificationGC.php',
|
||||
'Combodo\\iTop\\Service\\Notification\\NotificationsRepository' => __DIR__ . '/../..' . '/sources/Service/Notification/NotificationsRepository.php',
|
||||
'Combodo\\iTop\\Service\\Notification\\NotificationsService' => __DIR__ . '/../..' . '/sources/Service/Notification/NotificationsService.php',
|
||||
'Combodo\\iTop\\Service\\Router\\Exception\\RouteNotFoundException' => __DIR__ . '/../..' . '/sources/Service/Router/Exception/RouteNotFoundException.php',
|
||||
'Combodo\\iTop\\Service\\Router\\Exception\\RouterException' => __DIR__ . '/../..' . '/sources/Service/Router/Exception/RouterException.php',
|
||||
'Combodo\\iTop\\Service\\Router\\Router' => __DIR__ . '/../..' . '/sources/Service/Router/Router.php',
|
||||
|
||||
@@ -23,6 +23,8 @@ use Combodo\iTop\Application\UI\Preferences\BlockShortcuts\BlockShortcuts;
|
||||
use Combodo\iTop\Application\WebPage\ErrorPage;
|
||||
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
use Combodo\iTop\Controller\Notifications\NotificationsCenterController;
|
||||
use Combodo\iTop\Service\Router\Router;
|
||||
|
||||
require_once('../approot.inc.php');
|
||||
require_once(APPROOT.'/application/application.inc.php');
|
||||
@@ -139,6 +141,17 @@ function ValidateOtherSettings()
|
||||
JS
|
||||
);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Notifications
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
$oNotificationsBlock = new Panel(Dict::S('UI:Preferences:Notifications'), array(), Panel::ENUM_COLOR_SCHEME_GREY, 'ibo-notifications');
|
||||
$sNotificationsCenterUrl = Router::GetInstance()->GenerateUrl(NotificationsCenterController::ROUTE_NAMESPACE.'.display_page', [], true);
|
||||
$oNotificationsBlock->AddSubBlock(new Html('<p>'.Dict::Format('UI:Preferences:Notifications+', $sNotificationsCenterUrl).'</p>'));
|
||||
$oContentLayout->AddMainBlock($oNotificationsBlock);
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Favorite Organizations
|
||||
|
||||
@@ -43,10 +43,17 @@ class Set extends AbstractInput
|
||||
'js/selectize/plugin_combodo_auto_position.js',
|
||||
'js/selectize/plugin_combodo_update_operations.js',
|
||||
'js/selectize/plugin_combodo_multi_values_synthesis.js',
|
||||
'js/selectize/plugin_combodo_min_items.js',
|
||||
];
|
||||
|
||||
protected $bIsDisabled = false;
|
||||
|
||||
/** @var int|null $iMaxItems Maximum number of items selectable */
|
||||
private ?int $iMaxItems;
|
||||
/** @var int|null $iMinItems Minimum number of items selectable */
|
||||
|
||||
private ?int $iMinItems;
|
||||
|
||||
|
||||
/** @var int|null $iMaxItem Maximum number of displayed options */
|
||||
private ?int $iMaxOptions;
|
||||
@@ -63,6 +70,18 @@ class Set extends AbstractInput
|
||||
/** @var string $sAddButtonTitle Add button title */
|
||||
private string $sAddButtonTitle;
|
||||
|
||||
/** @var string|null $sOnOptionRemoveJs JS code to execute when an option is no longer among available options */
|
||||
private ?string $sOnOptionRemoveJs;
|
||||
|
||||
/** @var string|null $sOnOptionAddJs JS code to execute when an option is added to the available options */
|
||||
private ?string $sOnOptionAddJs;
|
||||
|
||||
/** @var string|null $sOnItemRemoveJs JS code to execute when a selected item is removed */
|
||||
private ?string $sOnItemRemoveJs;
|
||||
|
||||
/** @var string|null $sOnItemAddJs JS code to execute when a new item is selected */
|
||||
private ?string $sOnItemAddJs;
|
||||
|
||||
/** @var bool $bIsPreloadEnabled Load data at initialization (ajax data provider only) */
|
||||
private bool $bIsPreloadEnabled;
|
||||
|
||||
@@ -107,16 +126,22 @@ class Set extends AbstractInput
|
||||
// @todo BDA placeholder depending on autocomplete activation (search...., click to add...)
|
||||
$this->SetPlaceholder(Dict::S('Core:AttributeSet:placeholder'));
|
||||
$this->iMaxItems = null;
|
||||
$this->iMinItems = null;
|
||||
$this->iMaxOptions = null;
|
||||
$this->bHasRemoveItemButton = true;
|
||||
$this->bHasAddOptionButton = false;
|
||||
$this->sAddOptionButtonJsOnClick = null;
|
||||
$this->sAddButtonTitle = Dict::S('UI:Button:Create');
|
||||
$this->sOnItemAddJs = null;
|
||||
$this->sOnItemRemoveJs = null;
|
||||
$this->sOnOptionAddJs = null;
|
||||
$this->sOnOptionRemoveJs = null;
|
||||
$this->bIsPreloadEnabled = false;
|
||||
$this->sTemplateOptions = null;
|
||||
$this->sTemplateItems = null;
|
||||
$this->bIsMultiValuesSynthesis = false;
|
||||
$this->bHasError = false;
|
||||
$this->bIsDisabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,6 +168,32 @@ class Set extends AbstractInput
|
||||
return $this->iMaxItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetMinItems.
|
||||
*
|
||||
* @param int|null $iMinItems
|
||||
*
|
||||
* @return $this
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public function SetMinItems(?int $iMinItems)
|
||||
{
|
||||
$this->iMinItems = $iMinItems;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetMinItems.
|
||||
*
|
||||
* @return int|null
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public function GetMinItems(): ?int
|
||||
{
|
||||
return $this->iMinItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetMaxOptions.
|
||||
*
|
||||
@@ -458,4 +509,103 @@ class Set extends AbstractInput
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $sOnOptionRemoveJs
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function SetOnOptionRemoveJs(?string $sOnOptionRemoveJs)
|
||||
{
|
||||
$this->sOnOptionRemoveJs = $sOnOptionRemoveJs;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function GetOnOptionRemoveJs(): ?string
|
||||
{
|
||||
return $this->sOnOptionRemoveJs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $sOnOptionAddJs
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function SetOnOptionAddJs(?string $sOnOptionAddJs)
|
||||
{
|
||||
$this->sOnOptionAddJs = $sOnOptionAddJs;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function GetOnOptionAddJs(): ?string
|
||||
{
|
||||
return $this->sOnOptionAddJs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $sOnItemRemoveJs
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function SetOnItemRemoveJs(?string $sOnItemRemoveJs)
|
||||
{
|
||||
$this->sOnItemRemoveJs = $sOnItemRemoveJs;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function GetOnItemRemoveJs(): ?string
|
||||
{
|
||||
return $this->sOnItemRemoveJs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $sOnItemAddJs
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function SetOnItemAddJs(?string $sOnItemAddJs)
|
||||
{
|
||||
$this->sOnItemAddJs = $sOnItemAddJs;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function GetOnItemAddJs(): ?string
|
||||
{
|
||||
return $this->sOnItemAddJs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function IsDisabled(): bool
|
||||
{
|
||||
return $this->bIsDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bIsDisabled
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function SetIsDisabled(bool $bIsDisabled)
|
||||
{
|
||||
$this->bIsDisabled = $bIsDisabled;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ class SetUIBlockFactory extends AbstractUIBlockFactory
|
||||
*
|
||||
* @return \Combodo\iTop\Application\UI\Base\Component\Input\Set\Set
|
||||
*/
|
||||
public static function MakeForSimple(string $sId, array $aOptions, string $sLabelFields, string $sValueField, array $aSearchFields, ?string $sGroupField = null): Set
|
||||
public static function MakeForSimple(string $sId, array $aOptions, string $sLabelFields, string $sValueField, array $aSearchFields, ?string $sGroupField = null, ?string $sTooltipField = null): Set
|
||||
{
|
||||
// Create set ui block
|
||||
$oSetUIBlock = new Set($sId);
|
||||
@@ -67,7 +67,7 @@ class SetUIBlockFactory extends AbstractUIBlockFactory
|
||||
->SetDataLabelField($sLabelFields)
|
||||
->SetDataValueField($sValueField)
|
||||
->SetDataSearchFields($aSearchFields)
|
||||
->SetTooltipField($sLabelFields);
|
||||
->SetTooltipField($sTooltipField ?? $sLabelFields);
|
||||
if ($sGroupField != null) {
|
||||
$oDataProvider->SetGroupField($sGroupField);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,585 @@
|
||||
<?php
|
||||
namespace Combodo\iTop\Controller\Notifications;
|
||||
|
||||
use ActionNotification;
|
||||
use Combodo\iTop\Application\TwigBase\Controller\Controller;
|
||||
use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\Set\Set;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\Set\SetUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Panel\Panel;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
|
||||
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
||||
use Combodo\iTop\Renderer\BlockRenderer;
|
||||
use Combodo\iTop\Service\Notification\NotificationsRepository;
|
||||
use Combodo\iTop\Service\Router\Router;
|
||||
use Dict;
|
||||
use Exception;
|
||||
use MetaModel;
|
||||
use utils;
|
||||
|
||||
|
||||
/**
|
||||
* Class NotificationsCenterController
|
||||
*
|
||||
* @author Stephen Abello <stephen.abello@combodo.com>
|
||||
* @package Combodo\iTop\Controller\Notifications
|
||||
* @since 3.2.0
|
||||
*/
|
||||
class NotificationsCenterController extends Controller
|
||||
{
|
||||
public const ROUTE_NAMESPACE = 'notificationscenter';
|
||||
|
||||
public function CheckPostedCSRF(){
|
||||
$sToken = utils::ReadParam('token', '', true, 'raw_data');
|
||||
return utils::IsTransactionValid($sToken, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a table containing all ActionNotifications that current user is likely to receive and allows to unsubscribe from them.
|
||||
*
|
||||
* @return iTopWebPage
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \ConfigException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \DictExceptionMissingString
|
||||
* @throws \MySQLException
|
||||
* @throws \ReflectionException
|
||||
* @throws \Twig\Error\LoaderError
|
||||
* @throws \Twig\Error\RuntimeError
|
||||
* @throws \Twig\Error\SyntaxError
|
||||
*/
|
||||
public function OperationDisplayPage()
|
||||
{
|
||||
$oPage = new iTopWebPage(Dict::S('UI:NotificationsCenter:Page:Title'));
|
||||
// Create a panel that will contain the table
|
||||
$oNotificationsPanel = new Panel(Dict::S('UI:NotificationsCenter:Panel:Title'), array(), 'grey', 'ibo-notifications-center');
|
||||
$oNotificationsPanel->AddCSSClass('ibo-datatable-panel');
|
||||
$oSubtitleBlock = new UIContentBlock(null, ['ibo-notifications-center--sub-title']);
|
||||
$sDisplayAdvancedPageUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.display_advanced_page', [], true);
|
||||
$oSubtitleBlock->AddSubBlock(new Html(Dict::Format('UI:NotificationsCenter:Panel:SubTitle', $sDisplayAdvancedPageUrl)));
|
||||
$oNotificationsPanel->SetSubTitleBlock($oSubtitleBlock);
|
||||
$oNotificationsCenterTableColumns = [
|
||||
'trigger' => array('label' => MetaModel::GetName('Trigger')),
|
||||
'channels' => array('label' => Dict::S('UI:NotificationsCenter:Panel:Table:Channels')),
|
||||
];
|
||||
|
||||
// Get all subscribed/unsubscribed actions notifications for the current user
|
||||
$oLnkNotificationsSet = NotificationsRepository::GetInstance()->SearchSubscriptionByContact(\UserRights::GetContactId());
|
||||
$oActionsNotificationsByTrigger = [];
|
||||
$aSubscribedActionsNotificationsByTrigger = [];
|
||||
while ($oLnkActionsNotifications = $oLnkNotificationsSet->Fetch()) {
|
||||
$oSubscribedActionNotification = MetaModel::GetObject(ActionNotification::class, $oLnkActionsNotifications->Get('action_id'));
|
||||
$oTrigger = MetaModel::GetObject('Trigger', $oLnkActionsNotifications->Get('trigger_id'));
|
||||
$iTriggerId = $oTrigger->GetKey();
|
||||
// Create an new array for the trigger if it doesn't exist
|
||||
if (!isset($oActionsNotificationsByTrigger[$iTriggerId])) {
|
||||
$oActionsNotificationsByTrigger[$iTriggerId] = [];
|
||||
$aSubscribedActionsNotificationsByTrigger[$iTriggerId] = [];
|
||||
}
|
||||
// Add the action notification to the list of actions notifications for the trigger
|
||||
$oActionsNotificationsByTrigger[$iTriggerId][] = $oSubscribedActionNotification;
|
||||
// Add the subscribed status to the list of subscribed actions notifications for the trigger
|
||||
$aSubscribedActionsNotificationsByTrigger[$iTriggerId][$oSubscribedActionNotification->GetKey()] = $oLnkActionsNotifications->Get('subscribed') || $oTrigger->Get('subscription_policy') === 'force_all_channels';
|
||||
}
|
||||
|
||||
// Build table rows
|
||||
$sSubscribeUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.subscribe_to_channel', [], true);
|
||||
$sUnsubscribeUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.unsubscribe_from_channel', [], true);
|
||||
$aInputSetOptions = [];
|
||||
$aTableRows = [];
|
||||
foreach ($oActionsNotificationsByTrigger as $iTriggerId => $aActionsNotifications) {
|
||||
$oTrigger = MetaModel::GetObject('Trigger', $iTriggerId, false);
|
||||
if ($oTrigger === null) {
|
||||
continue;
|
||||
}
|
||||
$sTriggerSubscriptionPolicy = $oTrigger->Get('subscription_policy');
|
||||
$aChannels = [];
|
||||
$aSetValues = [];
|
||||
// Create a channel for each action notification class and add it to the channels array
|
||||
foreach ($aActionsNotifications as $oActionNotification) {
|
||||
$sNotificationClass = get_class($oActionNotification);
|
||||
// Create a new channel if it doesn't exist for the current action notification class
|
||||
if (!array_key_exists($sNotificationClass, $aChannels)) {
|
||||
$aChannels[$sNotificationClass] = [
|
||||
'friendlyname' => MetaModel::GetName($sNotificationClass),
|
||||
'has_additional_field' => true,
|
||||
'additional_field' => '',
|
||||
'value' => $oTrigger->GetKey().'|'.$sNotificationClass,
|
||||
'has_image' => true,
|
||||
'picture_url' => 'url("'.MetaModel::GetClassIcon($sNotificationClass, false).'")',
|
||||
'subscribed' => $aSubscribedActionsNotificationsByTrigger[$iTriggerId][$oActionNotification->GetKey()] === true,
|
||||
'status' => [$oActionNotification->Get('status') => 1],
|
||||
'class' => 'ibo-is-not-medallion',
|
||||
'total' => 1,
|
||||
'total_subscribed' => $aSubscribedActionsNotificationsByTrigger[$iTriggerId][$oActionNotification->GetKey()] === true ? 1 : 0,
|
||||
'mixed' => false,
|
||||
];
|
||||
} else {
|
||||
// Check if all actions from the same type are subscribed or not
|
||||
if (($aSubscribedActionsNotificationsByTrigger[$iTriggerId][$oActionNotification->GetKey()] === true) !== $aChannels[$sNotificationClass]['subscribed']) {
|
||||
$aChannels[$sNotificationClass]['subscribed'] = 'mixed';
|
||||
}
|
||||
$aChannels[$sNotificationClass]['total']++;
|
||||
$aChannels[$sNotificationClass]['total_subscribed'] += $aSubscribedActionsNotificationsByTrigger[$iTriggerId][$oActionNotification->GetKey()] === true ? 1 : 0;
|
||||
// Count the number of actions with the same status
|
||||
if (isset($aChannels[$sNotificationClass]['status'][$oActionNotification->Get('status')])) {
|
||||
$aChannels[$sNotificationClass]['status'][$oActionNotification->Get('status')]++;
|
||||
} else {
|
||||
$aChannels[$sNotificationClass]['status'][$oActionNotification->Get('status')] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($aChannels as $sNotificationClass => $aChannel) {
|
||||
// Define if all actions from the same type are subscribed or not
|
||||
if ($aChannel['subscribed'] === 'mixed') {
|
||||
$aSetValues[] = $aChannel['value'];
|
||||
$aChannels[$sNotificationClass]['mixed'] = true;
|
||||
$aChannels[$sNotificationClass]['mixed_value'] = Dict::Format('UI:NotificationsCenter:Channel:OutOf:Text', $aChannel['total_subscribed'], $aChannel['total']);
|
||||
} else if ($aChannel['subscribed'] === true) {
|
||||
$aSetValues[] = $aChannel['value'];
|
||||
}
|
||||
|
||||
// Explode status array into a readable string
|
||||
$aChannels[$sNotificationClass]['additional_field'] = implode(', ', array_map(function($iCount, $sStatus) use($sNotificationClass) {
|
||||
return $iCount.' '. MetaModel::GetStateLabel($sNotificationClass, $sStatus);
|
||||
}, $aChannel['status'], array_keys($aChannel['status'])));
|
||||
}
|
||||
// Create a input set for the channels
|
||||
$oChannelSet = SetUIBlockFactory::MakeForSimple('ibochannel'.$oTrigger->GetKey(), array_values($aChannels), 'friendlyname', 'value', ['friendlyname', 'additional_field'], null, 'additional_field');
|
||||
$oChannelSet->SetName('channel-'.$oTrigger->GetKey());
|
||||
$oChannelSet->SetInitialValue(json_encode($aSetValues));
|
||||
$oChannelSet->SetValue(json_encode($aSetValues));
|
||||
$oChannelSet->SetOptionsTemplate('application/object/set/option_renderer.html.twig');
|
||||
$oChannelSet->SetItemsTemplate('application/preferences/notification-center/item_renderer.html.twig');
|
||||
// Disable the input set if the subscription policy is 'force_all_channels'
|
||||
if($sTriggerSubscriptionPolicy === 'force_all_channels'){
|
||||
$oChannelSet->SetIsDisabled(true);
|
||||
}
|
||||
// Add a CSRF Token
|
||||
$sCSRFToken = utils::GetNewTransactionId();
|
||||
$oChannelSet->SetOnItemAddJs(
|
||||
<<<JS
|
||||
let oSelf = this;
|
||||
|
||||
// Send subscribe request
|
||||
$.ajax({
|
||||
url: '{$sSubscribeUrl}',
|
||||
type: 'POST',
|
||||
data: {
|
||||
channel: value,
|
||||
token: '{$sCSRFToken}',
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function (data) {
|
||||
if (data.status === 'success') {
|
||||
// Display success message
|
||||
oSelf.refreshItems();
|
||||
|
||||
CombodoToast.OpenSuccessToast(data.message);
|
||||
}
|
||||
else {
|
||||
CombodoToast.OpenErrorToast(data.message);
|
||||
}
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
CombodoToast.OpenErrorToast(data.message);
|
||||
}
|
||||
});
|
||||
JS
|
||||
);
|
||||
// Set the minimum number of channels to 1 if the subscription policy is 'force_at_least_one_channel'
|
||||
if($sTriggerSubscriptionPolicy === 'force_at_least_one_channel')
|
||||
{
|
||||
$oChannelSet->SetMinItems(1);
|
||||
}
|
||||
$oChannelSet->SetOnItemRemoveJs(
|
||||
<<<JS
|
||||
let oSelf = this;
|
||||
// Send unsubscribe request
|
||||
$.ajax({
|
||||
url: '{$sUnsubscribeUrl}',
|
||||
type: 'POST',
|
||||
data: {
|
||||
channel: value,
|
||||
token: '{$sCSRFToken}',
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function (data) {
|
||||
if (data.status === 'success') {
|
||||
// Display success message
|
||||
CombodoToast.OpenSuccessToast(data.message);
|
||||
// Remove item from set
|
||||
oSelf.options[value]['mixed'] = false;
|
||||
oSelf.clearCache();
|
||||
|
||||
$('#channel$iTriggerId').find('option[value="' + value + '"]').remove();
|
||||
$('#channel$iTriggerId').trigger('change');
|
||||
}
|
||||
else {
|
||||
CombodoToast.OpenErrorToast(data.message);
|
||||
}
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
CombodoToast.OpenErrorToast(data.message);
|
||||
}
|
||||
});
|
||||
JS
|
||||
);
|
||||
// Use a renderer to display the input set in a table row
|
||||
$oBlockRenderer = new BlockRenderer($oChannelSet);
|
||||
|
||||
$aTableRows[] = [
|
||||
'trigger' => $oTrigger->Get('description'),
|
||||
'channels' => $oBlockRenderer->RenderHtml(),
|
||||
'js' => $oBlockRenderer->RenderJsInline($oChannelSet::ENUM_JS_TYPE_ON_READY),
|
||||
];
|
||||
}
|
||||
$oNotificationsCenterTable = DataTableUIBlockFactory::MakeForStaticData('', $oNotificationsCenterTableColumns, $aTableRows, 'ibo-notifications-center--datatable', ['surround_with_panel' => true]);
|
||||
$oNotificationsPanel->AddSubBlock($oNotificationsCenterTable);
|
||||
|
||||
// Add input js on each page draw so when it's refreshed we keep js interactivity
|
||||
foreach ($aTableRows as $aAtt) {
|
||||
$sJS = $aAtt['js'];
|
||||
$oPage->add_ready_script(
|
||||
<<<JS
|
||||
$('#ibo-notifications-center--datatable').on('init.dt draw.dt', function(){
|
||||
$sJS
|
||||
CombodoTooltip.InitAllNonInstantiatedTooltips($(this));
|
||||
});
|
||||
JS
|
||||
);
|
||||
}
|
||||
|
||||
// Add Set JS files to the page as we used a renderer ourselves, they are not added automatically by the page
|
||||
foreach (Set::DEFAULT_JS_FILES_REL_PATH as $sJsFile) {
|
||||
$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().$sJsFile);
|
||||
}
|
||||
|
||||
$oPage->AddSubBlock($oNotificationsPanel);
|
||||
|
||||
return $oPage;
|
||||
}
|
||||
|
||||
public function OperationDisplayAdvancedPage(){
|
||||
$oPage = new iTopWebPage(Dict::S('UI:NotificationsCenter:Page:Title'));
|
||||
// Create a panel that will contain the table
|
||||
$oNotificationsPanel = new Panel(Dict::S('UI:NotificationsCenter:Panel:Title'), array(), 'grey', 'ibo-notifications-center');
|
||||
$oSubtitleBlock = new UIContentBlock(null, ['ibo-notifications-center--sub-title']);
|
||||
$sDisplayAdvancedPageUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.display_page', [], true);
|
||||
$oSubtitleBlock->AddSubBlock(new Html(Dict::Format('UI:NotificationsCenter:Panel:Advanced:SubTitle', $sDisplayAdvancedPageUrl)));
|
||||
$oNotificationsPanel->SetSubTitleBlock($oSubtitleBlock);
|
||||
$oPage->AddUiBlock($oNotificationsPanel);
|
||||
|
||||
// Get all subscribed/unsubscribed actions notifications for the current user
|
||||
$oLnkNotificationsSet = NotificationsRepository::GetInstance()->SearchSubscriptionByContact(\UserRights::GetContactId());
|
||||
$oActionsNotificationsByTrigger = [];
|
||||
$aSubscribedActionsNotificationsByTrigger = [];
|
||||
while ($oLnkActionsNotifications = $oLnkNotificationsSet->Fetch()) {
|
||||
$oSubscribedActionNotification = MetaModel::GetObject(ActionNotification::class, $oLnkActionsNotifications->Get('action_id'));
|
||||
$oTrigger = MetaModel::GetObject('Trigger', $oLnkActionsNotifications->Get('trigger_id'));
|
||||
$iTriggerId = $oTrigger->GetKey();
|
||||
// Create a new array for the trigger if it doesn't exist
|
||||
if (!isset($oActionsNotificationsByTrigger[$iTriggerId])) {
|
||||
$oActionsNotificationsByTrigger[$iTriggerId] = [];
|
||||
$aSubscribedActionsNotificationsByTrigger[$iTriggerId] = [];
|
||||
}
|
||||
// Add the action notification to the list of actions notifications for the trigger
|
||||
$oActionsNotificationsByTrigger[$iTriggerId][] = $oSubscribedActionNotification;
|
||||
// Add the subscribed status to the list of subscribed actions notifications for the trigger
|
||||
$aSubscribedActionsNotificationsByTrigger[$iTriggerId][$oSubscribedActionNotification->GetKey()] = $oLnkActionsNotifications->Get('subscribed') || $oTrigger->Get('subscription_policy') === 'force_all_channels';
|
||||
}
|
||||
|
||||
$oPage->AddTabContainer('NotificationsCenter', '', $oNotificationsPanel);
|
||||
$oPage->SetCurrentTabContainer('NotificationsCenter');
|
||||
// Create a new tab for each trigger
|
||||
foreach ($oActionsNotificationsByTrigger as $iTriggerId => $aActionsNotifications) {
|
||||
$oTrigger = MetaModel::GetObject('Trigger', $iTriggerId, false);
|
||||
if ($oTrigger === null) {
|
||||
continue;
|
||||
}
|
||||
foreach ($aActionsNotifications as $oActionNotification) {
|
||||
$oPage->SetCurrentTab(MetaModel::GetName(get_class($oActionNotification)));
|
||||
$oCheckBox = InputUIBlockFactory::MakeForInputWithLabel(
|
||||
Dict::Format('UI:NotificationsCenter:Advanced:Input:Label', $oTrigger->Get('description'), $oActionNotification->Get('name')),
|
||||
$oTrigger->GetKey().'|'.$oActionNotification->GetKey(),
|
||||
"",
|
||||
$oTrigger->GetKey().'|'.$oActionNotification->GetKey(),
|
||||
"checkbox"
|
||||
);
|
||||
$oCheckBox->GetInput()->SetIsChecked($aSubscribedActionsNotificationsByTrigger[$iTriggerId][$oActionNotification->GetKey()] === true);
|
||||
$oCheckBox->SetBeforeInput(false);
|
||||
$oCheckBox->GetInput()->AddCSSClass('ibo-input--label-right');
|
||||
$oCheckBox->GetInput()->AddCSSClass('ibo-input-checkbox');
|
||||
$oContainer = new UIContentBlock(null, ['ibo-notifications-center-advanced--input--container']);
|
||||
$oContainer->AddSubBlock($oCheckBox);
|
||||
$oPage->AddUiBlock($oContainer);
|
||||
}
|
||||
}
|
||||
$sSubscribeUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.subscribe', [], true);
|
||||
$sUnsubscribeUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.unsubscribe', [], true);
|
||||
$sCSRFToken = utils::GetNewTransactionId();
|
||||
$oPage->add_ready_script(
|
||||
<<<JS
|
||||
$('.ibo-notifications-center-advanced--input--container .ibo-input-checkbox').on('change', function(){
|
||||
let sUrl = '{$sUnsubscribeUrl}';
|
||||
if ($(this).prop("checked")) {
|
||||
sUrl = '{$sSubscribeUrl}'
|
||||
}
|
||||
$.ajax({
|
||||
url: sUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
channel: $(this).attr('name'),
|
||||
token: '{$sCSRFToken}',
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function (data) {
|
||||
if (data.status === 'success') {
|
||||
// Display success message
|
||||
CombodoToast.OpenSuccessToast(data.message);
|
||||
}
|
||||
else {
|
||||
CombodoToast.OpenErrorToast(data.message);
|
||||
}
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
CombodoToast.OpenErrorToast(data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
JS
|
||||
);
|
||||
|
||||
return $oPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \JsonPage
|
||||
*/
|
||||
function OperationUnsubscribe()
|
||||
{
|
||||
// Get the CSRF token from the request and check if it's valid
|
||||
if (!$this->CheckPostedCSRF()) {
|
||||
throw new \Exception('Invalid token');
|
||||
}
|
||||
|
||||
$sChannel = utils::ReadParam('channel', '', true, 'raw_data');
|
||||
$aChannel = explode('|', $sChannel);
|
||||
$oPage = new \JsonPage();
|
||||
$aReturnData = [];
|
||||
try {
|
||||
if (count($aChannel) !== 2) {
|
||||
throw new \Exception('Invalid channel');
|
||||
}
|
||||
$iTriggerKey = $aChannel[0];
|
||||
$iActionNotificationKey = $aChannel[1];
|
||||
$oTrigger = MetaModel::GetObject('Trigger', $iTriggerKey, false);
|
||||
if ($oTrigger === null) {
|
||||
throw new \Exception('Invalid trigger');
|
||||
}
|
||||
$oActionNotification = MetaModel::GetObject('ActionNotification', $iActionNotificationKey, false);
|
||||
if ($oActionNotification === null) {
|
||||
throw new \Exception('Invalid action notification');
|
||||
}
|
||||
$oSubscribedActionsNotificationsSearch = \DBObjectSearch::FromOQL("SELECT lnkActionNotificationToContact AS lnk WHERE lnk.action_id = :actionnotification_id AND lnk.contact_id = :contact_id AND lnk.trigger_id = :trigger_id AND lnk.subscribed = '1'");
|
||||
$oSubscribedActionsNotificationsSet = new \DBObjectSet($oSubscribedActionsNotificationsSearch, array(), array('actionnotification_id' => $iActionNotificationKey, 'contact_id' => \UserRights::GetContactId(), 'trigger_id' => $iTriggerKey));
|
||||
if ($oSubscribedActionsNotificationsSet->Count() === 0) {
|
||||
throw new \Exception('You are not subscribed to this channel');
|
||||
}
|
||||
while ($oSubscribedActionsNotifications = $oSubscribedActionsNotificationsSet->Fetch()) {
|
||||
$oSubscribedActionsNotifications->Set('subscribed', false);
|
||||
$oSubscribedActionsNotifications->DBUpdate();
|
||||
}
|
||||
$aReturnData = [
|
||||
'status' => 'success',
|
||||
'message' => Dict::S('UI:NotificationsCenter:Unsubscribe:Success'),
|
||||
];
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$aReturnData = [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
$oPage->SetData($aReturnData);
|
||||
$oPage->SetOutputDataOnly(true);
|
||||
|
||||
return $oPage;
|
||||
}
|
||||
|
||||
function OperationSubscribe()
|
||||
{
|
||||
|
||||
// Get the CSRF token from the request and check if it's valid
|
||||
if (!$this->CheckPostedCSRF()) {
|
||||
throw new \Exception('Invalid token');
|
||||
}
|
||||
|
||||
$sChannel = utils::ReadParam('channel', '', true, 'raw_data');
|
||||
$aChannel = explode('|', $sChannel);
|
||||
|
||||
$oPage = new \JsonPage();
|
||||
$aReturnData = [];
|
||||
try {
|
||||
if (count($aChannel) !== 2) {
|
||||
throw new \Exception('Invalid channel');
|
||||
}
|
||||
$iTriggerKey = $aChannel[0];
|
||||
$iActionNotificationKey = $aChannel[1];
|
||||
$oTrigger = MetaModel::GetObject('Trigger', $iTriggerKey, false);
|
||||
if ($oTrigger === null) {
|
||||
throw new \Exception('Invalid trigger');
|
||||
}
|
||||
$oActionNotification = MetaModel::GetObject('ActionNotification', $iActionNotificationKey, false);
|
||||
if ($oActionNotification === null) {
|
||||
throw new \Exception('Invalid action notification');
|
||||
}
|
||||
$oSubscribedActionsNotificationsSearch = \DBObjectSearch::FromOQL("SELECT lnkActionNotificationToContact AS lnk WHERE lnk.action_id = :actionnotification_id AND lnk.contact_id = :contact_id AND lnk.trigger_id = :trigger_id AND lnk.subscribed = '0'");
|
||||
$oSubscribedActionsNotificationsSet = new \DBObjectSet($oSubscribedActionsNotificationsSearch, array(), array('actionnotification_id' => $iActionNotificationKey, 'contact_id' => \UserRights::GetContactId(), 'trigger_id' => $iTriggerKey));
|
||||
if ($oSubscribedActionsNotificationsSet->Count() === 0) {
|
||||
throw new \Exception('You are not allow to subscribe to this channel');
|
||||
}
|
||||
while ($oSubscribedActionsNotifications = $oSubscribedActionsNotificationsSet->Fetch()) {
|
||||
$oSubscribedActionsNotifications->Set('subscribed', true);
|
||||
$oSubscribedActionsNotifications->DBUpdate();
|
||||
}
|
||||
$aReturnData = [
|
||||
'status' => 'success',
|
||||
'message' => Dict::S('UI:NotificationsCenter:Subscribe:Success'),
|
||||
];
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$aReturnData = [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
$oPage->SetData($aReturnData);
|
||||
$oPage->SetOutputDataOnly(true);
|
||||
|
||||
return $oPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \JsonPage
|
||||
* @throws \Exception
|
||||
*/
|
||||
function OperationUnsubscribeFromChannel(): \JsonPage
|
||||
{
|
||||
// Get the CSRF token from the request and check if it's valid
|
||||
if (!$this->CheckPostedCSRF()) {
|
||||
throw new \Exception('Invalid token');
|
||||
}
|
||||
|
||||
// Get the channel from the request
|
||||
$sChannel = utils::ReadParam('channel', '', true, 'raw_data');
|
||||
$aChannel = explode('|', $sChannel);
|
||||
|
||||
$oPage = new \JsonPage();
|
||||
$aReturnData = [];
|
||||
try {
|
||||
if (count($aChannel) !== 2) {
|
||||
throw new \Exception('Invalid channel');
|
||||
}
|
||||
[$iTriggerId, $sFinalclass] = $aChannel;
|
||||
$oTrigger = MetaModel::GetObject('Trigger', $iTriggerId, false);
|
||||
if ($oTrigger === null) {
|
||||
throw new \Exception('Invalid trigger');
|
||||
}
|
||||
// Check the trigger subscription policy
|
||||
if($oTrigger->Get('subscription_policy') === 'force_all_channels'){
|
||||
throw new \Exception('You are not allowed to unsubscribe from this channel');
|
||||
}
|
||||
|
||||
// Check if we are subscribed to at least 1 channel
|
||||
$oSubscribedActionsNotificationsSet = NotificationsRepository::GetInstance()->SearchSubscriptionByTriggerContactSubscriptionAndFinalclass($iTriggerId, \UserRights::GetContactId(), '1', $sFinalclass);
|
||||
if ($oSubscribedActionsNotificationsSet->Count() === 0) {
|
||||
throw new \Exception('You are not subscribed to any channel');
|
||||
}
|
||||
// Check the trigger subscription policy and if we are subscribed to at least 1 channel if necessary
|
||||
if($oTrigger->Get('subscription_policy') === 'force_at_least_one_channel') {
|
||||
$oTotalSubscribedActionsNotificationsSet = NotificationsRepository::GetInstance()->SearchSubscriptionByTriggerContactAndSubscription($iTriggerId, \UserRights::GetContactId(), '1');
|
||||
if (($oTotalSubscribedActionsNotificationsSet->Count() - $oSubscribedActionsNotificationsSet->Count()) === 0) {
|
||||
throw new \Exception('You can\'t unsubscribe from this channel, you must be subscribed to at least one channel');
|
||||
}
|
||||
}
|
||||
// Unsubscribe from all channels
|
||||
while ($oSubscribedActionsNotifications = $oSubscribedActionsNotificationsSet->Fetch()) {
|
||||
$oSubscribedActionsNotifications->Set('subscribed', false);
|
||||
$oSubscribedActionsNotifications->DBUpdate();
|
||||
}
|
||||
$aReturnData = [
|
||||
'status' => 'success',
|
||||
'message' => Dict::S('UI:NotificationsCenter:Unsubscribe:Success'),
|
||||
];
|
||||
}
|
||||
catch (Exception $e) {
|
||||
// Return an error message if an exception is thrown
|
||||
$aReturnData = [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
$oPage->SetData($aReturnData);
|
||||
$oPage->SetOutputDataOnly(true);
|
||||
|
||||
return $oPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \JsonPage
|
||||
* @throws \Exception
|
||||
*/
|
||||
function OperationSubscribeToChannel(): \JsonPage
|
||||
{
|
||||
// Get the CSRF token from the request and check if it's valid
|
||||
if (!$this->CheckPostedCSRF()) {
|
||||
throw new \Exception('Invalid token');
|
||||
}
|
||||
|
||||
// Get the channel from the request
|
||||
$sChannel = utils::ReadParam('channel', '', true, 'raw_data');
|
||||
$aChannel = explode('|', $sChannel);
|
||||
|
||||
$oPage = new \JsonPage();
|
||||
$aReturnData = [];
|
||||
try {
|
||||
if (count($aChannel) !== 2) {
|
||||
throw new \Exception('Invalid channel');
|
||||
}
|
||||
[$iTriggerId, $sFinalclass] = $aChannel;
|
||||
$oTrigger = MetaModel::GetObject('Trigger', $iTriggerId, false);
|
||||
if ($oTrigger === null) {
|
||||
throw new \Exception('Invalid trigger');
|
||||
}
|
||||
$oSubscribedActionsNotificationsSet = NotificationsRepository::GetInstance()->SearchSubscriptionByTriggerContactSubscriptionAndFinalclass($iTriggerId, \UserRights::GetContactId(), '0', $sFinalclass);
|
||||
if ($oSubscribedActionsNotificationsSet->Count() === 0) {
|
||||
throw new \Exception('You are not subscribed to any channel');
|
||||
}
|
||||
// Subscribe to all channels
|
||||
while ($oSubscribedActionsNotifications = $oSubscribedActionsNotificationsSet->Fetch()) {
|
||||
$oSubscribedActionsNotifications->Set('subscribed', true);
|
||||
$oSubscribedActionsNotifications->DBUpdate();
|
||||
}
|
||||
$aReturnData = [
|
||||
'status' => 'success',
|
||||
'message' => Dict::S('UI:NotificationsCenter:Subscribe:Success'),
|
||||
];
|
||||
}
|
||||
catch (Exception $e) {
|
||||
// Return an error message if an exception is thrown
|
||||
$aReturnData = [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
$oPage->SetData($aReturnData);
|
||||
$oPage->SetOutputDataOnly(true);
|
||||
|
||||
return $oPage;
|
||||
}
|
||||
}
|
||||
120
sources/Service/Notification/NotificationsRepository.php
Normal file
120
sources/Service/Notification/NotificationsRepository.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Service\Notification;
|
||||
|
||||
use DBObjectSearch;
|
||||
use DBObjectSet;
|
||||
|
||||
/**
|
||||
* Class NotificationsRepository
|
||||
*
|
||||
* @author Stephen Abello <stephen.abello@combodo.com>
|
||||
* @package Combodo\iTop\Service\Notification
|
||||
* @since 3.2.0
|
||||
*/
|
||||
class NotificationsRepository
|
||||
{
|
||||
/** @var NotificationsRepository|null Singleton */
|
||||
protected static ?NotificationsRepository $oSingleton = null;
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return static The singleton instance of the notifications repository
|
||||
*/
|
||||
public static function GetInstance(): static
|
||||
{
|
||||
if (is_null(self::$oSingleton)) {
|
||||
self::$oSingleton = new static();
|
||||
}
|
||||
|
||||
return self::$oSingleton;
|
||||
}
|
||||
|
||||
/**********************/
|
||||
/* Non-static methods */
|
||||
/**********************/
|
||||
|
||||
/**
|
||||
* Singleton pattern, can't use the constructor. Use {@see \Combodo\iTop\Service\Notification\NotificationsRepository::GetInstance()} instead.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function __construct()
|
||||
{
|
||||
// Don't do anything, we don't want to be initialized
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for subscriptions by contact ID.
|
||||
*
|
||||
* @param int $iContactId The ID of the contact.
|
||||
*
|
||||
* @return DBObjectSet The result set of subscriptions associated with the contact.
|
||||
*/
|
||||
public function SearchSubscriptionByContact(int $iContactId): DBObjectSet
|
||||
{
|
||||
$oSearch = DBObjectSearch::FromOQL("SELECT lnkActionNotificationToContact AS lnk WHERE lnk.contact_id = :contact_id");
|
||||
$oSet = new DBObjectSet($oSearch, array(), array('contact_id' => $iContactId));
|
||||
|
||||
return $oSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for a subscription by trigger, contact, and action.
|
||||
*
|
||||
* @param int $iTriggerId The ID of the trigger.
|
||||
* @param int $iContactId The ID of the contact.
|
||||
* @param int $iActionID The ID of the action.
|
||||
*
|
||||
* @return DBObjectSet The set of subscriptions matching the given trigger, contact, and action.
|
||||
*/
|
||||
public function SearchSubscriptionByTriggerContactAndAction(int $iTriggerId, int $iContactId, int $iActionID): DBObjectSet
|
||||
{
|
||||
$oSearch = DBObjectSearch::FromOQL("SELECT lnkActionNotificationToContact AS lnk WHERE lnk.contact_id = :contact_id AND lnk.trigger_id = :trigger_id AND lnk.action_id = :action_id");
|
||||
$oSet = new DBObjectSet($oSearch, array(), array('trigger_id' => $iTriggerId, 'contact_id' => $iContactId, 'action_id' => $iActionID));
|
||||
|
||||
return $oSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for subscriptions based on trigger, contact, and subscription type.
|
||||
*
|
||||
* @param int $iTriggerId The ID of the trigger.
|
||||
* @param int $iContactId The ID of the contact.
|
||||
* @param string $sSubscription The subscription type.
|
||||
*
|
||||
* @return DBObjectSet A set of subscription objects matching the given parameters.
|
||||
*/
|
||||
public function SearchSubscriptionByTriggerContactAndSubscription(int $iTriggerId, int $iContactId, string $sSubscription): DBObjectSet
|
||||
{
|
||||
$oSearch = DBObjectSearch::FromOQL("SELECT lnkActionNotificationToContact AS lnk WHERE lnk.contact_id = :contact_id AND lnk.trigger_id = :trigger_id AND lnk.subscribed = :subscription");
|
||||
$oSet = new DBObjectSet($oSearch, array(), array('trigger_id' => $iTriggerId, 'contact_id' => $iContactId, 'subscription' => $sSubscription));
|
||||
|
||||
return $oSet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Search for subscriptions based on trigger, contact, subscription type, and final class.
|
||||
*
|
||||
* @param int $iTriggerId The ID of the trigger.
|
||||
* @param int $iContactId The ID of the contact.
|
||||
* @param int $sSubscription The subscription type.
|
||||
* @param string $sFinalclass The final class of the action notification.
|
||||
*
|
||||
* @return DBObjectSet A set of subscription objects matching the given parameters.
|
||||
*/
|
||||
public function SearchSubscriptionByTriggerContactSubscriptionAndFinalclass(int $iTriggerId, int $iContactId, int $sSubscription, string $sFinalclass): DBObjectSet
|
||||
{
|
||||
$oSearch = DBObjectSearch::FromOQL("SELECT lnkActionNotificationToContact AS lnk JOIN ActionNotification AS an ON lnk.action_id = an.id WHERE lnk.contact_id = :contact_id AND lnk.trigger_id = :trigger_id AND lnk.subscribed = :subscription AND an.finalclass = :finalclass");
|
||||
$oSet = new DBObjectSet($oSearch, array(), array('trigger_id' => $iTriggerId, 'contact_id' => $iContactId, 'subscription' => $sSubscription, 'finalclass' => $sFinalclass));
|
||||
|
||||
return $oSet;
|
||||
}
|
||||
|
||||
public function GetSearchOQLContactUnsubscribedByTriggerAndAction(): DBObjectSearch
|
||||
{
|
||||
$oSearch = DBObjectSearch::FromOQL("SELECT Contact AS c JOIN lnkActionNotificationToContact AS lnk ON lnk.contact_id = c.id WHERE lnk.trigger_id = :trigger_id AND lnk.action_id = :action_id AND lnk.subscribed = '0'");
|
||||
return $oSearch;
|
||||
}
|
||||
}
|
||||
116
sources/Service/Notification/NotificationsService.php
Normal file
116
sources/Service/Notification/NotificationsService.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
namespace Combodo\iTop\Service\Notification;
|
||||
|
||||
|
||||
use ActionNotification;
|
||||
use Contact;
|
||||
use lnkActionNotificationToContact;
|
||||
use Trigger;
|
||||
|
||||
/**
|
||||
* Class NotificationsService
|
||||
*
|
||||
* @author Stephen Abello <stephen.abello@combodo.com>
|
||||
* @package Combodo\iTop\Service\Notification
|
||||
* @since 3.2.0
|
||||
*/
|
||||
class NotificationsService {
|
||||
protected static ?NotificationsService $oSingleton = null;
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return static The singleton instance of the notifications service
|
||||
*/
|
||||
public static function GetInstance(): static
|
||||
{
|
||||
if (null === static::$oSingleton) {
|
||||
static::$oSingleton = new static();
|
||||
}
|
||||
|
||||
return static::$oSingleton;
|
||||
}
|
||||
|
||||
/**********************/
|
||||
/* Non-static methods */
|
||||
/**********************/
|
||||
|
||||
/**
|
||||
* Singleton pattern, can't use the constructor. Use {@see \Combodo\iTop\Service\Notification\NotificationsService::GetInstance()} instead.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function __construct() {
|
||||
// Don't do anything, we don't want to be initialized
|
||||
}
|
||||
|
||||
/**
|
||||
* Register that $oRecipient was a recipient for the $oTrigger / $oActionNotification tuple at least one time
|
||||
*
|
||||
* @param \Trigger $oTrigger
|
||||
* @param \ActionNotification $oActionNotification
|
||||
* @param \Contact $oRecipient
|
||||
*
|
||||
* @return void
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreCannotSaveObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \CoreWarning
|
||||
* @throws \MissingQueryArgument
|
||||
* @throws \MySQLException
|
||||
* @throws \MySQLHasGoneAwayException
|
||||
* @throws \OQLException
|
||||
*/
|
||||
public function RegisterSubscription(Trigger $oTrigger, ActionNotification $oActionNotification, Contact $oRecipient): void
|
||||
{
|
||||
// Check if the user is already subscribed to the action notification
|
||||
$oSubscribedActionsNotificationsSet = NotificationsRepository::GetInstance()->SearchSubscriptionByTriggerContactAndAction($oTrigger->GetKey(), $oRecipient->GetKey(), $oActionNotification->GetKey());
|
||||
if ($oSubscribedActionsNotificationsSet->Count() === 0) {
|
||||
// Create a new subscription
|
||||
$oSubscribedActionsNotifications = new lnkActionNotificationToContact();
|
||||
$oSubscribedActionsNotifications->Set('action_id', $oActionNotification->GetKey());
|
||||
$oSubscribedActionsNotifications->Set('contact_id', $oRecipient->GetKey());
|
||||
$oSubscribedActionsNotifications->Set('trigger_id', $oTrigger->GetKey());
|
||||
$oSubscribedActionsNotifications->Set('subscribed', true);
|
||||
$oSubscribedActionsNotifications->DBInsertNoReload();
|
||||
}
|
||||
else {
|
||||
while ($oSubscribedActionsNotifications = $oSubscribedActionsNotificationsSet->Fetch()) {
|
||||
// Update the subscription
|
||||
$oSubscribedActionsNotifications->Set('subscribed', true);
|
||||
$oSubscribedActionsNotifications->DBUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Trigger $oTrigger
|
||||
* @param \ActionNotification $oActionNotification
|
||||
* @param \Contact $oRecipient
|
||||
*
|
||||
* @return bool
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MissingQueryArgument
|
||||
* @throws \MySQLException
|
||||
* @throws \MySQLHasGoneAwayException
|
||||
*/
|
||||
public function IsSubscribed(Trigger $oTrigger, ActionNotification $oActionNotification, Contact $oRecipient): bool
|
||||
{
|
||||
// Check if the trigger subscription policy is 'force_all_channels'
|
||||
if ($oTrigger->Get('subscription_policy') === 'force_all_channels') {
|
||||
return true;
|
||||
}
|
||||
// Check if the user is already subscribed to the action notification
|
||||
$oSubscribedActionsNotificationsSet = NotificationsRepository::GetInstance()->SearchSubscriptionByTriggerContactAndAction($oTrigger->GetKey(), $oRecipient->GetKey(), $oActionNotification->GetKey());
|
||||
if ($oSubscribedActionsNotificationsSet->Count() === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return the subscribed status
|
||||
$oSubscribedActionsNotifications = $oSubscribedActionsNotificationsSet->Fetch();
|
||||
return $oSubscribedActionsNotifications->Get('subscribed');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="option ibo-input-select--autocomplete-item" role="option" >
|
||||
|
||||
{# Image #}
|
||||
<span class="ibo-input-select--autocomplete-item-image" data-template-condition="has_image" data-template-css-background-image="picture_url" data-template-text="initials"></span>
|
||||
<span class="ibo-input-select--autocomplete-item-image" data-template-add-class="class" data-template-condition="has_image" data-template-css-background-image="picture_url" data-template-text="initials"></span>
|
||||
|
||||
{# Desc #}
|
||||
<span class="ibo-input-select--autocomplete-item-txt">
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class="item ibo-input-select--notification-item" role="item" >
|
||||
|
||||
{# Image #}
|
||||
<span class="ibo-input-select--autocomplete-item-image" data-template-add-class="class" data-template-condition="has_image" data-template-css-background-image="picture_url" data-template-text="initials"></span>
|
||||
|
||||
{# Desc #}
|
||||
<span class="ibo-input-select--autocomplete-item-txt">
|
||||
|
||||
{# Friendly name #}
|
||||
<span data-template-text="friendlyname"></span>
|
||||
|
||||
{# Additional content #}
|
||||
{% block additional_content %}
|
||||
{% endblock %}
|
||||
|
||||
<span class="ibo-input-select--notification-item--mixed-value" data-template-condition="mixed">
|
||||
<span data-template-text="mixed_value"></span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
@@ -7,6 +7,9 @@
|
||||
name="{{ oUIBlock.GetName() }}"
|
||||
multiple
|
||||
style="display: none;"
|
||||
{% if oUIBlock.IsDisabled() == true %}
|
||||
disabled
|
||||
{% endif %}
|
||||
>
|
||||
</select>
|
||||
|
||||
|
||||
@@ -49,6 +49,14 @@ let oWidget{{ oUIBlock.GetId() }} = $('#{{ oUIBlock.GetId() }}').selectize({
|
||||
{% if oUIBlock.HasRemoveItemButton() %}
|
||||
'remove_button' : {},
|
||||
{% endif %}
|
||||
|
||||
{# PLUGIN min items #}
|
||||
{% if oUIBlock.GetMinItems() is not empty %}
|
||||
'combodo_min_items' : {
|
||||
minItems: {{ oUIBlock.GetMinItems() }},
|
||||
errorMessage: '{{ 'UI:Component:Input:Set:MinimumItems'|dict_format(oUIBlock.GetMinItems()) }}'
|
||||
},
|
||||
{% endif %}
|
||||
},
|
||||
|
||||
{# Max items you can select #}
|
||||
@@ -197,6 +205,31 @@ let oWidget{{ oUIBlock.GetId() }} = $('#{{ oUIBlock.GetId() }}').selectize({
|
||||
'data-tooltip-html-enabled': true
|
||||
});
|
||||
CombodoTooltip.InitTooltipFromMarkup($item);
|
||||
|
||||
{% if oUIBlock.GetOnItemAddJs() is not null %}
|
||||
{{ oUIBlock.GetOnItemAddJs()|raw }}
|
||||
{% endif %}
|
||||
},
|
||||
|
||||
{# On item remove #}
|
||||
onItemRemove: function(value, $item){
|
||||
{% if oUIBlock.GetOnItemRemoveJs() is not null %}
|
||||
{{ oUIBlock.GetOnItemRemoveJs()|raw }}
|
||||
{% endif %}
|
||||
},
|
||||
|
||||
{# On option remove #}
|
||||
onOptionRemove: function(value, $item){
|
||||
{% if oUIBlock.GetOnOptionRemoveJs() is not null %}
|
||||
{{ oUIBlock.GetOnOptionRemoveJs()|raw }}
|
||||
{% endif %}
|
||||
},
|
||||
|
||||
{# On option add #}
|
||||
onOptionAdd: function(value, $item){
|
||||
{% if oUIBlock.GetOnOptionAddJs() is not null %}
|
||||
{{ oUIBlock.GetOnOptionAddJs()|raw }}
|
||||
{% endif %}
|
||||
},
|
||||
|
||||
{# plugin combodo_add_button #}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{# @copyright Copyright (C) 2010-2023 Combodo SARL #}
|
||||
{# @license http://opensource.org/licenses/AGPL-3.0 #}
|
||||
|
||||
<div class="option simple-option-renderer" role="option">
|
||||
|
||||
<div class="simple-option-renderer--container" data-template-add-class="class">
|
||||
|
||||
<img class="simple-option-renderer--container--icon" src="" data-template-attr-src="img">
|
||||
|
||||
<span class="simple-option-renderer--container--label" data-template-text="label">
|
||||
</span>
|
||||
|
||||
<span class="simple-option-renderer--container--complementary" data-template-text="complementary-text">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -44,12 +44,6 @@ if (!file_exists($sConfigFile))
|
||||
|
||||
require_once(APPROOT.'/application/startup.inc.php');
|
||||
|
||||
// Temporary fix until below bug is resolved properly:
|
||||
// N°7008 - Fix missing background tasks in CRON when NOT in "developer_mode"
|
||||
require_once(APPROOT.'/sources/SessionTracker/SessionGC.php');
|
||||
require_once(APPROOT.'/sources/Service/TemporaryObjects/TemporaryObjectGC.php');
|
||||
require_once(APPROOT.'/sources/Service/Notification/Event/EventiTopNotificationGC.php');
|
||||
|
||||
$oCtx = new ContextTag(ContextTag::TAG_CRON);
|
||||
|
||||
function ReadMandatoryParam($oP, $sParam, $sSanitizationFilter = 'parameter')
|
||||
@@ -414,71 +408,63 @@ function ReSyncProcesses($oP, $bVerbose, $bDebug)
|
||||
$oNow = new DateTime();
|
||||
|
||||
$aProcesses = array();
|
||||
foreach (get_declared_classes() as $sTaskClass)
|
||||
foreach (utils::GetClassesForInterface('iProcess', '', ['[\\\\/]lib[\\\\/]', '[\\\\/]node_modules[\\\\/]', '[\\\\/]test[\\\\/]', '[\\\\/]tests[\\\\/]']) as $sTaskClass)
|
||||
{
|
||||
$oRefClass = new ReflectionClass($sTaskClass);
|
||||
if ($oRefClass->isAbstract())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if ($oRefClass->implementsInterface('iProcess'))
|
||||
{
|
||||
$oProcess = new $sTaskClass;
|
||||
$aProcesses[$sTaskClass] = $oProcess;
|
||||
$oProcess = new $sTaskClass;
|
||||
$aProcesses[$sTaskClass] = $oProcess;
|
||||
|
||||
// Create missing entry if needed
|
||||
if (!array_key_exists($sTaskClass, $aTasks))
|
||||
// Create missing entry if needed
|
||||
if (!array_key_exists($sTaskClass, $aTasks))
|
||||
{
|
||||
// New entry, let's create a new BackgroundTask record, and plan the first execution
|
||||
$oTask = new BackgroundTask();
|
||||
$oTask->SetDebug($bDebug);
|
||||
$oTask->Set('class_name', $sTaskClass);
|
||||
$oTask->Set('total_exec_count', 0);
|
||||
$oTask->Set('min_run_duration', 99999.999);
|
||||
$oTask->Set('max_run_duration', 0);
|
||||
$oTask->Set('average_run_duration', 0);
|
||||
$oRefClass = new ReflectionClass($sTaskClass);
|
||||
if ($oRefClass->implementsInterface('iScheduledProcess'))
|
||||
{
|
||||
// New entry, let's create a new BackgroundTask record, and plan the first execution
|
||||
$oTask = new BackgroundTask();
|
||||
$oTask->SetDebug($bDebug);
|
||||
$oTask->Set('class_name', $sTaskClass);
|
||||
$oTask->Set('total_exec_count', 0);
|
||||
$oTask->Set('min_run_duration', 99999.999);
|
||||
$oTask->Set('max_run_duration', 0);
|
||||
$oTask->Set('average_run_duration', 0);
|
||||
$oNextOcc = $oProcess->GetNextOccurrence();
|
||||
$oTask->Set('next_run_date', $oNextOcc->format('Y-m-d H:i:s'));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Background processes do start asap, i.e. "now"
|
||||
$oTask->Set('next_run_date', $oNow->format('Y-m-d H:i:s'));
|
||||
}
|
||||
if ($bVerbose)
|
||||
{
|
||||
$oP->p('Creating record for: '.$sTaskClass);
|
||||
$oP->p('First execution planned at: '.$oTask->Get('next_run_date'));
|
||||
}
|
||||
$oTask->DBInsert();
|
||||
}
|
||||
else
|
||||
{
|
||||
/** @var \BackgroundTask $oTask */
|
||||
$oTask = $aTasks[$sTaskClass];
|
||||
if ($oTask->Get('next_run_date') == '3000-01-01 00:00:00')
|
||||
{
|
||||
// check for rescheduled tasks
|
||||
$oRefClass = new ReflectionClass($sTaskClass);
|
||||
if ($oRefClass->implementsInterface('iScheduledProcess'))
|
||||
{
|
||||
$oNextOcc = $oProcess->GetNextOccurrence();
|
||||
$oTask->Set('next_run_date', $oNextOcc->format('Y-m-d H:i:s'));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Background processes do start asap, i.e. "now"
|
||||
$oTask->Set('next_run_date', $oNow->format('Y-m-d H:i:s'));
|
||||
}
|
||||
if ($bVerbose)
|
||||
{
|
||||
$oP->p('Creating record for: '.$sTaskClass);
|
||||
$oP->p('First execution planned at: '.$oTask->Get('next_run_date'));
|
||||
}
|
||||
$oTask->DBInsert();
|
||||
}
|
||||
else
|
||||
{
|
||||
/** @var \BackgroundTask $oTask */
|
||||
$oTask = $aTasks[$sTaskClass];
|
||||
if ($oTask->Get('next_run_date') == '3000-01-01 00:00:00')
|
||||
{
|
||||
// check for rescheduled tasks
|
||||
$oRefClass = new ReflectionClass($sTaskClass);
|
||||
if ($oRefClass->implementsInterface('iScheduledProcess'))
|
||||
{
|
||||
$oNextOcc = $oProcess->GetNextOccurrence();
|
||||
$oTask->Set('next_run_date', $oNextOcc->format('Y-m-d H:i:s'));
|
||||
$oTask->DBUpdate();
|
||||
}
|
||||
}
|
||||
// Reactivate task if necessary
|
||||
if ($oTask->Get('status') == 'removed')
|
||||
{
|
||||
$oTask->Set('status', 'active');
|
||||
$oTask->DBUpdate();
|
||||
}
|
||||
// task having a real class to execute
|
||||
unset($aTasks[$sTaskClass]);
|
||||
}
|
||||
// Reactivate task if necessary
|
||||
if ($oTask->Get('status') == 'removed')
|
||||
{
|
||||
$oTask->Set('status', 'active');
|
||||
$oTask->DBUpdate();
|
||||
}
|
||||
// task having a real class to execute
|
||||
unset($aTasks[$sTaskClass]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user