diff --git a/application/Espo/Classes/AssignmentNotificators/Email.php b/application/Espo/Classes/AssignmentNotificators/Email.php index 4cd90e7ca1..d4e3f75dfd 100644 --- a/application/Espo/Classes/AssignmentNotificators/Email.php +++ b/application/Espo/Classes/AssignmentNotificators/Email.php @@ -30,6 +30,7 @@ namespace Espo\Classes\AssignmentNotificators; use Espo\Core\Field\DateTime; +use Espo\Core\Field\LinkParent; use Espo\Core\Name\Field; use Espo\Core\Notification\DefaultAssignmentNotificator; use Espo\Entities\EmailAddress; @@ -273,13 +274,16 @@ class Email implements AssignmentNotificator continue; } - $this->entityManager->createEntity(Notification::ENTITY_TYPE, [ - 'type' => Notification::TYPE_EMAIL_RECEIVED, - 'userId' => $userId, - 'data' => $data, - 'relatedId' => $entity->getId(), - 'relatedType' => EmailEntity::ENTITY_TYPE, - ]); + $notification = $this->entityManager->getRDBRepositoryByClass(Notification::class)->getNew(); + + $notification + ->setType(Notification::TYPE_EMAIL_RECEIVED) + ->setUserId($userId) + ->setData($data) + ->setRelated(LinkParent::createFromEntity($entity)) + ->setActionId($params->getActionId()); + + $this->entityManager->saveEntity($notification); } } } diff --git a/application/Espo/Core/Notification/AssignmentNotificator.php b/application/Espo/Core/Notification/AssignmentNotificator.php index 2c0d906833..0452d15666 100644 --- a/application/Espo/Core/Notification/AssignmentNotificator.php +++ b/application/Espo/Core/Notification/AssignmentNotificator.php @@ -39,5 +39,8 @@ use Espo\Core\Notification\AssignmentNotificator\Params; */ interface AssignmentNotificator { + /** + * @param TEntity $entity + */ public function process(Entity $entity, Params $params): void; } diff --git a/application/Espo/Core/Notification/AssignmentNotificator/Params.php b/application/Espo/Core/Notification/AssignmentNotificator/Params.php index 2387da6431..d7473e966d 100644 --- a/application/Espo/Core/Notification/AssignmentNotificator/Params.php +++ b/application/Espo/Core/Notification/AssignmentNotificator/Params.php @@ -34,11 +34,11 @@ namespace Espo\Core\Notification\AssignmentNotificator; */ class Params { - /** - * @var array - */ + /** @var array */ private $options = []; + private ?string $actionId = null; + /** * Whether an option is set. */ @@ -97,4 +97,23 @@ class Params { return new self(); } + + /** + * @since 9.2.0 + */ + public function withActionId(?string $actionId): self + { + $obj = clone $this; + $obj->actionId = $actionId; + + return $obj; + } + + /** + * @since 9.2.0 + */ + public function getActionId(): ?string + { + return $this->actionId; + } } diff --git a/application/Espo/Core/Notification/DefaultAssignmentNotificator.php b/application/Espo/Core/Notification/DefaultAssignmentNotificator.php index a48ade0001..3107b3cfd9 100644 --- a/application/Espo/Core/Notification/DefaultAssignmentNotificator.php +++ b/application/Espo/Core/Notification/DefaultAssignmentNotificator.php @@ -29,6 +29,7 @@ namespace Espo\Core\Notification; +use Espo\Core\Field\LinkParent; use Espo\Core\Name\Field; use Espo\Core\ORM\Entity as CoreEntity; use Espo\ORM\Entity; @@ -72,7 +73,7 @@ class DefaultAssignmentNotificator implements AssignmentNotificator continue; } - $this->processForUser($entity, $userId); + $this->processForUser($entity, $userId, $params); } return; @@ -88,10 +89,10 @@ class DefaultAssignmentNotificator implements AssignmentNotificator $assignedUserId = $entity->get(self::ATTR_ASSIGNED_USER_ID); - $this->processForUser($entity, $assignedUserId); + $this->processForUser($entity, $assignedUserId, $params); } - protected function processForUser(Entity $entity, string $assignedUserId): void + protected function processForUser(Entity $entity, string $assignedUserId, Params $params): void { if (!$this->userChecker->checkAssignment($entity->getEntityType(), $assignedUserId)) { return; @@ -113,19 +114,22 @@ class DefaultAssignmentNotificator implements AssignmentNotificator return; } - $this->entityManager->createEntity(Notification::ENTITY_TYPE, [ - 'type' => Notification::TYPE_ASSIGN, - 'userId' => $assignedUserId, - 'data' => [ + $notification = $this->entityManager->getRDBRepositoryByClass(Notification::class)->getNew(); + + $notification + ->setType(Notification::TYPE_ASSIGN) + ->setUserId($assignedUserId) + ->setData([ 'entityType' => $entity->getEntityType(), 'entityId' => $entity->getId(), 'entityName' => $entity->get(Field::NAME), 'isNew' => $entity->isNew(), 'userId' => $this->user->getId(), 'userName' => $this->user->getName(), - ], - 'relatedType' => $entity->getEntityType(), - 'relatedId' => $entity->getId(), - ]); + ]) + ->setRelated(LinkParent::createFromEntity($entity)) + ->setActionId($params->getActionId()); + + $this->entityManager->saveEntity($notification); } } diff --git a/application/Espo/Core/ORM/Repository/Option/SaveContext.php b/application/Espo/Core/ORM/Repository/Option/SaveContext.php index 11ecb1cd99..229cc85718 100644 --- a/application/Espo/Core/ORM/Repository/Option/SaveContext.php +++ b/application/Espo/Core/ORM/Repository/Option/SaveContext.php @@ -41,9 +41,10 @@ class SaveContext private string $id; private bool $linkUpdated = false; - public function __construct() - { - $this->id = Util::generateId(); + public function __construct( + ?string $id = null, + ) { + $this->id = $id ?? Util::generateId(); } /** diff --git a/application/Espo/Entities/Notification.php b/application/Espo/Entities/Notification.php index 6455786463..21a9146ca9 100644 --- a/application/Espo/Entities/Notification.php +++ b/application/Espo/Entities/Notification.php @@ -150,4 +150,14 @@ class Notification extends Entity return $this; } + + /** + * @since 9.2.0 + */ + public function setActionId(?string $actionId): self + { + $this->set('actionId', $actionId); + + return $this; + } } diff --git a/application/Espo/Hooks/Note/Notifications.php b/application/Espo/Hooks/Note/Notifications.php index c5af3c3edd..12e57d2500 100644 --- a/application/Espo/Hooks/Note/Notifications.php +++ b/application/Espo/Hooks/Note/Notifications.php @@ -29,32 +29,37 @@ namespace Espo\Hooks\Note; +use Espo\Core\Hook\Hook\AfterSave; +use Espo\Core\ORM\Repository\Option\SaveContext; use Espo\ORM\Entity; +use Espo\ORM\Repository\Option\SaveOptions; +use Espo\Tools\Notification\HookProcessor\Params; use Espo\Tools\Notification\NoteHookProcessor; use Espo\Entities\Note; -class Notifications +/** + * @implements AfterSave + */ +class Notifications implements AfterSave { public static int $order = 14; - private $processor; + public function __construct(private NoteHookProcessor $processor) + {} - public function __construct(NoteHookProcessor $processor) + public function afterSave(Entity $entity, SaveOptions $options): void { - $this->processor = $processor; - } - - /** - * @param array $options - */ - public function afterSave(Entity $entity, array $options): void - { - if (!$entity->isNew() && empty($options['forceProcessNotifications'])) { + if (!$entity->isNew() && !$options->get('forceProcessNotifications')) { return; } - assert($entity instanceof Note); - $this->processor->afterSave($entity); + $saveContext = $options->get(SaveContext::NAME); + + $actionId = $saveContext instanceof SaveContext ? $saveContext->getId() : null; + + $params = new Params(actionId: $actionId); + + $this->processor->afterSave($entity, $params); } } diff --git a/application/Espo/Modules/Crm/Classes/AssignmentNotificators/Meeting.php b/application/Espo/Modules/Crm/Classes/AssignmentNotificators/Meeting.php index e73dc977c2..650010db2c 100644 --- a/application/Espo/Modules/Crm/Classes/AssignmentNotificators/Meeting.php +++ b/application/Espo/Modules/Crm/Classes/AssignmentNotificators/Meeting.php @@ -55,12 +55,9 @@ class Meeting implements AssignmentNotificator private UserEnabledChecker $userEnabledChecker, private EntityManager $entityManager, private User $user, - private Metadata $metadata + private Metadata $metadata, ) {} - /** - * @param MeetingEntity|Call $entity - */ public function process(Entity $entity, Params $params): void { // Default assignment notifications not needed if stream is enabled. @@ -91,14 +88,11 @@ class Meeting implements AssignmentNotificator $newIds = array_values($newIds); foreach ($newIds as $id) { - $this->processForUser($entity, $id); + $this->processForUser($entity, $id, $params); } } - /** - * @param MeetingEntity|Call $entity - */ - private function processForUser(Entity $entity, string $userId): void + private function processForUser(MeetingEntity|Call $entity, string $userId, Params $params): void { if (!$this->userEnabledChecker->checkAssignment($entity->getEntityType(), $userId)) { return; @@ -115,23 +109,21 @@ class Meeting implements AssignmentNotificator return; } - /** @var Notification $notification */ $notification = $this->entityManager->getRDBRepositoryByClass(Notification::class)->getNew(); $notification ->setType(self::NOTIFICATION_TYPE_EVENT_ATTENDEE) ->setUserId($userId) - ->setRelated( - LinkParent::create($entity->getEntityType(), $entity->getId()) - ) - ->setData((object) [ + ->setRelated(LinkParent::createFromEntity($entity)) + ->setData([ 'entityType' => $entity->getEntityType(), 'entityId' => $entity->getId(), 'entityName' => $entity->getName(), 'isNew' => $entity->isNew(), 'userId' => $this->user->getId(), 'userName' => $this->user->getName(), - ]); + ]) + ->setActionId($params->getActionId()); $this->entityManager->saveEntity($notification); } diff --git a/application/Espo/Resources/metadata/entityDefs/Notification.json b/application/Espo/Resources/metadata/entityDefs/Notification.json index 5e1b929c96..3c626ba6a6 100644 --- a/application/Espo/Resources/metadata/entityDefs/Notification.json +++ b/application/Espo/Resources/metadata/entityDefs/Notification.json @@ -40,6 +40,11 @@ "type": "linkParent", "readOnly": true }, + "actionId": { + "type": "varchar", + "maxLength": 36, + "readOnly": true + }, "createdBy": { "type": "link", "readOnly": true, diff --git a/application/Espo/Tools/Notification/HookProcessor.php b/application/Espo/Tools/Notification/HookProcessor.php index 06fc73c2f8..01a4065357 100644 --- a/application/Espo/Tools/Notification/HookProcessor.php +++ b/application/Espo/Tools/Notification/HookProcessor.php @@ -33,6 +33,7 @@ use Espo\Core\Name\Field; use Espo\Core\Notification\AssignmentNotificatorFactory; use Espo\Core\Notification\AssignmentNotificator; use Espo\Core\Notification\AssignmentNotificator\Params as AssignmentNotificatorParams; +use Espo\Core\ORM\Repository\Option\SaveContext; use Espo\Core\Utils\Metadata; use Espo\Core\Utils\Config; use Espo\Tools\Stream\Service as StreamService; @@ -98,6 +99,12 @@ class HookProcessor $params = AssignmentNotificatorParams::create()->withRawOptions($options); + $saveContext = $options[SaveContext::NAME] ?? null; + + if ($saveContext instanceof SaveContext) { + $params = $params->withActionId($saveContext->getId()); + } + $notificator->process($entity, $params); } diff --git a/application/Espo/Tools/Notification/HookProcessor/Params.php b/application/Espo/Tools/Notification/HookProcessor/Params.php new file mode 100644 index 0000000000..1dcd5320f4 --- /dev/null +++ b/application/Espo/Tools/Notification/HookProcessor/Params.php @@ -0,0 +1,37 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\Notification\HookProcessor; + +readonly class Params +{ + public function __construct( + public ?string $actionId = null, + ) {} +} diff --git a/application/Espo/Tools/Notification/NoteHookProcessor.php b/application/Espo/Tools/Notification/NoteHookProcessor.php index 5a8e93165a..b827fe5b3e 100644 --- a/application/Espo/Tools/Notification/NoteHookProcessor.php +++ b/application/Espo/Tools/Notification/NoteHookProcessor.php @@ -34,6 +34,7 @@ use Espo\Core\Acl\Table; use Espo\Core\Name\Field; use Espo\ORM\Name\Attribute; +use Espo\Tools\Notification\HookProcessor\Params; use Espo\Tools\Stream\Service as StreamService; use Espo\ORM\EntityManager; @@ -59,10 +60,10 @@ class NoteHookProcessor private InternalAclManager $internalAclManager ) {} - public function afterSave(Note $note): void + public function afterSave(Note $note, Params $params): void { if ($note->getParentType() && $note->getParentId()) { - $this->afterSaveParent($note); + $this->afterSaveParent($note, $params); return; } @@ -70,7 +71,7 @@ class NoteHookProcessor $this->afterSaveNoParent($note); } - private function afterSaveParent(Note $note): void + private function afterSaveParent(Note $note, Params $params): void { $parentType = $note->getParentType(); $parentId = $note->getParentId(); @@ -159,7 +160,7 @@ class NoteHookProcessor $notifyUserIdList[] = $user->getId(); } - $this->processNotify($note, array_unique($notifyUserIdList)); + $this->processNotify($note, array_unique($notifyUserIdList), $params); } private function afterSaveNoParent(Note $note): void @@ -186,15 +187,13 @@ class NoteHookProcessor if ($targetType === Note::TARGET_ALL) { $this->afterSaveTargetAll($note); - - return; } } /** * @param string[] $userIdList */ - private function processNotify(Note $note, array $userIdList): void + private function processNotify(Note $note, array $userIdList, ?Params $params = null): void { $filteredUserIdList = array_filter( $userIdList, @@ -230,7 +229,7 @@ class NoteHookProcessor return; } - $this->service->notifyAboutNote($filteredUserIdList, $note); + $this->service->notifyAboutNote($filteredUserIdList, $note, $params); } private function afterSaveTargetUsers(Note $note): void diff --git a/application/Espo/Tools/Notification/Service.php b/application/Espo/Tools/Notification/Service.php index ccafc34c54..924d34808c 100644 --- a/application/Espo/Tools/Notification/Service.php +++ b/application/Espo/Tools/Notification/Service.php @@ -42,6 +42,7 @@ use Espo\Core\Utils\DateTime as DateTimeUtil; use Espo\Modules\Crm\Entities\CaseObj; use Espo\ORM\EntityManager; use Espo\ORM\Name\Attribute; +use Espo\Tools\Notification\HookProcessor\Params; class Service { @@ -67,8 +68,9 @@ class Service /** * @param string[] $userIdList + * @param ?Params $params Parameters. As of v9.2.0. */ - public function notifyAboutNote(array $userIdList, Note $note): void + public function notifyAboutNote(array $userIdList, Note $note, ?Params $params = null): void { $related = null; @@ -134,7 +136,8 @@ class Service ->setRelatedParent( $note->getParentType() && $note->getParentId() ? LinkParent::create($note->getParentType(), $note->getParentId()) : null - ); + ) + ->setActionId($params?->actionId); $collection[] = $notification; } diff --git a/application/Espo/Tools/Stream/Jobs/AutoFollow.php b/application/Espo/Tools/Stream/Jobs/AutoFollow.php index 21b453bd49..61b6901c0f 100644 --- a/application/Espo/Tools/Stream/Jobs/AutoFollow.php +++ b/application/Espo/Tools/Stream/Jobs/AutoFollow.php @@ -31,17 +31,14 @@ namespace Espo\Tools\Stream\Jobs; use Espo\Core\Job\Job; use Espo\Core\Job\Job\Data; - use Espo\Core\AclManager; use Espo\Core\Acl\Exceptions\NotImplemented as AclNotImplemented; - +use Espo\Core\Utils\Util; use Espo\Entities\Note; -use Espo\ORM\Collection; use Espo\ORM\EntityManager; - +use Espo\Tools\Notification\HookProcessor\Params; use Espo\Tools\Stream\Service as Service; use Espo\Tools\Notification\Service as NotificationService; - use Espo\Entities\User; /** @@ -49,22 +46,12 @@ use Espo\Entities\User; */ class AutoFollow implements Job { - private Service $service; - private NotificationService $notificationService; - private AclManager $aclManager; - private EntityManager $entityManager; - public function __construct( - Service $service, - NotificationService $notificationService, - AclManager $aclManager, - EntityManager $entityManager - ) { - $this->service = $service; - $this->notificationService = $notificationService; - $this->aclManager = $aclManager; - $this->entityManager = $entityManager; - } + private Service $service, + private NotificationService $notificationService, + private AclManager $aclManager, + private EntityManager $entityManager, + ) {} public function run(Data $data): void { @@ -84,8 +71,7 @@ class AutoFollow implements Job } foreach ($userIdList as $i => $userId) { - /** @var User|null $user */ - $user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $userId); + $user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId); if (!$user) { unset($userIdList[$i]); @@ -95,7 +81,7 @@ class AutoFollow implements Job try { $hasAccess = $this->aclManager->checkEntityStream($user, $entity); - } catch (AclNotImplemented $e) { + } catch (AclNotImplemented) { $hasAccess = false; } @@ -120,18 +106,20 @@ class AutoFollow implements Job $this->service->followEntityMass($entity, $userIdList); - /** @var Collection $noteList */ - $noteList = $this->entityManager - ->getRDBRepository(Note::ENTITY_TYPE) + $notes = $this->entityManager + ->getRDBRepositoryByClass(Note::class) ->where([ 'parentType' => $entityType, 'parentId' => $entityId, ]) - ->order('number', 'ASC') + ->order('number') ->find(); - foreach ($noteList as $note) { - $this->notificationService->notifyAboutNote($userIdList, $note); + // Group all notifications. + $params = new Params(Util::generateId()); + + foreach ($notes as $note) { + $this->notificationService->notifyAboutNote($userIdList, $note, $params); } } } diff --git a/application/Espo/Tools/Stream/Service.php b/application/Espo/Tools/Stream/Service.php index 6d061cdfe9..d642418552 100644 --- a/application/Espo/Tools/Stream/Service.php +++ b/application/Espo/Tools/Stream/Service.php @@ -33,6 +33,7 @@ use Espo\Core\Field\DateTime; use Espo\Core\Field\LinkMultiple; use Espo\Core\Field\LinkParent; use Espo\Core\Name\Field; +use Espo\Core\ORM\Repository\Option\SaveContext; use Espo\Core\ORM\Repository\Option\SaveOption; use Espo\Core\ORM\Type\FieldType; use Espo\Entities\StreamSubscription; @@ -691,6 +692,12 @@ class Service $noteOptions[SaveOption::CREATED_BY_ID] = $options[SaveOption::CREATED_BY_ID]; } + $saveContext = $options[SaveContext::NAME] ?? null; + + if ($saveContext instanceof SaveContext) { + $noteOptions[SaveContext::NAME] = new SaveContext($saveContext->getId()); + } + $this->entityManager->saveEntity($note, $noteOptions); $superParent = $note->getSuperParent(); @@ -754,6 +761,12 @@ class Service $noteOptions[SaveOption::CREATED_BY_ID] = $options[SaveOption::CREATED_BY_ID]; } + $saveContext = $options[SaveContext::NAME] ?? null; + + if ($saveContext instanceof SaveContext) { + $noteOptions[SaveContext::NAME] = new SaveContext($saveContext->getId()); + } + $this->entityManager->saveEntity($note, $noteOptions); $parent = $this->entityManager->getEntityById($parentType, $parentId); @@ -782,6 +795,12 @@ class Service $noteOptions[SaveOption::CREATED_BY_ID] = $options[SaveOption::CREATED_BY_ID]; } + $saveContext = $options[SaveContext::NAME] ?? null; + + if ($saveContext instanceof SaveContext) { + $noteOptions[SaveContext::NAME] = new SaveContext($saveContext->getId()); + } + $this->entityManager->saveEntity($note, $noteOptions); if (!$this->checkIsEnabled($parent->getEntityType())) { @@ -810,6 +829,12 @@ class Service $noteOptions[SaveOption::CREATED_BY_ID] = $options[SaveOption::MODIFIED_BY_ID]; } + $saveContext = $options[SaveContext::NAME] ?? null; + + if ($saveContext instanceof SaveContext) { + $noteOptions[SaveContext::NAME] = new SaveContext($saveContext->getId()); + } + $this->entityManager->saveEntity($note, $noteOptions); if (!$this->checkIsEnabled($parent->getEntityType())) { @@ -842,6 +867,12 @@ class Service $noteOptions[SaveOption::CREATED_BY_ID] = $options[SaveOption::MODIFIED_BY_ID]; } + $saveContext = $options[SaveContext::NAME] ?? null; + + if ($saveContext instanceof SaveContext) { + $noteOptions[SaveContext::NAME] = new SaveContext($saveContext->getId()); + } + $this->entityManager->saveEntity($note, $noteOptions); } @@ -879,6 +910,12 @@ class Service $noteOptions[SaveOption::CREATED_BY_ID] = $options[SaveOption::MODIFIED_BY_ID]; } + $saveContext = $options[SaveContext::NAME] ?? null; + + if ($saveContext instanceof SaveContext) { + $noteOptions[SaveContext::NAME] = new SaveContext($saveContext->getId()); + } + $this->entityManager->saveEntity($note, $noteOptions); } @@ -1065,7 +1102,7 @@ class Service $note->setType(Note::TYPE_UPDATE); $note->setParent(LinkParent::createFromEntity($entity)); - $note->set('data', [ + $note->setData([ 'fields' => $updatedFieldList, 'attributes' => [ 'was' => (object) $was, @@ -1073,13 +1110,19 @@ class Service ], ]); - $o = []; + $noteOptions = []; if (!empty($options['modifiedById'])) { - $o['createdById'] = $options['modifiedById']; + $noteOptions['createdById'] = $options['modifiedById']; } - $this->entityManager->saveEntity($note, $o); + $saveContext = $options[SaveContext::NAME] ?? null; + + if ($saveContext instanceof SaveContext) { + $noteOptions[SaveContext::NAME] = new SaveContext($saveContext->getId()); + } + + $this->entityManager->saveEntity($note, $noteOptions); } /**