action ID

This commit is contained in:
Yuri Kuznetsov
2025-06-30 19:03:55 +03:00
parent 0a7fe37014
commit 15de70c8f7
15 changed files with 216 additions and 96 deletions
@@ -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);
}
}
}
@@ -39,5 +39,8 @@ use Espo\Core\Notification\AssignmentNotificator\Params;
*/
interface AssignmentNotificator
{
/**
* @param TEntity $entity
*/
public function process(Entity $entity, Params $params): void;
}
@@ -34,11 +34,11 @@ namespace Espo\Core\Notification\AssignmentNotificator;
*/
class Params
{
/**
* @var array<string, mixed>
*/
/** @var array<string, mixed> */
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;
}
}
@@ -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);
}
}
@@ -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();
}
/**
@@ -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;
}
}
+19 -14
View File
@@ -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<Note>
*/
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<string, mixed> $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);
}
}
@@ -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);
}
@@ -40,6 +40,11 @@
"type": "linkParent",
"readOnly": true
},
"actionId": {
"type": "varchar",
"maxLength": 36,
"readOnly": true
},
"createdBy": {
"type": "link",
"readOnly": true,
@@ -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);
}
@@ -0,0 +1,37 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program 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.
*
* This program 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* 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,
) {}
}
@@ -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
@@ -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;
}
@@ -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<Note> $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);
}
}
}
+47 -4
View File
@@ -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);
}
/**